Skip to content

Time unit conversion - #47

Merged
kwabenantim merged 19 commits into
developfrom
sbml-time-unit-conversion
Aug 4, 2026
Merged

Time unit conversion#47
kwabenantim merged 19 commits into
developfrom
sbml-time-unit-conversion

Conversation

@kwabenantim

Copy link
Copy Markdown
Member

Fixes #15

kwabenantim and others added 10 commits July 30, 2026 14:44
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

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (73e0cdd) to head (dd3eb1a).

Additional details and impacted files

Impacted file tree graph

Flag Coverage Δ
cpp 100.00% <100.00%> (ø)
python 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...s/src/reference/Chen2000/Chen2000SbmlOdeSystem.cpp 100.00% <100.00%> (ø)
...s/src/reference/Chen2000/Chen2000SbmlOdeSystem.hpp 100.00% <ø> (ø)
...s/src/reference/Chen2004/Chen2004SbmlOdeSystem.cpp 100.00% <100.00%> (ø)
...s/src/reference/Chen2004/Chen2004SbmlOdeSystem.hpp 100.00% <ø> (ø)
...reference/Gardner1998/Gardner1998SbmlOdeSystem.cpp 100.00% <100.00%> (ø)
...reference/Gardner1998/Gardner1998SbmlOdeSystem.hpp 100.00% <ø> (ø)
...rence/Goldbeter1991/Goldbeter1991SbmlOdeSystem.cpp 100.00% <100.00%> (ø)
...rence/Goldbeter1991/Goldbeter1991SbmlOdeSystem.hpp 100.00% <ø> (ø)
...els/src/reference/Tan2014/Tan2014SbmlOdeSystem.cpp 100.00% <100.00%> (ø)
...els/src/reference/Tan2014/Tan2014SbmlOdeSystem.hpp 100.00% <ø> (ø)
... and 13 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

kwabenantim and others added 6 commits July 30, 2026 16:54
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>

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 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 + --timescale override).
  • 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.

@kwabenantim
kwabenantim marked this pull request as ready for review July 31, 2026 14:58
kwabenantim and others added 2 commits August 3, 2026 22:55
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>

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

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>
@kwabenantim
kwabenantim merged commit f710232 into develop Aug 4, 2026
14 checks passed
@kwabenantim
kwabenantim deleted the sbml-time-unit-conversion branch August 4, 2026 08:45
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.

Handling timescale

2 participants