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/.gitignore b/.gitignore
index 87dfbaedd..8480bff21 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,4 @@ docs/javadoc
working/
.DS_Store
MANIFEST.MF
+setup/
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
diff --git a/gradlew.bat b/gradlew.bat
index 62bd9b9cc..9109989e3 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -1,103 +1,103 @@
-@rem
-@rem Copyright 2015 the original author or authors.
-@rem
-@rem Licensed under the Apache License, Version 2.0 (the "License");
-@rem you may not use this file except in compliance with the License.
-@rem You may obtain a copy of the License at
-@rem
-@rem https://www.apache.org/licenses/LICENSE-2.0
-@rem
-@rem Unless required by applicable law or agreed to in writing, software
-@rem distributed under the License is distributed on an "AS IS" BASIS,
-@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-@rem See the License for the specific language governing permissions and
-@rem limitations under the License.
-@rem
-
-@if "%DEBUG%" == "" @echo off
-@rem ##########################################################################
-@rem
-@rem Gradle startup script for Windows
-@rem
-@rem ##########################################################################
-
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
-
-set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
-set APP_BASE_NAME=%~n0
-set APP_HOME=%DIRNAME%
-
-@rem Resolve any "." and ".." in APP_HOME to make it shorter.
-for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
-
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
-
-@rem Find java.exe
-if defined JAVA_HOME goto findJavaFromJavaHome
-
-set JAVA_EXE=java.exe
-%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto init
-
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:findJavaFromJavaHome
-set JAVA_HOME=%JAVA_HOME:"=%
-set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-
-if exist "%JAVA_EXE%" goto init
-
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:init
-@rem Get command-line arguments, handling Windows variants
-
-if not "%OS%" == "Windows_NT" goto win9xME_args
-
-:win9xME_args
-@rem Slurp the command line arguments.
-set CMD_LINE_ARGS=
-set _SKIP=2
-
-:win9xME_args_slurp
-if "x%~1" == "x" goto execute
-
-set CMD_LINE_ARGS=%*
-
-:execute
-@rem Setup the command line
-
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
-
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
-
-:end
-@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
-
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
-rem the _cmd.exe /c_ return code!
-if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
-
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
-
-:omega
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/src/arcade/potts/agent/cell/PottsCellContainer.java b/src/arcade/potts/agent/cell/PottsCellContainer.java
index 6bc855314..10e3f170b 100644
--- a/src/arcade/potts/agent/cell/PottsCellContainer.java
+++ b/src/arcade/potts/agent/cell/PottsCellContainer.java
@@ -176,6 +176,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/PottsCellFlyGMC.java b/src/arcade/potts/agent/cell/PottsCellFlyGMC.java
index 48f4a212a..f88ad4fc2 100644
--- a/src/arcade/potts/agent/cell/PottsCellFlyGMC.java
+++ b/src/arcade/potts/agent/cell/PottsCellFlyGMC.java
@@ -12,7 +12,9 @@
/**
* Implementation of {@link PottsCell} 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. The basal apoptosis rate of this cell should be set to 0 in the setup file.
+ * cells are Neurons. The differentiation of the parent cell is handled by the {@link
+ * PottsModuleProliferationVolumeBasedDivision} module. The basal apoptosis rate of this cell should
+ * be set to 0 in the setup file.
*/
public class PottsCellFlyGMC extends PottsCell {
diff --git a/src/arcade/potts/agent/cell/PottsCellFlyStem.java b/src/arcade/potts/agent/cell/PottsCellFlyStem.java
new file mode 100644
index 000000000..272f6d179
--- /dev/null
+++ b/src/arcade/potts/agent/cell/PottsCellFlyStem.java
@@ -0,0 +1,158 @@
+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;
+
+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}, consistent with the
+ * rectangular-cell approximation used elsewhere in the proliferation module.
+ */
+ 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;
+
+ 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);
+ }
+ }
+
+ 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.");
+ }
+
+ 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/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/PottsModuleFlyStemProliferation.java b/src/arcade/potts/agent/module/PottsModuleFlyStemProliferation.java
new file mode 100644
index 000000000..b6693375c
--- /dev/null
+++ b/src/arcade/potts/agent/module/PottsModuleFlyStemProliferation.java
@@ -0,0 +1,705 @@
+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 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;
+
+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;
+
+ final double volumeBasedCriticalVolumeMultiplier;
+
+ /**
+ * 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;
+
+ /**
+ * Population-level baseline critical volume in voxels (from the XML CRITICAL_VOLUME parameter,
+ * always converted to voxels). Used as the fixed V_ref denominator in volume-based growth rate
+ * scaling, so that V_ref stays anchored to the WT equilibrium regardless of per-cell critVol
+ * overrides (relevant when VOLUME_BASED_CRITICAL_VOLUME=1 causes daughter critVols to shrink).
+ */
+ final double populationCriticalVolume;
+
+ 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.");
+ }
+
+ volumeBasedCriticalVolumeMultiplier =
+ (parameters.getDouble("proliferation/VOLUME_BASED_CRITICAL_VOLUME_MULTIPLIER"));
+
+ 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();
+ populationCriticalVolume = parameters.getDouble("CRITICAL_VOLUME");
+
+ 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;
+ }
+ }
+
+ 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 equilibrium 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 (rectangular cell
+ * approximation).
+ *
+ * This reference volume is used as the normalization denominator in the volume-based growth
+ * rate formula, ensuring that at equilibrium the effective growth rate equals {@code
+ * cellGrowthRateBase}.
+ *
+ * @return the expected equilibrium average NB volume
+ */
+ double computeEquilibriumVolume() {
+ double vDiv = sizeTarget * populationCriticalVolume;
+ double fRetain = StemType.WT.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;
+ }
+
+ protected void updateGrowthRateBasedOnOtherNBs(Simulation sim) {
+ int nbSignal;
+ if (pdeLike) {
+ nbSignal = getNBsInSimulation(sim).size(); // include self in average
+ } else {
+ nbSignal = getNBNeighbors(sim).size(); // only look at neighbors
+ }
+ double np = Math.max(0.0, (double) nbSignal);
+
+ 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);
+ // System.out.println(
+ // "in getMUDDivisionPlane, default Normal = ("
+ // + defaultNormal.getX()
+ // + ", "
+ // + +defaultNormal.getY()
+ // + ", "
+ // + +defaultNormal.getZ()
+ // + ", "
+ // + ")");
+ 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() * volumeBasedCriticalVolumeMultiplier,
+ 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() * volumeBasedCriticalVolumeMultiplier,
+ 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/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/parameter.potts.xml b/src/arcade/potts/parameter.potts.xml
index ebc46e0da..c3d8d3b38 100644
--- a/src/arcade/potts/parameter.potts.xml
+++ b/src/arcade/potts/parameter.potts.xml
@@ -64,6 +64,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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/PottsModuleFlyGMCDifferentiationTest.java b/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiationTest.java
index c1b04e272..1334d9922 100644
--- a/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiationTest.java
+++ b/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiationTest.java
@@ -133,7 +133,8 @@ final void tearDown() {
@Test
public void addCell_called_callsExpectedMethods() {
- // When the module calls make() on the cell, return Quiescent PottsCellContainer mock
+ // 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);
@@ -150,7 +151,7 @@ public void addCell_called_callsExpectedMethods() {
verify(grid).addObject(newCell, null);
verify(potts).register(newCell);
- verify(newCell).reset(dummyIDs, dummyRegions);
+ verify(newCell).initialize(dummyIDs, dummyRegions);
verify(newCell).schedule(schedule);
verify(grid).removeObject(gmcCell, location);
@@ -161,7 +162,161 @@ public void addCell_called_callsExpectedMethods() {
(PottsCellFlyNeuron) constructed.convert(cellFactory, location, random);
verify(grid).addObject(diffCell, null);
verify(potts).register(diffCell);
- verify(diffCell).reset(dummyIDs, dummyRegions);
+ 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/PottsModuleFlyStemProliferationTest.java b/test/arcade/potts/agent/module/PottsModuleFlyStemProliferationTest.java
new file mode 100644
index 000000000..9b263c945
--- /dev/null
+++ b/test/arcade/potts/agent/module/PottsModuleFlyStemProliferationTest.java
@@ -0,0 +1,1280 @@
+package arcade.potts.agent.module;
+
+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.MiniBox;
+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);
+ when(parameters.getDouble("CRITICAL_VOLUME")).thenReturn(100.0);
+ }
+
+ @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);
+ }
+
+ // 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_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_returnsScaledValue() {
+ PottsLocation gmcLoc = mock(PottsLocation.class);
+ when(gmcLoc.getVolume()).thenReturn(50.0);
+ when(stemCell.getCriticalVolume()).thenReturn(100.0);
+ when(stemCell.getStemType()).thenReturn(PottsCellFlyStem.StemType.WT);
+
+ MiniBox popParametersMiniBox = mock(MiniBox.class);
+ when(popParametersMiniBox.getDouble("proliferation/SIZE_TARGET")).thenReturn(2.0);
+
+ when(sim.getCellFactory()).thenReturn(factory);
+ when(factory.getParameters(stemCellPop)).thenReturn(popParametersMiniBox);
+
+ when(parameters.getInt("proliferation/VOLUME_BASED_CRITICAL_VOLUME")).thenReturn(1);
+ when(parameters.getDouble("proliferation/VOLUME_BASED_CRITICAL_VOLUME_MULTIPLIER"))
+ .thenReturn(1.5);
+
+ module = new PottsModuleFlyStemProliferation(stemCell);
+
+ double result = module.calculateGMCDaughterCellCriticalVolume(gmcLoc);
+ assertEquals(75.0, result, EPSILON); // 50 * 1.5
+ }
+
+ // 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 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() {
+ // V_ref uses WT fRetain and population critVol regardless of cell type.
+ // Cell critVol is intentionally different from population critVol to show
+ // that V_ref uses the population parameter, not the per-cell value.
+ // populationCritVol (CRITICAL_VOLUME param) = 100, cell.getCriticalVolume() = 200
+ // fRetain = WT.splitOffsetPercentY / 100 = 86 / 100 = 0.86
+ // V_div = sizeTarget * populationCritVol = 1.2 * 100 = 120
+ // V_ref = 120 * (0.86 + 1) / 2 = 120 * 0.93 = 111.6
+ when(stemCell.getStemType()).thenReturn(PottsCellFlyStem.StemType.MUDMUT);
+ when(stemCell.getCriticalVolume()).thenReturn(200.0); // differs from CRITICAL_VOLUME param
+ module = new PottsModuleFlyStemProliferation(stemCell);
+ assertEquals(111.6, module.computeEquilibriumVolume(), EPSILON);
+ }
+
+ @Test
+ public void computeEquilibriumVolume_differentSizeTarget_scalesCorrectly() {
+ // V_div = sizeTarget * populationCritVol = 2.0 * 100 = 200
+ // V_ref = 200 * (0.86 + 1) / 2 = 200 * 0.93 = 186.0
+ when(parameters.getDouble("proliferation/SIZE_TARGET")).thenReturn(2.0);
+ module = new PottsModuleFlyStemProliferation(stemCell);
+ assertEquals(186.0, module.computeEquilibriumVolume(), EPSILON);
+ }
+
+ @Test
+ public void computeEquilibriumVolume_differentCritVol_scalesCorrectly() {
+ // V_ref scales with CRITICAL_VOLUME parameter (population-level), not
+ // cell.getCriticalVolume().
+ // CRITICAL_VOLUME param = 200, cell.getCriticalVolume() = 100 (setup default, intentionally
+ // different)
+ // 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(parameters.getDouble("CRITICAL_VOLUME")).thenReturn(200.0);
+ module = new PottsModuleFlyStemProliferation(stemCell);
+ assertEquals(186.0, module.computeEquilibriumVolume(), 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 + 1 self = 5 (K = 4, n = 2 → hill = 16/41 → 12 * 16/41)
+ 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(12.0 * 16.0 / 41.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 (K = 3, n = 2 → 9/(9+49)=9/58 → 20 * 9/58)
+ 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(20.0 * 9.0 / 58.0, module.cellGrowthRate, 1e-6);
+ verify(module, times(1)).getNBsInSimulation(sim);
+ verify(module, never()).getNBNeighbors(sim);
+ }
+
+ @Test
+ public void updateGrowthRateBasedOnOtherNBs_KZeroAndOnlySelf_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));
+
+ // 0 neighbors + 1 self = np=1; K=0 guard: Kn=0 & np>0 → hillRepression=0.0
+ doReturn(new HashSet()).when(module).getNBNeighbors(sim);
+
+ module.updateGrowthRateBasedOnOtherNBs(sim);
+
+ assertEquals(0.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));
+
+ // 2 neighbors + 1 self = np=3 → R = K/(K+N) = 4/(4+3) = 4/7
+ 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 * 4.0 / 7.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.");
+ }
+
+ // TODO: Have Danielle rename and fix
+ // @Test
+ // void daughterStem_DeterministicTrue() {
+ // // Mock parameters
+ // when(parameters.getString("proliferation/HAS_DETERMINISTIC_DIFFERENTIATION"))
+ // .thenReturn("TRUE");
+ //
+ // when(parameters.getString("proliferation/DIFFERENTIATION_RULESET")).thenReturn("volume");
+ // when(parameters.getDouble("proliferation/DIFFERENTIATION_RULESET_EQUALITY_RANGE"))
+ // .thenReturn(0.1);
+
+ // // Mock cell type + division plane normal vector
+ // Plane plane = mock(Plane.class);
+ // when(plane.getUnitNormalVector()).thenReturn(new Vector(1.0, 0, 0));
+
+ // // Construct module
+ // PottsModuleFlyStemProliferation module = new
+ // PottsModuleFlyStemProliferation(stemCell);
+
+ // // Call
+ // boolean result = module.daughterStem(stemLoc, daughterLoc, plane);
+
+ // // Verify
+ // assertTrue(
+ // result,
+ // "Expected daughterStemWrapper to return true for deterministic orientation");
+ // }
+
+ // @Test
+ // void testDaughterStem_DeterministicFalse() {
+ // when(parameters.getString("proliferation/HAS_DETERMINISTIC_DIFFERENTIATION"))
+ // .thenReturn("TRUE");
+ //
+ // when(parameters.getString("proliferation/DIFFERENTIATION_RULESET")).thenReturn("volume");
+ // when(parameters.getDouble("proliferation/DIFFERENTIATION_RULESET_EQUALITY_RANGE"))
+ // .thenReturn(0.1);
+
+ // Plane plane = mock(Plane.class);
+ // when(plane.getUnitNormalVector()).thenReturn(new Vector(0, 1.0, 0));
+
+ // PottsModuleFlyStemProliferation module = new
+ // PottsModuleFlyStemProliferation(stemCell);
+
+ // boolean result = module.daughterStem(stemLoc, daughterLoc, plane);
+
+ // assertFalse(result, "Expected false when division plane normal is not (1,0,0)");
+ // }
+
+ @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");
+ }
+}
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/env/location/PottsLocation3DTest.java b/test/arcade/potts/env/location/PottsLocation3DTest.java
index a971382d3..928741174 100644
--- a/test/arcade/potts/env/location/PottsLocation3DTest.java
+++ b/test/arcade/potts/env/location/PottsLocation3DTest.java
@@ -302,11 +302,13 @@ public void split_balanceableLocationRandomOne_returnsList() {
@Test
public void getOffsetInApicalFrame_called_raisesUnsupportedOperationException() {
- PottsLocation3D loc = new PottsLocation3D(voxelListAB);
- Vector apicalAxis = new Vector(0, 1, 0);
- ArrayList offsets = new ArrayList<>();
- assertThrows(
- UnsupportedOperationException.class,
- () -> loc.getOffsetInApicalFrame(offsets, apicalAxis));
+ {
+ PottsLocation3D loc = new PottsLocation3D(voxelListAB);
+ Vector apicalAxis = new Vector(0, 1, 0);
+ ArrayList offsets = new ArrayList<>();
+ assertThrows(
+ UnsupportedOperationException.class,
+ () -> loc.getOffsetInApicalFrame(offsets, apicalAxis));
+ }
}
}
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);