Skip to content

Add a weighted mode for asymmetric division - #67

Merged
drbergman merged 3 commits into
feature-extended-asym-divfrom
claude/normalized-weighted-mode-79cb76
Aug 21, 2026
Merged

drbergman merged 3 commits into
feature-extended-asym-divfrom
claude/normalized-weighted-mode-79cb76

Conversation

@drbergman

@drbergman drbergman commented Aug 21, 2026

Copy link
Copy Markdown
Owner

Stacked on feature-extended-asym-div (the branch behind MathCancer#385). Three commits, reviewable in order.

1. Read the asymmetric division block back as pairs when resuming

feature-extended-asym-div does not currently compile. The development merge brought in resume_from_MultiCellDS, which still reads asymmetric_division_probabilities as a vector indexed by cell type, while this branch made it a map keyed on the unordered daughter pair — std::map has no resize, and no operator[] taking an int.

The mismatch was not only a compile error. The writer emits one value per pair over the upper triangle, n*(n+1)/2 values; the reader consumed n. Everything after it in the stream — cell integrity, custom data — would have been read from the wrong offset. This is a rebase-onto-new-development miss, not a design question. The fix is the one already carried on my-physicell, adopted verbatim so the trees converge.

2. Adjust asymmetric division probabilities whenever they sum past 1

asymmetric_division_function entered its renormalization block on total > 1.0 + tolerance. That block rewrites the symmetric division probability, so gating it on a user-settable threshold let the tolerance change the model, not just its strictness.

With tolerance = 0.5 and probabilities of 0.3 for (stem, stem) and 1.0 for (stem, progenitor_1), the total of 1.3 fell inside the old gate, so nothing was adjusted and the draw ran against a distribution summing to 1.3. Since select_daughter_types draws from [0,1), (stem, stem) kept its 0.3 instead of going to 0, and progenitor_1 was starved to 0.7:

gate final cells stem progenitor_1
total > 1.0 + tolerance 464 137 (29.5%) 327
total > 1.0 16 1 (6.2%) 15

The tolerance now has one job: deciding whether the resulting symmetric probability is negative enough to be an error rather than round-off.

3. Add a weighted mode for asymmetric division

The values are probabilities: they must sum to at most 1, and the shortfall is the chance of symmetric division. That is awkward for the models this feature targets. The intended use is rule-driven values, and a Hypothesis Grammar rule cannot see what the other pairs currently evaluate to, so no rule set can hold a sum at or below 1. The tolerance buys slack for round-off; it cannot rescue a model whose values legitimately sum to 2 or 3.

<asymmetric_division_mode> in <options> takes probabilities (the default, and what an absent element means) or weights. Under weights the values are relative weights normalized by their own sum at each division, so scale stops mattering and each rule can be written independently.

Details worth a reviewer's attention:

  • A string, not a bool. xml_get_bool_value reads a typo as false and silently runs the other model. A bad value here exits.
  • Model-wide, not per cell definition. Mixing would make one rule line mean a probability for one source type and a weight for another, with nothing in the line to say which.
  • No implicit symmetric remainder under weights. Symmetric division needs its own (type,type) weight. The exception is an all-zero total, which has no normalized distribution and is defined as symmetric division — no special case needed, since leaving the draw scale at 1.0 makes select_daughter_types fall through to the parent and daughter types unchanged.
  • Mutually exclusive with <asymmetric_division_probability_tolerance>, rejected at parse time. The tolerance bounds how far probabilities may sum past 1; weights are normalized rather than bounded. An explicit probabilities mode alongside a tolerance stays legal, since that is unambiguous.

The draw itself is one line — select_daughter_types takes a total_weight scaling the random draw. Probability mode passes 1.0, which is exact in double precision, so no existing model's RNG stream moves.

Verification

  • Probability mode unchanged. Asymmetric division sample (seed 0), before vs after: 487 .mat byte-identical, 244 SVG and 243 XML identical once wall-clock timestamps are stripped.
  • Weights on a model already summing to 1 reproduce that baseline exactly — normalization is a true no-op at total 1.
  • Normalization math, 4M draws per case against the built objects: 1.0/0.5/0.5 → 50/25/25, 3/1 → 75/25, 7/2/1 → 70/20/10, and 0.07/0.02/0.01 → the same 70/20/10, confirming scale-invariance. Probability mode still leaves the shortfall to symmetric division.
  • Error paths exit non-zero with the intended message: weights + tolerance, and an invalid mode value. probabilities + tolerance runs.
  • Zero total: 100% symmetric, no crash, in both modes.
  • Resume path (unit_tests/resume_sim): all 6 pairs read for its 3 cell types, cell_integrity correctly aligned after them, and a run resumed from snapshot 48 reaches the same final composition as the uninterrupted run.
  • Both build systems clean, and each of the three commits builds on its own.

Sample

extended_asym_div gains PhysiCell_settings_weights.xml, cell_rules_weights.csv, and a README. The weights rules saturate at 1.0 where the probabilities rules use 0.5, chosen so the normalized weights reproduce the probabilities exactly — the two configs are the same model written two ways. Verified single-threaded: across all 121 snapshots the two runs agree on every cell, and the only recorded difference is the asymmetric division values themselves ((type0,type0) reads 0.5 in one and 1.0 in the other).

That pairing is deliberate, because the two modes renormalize differently and the difference is not subtle: probabilities take the whole overshoot out of the symmetric entry, while weights scale every entry proportionally. Feeding the probabilities config to weights mode unchanged gives (type0,type0) 1.0/1.5 = 2/3 of divisions instead of 1/2. Same numbers, different model — which is what the mutual-exclusion error exists to prevent.

One thing noticed in passing and left alone: with <omp_num_threads> above 1 this sample is not bit-reproducible from the seed, and because type0 sits near a critical branching process the run-to-run spread is large (44 vs 242 final cells across two runs of the same config). That is pre-existing and unrelated; the README notes it.

🤖 Generated with Claude Code

drbergman and others added 2 commits August 21, 2026 06:35
resume_from_MultiCellDS arrived from development while this branch was changing
asymmetric_division_probabilities from a vector indexed by cell type into a map keyed on the
unordered daughter-type pair. The merge left the resume path reading it the old way, so the branch
did not compile: std::map has no resize, and no operator[] taking an int.

The mismatch was not only a compile error. The writer emits one value per pair, walking the upper
triangle, which is n*(n+1)/2 values; the reader consumed n. Every field after it in the stream --
cell integrity, custom data -- would have been read from the wrong offset.

Clears the map and reads the same n*(n+1)/2 values in the same order, storing them by pair. This
matches the fix already carried on my-physicell, so the two trees converge here rather than
diverging on a second spelling of the same repair.

Verified against unit_tests/resume_sim: for its 3 cell types all 6 pairs are read, the
cell_integrity fields immediately after them come back correctly aligned, and a run resumed from
snapshot 48 reaches the same final composition as the uninterrupted run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
asymmetric_division_function entered its renormalization block on total > 1.0 + tolerance, so
raising the tolerance did not only relax when an overshoot is an error -- it also decided whether
the probabilities were rewritten at all. That block sets the symmetric division probability, so
gating it on a user-settable threshold let the tolerance change the model rather than its
strictness.

With a tolerance of 0.5 and probabilities of 0.3 for (stem, stem) and 1.0 for
(stem, progenitor_1), the total of 1.3 fell inside the old gate, so nothing was adjusted and the
draw ran against a distribution summing to 1.3. Because select_daughter_types draws from [0,1),
(stem, stem) kept its 0.3 instead of being taken down to 0, and progenitor_1 was starved to 0.7.
Running the asymmetric division sample that way ends with 464 cells, 137 of them stem; adjusting
whenever the total passes 1 ends with 16 cells and a stem population that stays at 1, which is
what those probabilities describe.

Adjust on total > 1.0 and leave the tolerance its one job: deciding whether the resulting
symmetric division probability is negative enough to be an error rather than round-off.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The extended asymmetric division values are probabilities: they must sum to at most 1, and
whatever they leave short of 1 is the chance of dividing symmetrically. That constraint is
awkward for the models this feature is aimed at. The intended use is rule-driven values, and a
Hypothesis Grammar rule cannot see what the other daughter-type pairs currently evaluate to, so
keeping a sum at or below 1 is not something a rule set can enforce. The configurable tolerance
buys slack for round-off, but cannot rescue a model whose values legitimately sum to 2 or 3.

Adds <asymmetric_division_mode> to the <options> block, accepting "probabilities" (the default,
and what an absent element means) or "weights". Under weights the values are relative weights,
normalized by their own sum at each division, so their scale stops mattering and each rule can be
written on its own. A string rather than a bool so a typo errors out instead of quietly reading as
false and running the other model. The mode is model-wide rather than per cell definition: mixing
them would make one rule line mean a probability for one source type and a weight for another,
with nothing in the line to say which.

Weights carry no implicit symmetric-division remainder -- symmetric division needs its own
(type,type) weight -- except that an all-zero total, which has no normalized distribution, is
defined as symmetric division. That needs no special case: leaving the draw scale at 1.0 makes
select_daughter_types fall through to the parent and daughter types unchanged.

The draw itself is one line. select_daughter_types takes a total_weight that scales the random
draw; probability mode passes 1.0, which is exact in double precision, so no existing model's RNG
stream moves. Confirmed by running the asymmetric division sample before and after: 487 .mat files
byte-identical, and 244 SVG and 243 XML identical once wall-clock timestamps are stripped.

Setting <asymmetric_division_probability_tolerance> together with weights is rejected at parse
time. The tolerance bounds how far probabilities may sum past 1, and weights are normalized rather
than bounded, so there is nothing for it to do; erroring beats letting one of the two silently
win. An explicit "probabilities" mode alongside a tolerance stays legal, since that is unambiguous.

The extended_asym_div sample gains a second config and rules file expressing the same model in
weights, plus a README covering both modes and the trap that separates them: probabilities take
the whole overshoot out of the symmetric entry, while weights scale every entry proportionally.
The weights rules saturate at 1.0 where the probabilities rules use 0.5, chosen so the normalized
weights reproduce the probabilities exactly. Verified single-threaded: across all 121 snapshots the
two runs agree on every cell, and the only recorded difference is the asymmetric division values.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@drbergman
drbergman force-pushed the claude/normalized-weighted-mode-79cb76 branch from 7b202cc to fe2423d Compare August 21, 2026 12:49
@drbergman
drbergman marked this pull request as ready for review August 21, 2026 14:49
Copilot AI lite review requested due to automatic review settings August 21, 2026 14:49
@drbergman
drbergman merged commit 6ec1cbd into feature-extended-asym-div Aug 21, 2026
370 checks passed

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@drbergman
drbergman deleted the claude/normalized-weighted-mode-79cb76 branch August 21, 2026 14:49
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