diff --git a/src/arcade/potts/agent/cell/PottsCellFlyGMC.java b/src/arcade/potts/agent/cell/PottsCellFlyGMC.java index 48f4a212a..5fdd15cf5 100644 --- a/src/arcade/potts/agent/cell/PottsCellFlyGMC.java +++ b/src/arcade/potts/agent/cell/PottsCellFlyGMC.java @@ -55,6 +55,41 @@ public PottsCellContainer make(int newID, CellState newState, MersenneTwisterFas criticalRegionHeights); } + /** + * Creates a container for a new daughter cell with an explicit critical volume. + * + *

Unlike {@link #make(int, CellState, MersenneTwisterFast)}, which copies this cell's + * critical volume, this overload lets the caller set the daughter's critical volume from the + * volume it is actually born into rather than inheriting the parent's. + * + * @param newID the daughter cell ID + * @param newState the daughter cell state + * @param assignedCritVol the critical volume assigned to the daughter cell + * @param random the random number generator + * @return a container for the daughter cell + */ + public PottsCellContainer make( + int newID, CellState newState, double assignedCritVol, MersenneTwisterFast random) { + divisions++; + + int newPop = links.next(random); + + return new PottsCellContainer( + newID, + id, + newPop, + age, + divisions, + newState, + null, + 0, + null, + assignedCritVol, + criticalHeight, + criticalRegionVolumes, + criticalRegionHeights); + } + @Override void setStateModule(CellState newState) { switch ((State) newState) { diff --git a/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java b/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java index 4405e45dc..f85e244c6 100644 --- a/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java +++ b/src/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiation.java @@ -29,11 +29,37 @@ public PottsModuleFlyGMCDifferentiation(PottsCellFlyGMC cell) { super(cell); } + /** + * Computes the expected equilibrium average GMC volume over one cell cycle. + * + *

In the Potts model, 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 @@ -50,12 +76,14 @@ void addCell(MersenneTwisterFast random, Simulation sim) { // Create and schedule new neuron cell int newID = sim.getID(); - CellContainer newContainer = cell.make(newID, State.QUIESCENT, random); + CellContainer newContainer = + ((PottsCellFlyGMC) cell) + .make(newID, State.QUIESCENT, newLocation.getVolume(), random); PottsCell newCell = (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 @@ -78,7 +106,7 @@ void addCell(MersenneTwisterFast random, Simulation sim) { null, 0, null, - oldCell.getCriticalVolume(), + location.getVolume(), oldCell.getCriticalHeight(), oldCell.getCriticalRegionVolumes(), oldCell.getCriticalRegionHeights()); @@ -88,7 +116,25 @@ 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. + * + *

The rule is selected as follows. When {@code DYNAMIC_GROWTH_RATE_VOLUME} is off the growth + * rate is simply the basal rate. When it is on, cells use a per-cell rule that compares each + * cell's own volume against its equilibrium volume + * + * @param sim the simulation + */ + public void updateGrowthRate(Simulation sim) { + if (!dynamicGrowthRateVolume) { + cellGrowthRate = cellGrowthRateBase; + } else { + updateCellVolumeBasedGrowthRate( + cell.getLocation().getVolume(), computeEquilibriumVolume()); + } + } } diff --git a/src/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivision.java b/src/arcade/potts/agent/module/PottsModuleProliferationVolumeBasedDivision.java index e154be0d4..aae4bc94f 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 ebc46e0da..1cee61d43 100644 --- a/src/arcade/potts/parameter.potts.xml +++ b/src/arcade/potts/parameter.potts.xml @@ -64,6 +64,10 @@ + + + + diff --git a/test/arcade/potts/agent/cell/PottsCellFlyGMCTest.java b/test/arcade/potts/agent/cell/PottsCellFlyGMCTest.java index adcd0cc7c..aa807a328 100644 --- a/test/arcade/potts/agent/cell/PottsCellFlyGMCTest.java +++ b/test/arcade/potts/agent/cell/PottsCellFlyGMCTest.java @@ -125,4 +125,26 @@ public void make_called_returnsCorrectNewContainer() { () -> assertNull(container.criticalRegionVolumes), () -> assertNull(container.criticalRegionHeights)); } + + @Test + public void make_withExplicitCritVol_usesProvidedCritVol() { + PottsCellFlyGMC gmc = + new PottsCellFlyGMC(baseContainer, locationMock, parametersMock, links); + double explicitCritVol = cellCriticalVolume + 50.0; + PottsCellContainer container = gmc.make(cellID, State.QUIESCENT, explicitCritVol, random); + assertAll( + () -> assertNotNull(container), + () -> assertEquals(cellID, container.parent), + () -> assertEquals(1, container.pop), + () -> assertEquals(cellAge, container.age), + () -> assertEquals(cellDivisions + 1, container.divisions), + () -> assertEquals(State.QUIESCENT, container.state), + () -> assertNull(container.phase), + () -> assertEquals(0, container.voxels), + () -> assertNull(container.regionVoxels), + () -> assertEquals(explicitCritVol, container.criticalVolume, EPSILON), + () -> assertEquals(cellCriticalHeight, container.criticalHeight, EPSILON), + () -> assertNull(container.criticalRegionVolumes), + () -> assertNull(container.criticalRegionHeights)); + } } diff --git a/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiationTest.java b/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiationTest.java index c1b04e272..7c808e485 100644 --- a/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiationTest.java +++ b/test/arcade/potts/agent/module/PottsModuleFlyGMCDifferentiationTest.java @@ -133,9 +133,18 @@ final void tearDown() { @Test public void addCell_called_callsExpectedMethods() { + double newLocVol = 42.0; + double locVol = 50.0; + when(newLocation.getVolume()).thenReturn(newLocVol); + when(location.getVolume()).thenReturn(locVol); + // 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))) + when(gmcCell.make( + eq(123), + eq(State.QUIESCENT), + eq(newLocVol), + any(MersenneTwisterFast.class))) .thenReturn(container); newCell = mock(PottsCell.class); when(container.convert(eq(cellFactory), eq(newLocation), any(MersenneTwisterFast.class))) @@ -146,11 +155,11 @@ public void addCell_called_callsExpectedMethods() { module.addCell(random, sim); verify(location).split(random); verify(gmcCell).reset(dummyIDs, dummyRegions); - verify(gmcCell).make(123, State.QUIESCENT, random); + verify(gmcCell).make(123, State.QUIESCENT, newLocVol, random); 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 +170,86 @@ 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_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); + // 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)); + } + + // 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_doubleSizeTarget_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 4856f81e0..733aaf896 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); + } }