From 7647685d8c1cc506de8a3a11a3407e6f90ba29e8 Mon Sep 17 00:00:00 2001 From: jannetty Date: Sat, 11 Apr 2026 18:48:19 -0700 Subject: [PATCH 01/20] update build and CI configuration --- .github/config/checks.xml | 1 + build.gradle | 4 ++-- gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/config/checks.xml b/.github/config/checks.xml index cfd73a51d..c54318bb2 100644 --- a/.github/config/checks.xml +++ b/.github/config/checks.xml @@ -44,6 +44,7 @@ + diff --git a/build.gradle b/build.gradle index e34909fab..fdd0c5417 100644 --- a/build.gradle +++ b/build.gradle @@ -62,8 +62,8 @@ jacocoTestReport { }) } reports { - xml.enabled true - html.enabled true + xml.required.set(true) + html.required.set(true) } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a4b442974..48c0a02ca 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 57817c13b42afd4c5dc7ce8edaec6bd3d1553b65 Mon Sep 17 00:00:00 2001 From: jannetty Date: Sat, 11 Apr 2026 19:33:48 -0700 Subject: [PATCH 02/20] add updateGrowthRate framework and make getUniqueIDs public --- .../PottsModuleFlyGMCDifferentiation.java | 85 ++++++++- ...oduleProliferationVolumeBasedDivision.java | 39 +++- src/arcade/potts/sim/Potts.java | 2 +- src/arcade/potts/sim/Potts2D.java | 2 +- src/arcade/potts/sim/Potts3D.java | 2 +- .../PottsModuleFlyGMCDifferentiation.java | 171 ++++++++++++++++++ .../PottsModuleFlyGMCDifferentiationTest.java | 167 ----------------- ...eProliferationVolumeBasedDivisionTest.java | 141 ++++++++++++++- test/arcade/potts/sim/PottsTest.java | 2 +- 9 files changed, 427 insertions(+), 184 deletions(-) create mode 100644 test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java delete mode 100644 test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiationTest.java diff --git a/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java b/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java index 4405e45dc..a7653e002 100644 --- a/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java +++ b/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java @@ -16,10 +16,15 @@ /** * Implementation of {@link PottsModuleProliferationVolumeBasedDivision} for fly GMC agents. These * cells divide into two {@link PottsCellFlyNeuron} cells. The links must be set in the setup file - * so that 100% of the daughter cells are Neurons. + * so that 100% of the daughter cells are Neurons. Implementation of {@link + * PottsModuleProliferationVolumeBasedDivision} for fly GMC agents. These cells divide into two + * {@link PottsCellFlyNeuron} cells. The links must be set in the setup file so that 100% of the + * daughter cells are Neurons. */ public class PottsModuleFlyGMCDifferentiation extends PottsModuleProliferationVolumeBasedDivision { + Boolean pdeLike; + /** * Creates a fly GMC proliferation module. * @@ -27,13 +32,42 @@ public class PottsModuleFlyGMCDifferentiation extends PottsModuleProliferationVo */ public PottsModuleFlyGMCDifferentiation(PottsCellFlyGMC cell) { super(cell); + pdeLike = (cell.getParameters().getInt("proliferation/PDELIKE") != 0); + } + + /** + * Computes the expected equilibrium average GMC volume over one cell cycle. + * + *

In the Potts model, a cell's target volume is initialized to {@code criticalVolume} on + * reset. The Potts energy immediately drives the cell's actual volume toward this target, + * regardless of the current growth rate. As a result, the volume-regulated growth phase + * effectively begins at {@code criticalVolume} (not the birth volume), even when {@code + * VOLUME_BASED_CRITICAL_VOLUME} is off and birth volume is below {@code criticalVolume}. + * + *

The regulated growth phase therefore runs from {@code criticalVolume} to {@code sizeTarget + * * criticalVolume}. Under constant-rate growth, the time-average volume over this phase is the + * arithmetic mean of the two endpoints: + * + *

+     *   V_ref = (criticalVolume + sizeTarget * criticalVolume) / 2
+     *         = criticalVolume * (1 + sizeTarget) / 2
+     * 
+ * + *

This formula is consistent with the PDE-like branch, which uses {@code avgCritVol * (1 + + * sizeTarget) / 2}, and holds whether or not {@code VOLUME_BASED_CRITICAL_VOLUME} is enabled. + * + * @return the expected equilibrium average GMC volume + */ + double computeEquilibriumVolume() { + return cell.getCriticalVolume() * (1.0 + sizeTarget) / 2.0; } /** * Adds a cell to the simulation. * *

The cell location is split. The new neuron cell is created, initialized, and added to the - * schedule. This cell's location is also assigned to a new Neuron cell. + * schedule. This cell's location is also assigned to a new Neuron cell. The critical volume of + * both neurons is set to the initial volume of each neuron's location. * * @param random the random number generator * @param sim the simulation instance @@ -55,7 +89,7 @@ void addCell(MersenneTwisterFast random, Simulation sim) { (PottsCell) newContainer.convert(sim.getCellFactory(), newLocation, random); sim.getGrid().addObject(newCell, null); potts.register(newCell); - newCell.reset(potts.ids, potts.regions); + newCell.initialize(potts.ids, potts.regions); newCell.schedule(sim.getSchedule()); // remove old GMC cell from simulation @@ -88,7 +122,50 @@ void addCell(MersenneTwisterFast random, Simulation sim) { sim.getGrid().addObject(differentiatedGMC, null); potts.register(differentiatedGMC); - differentiatedGMC.reset(potts.ids, potts.regions); + differentiatedGMC.initialize(potts.ids, potts.regions); differentiatedGMC.schedule(sim.getSchedule()); } + + public void updateGrowthRate(Simulation sim) { + if (!dynamicGrowthRateVolume) { + cellGrowthRate = cellGrowthRateBase; + } else { + if (!pdeLike) { + updateCellVolumeBasedGrowthRate( + cell.getLocation().getVolume(), computeEquilibriumVolume()); + } else { + // PDE-like: use population-wide averages for GMCs (same pop as this cell). + // The reference volume is the population-average equilibrium volume: + // avgVRef = avgCritVol * (1 + sizeTarget) / 2 + // This assumes VOLUME_BASED_CRITICAL_VOLUME_MULTIPLIER = 1 (so each GMC's + // birth volume equals its critVol). For multiplier != 1, avgCritVol would need + // to be replaced by the mean of (critVol_i / multiplier + sizeTarget*critVol_i)/2. + sim.util.Bag objs = sim.getGrid().getAllObjects(); + + double volSum = 0.0; + double critSum = 0.0; + int count = 0; + + for (int i = 0; i < objs.numObjs; i++) { + Object o = objs.objs[i]; + if (!(o instanceof arcade.potts.agent.cell.PottsCell)) continue; + + arcade.potts.agent.cell.PottsCell c = (arcade.potts.agent.cell.PottsCell) o; + if (c.getPop() != cell.getPop()) continue; // keep to same population + + if (o instanceof arcade.potts.agent.cell.PottsCellFlyGMC) { + arcade.potts.agent.cell.PottsCellFlyGMC gmc = + (arcade.potts.agent.cell.PottsCellFlyGMC) o; + volSum += gmc.getLocation().getVolume(); + critSum += gmc.getCriticalVolume(); + count++; + } + } + double avgVolume = volSum / count; + double avgCritVol = critSum / count; + double avgVRef = avgCritVol * (1.0 + sizeTarget) / 2.0; + updateCellVolumeBasedGrowthRate(avgVolume, avgVRef); + } + } + } } diff --git a/src/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivision.java b/src/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivision.java index e154be0d4..2e1de6699 100644 --- a/src/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivision.java +++ b/src/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivision.java @@ -7,13 +7,16 @@ import arcade.potts.util.PottsEnums.Phase; /** - * Implementation of {@link PottsModule} for fly GMC agents. The links must be set in the setup file - * so that 100% of the daughter cells are Neurons. + * Implementation of {@link PottsModule} for agents that divide upon reaching a volume threshold + * without any cell-cycle duration requirements. */ public abstract class PottsModuleProliferationVolumeBasedDivision extends PottsModuleProliferation { - /** Overall growth rate for cell (voxels/tick). */ - final double cellGrowthRate; + /** Base growth rate for cells (voxels/tick). */ + final double cellGrowthRateBase; + + /** Current growth rate for stem cells (voxels/tick). */ + double cellGrowthRate; /** * Target ratio of critical volume for division size checkpoint (cell must reach CRITICAL_VOLUME @@ -21,6 +24,14 @@ public abstract class PottsModuleProliferationVolumeBasedDivision extends PottsM */ final double sizeTarget; + /** Boolean flag indicating whether the growth rate should follow volume-sensitive ruleset. */ + final boolean dynamicGrowthRateVolume; + + /** + * Sensitivity of growth rate to cell volume, only relevant if dynamicGrowthRateVolume is true. + */ + final double growthRateVolumeSensitivity; + /** * Creates a proliferation module in which division is solely dependent on cell volume. * @@ -30,16 +41,34 @@ public PottsModuleProliferationVolumeBasedDivision(PottsCell cell) { super(cell); Parameters parameters = cell.getParameters(); sizeTarget = parameters.getDouble("proliferation/SIZE_TARGET"); - cellGrowthRate = parameters.getDouble("proliferation/CELL_GROWTH_RATE"); + cellGrowthRateBase = parameters.getDouble("proliferation/CELL_GROWTH_RATE"); + dynamicGrowthRateVolume = + (parameters.getInt("proliferation/DYNAMIC_GROWTH_RATE_VOLUME") != 0); + growthRateVolumeSensitivity = + parameters.getDouble("proliferation/GROWTH_RATE_VOLUME_SENSITIVITY"); setPhase(Phase.UNDEFINED); + cellGrowthRate = cellGrowthRateBase; } @Override public void step(MersenneTwisterFast random, Simulation sim) { + updateGrowthRate(sim); cell.updateTarget(cellGrowthRate, sizeTarget); boolean sizeCheck = cell.getVolume() >= sizeTarget * cell.getCriticalVolume(); if (sizeCheck) { addCell(random, sim); } } + + /** + * Updates the effective growth rate according to boolean flags specified in parameters. + * + * @param sim the simulation + */ + public abstract void updateGrowthRate(Simulation sim); + + public void updateCellVolumeBasedGrowthRate(double volume, double cellCriticalVolume) { + double Ka = cellCriticalVolume; + cellGrowthRate = cellGrowthRateBase * Math.pow((volume / Ka), growthRateVolumeSensitivity); + } } diff --git a/src/arcade/potts/sim/Potts.java b/src/arcade/potts/sim/Potts.java index a0b46f0db..a4ffb0ee2 100644 --- a/src/arcade/potts/sim/Potts.java +++ b/src/arcade/potts/sim/Potts.java @@ -398,7 +398,7 @@ public PottsCell getCell(int id) { * @param z the z coordinate * @return the list of unique IDs */ - abstract HashSet getUniqueIDs(int x, int y, int z); + public abstract HashSet getUniqueIDs(int x, int y, int z); /** * Gets unique regions adjacent to given voxel. diff --git a/src/arcade/potts/sim/Potts2D.java b/src/arcade/potts/sim/Potts2D.java index 011d6f850..f6d7dbb6f 100644 --- a/src/arcade/potts/sim/Potts2D.java +++ b/src/arcade/potts/sim/Potts2D.java @@ -151,7 +151,7 @@ private boolean getConnectivityThreeNeighbors(boolean[][] subarray) { } @Override - HashSet getUniqueIDs(int x, int y, int z) { + public HashSet getUniqueIDs(int x, int y, int z) { int id = ids[z][x][y]; HashSet unique = new HashSet<>(); diff --git a/src/arcade/potts/sim/Potts3D.java b/src/arcade/potts/sim/Potts3D.java index 7c4686e8d..7f3429650 100644 --- a/src/arcade/potts/sim/Potts3D.java +++ b/src/arcade/potts/sim/Potts3D.java @@ -465,7 +465,7 @@ private boolean getConnectivityFiveNeighbors(boolean[][][] array) { } @Override - HashSet getUniqueIDs(int x, int y, int z) { + public HashSet getUniqueIDs(int x, int y, int z) { int id = ids[z][x][y]; HashSet unique = new HashSet<>(); diff --git a/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java b/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java new file mode 100644 index 000000000..a7653e002 --- /dev/null +++ b/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java @@ -0,0 +1,171 @@ +package arcade.potts.agent.module; + +import ec.util.MersenneTwisterFast; +import arcade.core.agent.cell.CellContainer; +import arcade.core.env.location.Location; +import arcade.core.sim.Simulation; +import arcade.potts.agent.cell.PottsCell; +import arcade.potts.agent.cell.PottsCellContainer; +import arcade.potts.agent.cell.PottsCellFlyGMC; +import arcade.potts.agent.cell.PottsCellFlyNeuron; +import arcade.potts.env.location.PottsLocation2D; +import arcade.potts.sim.Potts; +import arcade.potts.sim.PottsSimulation; +import arcade.potts.util.PottsEnums.State; + +/** + * Implementation of {@link PottsModuleProliferationVolumeBasedDivision} for fly GMC agents. These + * cells divide into two {@link PottsCellFlyNeuron} cells. The links must be set in the setup file + * so that 100% of the daughter cells are Neurons. Implementation of {@link + * PottsModuleProliferationVolumeBasedDivision} for fly GMC agents. These cells divide into two + * {@link PottsCellFlyNeuron} cells. The links must be set in the setup file so that 100% of the + * daughter cells are Neurons. + */ +public class PottsModuleFlyGMCDifferentiation extends PottsModuleProliferationVolumeBasedDivision { + + Boolean pdeLike; + + /** + * Creates a fly GMC proliferation module. + * + * @param cell the cell to which this module is attached + */ + public PottsModuleFlyGMCDifferentiation(PottsCellFlyGMC cell) { + super(cell); + pdeLike = (cell.getParameters().getInt("proliferation/PDELIKE") != 0); + } + + /** + * Computes the expected equilibrium average GMC volume over one cell cycle. + * + *

In the Potts model, a cell's target volume is initialized to {@code criticalVolume} on + * reset. The Potts energy immediately drives the cell's actual volume toward this target, + * regardless of the current growth rate. As a result, the volume-regulated growth phase + * effectively begins at {@code criticalVolume} (not the birth volume), even when {@code + * VOLUME_BASED_CRITICAL_VOLUME} is off and birth volume is below {@code criticalVolume}. + * + *

The regulated growth phase therefore runs from {@code criticalVolume} to {@code sizeTarget + * * criticalVolume}. Under constant-rate growth, the time-average volume over this phase is the + * arithmetic mean of the two endpoints: + * + *

+     *   V_ref = (criticalVolume + sizeTarget * criticalVolume) / 2
+     *         = criticalVolume * (1 + sizeTarget) / 2
+     * 
+ * + *

This formula is consistent with the PDE-like branch, which uses {@code avgCritVol * (1 + + * sizeTarget) / 2}, and holds whether or not {@code VOLUME_BASED_CRITICAL_VOLUME} is enabled. + * + * @return the expected equilibrium average GMC volume + */ + double computeEquilibriumVolume() { + return cell.getCriticalVolume() * (1.0 + sizeTarget) / 2.0; + } + + /** + * Adds a cell to the simulation. + * + *

The cell location is split. The new neuron cell is created, initialized, and added to the + * schedule. This cell's location is also assigned to a new Neuron cell. The critical volume of + * both neurons is set to the initial volume of each neuron's location. + * + * @param random the random number generator + * @param sim the simulation instance + */ + @Override + void addCell(MersenneTwisterFast random, Simulation sim) { + Potts potts = ((PottsSimulation) sim).getPotts(); + + // Split current location + Location newLocation = ((PottsLocation2D) cell.getLocation()).split(random); + + // Reset current cell + cell.reset(potts.ids, potts.regions); + + // Create and schedule new neuron cell + int newID = sim.getID(); + CellContainer newContainer = cell.make(newID, State.QUIESCENT, random); + PottsCell newCell = + (PottsCell) newContainer.convert(sim.getCellFactory(), newLocation, random); + sim.getGrid().addObject(newCell, null); + potts.register(newCell); + newCell.initialize(potts.ids, potts.regions); + newCell.schedule(sim.getSchedule()); + + // remove old GMC cell from simulation + PottsCellFlyGMC oldCell = (PottsCellFlyGMC) cell; + Location location = oldCell.getLocation(); + sim.getGrid().removeObject(oldCell, location); + oldCell.stop(); + + // create new neuron cell and add to simulation. + int newPop = oldCell.getLinks().next(random); + + PottsCellContainer differentiatedGMCContainer = + new PottsCellContainer( + oldCell.getID(), + oldCell.getParent(), + newPop, + oldCell.getAge(), + oldCell.getDivisions(), + State.QUIESCENT, + null, + 0, + null, + oldCell.getCriticalVolume(), + oldCell.getCriticalHeight(), + oldCell.getCriticalRegionVolumes(), + oldCell.getCriticalRegionHeights()); + PottsCellFlyNeuron differentiatedGMC = + (PottsCellFlyNeuron) + differentiatedGMCContainer.convert(sim.getCellFactory(), location, random); + + sim.getGrid().addObject(differentiatedGMC, null); + potts.register(differentiatedGMC); + differentiatedGMC.initialize(potts.ids, potts.regions); + differentiatedGMC.schedule(sim.getSchedule()); + } + + public void updateGrowthRate(Simulation sim) { + if (!dynamicGrowthRateVolume) { + cellGrowthRate = cellGrowthRateBase; + } else { + if (!pdeLike) { + updateCellVolumeBasedGrowthRate( + cell.getLocation().getVolume(), computeEquilibriumVolume()); + } else { + // PDE-like: use population-wide averages for GMCs (same pop as this cell). + // The reference volume is the population-average equilibrium volume: + // avgVRef = avgCritVol * (1 + sizeTarget) / 2 + // This assumes VOLUME_BASED_CRITICAL_VOLUME_MULTIPLIER = 1 (so each GMC's + // birth volume equals its critVol). For multiplier != 1, avgCritVol would need + // to be replaced by the mean of (critVol_i / multiplier + sizeTarget*critVol_i)/2. + sim.util.Bag objs = sim.getGrid().getAllObjects(); + + double volSum = 0.0; + double critSum = 0.0; + int count = 0; + + for (int i = 0; i < objs.numObjs; i++) { + Object o = objs.objs[i]; + if (!(o instanceof arcade.potts.agent.cell.PottsCell)) continue; + + arcade.potts.agent.cell.PottsCell c = (arcade.potts.agent.cell.PottsCell) o; + if (c.getPop() != cell.getPop()) continue; // keep to same population + + if (o instanceof arcade.potts.agent.cell.PottsCellFlyGMC) { + arcade.potts.agent.cell.PottsCellFlyGMC gmc = + (arcade.potts.agent.cell.PottsCellFlyGMC) o; + volSum += gmc.getLocation().getVolume(); + critSum += gmc.getCriticalVolume(); + count++; + } + } + double avgVolume = volSum / count; + double avgCritVol = critSum / count; + double avgVRef = avgCritVol * (1.0 + sizeTarget) / 2.0; + updateCellVolumeBasedGrowthRate(avgVolume, avgVRef); + } + } + } +} diff --git a/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiationTest.java b/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiationTest.java deleted file mode 100644 index c1b04e272..000000000 --- a/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiationTest.java +++ /dev/null @@ -1,167 +0,0 @@ -package arcade.potts.agent.module; - -import java.util.EnumMap; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.MockedConstruction; -import sim.engine.Schedule; -import ec.util.MersenneTwisterFast; -import arcade.core.env.grid.Grid; -import arcade.core.sim.Simulation; -import arcade.core.util.GrabBag; -import arcade.core.util.Parameters; -import arcade.potts.agent.cell.PottsCell; -import arcade.potts.agent.cell.PottsCellContainer; -import arcade.potts.agent.cell.PottsCellFactory; -import arcade.potts.agent.cell.PottsCellFlyGMC; -import arcade.potts.agent.cell.PottsCellFlyNeuron; -import arcade.potts.env.location.PottsLocation; -import arcade.potts.env.location.PottsLocation2D; -import arcade.potts.sim.Potts; -import arcade.potts.sim.PottsSimulation; -import arcade.potts.util.PottsEnums.Region; -import arcade.potts.util.PottsEnums.State; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.mockConstruction; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -public class PottsModuleFlyGMCDifferentiationTest { - private int[][][] dummyIDs; - - private int[][][] dummyRegions; - - private Simulation sim; - - private Potts potts; - - private Grid grid; - - private PottsCellFactory cellFactory; - - private Schedule schedule; - - private PottsCellFlyGMC gmcCell; - - private PottsLocation2D location; - - private PottsLocation newLocation; - - private PottsCellContainer container; - - private PottsCell newCell; - - private GrabBag links; - - private Parameters parameters; - - private MersenneTwisterFast random; - - private MockedConstruction mockedConstruction; - - @BeforeEach - public final void setupMocks() { - dummyIDs = new int[1][1][1]; - dummyRegions = new int[0][0][0]; - - sim = mock(PottsSimulation.class); - potts = mock(Potts.class); - when(((PottsSimulation) sim).getPotts()).thenReturn(potts); - potts.ids = dummyIDs; - potts.regions = dummyRegions; - - grid = mock(Grid.class); - when(sim.getGrid()).thenReturn(grid); - cellFactory = mock(PottsCellFactory.class); - when(sim.getCellFactory()).thenReturn(cellFactory); - schedule = mock(Schedule.class); - when(sim.getSchedule()).thenReturn(schedule); - when(sim.getID()).thenReturn(123); - - gmcCell = mock(PottsCellFlyGMC.class); - location = mock(PottsLocation2D.class); - when(gmcCell.getLocation()).thenReturn(location); - newLocation = mock(PottsLocation.class); - when(location.split(any(MersenneTwisterFast.class))).thenReturn(newLocation); - - links = mock(GrabBag.class); - when(gmcCell.getLinks()).thenReturn(links); - int newPop = 2; - when(links.next(any(MersenneTwisterFast.class))).thenReturn(newPop); - - // Stub getters on the GMC cell for differentiated cell creation - when(gmcCell.getID()).thenReturn(100); - int parent = 0; - when(gmcCell.getParent()).thenReturn(parent); - when(gmcCell.getAge()).thenReturn(5); - when(gmcCell.getDivisions()).thenReturn(2); - when(gmcCell.getCriticalVolume()).thenReturn(1.0); - when(gmcCell.getCriticalHeight()).thenReturn(2.0); - EnumMap critRegionVolumes = new EnumMap<>(Region.class); - EnumMap critRegionHeights = new EnumMap<>(Region.class); - when(gmcCell.getCriticalRegionVolumes()).thenReturn(critRegionVolumes); - when(gmcCell.getCriticalRegionHeights()).thenReturn(critRegionHeights); - - parameters = mock(Parameters.class); - when(gmcCell.getParameters()).thenReturn(parameters); - - random = mock(MersenneTwisterFast.class); - - // Intercept construction of the differentiated cell container - mockedConstruction = - mockConstruction( - PottsCellContainer.class, - (mockContainer, context) -> { - // When convert() is called on this new container, return a - // differentiated cell - PottsCellFlyNeuron diffCell = mock(PottsCellFlyNeuron.class); - when(mockContainer.convert( - eq(cellFactory), - eq(location), - any(MersenneTwisterFast.class))) - .thenReturn(diffCell); - }); - } - - @AfterEach - final void tearDown() { - mockedConstruction.close(); - } - - @Test - public void addCell_called_callsExpectedMethods() { - // When the module calls make() on the cell, return Quiescent PottsCellContainer mock - container = mock(PottsCellContainer.class); - when(gmcCell.make(eq(123), eq(State.QUIESCENT), any(MersenneTwisterFast.class))) - .thenReturn(container); - newCell = mock(PottsCell.class); - when(container.convert(eq(cellFactory), eq(newLocation), any(MersenneTwisterFast.class))) - .thenReturn(newCell); - - PottsModuleProliferationVolumeBasedDivision module = - new PottsModuleFlyGMCDifferentiation(gmcCell); - module.addCell(random, sim); - verify(location).split(random); - verify(gmcCell).reset(dummyIDs, dummyRegions); - verify(gmcCell).make(123, State.QUIESCENT, random); - - verify(grid).addObject(newCell, null); - verify(potts).register(newCell); - verify(newCell).reset(dummyIDs, dummyRegions); - verify(newCell).schedule(schedule); - - verify(grid).removeObject(gmcCell, location); - verify(gmcCell).stop(); - - PottsCellContainer constructed = mockedConstruction.constructed().get(0); - PottsCellFlyNeuron diffCell = - (PottsCellFlyNeuron) constructed.convert(cellFactory, location, random); - verify(grid).addObject(diffCell, null); - verify(potts).register(diffCell); - verify(diffCell).reset(dummyIDs, dummyRegions); - verify(diffCell).schedule(schedule); - } -} diff --git a/test/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivisionTest.java b/test/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivisionTest.java index 4856f81e0..254cc1371 100644 --- a/test/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivisionTest.java +++ b/test/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivisionTest.java @@ -5,6 +5,9 @@ import arcade.core.sim.Simulation; import arcade.core.util.Parameters; import arcade.potts.agent.cell.PottsCellFlyGMC; +import arcade.potts.env.location.PottsLocation2D; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.mockito.Mockito.*; public class PottsModuleProliferationVolumeBasedDivisionTest { @@ -12,6 +15,7 @@ public class PottsModuleProliferationVolumeBasedDivisionTest { static class PottsModuleProliferationVolumeBasedDivisionMock extends PottsModuleProliferationVolumeBasedDivision { boolean addCellCalled = false; + boolean growthRateUpdated = false; PottsModuleProliferationVolumeBasedDivisionMock(PottsCellFlyGMC cell) { super(cell); @@ -21,15 +25,43 @@ static class PottsModuleProliferationVolumeBasedDivisionMock void addCell(MersenneTwisterFast random, Simulation sim) { addCellCalled = true; } + + @Override + public void updateGrowthRate(Simulation sim) { + growthRateUpdated = true; + cellGrowthRate = 7.5; + } } @Test - public void step_belowCheckpoint_updatesTargetOnly() { + public void step_called_usesGrowthRateSetByUpdateGrowthRate() { PottsCellFlyGMC cell = mock(PottsCellFlyGMC.class); Parameters params = mock(Parameters.class); + Simulation sim = mock(Simulation.class); + when(cell.getParameters()).thenReturn(params); when(params.getDouble("proliferation/SIZE_TARGET")).thenReturn(1.2); when(params.getDouble("proliferation/CELL_GROWTH_RATE")).thenReturn(4.0); + when(params.getInt("proliferation/DYNAMIC_GROWTH_RATE_VOLUME")).thenReturn(0); + when(params.getDouble("proliferation/GROWTH_RATE_VOLUME_SENSITIVITY")).thenReturn(2.0); + + when(cell.getVolume()).thenReturn(50.0); + when(cell.getCriticalVolume()).thenReturn(100.0); + + PottsModuleProliferationVolumeBasedDivisionMock module = + new PottsModuleProliferationVolumeBasedDivisionMock(cell); + module.step(new MersenneTwisterFast(), sim); + + verify(cell).updateTarget(7.5, 1.2); + assertFalse(module.addCellCalled); + } + + @Test + public void step_belowCheckpoint_updates() { + PottsCellFlyGMC cell = mock(PottsCellFlyGMC.class); + Parameters params = mock(Parameters.class); + when(cell.getParameters()).thenReturn(params); + when(params.getDouble("proliferation/SIZE_TARGET")).thenReturn(1.2); when(cell.getCriticalVolume()).thenReturn(100.0); when(cell.getVolume()).thenReturn(50.0); // below checkpoint @@ -38,7 +70,8 @@ public void step_belowCheckpoint_updatesTargetOnly() { module.step(mock(MersenneTwisterFast.class), mock(Simulation.class)); - verify(cell).updateTarget(4.0, 1.2); + verify(cell).updateTarget(7.5, 1.2); + assert module.growthRateUpdated : "growth rate should be updated on every step"; assert !module.addCellCalled : "addCell should not be called below checkpoint"; } @@ -49,7 +82,6 @@ public void step_atOrAboveCheckpoint_triggersAddCell() { Parameters params = mock(Parameters.class); when(cell.getParameters()).thenReturn(params); when(params.getDouble("proliferation/SIZE_TARGET")).thenReturn(1.2); - when(params.getDouble("proliferation/CELL_GROWTH_RATE")).thenReturn(4.0); when(cell.getCriticalVolume()).thenReturn(100.0); when(cell.getVolume()).thenReturn(120.0); // at or above checkpoint @@ -58,7 +90,108 @@ public void step_atOrAboveCheckpoint_triggersAddCell() { module.step(mock(MersenneTwisterFast.class), mock(Simulation.class)); - verify(cell).updateTarget(4.0, 1.2); + verify(cell).updateTarget(7.5, 1.2); + assert module.growthRateUpdated : "growth rate should be updated on every step"; assert module.addCellCalled : "addCell should be called at or above checkpoint"; } + + @Test + public void updateVolumeBasedGrowthRate_ratioOne_keepsBaseRate() { + // baseGrowth = 4.0, volume = Ka => growth = 4.0 + PottsCellFlyGMC cell = mock(PottsCellFlyGMC.class); + Parameters params = mock(Parameters.class); + PottsLocation2D loc = mock(PottsLocation2D.class); + + when(cell.getParameters()).thenReturn(params); + when(params.getDouble("proliferation/SIZE_TARGET")).thenReturn(1.2); + when(params.getDouble("proliferation/CELL_GROWTH_RATE")).thenReturn(4.0); + when(params.getInt("proliferation/DYNAMIC_GROWTH_RATE_VOLUME")).thenReturn(1); + when(params.getDouble("proliferation/GROWTH_RATE_VOLUME_SENSITIVITY")).thenReturn(2.0); + + when(cell.getLocation()).thenReturn(loc); + when(loc.getVolume()).thenReturn(100.0); + when(cell.getCriticalVolume()).thenReturn(100.0); + + PottsModuleProliferationVolumeBasedDivisionTest + .PottsModuleProliferationVolumeBasedDivisionMock + module = new PottsModuleProliferationVolumeBasedDivisionMock(cell); + + module.updateCellVolumeBasedGrowthRate(loc.getVolume(), cell.getCriticalVolume()); + assertEquals(4.0, module.cellGrowthRate, 1e-9); + } + + @Test + public void updateVolumeBasedGrowthRate_ratioGreaterThanOne_scalesUpByPowerLaw() { + // baseGrowth = 2.0, ratio = 2.0, sensitivity = 3 => 2 * 2^3 = 2 * 8 = 12 + PottsCellFlyGMC cell = mock(PottsCellFlyGMC.class); + Parameters params = mock(Parameters.class); + PottsLocation2D loc = mock(PottsLocation2D.class); + + when(cell.getParameters()).thenReturn(params); + when(params.getDouble("proliferation/SIZE_TARGET")).thenReturn(1.2); + when(params.getDouble("proliferation/CELL_GROWTH_RATE")).thenReturn(2.0); + when(params.getInt("proliferation/DYNAMIC_GROWTH_RATE_VOLUME")).thenReturn(1); + when(params.getDouble("proliferation/GROWTH_RATE_VOLUME_SENSITIVITY")).thenReturn(3.0); + + when(cell.getLocation()).thenReturn(loc); + when(loc.getVolume()).thenReturn(200.0); + when(cell.getCriticalVolume()).thenReturn(100.0); + + PottsModuleProliferationVolumeBasedDivisionTest + .PottsModuleProliferationVolumeBasedDivisionMock + module = new PottsModuleProliferationVolumeBasedDivisionMock(cell); + + module.updateCellVolumeBasedGrowthRate(loc.getVolume(), cell.getCriticalVolume()); + assertEquals(16.0, module.cellGrowthRate, 1e-9); + } + + @Test + public void updateVolumeBasedGrowthRate_ratioLessThanOne_scalesDownByPowerLaw() { + // baseGrowth = 4.0, ratio = 0.5, sensitivity = 2.0 => 4 * 0.5^2 = 1.0 + PottsCellFlyGMC cell = mock(PottsCellFlyGMC.class); + Parameters params = mock(Parameters.class); + PottsLocation2D loc = mock(PottsLocation2D.class); + + when(cell.getParameters()).thenReturn(params); + when(params.getDouble("proliferation/SIZE_TARGET")).thenReturn(1.2); + when(params.getDouble("proliferation/CELL_GROWTH_RATE")).thenReturn(4.0); + when(params.getInt("proliferation/DYNAMIC_GROWTH_RATE_VOLUME")).thenReturn(1); + when(params.getDouble("proliferation/GROWTH_RATE_VOLUME_SENSITIVITY")).thenReturn(2.0); + + when(cell.getLocation()).thenReturn(loc); + when(loc.getVolume()).thenReturn(50.0); + when(cell.getCriticalVolume()).thenReturn(100.0); + + PottsModuleProliferationVolumeBasedDivisionTest + .PottsModuleProliferationVolumeBasedDivisionMock + module = new PottsModuleProliferationVolumeBasedDivisionMock(cell); + + module.updateCellVolumeBasedGrowthRate(loc.getVolume(), cell.getCriticalVolume()); + assertEquals(1.0, module.cellGrowthRate, 1e-9); + } + + @Test + public void updateVolumeBasedGrowthRate_zeroSensitivity_returnsBaseRateRegardlessOfVolume() { + // sensitivity = 0 => growth = baseGrowth * ratio^0 = baseGrowth + PottsCellFlyGMC cell = mock(PottsCellFlyGMC.class); + Parameters params = mock(Parameters.class); + PottsLocation2D loc = mock(PottsLocation2D.class); + + when(cell.getParameters()).thenReturn(params); + when(params.getDouble("proliferation/SIZE_TARGET")).thenReturn(1.2); + when(params.getDouble("proliferation/CELL_GROWTH_RATE")).thenReturn(3.5); + when(params.getInt("proliferation/DYNAMIC_GROWTH_RATE_VOLUME")).thenReturn(1); + when(params.getDouble("proliferation/GROWTH_RATE_VOLUME_SENSITIVITY")).thenReturn(0.0); + + when(cell.getLocation()).thenReturn(loc); + when(loc.getVolume()).thenReturn(250.0); + when(cell.getCriticalVolume()).thenReturn(100.0); + + PottsModuleProliferationVolumeBasedDivisionTest + .PottsModuleProliferationVolumeBasedDivisionMock + module = new PottsModuleProliferationVolumeBasedDivisionMock(cell); + + module.updateCellVolumeBasedGrowthRate(loc.getVolume(), cell.getCriticalVolume()); + assertEquals(3.5, module.cellGrowthRate, 1e-9); + } } diff --git a/test/arcade/potts/sim/PottsTest.java b/test/arcade/potts/sim/PottsTest.java index 04c61d693..305658832 100644 --- a/test/arcade/potts/sim/PottsTest.java +++ b/test/arcade/potts/sim/PottsTest.java @@ -61,7 +61,7 @@ boolean getConnectivity(boolean[][][] array, boolean zero) { } @Override - HashSet getUniqueIDs(int x, int y, int z) { + public HashSet getUniqueIDs(int x, int y, int z) { HashSet set = new HashSet<>(); if (x == 0 && y == 0) { set.add(1); From 6392d9ed5df21ea996898d1f73064209a20c0303 Mon Sep 17 00:00:00 2001 From: jannetty Date: Sat, 11 Apr 2026 22:06:13 -0700 Subject: [PATCH 03/20] adding dostrings --- ...oduleProliferationVolumeBasedDivision.java | 17 ++++++++++ .../PottsModuleFlyGMCDifferentiation.java | 32 ++++++++++++------- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivision.java b/src/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivision.java index 2e1de6699..b6e0a88f1 100644 --- a/src/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivision.java +++ b/src/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivision.java @@ -67,6 +67,23 @@ public void step(MersenneTwisterFast random, Simulation sim) { */ public abstract void updateGrowthRate(Simulation sim); + /** + * Updates {@code cellGrowthRate} from a power-law relationship between current volume and a + * reference volume. + * + *

The updated rate is + * + *

+     * cellGrowthRate = cellGrowthRateBase * (volume / referenceVolume)^growthRateVolumeSensitivity
+     * 
+ * + *

The reference volume is the scale at which the basal growth rate is recovered. In the + * simplest case this can be the cell's critical volume, but callers may also supply another + * biologically motivated reference such as an equilibrium or population-averaged volume. + * + * @param volume the current volume used in the growth-rate scaling + * @param referenceVolume the reference volume that defines the baseline growth-rate scale + */ public void updateCellVolumeBasedGrowthRate(double volume, double cellCriticalVolume) { double Ka = cellCriticalVolume; cellGrowthRate = cellGrowthRateBase * Math.pow((volume / Ka), growthRateVolumeSensitivity); diff --git a/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java b/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java index a7653e002..f62db78a0 100644 --- a/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java +++ b/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java @@ -16,13 +16,14 @@ /** * Implementation of {@link PottsModuleProliferationVolumeBasedDivision} for fly GMC agents. These * cells divide into two {@link PottsCellFlyNeuron} cells. The links must be set in the setup file - * so that 100% of the daughter cells are Neurons. Implementation of {@link - * PottsModuleProliferationVolumeBasedDivision} for fly GMC agents. These cells divide into two - * {@link PottsCellFlyNeuron} cells. The links must be set in the setup file so that 100% of the - * daughter cells are Neurons. + * so that 100% of the daughter cells are Neurons. */ public class PottsModuleFlyGMCDifferentiation extends PottsModuleProliferationVolumeBasedDivision { + /** + * Indicates whether GMC growth rate is based on individual cell conditions or average cell + * conditions + */ Boolean pdeLike; /** @@ -39,10 +40,10 @@ public PottsModuleFlyGMCDifferentiation(PottsCellFlyGMC cell) { * Computes the expected equilibrium average GMC volume over one cell cycle. * *

In the Potts model, a cell's target volume is initialized to {@code criticalVolume} on - * reset. The Potts energy immediately drives the cell's actual volume toward this target, - * regardless of the current growth rate. As a result, the volume-regulated growth phase - * effectively begins at {@code criticalVolume} (not the birth volume), even when {@code - * VOLUME_BASED_CRITICAL_VOLUME} is off and birth volume is below {@code criticalVolume}. + * reset. The Potts energy drives the cell's actual volume toward this target, regardless of + * current growth rate. As a result, the volume-regulated growth phase effectively begins at + * {@code criticalVolume} (not the birth volume), even when {@code VOLUME_BASED_CRITICAL_VOLUME} + * is off and birth volume is below {@code criticalVolume}. * *

The regulated growth phase therefore runs from {@code criticalVolume} to {@code sizeTarget * * criticalVolume}. Under constant-rate growth, the time-average volume over this phase is the @@ -53,9 +54,6 @@ public PottsModuleFlyGMCDifferentiation(PottsCellFlyGMC cell) { * = criticalVolume * (1 + sizeTarget) / 2 * * - *

This formula is consistent with the PDE-like branch, which uses {@code avgCritVol * (1 + - * sizeTarget) / 2}, and holds whether or not {@code VOLUME_BASED_CRITICAL_VOLUME} is enabled. - * * @return the expected equilibrium average GMC volume */ double computeEquilibriumVolume() { @@ -126,7 +124,17 @@ void addCell(MersenneTwisterFast random, Simulation sim) { differentiatedGMC.schedule(sim.getSchedule()); } - public void updateGrowthRate(Simulation sim) { + /** + * Updates the GMC growth rate for the current simulation step. + * + *

If dynamic growth is disabled, the basal growth rate is used. Otherwise, the update is + * volume-based: either from this cell's own volume relative to its equilibrium reference + * volume, or in {@code pdeLike} mode from the population-average GMC volume and average + * equilibrium reference volume for cells in the same population. + * + * @param sim the simulation instance used to access the cell population + */ + public final void updateGrowthRate(Simulation sim) { if (!dynamicGrowthRateVolume) { cellGrowthRate = cellGrowthRateBase; } else { From 488ab02cd2c2be2fd3c61d55832e292a95ace311 Mon Sep 17 00:00:00 2001 From: jannetty Date: Sat, 11 Apr 2026 22:09:21 -0700 Subject: [PATCH 04/20] another docstring --- .../module/PottsModuleFlyGMCDifferentiation.java | 1 + .../module/PottsModuleFlyGMCDifferentiation.java | 15 ++++++--------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java b/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java index a7653e002..818209568 100644 --- a/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java +++ b/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java @@ -23,6 +23,7 @@ */ public class PottsModuleFlyGMCDifferentiation extends PottsModuleProliferationVolumeBasedDivision { + /** Indicates whether GMC growth rate is based on individual cell conditions or average cell conditions */ Boolean pdeLike; /** diff --git a/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java b/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java index f62db78a0..02bc73f48 100644 --- a/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java +++ b/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java @@ -20,10 +20,7 @@ */ public class PottsModuleFlyGMCDifferentiation extends PottsModuleProliferationVolumeBasedDivision { - /** - * Indicates whether GMC growth rate is based on individual cell conditions or average cell - * conditions - */ + /** Indicates whether GMC growth rate is based on individual cell conditions or average cell conditions */ Boolean pdeLike; /** @@ -40,10 +37,10 @@ public PottsModuleFlyGMCDifferentiation(PottsCellFlyGMC cell) { * Computes the expected equilibrium average GMC volume over one cell cycle. * *

In the Potts model, a cell's target volume is initialized to {@code criticalVolume} on - * reset. The Potts energy drives the cell's actual volume toward this target, regardless of - * current growth rate. As a result, the volume-regulated growth phase effectively begins at - * {@code criticalVolume} (not the birth volume), even when {@code VOLUME_BASED_CRITICAL_VOLUME} - * is off and birth volume is below {@code criticalVolume}. + * reset. The Potts energy drives the cell's actual volume toward this target, + * regardless of current growth rate. As a result, the volume-regulated growth phase + * effectively begins at {@code criticalVolume} (not the birth volume), even when {@code + * VOLUME_BASED_CRITICAL_VOLUME} is off and birth volume is below {@code criticalVolume}. * *

The regulated growth phase therefore runs from {@code criticalVolume} to {@code sizeTarget * * criticalVolume}. Under constant-rate growth, the time-average volume over this phase is the @@ -134,7 +131,7 @@ void addCell(MersenneTwisterFast random, Simulation sim) { * * @param sim the simulation instance used to access the cell population */ - public final void updateGrowthRate(Simulation sim) { + final public void updateGrowthRate(Simulation sim) { if (!dynamicGrowthRateVolume) { cellGrowthRate = cellGrowthRateBase; } else { From 4376f32e4737ad41bf5a53dabd12f1e2300792e1 Mon Sep 17 00:00:00 2001 From: jannetty Date: Sat, 11 Apr 2026 22:20:08 -0700 Subject: [PATCH 05/20] more formatting --- .../PottsModuleFlyGMCDifferentiation.java | 18 +- ...oduleProliferationVolumeBasedDivision.java | 11 +- .../PottsModuleFlyGMCDifferentiation.java | 176 ---------- .../PottsModuleFlyGMCDifferentiationTest.java | 322 ++++++++++++++++++ ...eProliferationVolumeBasedDivisionTest.java | 1 + 5 files changed, 344 insertions(+), 184 deletions(-) delete mode 100644 test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java create mode 100644 test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiationTest.java diff --git a/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java b/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java index 818209568..b289586fa 100644 --- a/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java +++ b/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java @@ -23,7 +23,10 @@ */ public class PottsModuleFlyGMCDifferentiation extends PottsModuleProliferationVolumeBasedDivision { - /** Indicates whether GMC growth rate is based on individual cell conditions or average cell conditions */ + /** + * Indicates whether GMC growth rate is based on individual cell conditions or average cell + * conditions. + */ Boolean pdeLike; /** @@ -127,6 +130,11 @@ void addCell(MersenneTwisterFast random, Simulation sim) { differentiatedGMC.schedule(sim.getSchedule()); } + /** + * Updates the effective growth rate according to boolean flags specified in parameters. + * + * @param sim the simulation + */ public void updateGrowthRate(Simulation sim) { if (!dynamicGrowthRateVolume) { cellGrowthRate = cellGrowthRateBase; @@ -149,10 +157,14 @@ public void updateGrowthRate(Simulation sim) { for (int i = 0; i < objs.numObjs; i++) { Object o = objs.objs[i]; - if (!(o instanceof arcade.potts.agent.cell.PottsCell)) continue; + if (!(o instanceof arcade.potts.agent.cell.PottsCell)) { + continue; + } arcade.potts.agent.cell.PottsCell c = (arcade.potts.agent.cell.PottsCell) o; - if (c.getPop() != cell.getPop()) continue; // keep to same population + if (c.getPop() != cell.getPop()) { + continue; // keep to same population + } if (o instanceof arcade.potts.agent.cell.PottsCellFlyGMC) { arcade.potts.agent.cell.PottsCellFlyGMC gmc = diff --git a/src/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivision.java b/src/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivision.java index b6e0a88f1..aae4bc94f 100644 --- a/src/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivision.java +++ b/src/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivision.java @@ -77,15 +77,16 @@ public void step(MersenneTwisterFast random, Simulation sim) { * cellGrowthRate = cellGrowthRateBase * (volume / referenceVolume)^growthRateVolumeSensitivity * * - *

The reference volume is the scale at which the basal growth rate is recovered. In the - * simplest case this can be the cell's critical volume, but callers may also supply another + *

The reference volume is the cell volume at which the basal growth rate is recovered. In + * the simplest case this can be the cell's critical volume, but users may use another * biologically motivated reference such as an equilibrium or population-averaged volume. * * @param volume the current volume used in the growth-rate scaling * @param referenceVolume the reference volume that defines the baseline growth-rate scale */ - public void updateCellVolumeBasedGrowthRate(double volume, double cellCriticalVolume) { - double Ka = cellCriticalVolume; - cellGrowthRate = cellGrowthRateBase * Math.pow((volume / Ka), growthRateVolumeSensitivity); + public void updateCellVolumeBasedGrowthRate(double volume, double referenceVolume) { + double refVol = referenceVolume; + cellGrowthRate = + cellGrowthRateBase * Math.pow((volume / refVol), growthRateVolumeSensitivity); } } diff --git a/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java b/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java deleted file mode 100644 index 02bc73f48..000000000 --- a/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java +++ /dev/null @@ -1,176 +0,0 @@ -package arcade.potts.agent.module; - -import ec.util.MersenneTwisterFast; -import arcade.core.agent.cell.CellContainer; -import arcade.core.env.location.Location; -import arcade.core.sim.Simulation; -import arcade.potts.agent.cell.PottsCell; -import arcade.potts.agent.cell.PottsCellContainer; -import arcade.potts.agent.cell.PottsCellFlyGMC; -import arcade.potts.agent.cell.PottsCellFlyNeuron; -import arcade.potts.env.location.PottsLocation2D; -import arcade.potts.sim.Potts; -import arcade.potts.sim.PottsSimulation; -import arcade.potts.util.PottsEnums.State; - -/** - * Implementation of {@link PottsModuleProliferationVolumeBasedDivision} for fly GMC agents. These - * cells divide into two {@link PottsCellFlyNeuron} cells. The links must be set in the setup file - * so that 100% of the daughter cells are Neurons. - */ -public class PottsModuleFlyGMCDifferentiation extends PottsModuleProliferationVolumeBasedDivision { - - /** Indicates whether GMC growth rate is based on individual cell conditions or average cell conditions */ - Boolean pdeLike; - - /** - * Creates a fly GMC proliferation module. - * - * @param cell the cell to which this module is attached - */ - public PottsModuleFlyGMCDifferentiation(PottsCellFlyGMC cell) { - super(cell); - pdeLike = (cell.getParameters().getInt("proliferation/PDELIKE") != 0); - } - - /** - * Computes the expected equilibrium average GMC volume over one cell cycle. - * - *

In the Potts model, a cell's target volume is initialized to {@code criticalVolume} on - * reset. The Potts energy drives the cell's actual volume toward this target, - * regardless of current growth rate. As a result, the volume-regulated growth phase - * effectively begins at {@code criticalVolume} (not the birth volume), even when {@code - * VOLUME_BASED_CRITICAL_VOLUME} is off and birth volume is below {@code criticalVolume}. - * - *

The regulated growth phase therefore runs from {@code criticalVolume} to {@code sizeTarget - * * criticalVolume}. Under constant-rate growth, the time-average volume over this phase is the - * arithmetic mean of the two endpoints: - * - *

-     *   V_ref = (criticalVolume + sizeTarget * criticalVolume) / 2
-     *         = criticalVolume * (1 + sizeTarget) / 2
-     * 
- * - * @return the expected equilibrium average GMC volume - */ - double computeEquilibriumVolume() { - return cell.getCriticalVolume() * (1.0 + sizeTarget) / 2.0; - } - - /** - * Adds a cell to the simulation. - * - *

The cell location is split. The new neuron cell is created, initialized, and added to the - * schedule. This cell's location is also assigned to a new Neuron cell. The critical volume of - * both neurons is set to the initial volume of each neuron's location. - * - * @param random the random number generator - * @param sim the simulation instance - */ - @Override - void addCell(MersenneTwisterFast random, Simulation sim) { - Potts potts = ((PottsSimulation) sim).getPotts(); - - // Split current location - Location newLocation = ((PottsLocation2D) cell.getLocation()).split(random); - - // Reset current cell - cell.reset(potts.ids, potts.regions); - - // Create and schedule new neuron cell - int newID = sim.getID(); - CellContainer newContainer = cell.make(newID, State.QUIESCENT, random); - PottsCell newCell = - (PottsCell) newContainer.convert(sim.getCellFactory(), newLocation, random); - sim.getGrid().addObject(newCell, null); - potts.register(newCell); - newCell.initialize(potts.ids, potts.regions); - newCell.schedule(sim.getSchedule()); - - // remove old GMC cell from simulation - PottsCellFlyGMC oldCell = (PottsCellFlyGMC) cell; - Location location = oldCell.getLocation(); - sim.getGrid().removeObject(oldCell, location); - oldCell.stop(); - - // create new neuron cell and add to simulation. - int newPop = oldCell.getLinks().next(random); - - PottsCellContainer differentiatedGMCContainer = - new PottsCellContainer( - oldCell.getID(), - oldCell.getParent(), - newPop, - oldCell.getAge(), - oldCell.getDivisions(), - State.QUIESCENT, - null, - 0, - null, - oldCell.getCriticalVolume(), - oldCell.getCriticalHeight(), - oldCell.getCriticalRegionVolumes(), - oldCell.getCriticalRegionHeights()); - PottsCellFlyNeuron differentiatedGMC = - (PottsCellFlyNeuron) - differentiatedGMCContainer.convert(sim.getCellFactory(), location, random); - - sim.getGrid().addObject(differentiatedGMC, null); - potts.register(differentiatedGMC); - differentiatedGMC.initialize(potts.ids, potts.regions); - differentiatedGMC.schedule(sim.getSchedule()); - } - - /** - * Updates the GMC growth rate for the current simulation step. - * - *

If dynamic growth is disabled, the basal growth rate is used. Otherwise, the update is - * volume-based: either from this cell's own volume relative to its equilibrium reference - * volume, or in {@code pdeLike} mode from the population-average GMC volume and average - * equilibrium reference volume for cells in the same population. - * - * @param sim the simulation instance used to access the cell population - */ - final public void updateGrowthRate(Simulation sim) { - if (!dynamicGrowthRateVolume) { - cellGrowthRate = cellGrowthRateBase; - } else { - if (!pdeLike) { - updateCellVolumeBasedGrowthRate( - cell.getLocation().getVolume(), computeEquilibriumVolume()); - } else { - // PDE-like: use population-wide averages for GMCs (same pop as this cell). - // The reference volume is the population-average equilibrium volume: - // avgVRef = avgCritVol * (1 + sizeTarget) / 2 - // This assumes VOLUME_BASED_CRITICAL_VOLUME_MULTIPLIER = 1 (so each GMC's - // birth volume equals its critVol). For multiplier != 1, avgCritVol would need - // to be replaced by the mean of (critVol_i / multiplier + sizeTarget*critVol_i)/2. - sim.util.Bag objs = sim.getGrid().getAllObjects(); - - double volSum = 0.0; - double critSum = 0.0; - int count = 0; - - for (int i = 0; i < objs.numObjs; i++) { - Object o = objs.objs[i]; - if (!(o instanceof arcade.potts.agent.cell.PottsCell)) continue; - - arcade.potts.agent.cell.PottsCell c = (arcade.potts.agent.cell.PottsCell) o; - if (c.getPop() != cell.getPop()) continue; // keep to same population - - if (o instanceof arcade.potts.agent.cell.PottsCellFlyGMC) { - arcade.potts.agent.cell.PottsCellFlyGMC gmc = - (arcade.potts.agent.cell.PottsCellFlyGMC) o; - volSum += gmc.getLocation().getVolume(); - critSum += gmc.getCriticalVolume(); - count++; - } - } - double avgVolume = volSum / count; - double avgCritVol = critSum / count; - double avgVRef = avgCritVol * (1.0 + sizeTarget) / 2.0; - updateCellVolumeBasedGrowthRate(avgVolume, avgVRef); - } - } - } -} diff --git a/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiationTest.java b/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiationTest.java new file mode 100644 index 000000000..1334d9922 --- /dev/null +++ b/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiationTest.java @@ -0,0 +1,322 @@ +package arcade.potts.agent.module; + +import java.util.EnumMap; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedConstruction; +import sim.engine.Schedule; +import ec.util.MersenneTwisterFast; +import arcade.core.env.grid.Grid; +import arcade.core.sim.Simulation; +import arcade.core.util.GrabBag; +import arcade.core.util.Parameters; +import arcade.potts.agent.cell.PottsCell; +import arcade.potts.agent.cell.PottsCellContainer; +import arcade.potts.agent.cell.PottsCellFactory; +import arcade.potts.agent.cell.PottsCellFlyGMC; +import arcade.potts.agent.cell.PottsCellFlyNeuron; +import arcade.potts.env.location.PottsLocation; +import arcade.potts.env.location.PottsLocation2D; +import arcade.potts.sim.Potts; +import arcade.potts.sim.PottsSimulation; +import arcade.potts.util.PottsEnums.Region; +import arcade.potts.util.PottsEnums.State; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class PottsModuleFlyGMCDifferentiationTest { + private int[][][] dummyIDs; + + private int[][][] dummyRegions; + + private Simulation sim; + + private Potts potts; + + private Grid grid; + + private PottsCellFactory cellFactory; + + private Schedule schedule; + + private PottsCellFlyGMC gmcCell; + + private PottsLocation2D location; + + private PottsLocation newLocation; + + private PottsCellContainer container; + + private PottsCell newCell; + + private GrabBag links; + + private Parameters parameters; + + private MersenneTwisterFast random; + + private MockedConstruction mockedConstruction; + + @BeforeEach + public final void setupMocks() { + dummyIDs = new int[1][1][1]; + dummyRegions = new int[0][0][0]; + + sim = mock(PottsSimulation.class); + potts = mock(Potts.class); + when(((PottsSimulation) sim).getPotts()).thenReturn(potts); + potts.ids = dummyIDs; + potts.regions = dummyRegions; + + grid = mock(Grid.class); + when(sim.getGrid()).thenReturn(grid); + cellFactory = mock(PottsCellFactory.class); + when(sim.getCellFactory()).thenReturn(cellFactory); + schedule = mock(Schedule.class); + when(sim.getSchedule()).thenReturn(schedule); + when(sim.getID()).thenReturn(123); + + gmcCell = mock(PottsCellFlyGMC.class); + location = mock(PottsLocation2D.class); + when(gmcCell.getLocation()).thenReturn(location); + newLocation = mock(PottsLocation.class); + when(location.split(any(MersenneTwisterFast.class))).thenReturn(newLocation); + + links = mock(GrabBag.class); + when(gmcCell.getLinks()).thenReturn(links); + int newPop = 2; + when(links.next(any(MersenneTwisterFast.class))).thenReturn(newPop); + + // Stub getters on the GMC cell for differentiated cell creation + when(gmcCell.getID()).thenReturn(100); + int parent = 0; + when(gmcCell.getParent()).thenReturn(parent); + when(gmcCell.getAge()).thenReturn(5); + when(gmcCell.getDivisions()).thenReturn(2); + when(gmcCell.getCriticalVolume()).thenReturn(1.0); + when(gmcCell.getCriticalHeight()).thenReturn(2.0); + EnumMap critRegionVolumes = new EnumMap<>(Region.class); + EnumMap critRegionHeights = new EnumMap<>(Region.class); + when(gmcCell.getCriticalRegionVolumes()).thenReturn(critRegionVolumes); + when(gmcCell.getCriticalRegionHeights()).thenReturn(critRegionHeights); + + parameters = mock(Parameters.class); + when(gmcCell.getParameters()).thenReturn(parameters); + + random = mock(MersenneTwisterFast.class); + + // Intercept construction of the differentiated cell container + mockedConstruction = + mockConstruction( + PottsCellContainer.class, + (mockContainer, context) -> { + // When convert() is called on this new container, return a + // differentiated cell + PottsCellFlyNeuron diffCell = mock(PottsCellFlyNeuron.class); + when(mockContainer.convert( + eq(cellFactory), + eq(location), + any(MersenneTwisterFast.class))) + .thenReturn(diffCell); + }); + } + + @AfterEach + final void tearDown() { + mockedConstruction.close(); + } + + @Test + public void addCell_called_callsExpectedMethods() { + // When the module calls make() on the cell, return Quiescent PottsCellContainer + // mock + container = mock(PottsCellContainer.class); + when(gmcCell.make(eq(123), eq(State.QUIESCENT), any(MersenneTwisterFast.class))) + .thenReturn(container); + newCell = mock(PottsCell.class); + when(container.convert(eq(cellFactory), eq(newLocation), any(MersenneTwisterFast.class))) + .thenReturn(newCell); + + PottsModuleProliferationVolumeBasedDivision module = + new PottsModuleFlyGMCDifferentiation(gmcCell); + module.addCell(random, sim); + verify(location).split(random); + verify(gmcCell).reset(dummyIDs, dummyRegions); + verify(gmcCell).make(123, State.QUIESCENT, random); + + verify(grid).addObject(newCell, null); + verify(potts).register(newCell); + verify(newCell).initialize(dummyIDs, dummyRegions); + verify(newCell).schedule(schedule); + + verify(grid).removeObject(gmcCell, location); + verify(gmcCell).stop(); + + PottsCellContainer constructed = mockedConstruction.constructed().get(0); + PottsCellFlyNeuron diffCell = + (PottsCellFlyNeuron) constructed.convert(cellFactory, location, random); + verify(grid).addObject(diffCell, null); + verify(potts).register(diffCell); + verify(diffCell).initialize(dummyIDs, dummyRegions); + verify(diffCell).schedule(schedule); + } + + @Test + public void updateGrowthRate_dynamicOff_setsBaseRate() { + // dynamicGrowthRateVolume = 0; base rate used + when(gmcCell.getParameters()).thenReturn(parameters); + when(parameters.getInt("proliferation/DYNAMIC_GROWTH_RATE_VOLUME")).thenReturn(0); + when(parameters.getDouble("proliferation/CELL_GROWTH_RATE")).thenReturn(7.5); + + PottsModuleFlyGMCDifferentiation module = new PottsModuleFlyGMCDifferentiation(gmcCell); + + module.updateGrowthRate(sim); + org.junit.jupiter.api.Assertions.assertEquals(7.5, module.cellGrowthRate, 1e-9); + } + + @Test + public void updateGrowthRate_dynamicOn_pdeLikeFalse_usesSelfVolumeAndEquilibriumRef() { + when(gmcCell.getParameters()).thenReturn(parameters); + when(parameters.getInt("proliferation/DYNAMIC_GROWTH_RATE_VOLUME")).thenReturn(1); + when(parameters.getDouble("proliferation/CELL_GROWTH_RATE")).thenReturn(4.0); + when(parameters.getDouble("proliferation/SIZE_TARGET")).thenReturn(1.2); + when(parameters.getInt("proliferation/PDELIKE")).thenReturn(0); + + // critVol = 150.0; sizeTarget = 1.2 + // vRef = critVol * (1 + sizeTarget) / 2 = 150.0 * 2.2 / 2 = 165.0 + when(gmcCell.getCriticalVolume()).thenReturn(150.0); + when(gmcCell.getLocation().getVolume()).thenReturn(30.0); + + PottsModuleFlyGMCDifferentiation module = + org.mockito.Mockito.spy(new PottsModuleFlyGMCDifferentiation(gmcCell)); + + org.mockito.Mockito.doNothing() + .when(module) + .updateCellVolumeBasedGrowthRate( + org.mockito.ArgumentMatchers.anyDouble(), + org.mockito.ArgumentMatchers.anyDouble()); + + module.updateGrowthRate(sim); + + double expectedVRef = 150.0 * (1.0 + 1.2) / 2.0; // 165.0 + org.mockito.Mockito.verify(module) + .updateCellVolumeBasedGrowthRate( + org.mockito.ArgumentMatchers.eq(30.0), + org.mockito.ArgumentMatchers.eq(expectedVRef)); + } + + @Test + public void + updateGrowthRate_dynamicOnPdeLikeTrue_usesAverageVolumeAndEquilibriumRefAcrossGMCs() { + // Flags + when(gmcCell.getParameters()).thenReturn(parameters); + when(parameters.getInt("proliferation/DYNAMIC_GROWTH_RATE_VOLUME")).thenReturn(1); + when(parameters.getDouble("proliferation/CELL_GROWTH_RATE")).thenReturn(4.0); + when(parameters.getDouble("proliferation/SIZE_TARGET")).thenReturn(1.2); + when(parameters.getInt("proliferation/PDELIKE")).thenReturn(1); + + // Same population for all GMCs we want included + when(gmcCell.getPop()).thenReturn(3); + + // Self (included in average) + when(gmcCell.getLocation().getVolume()).thenReturn(30.0); + when(gmcCell.getCriticalVolume()).thenReturn(150.0); + + // Two more GMCs in same population + PottsCellFlyGMC gmcB = mock(PottsCellFlyGMC.class); + PottsCellFlyGMC gmcC = mock(PottsCellFlyGMC.class); + when(gmcB.getPop()).thenReturn(3); + when(gmcC.getPop()).thenReturn(3); + + PottsLocation locB = mock(PottsLocation.class); + PottsLocation locC = mock(PottsLocation.class); + when(gmcB.getLocation()).thenReturn(locB); + when(gmcC.getLocation()).thenReturn(locC); + when(locB.getVolume()).thenReturn(10.0); + when(locC.getVolume()).thenReturn(20.0); + when(gmcB.getCriticalVolume()).thenReturn(100.0); + when(gmcC.getCriticalVolume()).thenReturn(200.0); + + // Noise: different type and/or different pop → must be ignored + PottsCell randomOtherPop = mock(PottsCell.class); + when(randomOtherPop.getPop()).thenReturn(99); + PottsCellFlyNeuron neuronSamePop = mock(PottsCellFlyNeuron.class); + when(neuronSamePop.getPop()).thenReturn(3); + + // Bag with self + two GMCs + noise + sim.util.Bag bag = new sim.util.Bag(); + bag.add(gmcCell); // self GMC (pop 3) + bag.add(gmcB); // GMC (pop 3) + bag.add(gmcC); // GMC (pop 3) + bag.add(randomOtherPop); // different pop → ignored + bag.add(neuronSamePop); // not a GMC → ignored + when(sim.getGrid().getAllObjects()).thenReturn(bag); + + PottsModuleFlyGMCDifferentiation module = + org.mockito.Mockito.spy(new PottsModuleFlyGMCDifferentiation(gmcCell)); + + // Observe the averaged args + org.mockito.Mockito.doNothing() + .when(module) + .updateCellVolumeBasedGrowthRate( + org.mockito.ArgumentMatchers.anyDouble(), + org.mockito.ArgumentMatchers.anyDouble()); + + module.updateGrowthRate(sim); + + // avgVol = (30 + 10 + 20) / 3 = 20.0 + // avgCritVol = (150 + 100 + 200) / 3 = 150.0 + // avgVRef = avgCritVol * (1 + sizeTarget) / 2 = 150.0 * (1 + 1.2) / 2 = 165.0 + double expectedAvgVol = (30.0 + 10.0 + 20.0) / 3.0; // 20.0 + double expectedAvgCrit = (150.0 + 100.0 + 200.0) / 3.0; // 150.0 + double expectedAvgVRef = expectedAvgCrit * (1.0 + 1.2) / 2.0; // 165.0 + + org.mockito.Mockito.verify(module) + .updateCellVolumeBasedGrowthRate( + org.mockito.ArgumentMatchers.eq(expectedAvgVol), + org.mockito.ArgumentMatchers.eq(expectedAvgVRef)); + } + + // computeEquilibriumVolume tests + + @Test + public void computeEquilibriumVolume_returnsArithmeticMeanOfCritAndDivisionVolumes() { + // critVol = 150.0; sizeTarget = 1.2 + // vRef = critVol * (1 + sizeTarget) / 2 = 150.0 * 2.2 / 2 = 165.0 + when(parameters.getDouble("proliferation/SIZE_TARGET")).thenReturn(1.2); + when(gmcCell.getCriticalVolume()).thenReturn(150.0); + + PottsModuleFlyGMCDifferentiation module = new PottsModuleFlyGMCDifferentiation(gmcCell); + org.junit.jupiter.api.Assertions.assertEquals( + 165.0, module.computeEquilibriumVolume(), 1e-9); + } + + @Test + public void computeEquilibriumVolume_differentSizeTarget_scalesCorrectly() { + // critVol = 100.0; sizeTarget = 2.0 + // vRef = 100.0 * (1 + 2.0) / 2 = 150.0 + when(parameters.getDouble("proliferation/SIZE_TARGET")).thenReturn(2.0); + when(gmcCell.getCriticalVolume()).thenReturn(100.0); + + PottsModuleFlyGMCDifferentiation module = new PottsModuleFlyGMCDifferentiation(gmcCell); + org.junit.jupiter.api.Assertions.assertEquals( + 150.0, module.computeEquilibriumVolume(), 1e-9); + } + + @Test + public void computeEquilibriumVolume_differentCritVol_scalesCorrectly() { + // critVol = 200.0; sizeTarget = 1.2 + // vRef = 200.0 * (1 + 1.2) / 2 = 220.0 + when(parameters.getDouble("proliferation/SIZE_TARGET")).thenReturn(1.2); + when(gmcCell.getCriticalVolume()).thenReturn(200.0); + + PottsModuleFlyGMCDifferentiation module = new PottsModuleFlyGMCDifferentiation(gmcCell); + org.junit.jupiter.api.Assertions.assertEquals( + 220.0, module.computeEquilibriumVolume(), 1e-9); + } +} diff --git a/test/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivisionTest.java b/test/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivisionTest.java index 254cc1371..4b028a17f 100644 --- a/test/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivisionTest.java +++ b/test/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivisionTest.java @@ -14,6 +14,7 @@ public class PottsModuleProliferationVolumeBasedDivisionTest { static class PottsModuleProliferationVolumeBasedDivisionMock extends PottsModuleProliferationVolumeBasedDivision { + boolean addCellCalled = false; boolean growthRateUpdated = false; From 731082f54a425a77f51d5073ea274e577f7f9fde Mon Sep 17 00:00:00 2001 From: jannetty Date: Sat, 11 Apr 2026 22:22:47 -0700 Subject: [PATCH 06/20] final formatting --- .../module/PottsModuleProliferationVolumeBasedDivisionTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/test/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivisionTest.java b/test/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivisionTest.java index 4b028a17f..733aaf896 100644 --- a/test/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivisionTest.java +++ b/test/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivisionTest.java @@ -16,6 +16,7 @@ static class PottsModuleProliferationVolumeBasedDivisionMock extends PottsModuleProliferationVolumeBasedDivision { boolean addCellCalled = false; + boolean growthRateUpdated = false; PottsModuleProliferationVolumeBasedDivisionMock(PottsCellFlyGMC cell) { From 6cb465dac0e49f077cdff0e074feb6d2aa9db29a Mon Sep 17 00:00:00 2001 From: jannetty Date: Sat, 11 Apr 2026 22:32:27 -0700 Subject: [PATCH 07/20] adding missing parameters to potts xml. Removing unnecessary parameter --- .../agent/module/PottsModuleFlyGMCDifferentiation.java | 3 --- src/arcade/potts/parameter.potts.xml | 7 +++++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java b/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java index b289586fa..35cc8fa2b 100644 --- a/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java +++ b/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java @@ -146,9 +146,6 @@ public void updateGrowthRate(Simulation sim) { // PDE-like: use population-wide averages for GMCs (same pop as this cell). // The reference volume is the population-average equilibrium volume: // avgVRef = avgCritVol * (1 + sizeTarget) / 2 - // This assumes VOLUME_BASED_CRITICAL_VOLUME_MULTIPLIER = 1 (so each GMC's - // birth volume equals its critVol). For multiplier != 1, avgCritVol would need - // to be replaced by the mean of (critVol_i / multiplier + sizeTarget*critVol_i)/2. sim.util.Bag objs = sim.getGrid().getAllObjects(); double volSum = 0.0; diff --git a/src/arcade/potts/parameter.potts.xml b/src/arcade/potts/parameter.potts.xml index ebc46e0da..c95d0ce61 100644 --- a/src/arcade/potts/parameter.potts.xml +++ b/src/arcade/potts/parameter.potts.xml @@ -64,6 +64,13 @@ + + + + + + + From 64ce606989f6b90c9df231418db55c27cacc5df1 Mon Sep 17 00:00:00 2001 From: jannetty Date: Sat, 11 Apr 2026 22:33:36 -0700 Subject: [PATCH 08/20] removing redundant section of docstring --- .../potts/agent/module/PottsModuleFlyGMCDifferentiation.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java b/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java index 35cc8fa2b..299116b01 100644 --- a/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java +++ b/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java @@ -16,10 +16,7 @@ /** * Implementation of {@link PottsModuleProliferationVolumeBasedDivision} for fly GMC agents. These * cells divide into two {@link PottsCellFlyNeuron} cells. The links must be set in the setup file - * so that 100% of the daughter cells are Neurons. Implementation of {@link - * PottsModuleProliferationVolumeBasedDivision} for fly GMC agents. These cells divide into two - * {@link PottsCellFlyNeuron} cells. The links must be set in the setup file so that 100% of the - * daughter cells are Neurons. + * so that 100% of the daughter cells are Neurons. */ public class PottsModuleFlyGMCDifferentiation extends PottsModuleProliferationVolumeBasedDivision { From 2ce8a27ce068ce820308c5c213ea0b7a4ee70037 Mon Sep 17 00:00:00 2001 From: jannetty Date: Sat, 11 Apr 2026 22:37:23 -0700 Subject: [PATCH 09/20] puppycrawler asking for some odd spacing --- .../module/PottsModuleProliferationVolumeBasedDivisionTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/test/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivisionTest.java b/test/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivisionTest.java index 733aaf896..7063f37bf 100644 --- a/test/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivisionTest.java +++ b/test/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivisionTest.java @@ -17,6 +17,7 @@ static class PottsModuleProliferationVolumeBasedDivisionMock boolean addCellCalled = false; + boolean growthRateUpdated = false; PottsModuleProliferationVolumeBasedDivisionMock(PottsCellFlyGMC cell) { From 3e8bebc1af88d046b7b52ce1842539af4273af06 Mon Sep 17 00:00:00 2001 From: jannetty Date: Sat, 11 Apr 2026 22:38:57 -0700 Subject: [PATCH 10/20] puppycrawl now wants me to undo the odd spacing it asked for --- .../module/PottsModuleProliferationVolumeBasedDivisionTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/test/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivisionTest.java b/test/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivisionTest.java index 7063f37bf..733aaf896 100644 --- a/test/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivisionTest.java +++ b/test/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivisionTest.java @@ -17,7 +17,6 @@ static class PottsModuleProliferationVolumeBasedDivisionMock boolean addCellCalled = false; - boolean growthRateUpdated = false; PottsModuleProliferationVolumeBasedDivisionMock(PottsCellFlyGMC cell) { From 9238f516207b8bdd11ef83f72a53fea52a02994a Mon Sep 17 00:00:00 2001 From: jannetty Date: Sat, 11 Apr 2026 22:48:59 -0700 Subject: [PATCH 11/20] removing part of PR more relevant to neuroblasts --- src/arcade/potts/sim/Potts.java | 2 +- src/arcade/potts/sim/Potts2D.java | 2 +- src/arcade/potts/sim/Potts3D.java | 2 +- test/arcade/potts/sim/PottsTest.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/arcade/potts/sim/Potts.java b/src/arcade/potts/sim/Potts.java index a4ffb0ee2..a0b46f0db 100644 --- a/src/arcade/potts/sim/Potts.java +++ b/src/arcade/potts/sim/Potts.java @@ -398,7 +398,7 @@ public PottsCell getCell(int id) { * @param z the z coordinate * @return the list of unique IDs */ - public abstract HashSet getUniqueIDs(int x, int y, int z); + abstract HashSet getUniqueIDs(int x, int y, int z); /** * Gets unique regions adjacent to given voxel. diff --git a/src/arcade/potts/sim/Potts2D.java b/src/arcade/potts/sim/Potts2D.java index f6d7dbb6f..011d6f850 100644 --- a/src/arcade/potts/sim/Potts2D.java +++ b/src/arcade/potts/sim/Potts2D.java @@ -151,7 +151,7 @@ private boolean getConnectivityThreeNeighbors(boolean[][] subarray) { } @Override - public HashSet getUniqueIDs(int x, int y, int z) { + HashSet getUniqueIDs(int x, int y, int z) { int id = ids[z][x][y]; HashSet unique = new HashSet<>(); diff --git a/src/arcade/potts/sim/Potts3D.java b/src/arcade/potts/sim/Potts3D.java index 7f3429650..7c4686e8d 100644 --- a/src/arcade/potts/sim/Potts3D.java +++ b/src/arcade/potts/sim/Potts3D.java @@ -465,7 +465,7 @@ private boolean getConnectivityFiveNeighbors(boolean[][][] array) { } @Override - public HashSet getUniqueIDs(int x, int y, int z) { + HashSet getUniqueIDs(int x, int y, int z) { int id = ids[z][x][y]; HashSet unique = new HashSet<>(); diff --git a/test/arcade/potts/sim/PottsTest.java b/test/arcade/potts/sim/PottsTest.java index 305658832..04c61d693 100644 --- a/test/arcade/potts/sim/PottsTest.java +++ b/test/arcade/potts/sim/PottsTest.java @@ -61,7 +61,7 @@ boolean getConnectivity(boolean[][][] array, boolean zero) { } @Override - public HashSet getUniqueIDs(int x, int y, int z) { + HashSet getUniqueIDs(int x, int y, int z) { HashSet set = new HashSet<>(); if (x == 0 && y == 0) { set.add(1); From 6ec7a8d8826f5cf5012b331ff66f18f2821099e5 Mon Sep 17 00:00:00 2001 From: jannetty Date: Sun, 12 Apr 2026 13:55:27 -0700 Subject: [PATCH 12/20] first portion of neuroblast files --- .../potts/agent/cell/PottsCellContainer.java | 6 + .../potts/agent/cell/PottsCellFactory.java | 10 +- .../potts/agent/cell/PottsCellFlyStem.java | 178 ++ .../PottsModuleFlyStemProliferation.java | 712 ++++++++ src/arcade/potts/parameter.potts.xml | 15 + src/arcade/potts/sim/Potts.java | 2 +- src/arcade/potts/sim/Potts2D.java | 2 +- src/arcade/potts/sim/Potts3D.java | 2 +- .../agent/cell/PottsCellFlyStemTest.java | 196 +++ .../PottsModuleFlyStemProliferationTest.java | 1449 +++++++++++++++++ test/arcade/potts/sim/PottsTest.java | 2 +- 11 files changed, 2568 insertions(+), 6 deletions(-) create mode 100644 src/arcade/potts/agent/cell/PottsCellFlyStem.java create mode 100644 src/arcade/potts/agent/module/PottsModuleFlyStemProliferation.java create mode 100644 test/arcade/potts/agent/cell/PottsCellFlyStemTest.java create mode 100644 test/arcade/potts/agent/module/PottsModuleFlyStemProliferationTest.java diff --git a/src/arcade/potts/agent/cell/PottsCellContainer.java b/src/arcade/potts/agent/cell/PottsCellContainer.java index 6bc855314..e161b0630 100644 --- a/src/arcade/potts/agent/cell/PottsCellContainer.java +++ b/src/arcade/potts/agent/cell/PottsCellContainer.java @@ -10,6 +10,8 @@ import arcade.core.util.GrabBag; import arcade.core.util.MiniBox; import arcade.core.util.Parameters; +import arcade.potts.util.PottsEnums.Phase; +import arcade.potts.util.PottsEnums.Region; import static arcade.potts.util.PottsEnums.Phase; import static arcade.potts.util.PottsEnums.Region; @@ -176,6 +178,10 @@ public Cell convert( return new PottsCellFlyNeuron(this, location, parameters, links); case "fly-gmc": return new PottsCellFlyGMC(this, location, parameters, links); + case "fly-stem-wt": + return new PottsCellFlyStem(this, location, parameters, links); + case "fly-stem-mudmut": + return new PottsCellFlyStem(this, location, parameters, links); default: case "stem": return new PottsCellStem(this, location, parameters, links); diff --git a/src/arcade/potts/agent/cell/PottsCellFactory.java b/src/arcade/potts/agent/cell/PottsCellFactory.java index 4d040f77a..8a17276f9 100644 --- a/src/arcade/potts/agent/cell/PottsCellFactory.java +++ b/src/arcade/potts/agent/cell/PottsCellFactory.java @@ -1,5 +1,6 @@ package arcade.potts.agent.cell; +import java.security.InvalidParameterException; import java.util.ArrayList; import java.util.EnumMap; import java.util.HashMap; @@ -213,8 +214,13 @@ void parseValues(Series series) { if (linkKeys.size() > 0) { links = new GrabBag(); for (String linkKey : linkKeys) { - int popLink = series.populations.get(linkKey).getInt("CODE"); - links.add(popLink, linksBox.getDouble(linkKey)); + try { + int popLink = series.populations.get(linkKey).getInt("CODE"); + links.add(popLink, linksBox.getDouble(linkKey)); + } catch (Exception e) { + throw new InvalidParameterException( + "A population link is set that references a population that does not exist."); + } } } diff --git a/src/arcade/potts/agent/cell/PottsCellFlyStem.java b/src/arcade/potts/agent/cell/PottsCellFlyStem.java new file mode 100644 index 000000000..3072a0f98 --- /dev/null +++ b/src/arcade/potts/agent/cell/PottsCellFlyStem.java @@ -0,0 +1,178 @@ +package arcade.potts.agent.cell; + +import ec.util.MersenneTwisterFast; +import arcade.core.agent.cell.CellState; +import arcade.core.env.location.Location; +import arcade.core.util.GrabBag; +import arcade.core.util.Parameters; +import arcade.core.util.Vector; +import arcade.potts.agent.module.PottsModule; +import arcade.potts.agent.module.PottsModuleFlyStemProliferation; +import arcade.potts.util.PottsEnums.Phase; +import static arcade.potts.util.PottsEnums.State; + +/** + * Implementation of {@link PottsCell} for fly stem agents. Genotype is specified by the StemType + * enum. + */ +public class PottsCellFlyStem extends PottsCell { + /** Enum outlining parameters for each cell type. */ + public enum StemType { + /** Wild type stem cell. */ + WT(50, 86, 0), + + /** mud Mutant stem cell. */ + MUDMUT(50, 50, -90); + + /** Percentage x offset from cell edge where division will occur. */ + public final int splitOffsetPercentX; + + /** Percentage y offset from cell edge where division will occur. */ + public final int splitOffsetPercentY; + + /** Default direction of division is rotated this much off the apical vector. */ + public final double splitDirectionRotation; + + /** + * The proportion of the NB division volume allocated to the GMC daughter cell. Derived from + * {@code splitOffsetPercentY} as {@code 1 - splitOffsetPercentY / 100} + */ + public final double daughterCellCriticalVolumeProportion; + + /** + * Constructor for StemType. + * + * @param splitOffsetPercentX percentage x offset from cell edge where division will occur + * @param splitOffsetPercentY percentage y offset from cell edge where division will occur + * @param splitDirectionRotation the plane of division's rotation off the apical vector + */ + StemType(int splitOffsetPercentX, int splitOffsetPercentY, double splitDirectionRotation) { + this.splitOffsetPercentX = splitOffsetPercentX; + this.splitOffsetPercentY = splitOffsetPercentY; + this.splitDirectionRotation = splitDirectionRotation; + this.daughterCellCriticalVolumeProportion = 1.0 - splitOffsetPercentY / 100.0; + } + } + + /** The type of stem cell. */ + public final StemType stemType; + + /** The cell's apical axis. The vector points towards the apical membrane. */ + private Vector apicalAxis; + + /** + * Constructor for PottsCellFlyStem. + * + * @param container the container for the cell + * @param location the location of the cell + * @param parameters the parameters for the cell + * @param links the links for the cell + * @throws IllegalArgumentException if the stem type is not recognized + */ + public PottsCellFlyStem( + PottsCellContainer container, Location location, Parameters parameters, GrabBag links) { + super(container, location, parameters, links); + + if (module != null) { + ((PottsModule) module).setPhase(Phase.UNDEFINED); + } + + String stemTypeString = parameters.getString("CLASS"); + switch (stemTypeString) { + case "fly-stem-wt": + stemType = StemType.WT; + break; + case "fly-stem-mudmut": + stemType = StemType.MUDMUT; + break; + default: + throw new IllegalArgumentException("Unknown StemType: " + stemTypeString); + } + } + + /** + * Sets the apical axis. + * + * @param apicalAxis the new apical axis + */ + public void setApicalAxis(Vector apicalAxis) { + this.apicalAxis = apicalAxis; + } + + /** + * Gets the apical axis of the cell. If no apical axis is set, it returns a vector along the y + * axis as a default vector + * + * @return the apical axis of the cell + */ + public Vector getApicalAxis() { + if (apicalAxis != null) { + return apicalAxis; + } else { + return new Vector(0, 1, 0); + } + } + + @Override + public PottsCellContainer make(int newID, CellState newState, MersenneTwisterFast random) { + throw new UnsupportedOperationException( + "make(int, CellState, MersenneTwisterFast) not supported. Please use make(int, CellState, MersenneTwisterFast, int, double) instead."); + } + + /** + * Makes a potts cell container with information about the daughter cell's population and + * critical volume calculated by the proliferation module. + * + * @param newID the new cell ID + * @param newState the new cell state + * @param random the random number generator + * @param newPop the new cell population + * @param daughterCellCriticalVolume the new cell's critical volume + * @return a {@link PottsCellContainer} with the information needed to make the daughter cell + */ + public PottsCellContainer make( + int newID, + CellState newState, + MersenneTwisterFast random, + int newPop, + double daughterCellCriticalVolume) { + + divisions++; + + return new PottsCellContainer( + newID, + id, + newPop, + age, + divisions, + newState, + Phase.UNDEFINED, + 0, + null, + daughterCellCriticalVolume, + criticalHeight, + criticalRegionVolumes, + criticalRegionHeights); + } + + @Override + void setStateModule(CellState newState) { + switch ((State) newState) { + case PROLIFERATIVE: + module = new PottsModuleFlyStemProliferation(this); + break; + default: + module = null; + break; + } + } + + /** + * Gets the stem type of the cell. + * + * @return the stem type of the cell + */ + public final StemType getStemType() { + return stemType; + } +} diff --git a/src/arcade/potts/agent/module/PottsModuleFlyStemProliferation.java b/src/arcade/potts/agent/module/PottsModuleFlyStemProliferation.java new file mode 100644 index 000000000..6a5266330 --- /dev/null +++ b/src/arcade/potts/agent/module/PottsModuleFlyStemProliferation.java @@ -0,0 +1,712 @@ +package arcade.potts.agent.module; + +import java.security.InvalidParameterException; +import java.util.ArrayList; +import java.util.HashSet; +import sim.util.Bag; +import sim.util.Double3D; +import ec.util.MersenneTwisterFast; +import arcade.core.env.location.Location; +import arcade.core.sim.Simulation; +import arcade.core.util.Parameters; +import arcade.core.util.Plane; +import arcade.core.util.Vector; +import arcade.core.util.distributions.Distribution; +import arcade.core.util.distributions.NormalDistribution; +import arcade.core.util.distributions.UniformDistribution; +import arcade.potts.agent.cell.PottsCell; +import arcade.potts.agent.cell.PottsCellContainer; +import arcade.potts.agent.cell.PottsCellFlyStem; +import arcade.potts.agent.cell.PottsCellFlyStem.StemType; +import arcade.potts.env.location.PottsLocation; +import arcade.potts.env.location.PottsLocation2D; +import arcade.potts.env.location.Voxel; +import arcade.potts.sim.Potts; +import arcade.potts.sim.PottsSimulation; +import static arcade.potts.util.PottsEnums.Direction; +import static arcade.potts.util.PottsEnums.Phase; +import static arcade.potts.util.PottsEnums.State; + +/** + * Implementation of {@link PottsModuleProliferationVolumeBasedDivision} for fly stem agents. Each + * division produces two daughters: one stem cell and one that is either stem or GMC depending on + * division geometry and rules. This module determines the division plane (affecting morphology) and + * assigns daughter cell identity. + */ +public class PottsModuleFlyStemProliferation extends PottsModuleProliferationVolumeBasedDivision { + + /** Threshold for critical volume size checkpoint. */ + static final double SIZE_CHECKPOINT = 0.95; + + /** Basal rate of apoptosis (ticks^-1). */ + final double basalApoptosisRate; + + /** Distribution that determines rotational offset of cell's division plane. */ + final NormalDistribution splitDirectionDistribution; + + /** Ruleset for determining which daughter cell is the GMC. Can be `volume` or `location`. */ + final String differentiationRuleset; + + /** + * Ruleset for determining how the cell determines its Apical Axis. Can be 'uniform', 'global', + * or 'rotation' + */ + final String apicalAxisRuleset; + + /** + * The distribution used to determine how apical axis should be rotated. Relevant when + * apicalAxisRuleset is set to 'uniform' or 'rotation'. + */ + final Distribution apicalAxisRotationDistribution; + + /** + * Boolean flag indicating whether or not the cell's critical volume should be affected by its + * volume at the time it divides. + */ + final boolean volumeBasedCriticalVolume; + + /** Boolean flag indicating whether growth rate should be regulated by NB-NB contact. */ + final boolean dynamicGrowthRateNBSelfRepression; + + /** + * Range of values considered equal when determining daughter cell identity. ex. if ruleset is + * location, range determines the distance between centroid y values that is considered equal. + */ + final double range; + + /** + * Half-max NB neighbor count for repression (K). Only relevant if dynamicGrowthRateNBContact is + * true. + */ + final double nbContactHalfMax; + + /** + * Hill coefficient for NB-contact repression (n). Only relevant if dynamicGrowthRateNBContact + * is true. + */ + final double nbContactHillN; + + /* + * Boolean flag for whether the daughter cell's differentiation is determined deterministically. + */ + final boolean hasDeterministicDifferentiation; + + final double initialSize; + + public static final double EPSILON = 1e-8; + + /** + * Boolean determining whether growth and division rates are universal across all NBs. If true + * model behaviors is PDE-like, if false it is ABM-like. + */ + final Boolean pdeLike; + + /** + * Creates a proliferation {@code Module} for the given {@link PottsCellFlyStem}. + * + * @param cell the {@link PottsCellFlyStem} the module is associated with + */ + public PottsModuleFlyStemProliferation(PottsCellFlyStem cell) { + super(cell); + + if (cell.hasRegions()) { + throw new UnsupportedOperationException( + "Regions are not yet implemented for fly cells"); + } + + Parameters parameters = cell.getParameters(); + + basalApoptosisRate = parameters.getDouble("proliferation/BASAL_APOPTOSIS_RATE"); + splitDirectionDistribution = + (NormalDistribution) + parameters.getDistribution("proliferation/DIV_ROTATION_DISTRIBUTION"); + differentiationRuleset = parameters.getString("proliferation/DIFFERENTIATION_RULESET"); + range = parameters.getDouble("proliferation/DIFFERENTIATION_RULESET_EQUALITY_RANGE"); + apicalAxisRuleset = parameters.getString("proliferation/APICAL_AXIS_RULESET"); + apicalAxisRotationDistribution = + (Distribution) + parameters.getDistribution( + "proliferation/APICAL_AXIS_ROTATION_DISTRIBUTION"); + + volumeBasedCriticalVolume = + (parameters.getInt("proliferation/VOLUME_BASED_CRITICAL_VOLUME") != 0); + + dynamicGrowthRateNBSelfRepression = + (parameters.getInt("proliferation/DYNAMIC_GROWTH_RATE_NB_SELF_REPRESSION") != 0); + + if (dynamicGrowthRateVolume && dynamicGrowthRateNBSelfRepression) { + throw new InvalidParameterException( + "Dynamic growth rate can be either volume-based or NB-contact-based, not both."); + } + + nbContactHalfMax = parameters.getDouble("proliferation/NB_CONTACT_HALF_MAX"); + nbContactHillN = parameters.getDouble("proliferation/NB_CONTACT_HILL_N"); + + String hasDeterministicDifferentiationString = + parameters.getString("proliferation/HAS_DETERMINISTIC_DIFFERENTIATION"); + if (!hasDeterministicDifferentiationString.equals("TRUE") + && !hasDeterministicDifferentiationString.equals("FALSE")) { + throw new InvalidParameterException( + "hasDeterministicDifferentiation must be either TRUE or FALSE"); + } + hasDeterministicDifferentiation = hasDeterministicDifferentiationString.equals("TRUE"); + + initialSize = cell.getVolume(); + + pdeLike = (parameters.getInt("proliferation/PDELIKE") != 0); + + setPhase(Phase.UNDEFINED); + } + + @Override + public void addCell(MersenneTwisterFast random, Simulation sim) { + Potts potts = ((PottsSimulation) sim).getPotts(); + PottsCellFlyStem flyStemCell = (PottsCellFlyStem) cell; + + Plane divisionPlane = chooseDivisionPlane(flyStemCell); + PottsLocation2D parentLoc = (PottsLocation2D) cell.getLocation(); + PottsLocation daughterLoc = (PottsLocation) parentLoc.split(random, divisionPlane); + + boolean isDaughterStem = daughterStem(parentLoc, daughterLoc, divisionPlane); + + if (isDaughterStem) { + makeDaughterStemCell(daughterLoc, sim, potts, random); + } else { + makeDaughterGMC( + parentLoc, + daughterLoc, + sim, + potts, + random, + divisionPlane.getUnitNormalVector()); + } + } + + /** + * Updates the effective growth rate according to the ruleset indicated in parameters. + * + * @param sim the simulation + */ + public void updateGrowthRate(Simulation sim) { + if (dynamicGrowthRateVolume == true) { + updateVolumeBasedGrowthRate(sim); + } else if (dynamicGrowthRateNBSelfRepression == true) { + updateGrowthRateBasedOnOtherNBs(sim); + } else { + cellGrowthRate = cellGrowthRateBase; + } + } + + /** + * Updates growth rate based on cell volume relative to a reference volume. + * + *

Growth is regulated by comparing a volume signal to an equilibrium reference. The source + * of the volume signal depends on the simulation mode: + * + *

    + *
  • Local mode: uses this cell's volume + *
  • PDE-like mode: uses the average volume of all NBs in the simulation (global + * coupling) + *
+ * + * The resulting signal is passed to the volume-based growth function, which adjusts growth rate + * relative to the equilibrium volume. + * + * @param sim the simulation + */ + public void updateVolumeBasedGrowthRate(Simulation sim) { + double vRef = computeEquilibriumVolume(); + if (pdeLike == false) { + updateCellVolumeBasedGrowthRate(cell.getLocation().getVolume(), vRef); + } else { + HashSet nbsInSimulation = getNBsInSimulation(sim); + double volSum = 0.0; + for (PottsCellFlyStem nb : nbsInSimulation) { + volSum += nb.getLocation().getVolume(); + } + double avgVolume = volSum / nbsInSimulation.size(); + updateCellVolumeBasedGrowthRate(avgVolume, vRef); + } + } + + /** + * Computes the expected average NB volume from structural parameters, using a rectangular + * approximation for the post-division volume retained by the NB. + * + *

For a NB growing at constant rate, the time-averaged volume over one cell cycle equals the + * arithmetic midpoint between birth volume and division volume: + * + *

+     *   V_ref = (V_birth + V_div) / 2
+     *         = sizeTarget * critVol * (f_retain + 1) / 2
+     * 
+ * + * where {@code f_retain = splitOffsetPercentY / 100} approximates the fraction of the + * pre-division volume retained by the NB after asymmetric division. + * + *

This reference volume is used as the normalization denominator in the volume-based growth + * rate formula, ensuring that at the average NB volume the effective growth rate equals {@code + * cellGrowthRateBase}. + * + * @return the expected average NB volume + */ + double computeEquilibriumVolume() { + double vDiv = sizeTarget * cell.getCriticalVolume(); + double fRetain = ((PottsCellFlyStem) cell).getStemType().splitOffsetPercentY / 100.0; + return vDiv * (fRetain + 1.0) / 2.0; + } + + /** + * Gets the neighbors of this cell that are unique neuroblasts. + * + * @param sim the simulation + * @return the number of unique neuroblast neighbors + */ + protected HashSet getNBNeighbors(Simulation sim) { + Potts potts = ((PottsSimulation) sim).getPotts(); + ArrayList voxels = ((PottsLocation) cell.getLocation()).getVoxels(); + HashSet stemNeighbors = new HashSet(); + + for (Voxel v : voxels) { + HashSet uniqueIDs = potts.getUniqueIDs(v.x, v.y, v.z); + for (Integer id : uniqueIDs) { + PottsCell neighbor = (PottsCell) sim.getGrid().getObjectAt(id); + if (neighbor == null) { + continue; + } + if (cell.getPop() == neighbor.getPop()) { + if (neighbor.getID() != cell.getID()) { + stemNeighbors.add((PottsCellFlyStem) sim.getGrid().getObjectAt(id)); + } + } + } + } + return stemNeighbors; + } + + /** + * Updates the neuroblast (NB) contact-dependent growth rate. + * + *

Growth is repressed as a function of NB contact using a Hill function. The number of + * interacting NBs is determined in one of two ways: + * + *

    + *
  • PDE-like mode: uses the total number of NBs in the simulation (global coupling) + *
  • Local mode: uses only this cell's neighboring NBs (local coupling) + *
+ * + * The resulting repression factor scales the base growth rate, reducing growth as NB contact + * increases. + * + * @param sim the simulation + */ + protected void updateGrowthRateBasedOnOtherNBs(Simulation sim) { + int nbsInContact; + if (pdeLike) { + int nbsInSim = getNBsInSimulation(sim).size(); + nbsInContact = nbsInSim - 1; + } else { + nbsInContact = getNBNeighbors(sim).size(); + } + double np = Math.max(0.0, (double) nbsInContact); + + double Kn = Math.pow(nbContactHalfMax, nbContactHillN); + double Npn = Math.pow(np, nbContactHillN); + + double hillRepression; + if (Kn == 0.0) { + hillRepression = (np == 0.0) ? 1.0 : 0.0; + } else { + hillRepression = Kn / (Kn + Npn); + } + + cellGrowthRate = cellGrowthRateBase * hillRepression; + } + + /** + * Chooses the division plane according to the type of stem cell this module is attached to. + * + * @param flyStemCell the stem cell this module is attached to + * @return the plane along which this cell should divide + */ + protected Plane chooseDivisionPlane(PottsCellFlyStem flyStemCell) { + double offset = sampleDivisionPlaneOffset(); + + if (flyStemCell.getStemType() == StemType.WT + || (flyStemCell.getStemType() == StemType.MUDMUT + && (Math.abs(offset - splitDirectionDistribution.getExpected()) <= 45))) { + return getWTDivisionPlaneWithRotationalVariance(flyStemCell, offset); + } else { + return getMUDDivisionPlane(flyStemCell); + } + } + + /** + * Gets the rotation offset for the division plane according to splitDirectionDistribution. + * + * @return the rotation offset for the division plane + */ + double sampleDivisionPlaneOffset() { + return splitDirectionDistribution.nextDouble(); + } + + /** + * Gets the division plane for the cell after rotating the plane according to + * splitDirectionDistribution. This follows WT division rules. The plane is rotated around the + * XY plane. + * + * @param cell the {@link PottsCellFlyStem} to get the division plane for + * @param rotationOffset the angle to rotate the plane + * @return the division plane for the cell + */ + public Plane getWTDivisionPlaneWithRotationalVariance( + PottsCellFlyStem cell, double rotationOffset) { + Vector apical_axis = cell.getApicalAxis(); + Vector rotatedNormalVector = + Vector.rotateVectorAroundAxis( + apical_axis, Direction.XY_PLANE.vector, rotationOffset); + Voxel splitVoxel = getCellSplitVoxel(StemType.WT, cell, rotatedNormalVector); + return new Plane( + new Double3D(splitVoxel.x, splitVoxel.y, splitVoxel.z), rotatedNormalVector); + } + + /** + * Gets the division plane for the cell. This follows MUDMUT division rules. The division plane + * is not rotated. + * + * @param cell the {@link PottsCellFlyStem} to get the division plane for + * @return the division plane for the cell + */ + public Plane getMUDDivisionPlane(PottsCellFlyStem cell) { + Vector defaultNormal = + Vector.rotateVectorAroundAxis( + cell.getApicalAxis(), + Direction.XY_PLANE.vector, + StemType.MUDMUT.splitDirectionRotation); + Voxel splitVoxel = getCellSplitVoxel(StemType.MUDMUT, cell, defaultNormal); + return new Plane(new Double3D(splitVoxel.x, splitVoxel.y, splitVoxel.z), defaultNormal); + } + + /** + * Gets the voxel location the cell's plane of division will pass through. + * + * @param cell the {@link PottsCellFlyStem} to get the division location for + * @return the voxel location where the cell will split + */ + public static Voxel getCellSplitVoxel( + StemType stemType, PottsCellFlyStem cell, Vector rotatedNormalVector) { + ArrayList splitOffsetPercent = new ArrayList<>(); + splitOffsetPercent.add(stemType.splitOffsetPercentX); + splitOffsetPercent.add(stemType.splitOffsetPercentY); + return ((PottsLocation2D) cell.getLocation()) + .getOffsetInApicalFrame(splitOffsetPercent, rotatedNormalVector); + } + + /** + * Determines whether the daughter cell should be a neuroblast or a GMC according to the type of + * cell this module is attached to, the differentiation ruleset specified in the parameters, and + * the morphologies of the daughter cell locations. + * + * @param loc1 one cell location post division + * @param loc2 the other cell location post division + * @return whether or not the daughter cell should be a stem cell + */ + private boolean daughterStemRuleBasedDifferentiation(PottsLocation loc1, PottsLocation loc2) { + if (((PottsCellFlyStem) cell).getStemType() == StemType.WT) { + return false; + } else if (((PottsCellFlyStem) cell).getStemType() == StemType.MUDMUT) { + if (differentiationRuleset.equals("volume")) { + double vol1 = loc1.getVolume(); + double vol2 = loc2.getVolume(); + if (Math.abs(vol1 - vol2) < range) { + return true; + } else { + return false; + } + } else if (differentiationRuleset.equals("location")) { + double[] centroid1 = loc1.getCentroid(); + double[] centroid2 = loc2.getCentroid(); + return (centroidsWithinRangeAlongApicalAxis( + centroid1, centroid2, ((PottsCellFlyStem) cell).getApicalAxis(), range)); + } + } + throw new IllegalArgumentException( + "Invalid differentiation ruleset: " + differentiationRuleset); + } + + /* + * Determines whether the daughter cell should be a neuroblast or a GMC according to the orientation. + * This is deterministic. + * + * @param divisionPlane + * @return {@code true} if the daughter should be a stem cell. {@code false} if the daughter should be a GMC. + */ + private boolean daughterStemDeterministic(Plane divisionPlane) { + + Vector normalVector = divisionPlane.getUnitNormalVector(); + + Vector apicalAxis = ((PottsCellFlyStem) cell).getApicalAxis(); + Vector expectedMUDNormalVector = + Vector.rotateVectorAroundAxis( + apicalAxis, + Direction.XY_PLANE.vector, + StemType.MUDMUT.splitDirectionRotation); + // If TRUE, the daughter should be stem. Otherwise, should be GMC + return Math.abs(normalVector.getX() - expectedMUDNormalVector.getX()) <= EPSILON + && Math.abs(normalVector.getY() - expectedMUDNormalVector.getY()) <= EPSILON + && Math.abs(normalVector.getZ() - expectedMUDNormalVector.getZ()) <= EPSILON; + } + + /** + * Determines whether a daughter cell should remain a stem cell or differentiate into a GMC. + * + *

This method serves as a wrapper that delegates to either a deterministic or rule-based + * differentiation mechanism depending on the value of {@code hasDeterministicDifferentiation}. + * + * @param parentsLoc the location of the parent cell before division + * @param daughterLoc the location of the daughter cell after division + * @param divisionPlane the plane of division for the daughter cell + * @return {@code true} if the daughter should remain a stem cell; {@code false} if it should be + * a GMC + */ + public boolean daughterStem( + PottsLocation2D parentsLoc, PottsLocation daughterLoc, Plane divisionPlane) { + return hasDeterministicDifferentiation + ? daughterStemDeterministic(divisionPlane) + : daughterStemRuleBasedDifferentiation(parentsLoc, daughterLoc); + } + + /** + * Determines if the distance between two centroids, projected along the apical axis, is less + * than or equal to the given range. + * + * @param centroid1 First centroid position. + * @param centroid2 Second centroid position. + * @param apicalAxis Unit {@link Vector} defining the apical-basal direction. + * @param range Maximum allowed distance along the apical axis. + * @return true if the centroids are within the given range along the apical axis. + */ + static boolean centroidsWithinRangeAlongApicalAxis( + double[] centroid1, double[] centroid2, Vector apicalAxis, double range) { + + Vector c1 = new Vector(centroid1[0], centroid1[1], centroid1.length > 2 ? centroid1[2] : 0); + Vector c2 = new Vector(centroid2[0], centroid2[1], centroid2.length > 2 ? centroid2[2] : 0); + + double proj1 = Vector.dotProduct(c1, apicalAxis); + double proj2 = Vector.dotProduct(c2, apicalAxis); + + double distanceAlongAxis = Math.abs(proj1 - proj2); + + return distanceAlongAxis - range <= EPSILON; + } + + /** + * Makes a daughter NB cell + * + * @param daughterLoc the location of the daughter NB cell + * @param sim the simulation + * @param potts the potts instance for this simulation + * @param random the random number generator + */ + private void makeDaughterStemCell( + PottsLocation daughterLoc, Simulation sim, Potts potts, MersenneTwisterFast random) { + int newID = sim.getID(); + double criticalVol; + if (volumeBasedCriticalVolume) { + criticalVol = Math.max(daughterLoc.getVolume(), initialSize * .5); + cell.setCriticalVolume(criticalVol); + } else { + criticalVol = cell.getCriticalVolume(); + } + cell.reset(potts.ids, potts.regions); + PottsCellContainer container = + ((PottsCellFlyStem) cell) + .make(newID, State.PROLIFERATIVE, random, cell.getPop(), criticalVol); + scheduleNewCell(container, daughterLoc, sim, potts, random); + } + + /** + * Makes a daughter GMC cell + * + * @param parentLoc the location of the parent NB cell + * @param daughterLoc the location of the daughter GMC cell + * @param sim the simulation + * @param potts the potts instance for this simulation + * @param random the random number generator + * @param divisionPlaneNormal the normal vector to the plane of division + */ + private void makeDaughterGMC( + PottsLocation parentLoc, + PottsLocation daughterLoc, + Simulation sim, + Potts potts, + MersenneTwisterFast random, + Vector divisionPlaneNormal) { + Location gmcLoc = determineGMCLocation(parentLoc, daughterLoc, divisionPlaneNormal); + + if (parentLoc == gmcLoc) { + PottsLocation.swapVoxels(parentLoc, daughterLoc); + } + cell.reset(potts.ids, potts.regions); + int newID = sim.getID(); + int newPop = ((PottsCellFlyStem) cell).getLinks().next(random); + double criticalVolume = calculateGMCDaughterCellCriticalVolume((PottsLocation) daughterLoc); + PottsCellContainer container = + ((PottsCellFlyStem) cell) + .make(newID, State.PROLIFERATIVE, random, newPop, criticalVolume); + scheduleNewCell(container, daughterLoc, sim, potts, random); + } + + /** + * Adds a new cell to the simulation grid and schedule. Resets the parent cell. + * + * @param container the daughter cell's container + * @param daughterLoc the daughter cell's location + * @param sim the simulation + * @param potts the potts instance for this simulation + * @param random the random number generator + */ + private void scheduleNewCell( + PottsCellContainer container, + PottsLocation daughterLoc, + Simulation sim, + Potts potts, + MersenneTwisterFast random) { + PottsCell newCell = + (PottsCell) container.convert(sim.getCellFactory(), daughterLoc, random); + if (newCell.getClass() == PottsCellFlyStem.class) { + ((PottsCellFlyStem) newCell).setApicalAxis(getDaughterCellApicalAxis(random)); + } + sim.getGrid().addObject(newCell, null); + potts.register(newCell); + newCell.reset(potts.ids, potts.regions); + newCell.schedule(sim.getSchedule()); + } + + /** + * Gets the apical axis of the daughter cell according to the apicalAxisRuleset specified in the + * parameters. + * + * @param random the random number generator + * @return the daughter cell's apical axis + */ + public Vector getDaughterCellApicalAxis(MersenneTwisterFast random) { + switch (apicalAxisRuleset) { + case "uniform": + if (!(apicalAxisRotationDistribution instanceof UniformDistribution)) { + throw new IllegalArgumentException( + "apicalAxisRotationDistribution must be a UniformDistribution under the uniform apical axis ruleset."); + } + Vector newRandomApicalAxis = + Vector.rotateVectorAroundAxis( + ((PottsCellFlyStem) cell).getApicalAxis(), + Direction.XY_PLANE.vector, + apicalAxisRotationDistribution.nextDouble()); + return newRandomApicalAxis; + case "global": + return ((PottsCellFlyStem) cell).getApicalAxis(); + case "normal": + if (!(apicalAxisRotationDistribution instanceof NormalDistribution)) { + throw new IllegalArgumentException( + "apicalAxisRotationDistribution must be a NormalDistribution under the rotation apical axis ruleset."); + } + Vector newRotatedApicalAxis = + Vector.rotateVectorAroundAxis( + ((PottsCellFlyStem) cell).getApicalAxis(), + Direction.XY_PLANE.vector, + apicalAxisRotationDistribution.nextDouble()); + return newRotatedApicalAxis; + default: + throw new IllegalArgumentException( + "Invalid apical axis ruleset: " + apicalAxisRuleset); + } + } + + /** + * Determines between two locations which will be the GMC and which will be the NB according to + * differentiation rules specified in the parameters. + * + * @param parentLoc the parent cell location + * @param daughterLoc the daughter cell location + * @param divisionPlaneNormal the normal vector to the plane of division + * @return the location that should be the GMC + */ + private Location determineGMCLocation( + PottsLocation parentLoc, PottsLocation daughterLoc, Vector divisionPlaneNormal) { + switch (differentiationRuleset) { + case "volume": + return getSmallerLocation(parentLoc, daughterLoc); + case "location": + return getBasalLocation(parentLoc, daughterLoc, divisionPlaneNormal); + default: + throw new IllegalArgumentException( + "Invalid differentiation ruleset: " + differentiationRuleset); + } + } + + /** + * Calculates the critical volume of a GMC daughter cell + * + * @param gmcLoc the location of the GMC daughter cell + * @return the critical volume of the GMC daughter cell + */ + protected double calculateGMCDaughterCellCriticalVolume(PottsLocation gmcLoc) { + double criticalVol; + if (volumeBasedCriticalVolume) { + criticalVol = Math.max(gmcLoc.getVolume(), initialSize * .1); + return criticalVol; + } else { + criticalVol = + ((PottsCellFlyStem) cell).getCriticalVolume() + * sizeTarget + * StemType.WT.daughterCellCriticalVolumeProportion; + return criticalVol; + } + } + + /** + * Gets the smaller location with fewer voxels and returns it. + * + * @param loc1 the {@link PottsLocation} to compare to location2. + * @param loc2 {@link PottsLocation} to compare to location1. + * @return the smaller location. + */ + public static PottsLocation getSmallerLocation(PottsLocation loc1, PottsLocation loc2) { + return (loc1.getVolume() < loc2.getVolume()) ? loc1 : loc2; + } + + /** + * Gets the location that is lower along the apical axis. + * + * @param loc1 {@link PottsLocation} to compare. + * @param loc2 {@link PottsLocation} to compare. + * @param apicalAxis Unit {@link Vector} defining the apical-basal direction. + * @return the basal location (lower along the apical axis). + */ + public static PottsLocation getBasalLocation( + PottsLocation loc1, PottsLocation loc2, Vector apicalAxis) { + double[] centroid1 = loc1.getCentroid(); + double[] centroid2 = loc2.getCentroid(); + Vector c1 = new Vector(centroid1[0], centroid1[1], centroid1.length > 2 ? centroid1[2] : 0); + Vector c2 = new Vector(centroid2[0], centroid2[1], centroid2.length > 2 ? centroid2[2] : 0); + + double proj1 = Vector.dotProduct(c1, apicalAxis); + double proj2 = Vector.dotProduct(c2, apicalAxis); + + return (proj1 < proj2) ? loc2 : loc1; // higher projection = more basal + } + + public HashSet getNBsInSimulation(Simulation sim) { + HashSet nbsInSimulation = new HashSet<>(); + Bag simObjects = sim.getGrid().getAllObjects(); + for (int i = 0; i < simObjects.numObjs; i++) { + Object o = simObjects.objs[i]; + if (!(o instanceof PottsCell)) continue; // skip non-cell objects + PottsCell cellInSim = (PottsCell) o; + if (cell.getPop() == cellInSim.getPop() && o instanceof PottsCellFlyStem) { + nbsInSimulation.add((PottsCellFlyStem) o); + } + } + return nbsInSimulation; + } +} diff --git a/src/arcade/potts/parameter.potts.xml b/src/arcade/potts/parameter.potts.xml index c95d0ce61..c63609158 100644 --- a/src/arcade/potts/parameter.potts.xml +++ b/src/arcade/potts/parameter.potts.xml @@ -70,6 +70,21 @@ + + + + + + + + + + + + + + + diff --git a/src/arcade/potts/sim/Potts.java b/src/arcade/potts/sim/Potts.java index a0b46f0db..a4ffb0ee2 100644 --- a/src/arcade/potts/sim/Potts.java +++ b/src/arcade/potts/sim/Potts.java @@ -398,7 +398,7 @@ public PottsCell getCell(int id) { * @param z the z coordinate * @return the list of unique IDs */ - abstract HashSet getUniqueIDs(int x, int y, int z); + public abstract HashSet getUniqueIDs(int x, int y, int z); /** * Gets unique regions adjacent to given voxel. diff --git a/src/arcade/potts/sim/Potts2D.java b/src/arcade/potts/sim/Potts2D.java index 011d6f850..f6d7dbb6f 100644 --- a/src/arcade/potts/sim/Potts2D.java +++ b/src/arcade/potts/sim/Potts2D.java @@ -151,7 +151,7 @@ private boolean getConnectivityThreeNeighbors(boolean[][] subarray) { } @Override - HashSet getUniqueIDs(int x, int y, int z) { + public HashSet getUniqueIDs(int x, int y, int z) { int id = ids[z][x][y]; HashSet unique = new HashSet<>(); diff --git a/src/arcade/potts/sim/Potts3D.java b/src/arcade/potts/sim/Potts3D.java index 7c4686e8d..7f3429650 100644 --- a/src/arcade/potts/sim/Potts3D.java +++ b/src/arcade/potts/sim/Potts3D.java @@ -465,7 +465,7 @@ private boolean getConnectivityFiveNeighbors(boolean[][][] array) { } @Override - HashSet getUniqueIDs(int x, int y, int z) { + public HashSet getUniqueIDs(int x, int y, int z) { int id = ids[z][x][y]; HashSet unique = new HashSet<>(); diff --git a/test/arcade/potts/agent/cell/PottsCellFlyStemTest.java b/test/arcade/potts/agent/cell/PottsCellFlyStemTest.java new file mode 100644 index 000000000..263a5aee0 --- /dev/null +++ b/test/arcade/potts/agent/cell/PottsCellFlyStemTest.java @@ -0,0 +1,196 @@ +package arcade.potts.agent.cell; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import ec.util.MersenneTwisterFast; +import arcade.core.util.GrabBag; +import arcade.core.util.MiniBox; +import arcade.core.util.Parameters; +import arcade.core.util.Vector; +import arcade.potts.env.location.PottsLocation; +import arcade.potts.util.PottsEnums.Phase; +import arcade.potts.util.PottsEnums.State; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static arcade.core.ARCADETestUtilities.*; + +public class PottsCellFlyStemTest { + static final double EPSILON = 1e-6; + + static MersenneTwisterFast random = new MersenneTwisterFast(); + + static PottsLocation locationMock; + + static Parameters parametersMock; + + static GrabBag links; + + static int cellID = randomIntBetween(1, 10); + + static int cellParent = randomIntBetween(1, 10); + + static int cellPop = randomIntBetween(1, 10); + + static int cellAge = randomIntBetween(1, 1000); + + static int cellDivisions = randomIntBetween(1, 100); + + static double cellCriticalVolume = randomDoubleBetween(10, 100); + + static double cellCriticalHeight = randomDoubleBetween(10, 100); + + static State cellState = State.UNDEFINED; + + static PottsCellContainer baseContainer; + + @BeforeEach + public final void setupMocks() { + locationMock = mock(PottsLocation.class); + parametersMock = spy(new Parameters(new MiniBox(), null, null)); + links = new GrabBag(); + links.add(1, 1); + + doReturn(0.0).when(parametersMock).getDouble(any()); + doReturn(0).when(parametersMock).getInt(any()); + + baseContainer = + new PottsCellContainer( + cellID, + cellParent, + cellPop, + cellAge, + cellDivisions, + cellState, + null, + 0, + cellCriticalVolume, + cellCriticalHeight); + } + + @Test + public void constructor_validWTStemType_createsInstance() { + doReturn("fly-stem-wt").when(parametersMock).getString("CLASS"); + PottsCellFlyStem cell = + new PottsCellFlyStem(baseContainer, locationMock, parametersMock, links); + assertNotNull(cell); + assertEquals(PottsCellFlyStem.StemType.WT, cell.stemType); + } + + @Test + public void constructor_validMUDMUTStemType_createsInstance() { + doReturn("fly-stem-mudmut").when(parametersMock).getString("CLASS"); + PottsCellFlyStem cell = + new PottsCellFlyStem(baseContainer, locationMock, parametersMock, links); + assertNotNull(cell); + assertEquals(PottsCellFlyStem.StemType.MUDMUT, cell.stemType); + } + + @Test + public void constructor_invalidStemType_throwsException() { + doReturn("invalid-class").when(parametersMock).getString("CLASS"); + assertThrows( + IllegalArgumentException.class, + () -> new PottsCellFlyStem(baseContainer, locationMock, parametersMock, links)); + } + + @Test + public void make_calledWT_returnsCorrectNewContainer() { + doReturn("fly-stem-wt").when(parametersMock).getString("CLASS"); + PottsCellFlyStem cell = + new PottsCellFlyStem(baseContainer, locationMock, parametersMock, links); + PottsCellContainer container = + cell.make(cellID, State.PROLIFERATIVE, random, cellPop, cellCriticalVolume); + + assertAll( + () -> assertNotNull(container), + () -> assertEquals(cellID, container.parent), + () -> assertEquals(cellPop, container.pop), + () -> assertEquals(cellAge, container.age), + () -> assertEquals(cellDivisions + 1, container.divisions), + () -> assertEquals(State.PROLIFERATIVE, container.state), + () -> assertEquals(container.phase, Phase.UNDEFINED), + () -> assertEquals(0, container.voxels), + () -> assertNull(container.regionVoxels), + () -> assertEquals(cellCriticalVolume, container.criticalVolume, EPSILON), + () -> assertEquals(cellCriticalHeight, container.criticalHeight, EPSILON), + () -> assertNull(container.criticalRegionVolumes), + () -> assertNull(container.criticalRegionHeights)); + } + + @Test + public void make_calledMUDMUT_returnsCorrectNewContainer() { + doReturn("fly-stem-mudmut").when(parametersMock).getString("CLASS"); + PottsCellFlyStem cell = + new PottsCellFlyStem(baseContainer, locationMock, parametersMock, links); + PottsCellContainer container = + cell.make(cellID, State.PROLIFERATIVE, random, cellPop, cellCriticalVolume); + + assertAll( + () -> assertNotNull(container), + () -> assertEquals(cellID, container.parent), + () -> assertEquals(cellPop, container.pop), + () -> assertEquals(cellAge, container.age), + () -> assertEquals(cellDivisions + 1, container.divisions), + () -> assertEquals(State.PROLIFERATIVE, container.state), + () -> assertEquals(container.phase, Phase.UNDEFINED), + () -> assertEquals(0, container.voxels), + () -> assertNull(container.regionVoxels), + () -> assertEquals(cellCriticalVolume, container.criticalVolume), + () -> assertEquals(cellCriticalHeight, container.criticalHeight, EPSILON), + () -> assertNull(container.criticalRegionVolumes), + () -> assertNull(container.criticalRegionHeights)); + } + + @Test + void make_noDaughterCellCriticalVolume_throwsUnsupportedOperationException() { + doReturn("fly-stem-wt").when(parametersMock).getString("CLASS"); + PottsCellFlyStem cell = + new PottsCellFlyStem(baseContainer, locationMock, parametersMock, links); + assertThrows( + UnsupportedOperationException.class, + () -> cell.make(cellID, State.PROLIFERATIVE, random)); + } + + @Test + void setStateModule_called_createsProliferationModuleOrSetsNull() { + doReturn("fly-stem-wt").when(parametersMock).getString("CLASS"); + PottsCellFlyStem cell = + new PottsCellFlyStem(baseContainer, locationMock, parametersMock, links); + for (State state : State.values()) { + if (state != State.PROLIFERATIVE) { + cell.setStateModule(state); + assertNull(cell.getModule()); + } + } + } + + @Test + void getStemType_called_returnsCorrectStemType() { + doReturn("fly-stem-wt").when(parametersMock).getString("CLASS"); + PottsCellFlyStem cell = + new PottsCellFlyStem(baseContainer, locationMock, parametersMock, links); + assertEquals(PottsCellFlyStem.StemType.WT, cell.getStemType()); + doReturn("fly-stem-mudmut").when(parametersMock).getString("CLASS"); + cell = new PottsCellFlyStem(baseContainer, locationMock, parametersMock, links); + assertEquals(PottsCellFlyStem.StemType.MUDMUT, cell.getStemType()); + } + + @Test + void getApicalAxis_notSet_returnsDefault() { + doReturn("fly-stem-wt").when(parametersMock).getString("CLASS"); + PottsCellFlyStem cell = + new PottsCellFlyStem(baseContainer, locationMock, parametersMock, links); + assertEquals(new Vector(0, 1, 0), cell.getApicalAxis()); + } + + @Test + void getApicalAxis_set_returnsStoredAxis() { + doReturn("fly-stem-wt").when(parametersMock).getString("CLASS"); + PottsCellFlyStem cell = + new PottsCellFlyStem(baseContainer, locationMock, parametersMock, links); + Vector custom = new Vector(1, 2, 3); + cell.setApicalAxis(custom); + assertEquals(custom, cell.getApicalAxis()); + } +} diff --git a/test/arcade/potts/agent/module/PottsModuleFlyStemProliferationTest.java b/test/arcade/potts/agent/module/PottsModuleFlyStemProliferationTest.java new file mode 100644 index 000000000..00e6fdc98 --- /dev/null +++ b/test/arcade/potts/agent/module/PottsModuleFlyStemProliferationTest.java @@ -0,0 +1,1449 @@ +package arcade.potts.agent.module; + +import java.security.InvalidParameterException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import sim.util.Bag; +import sim.util.Double3D; +import ec.util.MersenneTwisterFast; +import arcade.core.env.grid.Grid; +import arcade.core.util.GrabBag; +import arcade.core.util.Parameters; +import arcade.core.util.Plane; +import arcade.core.util.Vector; +import arcade.core.util.distributions.NormalDistribution; +import arcade.core.util.distributions.UniformDistribution; +import arcade.potts.agent.cell.PottsCell; +import arcade.potts.agent.cell.PottsCellContainer; +import arcade.potts.agent.cell.PottsCellFactory; +import arcade.potts.agent.cell.PottsCellFlyStem; +import arcade.potts.env.location.PottsLocation; +import arcade.potts.env.location.PottsLocation2D; +import arcade.potts.env.location.Voxel; +import arcade.potts.sim.Potts; +import arcade.potts.sim.PottsSimulation; +import arcade.potts.util.PottsEnums.Phase; +import arcade.potts.util.PottsEnums.State; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyDouble; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.*; +import static arcade.potts.util.PottsEnums.State; + +public class PottsModuleFlyStemProliferationTest { + PottsCellFlyStem stemCell; + + PottsModuleFlyStemProliferation module; + + PottsLocation2D stemLoc; + + PottsLocation daughterLoc; + + Parameters parameters; + + PottsSimulation sim; + + Potts potts; + + Grid grid; + + PottsCellFactory factory; + + MersenneTwisterFast random; + + NormalDistribution dist; + + float EPSILON = 1e-6f; + + int stemCellPop; + + @BeforeEach + public final void setup() { + // Core mocks + stemCell = mock(PottsCellFlyStem.class); + parameters = mock(Parameters.class); + dist = mock(NormalDistribution.class); + sim = mock(PottsSimulation.class); + potts = mock(Potts.class); + grid = mock(Grid.class); + factory = mock(PottsCellFactory.class); + random = mock(MersenneTwisterFast.class); + + // Location mocks + stemLoc = mock(PottsLocation2D.class); + daughterLoc = mock(PottsLocation.class); + + // Wire simulation + when(((PottsSimulation) sim).getPotts()).thenReturn(potts); + potts.ids = new int[1][1][1]; + potts.regions = new int[1][1][1]; + when(sim.getGrid()).thenReturn(grid); + when(sim.getCellFactory()).thenReturn(factory); + when(sim.getSchedule()).thenReturn(mock(sim.engine.Schedule.class)); + when(sim.getID()).thenReturn(42); + + // Wire cell + when(stemCell.getLocation()).thenReturn(stemLoc); + when(stemCell.getParameters()).thenReturn(parameters); + when(stemLoc.split(eq(random), any(Plane.class))).thenReturn(daughterLoc); + + // Default centroid and volume values (sometimes overridden in tests) + when(stemLoc.getVolume()).thenReturn(10.0); + when(daughterLoc.getVolume()).thenReturn(5.0); + when(stemLoc.getCentroid()).thenReturn(new double[] {0, 1.0, 0}); + when(daughterLoc.getCentroid()).thenReturn(new double[] {0, 1.6, 0}); + + // Parameter stubs (sometimes overridden in tests) + when(parameters.getDistribution("proliferation/DIV_ROTATION_DISTRIBUTION")) + .thenReturn(dist); + when(dist.nextDouble()).thenReturn(0.1); + when(parameters.getString("proliferation/DIFFERENTIATION_RULESET")).thenReturn("volume"); + when(parameters.getDouble("proliferation/DIFFERENTIATION_RULESET_EQUALITY_RANGE")) + .thenReturn(0.5); + when(parameters.getString("proliferation/HAS_DETERMINISTIC_DIFFERENTIATION")) + .thenReturn("TRUE"); + + // Link selection + GrabBag links = mock(GrabBag.class); + when(stemCell.getLinks()).thenReturn(links); + when(links.next(random)).thenReturn(2); + + // Other defaults + stemCellPop = 3; + when(stemCell.getPop()).thenReturn(stemCellPop); + when(stemCell.getCriticalVolume()).thenReturn(100.0); + when(stemCell.getStemType()).thenReturn(PottsCellFlyStem.StemType.WT); + when(parameters.getDouble("proliferation/SIZE_TARGET")).thenReturn(1.2); + } + + @AfterEach + final void tearDown() { + Mockito.framework().clearInlineMocks(); + } + + // Constructor tests + + @Test + public void constructor_volumeRuleset_setsExpectedFields() { + when(parameters.getString("proliferation/DIFFERENTIATION_RULESET")).thenReturn("volume"); + when(parameters.getDouble("proliferation/DIFFERENTIATION_RULESET_EQUALITY_RANGE")) + .thenReturn(0.42); + module = new PottsModuleFlyStemProliferation(stemCell); + + assertNotNull(module.splitDirectionDistribution); + assertEquals("volume", module.differentiationRuleset); + assertEquals(0.42, module.range, EPSILON); + assertEquals(arcade.potts.util.PottsEnums.Phase.UNDEFINED, module.phase); + } + + @Test + public void constructor_locationRuleset_setsExpectedFields() { + when(parameters.getString("proliferation/DIFFERENTIATION_RULESET")).thenReturn("location"); + when(parameters.getDouble("proliferation/DIFFERENTIATION_RULESET_EQUALITY_RANGE")) + .thenReturn(0.99); + module = new PottsModuleFlyStemProliferation(stemCell); + + assertNotNull(module.splitDirectionDistribution); + assertEquals("location", module.differentiationRuleset); + assertEquals(0.99, module.range, EPSILON); + assertEquals(arcade.potts.util.PottsEnums.Phase.UNDEFINED, module.phase); + } + + @Test + public void constructor_conflictingDynamicGrowthRateFlags_throwsInvalidParameterException() { + when(parameters.getInt("proliferation/DYNAMIC_GROWTH_RATE_VOLUME")).thenReturn(1); + when(parameters.getInt("proliferation/DYNAMIC_GROWTH_RATE_NB_SELF_REPRESSION")) + .thenReturn(1); + + assertThrows( + InvalidParameterException.class, + () -> new PottsModuleFlyStemProliferation(stemCell)); + } + + @Test + public void + constructor_invalidHasDeterministicDifferentiationString_throwsInvalidParameterException() { + when(parameters.getString("proliferation/HAS_DETERMINISTIC_DIFFERENTIATION")) + .thenReturn("yes"); + + assertThrows( + InvalidParameterException.class, + () -> new PottsModuleFlyStemProliferation(stemCell)); + } + + @Test + public void constructor_cellHasRegions_throwsUnsupportedOperationException() { + when(stemCell.hasRegions()).thenReturn(true); + + assertThrows( + UnsupportedOperationException.class, + () -> new PottsModuleFlyStemProliferation(stemCell)); + } + + // Static method tests + + @Test + public void getSmallerLocation_locationsDifferentSizes_returnsSmallerLocation() { + PottsLocation loc1 = mock(PottsLocation.class); + PottsLocation loc2 = mock(PottsLocation.class); + when(loc1.getVolume()).thenReturn(5.0); + when(loc2.getVolume()).thenReturn(10.0); + + PottsLocation result = PottsModuleFlyStemProliferation.getSmallerLocation(loc1, loc2); + assertEquals(loc1, result); + } + + @Test + public void getSmallerLocation_locationsSameSize_returnsSecondLocation() { + PottsLocation loc1 = mock(PottsLocation.class); + PottsLocation loc2 = mock(PottsLocation.class); + when(loc1.getVolume()).thenReturn(10.0); + when(loc2.getVolume()).thenReturn(10.0); + + PottsLocation result = PottsModuleFlyStemProliferation.getSmallerLocation(loc1, loc2); + assertEquals(loc2, result); + } + + @Test + public void getBasalLocation_centroidsDifferent_returnsBasalCentroid() { + PottsLocation loc1 = mock(PottsLocation.class); + PottsLocation loc2 = mock(PottsLocation.class); + when(loc1.getCentroid()).thenReturn(new double[] {0, 2, 0}); + when(loc2.getCentroid()).thenReturn(new double[] {0, 1, 0}); + Vector apicalAxis = new Vector(0, 1, 0); + + PottsLocation result = + PottsModuleFlyStemProliferation.getBasalLocation(loc1, loc2, apicalAxis); + assertEquals(loc1, result); + } + + @Test + public void getBasalLocation_centroidsSame_returnsFirstLocation() { + PottsLocation loc1 = mock(PottsLocation.class); + PottsLocation loc2 = mock(PottsLocation.class); + when(loc1.getCentroid()).thenReturn(new double[] {0, 2, 0}); + when(loc2.getCentroid()).thenReturn(new double[] {0, 2, 0}); + Vector apicalAxis = new Vector(0, 1, 0); + + PottsLocation result = + PottsModuleFlyStemProliferation.getBasalLocation(loc1, loc2, apicalAxis); + assertEquals(loc1, result); + } + + @Test + public void centroidsWithinRangeAlongApicalAxis_withinRange_returnsTrue() { + double[] centroid1 = new double[] {0, 1.0, 0}; + double[] centroid2 = new double[] {0, 1.3, 0}; + Vector apicalAxis = new Vector(0, 1, 0); // projecting along y-axis + double range = 0.5; + + module = new PottsModuleFlyStemProliferation(stemCell); + boolean result = + PottsModuleFlyStemProliferation.centroidsWithinRangeAlongApicalAxis( + centroid1, centroid2, apicalAxis, range); + + assertTrue(result); + } + + @Test + public void centroidsWithinRangeAlongApicalAxis_equalToRange_returnsTrue() { + double[] centroid1 = new double[] {0, 1.0, 0}; + double[] centroid2 = new double[] {0, 1.5, 0}; + Vector apicalAxis = new Vector(0, 1, 0); + double range = 0.5; + + module = new PottsModuleFlyStemProliferation(stemCell); + boolean result = + PottsModuleFlyStemProliferation.centroidsWithinRangeAlongApicalAxis( + centroid1, centroid2, apicalAxis, range); + + assertTrue(result); + } + + @Test + public void centroidsWithinRangeAlongApicalAxis_outsideRange_returnsFalse() { + double[] centroid1 = new double[] {0, 1.0, 0}; + double[] centroid2 = new double[] {0, 1.6, 0}; + Vector apicalAxis = new Vector(0, 1, 0); + double range = 0.5; + + module = new PottsModuleFlyStemProliferation(stemCell); + boolean result = + PottsModuleFlyStemProliferation.centroidsWithinRangeAlongApicalAxis( + centroid1, centroid2, apicalAxis, range); + + assertFalse(result); + } + + @Test + public void centroidsWithinRangeAlongApicalAxis_nonYAxis_returnsCorrectly() { + double[] centroid1 = new double[] {1.0, 0.0, 0.0}; + double[] centroid2 = new double[] {1.6, 0.0, 0.0}; + Vector apicalAxis = new Vector(1, 0, 0); // projecting along x-axis + double range = 0.6; + + module = new PottsModuleFlyStemProliferation(stemCell); + boolean result = + PottsModuleFlyStemProliferation.centroidsWithinRangeAlongApicalAxis( + centroid1, centroid2, apicalAxis, range); + + assertTrue(result); + } + + // Split location tests + + @Test + public void getCellSplitVoxel_WT_callsLocationOffsetWithCorrectParams() { + ArrayList expectedOffset = new ArrayList<>(); + expectedOffset.add(50); // WT.splitOffsetPercentX + expectedOffset.add(86); // WT.splitOffsetPercentY + + when(stemCell.getApicalAxis()).thenReturn(new Vector(0, 1, 0)); + when(stemCell.getLocation()).thenReturn(stemLoc); + when(stemLoc.getOffsetInApicalFrame(eq(expectedOffset), any(Vector.class))) + .thenReturn(new Voxel(0, 0, 0)); + + PottsModuleFlyStemProliferation.getCellSplitVoxel( + PottsCellFlyStem.StemType.WT, stemCell, stemCell.getApicalAxis()); + verify(stemLoc).getOffsetInApicalFrame(eq(expectedOffset), any(Vector.class)); + } + + @Test + public void getCellSplitVoxel_MUDMUT_callsLocationOffsetWithCorrectParams() { + ArrayList expectedOffset = new ArrayList<>(); + expectedOffset.add(50); // MUDMUT.splitOffsetPercentX + expectedOffset.add(50); // MUDMUT.splitOffsetPercentY + + when(stemCell.getApicalAxis()).thenReturn(new Vector(0, 1, 0)); + when(stemCell.getLocation()).thenReturn(stemLoc); + when(stemLoc.getOffsetInApicalFrame(eq(expectedOffset), any(Vector.class))) + .thenReturn(new Voxel(0, 0, 0)); + + PottsModuleFlyStemProliferation.getCellSplitVoxel( + PottsCellFlyStem.StemType.MUDMUT, stemCell, stemCell.getApicalAxis()); + verify(stemLoc).getOffsetInApicalFrame(eq(expectedOffset), any(Vector.class)); + } + + // Division plane tests + + @Test + public void getWTDivisionPlaneWithRotationalVariance_rotatesCorrectlyAndReturnsPlane() { + Vector apicalAxis = new Vector(0, 1, 0); + when(stemCell.getApicalAxis()).thenReturn(apicalAxis); + + double baseRotation = PottsCellFlyStem.StemType.WT.splitDirectionRotation; // 90 + double offsetRotation = -5.0; + + Voxel splitVoxel = new Voxel(3, 4, 5); + ArrayList expectedOffset = new ArrayList<>(); + expectedOffset.add(50); // WT x offset percent + expectedOffset.add(86); // WT y offset percent + + module = new PottsModuleFlyStemProliferation(stemCell); + + // Apply both rotations manually to get expected result + Vector afterBaseRotation = + Vector.rotateVectorAroundAxis(apicalAxis, new Vector(0, 0, 1), baseRotation); + Vector expectedNormal = + Vector.rotateVectorAroundAxis( + afterBaseRotation, new Vector(0, 0, 1), offsetRotation); + + when(stemLoc.getOffsetInApicalFrame(any(), eq(expectedNormal))).thenReturn(splitVoxel); + + Plane result = module.getWTDivisionPlaneWithRotationalVariance(stemCell, offsetRotation); + + Double3D refPoint = result.getReferencePoint(); + assertEquals(3.0, refPoint.x, EPSILON); + assertEquals(4.0, refPoint.y, EPSILON); + assertEquals(5.0, refPoint.z, EPSILON); + + Vector resultNormal = result.getUnitNormalVector(); + assertEquals(expectedNormal.getX(), resultNormal.getX(), EPSILON); + assertEquals(expectedNormal.getY(), resultNormal.getY(), EPSILON); + assertEquals(expectedNormal.getZ(), resultNormal.getZ(), EPSILON); + } + + @Test + public void getMUDDivisionPlane_returnsRotatedPlaneWithCorrectNormal() { + Vector apicalAxis = new Vector(0, 1, 0); + when(stemCell.getApicalAxis()).thenReturn(apicalAxis); + + Vector expectedNormal = new Vector(1.0, 0.0, 0.0); + + Voxel splitVoxel = new Voxel(7, 8, 9); + ArrayList expectedOffset = new ArrayList<>(); + expectedOffset.add(50); // MUDMUT x offset percent + expectedOffset.add(50); // MUDMUT y offset percent + when(stemLoc.getOffsetInApicalFrame(any(), any())).thenReturn(splitVoxel); + + module = new PottsModuleFlyStemProliferation(stemCell); + Plane result = module.getMUDDivisionPlane(stemCell); + + assertEquals(new Double3D(7, 8, 9), result.getReferencePoint()); + Vector resultNormal = result.getUnitNormalVector(); + assertEquals(expectedNormal.getX(), resultNormal.getX(), EPSILON); + assertEquals(expectedNormal.getY(), resultNormal.getY(), EPSILON); + assertEquals(expectedNormal.getZ(), resultNormal.getZ(), EPSILON); + } + + @Test + public void sampleDivisionPlaneOffset_callsNextDoubleOnDistribution() { + when(dist.nextDouble()).thenReturn(12.34); + + module = new PottsModuleFlyStemProliferation(stemCell); + double offset = module.sampleDivisionPlaneOffset(); + + assertEquals(12.34, offset, EPSILON); + } + + @Test + public void chooseDivisionPlane_WT_callsWTVariant() { + when(stemCell.getStemType()).thenReturn(PottsCellFlyStem.StemType.WT); + when(dist.nextDouble()).thenReturn(12.0); // this can be any value + + module = spy(new PottsModuleFlyStemProliferation(stemCell)); + + Plane expectedPlane = mock(Plane.class); + doReturn(expectedPlane) + .when(module) + .getWTDivisionPlaneWithRotationalVariance(stemCell, 12.0); + + Plane result = module.chooseDivisionPlane(stemCell); + + assertEquals(expectedPlane, result); + verify(module).getWTDivisionPlaneWithRotationalVariance(stemCell, 12.0); + verify(module, never()).getMUDDivisionPlane(any()); + } + + @Test + public void chooseDivisionPlane_MUDMUT_withLowOffset_callsWTVariant() { + when(stemCell.getStemType()).thenReturn(PottsCellFlyStem.StemType.MUDMUT); + when(dist.nextDouble()).thenReturn(10.0); // abs(offset) < 45 → WT logic + + module = spy(new PottsModuleFlyStemProliferation(stemCell)); + + Plane expectedPlane = mock(Plane.class); + doReturn(expectedPlane) + .when(module) + .getWTDivisionPlaneWithRotationalVariance(stemCell, 10.0); + + Plane result = module.chooseDivisionPlane(stemCell); + + assertEquals(expectedPlane, result); + verify(module).getWTDivisionPlaneWithRotationalVariance(stemCell, 10.0); + verify(module, never()).getMUDDivisionPlane(any()); + } + + @Test + public void chooseDivisionPlane_MUDMUT_withHighOffset_callsMUDVariant() { + when(stemCell.getStemType()).thenReturn(PottsCellFlyStem.StemType.MUDMUT); + when(dist.nextDouble()).thenReturn(60.0); // abs(offset) ≥ 45 → MUD logic + + module = spy(new PottsModuleFlyStemProliferation(stemCell)); + + Plane expectedPlane = mock(Plane.class); + doReturn(expectedPlane).when(module).getMUDDivisionPlane(stemCell); + + Plane result = module.chooseDivisionPlane(stemCell); + + assertEquals(expectedPlane, result); + verify(module).getMUDDivisionPlane(stemCell); + verify(module, never()).getWTDivisionPlaneWithRotationalVariance(any(), anyDouble()); + } + + // Step tests + @Test + public void step_volumeBelowCheckpoint_updatesTargetdoesNotDividePhaseStaysUndefined() { + when(parameters.getInt("proliferation/DYNAMIC_GROWTH_RATE_VOLUME")).thenReturn(0); + when(parameters.getDouble("proliferation/CELL_GROWTH_RATE")).thenReturn(4.0); + when(parameters.getDouble("proliferation/SIZE_TARGET")).thenReturn(1.2); + when(stemCell.getCriticalVolume()).thenReturn(100.0); + when(stemLoc.getVolume()).thenReturn(50.0); // 50 < 1.2 * 100 → below checkpoint + + module = new PottsModuleFlyStemProliferation(stemCell); + + module.step(random, sim); + + verify(stemCell).updateTarget(eq(4.0), anyDouble()); + // Checking functions within addCell are never called + // (checking addCell directly would require making module a mock) + verify(sim, never()).getPotts(); + verify(grid, never()).addObject(any(), any()); + verify(potts, never()).register(any()); + assertEquals(Phase.UNDEFINED, module.phase); + } + + @Test + public void step_volumeAtCheckpoint_callsAddCellPhaseStaysUndefined() { + // Trigger division + when(parameters.getInt("proliferation/DYNAMIC_GROWTH_RATE_VOLUME")).thenReturn(0); + when(parameters.getDouble("proliferation/CELL_GROWTH_RATE")).thenReturn(4.0); + when(parameters.getDouble("proliferation/SIZE_TARGET")).thenReturn(1.2); + when(stemCell.getCriticalVolume()).thenReturn(100.0); + when(stemCell.getVolume()).thenReturn(120.0); // ≥ 1.2 * 100 + + // Needed by calculateGMCDaughterCellCriticalVolume(...) + when(stemCell.getStemType()).thenReturn(PottsCellFlyStem.StemType.WT); + + // Plane/voxel path (chooseDivisionPlane -> WT -> + // getWTDivisionPlaneWithRotationalVariance) + when(parameters.getString("proliferation/APICAL_AXIS_RULESET")).thenReturn("global"); + when(stemCell.getApicalAxis()).thenReturn(new Vector(0, 1, 0)); + when(stemLoc.getOffsetInApicalFrame(any(), any(Vector.class))) + .thenReturn(new Voxel(1, 2, 3)); + + // Differentiation rule + when(parameters.getString("proliferation/DIFFERENTIATION_RULESET")).thenReturn("volume"); + when(parameters.getDouble("proliferation/DIFFERENTIATION_RULESET_EQUALITY_RANGE")) + .thenReturn(0.5); + + // Cell creation path used by scheduleNewCell(...) + PottsCellContainer container = mock(PottsCellContainer.class); + PottsCellFlyStem newCell = mock(PottsCellFlyStem.class); + when(stemCell.make(anyInt(), eq(State.PROLIFERATIVE), eq(random), anyInt(), anyDouble())) + .thenReturn(container); + when(container.convert(eq(factory), eq(daughterLoc), eq(random))).thenReturn(newCell); + + // split(...) inside addCell + when(stemLoc.split(eq(random), any(Plane.class))).thenReturn(daughterLoc); + + module = new PottsModuleFlyStemProliferation(stemCell); + module.step(random, sim); + + verify(stemCell).updateTarget(eq(4.0), anyDouble()); + verify(stemLoc).split(eq(random), any(Plane.class)); // addCell ran + verify(grid).addObject(any(), isNull()); // scheduled new cell + verify(potts).register(any()); // registered new cell + assertEquals(Phase.UNDEFINED, module.phase); // remains UNDEFINED + } + + // Apical axis rule tests + + @Test + public void getDaughterCellApicalAxis_global_returnsApicalAxis() { + Vector expectedAxis = new Vector(1.0, 2.0, 3.0); + when(parameters.getString("proliferation/APICAL_AXIS_RULESET")).thenReturn("global"); + when(stemCell.getApicalAxis()).thenReturn(expectedAxis); + + module = new PottsModuleFlyStemProliferation(stemCell); + Vector result = module.getDaughterCellApicalAxis(random); + + assertEquals(expectedAxis.getX(), result.getX(), EPSILON); + assertEquals(expectedAxis.getY(), result.getY(), EPSILON); + assertEquals(expectedAxis.getZ(), result.getZ(), EPSILON); + } + + @Test + public void getDaughterCellApicalAxis_rotation_returnsRotatedAxis() { + when(parameters.getString("proliferation/APICAL_AXIS_RULESET")).thenReturn("normal"); + + NormalDistribution rotDist = mock(NormalDistribution.class); + when(rotDist.nextDouble()).thenReturn(30.0); // rotation angle + when(parameters.getDistribution("proliferation/APICAL_AXIS_ROTATION_DISTRIBUTION")) + .thenReturn(rotDist); + + Vector originalAxis = new Vector(0, 1, 0); + when(stemCell.getApicalAxis()).thenReturn(originalAxis); + + module = new PottsModuleFlyStemProliferation(stemCell); + Vector result = module.getDaughterCellApicalAxis(random); + + Vector expected = Vector.rotateVectorAroundAxis(originalAxis, new Vector(0, 0, 1), 30.0); + assertEquals(expected.getX(), result.getX(), EPSILON); + assertEquals(expected.getY(), result.getY(), EPSILON); + assertEquals(expected.getZ(), result.getZ(), EPSILON); + } + + @Test + public void getDaughterCellApicalAxis_rotationwithInvalidDistribution_throwsException() { + when(parameters.getString("proliferation/APICAL_AXIS_RULESET")).thenReturn("rotation"); + when(parameters.getDistribution("proliferation/APICAL_AXIS_ROTATION_DISTRIBUTION")) + .thenReturn(mock(UniformDistribution.class)); + + module = new PottsModuleFlyStemProliferation(stemCell); + assertThrows( + IllegalArgumentException.class, () -> module.getDaughterCellApicalAxis(random)); + } + + @Test + public void getDaughterCellApicalAxis_normalRulesetWithUniformDistribution_throwsException() { + // "rotation" hits the default case; this test exercises the instanceof guard inside + // "normal" + when(parameters.getString("proliferation/APICAL_AXIS_RULESET")).thenReturn("normal"); + when(parameters.getDistribution("proliferation/APICAL_AXIS_ROTATION_DISTRIBUTION")) + .thenReturn(mock(UniformDistribution.class)); + + module = new PottsModuleFlyStemProliferation(stemCell); + assertThrows( + IllegalArgumentException.class, () -> module.getDaughterCellApicalAxis(random)); + } + + @Test + public void getDaughterCellApicalAxis_uniform_returnsRotatedAxis() { + when(parameters.getString("proliferation/APICAL_AXIS_RULESET")).thenReturn("uniform"); + + UniformDistribution rotDist = mock(UniformDistribution.class); + when(rotDist.nextDouble()).thenReturn(200.0); // rotation angle + when(parameters.getDistribution("proliferation/APICAL_AXIS_ROTATION_DISTRIBUTION")) + .thenReturn(rotDist); + + Vector originalAxis = new Vector(0, 1, 0); + when(stemCell.getApicalAxis()).thenReturn(originalAxis); + + module = new PottsModuleFlyStemProliferation(stemCell); + Vector result = module.getDaughterCellApicalAxis(random); + + Vector expected = Vector.rotateVectorAroundAxis(originalAxis, new Vector(0, 0, 1), 200.0); + assertEquals(expected.getX(), result.getX(), EPSILON); + assertEquals(expected.getY(), result.getY(), EPSILON); + assertEquals(expected.getZ(), result.getZ(), EPSILON); + } + + @Test + public void getDaughterCellApicalAxis_uniformwithInvalidDistribution_throwsException() { + when(parameters.getString("proliferation/APICAL_AXIS_RULESET")).thenReturn("uniform"); + when(parameters.getDistribution("proliferation/APICAL_AXIS_ROTATION_DISTRIBUTION")) + .thenReturn(mock(NormalDistribution.class)); + + module = new PottsModuleFlyStemProliferation(stemCell); + assertThrows( + IllegalArgumentException.class, () -> module.getDaughterCellApicalAxis(random)); + } + + // Critical volume calculation tests + + @Test + public void calculateGMCDaughterCellCriticalVolume_volumeBasedOff_returnsMaxCritVol() { + when(stemCell.getCriticalVolume()).thenReturn(100.0); + when(stemCell.getStemType()).thenReturn(PottsCellFlyStem.StemType.WT); + when(parameters.getDouble("proliferation/SIZE_TARGET")).thenReturn(1.2); + // WT has proportion = 0.2 + + module = new PottsModuleFlyStemProliferation(stemCell); + when(parameters.getInt("proliferation/VOLUME_BASED_CRITVOL")).thenReturn(0); + + double result = module.calculateGMCDaughterCellCriticalVolume(daughterLoc); + assertEquals((100 * .14 * 1.2), result, EPSILON); // 100 * 0.14 * 1.2 + } + + @Test + public void calculateGMCDaughterCellCriticalVolume_volumeBasedOn_returnsLocVolume() { + PottsLocation gmcLoc = mock(PottsLocation.class); + when(gmcLoc.getVolume()).thenReturn(50.0); + when(parameters.getInt("proliferation/VOLUME_BASED_CRITICAL_VOLUME")).thenReturn(1); + + module = new PottsModuleFlyStemProliferation(stemCell); + + double result = module.calculateGMCDaughterCellCriticalVolume(gmcLoc); + assertEquals(50.0, result, EPSILON); + } + + @Test + public void + calculateGMCDaughterCellCriticalVolume_volumeBasedOnVerySmallVolume_returnsFloor() { + // @BeforeEach: stemLoc.getVolume()=10.0, so initialSize=10.0 → floor = 10.0 * 0.1 = 1.0 + // gmcLoc.getVolume()=0.5 < 1.0, so Math.max picks the floor + when(parameters.getInt("proliferation/VOLUME_BASED_CRITICAL_VOLUME")).thenReturn(1); + + module = new PottsModuleFlyStemProliferation(stemCell); + + PottsLocation gmcLoc = mock(PottsLocation.class); + when(gmcLoc.getVolume()).thenReturn(0.5); + + double result = module.calculateGMCDaughterCellCriticalVolume(gmcLoc); + assertEquals(1.0, result, EPSILON); // initialSize * 0.1 = 10.0 * 0.1 + } + + // addCell integration tests + + @Test + public void addCell_WTVolumeSwap_swapsVoxelsAndCreatesNewCell() { + when(stemCell.getStemType()).thenReturn(PottsCellFlyStem.StemType.WT); + when(parameters.getString("proliferation/APICAL_AXIS_RULESET")).thenReturn("global"); + when(parameters.getString("proliferation/HAS_DETERMINISTIC_DIFFERENTIATION")) + .thenReturn("FALSE"); + when(stemCell.getApicalAxis()).thenReturn(new Vector(0, 1, 0)); + when(parameters.getDouble("proliferation/SIZE_TARGET")).thenReturn(1.0); + when(parameters.getInt("proliferation/VOLUME_BASED_CRITICAL_VOLUME")).thenReturn(0); + + // parent smaller than daughter -> rule-based 'volume' says parent is GMC -> + // triggers swap + when(stemLoc.getVolume()).thenReturn(5.0); + when(daughterLoc.getVolume()).thenReturn(10.0); + + Plane dummyPlane = mock(Plane.class); + when(dummyPlane.getUnitNormalVector()).thenReturn(new Vector(1, 0, 0)); + when(stemLoc.split(eq(random), eq(dummyPlane))).thenReturn(daughterLoc); + + PottsCellContainer container = mock(PottsCellContainer.class); + PottsCellFlyStem newStemCell = mock(PottsCellFlyStem.class); + when(stemCell.make(eq(42), eq(State.PROLIFERATIVE), eq(random), anyInt(), anyDouble())) + .thenReturn(container); + when(container.convert(eq(factory), eq(daughterLoc), eq(random))).thenReturn(newStemCell); + + PottsModuleFlyStemProliferation module = spy(new PottsModuleFlyStemProliferation(stemCell)); + doReturn(0.0).when(module).sampleDivisionPlaneOffset(); + doReturn(dummyPlane) + .when(module) + .getWTDivisionPlaneWithRotationalVariance(eq(stemCell), anyDouble()); + + try (MockedStatic mocked = mockStatic(PottsLocation.class)) { + module.addCell(random, sim); + mocked.verify(() -> PottsLocation.swapVoxels(stemLoc, daughterLoc)); + } + + verify(newStemCell).schedule(any()); + } + + @Test + public void addCell_WTVolumeNoSwap_doesNotSwapVoxelsAndCreatesNewCell() { + when(stemCell.getStemType()).thenReturn(PottsCellFlyStem.StemType.WT); + when(parameters.getString("proliferation/APICAL_AXIS_RULESET")).thenReturn("global"); + when(stemCell.getApicalAxis()).thenReturn(new Vector(0, 1, 0)); + when(parameters.getDouble("proliferation/SIZE_TARGET")).thenReturn(1.0); + when(parameters.getInt("proliferation/VOLUME_BASED_CRITICAL_VOLUME")).thenReturn(0); + + // Set up the condition that parent volume > daughter volume → no swap + when(stemLoc.getVolume()).thenReturn(10.0); + when(daughterLoc.getVolume()).thenReturn(5.0); + + // Stub division plane + Plane dummyPlane = mock(Plane.class); + when(dummyPlane.getUnitNormalVector()).thenReturn(new Vector(1, 0, 0)); + when(stemLoc.split(eq(random), eq(dummyPlane))).thenReturn(daughterLoc); + + // Stub cell creation + PottsCellContainer container = mock(PottsCellContainer.class); + PottsCellFlyStem newStemCell = mock(PottsCellFlyStem.class); + when(stemCell.make( + eq(42), eq(State.PROLIFERATIVE), eq(random), eq(stemCellPop), anyDouble())) + .thenReturn(container); + when(container.convert(eq(factory), eq(daughterLoc), eq(random))).thenReturn(newStemCell); + + // Spy and override division plane logic + PottsModuleFlyStemProliferation module = spy(new PottsModuleFlyStemProliferation(stemCell)); + doReturn(dummyPlane) + .when(module) + .getWTDivisionPlaneWithRotationalVariance(eq(stemCell), anyDouble()); + + try (MockedStatic mocked = mockStatic(PottsLocation.class)) { + module.addCell(random, sim); + mocked.verify(() -> PottsLocation.swapVoxels(any(), any()), never()); + } + verify(newStemCell).schedule(any()); + } + + @Test + public void addCell_MUDMUTOffsetAboveThreshold_createsStemCell() { + when(stemCell.getStemType()).thenReturn(PottsCellFlyStem.StemType.MUDMUT); + + when(parameters.getString("proliferation/DIFFERENTIATION_RULESET")).thenReturn("volume"); + when(parameters.getString("proliferation/APICAL_AXIS_RULESET")).thenReturn("global"); + when(stemCell.getApicalAxis()).thenReturn(new Vector(0, 1, 0)); + when(dist.nextDouble()).thenReturn(60.0); // triggers MUD plane + + sim = mock(PottsSimulation.class); + potts = mock(Potts.class); + factory = mock(PottsCellFactory.class); + grid = mock(Grid.class); + when(sim.getPotts()).thenReturn(potts); + when(sim.getGrid()).thenReturn(grid); + when(sim.getCellFactory()).thenReturn(factory); + when(sim.getSchedule()).thenReturn(mock(sim.engine.Schedule.class)); + when(sim.getID()).thenReturn(42); + potts.ids = new int[1][1][1]; + potts.regions = new int[1][1][1]; + + PottsCellContainer container = mock(PottsCellContainer.class); + PottsCellFlyStem newCell = mock(PottsCellFlyStem.class); + when(stemCell.make(eq(42), eq(State.PROLIFERATIVE), eq(random), eq(stemCellPop), eq(100.0))) + .thenReturn(container); + when(container.convert(eq(factory), eq(daughterLoc), eq(random))).thenReturn(newCell); + when(stemCell.getCriticalVolume()).thenReturn(100.0); + when(stemCell.getPop()).thenReturn(stemCellPop); + + module = spy(new PottsModuleFlyStemProliferation(stemCell)); + Plane dummyPlane = mock(Plane.class); + doReturn(dummyPlane).when(module).getMUDDivisionPlane(eq(stemCell)); + when(stemLoc.split(eq(random), eq(dummyPlane))).thenReturn(daughterLoc); + doReturn(true).when(module).daughterStem(any(), any(), any()); + + module.addCell(random, sim); + + verify(newCell).schedule(any()); + } + + @Test + public void addCell_MUDMUTOffsetBelowThreshold_createsGMCWithVolumeSwap() { + when(stemCell.getStemType()).thenReturn(PottsCellFlyStem.StemType.MUDMUT); + + when(parameters.getString("proliferation/DIFFERENTIATION_RULESET")).thenReturn("volume"); + when(parameters.getString("proliferation/APICAL_AXIS_RULESET")).thenReturn("global"); + when(stemCell.getApicalAxis()).thenReturn(new Vector(0, 1, 0)); + when(dist.nextDouble()).thenReturn(10.0); // below 45 threshold + + when(stemLoc.getVolume()).thenReturn(5.0); + when(daughterLoc.getVolume()).thenReturn(10.0); // triggers swap + + PottsCellContainer container = mock(PottsCellContainer.class); + PottsCellFlyStem newCell = mock(PottsCellFlyStem.class); + when(stemCell.make(eq(42), eq(State.PROLIFERATIVE), eq(random), anyInt(), anyDouble())) + .thenReturn(container); + when(container.convert(eq(factory), eq(daughterLoc), eq(random))).thenReturn(newCell); + when(stemCell.getCriticalVolume()).thenReturn(100.0); + when(stemCell.getPop()).thenReturn(stemCellPop); + + module = spy(new PottsModuleFlyStemProliferation(stemCell)); + Plane dummyPlane = mock(Plane.class); + doReturn(dummyPlane) + .when(module) + .getWTDivisionPlaneWithRotationalVariance(eq(stemCell), anyDouble()); + when(stemLoc.split(eq(random), eq(dummyPlane))).thenReturn(daughterLoc); + doReturn(false).when(module).daughterStem(any(), any(), any()); + + try (MockedStatic mocked = mockStatic(PottsLocation.class)) { + module.addCell(random, sim); + mocked.verify(() -> PottsLocation.swapVoxels(stemLoc, daughterLoc)); + } + + verify(newCell).schedule(any()); + } + + @Test + public void getNBNeighbors_withTwoUniqueStemNeighbors_returnsCorrectSet() { + module = spy(new PottsModuleFlyStemProliferation(stemCell)); + + // Stem voxels (two positions) + ArrayList voxels = new ArrayList<>(); + voxels.add(new Voxel(0, 0, 0)); + voxels.add(new Voxel(1, 0, 0)); + when(stemLoc.getVoxels()).thenReturn(voxels); + + // Unique IDs returned by Potts per voxel + HashSet idsVoxel1 = new HashSet<>(Arrays.asList(10, 11)); + HashSet idsVoxel2 = new HashSet<>(Arrays.asList(11, 12)); // 11 repeats + when(potts.getUniqueIDs(0, 0, 0)).thenReturn(idsVoxel1); + when(potts.getUniqueIDs(1, 0, 0)).thenReturn(idsVoxel2); + + // Neighbors + PottsCellFlyStem nb10 = mock(PottsCellFlyStem.class); + PottsCellFlyStem nb11 = mock(PottsCellFlyStem.class); + PottsCell nb12OtherPop = mock(PottsCell.class); + + when(nb10.getID()).thenReturn(10); + when(nb11.getID()).thenReturn(11); + when(nb12OtherPop.getID()).thenReturn(12); + + // Stem pop matches 3 + when(stemCell.getPop()).thenReturn(stemCellPop); + when(nb10.getPop()).thenReturn(stemCellPop); + when(nb11.getPop()).thenReturn(stemCellPop); + when(nb12OtherPop.getPop()).thenReturn(99); // no match + + when(grid.getObjectAt(10)).thenReturn(nb10); + when(grid.getObjectAt(11)).thenReturn(nb11); + when(grid.getObjectAt(12)).thenReturn(nb12OtherPop); + + when(stemCell.getID()).thenReturn(42); + + HashSet neighbors = module.getNBNeighbors(sim); + + assertEquals(2, neighbors.size(), "Should contain 2 unique matching neighbors (10 and 11)"); + assertTrue(neighbors.contains(nb10)); + assertTrue(neighbors.contains(nb11)); + } + + @Test + public void getNBNeighbors_noMatchingNeighbors_returnsEmptySet() { + module = spy(new PottsModuleFlyStemProliferation(stemCell)); + + ArrayList voxels = new ArrayList<>(); + voxels.add(new Voxel(0, 0, 0)); + when(stemLoc.getVoxels()).thenReturn(voxels); + + HashSet ids = new HashSet<>(Arrays.asList(50)); + when(potts.getUniqueIDs(0, 0, 0)).thenReturn(ids); + + PottsCell nonStemNeighbor = mock(PottsCell.class); + when(nonStemNeighbor.getPop()).thenReturn(99); // not stem pop + when(nonStemNeighbor.getID()).thenReturn(50); + when(grid.getObjectAt(50)).thenReturn(nonStemNeighbor); + + when(stemCell.getPop()).thenReturn(3); + when(stemCell.getID()).thenReturn(42); + + HashSet neighbors = module.getNBNeighbors(sim); + + assertNotNull(neighbors); + assertTrue(neighbors.isEmpty(), "No neighbors should be returned when pops do not match."); + } + + @Test + public void getNBNeighbors_doesNotIncludeSelf() { + module = spy(new PottsModuleFlyStemProliferation(stemCell)); + + ArrayList voxels = new ArrayList<>(); + voxels.add(new Voxel(0, 0, 0)); + when(stemLoc.getVoxels()).thenReturn(voxels); + + // Potts returns this cell's own ID + when(stemCell.getID()).thenReturn(42); + when(stemCell.getPop()).thenReturn(3); + + HashSet ids = new HashSet<>(Arrays.asList(42)); + when(potts.getUniqueIDs(0, 0, 0)).thenReturn(ids); + + when(grid.getObjectAt(42)).thenReturn(stemCell); + + HashSet neighbors = module.getNBNeighbors(sim); + assertTrue(neighbors.isEmpty(), "Self should not be included as a neighbor"); + } + + @Test + public void getNBNeighbors_nullNeighborInGrid_skipsNullAndContinues() { + module = spy(new PottsModuleFlyStemProliferation(stemCell)); + + ArrayList voxels = new ArrayList<>(); + voxels.add(new Voxel(0, 0, 0)); + when(stemLoc.getVoxels()).thenReturn(voxels); + + // Two IDs: one resolves to null, the other to a valid matching neighbor + HashSet ids = new HashSet<>(Arrays.asList(7, 8)); + when(potts.getUniqueIDs(0, 0, 0)).thenReturn(ids); + + when(grid.getObjectAt(7)).thenReturn(null); + + PottsCellFlyStem validNeighbor = mock(PottsCellFlyStem.class); + when(validNeighbor.getID()).thenReturn(8); + when(validNeighbor.getPop()).thenReturn(stemCellPop); + when(grid.getObjectAt(8)).thenReturn(validNeighbor); + + when(stemCell.getPop()).thenReturn(stemCellPop); + when(stemCell.getID()).thenReturn(42); + + HashSet neighbors = module.getNBNeighbors(sim); + + assertEquals( + 1, neighbors.size(), "Null grid entry should be skipped; valid neighbor returned"); + assertTrue(neighbors.contains(validNeighbor)); + } + + @Test + public void getNBsInSimulation_emptyBag_returnsEmptySet() { + Bag bag = new Bag(); // real MASON Bag + when(grid.getAllObjects()).thenReturn(bag); + + module = new PottsModuleFlyStemProliferation(stemCell); + HashSet result = module.getNBsInSimulation(sim); + + assertNotNull(result); + assertTrue(result.isEmpty(), "Empty grid should yield empty set"); + } + + @Test + public void getNBsInSimulation_mixedObjects_returnsOnlyMatchingFlyStems() { + // Arrange: matching NB, non-matching NB, matching non-FlyStem, random object, matching NB + PottsCellFlyStem nbMatch1 = mock(PottsCellFlyStem.class); + when(nbMatch1.getPop()).thenReturn(3); + + PottsCellFlyStem nbOtherPop = mock(PottsCellFlyStem.class); + when(nbOtherPop.getPop()).thenReturn(99); + + PottsCell nonNBButSamePop = mock(PottsCell.class); + when(nonNBButSamePop.getPop()).thenReturn(3); + + Object random = new Object(); + + PottsCellFlyStem nbMatch2 = mock(PottsCellFlyStem.class); + when(nbMatch2.getPop()).thenReturn(3); + + Bag bag = new Bag(); + bag.add(nbMatch1); + bag.add(nbOtherPop); + bag.add(nonNBButSamePop); + bag.add(random); + bag.add(nbMatch2); + when(grid.getAllObjects()).thenReturn(bag); + + when(stemCell.getPop()).thenReturn(3); + + module = new PottsModuleFlyStemProliferation(stemCell); + HashSet result = module.getNBsInSimulation(sim); + + assertEquals(2, result.size(), "Should return exactly the two matching FlyStem NBs"); + assertTrue(result.contains(nbMatch1)); + assertTrue(result.contains(nbMatch2)); + } + + @Test + public void getNBsInSimulation_includesSelfCell() { + // The module's 'cell' has pop = 3 (already stubbed in @BeforeEach) + when(stemCell.getPop()).thenReturn(3); + + // Bag contains: self (FlyStem, pop 3), another FlyStem pop 3, a non-FlyStem pop 3, and a + // random object + PottsCellFlyStem another = mock(PottsCellFlyStem.class); + when(another.getPop()).thenReturn(3); + PottsCell nonFlyStemSamePop = mock(PottsCell.class); + when(nonFlyStemSamePop.getPop()).thenReturn(3); + Object random = new Object(); + + Bag bag = new Bag(); + bag.add(stemCell); // self + bag.add(another); // matching FlyStem + bag.add(nonFlyStemSamePop); // same pop but NOT FlyStem → should be ignored + bag.add(random); // ignored + + when(grid.getAllObjects()).thenReturn(bag); + + module = new PottsModuleFlyStemProliferation(stemCell); + HashSet result = module.getNBsInSimulation(sim); + + assertTrue(result.contains(stemCell), "Result should include the module's own stem cell."); + assertTrue(result.contains(another), "Result should include other matching FlyStem cells."); + assertEquals( + 2, + result.size(), + "Only the two FlyStem cells with matching pop should be returned."); + } + + // computeEquilibriumVolume tests + + @Test + public void computeEquilibriumVolume_WT_returnsExpectedMidpoint() { + // V_div = sizeTarget * critVol = 1.2 * 100 = 120 + // fRetain = WT.splitOffsetPercentY / 100 = 86 / 100 = 0.86 + // V_ref = 120 * (0.86 + 1) / 2 = 120 * 0.93 = 111.6 + module = new PottsModuleFlyStemProliferation(stemCell); + assertEquals(111.6, module.computeEquilibriumVolume(), EPSILON); + } + + @Test + public void computeEquilibriumVolume_MUDMUT_returnsExpectedMidpoint() { + // fRetain = MUDMUT.splitOffsetPercentY / 100 = 50 / 100 = 0.5 + // V_ref = 120 * (0.5 + 1) / 2 = 120 * 0.75 = 90.0 + when(stemCell.getStemType()).thenReturn(PottsCellFlyStem.StemType.MUDMUT); + module = new PottsModuleFlyStemProliferation(stemCell); + assertEquals(90.0, module.computeEquilibriumVolume(), EPSILON); + } + + @Test + public void computeEquilibriumVolume_differentSizeTarget_scalesCorrectly() { + // V_div = 2.0 * 50 = 100; V_ref = 100 * (0.86 + 1) / 2 = 93.0 + when(parameters.getDouble("proliferation/SIZE_TARGET")).thenReturn(2.0); + when(stemCell.getCriticalVolume()).thenReturn(50.0); + module = new PottsModuleFlyStemProliferation(stemCell); + assertEquals(93.0, module.computeEquilibriumVolume(), EPSILON); + } + + @Test + public void computeEquilibriumVolume_differentCritVol_scalesCorrectly() { + // V_div = 1.0 * 200 = 200; V_ref = 200 * (0.86 + 1) / 2 = 186.0 + when(parameters.getDouble("proliferation/SIZE_TARGET")).thenReturn(1.0); + when(stemCell.getCriticalVolume()).thenReturn(200.0); + module = new PottsModuleFlyStemProliferation(stemCell); + assertEquals(186.0, module.computeEquilibriumVolume(), EPSILON); + } + + // updateGrowthRate dispatch tests + + @Test + public void updateGrowthRate_volumeDynamic_callsVolumeBasedMethod() { + when(parameters.getInt("proliferation/DYNAMIC_GROWTH_RATE_VOLUME")).thenReturn(1); + + module = spy(new PottsModuleFlyStemProliferation(stemCell)); + doNothing().when(module).updateVolumeBasedGrowthRate(any()); + + module.updateGrowthRate(sim); + + verify(module, times(1)).updateVolumeBasedGrowthRate(sim); + verify(module, never()).updateGrowthRateBasedOnOtherNBs(any()); + } + + @Test + public void updateGrowthRate_nbSelfRepression_callsNBMethod() { + when(parameters.getInt("proliferation/DYNAMIC_GROWTH_RATE_NB_SELF_REPRESSION")) + .thenReturn(1); + + module = spy(new PottsModuleFlyStemProliferation(stemCell)); + doNothing().when(module).updateGrowthRateBasedOnOtherNBs(any()); + + module.updateGrowthRate(sim); + + verify(module, times(1)).updateGrowthRateBasedOnOtherNBs(sim); + verify(module, never()).updateVolumeBasedGrowthRate(any()); + } + + @Test + public void updateGrowthRate_noFlags_setsCellGrowthRateToBase() { + when(parameters.getDouble("proliferation/CELL_GROWTH_RATE")).thenReturn(7.5); + + module = new PottsModuleFlyStemProliferation(stemCell); + module.updateGrowthRate(sim); + + assertEquals(module.cellGrowthRateBase, module.cellGrowthRate, EPSILON); + assertEquals(7.5, module.cellGrowthRate, EPSILON); + } + + @Test + public void updateVolumeBasedGrowthRate_pdeLikeFalse_usesCellVolumeAndEquilibriumRef() { + // pdeLike = 0 → uses this cell's own volume and computeEquilibriumVolume() as the ref + when(parameters.getInt("proliferation/PDELIKE")).thenReturn(0); + when(parameters.getInt("proliferation/DYNAMIC_GROWTH_RATE_NB_CONTACT")).thenReturn(1); + when(stemLoc.getVolume()).thenReturn(42.5); + + module = spy(new PottsModuleFlyStemProliferation(stemCell)); + doNothing().when(module).updateCellVolumeBasedGrowthRate(anyDouble(), anyDouble()); + + module.updateVolumeBasedGrowthRate(sim); + + // V_ref = sizeTarget * critVol * (WT.splitOffsetPercentY/100 + 1) / 2 + // = 1.2 * 100 * (0.86 + 1) / 2 = 111.6 + verify(module, times(1)).updateCellVolumeBasedGrowthRate(eq(42.5), eq(111.6)); + verify(module, never()).getNBsInSimulation(any()); + } + + @Test + public void updateVolumeBasedGrowthRate_pdeLikeTrue_usesAverageVolumeAndEquilibriumRef() { + // pdeLike = 1 → averages volumes across all NBs; uses computeEquilibriumVolume() as the ref + when(parameters.getInt("proliferation/PDELIKE")).thenReturn(1); + when(parameters.getInt("proliferation/DYNAMIC_GROWTH_RATE_NB_CONTACT")).thenReturn(0); + + module = spy(new PottsModuleFlyStemProliferation(stemCell)); + + PottsCellFlyStem nbA = mock(PottsCellFlyStem.class); + PottsCellFlyStem nbB = mock(PottsCellFlyStem.class); + PottsCellFlyStem nbC = mock(PottsCellFlyStem.class); + + PottsLocation locA = mock(PottsLocation.class); + PottsLocation locB = mock(PottsLocation.class); + PottsLocation locC = mock(PottsLocation.class); + + when(nbA.getLocation()).thenReturn(locA); + when(nbB.getLocation()).thenReturn(locB); + when(nbC.getLocation()).thenReturn(locC); + + // Volumes: 10, 20, 40 → avg = 70/3 + when(locA.getVolume()).thenReturn(10.0); + when(locB.getVolume()).thenReturn(20.0); + when(locC.getVolume()).thenReturn(40.0); + + HashSet allNBs = new HashSet<>(Arrays.asList(nbA, nbB, nbC)); + doReturn(allNBs).when(module).getNBsInSimulation(sim); + doNothing().when(module).updateCellVolumeBasedGrowthRate(anyDouble(), anyDouble()); + + module.updateVolumeBasedGrowthRate(sim); + + double expectedAvgVol = (10.0 + 20.0 + 40.0) / 3.0; + // V_ref = 1.2 * 100 * (0.86 + 1) / 2 = 111.6 (critVol from module's own cell) + double expectedVRef = 111.6; + + verify(module, times(1)).getNBsInSimulation(sim); + verify(module, times(1)) + .updateCellVolumeBasedGrowthRate(eq(expectedAvgVol), eq(expectedVRef)); + } + + @Test + public void updateGrowthRateBasedOnOtherNBs_pdeLikeFalse_usesNeighborsBranch() { + // pdeLike = 0 → neighbors branch + when(parameters.getInt("proliferation/PDELIKE")).thenReturn(0); + when(parameters.getInt("proliferation/DYNAMIC_GROWTH_RATE_NB_CONTACT")).thenReturn(1); + + when(parameters.getDouble("proliferation/NB_CONTACT_HALF_MAX")).thenReturn(4.0); + when(parameters.getDouble("proliferation/NB_CONTACT_HILL_N")).thenReturn(2.0); + when(parameters.getDouble("proliferation/CELL_GROWTH_RATE")).thenReturn(12.0); + + module = spy(new PottsModuleFlyStemProliferation(stemCell)); + + // N = 4 neighbors (K = 4, n = 2 → repression 0.5 → 12 * 0.5 = 6) + HashSet four = new HashSet<>(); + for (int i = 0; i < 4; i++) { + PottsCellFlyStem n = mock(PottsCellFlyStem.class); + when(n.getID()).thenReturn(100 + i); + four.add(n); + } + doReturn(four).when(module).getNBNeighbors(sim); + // Make sure population path is not used + doReturn(new HashSet()).when(module).getNBsInSimulation(sim); + + module.updateGrowthRateBasedOnOtherNBs(sim); + + assertEquals(6.0, module.cellGrowthRate, 1e-6); + verify(module, times(1)).getNBNeighbors(sim); + verify(module, never()).getNBsInSimulation(sim); + } + + @Test + public void updateGrowthRateBasedOnOtherNBs_pdeLikeTrue_usesPopulationBranch() { + // pdeLike = 1 and dynamicGrowthRateNBContact = 0 to avoid constructor exception + when(parameters.getInt("proliferation/PDELIKE")).thenReturn(1); + when(parameters.getInt("proliferation/DYNAMIC_GROWTH_RATE_NB_CONTACT")).thenReturn(0); + + when(parameters.getDouble("proliferation/NB_CONTACT_HALF_MAX")).thenReturn(3.0); + when(parameters.getDouble("proliferation/NB_CONTACT_HILL_N")).thenReturn(2.0); + when(parameters.getDouble("proliferation/CELL_GROWTH_RATE")).thenReturn(20.0); + + module = spy(new PottsModuleFlyStemProliferation(stemCell)); + + // N = 7 in-simulation, 6 neighbors (K = 3, n = 2 → 9/(9+36)=0.2 → 4.0) + HashSet seven = new HashSet<>(); + for (int i = 0; i < 7; i++) { + PottsCellFlyStem n = mock(PottsCellFlyStem.class); + when(n.getID()).thenReturn(200 + i); + seven.add(n); + } + doReturn(new HashSet()).when(module).getNBNeighbors(sim); + doReturn(seven).when(module).getNBsInSimulation(sim); + + module.updateGrowthRateBasedOnOtherNBs(sim); + + assertEquals(4.0, module.cellGrowthRate, 1e-6); + verify(module, times(1)).getNBsInSimulation(sim); + verify(module, never()).getNBNeighbors(sim); + } + + @Test + public void updateGrowthRateBasedOnOtherNBs_KZeroandZeroNeighbors_returnsBase() { + when(parameters.getInt("proliferation/PDELIKE")).thenReturn(0); + when(parameters.getInt("proliferation/DYNAMIC_GROWTH_RATE_NB_CONTACT")).thenReturn(1); + + when(parameters.getDouble("proliferation/NB_CONTACT_HALF_MAX")).thenReturn(0.0); // K = 0 + when(parameters.getDouble("proliferation/NB_CONTACT_HILL_N")).thenReturn(2.0); + when(parameters.getDouble("proliferation/CELL_GROWTH_RATE")).thenReturn(10.0); + + module = spy(new PottsModuleFlyStemProliferation(stemCell)); + + // N = 0 → with your guard, repression = 1.0 when K=0 & N=0 + doReturn(new HashSet()).when(module).getNBNeighbors(sim); + + module.updateGrowthRateBasedOnOtherNBs(sim); + + assertEquals(10.0, module.cellGrowthRate, 1e-6); + } + + @Test + public void updateGrowthRateBasedOnOtherNBs_KZeroandPositiveNeighbors_returnsZero() { + when(parameters.getInt("proliferation/PDELIKE")).thenReturn(0); + when(parameters.getInt("proliferation/DYNAMIC_GROWTH_RATE_NB_CONTACT")).thenReturn(1); + + when(parameters.getDouble("proliferation/NB_CONTACT_HALF_MAX")).thenReturn(0.0); // K = 0 + when(parameters.getDouble("proliferation/NB_CONTACT_HILL_N")).thenReturn(2.0); + when(parameters.getDouble("proliferation/CELL_GROWTH_RATE")).thenReturn(10.0); + + module = spy(new PottsModuleFlyStemProliferation(stemCell)); + + // N > 0 → with your guard, repression = 0.0 when K=0 & N>0 + HashSet one = new HashSet<>(); + PottsCellFlyStem n = mock(PottsCellFlyStem.class); + when(n.getID()).thenReturn(999); + one.add(n); + doReturn(one).when(module).getNBNeighbors(sim); + + module.updateGrowthRateBasedOnOtherNBs(sim); + + assertEquals(0.0, module.cellGrowthRate, 1e-9); + } + + @Test + public void updateGrowthRateBasedOnOtherNBs_hillExponentOne_linearCase() { + when(parameters.getInt("proliferation/PDELIKE")).thenReturn(0); + when(parameters.getInt("proliferation/DYNAMIC_GROWTH_RATE_NB_CONTACT")).thenReturn(1); + + when(parameters.getDouble("proliferation/NB_CONTACT_HALF_MAX")).thenReturn(4.0); + when(parameters.getDouble("proliferation/NB_CONTACT_HILL_N")).thenReturn(1.0); // linear + when(parameters.getDouble("proliferation/CELL_GROWTH_RATE")).thenReturn(10.0); + + module = spy(new PottsModuleFlyStemProliferation(stemCell)); + + // N = 2 → R = K/(K+N) = 4/(4+2) = 2/3 + HashSet two = new HashSet<>(); + for (int i = 0; i < 2; i++) { + PottsCellFlyStem nn = mock(PottsCellFlyStem.class); + when(nn.getID()).thenReturn(300 + i); + two.add(nn); + } + doReturn(two).when(module).getNBNeighbors(sim); + + module.updateGrowthRateBasedOnOtherNBs(sim); + + assertEquals(10.0 * (2.0 / 3.0), module.cellGrowthRate, 1e-6); + } + + @Test + public void updateGrowthRateBasedOnOtherNBs_largeNeighbors_approachesZero() { + when(parameters.getInt("proliferation/PDELIKE")).thenReturn(0); + when(parameters.getInt("proliferation/DYNAMIC_GROWTH_RATE_NB_CONTACT")).thenReturn(1); + + when(parameters.getDouble("proliferation/NB_CONTACT_HALF_MAX")).thenReturn(5.0); + when(parameters.getDouble("proliferation/NB_CONTACT_HILL_N")).thenReturn(3.0); + when(parameters.getDouble("proliferation/CELL_GROWTH_RATE")).thenReturn(7.0); + + module = spy(new PottsModuleFlyStemProliferation(stemCell)); + + // N = 100 >> K = 5 → repression ~ 0 + HashSet many = new HashSet<>(); + for (int i = 0; i < 100; i++) { + PottsCellFlyStem nn = mock(PottsCellFlyStem.class); + when(nn.getID()).thenReturn(400 + i); + many.add(nn); + } + doReturn(many).when(module).getNBNeighbors(sim); + + module.updateGrowthRateBasedOnOtherNBs(sim); + + assertTrue(module.cellGrowthRate < 0.01, "Growth should be ~0 with very large N."); + } + + @Test + public void daughterStem_deterministicTruematchingNormalVector_returnsTrue() { + // hasDeterministicDifferentiation=true is already set in @BeforeEach + Vector apicalAxis = new Vector(0, 1, 0); + when(stemCell.getApicalAxis()).thenReturn(apicalAxis); + + // Compute the MUDMUT expected normal the same way daughterStemDeterministic does: + // rotateVectorAroundAxis(apicalAxis, XY_PLANE, MUDMUT.splitDirectionRotation=-90) → (1,0,0) + Vector expectedNormal = + Vector.rotateVectorAroundAxis( + apicalAxis, + new Vector(0, 0, 1), // Direction.XY_PLANE.vector + PottsCellFlyStem.StemType.MUDMUT.splitDirectionRotation); + + Plane plane = mock(Plane.class); + when(plane.getUnitNormalVector()).thenReturn(expectedNormal); + + module = new PottsModuleFlyStemProliferation(stemCell); + boolean result = module.daughterStem(stemLoc, daughterLoc, plane); + + assertTrue( + result, "Expected true when division plane normal matches MUDMUT expected normal"); + } + + @Test + public void daughterStem_deterministicTrueNonMatchingNormalVector_returnsFalse() { + // hasDeterministicDifferentiation=true is already set in @BeforeEach + Vector apicalAxis = new Vector(0, 1, 0); + when(stemCell.getApicalAxis()).thenReturn(apicalAxis); + + // Use a normal that does NOT match the MUDMUT expected normal (1,0,0) + Plane plane = mock(Plane.class); + when(plane.getUnitNormalVector()).thenReturn(new Vector(0, 1, 0)); + + module = new PottsModuleFlyStemProliferation(stemCell); + boolean result = module.daughterStem(stemLoc, daughterLoc, plane); + + assertFalse( + result, + "Expected false when division plane normal does not match MUDMUT expected normal"); + } + + @Test + void testDaughterStem_RuleBased_VolumeTrue() { + when(parameters.getString("proliferation/HAS_DETERMINISTIC_DIFFERENTIATION")) + .thenReturn("FALSE"); + when(parameters.getString("proliferation/DIFFERENTIATION_RULESET")).thenReturn("volume"); + when(parameters.getDouble("proliferation/DIFFERENTIATION_RULESET_EQUALITY_RANGE")) + .thenReturn(10.0); // large enough for |10 - 5| < 10 + + when(stemCell.getStemType()).thenReturn(PottsCellFlyStem.StemType.MUDMUT); + + PottsModuleFlyStemProliferation module = new PottsModuleFlyStemProliferation(stemCell); + + boolean result = module.daughterStem(stemLoc, daughterLoc, mock(Plane.class)); + + assertTrue(result, "Expected true since |10-5| < range"); + } + + @Test + void testDaughterStem_RuleBased_VolumeFalse() { + when(parameters.getString("proliferation/HAS_DETERMINISTIC_DIFFERENTIATION")) + .thenReturn("FALSE"); + when(parameters.getString("proliferation/DIFFERENTIATION_RULESET")).thenReturn("volume"); + when(parameters.getDouble("proliferation/DIFFERENTIATION_RULESET_EQUALITY_RANGE")) + .thenReturn(1.0); // |10 - 5| = 5 > 1 + + when(stemCell.getStemType()).thenReturn(PottsCellFlyStem.StemType.MUDMUT); + + PottsModuleFlyStemProliferation module = new PottsModuleFlyStemProliferation(stemCell); + + boolean result = module.daughterStem(stemLoc, daughterLoc, mock(Plane.class)); + + assertFalse(result, "Expected false since |10-5| > range"); + } + + @Test + public void daughterStem_ruleBasedWT_alwaysReturnsFalse() { + when(parameters.getString("proliferation/HAS_DETERMINISTIC_DIFFERENTIATION")) + .thenReturn("FALSE"); + when(stemCell.getStemType()).thenReturn(PottsCellFlyStem.StemType.WT); + + module = new PottsModuleFlyStemProliferation(stemCell); + boolean result = module.daughterStem(stemLoc, daughterLoc, mock(Plane.class)); + + assertFalse(result, "WT rule-based differentiation should always return false"); + } + + @Test + public void daughterStem_ruleBasedMUDMUTLocation_withinRange_returnsTrue() { + // @BeforeEach sets: stemLoc centroid=(0,1.0,0), daughterLoc centroid=(0,1.6,0) + // With apical axis (0,1,0), distance along axis = |1.6 - 1.0| = 0.6 + // range=1.0 > 0.6 → within range → true + when(parameters.getString("proliferation/HAS_DETERMINISTIC_DIFFERENTIATION")) + .thenReturn("FALSE"); + when(parameters.getString("proliferation/DIFFERENTIATION_RULESET")).thenReturn("location"); + when(parameters.getDouble("proliferation/DIFFERENTIATION_RULESET_EQUALITY_RANGE")) + .thenReturn(1.0); + when(stemCell.getStemType()).thenReturn(PottsCellFlyStem.StemType.MUDMUT); + when(stemCell.getApicalAxis()).thenReturn(new Vector(0, 1, 0)); + + module = new PottsModuleFlyStemProliferation(stemCell); + boolean result = module.daughterStem(stemLoc, daughterLoc, mock(Plane.class)); + + assertTrue( + result, + "Expected true: centroid distance 0.6 is within range 1.0 along apical axis"); + } + + @Test + public void daughterStem_ruleBasedMUDMUTLocation_outsideRange_returnsFalse() { + // @BeforeEach sets: stemLoc centroid=(0,1.0,0), daughterLoc centroid=(0,1.6,0) + // With apical axis (0,1,0), distance along axis = |1.6 - 1.0| = 0.6 + // range=0.5 < 0.6 → outside range → false + when(parameters.getString("proliferation/HAS_DETERMINISTIC_DIFFERENTIATION")) + .thenReturn("FALSE"); + when(parameters.getString("proliferation/DIFFERENTIATION_RULESET")).thenReturn("location"); + when(parameters.getDouble("proliferation/DIFFERENTIATION_RULESET_EQUALITY_RANGE")) + .thenReturn(0.5); + when(stemCell.getStemType()).thenReturn(PottsCellFlyStem.StemType.MUDMUT); + when(stemCell.getApicalAxis()).thenReturn(new Vector(0, 1, 0)); + + module = new PottsModuleFlyStemProliferation(stemCell); + boolean result = module.daughterStem(stemLoc, daughterLoc, mock(Plane.class)); + + assertFalse( + result, + "Expected false: centroid distance 0.6 exceeds range 0.5 along apical axis"); + } + + @Test + public void daughterStem_ruleBasedMUDMUTInvalidRuleset_throwsException() { + when(parameters.getString("proliferation/HAS_DETERMINISTIC_DIFFERENTIATION")) + .thenReturn("FALSE"); + when(parameters.getString("proliferation/DIFFERENTIATION_RULESET")).thenReturn("invalid"); + when(stemCell.getStemType()).thenReturn(PottsCellFlyStem.StemType.MUDMUT); + + module = new PottsModuleFlyStemProliferation(stemCell); + + assertThrows( + IllegalArgumentException.class, + () -> module.daughterStem(stemLoc, daughterLoc, mock(Plane.class))); + } +} diff --git a/test/arcade/potts/sim/PottsTest.java b/test/arcade/potts/sim/PottsTest.java index 04c61d693..305658832 100644 --- a/test/arcade/potts/sim/PottsTest.java +++ b/test/arcade/potts/sim/PottsTest.java @@ -61,7 +61,7 @@ boolean getConnectivity(boolean[][][] array, boolean zero) { } @Override - HashSet getUniqueIDs(int x, int y, int z) { + public HashSet getUniqueIDs(int x, int y, int z) { HashSet set = new HashSet<>(); if (x == 0 && y == 0) { set.add(1); From 19212bbbee96e8cbc651ab4ec03c0a1ca1eb2a91 Mon Sep 17 00:00:00 2001 From: jannetty Date: Sun, 12 Apr 2026 13:59:23 -0700 Subject: [PATCH 13/20] finish covering PottsModuleFlyStemProliferation with tests --- .../agent/module/PottsModuleFlyStemProliferationTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/arcade/potts/agent/module/PottsModuleFlyStemProliferationTest.java b/test/arcade/potts/agent/module/PottsModuleFlyStemProliferationTest.java index 00e6fdc98..40f6ea91f 100644 --- a/test/arcade/potts/agent/module/PottsModuleFlyStemProliferationTest.java +++ b/test/arcade/potts/agent/module/PottsModuleFlyStemProliferationTest.java @@ -98,6 +98,7 @@ public final void setup() { // Default centroid and volume values (sometimes overridden in tests) when(stemLoc.getVolume()).thenReturn(10.0); + when(stemCell.getVolume()).thenReturn(10.0); when(daughterLoc.getVolume()).thenReturn(5.0); when(stemLoc.getCentroid()).thenReturn(new double[] {0, 1.0, 0}); when(daughterLoc.getCentroid()).thenReturn(new double[] {0, 1.6, 0}); @@ -648,8 +649,7 @@ public void calculateGMCDaughterCellCriticalVolume_volumeBasedOn_returnsLocVolum } @Test - public void - calculateGMCDaughterCellCriticalVolume_volumeBasedOnVerySmallVolume_returnsFloor() { + public void calculateGMCDaughterCellCriticalVolume_volumeBasedOnVerySmallVolume_returnsFloor() { // @BeforeEach: stemLoc.getVolume()=10.0, so initialSize=10.0 → floor = 10.0 * 0.1 = 1.0 // gmcLoc.getVolume()=0.5 < 1.0, so Math.max picks the floor when(parameters.getInt("proliferation/VOLUME_BASED_CRITICAL_VOLUME")).thenReturn(1); From 6b7db55b45343f719cac952f980d36a2f944d4a6 Mon Sep 17 00:00:00 2001 From: jannetty Date: Sun, 12 Apr 2026 14:25:08 -0700 Subject: [PATCH 14/20] adding tests --- .../agent/cell/PottsCellContainerTest.java | 61 +++++++++++++++++++ .../agent/cell/PottsCellFactoryTest.java | 15 +++++ .../agent/cell/PottsCellFlyStemTest.java | 23 +++++++ 3 files changed, 99 insertions(+) diff --git a/test/arcade/potts/agent/cell/PottsCellContainerTest.java b/test/arcade/potts/agent/cell/PottsCellContainerTest.java index 4b389870e..31fc2aa95 100644 --- a/test/arcade/potts/agent/cell/PottsCellContainerTest.java +++ b/test/arcade/potts/agent/cell/PottsCellContainerTest.java @@ -6,6 +6,7 @@ import org.junit.jupiter.api.Test; import ec.util.MersenneTwisterFast; import arcade.core.env.location.Location; +import arcade.core.util.GrabBag; import arcade.core.util.MiniBox; import arcade.core.util.Parameters; import arcade.potts.env.location.PottsLocation; @@ -118,6 +119,66 @@ public void getID_called_returnsValue() { assertEquals(id, cellContainer.getID()); } + @Test + public void convert_flyStemWT_createsPottsCellFlyStem() { + Location location = mock(PottsLocation.class); + PottsCellFactory factory = new PottsCellFactory(); + + int cellPop = randomIntBetween(1, 10); + MiniBox parameters = new MiniBox(); + parameters.put("CLASS", "fly-stem-wt"); + + GrabBag links = new GrabBag(); + links.add(1, 1); + factory.popToParameters.put(cellPop, parameters); + factory.popToLinks.put(cellPop, links); + factory.popToRegions.put(cellPop, false); + + PottsCellContainer container = + new PottsCellContainer( + randomIntBetween(1, 10), + randomIntBetween(1, 10), + cellPop, + randomIntBetween(1, 100), + randomIntBetween(1, 100), + State.UNDEFINED, + Phase.UNDEFINED, + 0, + randomDoubleBetween(10, 100), + randomDoubleBetween(10, 100)); + assertInstanceOf(PottsCellFlyStem.class, container.convert(factory, location, RANDOM)); + } + + @Test + public void convert_flyStemMUDMUT_createsPottsCellFlyStem() { + Location location = mock(PottsLocation.class); + PottsCellFactory factory = new PottsCellFactory(); + + int cellPop = randomIntBetween(1, 10); + MiniBox parameters = new MiniBox(); + parameters.put("CLASS", "fly-stem-mudmut"); + + GrabBag links = new GrabBag(); + links.add(1, 1); + factory.popToParameters.put(cellPop, parameters); + factory.popToLinks.put(cellPop, links); + factory.popToRegions.put(cellPop, false); + + PottsCellContainer container = + new PottsCellContainer( + randomIntBetween(1, 10), + randomIntBetween(1, 10), + cellPop, + randomIntBetween(1, 100), + randomIntBetween(1, 100), + State.UNDEFINED, + Phase.UNDEFINED, + 0, + randomDoubleBetween(10, 100), + randomDoubleBetween(10, 100)); + assertInstanceOf(PottsCellFlyStem.class, container.convert(factory, location, RANDOM)); + } + @Test public void convert_noRegionsNoParent_createsObject() { Location location = mock(PottsLocation.class); diff --git a/test/arcade/potts/agent/cell/PottsCellFactoryTest.java b/test/arcade/potts/agent/cell/PottsCellFactoryTest.java index 1660ea70b..30987d544 100644 --- a/test/arcade/potts/agent/cell/PottsCellFactoryTest.java +++ b/test/arcade/potts/agent/cell/PottsCellFactoryTest.java @@ -1,6 +1,7 @@ package arcade.potts.agent.cell; import java.lang.reflect.Field; +import java.security.InvalidParameterException; import java.util.ArrayList; import java.util.EnumSet; import java.util.HashMap; @@ -272,6 +273,20 @@ public void parseValues_withRegionsWithLinks_updatesLists() { } } + @Test + public void parseValues_invalidPopulationLink_throwsInvalidParameterException() { + Series series = mock(Series.class); + series.populations = new HashMap<>(); + + MiniBox population = new MiniBox(); + population.put("CODE", 1); + population.put("(LINK)" + TAG_SEPARATOR + "NONEXISTENT", 1); + series.populations.put("A", population); + + PottsCellFactory factory = new PottsCellFactory(); + assertThrows(InvalidParameterException.class, () -> factory.parseValues(series)); + } + @Test public void loadCells_givenLoadedValidPops_updatesLists() { int n = randomIntBetween(1, 10); diff --git a/test/arcade/potts/agent/cell/PottsCellFlyStemTest.java b/test/arcade/potts/agent/cell/PottsCellFlyStemTest.java index 263a5aee0..df4a7cd41 100644 --- a/test/arcade/potts/agent/cell/PottsCellFlyStemTest.java +++ b/test/arcade/potts/agent/cell/PottsCellFlyStemTest.java @@ -7,6 +7,8 @@ import arcade.core.util.MiniBox; import arcade.core.util.Parameters; import arcade.core.util.Vector; +import arcade.core.util.distributions.NormalDistribution; +import arcade.potts.agent.module.PottsModuleFlyStemProliferation; import arcade.potts.env.location.PottsLocation; import arcade.potts.util.PottsEnums.Phase; import arcade.potts.util.PottsEnums.State; @@ -165,6 +167,27 @@ void setStateModule_called_createsProliferationModuleOrSetsNull() { } } + @Test + void setStateModule_proliferative_createsFlyStemProliferationModule() { + NormalDistribution distMock = mock(NormalDistribution.class); + doReturn("fly-stem-wt").when(parametersMock).getString("CLASS"); + doReturn("FALSE") + .when(parametersMock) + .getString("proliferation/HAS_DETERMINISTIC_DIFFERENTIATION"); + doReturn("volume").when(parametersMock).getString("proliferation/DIFFERENTIATION_RULESET"); + doReturn("global").when(parametersMock).getString("proliferation/APICAL_AXIS_RULESET"); + doReturn(distMock) + .when(parametersMock) + .getDistribution("proliferation/DIV_ROTATION_DISTRIBUTION"); + doReturn(distMock) + .when(parametersMock) + .getDistribution("proliferation/APICAL_AXIS_ROTATION_DISTRIBUTION"); + PottsCellFlyStem cell = + new PottsCellFlyStem(baseContainer, locationMock, parametersMock, links); + cell.setStateModule(State.PROLIFERATIVE); + assertInstanceOf(PottsModuleFlyStemProliferation.class, cell.getModule()); + } + @Test void getStemType_called_returnsCorrectStemType() { doReturn("fly-stem-wt").when(parametersMock).getString("CLASS"); From d80b49fd4e5c24b4f0cf82dbd3f40360cbd91a04 Mon Sep 17 00:00:00 2001 From: jannetty Date: Sun, 12 Apr 2026 14:34:24 -0700 Subject: [PATCH 15/20] importing PottsCell and Bag to GMC differentiation module instead of inline import --- .../module/PottsModuleFlyGMCDifferentiation.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java b/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java index 299116b01..e91c27f58 100644 --- a/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java +++ b/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java @@ -1,5 +1,6 @@ package arcade.potts.agent.module; +import sim.util.Bag; import ec.util.MersenneTwisterFast; import arcade.core.agent.cell.CellContainer; import arcade.core.env.location.Location; @@ -143,7 +144,7 @@ public void updateGrowthRate(Simulation sim) { // PDE-like: use population-wide averages for GMCs (same pop as this cell). // The reference volume is the population-average equilibrium volume: // avgVRef = avgCritVol * (1 + sizeTarget) / 2 - sim.util.Bag objs = sim.getGrid().getAllObjects(); + Bag objs = sim.getGrid().getAllObjects(); double volSum = 0.0; double critSum = 0.0; @@ -151,18 +152,17 @@ public void updateGrowthRate(Simulation sim) { for (int i = 0; i < objs.numObjs; i++) { Object o = objs.objs[i]; - if (!(o instanceof arcade.potts.agent.cell.PottsCell)) { + if (!(o instanceof PottsCell)) { continue; } - arcade.potts.agent.cell.PottsCell c = (arcade.potts.agent.cell.PottsCell) o; + PottsCell c = (PottsCell) o; if (c.getPop() != cell.getPop()) { continue; // keep to same population } - if (o instanceof arcade.potts.agent.cell.PottsCellFlyGMC) { - arcade.potts.agent.cell.PottsCellFlyGMC gmc = - (arcade.potts.agent.cell.PottsCellFlyGMC) o; + if (o instanceof PottsCellFlyGMC) { + PottsCellFlyGMC gmc = (PottsCellFlyGMC) o; volSum += gmc.getLocation().getVolume(); critSum += gmc.getCriticalVolume(); count++; From 6d8a7f830916ae6e43e64fdd9dcc3c39b3a07c3f Mon Sep 17 00:00:00 2001 From: jannetty Date: Sun, 12 Apr 2026 14:45:53 -0700 Subject: [PATCH 16/20] fixing PottsCellFlyStem line length issue --- src/arcade/potts/agent/cell/PottsCellFlyStem.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/arcade/potts/agent/cell/PottsCellFlyStem.java b/src/arcade/potts/agent/cell/PottsCellFlyStem.java index 3072a0f98..36389db58 100644 --- a/src/arcade/potts/agent/cell/PottsCellFlyStem.java +++ b/src/arcade/potts/agent/cell/PottsCellFlyStem.java @@ -116,7 +116,8 @@ public Vector getApicalAxis() { @Override public PottsCellContainer make(int newID, CellState newState, MersenneTwisterFast random) { throw new UnsupportedOperationException( - "make(int, CellState, MersenneTwisterFast) not supported. Please use make(int, CellState, MersenneTwisterFast, int, double) instead."); + "make(int, CellState, MersenneTwisterFast) not supported." + + "Please use make(int, CellState, MersenneTwisterFast, int, double) instead."); } /** From 30126b14d90f2760098eb923b558fb21fd1fab5a Mon Sep 17 00:00:00 2001 From: jannetty Date: Sun, 12 Apr 2026 15:10:02 -0700 Subject: [PATCH 17/20] addressing puppycrawler issues --- .../PottsModuleFlyStemProliferation.java | 75 ++++++++++++------- .../PottsModuleFlyStemProliferationTest.java | 31 ++++---- 2 files changed, 65 insertions(+), 41 deletions(-) diff --git a/src/arcade/potts/agent/module/PottsModuleFlyStemProliferation.java b/src/arcade/potts/agent/module/PottsModuleFlyStemProliferation.java index 6a5266330..5b7bb10dc 100644 --- a/src/arcade/potts/agent/module/PottsModuleFlyStemProliferation.java +++ b/src/arcade/potts/agent/module/PottsModuleFlyStemProliferation.java @@ -23,6 +23,9 @@ import arcade.potts.env.location.Voxel; import arcade.potts.sim.Potts; import arcade.potts.sim.PottsSimulation; +import arcade.potts.util.PottsEnums.Direction; +import arcade.potts.util.PottsEnums.Phase; +import arcade.potts.util.PottsEnums.State; import static arcade.potts.util.PottsEnums.Direction; import static arcade.potts.util.PottsEnums.Phase; import static arcade.potts.util.PottsEnums.State; @@ -86,13 +89,15 @@ public class PottsModuleFlyStemProliferation extends PottsModuleProliferationVol */ final double nbContactHillN; - /* + /** * Boolean flag for whether the daughter cell's differentiation is determined deterministically. */ final boolean hasDeterministicDifferentiation; + /** The cell's initial size/volume (in voxels). */ final double initialSize; + /** Epsilon. */ public static final double EPSILON = 1e-8; /** @@ -188,9 +193,9 @@ public void addCell(MersenneTwisterFast random, Simulation sim) { * @param sim the simulation */ public void updateGrowthRate(Simulation sim) { - if (dynamicGrowthRateVolume == true) { + if (dynamicGrowthRateVolume) { updateVolumeBasedGrowthRate(sim); - } else if (dynamicGrowthRateNBSelfRepression == true) { + } else if (dynamicGrowthRateNBSelfRepression) { updateGrowthRateBasedOnOtherNBs(sim); } else { cellGrowthRate = cellGrowthRateBase; @@ -216,7 +221,7 @@ public void updateGrowthRate(Simulation sim) { */ public void updateVolumeBasedGrowthRate(Simulation sim) { double vRef = computeEquilibriumVolume(); - if (pdeLike == false) { + if (!pdeLike) { updateCellVolumeBasedGrowthRate(cell.getLocation().getVolume(), vRef); } else { HashSet nbsInSimulation = getNBsInSimulation(sim); @@ -308,16 +313,16 @@ protected void updateGrowthRateBasedOnOtherNBs(Simulation sim) { } else { nbsInContact = getNBNeighbors(sim).size(); } - double np = Math.max(0.0, (double) nbsInContact); + double neighborSignal = Math.max(0.0, (double) nbsInContact); - double Kn = Math.pow(nbContactHalfMax, nbContactHillN); - double Npn = Math.pow(np, nbContactHillN); + double kHalfMaxPowN = Math.pow(nbContactHalfMax, nbContactHillN); + double neighborSignalPowN = Math.pow(neighborSignal, nbContactHillN); double hillRepression; - if (Kn == 0.0) { - hillRepression = (np == 0.0) ? 1.0 : 0.0; + if (kHalfMaxPowN == 0.0) { + hillRepression = (neighborSignal == 0.0) ? 1.0 : 0.0; } else { - hillRepression = Kn / (Kn + Npn); + hillRepression = kHalfMaxPowN / (kHalfMaxPowN + neighborSignalPowN); } cellGrowthRate = cellGrowthRateBase * hillRepression; @@ -361,10 +366,10 @@ protected Plane chooseDivisionPlane(PottsCellFlyStem flyStemCell) { */ public Plane getWTDivisionPlaneWithRotationalVariance( PottsCellFlyStem cell, double rotationOffset) { - Vector apical_axis = cell.getApicalAxis(); + Vector apicalAxis = cell.getApicalAxis(); Vector rotatedNormalVector = Vector.rotateVectorAroundAxis( - apical_axis, Direction.XY_PLANE.vector, rotationOffset); + apicalAxis, Direction.XY_PLANE.vector, rotationOffset); Voxel splitVoxel = getCellSplitVoxel(StemType.WT, cell, rotatedNormalVector); return new Plane( new Double3D(splitVoxel.x, splitVoxel.y, splitVoxel.z), rotatedNormalVector); @@ -388,10 +393,16 @@ public Plane getMUDDivisionPlane(PottsCellFlyStem cell) { } /** - * Gets the voxel location the cell's plane of division will pass through. + * Computes the voxel through which the division plane passes for a given cell. + * + *

The split position is determined from the stem-type-specific offset percentages, + * interpreted in the cell's apical frame. The supplied normal vector is assumed to already + * reflect any rule-based rotation of the division plane. * - * @param cell the {@link PottsCellFlyStem} to get the division location for - * @return the voxel location where the cell will split + * @param stemType the {@link StemType} providing the split offset percentages + * @param cell the {@link PottsCellFlyStem} whose division position is being computed + * @param rotatedNormalVector the division plane normal after any rule-based rotation + * @return the voxel used as the anchor point for the division plane */ public static Voxel getCellSplitVoxel( StemType stemType, PottsCellFlyStem cell, Vector rotatedNormalVector) { @@ -420,9 +431,8 @@ private boolean daughterStemRuleBasedDifferentiation(PottsLocation loc1, PottsLo double vol2 = loc2.getVolume(); if (Math.abs(vol1 - vol2) < range) { return true; - } else { - return false; } + return false; } else if (differentiationRuleset.equals("location")) { double[] centroid1 = loc1.getCentroid(); double[] centroid2 = loc2.getCentroid(); @@ -434,12 +444,13 @@ private boolean daughterStemRuleBasedDifferentiation(PottsLocation loc1, PottsLo "Invalid differentiation ruleset: " + differentiationRuleset); } - /* - * Determines whether the daughter cell should be a neuroblast or a GMC according to the orientation. - * This is deterministic. + /** + * Determines whether the daughter cell should be a neuroblast or a GMC according to the + * orientation. This is deterministic. * * @param divisionPlane - * @return {@code true} if the daughter should be a stem cell. {@code false} if the daughter should be a GMC. + * @return {@code true} if the daughter should be a stem cell. {@code false} if the daughter + * should be a GMC. */ private boolean daughterStemDeterministic(Plane divisionPlane) { @@ -501,7 +512,7 @@ static boolean centroidsWithinRangeAlongApicalAxis( } /** - * Makes a daughter NB cell + * Makes a daughter NB cell. * * @param daughterLoc the location of the daughter NB cell * @param sim the simulation @@ -526,7 +537,7 @@ private void makeDaughterStemCell( } /** - * Makes a daughter GMC cell + * Makes a daughter GMC cell. * * @param parentLoc the location of the parent NB cell * @param daughterLoc the location of the daughter GMC cell @@ -595,7 +606,8 @@ public Vector getDaughterCellApicalAxis(MersenneTwisterFast random) { case "uniform": if (!(apicalAxisRotationDistribution instanceof UniformDistribution)) { throw new IllegalArgumentException( - "apicalAxisRotationDistribution must be a UniformDistribution under the uniform apical axis ruleset."); + "apicalAxisRotationDistribution must be a UniformDistribution" + + "under the uniform apical axis ruleset."); } Vector newRandomApicalAxis = Vector.rotateVectorAroundAxis( @@ -608,7 +620,8 @@ public Vector getDaughterCellApicalAxis(MersenneTwisterFast random) { case "normal": if (!(apicalAxisRotationDistribution instanceof NormalDistribution)) { throw new IllegalArgumentException( - "apicalAxisRotationDistribution must be a NormalDistribution under the rotation apical axis ruleset."); + "apicalAxisRotationDistribution must be a NormalDistribution" + + "under the rotation apical axis ruleset."); } Vector newRotatedApicalAxis = Vector.rotateVectorAroundAxis( @@ -645,7 +658,7 @@ private Location determineGMCLocation( } /** - * Calculates the critical volume of a GMC daughter cell + * Calculates the critical volume of a GMC daughter cell. * * @param gmcLoc the location of the GMC daughter cell * @return the critical volume of the GMC daughter cell @@ -696,12 +709,20 @@ public static PottsLocation getBasalLocation( return (proj1 < proj2) ? loc2 : loc1; // higher projection = more basal } + /** + * Gets all cell objects in the simulation that are Neuroblasts. + * + * @param sim the simulation + * @return a HashSet of all PottsCellFlyStem cell objects in the simulation + */ public HashSet getNBsInSimulation(Simulation sim) { HashSet nbsInSimulation = new HashSet<>(); Bag simObjects = sim.getGrid().getAllObjects(); for (int i = 0; i < simObjects.numObjs; i++) { Object o = simObjects.objs[i]; - if (!(o instanceof PottsCell)) continue; // skip non-cell objects + if (!(o instanceof PottsCell)) { + continue; // skip non-cell objects + } PottsCell cellInSim = (PottsCell) o; if (cell.getPop() == cellInSim.getPop() && o instanceof PottsCellFlyStem) { nbsInSimulation.add((PottsCellFlyStem) o); diff --git a/test/arcade/potts/agent/module/PottsModuleFlyStemProliferationTest.java b/test/arcade/potts/agent/module/PottsModuleFlyStemProliferationTest.java index 40f6ea91f..1b418a628 100644 --- a/test/arcade/potts/agent/module/PottsModuleFlyStemProliferationTest.java +++ b/test/arcade/potts/agent/module/PottsModuleFlyStemProliferationTest.java @@ -62,7 +62,7 @@ public class PottsModuleFlyStemProliferationTest { NormalDistribution dist; - float EPSILON = 1e-6f; + final float EPSILON = 1e-6f; int stemCellPop; @@ -690,14 +690,15 @@ public void addCell_WTVolumeSwap_swapsVoxelsAndCreatesNewCell() { .thenReturn(container); when(container.convert(eq(factory), eq(daughterLoc), eq(random))).thenReturn(newStemCell); - PottsModuleFlyStemProliferation module = spy(new PottsModuleFlyStemProliferation(stemCell)); - doReturn(0.0).when(module).sampleDivisionPlaneOffset(); + PottsModuleFlyStemProliferation spyModule = + spy(new PottsModuleFlyStemProliferation(stemCell)); + doReturn(0.0).when(spyModule).sampleDivisionPlaneOffset(); doReturn(dummyPlane) - .when(module) + .when(spyModule) .getWTDivisionPlaneWithRotationalVariance(eq(stemCell), anyDouble()); try (MockedStatic mocked = mockStatic(PottsLocation.class)) { - module.addCell(random, sim); + spyModule.addCell(random, sim); mocked.verify(() -> PottsLocation.swapVoxels(stemLoc, daughterLoc)); } @@ -730,13 +731,14 @@ public void addCell_WTVolumeNoSwap_doesNotSwapVoxelsAndCreatesNewCell() { when(container.convert(eq(factory), eq(daughterLoc), eq(random))).thenReturn(newStemCell); // Spy and override division plane logic - PottsModuleFlyStemProliferation module = spy(new PottsModuleFlyStemProliferation(stemCell)); + PottsModuleFlyStemProliferation spyModule = + spy(new PottsModuleFlyStemProliferation(stemCell)); doReturn(dummyPlane) - .when(module) + .when(spyModule) .getWTDivisionPlaneWithRotationalVariance(eq(stemCell), anyDouble()); try (MockedStatic mocked = mockStatic(PottsLocation.class)) { - module.addCell(random, sim); + spyModule.addCell(random, sim); mocked.verify(() -> PottsLocation.swapVoxels(any(), any()), never()); } verify(newStemCell).schedule(any()); @@ -771,13 +773,14 @@ public void addCell_MUDMUTOffsetAboveThreshold_createsStemCell() { when(stemCell.getCriticalVolume()).thenReturn(100.0); when(stemCell.getPop()).thenReturn(stemCellPop); - module = spy(new PottsModuleFlyStemProliferation(stemCell)); + PottsModuleFlyStemProliferation spyModule = + spy(new PottsModuleFlyStemProliferation(stemCell)); Plane dummyPlane = mock(Plane.class); - doReturn(dummyPlane).when(module).getMUDDivisionPlane(eq(stemCell)); + doReturn(dummyPlane).when(spyModule).getMUDDivisionPlane(eq(stemCell)); when(stemLoc.split(eq(random), eq(dummyPlane))).thenReturn(daughterLoc); - doReturn(true).when(module).daughterStem(any(), any(), any()); + doReturn(true).when(spyModule).daughterStem(any(), any(), any()); - module.addCell(random, sim); + spyModule.addCell(random, sim); verify(newCell).schedule(any()); } @@ -1346,7 +1349,7 @@ public void daughterStem_deterministicTrueNonMatchingNormalVector_returnsFalse() } @Test - void testDaughterStem_RuleBased_VolumeTrue() { + void daughterStem_volumeRuleBased_true() { when(parameters.getString("proliferation/HAS_DETERMINISTIC_DIFFERENTIATION")) .thenReturn("FALSE"); when(parameters.getString("proliferation/DIFFERENTIATION_RULESET")).thenReturn("volume"); @@ -1363,7 +1366,7 @@ void testDaughterStem_RuleBased_VolumeTrue() { } @Test - void testDaughterStem_RuleBased_VolumeFalse() { + void daughterStem_volumeRuleBased_false() { when(parameters.getString("proliferation/HAS_DETERMINISTIC_DIFFERENTIATION")) .thenReturn("FALSE"); when(parameters.getString("proliferation/DIFFERENTIATION_RULESET")).thenReturn("volume"); From 510c8a9bf7f2675136d040ff5fa7b298160c9d21 Mon Sep 17 00:00:00 2001 From: jannetty Date: Sun, 12 Apr 2026 15:15:31 -0700 Subject: [PATCH 18/20] more formatting --- .../module/PottsModuleFlyStemProliferation.java | 6 +++--- .../PottsModuleFlyStemProliferationTest.java | 16 +++++++--------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/arcade/potts/agent/module/PottsModuleFlyStemProliferation.java b/src/arcade/potts/agent/module/PottsModuleFlyStemProliferation.java index 5b7bb10dc..907a17c1e 100644 --- a/src/arcade/potts/agent/module/PottsModuleFlyStemProliferation.java +++ b/src/arcade/potts/agent/module/PottsModuleFlyStemProliferation.java @@ -448,9 +448,9 @@ private boolean daughterStemRuleBasedDifferentiation(PottsLocation loc1, PottsLo * Determines whether the daughter cell should be a neuroblast or a GMC according to the * orientation. This is deterministic. * - * @param divisionPlane - * @return {@code true} if the daughter should be a stem cell. {@code false} if the daughter - * should be a GMC. + * @param divisionPlane the plane the cell will divide along + * @return {@code true} if the daughter should be a stem cell; {@code false} if the daughter + * should be a GMC */ private boolean daughterStemDeterministic(Plane divisionPlane) { diff --git a/test/arcade/potts/agent/module/PottsModuleFlyStemProliferationTest.java b/test/arcade/potts/agent/module/PottsModuleFlyStemProliferationTest.java index 1b418a628..c6543822c 100644 --- a/test/arcade/potts/agent/module/PottsModuleFlyStemProliferationTest.java +++ b/test/arcade/potts/agent/module/PottsModuleFlyStemProliferationTest.java @@ -30,6 +30,7 @@ import arcade.potts.sim.PottsSimulation; import arcade.potts.util.PottsEnums.Phase; import arcade.potts.util.PottsEnums.State; + import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyDouble; @@ -62,7 +63,7 @@ public class PottsModuleFlyStemProliferationTest { NormalDistribution dist; - final float EPSILON = 1e-6f; + public static final double EPSILON = 1e-6f; int stemCellPop; @@ -690,8 +691,7 @@ public void addCell_WTVolumeSwap_swapsVoxelsAndCreatesNewCell() { .thenReturn(container); when(container.convert(eq(factory), eq(daughterLoc), eq(random))).thenReturn(newStemCell); - PottsModuleFlyStemProliferation spyModule = - spy(new PottsModuleFlyStemProliferation(stemCell)); + PottsModuleFlyStemProliferation spyModule = spy(new PottsModuleFlyStemProliferation(stemCell)); doReturn(0.0).when(spyModule).sampleDivisionPlaneOffset(); doReturn(dummyPlane) .when(spyModule) @@ -731,8 +731,7 @@ public void addCell_WTVolumeNoSwap_doesNotSwapVoxelsAndCreatesNewCell() { when(container.convert(eq(factory), eq(daughterLoc), eq(random))).thenReturn(newStemCell); // Spy and override division plane logic - PottsModuleFlyStemProliferation spyModule = - spy(new PottsModuleFlyStemProliferation(stemCell)); + PottsModuleFlyStemProliferation spyModule = spy(new PottsModuleFlyStemProliferation(stemCell)); doReturn(dummyPlane) .when(spyModule) .getWTDivisionPlaneWithRotationalVariance(eq(stemCell), anyDouble()); @@ -773,8 +772,7 @@ public void addCell_MUDMUTOffsetAboveThreshold_createsStemCell() { when(stemCell.getCriticalVolume()).thenReturn(100.0); when(stemCell.getPop()).thenReturn(stemCellPop); - PottsModuleFlyStemProliferation spyModule = - spy(new PottsModuleFlyStemProliferation(stemCell)); + PottsModuleFlyStemProliferation spyModule = spy(new PottsModuleFlyStemProliferation(stemCell)); Plane dummyPlane = mock(Plane.class); doReturn(dummyPlane).when(spyModule).getMUDDivisionPlane(eq(stemCell)); when(stemLoc.split(eq(random), eq(dummyPlane))).thenReturn(daughterLoc); @@ -1358,7 +1356,7 @@ void daughterStem_volumeRuleBased_true() { when(stemCell.getStemType()).thenReturn(PottsCellFlyStem.StemType.MUDMUT); - PottsModuleFlyStemProliferation module = new PottsModuleFlyStemProliferation(stemCell); + module = new PottsModuleFlyStemProliferation(stemCell); boolean result = module.daughterStem(stemLoc, daughterLoc, mock(Plane.class)); @@ -1375,7 +1373,7 @@ void daughterStem_volumeRuleBased_false() { when(stemCell.getStemType()).thenReturn(PottsCellFlyStem.StemType.MUDMUT); - PottsModuleFlyStemProliferation module = new PottsModuleFlyStemProliferation(stemCell); + module = new PottsModuleFlyStemProliferation(stemCell); boolean result = module.daughterStem(stemLoc, daughterLoc, mock(Plane.class)); From 4c8b0d1393ec3ed424554bc5f2e360ae326ea0a7 Mon Sep 17 00:00:00 2001 From: jannetty Date: Sun, 12 Apr 2026 15:17:05 -0700 Subject: [PATCH 19/20] format --- .../potts/agent/module/PottsModuleFlyStemProliferationTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/arcade/potts/agent/module/PottsModuleFlyStemProliferationTest.java b/test/arcade/potts/agent/module/PottsModuleFlyStemProliferationTest.java index c6543822c..88ad7e9ac 100644 --- a/test/arcade/potts/agent/module/PottsModuleFlyStemProliferationTest.java +++ b/test/arcade/potts/agent/module/PottsModuleFlyStemProliferationTest.java @@ -29,8 +29,6 @@ import arcade.potts.sim.Potts; import arcade.potts.sim.PottsSimulation; import arcade.potts.util.PottsEnums.Phase; -import arcade.potts.util.PottsEnums.State; - import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyDouble; From 944fa1d6a5bef8e59054ef231b8e8cd1d3e4bfa2 Mon Sep 17 00:00:00 2001 From: jannetty Date: Sun, 12 Apr 2026 15:18:53 -0700 Subject: [PATCH 20/20] last formatting please --- .../module/PottsModuleFlyStemProliferationTest.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/arcade/potts/agent/module/PottsModuleFlyStemProliferationTest.java b/test/arcade/potts/agent/module/PottsModuleFlyStemProliferationTest.java index 88ad7e9ac..87132f681 100644 --- a/test/arcade/potts/agent/module/PottsModuleFlyStemProliferationTest.java +++ b/test/arcade/potts/agent/module/PottsModuleFlyStemProliferationTest.java @@ -689,7 +689,8 @@ public void addCell_WTVolumeSwap_swapsVoxelsAndCreatesNewCell() { .thenReturn(container); when(container.convert(eq(factory), eq(daughterLoc), eq(random))).thenReturn(newStemCell); - PottsModuleFlyStemProliferation spyModule = spy(new PottsModuleFlyStemProliferation(stemCell)); + PottsModuleFlyStemProliferation spyModule = + spy(new PottsModuleFlyStemProliferation(stemCell)); doReturn(0.0).when(spyModule).sampleDivisionPlaneOffset(); doReturn(dummyPlane) .when(spyModule) @@ -729,7 +730,8 @@ public void addCell_WTVolumeNoSwap_doesNotSwapVoxelsAndCreatesNewCell() { when(container.convert(eq(factory), eq(daughterLoc), eq(random))).thenReturn(newStemCell); // Spy and override division plane logic - PottsModuleFlyStemProliferation spyModule = spy(new PottsModuleFlyStemProliferation(stemCell)); + PottsModuleFlyStemProliferation spyModule = + spy(new PottsModuleFlyStemProliferation(stemCell)); doReturn(dummyPlane) .when(spyModule) .getWTDivisionPlaneWithRotationalVariance(eq(stemCell), anyDouble()); @@ -770,7 +772,8 @@ public void addCell_MUDMUTOffsetAboveThreshold_createsStemCell() { when(stemCell.getCriticalVolume()).thenReturn(100.0); when(stemCell.getPop()).thenReturn(stemCellPop); - PottsModuleFlyStemProliferation spyModule = spy(new PottsModuleFlyStemProliferation(stemCell)); + PottsModuleFlyStemProliferation spyModule = + spy(new PottsModuleFlyStemProliferation(stemCell)); Plane dummyPlane = mock(Plane.class); doReturn(dummyPlane).when(spyModule).getMUDDivisionPlane(eq(stemCell)); when(stemLoc.split(eq(random), eq(dummyPlane))).thenReturn(daughterLoc);