diff --git a/src/arcade/potts/agent/cell/PottsCellFactory.java b/src/arcade/potts/agent/cell/PottsCellFactory.java index 4d040f77..8a17276f 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/module/PottsModuleFlyGMCDifferentiation.java b/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java index 642619a2..dc32c6cd 100644 --- a/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java +++ b/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java @@ -20,6 +20,8 @@ */ public class PottsModuleFlyGMCDifferentiation extends PottsModuleProliferationVolumeBasedDivision { + Boolean pdeLike; + /** * Creates a fly GMC proliferation module. * @@ -27,6 +29,7 @@ public class PottsModuleFlyGMCDifferentiation extends PottsModuleProliferationVo */ public PottsModuleFlyGMCDifferentiation(PottsCellFlyGMC cell) { super(cell); + pdeLike = (cell.getParameters().getInt("proliferation/PDELIKE") != 0); } /** @@ -93,10 +96,40 @@ void addCell(MersenneTwisterFast random, Simulation sim) { } public void updateGrowthRate(Simulation sim) { - if (dynamicGrowthRateVolume == false) { + if (!dynamicGrowthRateVolume) { cellGrowthRate = cellGrowthRateBase; - } else if (dynamicGrowthRateVolume == true) { - updateVolumeBasedGrowthRate(); + } 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 index be50874d..2df7ac42 100644 --- a/src/arcade/potts/agent/module/PottsModuleFlyStemProliferation.java +++ b/src/arcade/potts/agent/module/PottsModuleFlyStemProliferation.java @@ -3,6 +3,7 @@ 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; @@ -59,7 +60,7 @@ public class PottsModuleFlyStemProliferation extends PottsModuleProliferationVol final boolean volumeBasedCriticalVolume; /** Boolean flag indicating whether growth rate should be regulated by NB-NB contact. */ - final boolean dynamicGrowthRateNBContact; + final boolean dynamicGrowthRateNBSelfRepression; final double volumeBasedCriticalVolumeMultiplier; @@ -83,6 +84,12 @@ public class PottsModuleFlyStemProliferation extends PottsModuleProliferationVol final double initialSize; + /** + * 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}. * @@ -113,10 +120,10 @@ public PottsModuleFlyStemProliferation(PottsCellFlyStem cell) { volumeBasedCriticalVolume = (parameters.getInt("proliferation/VOLUME_BASED_CRITICAL_VOLUME") != 0); - dynamicGrowthRateNBContact = - (parameters.getInt("proliferation/DYNAMIC_GROWTH_RATE_NB_CONTACT") != 0); + dynamicGrowthRateNBSelfRepression = + (parameters.getInt("proliferation/DYNAMIC_GROWTH_RATE_NB_SELF_REPRESSION") != 0); - if (dynamicGrowthRateVolume && dynamicGrowthRateNBContact) { + if (dynamicGrowthRateVolume && dynamicGrowthRateNBSelfRepression) { throw new InvalidParameterException( "Dynamic growth rate can be either volume-based or NB-contact-based, not both."); } @@ -129,6 +136,8 @@ public PottsModuleFlyStemProliferation(PottsCellFlyStem cell) { initialSize = cell.getVolume(); + pdeLike = (parameters.getInt("proliferation/PDELIKE") != 0); + setPhase(Phase.UNDEFINED); } @@ -165,24 +174,42 @@ public void addCell(MersenneTwisterFast random, Simulation sim) { */ public void updateGrowthRate(Simulation sim) { if (dynamicGrowthRateVolume == true) { - updateVolumeBasedGrowthRate(); - } else if (dynamicGrowthRateNBContact == true) { - updateNBContactGrowthRate(sim); + 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 number of neighbors of this cell that are unique neuroblasts. + * Gets the neighbors of this cell that are unique neuroblasts. * * @param sim the simulation * @return the number of unique neuroblast neighbors */ - protected Integer getNumNBNeighbors(Simulation sim) { + protected HashSet getNBNeighbors(Simulation sim) { Potts potts = ((PottsSimulation) sim).getPotts(); ArrayList voxels = ((PottsLocation) cell.getLocation()).getVoxels(); - HashSet stemNeighbors = new HashSet(); + HashSet stemNeighbors = new HashSet(); for (Voxel v : voxels) { HashSet uniqueIDs = potts.getUniqueIDs(v.x, v.y, v.z); @@ -193,50 +220,32 @@ protected Integer getNumNBNeighbors(Simulation sim) { } if (cell.getPop() == neighbor.getPop()) { if (neighbor.getID() != cell.getID()) { - stemNeighbors.add((PottsCell) sim.getGrid().getObjectAt(id)); + stemNeighbors.add((PottsCellFlyStem) sim.getGrid().getObjectAt(id)); } } } } - return stemNeighbors.size(); + return stemNeighbors; } - /** - * Updates the cell's growth rate based on the number of neighboring neuroblasts. - * - *

This method applies a Hill-type repression function to scale the cell's base growth rate - * according to local neuroblast density. Specifically, it counts the number of neighboring - * neuroblasts (using {@link #getNumNBNeighbors(Simulation)}) and applies: - * - *

-     *   hillRepression = K^n / (K^n + Np^n)
-     *   cellGrowthRate = cellGrowthRateBase * hillRepression
-     * 
- * - * where: - * - *
    - *
  • Np is the number of neighboring neuroblasts - *
  • K is the half-max parameter for repression - * (proliferation/NB_CONTACT_HALF_MAX) - *
  • n is the Hill coefficient controlling steepness - * (proliferation/NB_CONTACT_HILL_N) - *
  • cellGrowthRateBase is the base growth rate in the absence of neighbors - *
- * - *

This formulation ensures that when Np = 0, the cell grows at the base rate, and as the - * number of neighbors increases, growth is repressed toward zero. - * - * @param sim the simulation - */ - protected void updateNBContactGrowthRate(Simulation sim) { - int NpRaw = getNumNBNeighbors(sim); - double Np = Math.max(0.0, (double) NpRaw); + 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 Npn = Math.pow(np, nbContactHillN); - double hillRepression = Kn / (Kn + Npn); + double hillRepression; + if (Kn == 0.0) { + hillRepression = (np == 0.0) ? 1.0 : 0.0; + } else { + hillRepression = Kn / (Kn + Npn); + } cellGrowthRate = cellGrowthRateBase * hillRepression; } @@ -578,4 +587,18 @@ public static PottsLocation getBasalLocation( return (proj1 < proj2) ? loc2 : loc1; // higher projection = more basal } + + public HashSet getNBsInSimulation(Simulation sim) { + HashSet nbsInSimulation = new HashSet<>(); + Bag simObjects = sim.getGrid().getAllObjects(); + for (int i = 0; i < simObjects.numObjs; i++) { + Object o = simObjects.objs[i]; + if (!(o instanceof PottsCell)) continue; // skip non-cell objects + PottsCell cellInSim = (PottsCell) o; + if (cell.getPop() == cellInSim.getPop() && o instanceof PottsCellFlyStem) { + nbsInSimulation.add((PottsCellFlyStem) o); + } + } + return nbsInSimulation; + } } diff --git a/src/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivision.java b/src/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivision.java index aa1d188d..97c8cb10 100644 --- a/src/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivision.java +++ b/src/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivision.java @@ -69,22 +69,8 @@ public void step(MersenneTwisterFast random, Simulation sim) { */ public abstract void updateGrowthRate(Simulation sim); - /** - * Updates the cell growth rate based on the volume of the cell. - * - *

The growth rate is scaled according to a power-law relationship between the current cell - * volume and its critical volume. As the cell volume increases relative to the critical volume, - * the growth rate accelerates proportionally: - * - *

-     *   growthRate = baseGrowthRate * (volume / criticalVolume) ^ sensitivity
-     * 
- * - * This allows larger cells to grow faster, capturing volume-dependent growth dynamics. - */ - public void updateVolumeBasedGrowthRate() { - double volume = cell.getLocation().getVolume(); - double Ka = cell.getCriticalVolume(); + public void updateCellVolumeBasedGrowthRate(double volume, double cellCriticalVolume) { + double Ka = cellCriticalVolume; cellGrowthRate = cellGrowthRateBase * Math.pow((volume / Ka), growthRateVolumeSensitivity); } } diff --git a/src/arcade/potts/parameter.potts.xml b/src/arcade/potts/parameter.potts.xml index 73f91b6c..a48eee0b 100644 --- a/src/arcade/potts/parameter.potts.xml +++ b/src/arcade/potts/parameter.potts.xml @@ -63,11 +63,15 @@ + + + + - - + + diff --git a/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiationTest.java b/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiationTest.java index c1b04e27..b3babde5 100644 --- a/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiationTest.java +++ b/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiationTest.java @@ -133,7 +133,8 @@ final void tearDown() { @Test public void addCell_called_callsExpectedMethods() { - // When the module calls make() on the cell, return Quiescent PottsCellContainer mock + // When the module calls make() on the cell, return Quiescent PottsCellContainer + // mock container = mock(PottsCellContainer.class); when(gmcCell.make(eq(123), eq(State.QUIESCENT), any(MersenneTwisterFast.class))) .thenReturn(container); @@ -164,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 index 3773f102..3fa2745d 100644 --- a/test/arcade/potts/agent/module/PottsModuleFlyStemProliferationTest.java +++ b/test/arcade/potts/agent/module/PottsModuleFlyStemProliferationTest.java @@ -1,12 +1,14 @@ 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; @@ -699,10 +701,11 @@ public void addCell_WTVolumeSwap_swapsVoxelsAndCreatesNewCell() { 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); // default for volume - when(parameters.getInt("proliferation/VOLUME_BASED_CRITICAL_VOLUME")) - .thenReturn(0); // use classic mode + when(parameters.getDouble("proliferation/SIZE_TARGET")).thenReturn(1.0); // default for + // volume + when(parameters.getInt("proliferation/VOLUME_BASED_CRITICAL_VOLUME")).thenReturn(0); // use + // classic + // mode // Set up the condition that parent volume < daughter volume → stem/daughter swap required when(stemLoc.getVolume()).thenReturn(5.0); @@ -856,145 +859,392 @@ public void addCell_MUDMUTOffsetBelowThreshold_createsGMCWithVolumeSwap() { } @Test - public void getNumNBNeighbors_withTwoUniqueStemNeighbors_returnsCorrectCount() { + 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 - HashSet idsVoxel1 = new HashSet<>(); - idsVoxel1.add(10); - idsVoxel1.add(11); - HashSet idsVoxel2 = new HashSet<>(); - idsVoxel2.add(11); // repeat → should still count neighbor 11 only once - idsVoxel2.add(12); - + // 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 - PottsCell neighbor10 = mock(PottsCell.class); - PottsCell neighbor11 = mock(PottsCell.class); - PottsCell neighbor12 = mock(PottsCell.class); + PottsCellFlyStem nb10 = mock(PottsCellFlyStem.class); + PottsCellFlyStem nb11 = mock(PottsCellFlyStem.class); + PottsCell nb12OtherPop = mock(PottsCell.class); - when(neighbor10.getID()).thenReturn(10); - when(neighbor11.getID()).thenReturn(11); - when(neighbor12.getID()).thenReturn(12); - when(stemCell.getID()).thenReturn(42); + when(nb10.getID()).thenReturn(10); + when(nb11.getID()).thenReturn(11); + when(nb12OtherPop.getID()).thenReturn(12); - when(neighbor10.getPop()).thenReturn(3); // match cell.getPop - when(neighbor11.getPop()).thenReturn(3); // match cell.getPop - when(neighbor12.getPop()).thenReturn(99); // no match + // 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(grid.getObjectAt(10)).thenReturn(neighbor10); - when(grid.getObjectAt(11)).thenReturn(neighbor11); - when(grid.getObjectAt(12)).thenReturn(neighbor12); + when(stemCell.getID()).thenReturn(42); - int numNeighbors = module.getNumNBNeighbors(sim); + when(grid.getObjectAt(10)).thenReturn(nb10); + when(grid.getObjectAt(11)).thenReturn(nb11); + when(grid.getObjectAt(12)).thenReturn(nb12OtherPop); - assertEquals(2, numNeighbors, "Should count 2 unique matching neighbors (10 and 11)"); + 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 getNumNBNeighbors_noMatchingNeighbors_returnsZero() { + 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<>(); - ids.add(50); + 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); // doesn't match stem pop + 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); - int numNeighbors = module.getNumNBNeighbors(sim); + HashSet neighbors = module.getNBNeighbors(sim); - assertEquals(0, numNeighbors, "No neighbors should be counted when pops do not match."); + assertNotNull(neighbors); + assertTrue(neighbors.isEmpty(), "No neighbors should be returned when pops do not match."); } @Test - public void getNumNBNeighbors_called_doesNotCountSelf() { + 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); - HashSet ids = new HashSet<>(); - ids.add(42); // same as sim.getID() mock (self) + // 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); - PottsCell selfCell = stemCell; // or mock another PottsCell with same pop - when(grid.getObjectAt(42)).thenReturn(selfCell); + when(grid.getObjectAt(42)).thenReturn(stemCell); - int numNeighbors = module.getNumNBNeighbors(sim); - assertEquals(0, numNeighbors, "Self should not be counted as a neighbor"); + HashSet neighbors = module.getNBNeighbors(sim); + assertTrue(neighbors.isEmpty(), "Self should not be included as a neighbor"); } @Test - public void updateNBContactGrowthRate_noNeighbors_returnsBaseGrowthRate() { - // Mock parameters - when(parameters.getDouble("proliferation/NB_CONTACT_HALF_MAX")).thenReturn(5.0); + 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(10.0); + when(parameters.getDouble("proliferation/CELL_GROWTH_RATE")).thenReturn(12.0); module = spy(new PottsModuleFlyStemProliferation(stemCell)); - // Mock neighbor count - doReturn(0).when(module).getNumNBNeighbors(sim); + // 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); - module.updateNBContactGrowthRate(sim); - assertEquals( - 10.0, - module.cellGrowthRate, - 1e-6, - "With 0 neighbors, hill repression should be 1.0"); + assertEquals(6.0, module.cellGrowthRate, 1e-6); + verify(module, times(1)).getNBNeighbors(sim); + verify(module, never()).getNBsInSimulation(sim); } @Test - public void updateNBContactGrowthRate_halfMaxNeighbors_returnsHalfBaseGrowthRate() { - // Mock parameters - when(parameters.getDouble("proliferation/NB_CONTACT_HALF_MAX")).thenReturn(5.0); + 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)); - // Mock neighbor count - doReturn(5).when(module).getNumNBNeighbors(sim); + // N = 0 → with your guard, repression = 1.0 when K=0 & N=0 + doReturn(new HashSet()).when(module).getNBNeighbors(sim); - module.updateNBContactGrowthRate(sim); - // Hill repression = K^n / (K^n + N^n) = 25 / (25 + 25) = 0.5 - assertEquals( - 5.0, - module.cellGrowthRate, - 1e-6, - "With 0 neighbors, hill repression should be 1.0"); + module.updateGrowthRateBasedOnOtherNBs(sim); + + assertEquals(10.0, module.cellGrowthRate, 1e-6); } @Test - public void updateNBContactGrowthRate_highNeighbors_returnsLowGrowthRate() { - when(parameters.getDouble("proliferation/NB_CONTACT_HALF_MAX")).thenReturn(5.0); + 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)); - doReturn(20).when(module).getNumNBNeighbors(sim); + // 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.updateNBContactGrowthRate(sim); + module.updateGrowthRateBasedOnOtherNBs(sim); - // Hill repression = 25 / (25 + 400) = 25 / 425 ≈ 0.0588 - assertEquals(10.0 * (25.0 / 425.0), module.cellGrowthRate, 1e-6); + assertTrue(module.cellGrowthRate < 0.01, "Growth should be ~0 with very large N."); } } diff --git a/test/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivisionTest.java b/test/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivisionTest.java index 9229ae9c..4c41c166 100644 --- a/test/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivisionTest.java +++ b/test/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivisionTest.java @@ -93,7 +93,7 @@ public void updateVolumeBasedGrowthRate_ratioOne_keepsBaseRate() { .PottsModuleProliferationVolumeBasedDivisionMock module = new PottsModuleProliferationVolumeBasedDivisionMock(cell); - module.updateVolumeBasedGrowthRate(); + module.updateCellVolumeBasedGrowthRate(loc.getVolume(), cell.getCriticalVolume()); assertEquals(4.0, module.cellGrowthRate, 1e-9); } @@ -118,7 +118,7 @@ public void updateVolumeBasedGrowthRate_ratioGreaterThanOne_scalesUpByPowerLaw() .PottsModuleProliferationVolumeBasedDivisionMock module = new PottsModuleProliferationVolumeBasedDivisionMock(cell); - module.updateVolumeBasedGrowthRate(); + module.updateCellVolumeBasedGrowthRate(loc.getVolume(), cell.getCriticalVolume()); assertEquals(16.0, module.cellGrowthRate, 1e-9); } @@ -143,7 +143,7 @@ public void updateVolumeBasedGrowthRate_ratioLessThanOne_scalesDownByPowerLaw() .PottsModuleProliferationVolumeBasedDivisionMock module = new PottsModuleProliferationVolumeBasedDivisionMock(cell); - module.updateVolumeBasedGrowthRate(); + module.updateCellVolumeBasedGrowthRate(loc.getVolume(), cell.getCriticalVolume()); assertEquals(1.0, module.cellGrowthRate, 1e-9); } @@ -168,7 +168,7 @@ public void updateVolumeBasedGrowthRate_zeroSensitivity_returnsBaseRateRegardles .PottsModuleProliferationVolumeBasedDivisionMock module = new PottsModuleProliferationVolumeBasedDivisionMock(cell); - module.updateVolumeBasedGrowthRate(); + module.updateCellVolumeBasedGrowthRate(loc.getVolume(), cell.getCriticalVolume()); assertEquals(3.5, module.cellGrowthRate, 1e-9); } }