Skip to content

feat: events (VCML BioEvents) are read, modelled and written back - #67

Merged
jcschaff merged 1 commit into
mainfrom
feat/events
Oct 24, 2026
Merged

jcschaff merged 1 commit into
mainfrom
feat/events

Conversation

@jcschaff

@jcschaff jcschaff commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

Problem. pyvcell had no model for VCell events, so every read and write dropped them. simulate_ode writes the model back before solving, so any model with events ran without them.

Found through biosimulations/compose-api#364, where pbsim-common's VCellUTCStep runs SED-ML archives beside COPASI and Tellurium. On BioModel 297 (Ciliberto 2003), cell division and budding are events. VCell tracked COPASI until the first trigger (t ≈ 73 min), then diverged by up to 6× a variable's range.

Data model (pyvcell/vcml/models_app.py)

  • Application.events: list[Event], one per VCML <BioEvents><BioEvent> (VCell's BioEvent).
  • Event:
    • name;
    • trigger_type: an EventTriggerType (VCell's seven: general, single time, observable above or below a threshold, linear or log range of times, list of times);
    • use_values_from_trigger_time;
    • parameters: a list of EventParameter;
    • assignments: a list of EventAssignment (variable, expression).
    • trigger and delay are convenience properties for the general-trigger function and the delay.
  • EventParameter: name, role (an EventParameterRole, VCell's BioEventParameterType), expression and optional unit.
  • Application.add_event(name, trigger, assignments, delay=None, use_values_from_trigger_time=True) adds a general-trigger event.
  • All are exported from pyvcell.vcml.

Reading and writing

  • The reader (visit_BioEvent) follows VCell's XmlReader.getBioEvents.
    • It reads the current form: TriggerType, role-tagged <Parameter>s, <EventAssignment>s.
    • It reads both legacy forms as general triggers: VCell 5.3's <Trigger>expr</Trigger> and the first-pass 5.4 <TriggerParameters triggerClass="TriggerGeneral">, each with <Delay>.
    • It keeps VCell's quirks: a non-general legacy trigger becomes (t > -1.0), which never rises, and a missing UseValuesFromTriggerTime reads as false.
  • The writer emits <BioEvents> after the simulations, as VCell's Xmlproducer does. VCell regenerates the math events from them.

Results

  • Ciliberto:
    • Round trip: VCell's raw import and pyvcell's rewrite now have the same 4 BioEvents, 10 assignments, 4 math events, 21 ODEs, 140 parameters and 63 functions. Before: 0 events, 19 ODEs, 132 parameters, 59 functions.
    • Against COPASI: VCell now agrees on all 201 points within 3.5e-6 of each variable's range.
  • Tests, in tests/vcml/test_events.py:
    • every trigger type and parameter role round-trips;
    • legacy forms are read as VCell reads them;
    • an event imported from SBML survives the round trip and fires: the same as the event-free model up to t = 5, then the same decay restarted from its reset value.
    • Locally the event and ODE tests pass (23), and mypy is clean.
  • Corpus sweep, the same 200 models as feat: run non-spatial simulations on VCell's Sundials ODE solver (simulate_ode) #65's: each original document and pyvcell's round trip of it are solved, and the results compared.
    • 108 now give the same answer, against 103 on 0.5.1. Five models moved from "different variables" to "same", and none got worse.
    • The two models that crashed the solver on 0.5.0 now fail with 0.5.1's zero-equation message, or solve with different variables.
    • One of them, biomodel_98130468, loses all its equations in pyvcell's round trip. That's a separate gap for a follow-up.

Not modelled: RDF and annotation metadata (<Annotation>, vcml:uriBinding, ...) is still dropped on a round trip. It doesn't change the math.

🤖 Generated with Claude Code

Application.events holds each BioEvent: its trigger type (VCell's seven), whether it uses values from trigger
time, its role-tagged parameters (trigger function, delay, observable, threshold, trigger times) and its event
assignments; Application.add_event adds a general-trigger event. The reader takes VCell's current form and the
legacy <Trigger>/<Delay> forms the way VCell's XmlReader does. Before, events were dropped on every read and
write, so a re-written model (simulate_ode writes the model back) ran without them: BioModel 297 (Ciliberto 2003)
left COPASI's answer at its first event; with its four events kept it agrees within 4e-6.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@jcschaff
jcschaff merged commit 0110e92 into main Oct 24, 2026
5 checks passed
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.

1 participant