Repository navigation
Generate the code of an initial assignment once per symbol - #1435
Open
matthiaskoenig wants to merge 1 commit into
Open
matthiaskoenig wants to merge 1 commit into
matthiaskoenig wants to merge 1 commit into
Conversation
The initial value resolvers inlined the math of every initial assignment and assignment rule into the code of each reader, without the symbol cache the model data resolvers use. A value which other initial assignments read several times was generated again for every read, so the code of a chain of initial assignments grew exponentially with its depth; physiologically based models, in which the organ volumes, flows and starts are initial assignments over a few covariates, spent most of their load time compiling these copies. SBMLInitialValueSymbolResolver and ModelInitialValueSymbolResolver now cache the value of an initial assignment (and of an assignment rule) per symbol with cacheValue, as the other resolvers do. The piecewise code already pushes a cache block per branch, so a value generated in a branch is not reused outside of it. The generated accessors of the initial values (GetInitialValueCodeGenBase, SetInitialValueCodeGenBase) generate one case block per symbol and now flush the cache at the start of each case, as GetValueCodeGenBase does, so no case reads a value of another case. test_initial_assignment_cache.py loads a chain of 40 initial assignments, each reading the one before twice and in a piecewise, and checks the values before and after a changed initial value; without the cache it does not load within minutes. A physiologically based model of the pkpdsim test suite compiles in 0.53 s instead of 2.07 s (LLJIT O0) and 1.14 s instead of 4.54 s (O2), its age dependent variant in 2.35 s instead of 24.6 s (O0) and 8.5 s instead of 363 s (O2), CPU time of a fresh process. The ctest suite passes.
This was referenced Nov 6, 2026
matthiaskoenig
added a commit
to matthiaskoenig/pkpdsim
that referenced
this pull request
Nov 6, 2026
`PBPKDefinition(age=True)` makes every reference value and factor a curve of the covariate `AGE`; without it (the default) the model is the reference individual of 35 years, the factors of the age are the constant 1 and the model has no `AGE`, so a change of it fails instead of being ignored. The factors of the sex are the constants of the adult in such a build. The python side (`reference_individual(sex, age)`, the age classes, the virtual populations) is unchanged. The reason is roadrunner: without the cache of the initial value resolvers (sys-bio/roadrunner#1435) every initial assignment is inlined into each of its readers, so the curves of the age are repeated through the chain of the physiology. On the toy model of the tests this grows the model from 60,764 to 822,089 expanded nodes and its compile from 7.2 to 65.6 s at LLJIT O0 with libroadrunner 2.11.0 (4.9 s with the cache), which no continuous integration can carry. The tests which build a model over age carry the marker `over_age`, which skips them below the first roadrunner with the cache; the equations of the documentation are checked on toys over age, which build SBML only. The stored reference SBML and the pinned counts are those of the default build again.
matthiaskoenig
added a commit
to matthiaskoenig/pkpdsim
that referenced
this pull request
Nov 6, 2026
…regnancy (#133, #137, #138, #131, #134) (#142) * The physiology over age and sex, and virtual populations (#133, #137) The physiology of the PBPK library is a function of the age from the newborn to 100 years (#133): the reference children of ICRP 89 and the aging of the adults (`pbpk/age.py`, curves written once as numbers and as SBML formulas in `pbpk/curves.py`), the maturation of the GFR, the hematocrit, the albumin and AGP over age (NHANES 2013-2018), the ontogeny of the enzymes and transporters, the flow fractions over age, age classes, and virtual populations drawn from the anthropometry of each sex over age (`pbpk/virtual.py`, NHANES population files). The female build gets the differences of the sexes beyond the reference anatomy of ICRP 89 (#137, `pbpk/sex_differences.py`, `database/tables/sex_human.toml`), factors relative to a man of the same age, body weight and height, 1 in the children: the cardiac output (the same cardiac index of the sexes, 0.914), the brain fraction of the cardiac output, the albumin (the NHANES ratio), the transit of the stomach in the fed state and of the large intestine, and CYP3A4, CYP1A2, UGT2B17, UGT2B15 and CYP2A6 of the liver; the male build is unchanged. The evidence and the quantities without a difference are documented in docs/pbpk/physiology.md (Sex). The physiology over age needs the cache of the initial value resolvers of roadrunner (sys-bio/roadrunner#1435), without which a model takes minutes to compile. * The code of the populations page in the format of ruff * The factor of the sex on the cardiac output computed from the physiology at every age * The physiologies of the rat, the mouse and the pig: PBPKDefinition.species (#138) * Circadian rhythms of the physiology: the clock time and the factors over the day and the night (#131) * The pregnancy: the maternal physiology over the gestational age and the fetal exposure (#134) * The physiology over age is a choice of the build (#133) `PBPKDefinition(age=True)` makes every reference value and factor a curve of the covariate `AGE`; without it (the default) the model is the reference individual of 35 years, the factors of the age are the constant 1 and the model has no `AGE`, so a change of it fails instead of being ignored. The factors of the sex are the constants of the adult in such a build. The python side (`reference_individual(sex, age)`, the age classes, the virtual populations) is unchanged. The reason is roadrunner: without the cache of the initial value resolvers (sys-bio/roadrunner#1435) every initial assignment is inlined into each of its readers, so the curves of the age are repeated through the chain of the physiology. On the toy model of the tests this grows the model from 60,764 to 822,089 expanded nodes and its compile from 7.2 to 65.6 s at LLJIT O0 with libroadrunner 2.11.0 (4.9 s with the cache), which no continuous integration can carry. The tests which build a model over age carry the marker `over_age`, which skips them below the first roadrunner with the cache; the equations of the documentation are checked on toys over age, which build SBML only. The stored reference SBML and the pinned counts are those of the default build again. * Two guards of the tests which are at their boundary, not at their purpose `test_load_time` guards the physiology as assignment rules, which took 28.8 s to compile where the initial assignments took 2 s. Its limit of 15 s was the 10.6 s of CPU of a windows runner when it was written; the same model takes 15.1 to 15.7 s on the windows and macos runners of today and 20 s on a developer machine, so the test failed on a model which differs in no byte from the one of `develop`. The limit is 30 s, twice the slowest measurement and far below the factor 14 it guards against. The absorbed amount of an endogenous substance after an equilibration grows by 4.6e-18 mmole over 60 min and 3.7e-16 over 6000 min: the difference of two nearly equal fluxes, whose sign is that of the platform (-1.4e-17 on a macos runner). The test asserts that the readout starts at exactly 0 and stays there within that noise; that it grows at all is the assertion of the simulation without the equilibration, over 1e5 min.
This branch has not been deployed
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.
Fixes #1436.
The initial value resolvers inline the math of every initial assignment (and of an assignment rule read at initialization) into the code of each reader. Unlike the model data resolvers they do not use the symbol cache, so a value which several initial assignments read is generated again for every read, and the code of a chain of initial assignments grows exponentially with its depth (the reproducer and the timings are in #1436).
This matters for physiologically based (PBPK) models, where organ masses, volumes, blood flows, partition coefficients and species starts are initial assignments over a few covariates (body weight, height, age, organ function), so that a changed covariate updates the whole physiology at a reset. Most of their load time is spent compiling these copies; with age-dependent reference values (a piecewise per organ) the compile became unusable.
Change
SBMLInitialValueSymbolResolverandModelInitialValueSymbolResolvercache the value of an initial assignment and of an assignment rule per symbol withcacheValue, as the other resolvers do.piecewiseCodeGenalready pushes a cache block per condition and branch, so a value generated inside a branch is not reused outside of it.GetInitialValueCodeGenBaseandSetInitialValueCodeGenBasegenerate one case block per symbol and now flush the cache at the start of each case, asGetValueCodeGenBasealready does, so no case reads a value of another case.Tests
test/python/test_initial_assignment_cache.py: a chain of 40 initial assignments, each reading the one before twice and inside a piecewise, read by a species and an assignment rule; checks the values at load and afterinit(AGE)is changed and the model reset (the other branch). Without the cache the model does not load within minutes.ctest: all tests pass (with-j 12some ModelEditing, StateSaving and Auto2000 tests fail on shared files; they pass serially).Compile times (pkpdsim PBPK test model, CPU seconds of a fresh process)
Reproducer of #1436 (chain of
ninitial assignments, each reading the one before twice)