Skip to content

Make linear_compartment_model ODE print order deterministic - #566

Merged
pogudingleb merged 2 commits into
SciML:masterfrom
ChrisRackauckas-Claude:agent/559-deterministic-order
Oct 1, 2026
Merged

pogudingleb merged 2 commits into
SciML:masterfrom
ChrisRackauckas-Claude:agent/559-deterministic-order

Conversation

@ChrisRackauckas-Claude

@ChrisRackauckas-Claude ChrisRackauckas-Claude commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

Please ignore until reviewed by @ChrisRackauckas.

Summary

linear_compartment_model was building an ODE via the 3-arg constructor, which sets x_vars/y_vars from Dict key order. show(::ODE) already iterates x_vars, so the printed (and doctest-pinned) equation order reshuffled across Julia versions. This passes the already-built ordered x_vars/y_vars into the 5-arg constructor and re-pins the lincomp doctest to compartment index order (x1…x4).

Fail before / pass after

Negative control (src reverted to master, same order assertions):

 [4] macro expansion
   @ ~/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/Test/src/Test.jl:680 [inlined]
linear_compartment_model variable order is deterministic: Test Failed at /home/crackauc/sandbox/goals/issue-backlog/ibd/StructuralIdentifiability.jl/jobs/559-cursor/scratch/check_order.jl:20
  Expression: occursin(r"^x1'.*\nx2'.*\nx3'.*\nx4'.*\ny1 ="s, printed)
   Evaluated: occursin(r"^x1'.*\nx2'.*\nx3'.*\nx4'.*\ny1 ="s, "x1' = -x1*a_2_1 - x1*a_4_1 + x2*a_1_2 + x4*a_1_4\nx3' = x2*a_3_2 - x3*a_2_3 - x3*a_4_3 - x3*a_0_3 + x4*a_3_4\nx2' = x1*a_2_1 - x2*a_1_2 - x2*a_3_2 - x2*a_0_2 + x3*a_2_3 + u2\nx4' = x1*a_4_1 + x3*a_4_3 - x4*a_1_4 - x4*a_3_4\ny1 = x1\n")

Stacktrace:
 [1] top-level scope
   @ ~/sandbox/goals/issue-backlog/ibd/StructuralIdentifiability.jl/jobs/559-cursor/scratch/check_order.jl:17
 [2] macro expansion
   @ ~/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/Test/src/Test.jl:1777 [inlined]
 [3] macro expansion
   @ ~/sandbox/goals/issue-backlog/ibd/StructuralIdentifiability.jl/jobs/559-cursor/scratch/check_order.jl:20 [inlined]
 [4] macro expansion
   @ ~/.julia/juliaup/julia-1.12.7+0.x64.linux.gnu/share/julia/stdlib/v1.12/Test/src/Test.jl:680 [inlined]
Test Summary:                                            | Pass  Fail  Total  Time
linear_compartment_model variable order is deterministic |    2     2      4  5.6s
RNG of the outermost testset: Random.Xoshiro(0xe07a0862f74f428a, 0x6f30d612ccbb3381, 0x61dd84f68d83e949, 0x1ef8782cffaa70ca, 0xf02b6ca71ebd8100)
ERROR: LoadError: Some tests did not pass: 2 passed, 2 failed, 0 errored, 0 broken.
in expression starting at /home/crackauc/sandbox/goals/issue-backlog/ibd/StructuralIdentifiability.jl/jobs/559-cursor/scratch/check_order.jl:16

After the fix (3 fresh Julia processes):

x1' = -x1*a_2_1 - x1*a_4_1 + x2*a_1_2 + x4*a_1_4
x2' = x1*a_2_1 - x2*a_1_2 - x2*a_3_2 - x2*a_0_2 + x3*a_2_3 + u2
x3' = x2*a_3_2 - x3*a_2_3 - x3*a_4_3 - x3*a_0_3 + x4*a_3_4
x4' = x1*a_4_1 + x3*a_4_3 - x4*a_1_4 - x4*a_3_4
y1 = x1
--- assertions ---
Test Summary:                                            | Pass  Total  Time
linear_compartment_model variable order is deterministic |    4      4  1.7s
===== PASS RUN 2 =====
x_vars = ["x1", "x2", "x3", "x4"]
--- printed ---
x1' = -x1*a_2_1 - x1*a_4_1 + x2*a_1_2 + x4*a_1_4
x2' = x1*a_2_1 - x2*a_1_2 - x2*a_3_2 - x2*a_0_2 + x3*a_2_3 + u2
x3' = x2*a_3_2 - x3*a_2_3 - x3*a_4_3 - x3*a_0_3 + x4*a_3_4
x4' = x1*a_4_1 + x3*a_4_3 - x4*a_1_4 - x4*a_3_4
y1 = x1
--- assertions ---
Test Summary:                                            | Pass  Total  Time
linear_compartment_model variable order is deterministic |    4      4  1.8s
===== PASS RUN 3 =====
x_vars = ["x1", "x2", "x3", "x4"]
--- printed ---
x1' = -x1*a_2_1 - x1*a_4_1 + x2*a_1_2 + x4*a_1_4
x2' = x1*a_2_1 - x2*a_1_2 - x2*a_3_2 - x2*a_0_2 + x3*a_2_3 + u2
x3' = x2*a_3_2 - x3*a_2_3 - x3*a_4_3 - x3*a_0_3 + x4*a_3_4
x4' = x1*a_4_1 + x3*a_4_3 - x4*a_1_4 - x4*a_3_4
y1 = x1
--- assertions ---
Test Summary:                                            | Pass  Total  Time
linear_compartment_model variable order is deterministic |    4      4  2.1s

