Time unit conversion - #47
Conversation
Chaste simulates in hours; SBML models declare their own time unit (defaulting to seconds in Level 2). Add a TimeUnit enum and structural detection of the model's time unit: read the Level 3 timeUnits attribute (a base-unit keyword or a UnitDefinition) and the Level 2 <unitDefinition id="time">, computing the seconds factor from <unit kind="second" multiplier/scale/exponent> rather than matching the unit name. Detection runs before removeUnusedUnits so an unreferenced time definition is not stripped first. ChasteSbmlModel resolves the derivative multiplier spec-faithfully: an explicit override wins (warning on a declared-unit mismatch), else the declared unit, else the SBML default -- Level 2 predefines time = second, while Level 3 leaves an unset time unit undefined (no conversion). The resolved multiplier is exposed to the templates as scale_time/time_multiplier. Remove the dead _get_timescale_multiplier helper it replaces. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Apply the time-unit conversion in the generated ODE system, gated on the resolved multiplier: EvaluateYDerivatives converts the incoming Chaste time (hours) to the model's native units and scales the returned derivatives, and ComputeDerivedQuantities/ProcessModelEvents evaluate at native time so the csymbol time and event triggers stay correct. When the multiplier is 1 the output is byte-identical to before. Regenerate the reference models: the four minute-based models (x60) and the seconds-defaulted Goldbeter1991/Gardner1998 (x3600) now scale; the hours and dimensionless models are unchanged. Declare an explicit <unitDefinition id="time"> of hours in VanLeeuwen2007.xml so it is detected as hours (matching its per-hour rate constants) and needs no conversion. Force TimeUnit.NONE for the SBML test suite generation, which validates output in the model's own (dimensionless) time and must never be scaled. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a --timescale ms|s|m|h option to the generate command that overrides the model's auto-detected time unit, for models that declare no time unit or declare one that should be superseded. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cover TimeUnit helpers, structural detection across SBML levels and unit forms (minutes/hours/seconds/milliseconds, scale handling, composite/non-second/ wrong-exponent -> undetermined), detection through load_sbml_model, the spec-faithful resolution rule including the override mismatch warning, and the rendered scaling (minutes scales, override forces conversion, NONE suppresses it). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Emit the time-scaling factor once as a file-scope constexpr constant in the generated ODE system, and reference it in EvaluateYDerivatives, ComputeDerivedQuantities and ProcessModelEvents, instead of repeating the literal. Still gated on scale_time, so unconverted (hours/dimensionless) models and the SBML test suite stay byte-identical. Regenerate the reference models. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The generated ODE systems now integrate in Chaste hours (minutes models scaled by 60, the seconds-defaulted Goldbeter1991/Gardner1998 by 3600), so the reference tests are updated to keep the same expected values while adjusting the time axis: - Divide simulation times (end_time, run_length, max_step, sampling_interval, dt/SetDt, SetEndTimeAndNumberOfTimeSteps) by the model's multiplier, keeping the step counts. - Compare point derivatives from EvaluateYDerivatives as derivatives[i] / m and measured stop/divide times as time * m, so the golden (native) values are unchanged. - Widen Chen2004's BackwardEuler divide-time tolerance from 0.02 to 0.1: the adaptive CVODE cell stays accurate under the x60 scaling but the fixed-step implicit solver picks up a sub-0.1-minute timing shift. Verified: all 17 SbmlRefModels reference tests pass (chaste/base, Debug, -DChaste_UPDATE_PROVENANCE=OFF). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When a model declares no time unit and no --timescale override is given, warn the user instead of silently applying the default: on SBML Level 2 the seconds default scales derivatives by 3600, and on Level 3 no conversion is applied. Both warnings point to the --timescale ms|s|m|h option. Models that declare a unit stay quiet. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Now that time scaling is implemented, the unscaled branch no longer carries a "TODO: Scale time appropriately" note; replace it with an accurate comment explaining no scaling is applied when the model is already in Chaste's hours (or no unit was determined). Regenerate the two affected reference models. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The generator supports SBML Levels 2 and 3. Warn when a model is Level 1 (which predates much of the structure the generator relies on) or a level above 3 (no Level 4 exists yet). It is a warning, not an error, so generation still proceeds but flags that the output may be incorrect. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The time-unit resolution treated the non-Level-2 branch as Level 3, so an undeclared model at an unsupported level (Level 1, or a future Level 4) was warned about with a misleading "Level 3 leaves it undefined" message. Give Level 3 its own branch and add a distinct branch for any other level that applies no conversion and names the actual (unsupported) level. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
Replace the scattered 60.0 / 3600.0 time-conversion literals in the reference tests with a per-file constexpr TIMESCALE_MULTIPLIER, mirroring the generated code. Time inputs divide by it and measured stop/divide times and point derivatives convert back through it, so the golden values are unchanged. Verified: all 17 SbmlRefModels reference tests pass (chaste/base, Debug, -DChaste_UPDATE_PROVENANCE=OFF). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reword the TIMESCALE_MULTIPLIER comments to describe the conversion from the model's native time units to Chaste's hours, and drop the now-redundant unscaled-branch comment. Regenerate the reference models. Comment-only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop constructs with no effect, found by an audit: - TimeUnit._key: assigned in __init__ but never read (with its now-unused enum tuple column). - assignment_rules / stoichiometry_variables: template-context keys exported by ModelBuilder.template_data() that no template consumes. - The STOICHIOMETRY VARIABLES loop in ode.hpp, which was always empty because _stoichiometry_variables is never populated. Regenerate the reference models: the only change is the removed (empty) STOICHIOMETRY VARIABLES comment in each *OdeSystem.hpp. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- StrictUndefined: a template referencing an unprovided variable now errors instead of rendering empty. - finalize hook: a None reaching the output errors instead of emitting the literal "None". - autoescape=False (explicit, not select_autoescape): these templates emit C++, where HTML-escaping < > & would corrupt operators. Fixes surfaced by StrictUndefined: - Provide model_name to the template context, so semantic-case test classes are named (they previously rendered as a bare "class Test"). - Add a label field to the Equation record, which templates read via eq["label"]. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Statically parse every template and assert it references only variables its render context provides -- catching a missing variable in any branch, not just the ones a given render happens to execute (StrictUndefined's blind spot). Anchor the context contract to what real models provide, so it cannot drift from _populate_template_vars and any dead (provided-but-unreferenced) context key is flagged. Also guard that C++ metacharacters render unescaped. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stochastic and syntactic cases are not going to be implemented, so remove their speculative scaffolding: the TestType enum, the test-type dispatch in ChasteSbmlTestSuiteModel, and the references to non-existent stochastic.hpp / syntactic.hpp templates. Also drop the unused test_steady_state template key. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR addresses SBML→Chaste time unit handling by introducing explicit time-unit detection/resolution and generating ODE systems that consistently scale time inputs and derivatives to Chaste’s default time base (hours), with a CLI override for ambiguous cases.
Changes:
- Add
TimeUnit(ms/s/min/h/none), detect declared SBML time units, and resolve a final scaling choice (with warnings +--timescaleoverride). - Update ODE templates and checked-in reference models/tests to apply the chosen time scaling (including updated expectations/timesteps in CxxTests).
- Harden template rendering correctness (StrictUndefined + reject rendering
None) and add a static template-context contract test.
Reviewed changes
Copilot reviewed 43 out of 43 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| chaste_sbml/tests/test_timescale.py | New unit + end-to-end tests for time-unit detection/resolution and codegen scaling output. |
| chaste_sbml/tests/test_template_contract.py | New static lint ensuring templates only reference provided context variables; checks no C++ escaping. |
| chaste_sbml/tests/test_sbml_reader.py | Updates for new load_sbml_model return values; adds unsupported-level warning tests. |
| chaste_sbml/tests/test_model_builder.py | Updates to unpack new load_sbml_model tuple return. |
| chaste_sbml/templates/ode/ode.hpp | Removes unused “stoichiometry variables” section from the generated header. |
| chaste_sbml/templates/ode/ode.cpp | Adds conditional time scaling (multiplier const, scaling in derivatives/derived quantities/events). |
| chaste_sbml/SbmlRefModels/test/reference/TestTysonNovak2001SbmlOdeSystem.hpp | Adjusts solver times/derivative comparisons for minute→hour scaling. |
| chaste_sbml/SbmlRefModels/test/reference/TestTysonNovak2001SbmlCellCycleModel.hpp | Adjusts SimulationTime dt/end-time and comparisons for minute→hour scaling. |
| chaste_sbml/SbmlRefModels/test/reference/TestTan2014SbmlSrnModel.hpp | Adjusts SimulationTime spans for minute→hour scaling. |
| chaste_sbml/SbmlRefModels/test/reference/TestTan2014SbmlOdeSystem.hpp | Adjusts dt/end-time and derivative comparisons for minute→hour scaling. |
| chaste_sbml/SbmlRefModels/test/reference/TestGoldbeter1991SbmlSrnModel.hpp | Adjusts SimulationTime spans for second→hour scaling. |
| chaste_sbml/SbmlRefModels/test/reference/TestGoldbeter1991SbmlOdeSystem.hpp | Adjusts dt/end-time and derivative comparisons for second→hour scaling. |
| chaste_sbml/SbmlRefModels/test/reference/TestGardner1998SbmlOdeSystem.hpp | Adjusts dt/end-time and derivative comparisons for second→hour scaling. |
| chaste_sbml/SbmlRefModels/test/reference/TestGardner1998SbmlCellCycleModel.hpp | Adjusts SimulationTime dt/end-time for second→hour scaling. |
| chaste_sbml/SbmlRefModels/test/reference/TestChen2004SbmlOdeSystem.hpp | Adjusts stop-time + derivative comparisons for minute→hour scaling. |
| chaste_sbml/SbmlRefModels/test/reference/TestChen2004SbmlCellCycleModel.hpp | Adjusts SimulationTime dt/end-time and comparisons for minute→hour scaling. |
| chaste_sbml/SbmlRefModels/test/reference/TestChen2000SbmlOdeSystem.hpp | Adjusts dt/end-time and derivative comparisons for minute→hour scaling. |
| chaste_sbml/SbmlRefModels/test/reference/TestChen2000SbmlCellCycleModel.hpp | Adjusts SimulationTime dt/end-time for minute→hour scaling. |
| chaste_sbml/SbmlRefModels/src/reference/VanLeeuwen2007NonDim/VanLeeuwen2007NonDimSbmlOdeSystem.hpp | Removes unused “stoichiometry variables” comment block in checked-in generated header. |
| chaste_sbml/SbmlRefModels/src/reference/VanLeeuwen2007NonDim/VanLeeuwen2007NonDimSbmlOdeSystem.cpp | Removes obsolete “TODO: Scale time appropriately” comment in checked-in generated source. |
| chaste_sbml/SbmlRefModels/src/reference/VanLeeuwen2007/VanLeeuwen2007SbmlOdeSystem.hpp | Removes unused “stoichiometry variables” comment block in checked-in generated header. |
| chaste_sbml/SbmlRefModels/src/reference/VanLeeuwen2007/VanLeeuwen2007SbmlOdeSystem.cpp | Removes obsolete “TODO: Scale time appropriately” comment in checked-in generated source. |
| chaste_sbml/SbmlRefModels/src/reference/VanLeeuwen2007/VanLeeuwen2007.xml | Adds explicit unitDefinition id="time" to declare hours (via seconds×3600) for detection. |
| chaste_sbml/SbmlRefModels/src/reference/TysonNovak2001/TysonNovak2001SbmlOdeSystem.hpp | Removes unused “stoichiometry variables” comment block in checked-in generated header. |
| chaste_sbml/SbmlRefModels/src/reference/TysonNovak2001/TysonNovak2001SbmlOdeSystem.cpp | Adds minute→hour time/derivative scaling logic in checked-in generated source. |
| chaste_sbml/SbmlRefModels/src/reference/Tan2014/Tan2014SbmlOdeSystem.hpp | Removes unused “stoichiometry variables” comment block in checked-in generated header. |
| chaste_sbml/SbmlRefModels/src/reference/Tan2014/Tan2014SbmlOdeSystem.cpp | Adds minute→hour time/derivative scaling logic in checked-in generated source. |
| chaste_sbml/SbmlRefModels/src/reference/Goldbeter1991/Goldbeter1991SbmlOdeSystem.hpp | Removes unused “stoichiometry variables” comment block in checked-in generated header. |
| chaste_sbml/SbmlRefModels/src/reference/Goldbeter1991/Goldbeter1991SbmlOdeSystem.cpp | Adds second→hour time/derivative scaling logic in checked-in generated source. |
| chaste_sbml/SbmlRefModels/src/reference/Gardner1998/Gardner1998SbmlOdeSystem.hpp | Removes unused “stoichiometry variables” comment block in checked-in generated header. |
| chaste_sbml/SbmlRefModels/src/reference/Gardner1998/Gardner1998SbmlOdeSystem.cpp | Adds second→hour time/derivative scaling logic in checked-in generated source. |
| chaste_sbml/SbmlRefModels/src/reference/Chen2004/Chen2004SbmlOdeSystem.hpp | Removes unused “stoichiometry variables” comment block in checked-in generated header. |
| chaste_sbml/SbmlRefModels/src/reference/Chen2004/Chen2004SbmlOdeSystem.cpp | Adds minute→hour time/derivative scaling logic in checked-in generated source. |
| chaste_sbml/SbmlRefModels/src/reference/Chen2000/Chen2000SbmlOdeSystem.hpp | Removes unused “stoichiometry variables” comment block in checked-in generated header. |
| chaste_sbml/SbmlRefModels/src/reference/Chen2000/Chen2000SbmlOdeSystem.cpp | Adds minute→hour time/derivative scaling logic in checked-in generated source. |
| chaste_sbml/SbmlRefModels/generate_cases.py | Forces semantic cases to be time-dimensionless (TimeUnit.NONE) and simplifies case generation path. |
| chaste_sbml/chaste_sbml_model.py | Adds time_unit override, resolves time scaling policy, and injects new template vars. |
| chaste_sbml/_sbml_reader.py | Extends loader to return (model, declared unit, SBML level); adds time-unit detection helpers and level warnings. |
| chaste_sbml/_rendering.py | Disables autoescape explicitly; enforces StrictUndefined + fails fast when rendering None. |
| chaste_sbml/_records.py | Adds Equation.label field to keep template parity (avoid missing label lookups). |
| chaste_sbml/_model_builder.py | Removes unused time-scaling stub and no-longer-used template vars from builder output. |
| chaste_sbml/_config.py | Introduces TimeUnit enum with seconds factor matching and C++ literal rendering. |
| chaste_sbml/main.py | Adds --timescale CLI option and wires it into ChasteSbmlModel. |
Suppressed comments (1)
chaste_sbml/templates/ode/ode.cpp:129
- When time scaling is enabled (scale_time), Initialise(double time) still receives Chaste time in hours but does not convert it to the model’s native units before evaluating the initial equation assignments. If any initial assignments / rules reference
time(or if Initialise is ever called with nonzero time), those values will be computed in the wrong units, making the ODE system inconsistent with EvaluateYDerivatives/ProcessModelEvents which do scale time.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Explain why Initialise skips time scaling (only ever runs at t=0), walk through the resolution steps in detect_time_unit, and reformat the TimeUnit / DerivedQuantityKind / pipeline / _resolve_time_unit docstrings into cleaner lists. Regenerate the affected reference models to pick up the new Initialise comment. No behaviour change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The _reject_none finalize hook's raise is a defensive guard that never fires during normal generation, leaving one uncovered line that tripped the 100% Codecov patch gate. Add a test that renders a provided-yet-None value through the real environment and asserts it raises, exercising the guard and confirming the hook is wired up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 43 out of 43 changed files in this pull request and generated no new comments.
Suppressed comments (1)
chaste_sbml/_config.py:111
- TimeUnit.from_cli currently raises a bare KeyError for unknown tokens. Since this method can be called outside the argparse-protected CLI, a ValueError with a clearer message would be a more appropriate public API failure mode.
from_cli is a public classmethod: the CLI restricts --timescale via argparse choices, but a direct caller passing an unknown token got an opaque KeyError. Raise a ValueError naming the valid tokens instead, and cover the failure path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes #15