Skip to content

Give each cell definition its own fixed_duration and death-phase parameters - #424

Open
drbergman wants to merge 3 commits into
MathCancer:developmentfrom
drbergman:claude/issue-199-per-definition-death-params
Open

Give each cell definition its own fixed_duration and death-phase parameters#424
drbergman wants to merge 3 commits into
MathCancer:developmentfrom
drbergman:claude/issue-199-per-definition-death-params

Conversation

@drbergman

@drbergman drbergman commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Fixes #199.

Death::models is a std::vector<Cycle_Model*> holding the addresses of the apoptosis and necrosis globals, so every cell definition shares one death model object. fixed_duration lived in Phase_Link, inside that shared object, and the parser wrote it per cell definition — so each definition overwrote the flag for every other one and the last one parsed decided for all of them. The <duration> value went the same way, since the parser also wrote models[i]->transition_rate(...).

This is specific to death. The live cycle was never affected: Cell_Functions::cycle_model is a Cycle_Model held by value, so each definition owns its own copy of live, Ki67_basic and the rest, and its Phase_Link flags were already per-definition.

What this does

Migrates the flag from the graph structure to the param structure, which is what @MathCancer proposed on the issue.

  • Cycle_Data gains fixed_durations, indexed exactly like transition_rates, with fixed_duration(i,j) and exit_fixed_duration(i) mirroring transition_rate(i,j) and exit_rate(i). Phase_Link::fixed_duration is removed, and the six standard models declare their defaults through the model's own Cycle_Data. For the live cycle this is plumbing rather than a fix — it gives death models somewhere per-definition to keep the flag. Its one behavioural effect is that the flag is now per-cell rather than per-definition, which is how transition_rates already worked.
  • Death models need more than that. There is no per-definition Cycle_Data for a death model, and Cycle::sync_to_cycle_model() does data = cm.data, so even a correctly per-definition flag would be overwritten from the shared model at the moment of death — which is also why the <duration> value is flattened along with the flag, as @rheiland observed. Death gains model_data, one Cycle_Data per death model, seeded at add_death_model(), written by the parser, applied through a new two-argument sync_to_cycle_model( cm, cd ).
  • Both sites that move a dying cell onto its death model use it: start_death() and the check_for_death() branch of advance_bundled_phenotype_functions(). The second matters most, since that is the path a cell takes when it dies from its death_rate.
  • display_cell_definitions() reports the per-definition parameters rather than the shared model's.

Verification

interaction-sample, with stem asking for a fixed 100 min apoptosis, differentiated for 700, and neutrophil — parsed last, so previously the one that won — for a rate. Logging every cell as it enters a death model, over 649 deaths and 61 transformations:

cell at death before after
stem fixed=0, 516 fixed=1, 100
differentiated fixed=0, 516 fixed=1, 700
differentiated, having transformed from stem fixed=0, 516 fixed=1, 700
neutrophil fixed=0, 516 fixed=0
bacteria, no <phase_durations> in XML fixed=0, 516 fixed=1, 516

A transformed cell picks up the death parameters of the definition it became: convert_to_cell_definition() does phenotype = cd.phenotype, so model_data travels with models and rates. The two stayed the same length at all 649 deaths. The startup summary now prints the same per-definition values.

Compatibility

Results change for any model whose cell definitions do not all give their death models the same parameters. Two things were being flattened onto the shared model, independently of each other:

  1. the transition rate for a death phase, and
  2. the boolean saying whether that phase has a deterministic length.

Whichever cell definition was parsed last set both, for every definition.

Neither is tied to how the XML expresses it. <phase_durations> and <phase_transition_rates> write the same rate slot — exit_rate(i) is transition_rates[i][0], and transition_rate(i,j) resolves there too for a single-link death phase — and both read fixed_duration from the attribute with identical logic, so a definition can perfectly well ask for a duration with a stochastic exit or a rate with a deterministic one.

Of the two, the boolean is the more disruptive to get wrong: it switches a phase between a deterministic exit and a Poisson process with the same mean, so aggregate numbers look plausible while the variance is entirely different and the phase can now be exited on its first step. The rate being flattened is a plain quantitative error.

Live cycle timing is unchanged.

Phase_Link::fixed_duration is removed, so out-of-tree code setting phase_link(i,j).fixed_duration needs data.fixed_duration(i,j) instead. Nothing in this repository outside core/ uses it in C++.

…meters

Fixes MathCancer#199.

Cycle_Model objects are shared by pointer between cell definitions: apoptosis
and necrosis are globals, and every definition registers the same address via
add_death_model( rate, &apoptosis, ... ). Cell_Definition's copy constructor
copies the pointer, not the model.