test/bodies/linear_compartment.jl with fix: 12/12 (master baseline was 8/8; +4 order assertions):

Test Summary:                                | Pass  Total     Time
Identifiability of linear compartment models |   12     12  2m13.2s

Doctest expected text matches sprint(show, ode) (DOCTEST_MATCH on Julia 1.12.7).

Commands run

  • julia +1.12 --project=. scratch/check_order.jl (before, negative control, 3× after)
  • julia +1.12 --project=. -e '… include(\"test/bodies/linear_compartment.jl\")' (master baseline 8/8; with fix 12/12)
  • Runic --check src/lincomp.jl test/bodies/linear_compartment.jl (exit 0)
  • typos src/lincomp.jl test/bodies/linear_compartment.jl (exit 0)

Not verified

  • Full Pkg.test() / docs Documenter doctest CI job on Julia 1.13
  • Other call sites that still use the 3-arg ODE constructor (still Dict-order for x_vars)

Reviewer pushback

  • Prefer sorting in the 3-arg ODE constructor instead of only fixing linear_compartment_model
  • Prefer collect(x_vars) vs Array{…}(x_vars) style

Fixes #559

Please ignore this draft until reviewed by @ChrisRackauckas.

Risk assessment

Risk: low
Blast radius: linear_compartment_model return value's x_vars/y_vars order and printed equation order only; identifiability results unchanged (existing 8 cases still pass).
Evidence: fail-before / pass-after order assertions; linear_compartment body 8→12; doctest text match; Runic/typos clean.
Independent review: Claude Code (Opus 5.5) rated it low, high confidence: order assertions fail on master in 6/6 fresh processes (session-dependent order, e.g. x1,x3,x2,x4) and pass on the PR in 4/4; doctest equals sprint(show) byte-for-byte; assess_identifiability identical on master and PR for a 2-compartment model and the test file's 10 result assertions pass on both; the 5-arg ODE constructor receives exactly the dict keys. Follow-up (not in this PR): other 3-arg ODE{P}(x_eqs, y_eqs, inputs) call sites inherit Dict order the same way (src/ODE.jl:53-54 root cause; src/ODE.jl:114, src/parametrizations.jl:246).
Merge: needs human review (StructuralIdentifiability is excluded from auto-merge).

Harness: Cursor Agent CLI 2026.09.28-64d2043; model: auto; transcript: /home/crackauc/sandbox/goals/issue-backlog/ibd/StructuralIdentifiability.jl/jobs/559-cursor/log.txt on amdci2.julia.csail.mit.edu

ChrisRackauckas and others added 2 commits October 1, 2026 10:17
Pass explicit x_vars/y_vars into the 5-arg ODE constructor so show order
follows compartment indices instead of Dict iteration order across Julia versions.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Cursor Agent <noreply@cursor.com>
Agent-Harness: Cursor Agent CLI 2026.09.28-64d2043
Agent-Model: auto
Agent-Session: local session, transcript at /home/crackauc/sandbox/goals/issue-backlog/ibd/StructuralIdentifiability.jl/jobs/559-cursor/log.txt on amdci2.julia.csail.mit.edu
Asserts x_vars are x1..xn and show output is in that order.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Cursor Agent <noreply@cursor.com>
Agent-Harness: Cursor Agent CLI 2026.09.28-64d2043
Agent-Model: auto
Agent-Session: local session, transcript at /home/crackauc/sandbox/goals/issue-backlog/ibd/StructuralIdentifiability.jl/jobs/559-cursor/log.txt on amdci2.julia.csail.mit.edu
@pogudingleb
pogudingleb marked this pull request as ready for review October 1, 2026 19:30
@pogudingleb

Copy link
Copy Markdown
Collaborator

Thanks!

@pogudingleb
pogudingleb merged commit 6c936b3 into SciML:master Oct 1, 2026
7 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.

Docs build fails on Julia 1.13: lincomp doctest pins Dict iteration order

3 participants