diff --git a/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java b/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java index 4405e45d..299116b0 100644 --- a/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java +++ b/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java @@ -20,6 +20,12 @@ */ public class PottsModuleFlyGMCDifferentiation extends PottsModuleProliferationVolumeBasedDivision { + /** + * Indicates whether GMC growth rate is based on individual cell conditions or average cell + * conditions. + */ + Boolean pdeLike; + /** * Creates a fly GMC proliferation module. * @@ -27,13 +33,42 @@ public class PottsModuleFlyGMCDifferentiation extends PottsModuleProliferationVo */ public PottsModuleFlyGMCDifferentiation(PottsCellFlyGMC cell) { super(cell); + pdeLike = (cell.getParameters().getInt("proliferation/PDELIKE") != 0); + } + + /** + * Computes the expected equilibrium average GMC volume over one cell cycle. + * + *

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

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

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

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

The cell location is split. The new neuron cell is created, initialized, and added to the - * schedule. This cell's location is also assigned to a new Neuron cell. + * schedule. This cell's location is also assigned to a new Neuron cell. The critical volume of + * both neurons is set to the initial volume of each neuron's location. * * @param random the random number generator * @param sim the simulation instance @@ -55,7 +90,7 @@ void addCell(MersenneTwisterFast random, Simulation sim) { (PottsCell) newContainer.convert(sim.getCellFactory(), newLocation, random); sim.getGrid().addObject(newCell, null); potts.register(newCell); - newCell.reset(potts.ids, potts.regions); + newCell.initialize(potts.ids, potts.regions); newCell.schedule(sim.getSchedule()); // remove old GMC cell from simulation @@ -88,7 +123,56 @@ void addCell(MersenneTwisterFast random, Simulation sim) { sim.getGrid().addObject(differentiatedGMC, null); potts.register(differentiatedGMC); - differentiatedGMC.reset(potts.ids, potts.regions); + differentiatedGMC.initialize(potts.ids, potts.regions); differentiatedGMC.schedule(sim.getSchedule()); } + + /** + * Updates the effective growth rate according to boolean flags specified in parameters. + * + * @param sim the simulation + */ + public void updateGrowthRate(Simulation sim) { + if (!dynamicGrowthRateVolume) { + cellGrowthRate = cellGrowthRateBase; + } else { + if (!pdeLike) { + updateCellVolumeBasedGrowthRate( + cell.getLocation().getVolume(), computeEquilibriumVolume()); + } else { + // PDE-like: use population-wide averages for GMCs (same pop as this cell). + // The reference volume is the population-average equilibrium volume: + // avgVRef = avgCritVol * (1 + sizeTarget) / 2 + sim.util.Bag objs = sim.getGrid().getAllObjects(); + + double volSum = 0.0; + double critSum = 0.0; + int count = 0; + + for (int i = 0; i < objs.numObjs; i++) { + Object o = objs.objs[i]; + if (!(o instanceof arcade.potts.agent.cell.PottsCell)) { + continue; + } + + arcade.potts.agent.cell.PottsCell c = (arcade.potts.agent.cell.PottsCell) o; + if (c.getPop() != cell.getPop()) { + continue; // keep to same population + } + + if (o instanceof arcade.potts.agent.cell.PottsCellFlyGMC) { + arcade.potts.agent.cell.PottsCellFlyGMC gmc = + (arcade.potts.agent.cell.PottsCellFlyGMC) o; + volSum += gmc.getLocation().getVolume(); + critSum += gmc.getCriticalVolume(); + count++; + } + } + double avgVolume = volSum / count; + double avgCritVol = critSum / count; + double avgVRef = avgCritVol * (1.0 + sizeTarget) / 2.0; + updateCellVolumeBasedGrowthRate(avgVolume, avgVRef); + } + } + } } diff --git a/src/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivision.java b/src/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivision.java index e154be0d..aae4bc94 100644 --- a/src/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivision.java +++ b/src/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivision.java @@ -7,13 +7,16 @@ import arcade.potts.util.PottsEnums.Phase; /** - * Implementation of {@link PottsModule} for fly GMC agents. The links must be set in the setup file - * so that 100% of the daughter cells are Neurons. + * Implementation of {@link PottsModule} for agents that divide upon reaching a volume threshold + * without any cell-cycle duration requirements. */ public abstract class PottsModuleProliferationVolumeBasedDivision extends PottsModuleProliferation { - /** Overall growth rate for cell (voxels/tick). */ - final double cellGrowthRate; + /** Base growth rate for cells (voxels/tick). */ + final double cellGrowthRateBase; + + /** Current growth rate for stem cells (voxels/tick). */ + double cellGrowthRate; /** * Target ratio of critical volume for division size checkpoint (cell must reach CRITICAL_VOLUME @@ -21,6 +24,14 @@ public abstract class PottsModuleProliferationVolumeBasedDivision extends PottsM */ final double sizeTarget; + /** Boolean flag indicating whether the growth rate should follow volume-sensitive ruleset. */ + final boolean dynamicGrowthRateVolume; + + /** + * Sensitivity of growth rate to cell volume, only relevant if dynamicGrowthRateVolume is true. + */ + final double growthRateVolumeSensitivity; + /** * Creates a proliferation module in which division is solely dependent on cell volume. * @@ -30,16 +41,52 @@ public PottsModuleProliferationVolumeBasedDivision(PottsCell cell) { super(cell); Parameters parameters = cell.getParameters(); sizeTarget = parameters.getDouble("proliferation/SIZE_TARGET"); - cellGrowthRate = parameters.getDouble("proliferation/CELL_GROWTH_RATE"); + cellGrowthRateBase = parameters.getDouble("proliferation/CELL_GROWTH_RATE"); + dynamicGrowthRateVolume = + (parameters.getInt("proliferation/DYNAMIC_GROWTH_RATE_VOLUME") != 0); + growthRateVolumeSensitivity = + parameters.getDouble("proliferation/GROWTH_RATE_VOLUME_SENSITIVITY"); setPhase(Phase.UNDEFINED); + cellGrowthRate = cellGrowthRateBase; } @Override public void step(MersenneTwisterFast random, Simulation sim) { + updateGrowthRate(sim); cell.updateTarget(cellGrowthRate, sizeTarget); boolean sizeCheck = cell.getVolume() >= sizeTarget * cell.getCriticalVolume(); if (sizeCheck) { addCell(random, sim); } } + + /** + * Updates the effective growth rate according to boolean flags specified in parameters. + * + * @param sim the simulation + */ + public abstract void updateGrowthRate(Simulation sim); + + /** + * Updates {@code cellGrowthRate} from a power-law relationship between current volume and a + * reference volume. + * + *

The updated rate is + * + *

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

The reference volume is the cell volume at which the basal growth rate is recovered. In + * the simplest case this can be the cell's critical volume, but users may use another + * biologically motivated reference such as an equilibrium or population-averaged volume. + * + * @param volume the current volume used in the growth-rate scaling + * @param referenceVolume the reference volume that defines the baseline growth-rate scale + */ + public void updateCellVolumeBasedGrowthRate(double volume, double referenceVolume) { + double refVol = referenceVolume; + cellGrowthRate = + cellGrowthRateBase * Math.pow((volume / refVol), growthRateVolumeSensitivity); + } } diff --git a/src/arcade/potts/parameter.potts.xml b/src/arcade/potts/parameter.potts.xml index ebc46e0d..c95d0ce6 100644 --- a/src/arcade/potts/parameter.potts.xml +++ b/src/arcade/potts/parameter.potts.xml @@ -64,6 +64,13 @@ + + + + + + + diff --git a/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiationTest.java b/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiationTest.java index c1b04e27..1334d992 100644 --- a/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiationTest.java +++ b/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiationTest.java @@ -133,7 +133,8 @@ final void tearDown() { @Test public void addCell_called_callsExpectedMethods() { - // When the module calls make() on the cell, return Quiescent PottsCellContainer mock + // When the module calls make() on the cell, return Quiescent PottsCellContainer + // mock container = mock(PottsCellContainer.class); when(gmcCell.make(eq(123), eq(State.QUIESCENT), any(MersenneTwisterFast.class))) .thenReturn(container); @@ -150,7 +151,7 @@ public void addCell_called_callsExpectedMethods() { verify(grid).addObject(newCell, null); verify(potts).register(newCell); - verify(newCell).reset(dummyIDs, dummyRegions); + verify(newCell).initialize(dummyIDs, dummyRegions); verify(newCell).schedule(schedule); verify(grid).removeObject(gmcCell, location); @@ -161,7 +162,161 @@ public void addCell_called_callsExpectedMethods() { (PottsCellFlyNeuron) constructed.convert(cellFactory, location, random); verify(grid).addObject(diffCell, null); verify(potts).register(diffCell); - verify(diffCell).reset(dummyIDs, dummyRegions); + verify(diffCell).initialize(dummyIDs, dummyRegions); verify(diffCell).schedule(schedule); } + + @Test + public void updateGrowthRate_dynamicOff_setsBaseRate() { + // dynamicGrowthRateVolume = 0; base rate used + when(gmcCell.getParameters()).thenReturn(parameters); + when(parameters.getInt("proliferation/DYNAMIC_GROWTH_RATE_VOLUME")).thenReturn(0); + when(parameters.getDouble("proliferation/CELL_GROWTH_RATE")).thenReturn(7.5); + + PottsModuleFlyGMCDifferentiation module = new PottsModuleFlyGMCDifferentiation(gmcCell); + + module.updateGrowthRate(sim); + org.junit.jupiter.api.Assertions.assertEquals(7.5, module.cellGrowthRate, 1e-9); + } + + @Test + public void updateGrowthRate_dynamicOn_pdeLikeFalse_usesSelfVolumeAndEquilibriumRef() { + when(gmcCell.getParameters()).thenReturn(parameters); + when(parameters.getInt("proliferation/DYNAMIC_GROWTH_RATE_VOLUME")).thenReturn(1); + when(parameters.getDouble("proliferation/CELL_GROWTH_RATE")).thenReturn(4.0); + when(parameters.getDouble("proliferation/SIZE_TARGET")).thenReturn(1.2); + when(parameters.getInt("proliferation/PDELIKE")).thenReturn(0); + + // critVol = 150.0; sizeTarget = 1.2 + // vRef = critVol * (1 + sizeTarget) / 2 = 150.0 * 2.2 / 2 = 165.0 + when(gmcCell.getCriticalVolume()).thenReturn(150.0); + when(gmcCell.getLocation().getVolume()).thenReturn(30.0); + + PottsModuleFlyGMCDifferentiation module = + org.mockito.Mockito.spy(new PottsModuleFlyGMCDifferentiation(gmcCell)); + + org.mockito.Mockito.doNothing() + .when(module) + .updateCellVolumeBasedGrowthRate( + org.mockito.ArgumentMatchers.anyDouble(), + org.mockito.ArgumentMatchers.anyDouble()); + + module.updateGrowthRate(sim); + + double expectedVRef = 150.0 * (1.0 + 1.2) / 2.0; // 165.0 + org.mockito.Mockito.verify(module) + .updateCellVolumeBasedGrowthRate( + org.mockito.ArgumentMatchers.eq(30.0), + org.mockito.ArgumentMatchers.eq(expectedVRef)); + } + + @Test + public void + updateGrowthRate_dynamicOnPdeLikeTrue_usesAverageVolumeAndEquilibriumRefAcrossGMCs() { + // Flags + when(gmcCell.getParameters()).thenReturn(parameters); + when(parameters.getInt("proliferation/DYNAMIC_GROWTH_RATE_VOLUME")).thenReturn(1); + when(parameters.getDouble("proliferation/CELL_GROWTH_RATE")).thenReturn(4.0); + when(parameters.getDouble("proliferation/SIZE_TARGET")).thenReturn(1.2); + when(parameters.getInt("proliferation/PDELIKE")).thenReturn(1); + + // Same population for all GMCs we want included + when(gmcCell.getPop()).thenReturn(3); + + // Self (included in average) + when(gmcCell.getLocation().getVolume()).thenReturn(30.0); + when(gmcCell.getCriticalVolume()).thenReturn(150.0); + + // Two more GMCs in same population + PottsCellFlyGMC gmcB = mock(PottsCellFlyGMC.class); + PottsCellFlyGMC gmcC = mock(PottsCellFlyGMC.class); + when(gmcB.getPop()).thenReturn(3); + when(gmcC.getPop()).thenReturn(3); + + PottsLocation locB = mock(PottsLocation.class); + PottsLocation locC = mock(PottsLocation.class); + when(gmcB.getLocation()).thenReturn(locB); + when(gmcC.getLocation()).thenReturn(locC); + when(locB.getVolume()).thenReturn(10.0); + when(locC.getVolume()).thenReturn(20.0); + when(gmcB.getCriticalVolume()).thenReturn(100.0); + when(gmcC.getCriticalVolume()).thenReturn(200.0); + + // Noise: different type and/or different pop → must be ignored + PottsCell randomOtherPop = mock(PottsCell.class); + when(randomOtherPop.getPop()).thenReturn(99); + PottsCellFlyNeuron neuronSamePop = mock(PottsCellFlyNeuron.class); + when(neuronSamePop.getPop()).thenReturn(3); + + // Bag with self + two GMCs + noise + sim.util.Bag bag = new sim.util.Bag(); + bag.add(gmcCell); // self GMC (pop 3) + bag.add(gmcB); // GMC (pop 3) + bag.add(gmcC); // GMC (pop 3) + bag.add(randomOtherPop); // different pop → ignored + bag.add(neuronSamePop); // not a GMC → ignored + when(sim.getGrid().getAllObjects()).thenReturn(bag); + + PottsModuleFlyGMCDifferentiation module = + org.mockito.Mockito.spy(new PottsModuleFlyGMCDifferentiation(gmcCell)); + + // Observe the averaged args + org.mockito.Mockito.doNothing() + .when(module) + .updateCellVolumeBasedGrowthRate( + org.mockito.ArgumentMatchers.anyDouble(), + org.mockito.ArgumentMatchers.anyDouble()); + + module.updateGrowthRate(sim); + + // avgVol = (30 + 10 + 20) / 3 = 20.0 + // avgCritVol = (150 + 100 + 200) / 3 = 150.0 + // avgVRef = avgCritVol * (1 + sizeTarget) / 2 = 150.0 * (1 + 1.2) / 2 = 165.0 + double expectedAvgVol = (30.0 + 10.0 + 20.0) / 3.0; // 20.0 + double expectedAvgCrit = (150.0 + 100.0 + 200.0) / 3.0; // 150.0 + double expectedAvgVRef = expectedAvgCrit * (1.0 + 1.2) / 2.0; // 165.0 + + org.mockito.Mockito.verify(module) + .updateCellVolumeBasedGrowthRate( + org.mockito.ArgumentMatchers.eq(expectedAvgVol), + org.mockito.ArgumentMatchers.eq(expectedAvgVRef)); + } + + // computeEquilibriumVolume tests + + @Test + public void computeEquilibriumVolume_returnsArithmeticMeanOfCritAndDivisionVolumes() { + // critVol = 150.0; sizeTarget = 1.2 + // vRef = critVol * (1 + sizeTarget) / 2 = 150.0 * 2.2 / 2 = 165.0 + when(parameters.getDouble("proliferation/SIZE_TARGET")).thenReturn(1.2); + when(gmcCell.getCriticalVolume()).thenReturn(150.0); + + PottsModuleFlyGMCDifferentiation module = new PottsModuleFlyGMCDifferentiation(gmcCell); + org.junit.jupiter.api.Assertions.assertEquals( + 165.0, module.computeEquilibriumVolume(), 1e-9); + } + + @Test + public void computeEquilibriumVolume_differentSizeTarget_scalesCorrectly() { + // critVol = 100.0; sizeTarget = 2.0 + // vRef = 100.0 * (1 + 2.0) / 2 = 150.0 + when(parameters.getDouble("proliferation/SIZE_TARGET")).thenReturn(2.0); + when(gmcCell.getCriticalVolume()).thenReturn(100.0); + + PottsModuleFlyGMCDifferentiation module = new PottsModuleFlyGMCDifferentiation(gmcCell); + org.junit.jupiter.api.Assertions.assertEquals( + 150.0, module.computeEquilibriumVolume(), 1e-9); + } + + @Test + public void computeEquilibriumVolume_differentCritVol_scalesCorrectly() { + // critVol = 200.0; sizeTarget = 1.2 + // vRef = 200.0 * (1 + 1.2) / 2 = 220.0 + when(parameters.getDouble("proliferation/SIZE_TARGET")).thenReturn(1.2); + when(gmcCell.getCriticalVolume()).thenReturn(200.0); + + PottsModuleFlyGMCDifferentiation module = new PottsModuleFlyGMCDifferentiation(gmcCell); + org.junit.jupiter.api.Assertions.assertEquals( + 220.0, module.computeEquilibriumVolume(), 1e-9); + } } diff --git a/test/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivisionTest.java b/test/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivisionTest.java index 4856f81e..733aaf89 100644 --- a/test/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivisionTest.java +++ b/test/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivisionTest.java @@ -5,14 +5,20 @@ import arcade.core.sim.Simulation; import arcade.core.util.Parameters; import arcade.potts.agent.cell.PottsCellFlyGMC; +import arcade.potts.env.location.PottsLocation2D; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.mockito.Mockito.*; public class PottsModuleProliferationVolumeBasedDivisionTest { static class PottsModuleProliferationVolumeBasedDivisionMock extends PottsModuleProliferationVolumeBasedDivision { + boolean addCellCalled = false; + boolean growthRateUpdated = false; + PottsModuleProliferationVolumeBasedDivisionMock(PottsCellFlyGMC cell) { super(cell); } @@ -21,15 +27,43 @@ static class PottsModuleProliferationVolumeBasedDivisionMock void addCell(MersenneTwisterFast random, Simulation sim) { addCellCalled = true; } + + @Override + public void updateGrowthRate(Simulation sim) { + growthRateUpdated = true; + cellGrowthRate = 7.5; + } } @Test - public void step_belowCheckpoint_updatesTargetOnly() { + public void step_called_usesGrowthRateSetByUpdateGrowthRate() { PottsCellFlyGMC cell = mock(PottsCellFlyGMC.class); Parameters params = mock(Parameters.class); + Simulation sim = mock(Simulation.class); + when(cell.getParameters()).thenReturn(params); when(params.getDouble("proliferation/SIZE_TARGET")).thenReturn(1.2); when(params.getDouble("proliferation/CELL_GROWTH_RATE")).thenReturn(4.0); + when(params.getInt("proliferation/DYNAMIC_GROWTH_RATE_VOLUME")).thenReturn(0); + when(params.getDouble("proliferation/GROWTH_RATE_VOLUME_SENSITIVITY")).thenReturn(2.0); + + when(cell.getVolume()).thenReturn(50.0); + when(cell.getCriticalVolume()).thenReturn(100.0); + + PottsModuleProliferationVolumeBasedDivisionMock module = + new PottsModuleProliferationVolumeBasedDivisionMock(cell); + module.step(new MersenneTwisterFast(), sim); + + verify(cell).updateTarget(7.5, 1.2); + assertFalse(module.addCellCalled); + } + + @Test + public void step_belowCheckpoint_updates() { + PottsCellFlyGMC cell = mock(PottsCellFlyGMC.class); + Parameters params = mock(Parameters.class); + when(cell.getParameters()).thenReturn(params); + when(params.getDouble("proliferation/SIZE_TARGET")).thenReturn(1.2); when(cell.getCriticalVolume()).thenReturn(100.0); when(cell.getVolume()).thenReturn(50.0); // below checkpoint @@ -38,7 +72,8 @@ public void step_belowCheckpoint_updatesTargetOnly() { module.step(mock(MersenneTwisterFast.class), mock(Simulation.class)); - verify(cell).updateTarget(4.0, 1.2); + verify(cell).updateTarget(7.5, 1.2); + assert module.growthRateUpdated : "growth rate should be updated on every step"; assert !module.addCellCalled : "addCell should not be called below checkpoint"; } @@ -49,7 +84,6 @@ public void step_atOrAboveCheckpoint_triggersAddCell() { Parameters params = mock(Parameters.class); when(cell.getParameters()).thenReturn(params); when(params.getDouble("proliferation/SIZE_TARGET")).thenReturn(1.2); - when(params.getDouble("proliferation/CELL_GROWTH_RATE")).thenReturn(4.0); when(cell.getCriticalVolume()).thenReturn(100.0); when(cell.getVolume()).thenReturn(120.0); // at or above checkpoint @@ -58,7 +92,108 @@ public void step_atOrAboveCheckpoint_triggersAddCell() { module.step(mock(MersenneTwisterFast.class), mock(Simulation.class)); - verify(cell).updateTarget(4.0, 1.2); + verify(cell).updateTarget(7.5, 1.2); + assert module.growthRateUpdated : "growth rate should be updated on every step"; assert module.addCellCalled : "addCell should be called at or above checkpoint"; } + + @Test + public void updateVolumeBasedGrowthRate_ratioOne_keepsBaseRate() { + // baseGrowth = 4.0, volume = Ka => growth = 4.0 + PottsCellFlyGMC cell = mock(PottsCellFlyGMC.class); + Parameters params = mock(Parameters.class); + PottsLocation2D loc = mock(PottsLocation2D.class); + + when(cell.getParameters()).thenReturn(params); + when(params.getDouble("proliferation/SIZE_TARGET")).thenReturn(1.2); + when(params.getDouble("proliferation/CELL_GROWTH_RATE")).thenReturn(4.0); + when(params.getInt("proliferation/DYNAMIC_GROWTH_RATE_VOLUME")).thenReturn(1); + when(params.getDouble("proliferation/GROWTH_RATE_VOLUME_SENSITIVITY")).thenReturn(2.0); + + when(cell.getLocation()).thenReturn(loc); + when(loc.getVolume()).thenReturn(100.0); + when(cell.getCriticalVolume()).thenReturn(100.0); + + PottsModuleProliferationVolumeBasedDivisionTest + .PottsModuleProliferationVolumeBasedDivisionMock + module = new PottsModuleProliferationVolumeBasedDivisionMock(cell); + + module.updateCellVolumeBasedGrowthRate(loc.getVolume(), cell.getCriticalVolume()); + assertEquals(4.0, module.cellGrowthRate, 1e-9); + } + + @Test + public void updateVolumeBasedGrowthRate_ratioGreaterThanOne_scalesUpByPowerLaw() { + // baseGrowth = 2.0, ratio = 2.0, sensitivity = 3 => 2 * 2^3 = 2 * 8 = 12 + PottsCellFlyGMC cell = mock(PottsCellFlyGMC.class); + Parameters params = mock(Parameters.class); + PottsLocation2D loc = mock(PottsLocation2D.class); + + when(cell.getParameters()).thenReturn(params); + when(params.getDouble("proliferation/SIZE_TARGET")).thenReturn(1.2); + when(params.getDouble("proliferation/CELL_GROWTH_RATE")).thenReturn(2.0); + when(params.getInt("proliferation/DYNAMIC_GROWTH_RATE_VOLUME")).thenReturn(1); + when(params.getDouble("proliferation/GROWTH_RATE_VOLUME_SENSITIVITY")).thenReturn(3.0); + + when(cell.getLocation()).thenReturn(loc); + when(loc.getVolume()).thenReturn(200.0); + when(cell.getCriticalVolume()).thenReturn(100.0); + + PottsModuleProliferationVolumeBasedDivisionTest + .PottsModuleProliferationVolumeBasedDivisionMock + module = new PottsModuleProliferationVolumeBasedDivisionMock(cell); + + module.updateCellVolumeBasedGrowthRate(loc.getVolume(), cell.getCriticalVolume()); + assertEquals(16.0, module.cellGrowthRate, 1e-9); + } + + @Test + public void updateVolumeBasedGrowthRate_ratioLessThanOne_scalesDownByPowerLaw() { + // baseGrowth = 4.0, ratio = 0.5, sensitivity = 2.0 => 4 * 0.5^2 = 1.0 + PottsCellFlyGMC cell = mock(PottsCellFlyGMC.class); + Parameters params = mock(Parameters.class); + PottsLocation2D loc = mock(PottsLocation2D.class); + + when(cell.getParameters()).thenReturn(params); + when(params.getDouble("proliferation/SIZE_TARGET")).thenReturn(1.2); + when(params.getDouble("proliferation/CELL_GROWTH_RATE")).thenReturn(4.0); + when(params.getInt("proliferation/DYNAMIC_GROWTH_RATE_VOLUME")).thenReturn(1); + when(params.getDouble("proliferation/GROWTH_RATE_VOLUME_SENSITIVITY")).thenReturn(2.0); + + when(cell.getLocation()).thenReturn(loc); + when(loc.getVolume()).thenReturn(50.0); + when(cell.getCriticalVolume()).thenReturn(100.0); + + PottsModuleProliferationVolumeBasedDivisionTest + .PottsModuleProliferationVolumeBasedDivisionMock + module = new PottsModuleProliferationVolumeBasedDivisionMock(cell); + + module.updateCellVolumeBasedGrowthRate(loc.getVolume(), cell.getCriticalVolume()); + assertEquals(1.0, module.cellGrowthRate, 1e-9); + } + + @Test + public void updateVolumeBasedGrowthRate_zeroSensitivity_returnsBaseRateRegardlessOfVolume() { + // sensitivity = 0 => growth = baseGrowth * ratio^0 = baseGrowth + PottsCellFlyGMC cell = mock(PottsCellFlyGMC.class); + Parameters params = mock(Parameters.class); + PottsLocation2D loc = mock(PottsLocation2D.class); + + when(cell.getParameters()).thenReturn(params); + when(params.getDouble("proliferation/SIZE_TARGET")).thenReturn(1.2); + when(params.getDouble("proliferation/CELL_GROWTH_RATE")).thenReturn(3.5); + when(params.getInt("proliferation/DYNAMIC_GROWTH_RATE_VOLUME")).thenReturn(1); + when(params.getDouble("proliferation/GROWTH_RATE_VOLUME_SENSITIVITY")).thenReturn(0.0); + + when(cell.getLocation()).thenReturn(loc); + when(loc.getVolume()).thenReturn(250.0); + when(cell.getCriticalVolume()).thenReturn(100.0); + + PottsModuleProliferationVolumeBasedDivisionTest + .PottsModuleProliferationVolumeBasedDivisionMock + module = new PottsModuleProliferationVolumeBasedDivisionMock(cell); + + module.updateCellVolumeBasedGrowthRate(loc.getVolume(), cell.getCriticalVolume()); + assertEquals(3.5, module.cellGrowthRate, 1e-9); + } }