diff --git a/.github/config/checks.xml b/.github/config/checks.xml index ec6a8f485..7dbf00e1f 100644 --- a/.github/config/checks.xml +++ b/.github/config/checks.xml @@ -33,11 +33,13 @@ + + diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9bec19056..929733889 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -15,12 +15,12 @@ jobs: fetch-depth: 0 - name: Lint code base - uses: dbelyaev/action-checkstyle@v0.9.5 + uses: dbelyaev/action-checkstyle@v3.0.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} reporter: github-check checkstyle_config: .github/config/checks.xml - fail_on_error: true + fail_level: error update-lint-badges: if: ${{ always() && github.ref == 'refs/heads/main' }} diff --git a/src/arcade/core/util/Vector.java b/src/arcade/core/util/Vector.java index d71a16caa..efdcafc8d 100644 --- a/src/arcade/core/util/Vector.java +++ b/src/arcade/core/util/Vector.java @@ -33,7 +33,7 @@ public Vector(Double3D vector) { * @return the x component */ public double getX() { - return vector.x; + return vector.getX(); } /** @@ -42,7 +42,7 @@ public double getX() { * @return the y component */ public double getY() { - return vector.y; + return vector.getY(); } /** @@ -51,7 +51,7 @@ public double getY() { * @return the z component */ public double getZ() { - return vector.z; + return vector.getZ(); } /** diff --git a/src/arcade/potts/agent/cell/PottsCell.java b/src/arcade/potts/agent/cell/PottsCell.java index ab3996071..b05310e27 100644 --- a/src/arcade/potts/agent/cell/PottsCell.java +++ b/src/arcade/potts/agent/cell/PottsCell.java @@ -79,7 +79,7 @@ public abstract class PottsCell implements Cell { private final EnumMap targetRegionSurfaces; /** Critical volume for cell [voxels]. */ - final double criticalVolume; + double criticalVolume; /** Critical volumes for cell by region [voxels]. */ final EnumMap 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);