Fix asymmetric division daughter selection, determinism, and spurious exit(-1) - #58
Merged
Merged
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes correctness and reproducibility issues in PhysiCell’s asymmetric division daughter-type selection, and replaces a hard exit(-1) failure mode with a tolerant, warn-and-continue repair pathway. It also adds a dedicated unit test suite to validate frequency matching, determinism, and round-off / rescale behavior.
Changes:
- Correct
select_daughter_types()to sample from a cumulative distribution (instead of comparing against individual probabilities). - Make selection deterministic for a given RNG seed by changing
asymmetric_division_probabilitiesfromstd::unordered_mapto an orderedstd::mapwith a custom comparator. - Replace fatal
exit(-1)on floating-point/overspec cases withenforce_valid_distribution()(tolerant absorb/rescale behavior) plus warn-once-per-cell-type logging, and add unit tests covering these cases.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
core/PhysiCell_phenotype.h |
Switch asymmetric-division probability storage to std::map with deterministic ordering; add enforce_valid_distribution() API and helper totals/normalization declarations. |
core/PhysiCell_phenotype.cpp |
Fix daughter selection to use cumulative probability; implement probabilities_total_excluding_self(), normalize_probabilities(), and enforce_valid_distribution(). |
core/PhysiCell_standard_models.cpp |
Use the new repair function in asymmetric_division_function() and replace fatal exit with warn-once behavior. |
modules/PhysiCell_MultiCellDS.cpp |
Remove reserve() on asymmetric_division_probabilities since it is now a std::map. |
unit_tests/asymmetric_division/test_asymmetric_division.cpp |
Add a unit test executable validating sampling frequencies, determinism, and repair behavior. |
unit_tests/asymmetric_division/Makefile |
Add build/run targets for the new asymmetric division unit tests. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
drbergman
force-pushed
the
fix-asym-div-selection
branch
from
August 7, 2026 12:10
eb35f07 to
5626c25
Compare
Three problems in Asymmetric_Division. select_daughter_types() never formed a cumulative distribution. It drew one UniformRandom() and then compared it against each entry's own probability, so the first entry whose probability happened to exceed r won. Realised daughter type frequencies did not match the configured probabilities. It now walks a running sum, the same idiom as choose_event() in PhysiCell_utilities.cpp. The outcome depended on std::unordered_map iteration order. Even with the arithmetic corrected, the code drew one random number and then walked a hash-ordered container, whose order is unspecified and varies with the standard library, with insertion history, and with rehashing -- so two runs with the same seed could pick different daughter types across platforms or compilers. asymmetric_division_probabilities is now a std::map ordered by the upper-triangular (min,max) form of the type pair, which is a function of the cell type indices alone. modules/PhysiCell_MultiCellDS.cpp appears in this diff only because std::map is node-based and has no reserve(), so recreate_sim_state() drops that call. exit(-1) fired on correctly configured models. Probabilities meant to sum to exactly 1 routinely sum to a hair over it in double precision -- 0.11 + 0.33 + 0.56 is 1.000000000000000222 -- which tripped the "cannot be normalized" branch on models that were fine. A 1e-12 tolerance fixes that. Behaviour on a genuinely over-specified model is deliberately unchanged: it still reports and exits. Nothing is rescaled. Quietly renormalizing would let a run continue with daughter frequencies the modeller never asked for, and that is not a decision this code should make on their behalf. The diagnostic now prints the CELL's current probabilities rather than the cell definition's configured ones, since a rule or custom function may have changed them since setup. The repair of an absorbable excess -- where the asymmetric pairs still sum to at most 1 and the remainder belongs to symmetric division -- is unchanged in behaviour, but is now computed from the other pairs rather than from the previous self-pair value, which makes it exactly idempotent. It runs on every division, so that matters. Verified with a unit test suite covering frequency matching, insertion-order independence, idempotence over 1,000,000 repeats, the round-off tolerance, and the over-specified path: 25 checks, all passing. The tests are kept out of this branch so the diff stays limited to files that compile into a simulation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
drbergman
force-pushed
the
fix-asym-div-selection
branch
from
August 7, 2026 12:45
5626c25 to
17802aa
Compare
select_daughter_types() never formed a cumulative distribution. It drew one UniformRandom() and compared it against each entry's own probability, so the first entry whose probability happened to exceed r won, and realised daughter type frequencies did not match the configured ones. It now walks a running sum. The outcome also depended on std::unordered_map iteration order, which is unspecified and varies with the standard library, insertion history, and rehashing -- so two runs with the same seed could pick different daughter types on different platforms. asymmetric_division_probabilities is now a std::map ordered by the (min,max) form of the type pair, which depends only on the cell type indices. modules/PhysiCell_MultiCellDS.cpp is in this diff only because std::map has no reserve(), so recreate_sim_state() drops that call. exit(-1) fired on correctly configured models. Probabilities meant to sum to exactly 1 routinely sum to a hair over it in double precision -- 0.11 + 0.33 + 0.56 is 1.000000000000000222 -- which tripped the "cannot be normalized" branch on models that were fine. A 1e-12 tolerance on both the entry test and the negative-probability test fixes that, and the arithmetic is otherwise left as it was. Behaviour on a genuinely over-specified model is unchanged: it still reports and exits, and nothing is rescaled. The diagnostic now prints the CELL's current probabilities rather than iterating every cell type pair from the definitions, since a rule or custom function may have changed them since setup. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
drbergman
force-pushed
the
fix-asym-div-selection
branch
from
August 7, 2026 13:30
69aa4c4 to
38772af
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three problems in
Asymmetric_Division, plus a unit test suite.1.
select_daughter_typesnever formed a cumulative distributionrwas tested against each entry's own probability instead of a running sum, so the first entry whose probability happened to exceedrwon. Realised daughter-type frequencies did not match the configured probabilities.PhysiCell_utilities.cpp choose_event()already had the correct idiom.2. The outcome depended on
std::unordered_mapiteration orderEven with the arithmetic corrected, the code drew one
UniformRandom()and then walked a hash-ordered container. Iteration order there is unspecified and varies with the standard library, with insertion history, and with rehashing — so two runs with the same seed could select different daughter types across platforms or compilers. For a simulator that seeds specifically for reproducibility that is a defect in its own right, and fixing the arithmetic alone does not address it.asymmetric_division_probabilitiesis now astd::mapwith apair_compareordering keyed on the upper-triangular(min, max)form of the type pair. That is a function of the cell type indices alone, so iteration order — and hence the daughter types chosen for a given seed — no longer depends on insertion order, rehashing, or the standard library implementation.This is why
modules/PhysiCell_MultiCellDS.cppappears in the diff:std::mapis node-based and has noreserve(), sorecreate_sim_state()drops that call. Four lines, and it is a build requirement of the container change rather than unrelated churn.3.
exit(-1)fired on correctly configured modelsThe
sym_div_prob < 0.0branch hard-killed the simulation from inside a cell division. It turns out this triggers on models whose probabilities are configured correctly, purely from floating-point round-off — a distribution summing to 1.0 in exact arithmetic can land fractionally below zero after the subtraction. At least one user has commented this branch out of their production build to stop it killing multi-hour HPC runs, which means they are now running with no diagnostic at all.Replaced with a round-off tolerance plus a warn-once-per-cell-type rescale, extracted into a testable
enforce_valid_distribution(). A long run survives and is still told.Worth recording what was checked and found not to be broken: the
> 1.0normalization was suspected of drifting over repeated divisions, since it rewrites the cell's own map rather than normalizing a copy. It does not. It is exactly idempotent, verified across 200,000 configurations and 1,000,000 repeated applications, so it was left alone.Tests
New
unit_tests/asymmetric_division/. The frequency test samplesselect_daughter_typesagainst a known distribution and asserts the empirical frequencies match — it fails on the old code and passes on the new. Determinism, round-off, and rescale behaviour are covered too.Both the
standard_asymmetric_divisionandextended_asymmetric_divisionXML paths route through this code and both still work.The same change is prepared for
feature-extended-asym-div(PR MathCancer#385 upstream) and is not included here.🤖 Generated with Claude Code