criticalRegionVolumes;
@@ -295,6 +295,15 @@ public double getCriticalVolume() {
return criticalVolume;
}
+ /**
+ * Sets the critical volume of the cell.
+ *
+ * @param newCriticalVolume the new critical volume
+ */
+ public void setCriticalVolume(double newCriticalVolume) {
+ criticalVolume = newCriticalVolume;
+ }
+
/**
* Gets the critical volume for a region.
*
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 11a75f068..f88ad4fc2 100644
--- a/src/arcade/potts/agent/cell/PottsCellFlyGMC.java
+++ b/src/arcade/potts/agent/cell/PottsCellFlyGMC.java
@@ -13,8 +13,8 @@
* 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 differentiation of the parent cell is handled by the {@link
- * PottsModuleFlyGMCDifferentiation} module. The basal apoptosis rate of this cell should be set to
- * 0 in the setup file.
+ * 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..44aa96ba2
--- /dev/null
+++ b/src/arcade/potts/agent/cell/PottsCellFlyStem.java
@@ -0,0 +1,163 @@
+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, 75, 0, 0.25),
+
+ /** mud Mutant stem cell. */
+ MUDMUT(50, 50, -90, 0.5);
+
+ /** 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 stem cell's critical volume that will be the daughter cell's
+ * critical volume.
+ */
+ 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
+ * @param daughterCellCriticalVolumeProportion proportion of the stem cell's critical volume
+ * that will be the daughter cell's critical volume
+ */
+ StemType(
+ int splitOffsetPercentX,
+ int splitOffsetPercentY,
+ double splitDirectionRotation,
+ double daughterCellCriticalVolumeProportion) {
+ this.splitOffsetPercentX = splitOffsetPercentX;
+ this.splitOffsetPercentY = splitOffsetPercentY;
+ this.splitDirectionRotation = splitDirectionRotation;
+ this.daughterCellCriticalVolumeProportion = daughterCellCriticalVolumeProportion;
+ }
+ }
+
+ /** 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/cell/PottsCellStem.java b/src/arcade/potts/agent/cell/PottsCellStem.java
index 6d0d15e4e..0bf5141ff 100644
--- a/src/arcade/potts/agent/cell/PottsCellStem.java
+++ b/src/arcade/potts/agent/cell/PottsCellStem.java
@@ -9,7 +9,7 @@
import arcade.potts.agent.module.PottsModuleApoptosisSimple;
import arcade.potts.agent.module.PottsModuleAutosis;
import arcade.potts.agent.module.PottsModuleNecrosis;
-import arcade.potts.agent.module.PottsModuleProliferationSimple;
+import arcade.potts.agent.module.PottsModuleProliferationWithCellCycleCheckSimple;
import arcade.potts.agent.module.PottsModuleQuiescence;
import static arcade.potts.util.PottsEnums.Region;
import static arcade.potts.util.PottsEnums.State;
@@ -73,7 +73,7 @@ void setStateModule(CellState newState) {
module = new PottsModuleQuiescence(this);
break;
case PROLIFERATIVE:
- module = new PottsModuleProliferationSimple(this);
+ module = new PottsModuleProliferationWithCellCycleCheckSimple(this);
break;
case APOPTOTIC:
module = new PottsModuleApoptosisSimple(this);
diff --git a/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java b/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java
index ec7cdba9c..dc32c6cd0 100644
--- a/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java
+++ b/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java
@@ -14,11 +14,13 @@
import arcade.potts.util.PottsEnums.State;
/**
- * Implementation of {@link PottsModuleProliferationSimple} for fly GMC agents. These cells divide
- * into two {@link PottsCellFlyNeuron} cells. The links must be set in the setup file so that 100%
- * of the daughter cells are Neurons.
+ * Implementation of {@link PottsModuleProliferationVolumeBasedDivision} for fly GMC agents. These
+ * cells divide into two {@link PottsCellFlyNeuron} cells. The links must be set in the setup file
+ * so that 100% of the daughter cells are Neurons.
*/
-public class PottsModuleFlyGMCDifferentiation extends PottsModuleProliferationSimple {
+public class PottsModuleFlyGMCDifferentiation extends PottsModuleProliferationVolumeBasedDivision {
+
+ Boolean pdeLike;
/**
* Creates a fly GMC proliferation module.
@@ -27,8 +29,18 @@ public class PottsModuleFlyGMCDifferentiation extends PottsModuleProliferationSi
*/
public PottsModuleFlyGMCDifferentiation(PottsCellFlyGMC cell) {
super(cell);
+ pdeLike = (cell.getParameters().getInt("proliferation/PDELIKE") != 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.
+ *
+ * @param random the random number generator
+ * @param sim the simulation instance
+ */
@Override
void addCell(MersenneTwisterFast random, Simulation sim) {
Potts potts = ((PottsSimulation) sim).getPotts();
@@ -82,4 +94,42 @@ void addCell(MersenneTwisterFast random, Simulation sim) {
differentiatedGMC.reset(potts.ids, potts.regions);
differentiatedGMC.schedule(sim.getSchedule());
}
+
+ public void updateGrowthRate(Simulation sim) {
+ if (!dynamicGrowthRateVolume) {
+ cellGrowthRate = cellGrowthRateBase;
+ } else {
+ if (!pdeLike) {
+ updateCellVolumeBasedGrowthRate(
+ cell.getLocation().getVolume(), cell.getCriticalVolume());
+ } else {
+ // PDE-like: use population-wide averages for GMCs (same pop as this cell)
+ 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;
+ updateCellVolumeBasedGrowthRate(avgVolume, avgCritVol);
+ System.out.println("GMC " + cell.getID() + "growth rate = " + cellGrowthRate);
+ }
+ }
+ }
}
diff --git a/src/arcade/potts/agent/module/PottsModuleFlyStemProliferation.java b/src/arcade/potts/agent/module/PottsModuleFlyStemProliferation.java
new file mode 100644
index 000000000..dc679831a
--- /dev/null
+++ b/src/arcade/potts/agent/module/PottsModuleFlyStemProliferation.java
@@ -0,0 +1,672 @@
+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;
+
+ 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();
+
+ 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) {
+ if (pdeLike == false) {
+ updateCellVolumeBasedGrowthRate(
+ cell.getLocation().getVolume(), cell.getCriticalVolume());
+ } else {
+ HashSet nbsInSimulation = getNBsInSimulation(sim);
+ double volSum = 0.0;
+ double critVolSum = 0.0;
+ for (PottsCellFlyStem nb : nbsInSimulation) {
+ volSum += nb.getLocation().getVolume();
+ critVolSum += nb.getCriticalVolume();
+ }
+ double avgVolume = volSum / nbsInSimulation.size();
+ double avgCritVol = critVolSum / nbsInSimulation.size();
+ updateCellVolumeBasedGrowthRate(avgVolume, avgCritVol);
+ }
+ }
+
+ /**
+ * 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 npRaw;
+ if (pdeLike) {
+ npRaw = getNBsInSimulation(sim).size();
+ } else {
+ npRaw = getNBNeighbors(sim).size();
+ }
+ double np = Math.max(0.0, (double) npRaw);
+
+ 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) < 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) {
+ cell.reset(potts.ids, potts.regions);
+ int newID = sim.getID();
+ double criticalVol;
+ if (volumeBasedCriticalVolume) {
+ criticalVol =
+ Math.max(
+ daughterLoc.getVolume() * volumeBasedCriticalVolumeMultiplier,
+ initialSize / 2);
+ cell.setCriticalVolume(criticalVol);
+ } else {
+ criticalVol = cell.getCriticalVolume();
+ }
+ 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 / 2);
+ return criticalVol;
+ } else {
+ criticalVol =
+ ((PottsCellFlyStem) cell).getCriticalVolume()
+ * sizeTarget
+ * ((PottsCellFlyStem) cell)
+ .getStemType()
+ .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/PottsModuleProliferation.java b/src/arcade/potts/agent/module/PottsModuleProliferation.java
index c1838e53e..cda674b14 100644
--- a/src/arcade/potts/agent/module/PottsModuleProliferation.java
+++ b/src/arcade/potts/agent/module/PottsModuleProliferation.java
@@ -1,115 +1,26 @@
package arcade.potts.agent.module;
import ec.util.MersenneTwisterFast;
-import arcade.core.agent.cell.CellContainer;
-import arcade.core.env.location.Location;
import arcade.core.sim.Simulation;
import arcade.potts.agent.cell.PottsCell;
-import arcade.potts.env.location.PottsLocation;
-import arcade.potts.sim.Potts;
-import arcade.potts.sim.PottsSimulation;
-import static arcade.potts.util.PottsEnums.Phase;
-import static arcade.potts.util.PottsEnums.State;
-/**
- * Extension of {@link PottsModule} for proliferation.
- *
- * During proliferation, cells cycle through G1, S, G2, and M phases. Once the cell complete M
- * phase, it divides to create a new daughter cell.
- */
+/** Abstract extention of {@link PottsModule} for proliferation modules. */
public abstract class PottsModuleProliferation extends PottsModule {
+
/**
- * Creates a proliferation {@code Module} for the given {@link PottsCell}.
+ * Creates a proliferation module.
*
- * @param cell the {@link PottsCell} the module is associated with
+ * @param cell the cell to which this module is attached
*/
public PottsModuleProliferation(PottsCell cell) {
super(cell);
- setPhase(Phase.PROLIFERATIVE_G1);
}
- /**
- * Calls the step method for the current simple phase.
- *
- * @param random the random number generator
- * @param sim the simulation instance
- */
- public void step(MersenneTwisterFast random, Simulation sim) {
- switch (phase) {
- case PROLIFERATIVE_G1:
- stepG1(random);
- break;
- case PROLIFERATIVE_S:
- stepS(random);
- break;
- case PROLIFERATIVE_G2:
- stepG2(random);
- break;
- case PROLIFERATIVE_M:
- stepM(random, sim);
- break;
- default:
- break;
- }
- }
-
- /**
- * Performs actions for G1 phase.
- *
- * @param random the random number generator
- */
- abstract void stepG1(MersenneTwisterFast random);
-
- /**
- * Performs actions for S phase.
- *
- * @param random the random number generator
- */
- abstract void stepS(MersenneTwisterFast random);
-
- /**
- * Performs actions for G2 phase.
- *
- * @param random the random number generator
- */
- abstract void stepG2(MersenneTwisterFast random);
-
- /**
- * Performs actions for M phase.
- *
- * @param random the random number generator
- * @param sim the simulation instance
- */
- abstract void stepM(MersenneTwisterFast random, Simulation sim);
-
/**
* Adds a cell to the simulation.
*
- *
The cell location is split, along with any regions. The new cell is created, initialized,
- * and added to the schedule. Both cells are reset and remain in the proliferative state.
- *
* @param random the random number generator
* @param sim the simulation instance
*/
- void addCell(MersenneTwisterFast random, Simulation sim) {
- Potts potts = ((PottsSimulation) sim).getPotts();
-
- // Split current location.
- Location newLocation = ((PottsLocation) cell.getLocation()).split(random);
-
- // Reset current cell.
- cell.reset(potts.ids, potts.regions);
-
- // Create and schedule new cell.
- int newID = sim.getID();
- CellContainer newContainer = cell.make(newID, State.PROLIFERATIVE, random);
- PottsCell newCell =
- (PottsCell)
- newContainer.convert(
- sim.getCellFactory(), newLocation, random, cell.getParameters());
- sim.getGrid().addObject(newCell, null);
- potts.register(newCell);
- newCell.reset(potts.ids, potts.regions);
- newCell.schedule(sim.getSchedule());
- }
+ abstract void addCell(MersenneTwisterFast random, Simulation sim);
}
diff --git a/src/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivision.java b/src/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivision.java
new file mode 100644
index 000000000..97c8cb101
--- /dev/null
+++ b/src/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivision.java
@@ -0,0 +1,76 @@
+package arcade.potts.agent.module;
+
+import ec.util.MersenneTwisterFast;
+import arcade.core.sim.Simulation;
+import arcade.core.util.Parameters;
+import arcade.potts.agent.cell.PottsCell;
+import arcade.potts.agent.cell.PottsCellFlyNeuron;
+import arcade.potts.util.PottsEnums.Phase;
+
+/**
+ * Implementation of {@link PottsModule} 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 abstract class PottsModuleProliferationVolumeBasedDivision extends PottsModuleProliferation {
+
+ /** 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
+ * * SIZE_TARGET * SIZE_CHECKPOINT to divide).
+ */
+ 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.
+ *
+ * @param cell the cell to which this module is attached
+ */
+ public PottsModuleProliferationVolumeBasedDivision(PottsCell cell) {
+ super(cell);
+ Parameters parameters = cell.getParameters();
+ sizeTarget = parameters.getDouble("proliferation/SIZE_TARGET");
+ 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/agent/module/PottsModuleProliferationWithCellCycleCheck.java b/src/arcade/potts/agent/module/PottsModuleProliferationWithCellCycleCheck.java
new file mode 100644
index 000000000..852516427
--- /dev/null
+++ b/src/arcade/potts/agent/module/PottsModuleProliferationWithCellCycleCheck.java
@@ -0,0 +1,110 @@
+package arcade.potts.agent.module;
+
+import ec.util.MersenneTwisterFast;
+import arcade.core.agent.cell.CellContainer;
+import arcade.core.env.location.Location;
+import arcade.core.sim.Simulation;
+import arcade.potts.agent.cell.PottsCell;
+import arcade.potts.env.location.PottsLocation;
+import arcade.potts.sim.Potts;
+import arcade.potts.sim.PottsSimulation;
+import static arcade.potts.util.PottsEnums.Phase;
+import static arcade.potts.util.PottsEnums.State;
+
+/**
+ * Extension of {@link PottsModule} for proliferation.
+ *
+ *
During proliferation, cells cycle through G1, S, G2, and M phases. Once the cell complete M
+ * phase, it divides to create a new daughter cell.
+ */
+public abstract class PottsModuleProliferationWithCellCycleCheck extends PottsModuleProliferation {
+ /**
+ * Creates a proliferation {@code Module} for the given {@link PottsCell}.
+ *
+ * @param cell the {@link PottsCell} the module is associated with
+ */
+ public PottsModuleProliferationWithCellCycleCheck(PottsCell cell) {
+ super(cell);
+ setPhase(Phase.PROLIFERATIVE_G1);
+ }
+
+ @Override
+ public void step(MersenneTwisterFast random, Simulation sim) {
+ switch (phase) {
+ case PROLIFERATIVE_G1:
+ stepG1(random);
+ break;
+ case PROLIFERATIVE_S:
+ stepS(random);
+ break;
+ case PROLIFERATIVE_G2:
+ stepG2(random);
+ break;
+ case PROLIFERATIVE_M:
+ stepM(random, sim);
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Performs actions for G1 phase.
+ *
+ * @param random the random number generator
+ */
+ abstract void stepG1(MersenneTwisterFast random);
+
+ /**
+ * Performs actions for S phase.
+ *
+ * @param random the random number generator
+ */
+ abstract void stepS(MersenneTwisterFast random);
+
+ /**
+ * Performs actions for G2 phase.
+ *
+ * @param random the random number generator
+ */
+ abstract void stepG2(MersenneTwisterFast random);
+
+ /**
+ * Performs actions for M phase.
+ *
+ * @param random the random number generator
+ * @param sim the simulation instance
+ */
+ abstract void stepM(MersenneTwisterFast random, Simulation sim);
+
+ /**
+ * Adds a cell to the simulation.
+ *
+ *
The cell location is split, along with any regions. The new cell is created, initialized,
+ * and added to the schedule. Both cells are reset and remain in the proliferative state.
+ *
+ * @param random the random number generator
+ * @param sim the simulation instance
+ */
+ void addCell(MersenneTwisterFast random, Simulation sim) {
+ Potts potts = ((PottsSimulation) sim).getPotts();
+
+ // Split current location.
+ Location newLocation = ((PottsLocation) cell.getLocation()).split(random);
+
+ // Reset current cell.
+ cell.reset(potts.ids, potts.regions);
+
+ // Create and schedule new cell.
+ int newID = sim.getID();
+ CellContainer newContainer = cell.make(newID, State.PROLIFERATIVE, random);
+ PottsCell newCell =
+ (PottsCell)
+ newContainer.convert(
+ sim.getCellFactory(), newLocation, random, cell.getParameters());
+ sim.getGrid().addObject(newCell, null);
+ potts.register(newCell);
+ newCell.reset(potts.ids, potts.regions);
+ newCell.schedule(sim.getSchedule());
+ }
+}
diff --git a/src/arcade/potts/agent/module/PottsModuleProliferationSimple.java b/src/arcade/potts/agent/module/PottsModuleProliferationWithCellCycleCheckSimple.java
similarity index 96%
rename from src/arcade/potts/agent/module/PottsModuleProliferationSimple.java
rename to src/arcade/potts/agent/module/PottsModuleProliferationWithCellCycleCheckSimple.java
index 956f48651..4b22c864a 100644
--- a/src/arcade/potts/agent/module/PottsModuleProliferationSimple.java
+++ b/src/arcade/potts/agent/module/PottsModuleProliferationWithCellCycleCheckSimple.java
@@ -12,8 +12,9 @@
import static arcade.potts.util.PottsEnums.Region;
import static arcade.potts.util.PottsEnums.State;
-/** Extension of {@link PottsModuleProliferation} with Poisson transitions. */
-public class PottsModuleProliferationSimple extends PottsModuleProliferation {
+/** Extension of {@link PottsModuleProliferationWithCellCycleCheck} with Poisson transitions. */
+public class PottsModuleProliferationWithCellCycleCheckSimple
+ extends PottsModuleProliferationWithCellCycleCheck {
/** Threshold for critical volume size checkpoint. */
static final double SIZE_CHECKPOINT = 0.95;
@@ -64,7 +65,7 @@ public class PottsModuleProliferationSimple extends PottsModuleProliferation {
*
* @param cell the {@link PottsCell} the module is associated with
*/
- public PottsModuleProliferationSimple(PottsCell cell) {
+ public PottsModuleProliferationWithCellCycleCheckSimple(PottsCell cell) {
super(cell);
Parameters parameters = cell.getParameters();
diff --git a/src/arcade/potts/env/location/PottsLocation.java b/src/arcade/potts/env/location/PottsLocation.java
index ddb5b1d94..ee15838a4 100644
--- a/src/arcade/potts/env/location/PottsLocation.java
+++ b/src/arcade/potts/env/location/PottsLocation.java
@@ -10,6 +10,9 @@
import arcade.core.env.location.LocationContainer;
import arcade.core.util.Plane;
import arcade.core.util.Utilities;
+import arcade.core.util.Vector;
+import arcade.potts.util.PottsEnums.Direction;
+import arcade.potts.util.PottsEnums.Region;
import static arcade.potts.util.PottsEnums.Direction;
import static arcade.potts.util.PottsEnums.Region;
@@ -605,6 +608,16 @@ void updateCenter(int x, int y, int z, int change) {
*/
abstract ArrayList getSelected(Voxel focus, double n);
+ /**
+ * Gets the voxel at specified percentage offsets along the location's axes with the provided
+ * ApicalAxis considered to be pointing up the Y axis.
+ *
+ * @param offsets the percent offsets along the location's axes
+ * @param apicalAxis the axis considered to be pointing up along the Y axis
+ * @return the voxel at the specified offset in the frame of the apical axis
+ */
+ abstract Voxel getOffsetInApicalFrame(ArrayList offsets, Vector apicalAxis);
+
/**
* Gets the direction of the slice orthagonal to the direction with the smallest diameter.
*
diff --git a/src/arcade/potts/env/location/PottsLocation2D.java b/src/arcade/potts/env/location/PottsLocation2D.java
index 0a8fcc986..f61165147 100644
--- a/src/arcade/potts/env/location/PottsLocation2D.java
+++ b/src/arcade/potts/env/location/PottsLocation2D.java
@@ -1,7 +1,10 @@
package arcade.potts.env.location;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
import java.util.HashMap;
+import arcade.core.util.Vector;
import static arcade.potts.util.PottsEnums.Direction;
/** Concrete implementation of {@link PottsLocation} for 2D. */
@@ -64,4 +67,57 @@ Direction getSlice(Direction direction, HashMap diameters) {
ArrayList getSelected(Voxel focus, double n) {
return Location2D.getSelected(voxels, focus, n);
}
+
+ /**
+ * Gets the voxel at specified percentage offsets along the location's X and Y axes with the
+ * provided apicalAxis considered to be pointing up the Y axis. Returns null if this
+ * PottsLocation2D contains no voxels.
+ *
+ * @param offsets the percent offsets along the location's X and Y axes
+ * @param apicalAxis the axis considered to be pointing up along the Y axis
+ * @return the voxel through which the plane of division will pass
+ */
+ @Override
+ public Voxel getOffsetInApicalFrame(ArrayList offsets, Vector apicalAxis) {
+ if (voxels.isEmpty()) {
+ return null;
+ }
+ if (offsets == null || offsets.size() != 2) {
+ throw new IllegalArgumentException("Offsets must be 2 integers.");
+ }
+
+ // Normalize axes
+ Vector yAxis = Vector.normalizeVector(apicalAxis);
+ Vector xAxis = Vector.normalizeVector(new Vector(apicalAxis.getY(), -apicalAxis.getX(), 0));
+
+ // Project voxels onto apical axis and group by rounded projection
+ HashMap> apicalBands = new HashMap<>();
+ ArrayList apicalKeys = new ArrayList<>();
+
+ for (Voxel v : voxels) {
+ Vector pos = new Vector(v.x, v.y, 0);
+ double apicalProj = Vector.dotProduct(pos, yAxis);
+ int roundedProj = (int) Math.round(apicalProj);
+ apicalBands.computeIfAbsent(roundedProj, k -> new ArrayList<>()).add(v);
+ apicalKeys.add(roundedProj);
+ }
+
+ // Sort apical keys and choose percentile
+ Collections.sort(apicalKeys);
+ int yIndex =
+ Math.min(
+ apicalKeys.size() - 1,
+ (int) ((offsets.get(1) / 100.0) * apicalKeys.size()));
+ int targetApicalKey = apicalKeys.get(yIndex);
+
+ ArrayList band = apicalBands.get(targetApicalKey);
+ if (band == null || band.isEmpty()) {
+ return null;
+ }
+ // Project to orthogonal axis within the band and sort
+ band.sort(
+ Comparator.comparingDouble(v -> Vector.dotProduct(new Vector(v.x, v.y, 0), xAxis)));
+ int xIndex = Math.min(band.size() - 1, (int) ((offsets.get(0) / 100.0) * band.size()));
+ return band.get(xIndex);
+ }
}
diff --git a/src/arcade/potts/env/location/PottsLocation3D.java b/src/arcade/potts/env/location/PottsLocation3D.java
index b9e9c1aec..c6ad40cd1 100644
--- a/src/arcade/potts/env/location/PottsLocation3D.java
+++ b/src/arcade/potts/env/location/PottsLocation3D.java
@@ -2,6 +2,8 @@
import java.util.ArrayList;
import java.util.HashMap;
+import arcade.core.util.Vector;
+import arcade.potts.util.PottsEnums.Direction;
import static arcade.potts.util.PottsEnums.Direction;
/** Concrete implementation of {@link PottsLocation} for 3D. */
@@ -64,4 +66,10 @@ Direction getSlice(Direction direction, HashMap diameters) {
ArrayList getSelected(Voxel focus, double n) {
return Location3D.getSelected(voxels, focus, n);
}
+
+ @Override
+ Voxel getOffsetInApicalFrame(ArrayList offsets, Vector apicalAxis) {
+ throw new UnsupportedOperationException(
+ "getOffsetInApicalFrame is not implemented for PottsLocation3D");
+ }
}
diff --git a/src/arcade/potts/env/location/PottsLocations.java b/src/arcade/potts/env/location/PottsLocations.java
index 1b829e426..4ae66311e 100644
--- a/src/arcade/potts/env/location/PottsLocations.java
+++ b/src/arcade/potts/env/location/PottsLocations.java
@@ -6,6 +6,8 @@
import ec.util.MersenneTwisterFast;
import arcade.core.env.location.Location;
import arcade.core.env.location.LocationContainer;
+import arcade.core.util.Vector;
+import arcade.potts.util.PottsEnums.Region;
import static arcade.potts.util.PottsEnums.Region;
/**
@@ -286,4 +288,10 @@ Location separateVoxels(
return splitLocation;
}
+
+ @Override
+ Voxel getOffsetInApicalFrame(ArrayList offsets, Vector apicalAxis) {
+ throw new UnsupportedOperationException(
+ "getOffsetInApicalFrame is not implemented for PottsLocations");
+ }
}
diff --git a/src/arcade/potts/env/location/PottsLocations2D.java b/src/arcade/potts/env/location/PottsLocations2D.java
index 7877b24f9..52b5721db 100644
--- a/src/arcade/potts/env/location/PottsLocations2D.java
+++ b/src/arcade/potts/env/location/PottsLocations2D.java
@@ -2,6 +2,9 @@
import java.util.ArrayList;
import java.util.HashMap;
+import arcade.core.util.Vector;
+import arcade.potts.util.PottsEnums.Direction;
+import arcade.potts.util.PottsEnums.Region;
import static arcade.potts.util.PottsEnums.Direction;
import static arcade.potts.util.PottsEnums.Region;
@@ -70,4 +73,10 @@ Direction getSlice(Direction direction, HashMap diameters) {
ArrayList getSelected(Voxel focus, double n) {
return Location2D.getSelected(locations.get(Region.DEFAULT).voxels, focus, n);
}
+
+ @Override
+ Voxel getOffsetInApicalFrame(ArrayList offsets, Vector apicalAxis) {
+ throw new UnsupportedOperationException(
+ "getOffsetInApicalFrame is not implemented for PottsLocations2D");
+ }
}
diff --git a/src/arcade/potts/parameter.potts.xml b/src/arcade/potts/parameter.potts.xml
index ebc46e0da..b6e1c5858 100644
--- a/src/arcade/potts/parameter.potts.xml
+++ b/src/arcade/potts/parameter.potts.xml
@@ -64,6 +64,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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/cell/PottsCellStemTest.java b/test/arcade/potts/agent/cell/PottsCellStemTest.java
index 383ea7a13..9d876199a 100644
--- a/test/arcade/potts/agent/cell/PottsCellStemTest.java
+++ b/test/arcade/potts/agent/cell/PottsCellStemTest.java
@@ -10,7 +10,7 @@
import arcade.potts.agent.module.PottsModuleApoptosis;
import arcade.potts.agent.module.PottsModuleAutosis;
import arcade.potts.agent.module.PottsModuleNecrosis;
-import arcade.potts.agent.module.PottsModuleProliferation;
+import arcade.potts.agent.module.PottsModuleProliferationWithCellCycleCheck;
import arcade.potts.agent.module.PottsModuleQuiescence;
import arcade.potts.env.location.PottsLocation;
import static org.junit.jupiter.api.Assertions.*;
@@ -94,7 +94,7 @@ public void setState_givenState_updatesModule() {
assertTrue(cell.module instanceof PottsModuleQuiescence);
cell.setState(State.PROLIFERATIVE);
- assertTrue(cell.module instanceof PottsModuleProliferation);
+ assertTrue(cell.module instanceof PottsModuleProliferationWithCellCycleCheck);
cell.setState(State.APOPTOTIC);
assertTrue(cell.module instanceof PottsModuleApoptosis);
diff --git a/test/arcade/potts/agent/cell/PottsCellTest.java b/test/arcade/potts/agent/cell/PottsCellTest.java
index b4e75299c..76de36dba 100644
--- a/test/arcade/potts/agent/cell/PottsCellTest.java
+++ b/test/arcade/potts/agent/cell/PottsCellTest.java
@@ -652,6 +652,28 @@ public void getCriticalVolume_afterInitializeNoRegion_returnsZero() {
}
}
+ @Test
+ public void setCriticalVolume_calledWithRegions_correctlySetsCriticalVolume() {
+ PottsCell cell = new PottsCellMock(containerWithRegions, locationMock, parametersMock);
+ assertEquals(cellCriticalVolume, cell.getCriticalVolume());
+ double newCriticalVolume = randomDoubleBetween(10, 20);
+ cell.setCriticalVolume(newCriticalVolume);
+ assertEquals(newCriticalVolume, cell.getCriticalVolume());
+ cell.setCriticalVolume(cellCriticalVolume);
+ assertEquals(cellCriticalVolume, cell.getCriticalVolume());
+ }
+
+ @Test
+ public void setCriticalVolume_calledWithoutRegions_correctlySetsCriticalVolume() {
+ PottsCell cell = new PottsCellMock(containerWithoutRegions, locationMock, parametersMock);
+ assertEquals(cellCriticalVolume, cell.getCriticalVolume());
+ double newCriticalVolume = randomDoubleBetween(10, 20);
+ cell.setCriticalVolume(newCriticalVolume);
+ assertEquals(newCriticalVolume, cell.getCriticalVolume());
+ cell.setCriticalVolume(cellCriticalVolume);
+ assertEquals(cellCriticalVolume, cell.getCriticalVolume());
+ }
+
@Test
public void getCriticalHeight_beforeInitialize_returnsValue() {
assertEquals(cellCriticalHeight, cellWithoutRegions.getCriticalHeight(), EPSILON);
diff --git a/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiationTest.java b/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiationTest.java
index 2cb4df20c..b3babde5d 100644
--- a/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiationTest.java
+++ b/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiationTest.java
@@ -24,9 +24,10 @@
import arcade.potts.util.PottsEnums.State;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.*;
-import static arcade.potts.util.PottsEnums.Region;
-import static arcade.potts.util.PottsEnums.State;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockConstruction;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
public class PottsModuleFlyGMCDifferentiationTest {
private int[][][] dummyIDs;
@@ -104,20 +105,7 @@ public final void setupMocks() {
when(gmcCell.getCriticalRegionVolumes()).thenReturn(critRegionVolumes);
when(gmcCell.getCriticalRegionHeights()).thenReturn(critRegionHeights);
- // Stub parameters
parameters = mock(Parameters.class);
- when(parameters.getDouble("proliferation/RATE_G1")).thenReturn(1.0);
- when(parameters.getDouble("proliferation/RATE_S")).thenReturn(1.0);
- when(parameters.getDouble("proliferation/RATE_G2")).thenReturn(1.0);
- when(parameters.getDouble("proliferation/RATE_M")).thenReturn(1.0);
- when(parameters.getInt("proliferation/STEPS_G1")).thenReturn(1);
- when(parameters.getInt("proliferation/STEPS_S")).thenReturn(1);
- when(parameters.getInt("proliferation/STEPS_G2")).thenReturn(1);
- when(parameters.getInt("proliferation/STEPS_M")).thenReturn(1);
- when(parameters.getDouble("proliferation/CELL_GROWTH_RATE")).thenReturn(1.0);
- when(parameters.getDouble("proliferation/NUCLEUS_GROWTH_RATE")).thenReturn(1.0);
- when(parameters.getDouble("proliferation/BASAL_APOPTOSIS_RATE")).thenReturn(0.1);
- when(parameters.getDouble("proliferation/NUCLEUS_CONDENSATION_FRACTION")).thenReturn(0.5);
when(gmcCell.getParameters()).thenReturn(parameters);
random = mock(MersenneTwisterFast.class);
@@ -145,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);
@@ -153,7 +142,8 @@ public void addCell_called_callsExpectedMethods() {
when(container.convert(eq(cellFactory), eq(newLocation), any(MersenneTwisterFast.class)))
.thenReturn(newCell);
- PottsModuleFlyGMCDifferentiation module = new PottsModuleFlyGMCDifferentiation(gmcCell);
+ PottsModuleProliferationVolumeBasedDivision module =
+ new PottsModuleFlyGMCDifferentiation(gmcCell);
module.addCell(random, sim);
verify(location).split(random);
verify(gmcCell).reset(dummyIDs, dummyRegions);
@@ -175,4 +165,111 @@ public void addCell_called_callsExpectedMethods() {
verify(diffCell).reset(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_usesSelfVolumeAndCrit() {
+ 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.getInt("proliferation/PDELIKE")).thenReturn(0);
+
+ // Self values
+ 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);
+
+ org.mockito.Mockito.verify(module)
+ .updateCellVolumeBasedGrowthRate(
+ org.mockito.ArgumentMatchers.eq(30.0),
+ org.mockito.ArgumentMatchers.eq(150.0));
+ }
+
+ @Test
+ public void updateGrowthRate_dynamicOnPdeLikeTrue_usesAverageVolumeAndCritAcrossGMCs() {
+ // 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.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);
+
+ double expectedAvgVol = (30.0 + 10.0 + 20.0) / 3.0; // 20.0
+ double expectedAvgCrit = (150.0 + 100.0 + 200.0) / 3.0; // 150.0
+
+ org.mockito.Mockito.verify(module)
+ .updateCellVolumeBasedGrowthRate(
+ org.mockito.ArgumentMatchers.eq(expectedAvgVol),
+ org.mockito.ArgumentMatchers.eq(expectedAvgCrit));
+ }
}
diff --git a/test/arcade/potts/agent/module/PottsModuleFlyStemProliferationTest.java b/test/arcade/potts/agent/module/PottsModuleFlyStemProliferationTest.java
new file mode 100644
index 000000000..d4b1f66fb
--- /dev/null
+++ b/test/arcade/potts/agent/module/PottsModuleFlyStemProliferationTest.java
@@ -0,0 +1,1241 @@
+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);
+ }
+
+ @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(75); // 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(80); // 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 * .25 * 1.2), result, EPSILON); // 100 * 0.25 * 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"); // ⬅️ force rule-based path
+ 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); // ⬅️ relax CV match
+ 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(3);
+ when(nb10.getPop()).thenReturn(3);
+ when(nb11.getPop()).thenReturn(3);
+ when(nb12OtherPop.getPop()).thenReturn(99); // filtered
+
+ when(stemCell.getID()).thenReturn(42);
+
+ when(nb10.getPop()).thenReturn(stemCellPop); // match cell.getPop
+ when(nb11.getPop()).thenReturn(stemCellPop); // match cell.getPop
+ when(nb12OtherPop.getPop()).thenReturn(99); // no match
+
+ 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.");
+ }
+
+ @Test
+ public void updateVolumeBasedGrowthRate_pdeLikeFalse_usesCellVolume() {
+ // pdeLike = 0 → should call updateCellVolumeBasedGrowthRate with THIS cell's volume
+ when(parameters.getInt("proliferation/PDELIKE")).thenReturn(0);
+ when(parameters.getInt("proliferation/DYNAMIC_GROWTH_RATE_NB_CONTACT")).thenReturn(1);
+
+ // Make the current cell's volume distinctive so we can verify it
+ when(stemCell.getLocation()).thenReturn(stemLoc);
+ when(stemLoc.getVolume()).thenReturn(42.5);
+
+ module = spy(new PottsModuleFlyStemProliferation(stemCell));
+
+ // We only want to verify the value it was called with
+ doNothing().when(module).updateCellVolumeBasedGrowthRate(anyDouble(), anyDouble());
+ when(stemCell.getCriticalVolume()).thenReturn(100.0);
+
+ module.updateVolumeBasedGrowthRate(sim);
+
+ verify(module, times(1)).updateCellVolumeBasedGrowthRate(eq(42.5), eq(100.0));
+ verify(module, never()).getNBsInSimulation(any());
+ }
+
+ @Test
+ public void
+ updateVolumeBasedGrowthRate_pdeLikeTrue_usesAverageVolumeAndAverageCritVolAcrossNBs() {
+ // pdeLike = 1 (PDE-like) and dynamicGrowthRateNBContact must be 0 to avoid ctor exception
+ when(parameters.getInt("proliferation/PDELIKE")).thenReturn(1);
+ when(parameters.getInt("proliferation/DYNAMIC_GROWTH_RATE_NB_CONTACT")).thenReturn(0);
+
+ module = spy(new PottsModuleFlyStemProliferation(stemCell));
+
+ // NB mocks
+ PottsCellFlyStem nbA = mock(PottsCellFlyStem.class);
+ PottsCellFlyStem nbB = mock(PottsCellFlyStem.class);
+ PottsCellFlyStem nbC = mock(PottsCellFlyStem.class);
+
+ // Location mocks for each NB
+ 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);
+
+ // Critical volumes: 90, 110, 100 -> avg = 300/3 = 100
+ when(nbA.getCriticalVolume()).thenReturn(90.0);
+ when(nbB.getCriticalVolume()).thenReturn(110.0);
+ when(nbC.getCriticalVolume()).thenReturn(100.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; // 23.333333333333332
+ double expectedAvgCrit = (90.0 + 110.0 + 100.0) / 3.0; // 100.0
+
+ verify(module, times(1)).getNBsInSimulation(sim);
+ verify(module, times(1))
+ .updateCellVolumeBasedGrowthRate(eq(expectedAvgVol), eq(expectedAvgCrit));
+ }
+
+ @Test
+ public void updateGrowthRateBasedOnOtherNBs_pdeLikeFalse_usesNeighborsBranch() {
+ // pdeLike = 0 → neighbors branch
+ when(parameters.getInt("proliferation/PDELIKE")).thenReturn(0);
+ when(parameters.getInt("proliferation/DYNAMIC_GROWTH_RATE_NB_CONTACT")).thenReturn(1);
+
+ when(parameters.getDouble("proliferation/NB_CONTACT_HALF_MAX")).thenReturn(4.0);
+ when(parameters.getDouble("proliferation/NB_CONTACT_HILL_N")).thenReturn(2.0);
+ when(parameters.getDouble("proliferation/CELL_GROWTH_RATE")).thenReturn(12.0);
+
+ module = spy(new PottsModuleFlyStemProliferation(stemCell));
+
+ // N = 4 neighbors (K = 4, n = 2 → repression 0.5 → 12 * 0.5 = 6)
+ HashSet four = new HashSet<>();
+ for (int i = 0; i < 4; i++) {
+ PottsCellFlyStem n = mock(PottsCellFlyStem.class);
+ when(n.getID()).thenReturn(100 + i);
+ four.add(n);
+ }
+ doReturn(four).when(module).getNBNeighbors(sim);
+ // Make sure population path is not used
+ doReturn(new HashSet()).when(module).getNBsInSimulation(sim);
+
+ module.updateGrowthRateBasedOnOtherNBs(sim);
+
+ assertEquals(6.0, module.cellGrowthRate, 1e-6);
+ verify(module, times(1)).getNBNeighbors(sim);
+ verify(module, never()).getNBsInSimulation(sim);
+ }
+
+ @Test
+ public void updateGrowthRateBasedOnOtherNBs_pdeLikeTrue_usesPopulationBranch() {
+ // pdeLike = 1 and dynamicGrowthRateNBContact = 0 to avoid constructor exception
+ when(parameters.getInt("proliferation/PDELIKE")).thenReturn(1);
+ when(parameters.getInt("proliferation/DYNAMIC_GROWTH_RATE_NB_CONTACT")).thenReturn(0);
+
+ when(parameters.getDouble("proliferation/NB_CONTACT_HALF_MAX")).thenReturn(3.0);
+ when(parameters.getDouble("proliferation/NB_CONTACT_HILL_N")).thenReturn(2.0);
+ when(parameters.getDouble("proliferation/CELL_GROWTH_RATE")).thenReturn(20.0);
+
+ module = spy(new PottsModuleFlyStemProliferation(stemCell));
+
+ // N = 6 in-simulation (K = 3, n = 2 → 9/(9+36)=0.2 → 4.0)
+ HashSet six = new HashSet<>();
+ for (int i = 0; i < 6; i++) {
+ PottsCellFlyStem n = mock(PottsCellFlyStem.class);
+ when(n.getID()).thenReturn(200 + i);
+ six.add(n);
+ }
+ doReturn(new HashSet()).when(module).getNBNeighbors(sim);
+ doReturn(six).when(module).getNBsInSimulation(sim);
+
+ module.updateGrowthRateBasedOnOtherNBs(sim);
+
+ assertEquals(4.0, module.cellGrowthRate, 1e-6);
+ verify(module, times(1)).getNBsInSimulation(sim);
+ verify(module, never()).getNBNeighbors(sim);
+ }
+
+ @Test
+ public void updateGrowthRateBasedOnOtherNBs_KZeroandZeroNeighbors_returnsBase() {
+ when(parameters.getInt("proliferation/PDELIKE")).thenReturn(0);
+ when(parameters.getInt("proliferation/DYNAMIC_GROWTH_RATE_NB_CONTACT")).thenReturn(1);
+
+ when(parameters.getDouble("proliferation/NB_CONTACT_HALF_MAX")).thenReturn(0.0); // K = 0
+ when(parameters.getDouble("proliferation/NB_CONTACT_HILL_N")).thenReturn(2.0);
+ when(parameters.getDouble("proliferation/CELL_GROWTH_RATE")).thenReturn(10.0);
+
+ module = spy(new PottsModuleFlyStemProliferation(stemCell));
+
+ // N = 0 → with your guard, repression = 1.0 when K=0 & N=0
+ doReturn(new HashSet()).when(module).getNBNeighbors(sim);
+
+ module.updateGrowthRateBasedOnOtherNBs(sim);
+
+ assertEquals(10.0, module.cellGrowthRate, 1e-6);
+ }
+
+ @Test
+ public void updateGrowthRateBasedOnOtherNBs_KZeroandPositiveNeighbors_returnsZero() {
+ when(parameters.getInt("proliferation/PDELIKE")).thenReturn(0);
+ when(parameters.getInt("proliferation/DYNAMIC_GROWTH_RATE_NB_CONTACT")).thenReturn(1);
+
+ when(parameters.getDouble("proliferation/NB_CONTACT_HALF_MAX")).thenReturn(0.0); // K = 0
+ when(parameters.getDouble("proliferation/NB_CONTACT_HILL_N")).thenReturn(2.0);
+ when(parameters.getDouble("proliferation/CELL_GROWTH_RATE")).thenReturn(10.0);
+
+ module = spy(new PottsModuleFlyStemProliferation(stemCell));
+
+ // N > 0 → with your guard, repression = 0.0 when K=0 & N>0
+ HashSet one = new HashSet<>();
+ PottsCellFlyStem n = mock(PottsCellFlyStem.class);
+ when(n.getID()).thenReturn(999);
+ one.add(n);
+ doReturn(one).when(module).getNBNeighbors(sim);
+
+ module.updateGrowthRateBasedOnOtherNBs(sim);
+
+ assertEquals(0.0, module.cellGrowthRate, 1e-9);
+ }
+
+ @Test
+ public void updateGrowthRateBasedOnOtherNBs_hillExponentOne_linearCase() {
+ when(parameters.getInt("proliferation/PDELIKE")).thenReturn(0);
+ when(parameters.getInt("proliferation/DYNAMIC_GROWTH_RATE_NB_CONTACT")).thenReturn(1);
+
+ when(parameters.getDouble("proliferation/NB_CONTACT_HALF_MAX")).thenReturn(4.0);
+ when(parameters.getDouble("proliferation/NB_CONTACT_HILL_N")).thenReturn(1.0); // linear
+ when(parameters.getDouble("proliferation/CELL_GROWTH_RATE")).thenReturn(10.0);
+
+ module = spy(new PottsModuleFlyStemProliferation(stemCell));
+
+ // N = 2 → R = K/(K+N) = 4/(4+2) = 2/3
+ HashSet two = new HashSet<>();
+ for (int i = 0; i < 2; i++) {
+ PottsCellFlyStem nn = mock(PottsCellFlyStem.class);
+ when(nn.getID()).thenReturn(300 + i);
+ two.add(nn);
+ }
+ doReturn(two).when(module).getNBNeighbors(sim);
+
+ module.updateGrowthRateBasedOnOtherNBs(sim);
+
+ assertEquals(10.0 * (2.0 / 3.0), module.cellGrowthRate, 1e-6);
+ }
+
+ @Test
+ public void updateGrowthRateBasedOnOtherNBs_largeNeighbors_approachesZero() {
+ when(parameters.getInt("proliferation/PDELIKE")).thenReturn(0);
+ when(parameters.getInt("proliferation/DYNAMIC_GROWTH_RATE_NB_CONTACT")).thenReturn(1);
+
+ when(parameters.getDouble("proliferation/NB_CONTACT_HALF_MAX")).thenReturn(5.0);
+ when(parameters.getDouble("proliferation/NB_CONTACT_HILL_N")).thenReturn(3.0);
+ when(parameters.getDouble("proliferation/CELL_GROWTH_RATE")).thenReturn(7.0);
+
+ module = spy(new PottsModuleFlyStemProliferation(stemCell));
+
+ // N = 100 >> K = 5 → repression ~ 0
+ HashSet many = new HashSet<>();
+ for (int i = 0; i < 100; i++) {
+ PottsCellFlyStem nn = mock(PottsCellFlyStem.class);
+ when(nn.getID()).thenReturn(400 + i);
+ many.add(nn);
+ }
+ doReturn(many).when(module).getNBNeighbors(sim);
+
+ module.updateGrowthRateBasedOnOtherNBs(sim);
+
+ assertTrue(module.cellGrowthRate < 0.01, "Growth should be ~0 with very large N.");
+ }
+
+ // 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
new file mode 100644
index 000000000..4c41c1661
--- /dev/null
+++ b/test/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivisionTest.java
@@ -0,0 +1,174 @@
+package arcade.potts.agent.module;
+
+import org.junit.jupiter.api.Test;
+import ec.util.MersenneTwisterFast;
+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.mockito.Mockito.*;
+
+public class PottsModuleProliferationVolumeBasedDivisionTest {
+
+ static class PottsModuleProliferationVolumeBasedDivisionMock
+ extends PottsModuleProliferationVolumeBasedDivision {
+ boolean addCellCalled = false;
+ boolean growthRateUpdated = false;
+
+ PottsModuleProliferationVolumeBasedDivisionMock(PottsCellFlyGMC cell) {
+ super(cell);
+ }
+
+ @Override
+ void addCell(MersenneTwisterFast random, Simulation sim) {
+ addCellCalled = true;
+ }
+
+ @Override
+ public void updateGrowthRate(Simulation sim) {
+ growthRateUpdated = true;
+ }
+ }
+
+ @Test
+ public void step_belowCheckpoint_updatesTarget() {
+ 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(params.getDouble("proliferation/CELL_GROWTH_RATE")).thenReturn(4.0);
+ when(cell.getCriticalVolume()).thenReturn(100.0);
+ when(cell.getVolume()).thenReturn(50.0); // below checkpoint
+
+ PottsModuleProliferationVolumeBasedDivisionMock module =
+ new PottsModuleProliferationVolumeBasedDivisionMock(cell);
+
+ module.step(mock(MersenneTwisterFast.class), mock(Simulation.class));
+
+ verify(cell).updateTarget(4.0, 1.2);
+ assert module.growthRateUpdated : "growth rate should be updated on every step";
+ assert !module.addCellCalled : "addCell should not be called below checkpoint";
+ }
+
+ @Test
+ public void step_atOrAboveCheckpoint_triggersAddCell() {
+
+ 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(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
+
+ PottsModuleProliferationVolumeBasedDivisionMock module =
+ new PottsModuleProliferationVolumeBasedDivisionMock(cell);
+
+ module.step(mock(MersenneTwisterFast.class), mock(Simulation.class));
+
+ verify(cell).updateTarget(4.0, 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/agent/module/PottsModuleProliferationSimpleTest.java b/test/arcade/potts/agent/module/PottsModuleProliferationWithCellCycleCheckSimpleTest.java
similarity index 87%
rename from test/arcade/potts/agent/module/PottsModuleProliferationSimpleTest.java
rename to test/arcade/potts/agent/module/PottsModuleProliferationWithCellCycleCheckSimpleTest.java
index a640a13d5..aa167211b 100644
--- a/test/arcade/potts/agent/module/PottsModuleProliferationSimpleTest.java
+++ b/test/arcade/potts/agent/module/PottsModuleProliferationWithCellCycleCheckSimpleTest.java
@@ -17,12 +17,12 @@
import static org.mockito.Mockito.*;
import static arcade.core.ARCADETestUtilities.*;
import static arcade.potts.agent.module.PottsModule.PoissonFactory;
-import static arcade.potts.agent.module.PottsModuleProliferationSimple.*;
+import static arcade.potts.agent.module.PottsModuleProliferationWithCellCycleCheckSimple.*;
import static arcade.potts.util.PottsEnums.Phase;
import static arcade.potts.util.PottsEnums.Region;
import static arcade.potts.util.PottsEnums.State;
-public class PottsModuleProliferationSimpleTest {
+public class PottsModuleProliferationWithCellCycleCheckSimpleTest {
private static final double EPSILON = 1E-10;
private static final double R = 1.0;
@@ -74,7 +74,8 @@ public static void setupMocks() {
public void constructor_setsParameters() {
PottsCell cell = mock(PottsCell.class);
doReturn(parameters).when(cell).getParameters();
- PottsModuleProliferationSimple module = new PottsModuleProliferationSimple(cell);
+ PottsModuleProliferationWithCellCycleCheckSimple module =
+ new PottsModuleProliferationWithCellCycleCheckSimple(cell);
assertEquals(parameters.getDouble("proliferation/SIZE_TARGET"), module.sizeTarget);
assertEquals(parameters.getDouble("proliferation/RATE_G1"), module.rateG1, EPSILON);
@@ -107,7 +108,8 @@ public void constructor_setsParameters() {
public void stepG1_withStateChange_callsMethods() {
PottsCell cell = mock(PottsCell.class);
doReturn(parameters).when(cell).getParameters();
- PottsModuleProliferationSimple module = spy(new PottsModuleProliferationSimple(cell));
+ PottsModuleProliferationWithCellCycleCheckSimple module =
+ spy(new PottsModuleProliferationWithCellCycleCheckSimple(cell));
module.phase = Phase.PROLIFERATIVE_G1;
module.currentSteps = Integer.MAX_VALUE;
@@ -127,7 +129,8 @@ public void stepG1_withStateChange_callsMethods() {
public void stepG1_withoutStateChange_callsMethods() {
PottsCell cell = mock(PottsCell.class);
doReturn(parameters).when(cell).getParameters();
- PottsModuleProliferationSimple module = spy(new PottsModuleProliferationSimple(cell));
+ PottsModuleProliferationWithCellCycleCheckSimple module =
+ spy(new PottsModuleProliferationWithCellCycleCheckSimple(cell));
module.phase = Phase.PROLIFERATIVE_G1;
module.currentSteps = Integer.MAX_VALUE;
@@ -149,7 +152,8 @@ public void stepG1_withTransition_updatesPhase() {
int steps = randomIntBetween(1, parameters.getInt("proliferation/STEPS_G1"));
PottsCell cell = mock(PottsCell.class);
doReturn(parameters).when(cell).getParameters();
- PottsModuleProliferationSimple module = spy(new PottsModuleProliferationSimple(cell));
+ PottsModuleProliferationWithCellCycleCheckSimple module =
+ spy(new PottsModuleProliferationWithCellCycleCheckSimple(cell));
module.phase = Phase.PROLIFERATIVE_G1;
module.currentSteps = module.stepsG1 - steps;
@@ -169,7 +173,8 @@ public void stepG1_withTransition_updatesPhase() {
public void stepG1_withoutTransition_maintainsPhase() {
PottsCell cell = mock(PottsCell.class);
doReturn(parameters).when(cell).getParameters();
- PottsModuleProliferationSimple module = spy(new PottsModuleProliferationSimple(cell));
+ PottsModuleProliferationWithCellCycleCheckSimple module =
+ spy(new PottsModuleProliferationWithCellCycleCheckSimple(cell));
module.phase = Phase.PROLIFERATIVE_G1;
module.currentSteps = module.stepsG1;
@@ -189,7 +194,8 @@ public void stepG1_withoutTransition_maintainsPhase() {
public void stepG1_anyTransition_updatesCell() {
PottsCell cell = mock(PottsCell.class);
doReturn(parameters).when(cell).getParameters();
- PottsModuleProliferationSimple module = spy(new PottsModuleProliferationSimple(cell));
+ PottsModuleProliferationWithCellCycleCheckSimple module =
+ spy(new PottsModuleProliferationWithCellCycleCheckSimple(cell));
PoissonFactory poissonFactory = mock(PoissonFactory.class);
doReturn(poissonMock).when(poissonFactory).createPoisson(anyDouble(), eq(random));
@@ -213,7 +219,8 @@ public void stepG1_anyTransitionWithRegionOverThreshold_updatesCell() {
doReturn((double) criticalVolume).when(cell).getCriticalVolume(Region.NUCLEUS);
doReturn((double) criticalVolume + 1).when(cell).getVolume(Region.NUCLEUS);
- PottsModuleProliferationSimple module = spy(new PottsModuleProliferationSimple(cell));
+ PottsModuleProliferationWithCellCycleCheckSimple module =
+ spy(new PottsModuleProliferationWithCellCycleCheckSimple(cell));
PoissonFactory poissonFactory = mock(PoissonFactory.class);
doReturn(poissonMock).when(poissonFactory).createPoisson(anyDouble(), eq(random));
@@ -238,7 +245,8 @@ public void stepG1_anyTransitionWithRegionUnderThreshold_updatesCell() {
doReturn((double) criticalVolume).when(cell).getCriticalVolume(Region.NUCLEUS);
doReturn((double) criticalVolume - 1).when(cell).getVolume(Region.NUCLEUS);
- PottsModuleProliferationSimple module = spy(new PottsModuleProliferationSimple(cell));
+ PottsModuleProliferationWithCellCycleCheckSimple module =
+ spy(new PottsModuleProliferationWithCellCycleCheckSimple(cell));
PoissonFactory poissonFactory = mock(PoissonFactory.class);
doReturn(poissonMock).when(poissonFactory).createPoisson(anyDouble(), eq(random));
@@ -257,7 +265,8 @@ public void stepS_withTransition_updatesPhase() {
int steps = randomIntBetween(1, parameters.getInt("proliferation/STEPS_S"));
PottsCell cell = mock(PottsCell.class);
doReturn(parameters).when(cell).getParameters();
- PottsModuleProliferationSimple module = spy(new PottsModuleProliferationSimple(cell));
+ PottsModuleProliferationWithCellCycleCheckSimple module =
+ spy(new PottsModuleProliferationWithCellCycleCheckSimple(cell));
module.phase = Phase.PROLIFERATIVE_S;
module.currentSteps = module.stepsS - steps;
@@ -277,7 +286,8 @@ public void stepS_withTransition_updatesPhase() {
public void stepS_withoutTransition_maintainsPhase() {
PottsCell cell = mock(PottsCell.class);
doReturn(parameters).when(cell).getParameters();
- PottsModuleProliferationSimple module = spy(new PottsModuleProliferationSimple(cell));
+ PottsModuleProliferationWithCellCycleCheckSimple module =
+ spy(new PottsModuleProliferationWithCellCycleCheckSimple(cell));
module.phase = Phase.PROLIFERATIVE_S;
module.currentSteps = module.stepsS;
@@ -297,7 +307,8 @@ public void stepS_withoutTransition_maintainsPhase() {
public void stepS_anyTransition_updatesCell() {
PottsCell cell = mock(PottsCell.class);
doReturn(parameters).when(cell).getParameters();
- PottsModuleProliferationSimple module = spy(new PottsModuleProliferationSimple(cell));
+ PottsModuleProliferationWithCellCycleCheckSimple module =
+ spy(new PottsModuleProliferationWithCellCycleCheckSimple(cell));
PoissonFactory poissonFactory = mock(PoissonFactory.class);
doReturn(poissonMock).when(poissonFactory).createPoisson(anyDouble(), eq(random));
@@ -316,7 +327,8 @@ public void stepS_anyTransitionWithRegion_updatesCell() {
PottsCell cell = mock(PottsCell.class);
doReturn(parameters).when(cell).getParameters();
doReturn(true).when(cell).hasRegions();
- PottsModuleProliferationSimple module = spy(new PottsModuleProliferationSimple(cell));
+ PottsModuleProliferationWithCellCycleCheckSimple module =
+ spy(new PottsModuleProliferationWithCellCycleCheckSimple(cell));
PoissonFactory poissonFactory = mock(PoissonFactory.class);
doReturn(poissonMock).when(poissonFactory).createPoisson(anyDouble(), eq(random));
@@ -335,7 +347,8 @@ public void stepS_anyTransitionWithRegion_updatesCell() {
public void stepG2_withStateChange_callsMethods() {
PottsCell cell = mock(PottsCell.class);
doReturn(parameters).when(cell).getParameters();
- PottsModuleProliferationSimple module = spy(new PottsModuleProliferationSimple(cell));
+ PottsModuleProliferationWithCellCycleCheckSimple module =
+ spy(new PottsModuleProliferationWithCellCycleCheckSimple(cell));
module.phase = Phase.PROLIFERATIVE_G2;
module.currentSteps = Integer.MAX_VALUE;
@@ -355,7 +368,8 @@ public void stepG2_withStateChange_callsMethods() {
public void stepG2_withoutStateChange_callsMethods() {
PottsCell cell = mock(PottsCell.class);
doReturn(parameters).when(cell).getParameters();
- PottsModuleProliferationSimple module = spy(new PottsModuleProliferationSimple(cell));
+ PottsModuleProliferationWithCellCycleCheckSimple module =
+ spy(new PottsModuleProliferationWithCellCycleCheckSimple(cell));
module.phase = Phase.PROLIFERATIVE_G2;
module.currentSteps = Integer.MAX_VALUE;
@@ -382,7 +396,8 @@ public void stepG2_withTransitionNotArrested_updatesPhase() {
doReturn((volume * SIZE_CHECKPOINT * sizeTarget) + 1).when(cell).getVolume();
doReturn(volume).when(cell).getCriticalVolume();
- PottsModuleProliferationSimple module = spy(new PottsModuleProliferationSimple(cell));
+ PottsModuleProliferationWithCellCycleCheckSimple module =
+ spy(new PottsModuleProliferationWithCellCycleCheckSimple(cell));
module.phase = Phase.PROLIFERATIVE_G2;
module.currentSteps = module.stepsG2 - steps;
@@ -407,7 +422,8 @@ public void stepG2_withoutTransitionNotArrested_maintainsPhase() {
doReturn((volume * SIZE_CHECKPOINT * sizeTarget) + 1).when(cell).getVolume();
doReturn(volume).when(cell).getCriticalVolume();
- PottsModuleProliferationSimple module = spy(new PottsModuleProliferationSimple(cell));
+ PottsModuleProliferationWithCellCycleCheckSimple module =
+ spy(new PottsModuleProliferationWithCellCycleCheckSimple(cell));
module.phase = Phase.PROLIFERATIVE_G2;
module.currentSteps = module.stepsG2;
@@ -433,7 +449,8 @@ public void stepG2_withTransitionArrested_maintainsPhase() {
doReturn((volume * SIZE_CHECKPOINT * sizeTarget) - 1).when(cell).getVolume();
doReturn(volume).when(cell).getCriticalVolume();
- PottsModuleProliferationSimple module = spy(new PottsModuleProliferationSimple(cell));
+ PottsModuleProliferationWithCellCycleCheckSimple module =
+ spy(new PottsModuleProliferationWithCellCycleCheckSimple(cell));
module.phase = Phase.PROLIFERATIVE_G2;
module.currentSteps = module.stepsG2 - steps;
@@ -467,7 +484,8 @@ public void stepG2_withTransitionArrestedRegion_maintainsPhase() {
.getVolume(Region.NUCLEUS);
doReturn(regionVolume).when(cell).getCriticalVolume(Region.NUCLEUS);
- PottsModuleProliferationSimple module = spy(new PottsModuleProliferationSimple(cell));
+ PottsModuleProliferationWithCellCycleCheckSimple module =
+ spy(new PottsModuleProliferationWithCellCycleCheckSimple(cell));
module.phase = Phase.PROLIFERATIVE_G2;
module.currentSteps = module.stepsG2 - steps;
@@ -492,7 +510,8 @@ public void stepM_withoutTransitionArrested_maintainPhase() {
doReturn((volume * SIZE_CHECKPOINT * sizeTarget) - 1).when(cell).getVolume();
doReturn(volume).when(cell).getCriticalVolume();
- PottsModuleProliferationSimple module = spy(new PottsModuleProliferationSimple(cell));
+ PottsModuleProliferationWithCellCycleCheckSimple module =
+ spy(new PottsModuleProliferationWithCellCycleCheckSimple(cell));
module.phase = Phase.PROLIFERATIVE_G2;
module.currentSteps = module.stepsG2;
@@ -512,7 +531,8 @@ public void stepM_withoutTransitionArrested_maintainPhase() {
public void stepG2_anyTransition_updatesCell() {
PottsCell cell = mock(PottsCell.class);
doReturn(parameters).when(cell).getParameters();
- PottsModuleProliferationSimple module = spy(new PottsModuleProliferationSimple(cell));
+ PottsModuleProliferationWithCellCycleCheckSimple module =
+ spy(new PottsModuleProliferationWithCellCycleCheckSimple(cell));
PoissonFactory poissonFactory = mock(PoissonFactory.class);
doReturn(poissonMock).when(poissonFactory).createPoisson(anyDouble(), eq(random));
@@ -531,7 +551,8 @@ public void stepG2_anyTransitionWithRegion_updatesCell() {
PottsCell cell = mock(PottsCell.class);
doReturn(parameters).when(cell).getParameters();
doReturn(true).when(cell).hasRegions();
- PottsModuleProliferationSimple module = spy(new PottsModuleProliferationSimple(cell));
+ PottsModuleProliferationWithCellCycleCheckSimple module =
+ spy(new PottsModuleProliferationWithCellCycleCheckSimple(cell));
PoissonFactory poissonFactory = mock(PoissonFactory.class);
doReturn(poissonMock).when(poissonFactory).createPoisson(anyDouble(), eq(random));
@@ -551,7 +572,8 @@ public void stepM_withTransition_updatesPhase() {
int steps = randomIntBetween(1, parameters.getInt("proliferation/STEPS_M"));
PottsCell cell = mock(PottsCell.class);
doReturn(parameters).when(cell).getParameters();
- PottsModuleProliferationSimple module = spy(new PottsModuleProliferationSimple(cell));
+ PottsModuleProliferationWithCellCycleCheckSimple module =
+ spy(new PottsModuleProliferationWithCellCycleCheckSimple(cell));
module.phase = Phase.PROLIFERATIVE_M;
module.currentSteps = module.stepsM - steps;
doNothing().when(module).addCell(random, simMock);
@@ -573,7 +595,8 @@ public void stepM_withTransition_updatesPhase() {
public void stepM_withoutTransition_maintainsPhase() {
PottsCell cell = mock(PottsCell.class);
doReturn(parameters).when(cell).getParameters();
- PottsModuleProliferationSimple module = spy(new PottsModuleProliferationSimple(cell));
+ PottsModuleProliferationWithCellCycleCheckSimple module =
+ spy(new PottsModuleProliferationWithCellCycleCheckSimple(cell));
module.phase = Phase.PROLIFERATIVE_M;
module.currentSteps = module.stepsM;
doNothing().when(module).addCell(random, simMock);
@@ -595,7 +618,8 @@ public void stepM_withoutTransition_maintainsPhase() {
public void stepM_anyTransition_updatesCell() {
PottsCell cell = mock(PottsCell.class);
doReturn(parameters).when(cell).getParameters();
- PottsModuleProliferationSimple module = spy(new PottsModuleProliferationSimple(cell));
+ PottsModuleProliferationWithCellCycleCheckSimple module =
+ spy(new PottsModuleProliferationWithCellCycleCheckSimple(cell));
doNothing().when(module).addCell(random, simMock);
PoissonFactory poissonFactory = mock(PoissonFactory.class);
@@ -630,7 +654,8 @@ public void stepM_withRegionOverThreshold_doesNotUpdateCell() {
pottsMock.ids = ids;
pottsMock.regions = regions;
- PottsModuleProliferationSimple module = spy(new PottsModuleProliferationSimple(cell));
+ PottsModuleProliferationWithCellCycleCheckSimple module =
+ spy(new PottsModuleProliferationWithCellCycleCheckSimple(cell));
doNothing().when(module).addCell(random, simMock);
PoissonFactory poissonFactory = mock(PoissonFactory.class);
@@ -672,7 +697,8 @@ public void stepM_withRegionUnderThreshold_updatesCell() {
pottsMock.ids = ids;
pottsMock.regions = regions;
- PottsModuleProliferationSimple module = spy(new PottsModuleProliferationSimple(cell));
+ PottsModuleProliferationWithCellCycleCheckSimple module =
+ spy(new PottsModuleProliferationWithCellCycleCheckSimple(cell));
doNothing().when(module).addCell(random, simMock);
PoissonFactory poissonFactory = mock(PoissonFactory.class);
diff --git a/test/arcade/potts/agent/module/PottsModuleProliferationTest.java b/test/arcade/potts/agent/module/PottsModuleProliferationWithCellCycleCheckTest.java
similarity index 78%
rename from test/arcade/potts/agent/module/PottsModuleProliferationTest.java
rename to test/arcade/potts/agent/module/PottsModuleProliferationWithCellCycleCheckTest.java
index cc5400a37..35838a038 100644
--- a/test/arcade/potts/agent/module/PottsModuleProliferationTest.java
+++ b/test/arcade/potts/agent/module/PottsModuleProliferationWithCellCycleCheckTest.java
@@ -18,15 +18,16 @@
import static arcade.potts.util.PottsEnums.Phase;
import static arcade.potts.util.PottsEnums.State;
-public class PottsModuleProliferationTest {
+public class PottsModuleProliferationWithCellCycleCheckTest {
static MersenneTwisterFast randomMock = new MersenneTwisterFast();
static PottsSimulation simMock = mock(PottsSimulation.class);
static PottsCell cellMock = mock(PottsCell.class);
- static class PottsModuleProliferationMock extends PottsModuleProliferation {
- PottsModuleProliferationMock(PottsCell cell) {
+ static class PottsModuleProliferationwithCellCycleCheckMock
+ extends PottsModuleProliferationWithCellCycleCheck {
+ PottsModuleProliferationwithCellCycleCheckMock(PottsCell cell) {
super(cell);
}
@@ -53,20 +54,23 @@ void stepM(MersenneTwisterFast random, Simulation sim) {
@Test
public void constructor_initializesFactory() {
- PottsModuleProliferationMock module = new PottsModuleProliferationMock(cellMock);
+ PottsModuleProliferationWithCellCycleCheck module =
+ new PottsModuleProliferationwithCellCycleCheckMock(cellMock);
assertNotNull(module.poissonFactory);
}
@Test
public void getPhase_defaultConstructor_returnsValue() {
- PottsModuleProliferation module = new PottsModuleProliferationMock(cellMock);
+ PottsModuleProliferationWithCellCycleCheck module =
+ new PottsModuleProliferationwithCellCycleCheckMock(cellMock);
assertEquals(Phase.PROLIFERATIVE_G1, module.getPhase());
}
@Test
public void setPhase_givenValue_setsValue() {
Phase phase = Phase.random(randomMock);
- PottsModuleProliferation module = new PottsModuleProliferationMock(cellMock);
+ PottsModuleProliferationWithCellCycleCheck module =
+ new PottsModuleProliferationwithCellCycleCheckMock(cellMock);
module.setPhase(phase);
assertEquals(phase, module.phase);
}
@@ -74,7 +78,8 @@ public void setPhase_givenValue_setsValue() {
@Test
public void setPhase_givenValue_resetsSteps() {
Phase phase = Phase.random(randomMock);
- PottsModuleProliferation module = new PottsModuleProliferationMock(cellMock);
+ PottsModuleProliferationWithCellCycleCheck module =
+ new PottsModuleProliferationwithCellCycleCheckMock(cellMock);
module.currentSteps = randomIntBetween(1, 10);
module.setPhase(phase);
assertEquals(0, module.currentSteps);
@@ -82,7 +87,8 @@ public void setPhase_givenValue_resetsSteps() {
@Test
public void step_givenPhaseG1_callsMethod() {
- PottsModuleProliferation module = spy(new PottsModuleProliferationMock(cellMock));
+ PottsModuleProliferationWithCellCycleCheck module =
+ spy(new PottsModuleProliferationwithCellCycleCheckMock(cellMock));
module.phase = Phase.PROLIFERATIVE_G1;
module.step(randomMock, simMock);
@@ -94,7 +100,8 @@ public void step_givenPhaseG1_callsMethod() {
@Test
public void step_givenPhaseS_callsMethod() {
- PottsModuleProliferation module = spy(new PottsModuleProliferationMock(cellMock));
+ PottsModuleProliferationWithCellCycleCheck module =
+ spy(new PottsModuleProliferationwithCellCycleCheckMock(cellMock));
module.phase = Phase.PROLIFERATIVE_S;
module.step(randomMock, simMock);
@@ -106,7 +113,8 @@ public void step_givenPhaseS_callsMethod() {
@Test
public void step_givenPhaseG2_callsMethod() {
- PottsModuleProliferation module = spy(new PottsModuleProliferationMock(cellMock));
+ PottsModuleProliferationWithCellCycleCheck module =
+ spy(new PottsModuleProliferationwithCellCycleCheckMock(cellMock));
module.phase = Phase.PROLIFERATIVE_G2;
module.step(randomMock, simMock);
@@ -118,7 +126,8 @@ public void step_givenPhaseG2_callsMethod() {
@Test
public void step_givenPhaseM_callsMethod() {
- PottsModuleProliferation module = spy(new PottsModuleProliferationMock(cellMock));
+ PottsModuleProliferationWithCellCycleCheck module =
+ spy(new PottsModuleProliferationwithCellCycleCheckMock(cellMock));
doNothing().when(module).addCell(randomMock, simMock);
module.phase = Phase.PROLIFERATIVE_M;
@@ -131,7 +140,8 @@ public void step_givenPhaseM_callsMethod() {
@Test
public void step_invalidPhase_doesNothing() {
- PottsModuleProliferation module = spy(new PottsModuleProliferationMock(cellMock));
+ PottsModuleProliferationWithCellCycleCheck module =
+ spy(new PottsModuleProliferationwithCellCycleCheckMock(cellMock));
module.phase = Phase.UNDEFINED;
module.step(randomMock, simMock);
@@ -178,7 +188,8 @@ public void addCell_called_addsObject() {
doNothing().when(cell).reset(any(), any());
doNothing().when(newCell).reset(any(), any());
- PottsModuleProliferation module = new PottsModuleProliferationMock(cell);
+ PottsModuleProliferationWithCellCycleCheck module =
+ new PottsModuleProliferationwithCellCycleCheckMock(cell);
module.addCell(randomMock, sim);
verify(cell).reset(potts.ids, potts.regions);
diff --git a/test/arcade/potts/env/location/PottsLocation2DTest.java b/test/arcade/potts/env/location/PottsLocation2DTest.java
index 81f861ab3..da5d53c6a 100644
--- a/test/arcade/potts/env/location/PottsLocation2DTest.java
+++ b/test/arcade/potts/env/location/PottsLocation2DTest.java
@@ -4,6 +4,7 @@
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import ec.util.MersenneTwisterFast;
+import arcade.core.util.Vector;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import static arcade.potts.env.location.Voxel.VOXEL_COMPARATOR;
@@ -278,4 +279,92 @@ public void split_balanceableLocationRandomOne_returnsList() {
assertEquals(locVoxels, loc.voxels);
assertEquals(splitVoxels, split.voxels);
}
+
+ @Test
+ public void getOffsetInApicalFrame2D_offsetAtCenter_returnsExpectedVoxel() {
+ ArrayList voxels = new ArrayList<>();
+ // 3x3 grid centered at (0,0)
+ for (int x = -1; x <= 1; x++) {
+ for (int y = -1; y <= 1; y++) {
+ voxels.add(new Voxel(x, y, 0));
+ }
+ }
+ PottsLocation2D loc = new PottsLocation2D(voxels);
+
+ Vector apicalAxis = new Vector(0, 1, 0); // Y-axis
+ ArrayList offsets = new ArrayList<>();
+ offsets.add(50); // middle of X axis
+ offsets.add(50); // middle of Y axis
+
+ Voxel result = loc.getOffsetInApicalFrame(offsets, apicalAxis);
+ assertEquals(new Voxel(0, 0, 0), result);
+ }
+
+ @Test
+ public void getOffsetInApicalFrame2D_offsetUpperRight_returnsExpectedVoxel() {
+ ArrayList voxels = new ArrayList<>();
+ for (int x = 0; x <= 4; x++) {
+ for (int y = 0; y <= 4; y++) {
+ voxels.add(new Voxel(x, y, 0));
+ }
+ }
+ PottsLocation2D loc = new PottsLocation2D(voxels);
+
+ Vector apicalAxis = new Vector(0, 1, 0); // Y-axis
+ ArrayList offsets = new ArrayList<>();
+ offsets.add(100); // far right of X axis
+ offsets.add(100); // top of Y axis
+
+ Voxel result = loc.getOffsetInApicalFrame(offsets, apicalAxis);
+ assertEquals(new Voxel(4, 4, 0), result);
+ }
+
+ @Test
+ public void getOffsetInApicalFrame2D_emptyVoxels_returnsNull() {
+ PottsLocation2D loc = new PottsLocation2D(new ArrayList<>());
+
+ Vector apicalAxis = new Vector(1, 0, 0);
+ ArrayList offsets = new ArrayList<>();
+ offsets.add(50);
+ offsets.add(50);
+
+ Voxel result = loc.getOffsetInApicalFrame(offsets, apicalAxis);
+ assertNull(result);
+ }
+
+ @Test
+ public void getOffsetInApicalFrame2D_invalidOffset_throwsException() {
+ ArrayList voxels = new ArrayList<>();
+ voxels.add(new Voxel(0, 0, 0));
+ PottsLocation2D loc = new PottsLocation2D(voxels);
+
+ Vector apicalAxis = new Vector(1, 0, 0);
+
+ ArrayList badOffset = new ArrayList<>();
+ badOffset.add(50); // only one element
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ loc.getOffsetInApicalFrame(badOffset, apicalAxis);
+ });
+ }
+
+ @Test
+ public void getOffsetInApicalFrame2D_nonOrthogonalAxis_returnsExpected() {
+ ArrayList voxels = new ArrayList<>();
+ voxels.add(new Voxel(0, 0, 0));
+ voxels.add(new Voxel(1, 1, 0));
+ voxels.add(new Voxel(2, 2, 0));
+ voxels.add(new Voxel(3, 3, 0));
+ PottsLocation2D loc = new PottsLocation2D(voxels);
+
+ Vector apicalAxis = new Vector(1, 1, 0); // diagonal
+ ArrayList offsets = new ArrayList<>();
+ offsets.add(0); // lowest orthogonal axis
+ offsets.add(100); // farthest along apical
+
+ Voxel result = loc.getOffsetInApicalFrame(offsets, apicalAxis);
+ assertEquals(new Voxel(3, 3, 0), result);
+ }
}
diff --git a/test/arcade/potts/env/location/PottsLocation3DTest.java b/test/arcade/potts/env/location/PottsLocation3DTest.java
index 9b5742c41..928741174 100644
--- a/test/arcade/potts/env/location/PottsLocation3DTest.java
+++ b/test/arcade/potts/env/location/PottsLocation3DTest.java
@@ -4,6 +4,7 @@
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import ec.util.MersenneTwisterFast;
+import arcade.core.util.Vector;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import static arcade.potts.env.location.Voxel.VOXEL_COMPARATOR;
@@ -298,4 +299,16 @@ public void split_balanceableLocationRandomOne_returnsList() {
assertEquals(locVoxels, loc.voxels);
assertEquals(splitVoxels, split.voxels);
}
+
+ @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));
+ }
+ }
}
diff --git a/test/arcade/potts/env/location/PottsLocationTest.java b/test/arcade/potts/env/location/PottsLocationTest.java
index ef48c8f5b..00a90c1a3 100644
--- a/test/arcade/potts/env/location/PottsLocationTest.java
+++ b/test/arcade/potts/env/location/PottsLocationTest.java
@@ -169,6 +169,11 @@ Direction getSlice(Direction direction, HashMap diameters) {
ArrayList getSelected(Voxel center, double n) {
return new ArrayList<>();
}
+
+ @Override
+ Voxel getOffsetInApicalFrame(ArrayList offsets, Vector apicalAxis) {
+ return new Voxel(0, 0, 0);
+ }
}
@Test
diff --git a/test/arcade/potts/env/location/PottsLocationsTest.java b/test/arcade/potts/env/location/PottsLocationsTest.java
index 6c082948a..343902f7c 100644
--- a/test/arcade/potts/env/location/PottsLocationsTest.java
+++ b/test/arcade/potts/env/location/PottsLocationsTest.java
@@ -6,6 +6,10 @@
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import ec.util.MersenneTwisterFast;
+import arcade.core.util.Vector;
+import arcade.potts.env.location.PottsLocationTest.PottsLocationMock;
+import arcade.potts.util.PottsEnums.Direction;
+import arcade.potts.util.PottsEnums.Region;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import static arcade.core.ARCADETestUtilities.*;
@@ -963,4 +967,14 @@ public void separateVoxels_validListsWithRegions_updatesLists() {
assertEquals(2, split.locations.get(Region.DEFAULT).voxels.size());
assertEquals(2, split.locations.get(Region.DEFAULT).voxels.size());
}
+
+ @Test
+ public void getOffsetInApicalFrame_called_raisesUnsupportedOperationException() {
+ PottsLocationsMock loc = new PottsLocationsMock(voxelListForMultipleRegionsA);
+ 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);