Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions src/arcade/potts/agent/cell/PottsCellFlyGMC.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,37 @@ public PottsModuleFlyGMCDifferentiation(PottsCellFlyGMC cell) {
super(cell);
}

/**
* Computes the expected equilibrium average GMC volume over one cell cycle.
*
* <p>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}.
*
* <p>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:
*
* <pre>
* V_ref = (criticalVolume + sizeTarget * criticalVolume) / 2
* = criticalVolume * (1 + sizeTarget) / 2
* </pre>
*
* <p>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.
*
* <p>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
Expand All @@ -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);
Comment thread
kristaphommatha marked this conversation as resolved.
newCell.schedule(sim.getSchedule());

// remove old GMC cell from simulation
Expand All @@ -78,7 +106,7 @@ void addCell(MersenneTwisterFast random, Simulation sim) {
null,
0,
null,
oldCell.getCriticalVolume(),
location.getVolume(),
oldCell.getCriticalHeight(),
oldCell.getCriticalRegionVolumes(),
oldCell.getCriticalRegionHeights());
Expand All @@ -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.
*
* <p>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) {
Comment thread
Jannetty marked this conversation as resolved.
if (!dynamicGrowthRateVolume) {
cellGrowthRate = cellGrowthRateBase;
} else {
updateCellVolumeBasedGrowthRate(
cell.getLocation().getVolume(), computeEquilibriumVolume());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,31 @@
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
* * 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;
Comment thread
Jannetty marked this conversation as resolved.

/**
* Creates a proliferation module in which division is solely dependent on cell volume.
*
Expand All @@ -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);
Comment thread
allison-li-1016 marked this conversation as resolved.

/**
* Updates {@code cellGrowthRate} from a power-law relationship between current volume and a
* reference volume.
*
* <p>The updated rate is
*
* <pre>
* cellGrowthRate = cellGrowthRateBase * (volume / referenceVolume)^growthRateVolumeSensitivity
* </pre>
*
* <p>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);
}
}
4 changes: 4 additions & 0 deletions src/arcade/potts/parameter.potts.xml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@
<population.module module="proliferation" id="NUCLEUS_GROWTH_RATE" value="30" units="um^3/hour" conversion="DS^-3.DT" description="basal rate of nucleus growth" />
<population.module module="proliferation" id="NUCLEUS_CONDENSATION_FRACTION" value="0.5" description="fraction of nuclear volume when condensed" />

<!-- ── Fly cell proliferation module parameters ───────────────────── -->
<population.module module="proliferation" id="DYNAMIC_GROWTH_RATE_VOLUME" value="0" description="1 → enable volume-sensitive growth rate; 0 → disabled" />
<population.module module="proliferation" id="GROWTH_RATE_VOLUME_SENSITIVITY" value="3.0" description="exponent in (volume/criticalVolume)^sensitivity when volume rule is enabled" />

<!-- apoptosis module parameters -->
<population.module module="apoptosis" id="RATE_EARLY" value="4" units="steps/hour" conversion="DT" description="rate of events in early apoptosis phase" />
<population.module module="apoptosis" id="RATE_LATE" value="1.8" units="steps/hour" conversion="DT" description="rate of events in late apoptosis phase" />
Expand Down
22 changes: 22 additions & 0 deletions test/arcade/potts/agent/cell/PottsCellFlyGMCTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand All @@ -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);
Expand All @@ -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);
}
}
Loading
Loading