fixed_duration lived in Phase_Link, inside that shared object, while the XML
parser wrote it per cell definition. So each definition overwrote the flag for
every other one and the last definition parsed decided for all of them. A model
asking for deterministic apoptosis on most of its cell types silently got the
stochastic branch on all of them, with no warning and nothing in the saved
output to reveal it.

Migrate the flag from the graph structure to the param structure, as Paul
suggested on the issue: Cycle_Data gains fixed_durations, indexed exactly like
transition_rates, with fixed_duration() / exit_fixed_duration() accessors
mirroring transition_rate() / exit_rate(). Cycle_Model::advance_model now reads
the per-cell copy. Phase_Link::fixed_duration is removed; the six standard
models declare their defaults through the model's own Cycle_Data instead, and
those defaults still reach cells that do not override them in XML.

That alone is not enough for the death models. There is no per-definition
Cycle_Data for a death model -- Death holds rates, models and parameters, none
of which carry cycle parameters -- so the parser wrote durations to the shared
models[i]->data, and Cycle::sync_to_cycle_model() copies that same shared data
over the cell at start_death(). Relocating the flag would have moved it from
one shared object to another. This is also why rheiland observed on the issue
that the <duration> value is flattened along with the flag.

So Death gains model_data, one Cycle_Data per death model, seeded from the model
at add_death_model(), written by the parser, and applied at start_death()
through a new two-argument Cycle::sync_to_cycle_model( cm, cd ). The
single-argument form delegates to it and keeps its exact previous behaviour.

Verified on a 7-cell-type model where six definitions ask for
<phase_durations fixed_duration="true"> and the last asks for a rate. Before:
all seven report fixed=0. After: six report 1 and the last reports 0, while all
seven still share one Cycle_Model object, so the phase graph stays shared and
Phenotype copies stay cheap. A definition with no apoptosis <phase_durations> at
all still inherits the standard model's default.

Note: this changes results for any model that mixes <phase_durations> and
<phase_transition_rates> across its cell definitions. Those models were not
getting the death timing their XML asked for; they will now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a long-standing bug (#199) where fixed_duration (and related phase timing parameters) were effectively shared across cell definitions due to shared Cycle_Model instances—most notably for the standard apoptosis/necrosis death models—causing later-parsed XML definitions to overwrite earlier ones.

Changes:

  • Moves the fixed_duration flag from Phase_Link (shared graph) into Cycle_Data (per-instance parameters) and updates standard model defaults accordingly.
  • Introduces per-definition death-cycle parameter storage via Death::model_data, and applies it at death start using a new Cycle::sync_to_cycle_model(cm, cd) overload.
  • Updates XML parsing and death start logic to write/apply per-definition (non-shared) cycle/death timing parameters.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
core/PhysiCell_standard_models.cpp Updates standard models to set fixed-duration defaults via Cycle_Data rather than Phase_Link.
core/PhysiCell_phenotype.h Removes Phase_Link::fixed_duration, adds Cycle_Data::fixed_durations, adds Death::model_data, and adds a 2-arg Cycle::sync_to_cycle_model.
core/PhysiCell_phenotype.cpp Implements fixed_duration accessors, switches cycle advancement to read per-cell Cycle_Data, seeds Death::model_data, and adds the 2-arg sync implementation.
core/PhysiCell_cell.cpp Applies per-definition death-cycle parameters at start_death() and updates XML parsing to write into per-definition data instead of shared models.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread core/PhysiCell_phenotype.cpp Outdated
Comment thread core/PhysiCell_phenotype.h Outdated
Comment thread core/PhysiCell_phenotype.cpp Outdated
Comment thread core/PhysiCell_phenotype.h
drbergman and others added 2 commits August 6, 2026 20:48
Cell::advance_bundled_phenotype_functions syncs the cycle to the death
model directly when check_for_death fires, rather than going through
start_death. That site was still using the single-argument
sync_to_cycle_model, so cells dying by death_rate -- the common case --
kept getting parameters from the shared Cycle_Model and MathCancer#199 was
unfixed on that path.

Also from review: hoist the transition_rates / fixed_durations resize
out of the inner phase-link loop in Cycle_Data::sync_to_cycle_model, and
take the Cycle_Data by const reference in the new Cycle overload.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
display_cell_definitions() read the death phase durations out of
death.models[k]->data, the shared Cycle_Model. The parser no longer
writes that object, so after the rest of this PR the summary printed the
compiled-in standard-model defaults for every definition regardless of
the XML -- and that summary is exactly what a user reads to check
whether MathCancer#199 is fixed.

Take the parameters from death.model_data[k] instead. Phases and phase
links still come from the shared model, which is correct: only the
parameters moved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@drbergman
drbergman marked this pull request as ready for review August 8, 2026 19:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants