Skip to content

Map SCC-lowered steady-state solutions back onto the problem's state (stacked on #173) - #177

Draft
ChrisRackauckas-Claude wants to merge 11 commits into
SciML:masterfrom
ChrisRackauckas-Claude:fix/scc-solution-on-problem-state
Draft

ChrisRackauckas-Claude wants to merge 11 commits into
SciML:masterfrom
ChrisRackauckas-Claude:fix/scc-solution-on-problem-state

Conversation

@ChrisRackauckas-Claude

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

Copy link
Copy Markdown
Member

Please ignore until reviewed by @ChrisRackauckas.

Stacked on #173 (#173's commits are included here, now up to its head aabe095 via a merge commit; review only Map SCC-lowered steady-state solutions back onto the problem's state and the merge resolution). Merge #173 first.

What changed and why

Since 2.17/2.18, a SteadyStateProblem built by ModelingToolkit carries an SCC lowering (SciML/ModelingToolkit.jl#5155), and DynamicSS/SICNM/SSRootfind solve through it (#166, #169). The lowering is mtkcompile(NonlinearSystem(sys)), a separately compiled system whose state can differ from the problem's in length and order. The returned solution was built on the lowering, so sol.u no longer matched prob.u0. For a completed (uncompiled) two-state ODE system, the lowering keeps 1 unknown (X) and makes Y observed, so sol.u == [5.0] while prob.u0 has 2 entries. Anything that indexes the solution by the system's variable indices then fails. That includes SciMLBase's test/downstream/comprehensive_indexing.jl:128/165 (BoundsError), which keeps tests / SymbolicIndexingInterface red on SciMLBase master and downstream in SymbolicIndexingInterface.jl and ModelingToolkit.jl. It also breaks the common ODEProblem(sys, ...; u0 = sssol.u) pattern.

The fix is in SSDE because SSDE builds the returned solution. The lowering is an internal solve strategy, so solve(prob) should return a solution of prob. __scc_solution_on_problem maps the SCC solution back when two conditions hold: prob.f has a sys, and the lowering's index provider resolves every variable_symbols(prob) (as a variable or observed). It then rebuilds the solution on prob in prob's ordering. It recomputes resid as prob.f(u, p, Inf) and keeps the SCC solution as sol.original. save_idxs now indexes prob's ordering. A problem without symbolic information (a hand-built lowered_problem) has no mapping, so its solution stays on the lowering, as before. This applies to DynamicSS, SICNM and SSRootfind, and to init on an SCC lowering from #173.

MTK and SciMLBase need no change. Symbolic indexing (sol[x]) already worked on the lowering-based solution and still works. Only the state vector and its index provider change.

Behaviour change (reviewer: push back here if you disagree)

For an MTK-built SteadyStateProblem with an SCC lowering, sol.prob is now the (concretized) SteadyStateProblem, not the SCCNonlinearProblem, and sol.original is the SCC solution. Five existing assertions in test/scc.jl encoded the old choice (sol.prob isa SCCNonlinearProblem). They now assert sol.prob isa SteadyStateProblem, length(sol.u) == length(prob.u0) and sol.original.prob isa SCCNonlinearProblem. Every symbolic-value assertion is unchanged. MTK's own test (test/scc_nonlinear_problem.jl:665) already asserts sol.prob isa SteadyStateProblem for DynamicSS.

Verification

New testset SCC-lowered solution is expressed in the problem's state (DynamicSS, SICNM, SSRootfind): on a completed 2-state system whose lowering has fewer unknowns, it checks length(sol.u) == 2, sol.u[variable_index(osys, [X, Y])], getsym(osys, [X, Y])(sol), observed sol[XY], resid ≈ 0, and save_idxs.

test/scc.jl run in an env with SciMLBase#1624 dev'd, MTK 11.45.3 and NonlinearSolve 4.32.0:

Standalone MTK repro (the ssprob from SciMLBase's comprehensive_indexing.jl):

before: typeof(sssol.prob).name = SCCNonlinearProblem; sssol.u = [5.0]
after:  typeof(sssol.prob).name = SteadyStateProblem;  sssol.u = [5.0, 1.5]; sssol[XY] = 6.5

Runic --check and typos are clean on the changed files.

SciMLBase GROUP=SymbolicIndexingInterface with all branches dev'd together. SciMLBase#1624 head 717d1123, SSDE at this branch 97da160 (includes #173), MTK 11.45.3, Julia 1.12, run as GROUP=SymbolicIndexingInterface julia --project -e 'using Pkg; Pkg.test()':

Remake                                                      | 4605  4605
Symbol and integer based indexing of interpolated solutions | 3777  3777
Symbol and integer based indexing of integrators            |   99    99
Problem Indexing                                            |  122   122
Adjoints                                                    | Broken 5 (pre-existing @test_broken)
ModelingToolkit Remake                                      |  248 Pass, 3 Broken (pre-existing)
     Testing SciMLBase tests passed

Update 2026-10-08: merged #173 head aabe095

#173 now returns SteadyStateODECache/SteadyStateSCCCache from init, and solve! on them gives the same solution as solve. Merge commit 2542436 brings that into this branch with no rebase and no force push. Only imports conflicted, in src/SteadyStateDiffEq.jl and test/scc.jl, plus the tail of the MTK init testset; both sides were kept. src/solve.jl merged cleanly. The SCC cache's solve! goes through __solve_scc_lowering, so it maps onto prob in the same way. The new state-mapping testset now also checks solve!(init(prob, alg)) for DynamicSS/SICNM (sol.prob isa SteadyStateProblem, sol.u[uidxs] ≈ [Xss, Yss]).

Verification at 2542436 (Julia 1.12.4; SciMLBase#1624 head b18f1fc3 dev'd; MTK 11.45.3; NonlinearSolve 4.32.0):

  • GROUP=Core (test/runtests.jl) exit 0:
    • core.jl 2322/2322
    • developer_interfaces.jl 5/5
    • precompile_workload.jl 8/8
    • scc.jl 543/543
    • sicnm.jl 33/33
    • autodiff.jl has 0 assertions, the same as on master.
  • test/qa/qa.jl: Quality Assurance 21/21, Reexport surface 16/16.
  • Runic --check and typos: clean.
  • SciMLBase GROUP=SymbolicIndexingInterface with SciMLBase#1624 and this branch dev'd. Pkg.test(), exit 0:
Remake                                                      | 4605  4605
Symbol and integer based indexing of interpolated solutions | 3777  3777
Symbol and integer based indexing of integrators            |   99    99
Problem Indexing                                            |  122   122
Adjoints                                                    |    5     5 (Broken)
ModelingToolkit Remake                                      |  248 Pass, 3 Broken
     Testing SciMLBase tests passed

The Broken counts are pre-existing @test_broken. This PR adds none.

Update 2026-10-08: Grok 4.7 review round 1 (CHANGES): three blocking findings fixed in 9a99d5a

  1. Eltype. The mapped state is now convert(typeof(prob.u0), collect(u)). Before, an observed integer literal returned Vector{Int64} for a Float64 problem.

  2. Stats. The mapped solution now carries the lowering solution's stats, left and right, as __build_ssrootfind_solution does. Before, SSRootfind returned stats = nothing after mapping.

  3. save_idxs original. A save_idxs slice now sets original = sol, the unsliced solution:

    Before, original was one level too deep (the per-block tuple, or the SCC-order state only).

New testset SCC-lowered solution metadata in test/scc.jl:

  • eltype, for SSRootfind, DynamicSS and SICNM on a completed one-state system whose lowering observes x = 1.
  • stats, left and right on the reordered Lotka–Volterra SSRootfind solve.
  • save_idxs → .original, on a manual two-block SCC and on a symbolic linear system.

Both runs below use the standalone testset with this PR's environment.

  • Fail-before (2542436 src): SCC-lowered solution metadata | Pass 16 Fail 5 Error 3 Total 24. Representative failures:
    • Evaluated: [1] isa Vector{Float64} and [0] isa Vector{Float64}
    • nothing !== nothing and nothing === SciMLBase.NLStats(4, 4, 4, 4, 4)
    • sliced.original.prob isa SteadyStateProblem evaluated to SCCNonlinearProblem…; the .original.u checks errored.
  • Pass-after (9a99d5a): Pass 24 Total 24.

Verification at 9a99d5a (Julia 1.12.4, SciMLBase#1624 b18f1fc3 dev'd, ModelingToolkit 11.45.3):

  • GROUP=Core exits 0:

    • core.jl 2322/2322
    • developer_interfaces.jl 5/5
    • precompile_workload.jl 8/8
    • scc.jl 567/567
    • sicnm.jl 33/33
    • autodiff.jl has 0 assertions.
  • QA: Quality Assurance 21/21, Reexport surface 16/16.

  • Runic and typos: clean.

  • SciMLBase GROUP=SymbolicIndexingInterface is rerun because the mapping path changed, and exits 0:

    Testset Result
    Remake 4605/4605
    interpolated-solution indexing 3777/3777
    integrator indexing 99/99
    Problem Indexing 122/122
    Adjoints 5 broken (pre-existing)
    ModelingToolkit Remake 248 pass, 3 broken (pre-existing)

The review's non-blocking notes are not addressed here, because each is outside this PR's scope:

Update 2026-10-09: Downgrade fix in dc54954

CI test / Downgrade Tests failed on 9a99d5a (https://github.com/SciML/SteadyStateDiffEq.jl/actions/runs/37858191872/job/113587359583): scc.jl reported 555 pass and 3 errors.

Cause. The eltype testset's system has every unknown eliminated, so its SCC lowering is empty. ModelingToolkit 11.44.0, the old test floor, throws MethodError: no method matching ModelingToolkit.SCCDecomposition(::System, ::Vector{Vector{Int64}}, ::Vector{Any}; combine_sccs::Bool) on it. 11.45.0 added the isempty(var_sccs) branch in src/problems/sccnonlinearproblem.jl.

Fix (no test skipped or version-guarded):

  • The ModelingToolkit compat floor goes from 11.44 to 11.45. ModelingToolkit is a test-only [extras] dependency, so users are not affected.
  • At the floor NonlinearSolve stack (NonlinearSolve 4.21.0, NonlinearSolveBase 2.49.4, ForwardDiff 1.3.3), Newton cannot form a ForwardDiff Jacobian for the zero-length problem: chunk size cannot be greater than ForwardDiff.structural_length(x) (1 > 0). The eltype check therefore uses the Jacobian-free SSRootfind(Broyden()). The mapping bug that this test targets is independent of the algorithm.

Verification

  • Downgrade resolution. I ran julia-actions/julia-downgrade-compat at the CI SHA (downgrade.jl … "." "deps" "1" "Mooncake", Resolver --min=@deps, Julia 1.10.12). It reports Successfully resolved minimal versions … (with extras), with ModelingToolkit 11.45.0, ModelingToolkitBase 1.77.0, SciMLBase 3.55.0, NonlinearSolve 4.21.0, NonlinearSolveBase 2.49.4, OrdinaryDiffEq 7.2.1, Symbolics 7.39.2, SymbolicIndexingInterface 0.3.47, SCCNonlinearSolve 1.13.0 and DiffEqBase 7.18.1.
  • GROUP=Core in that environment (Julia 1.10, Pkg.test(allow_reresolve=false)) exits 0:
    • core.jl 2903/2903
    • developer_interfaces.jl 5/5
    • precompile_workload.jl 8/8
    • scc.jl 567/567
    • sicnm.jl 33/33
  • Fail-before / pass-after. I ran the testset standalone on Julia 1.12 with the Broyden eltype check:
    • Against 2542436 source: Pass 16 Fail 5 Error 3 Total 24, including [1] isa Vector{Float64} for SSRootfind.
    • Against this head: Pass 24 Total 24.
  • Runic and typos are clean.

#173 is unaffected: its branch does not contain this testset or the compat change, and its Downgrade check was green.

Not verified locally

  • Julia lts/pre/x86 and downgrade; CI covers them.
  • MTK's test/scc_nonlinear_problem.jl with this branch. Its assertions are symbolic (sol[x]) or sol.prob isa SteadyStateProblem, so it should be unaffected, but I did not run it.

Risk assessment

  • Risk: medium
  • Blast radius: solve/init of SteadyStateProblems that carry an SCC lowering (MTK-built only) with DynamicSS/SICNM/SSRootfind: sol.u, sol.resid and sol.prob move from the lowering's coordinates back to the problem's. Problems without a lowering, or without symbolic information, are unchanged. This reverts the coordinate choice that 2.17/2.18 introduced, so anyone who relied on sol.prob isa SCCNonlinearProblem (only this package's own tests, as far as I found) sees a change.
  • Evidence: fail-before/pass-after above. It also fixes SciMLBase tests / SymbolicIndexingInterface together with SciMLBase#1624 and SSDE#173.
  • Independent review: pending
  • Merge: needs human review: a public behaviour change of a recently released feature, and stacked on Add __init for DynamicSS/SICNM so init() doesn't fall through to NonlinearSolve's default #173.

🤖 Generated with Claude Code (model: claude-opus-5-5[1m]) https://claude.ai/code/session_011aK5Xvp9NpdkzKuEwtiMGd

🤖 Automated comment from an AI agent running as @ChrisRackauckas — not written or reviewed by Chris.

Risk assessment (independent review, Codex gpt-6.1-sol, head dc54954)

  • Verdict: CHANGES

  • Rating: Codex gpt-6.1-sol rated it high (H1) at head dc54954

  • Risk: high (H1: changes the documented problem and state ordering returned by published algorithms, including sol.prob, sol.u, and sol.original; the risk policy explicitly identifies this PR as an H1 example).

  • Blast radius: Symbolic steady-state problems with stored lowerings solved using SSRootfind, DynamicSS, or SICNM, including positional indexing, residuals, save_idxs, and consumers of solution metadata. The included stack also changes init/solve! and introduces persistent ODE/SCC caches. The scalar regression affects a symbolic original problem with a scalar initial state and a vector-valued nonlinear lowering.

  • Evidence: Read the full git diff origin/HEAD...HEAD, the issue and stack description, public dependency declarations, and licenses. Default source snapshot: 471a4a039f637459ff8625cc832f20a7b85f4f49; head: dc54954c57c221daaa3efec71a5f0fd5d8fc8fda. All execution used scratch source copies and one Julia process at a time, Julia 1.12.4, JULIA_NUM_THREADS=4, the requested depot path, and hour-long timeouts. Both environments use ModelingToolkit 11.45.3, NonlinearSolve 4.32.0, and SciMLBase 3.57.0.

    With S=<local path>, ran timeout 3600 julia --project="$S/env" --compiled-modules=existing "$S/run-targeted.jl": 54/54 pass, exit 0. This extracts only the two newly added mapping/metadata testsets from head. Ran the identical tests against base using timeout 3600 julia -O0 --compiled-modules=existing --project="$S/base-env" "$S/run-targeted.jl": 27 pass, 8 fail, 13 errors, exit 1, including the problem-type assertion and positional-indexing errors. These are intentional negative-control failures in the new tests, not evidence of unrelated failures in the existing default-branch suite. Logs: head, base.

    Ran timeout 3600 julia -O0 --compiled-modules=existing --project="$S/base-env" "$S/scalar.jl" and the same command with --project="$S/env": base 2/2 pass, exit 0; head 1 error, exit 1, with Cannot convert Vector{Float64} to Float64 at line 137. Reproducer: scalar.jl; logs: base, head.

    gh pr checks 177 --repo SciML/SteadyStateDiffEq.jl reports no failing current-head checks; the Core job log confirms 567/567 SCC assertions. There is no current failing CI check to classify as pre-existing. git diff --check origin/HEAD...HEAD is clean. No new runtime dependency, copyleft dependency, skipped test, or loosened numerical tolerance was found; SymbolicIndexingInterface is already a runtime dependency and is MIT licensed. Newly accessed dependency names are public at their owners; no new package-owned public name is introduced.

    G1: pass — the mapping test checks the state against an independently assembled linear-system solution and checks the original residual against zero. G2: not applicable — no GPU kernels, custom AD rules, Reactant tracing, or MPI implementation changes; the existing ForwardDiff calculations inside SICNM execute in the targeted tests. G3: pass — the included cache tests exercise stepping before completion, termination/failure codes, and solve!(init(...)) versus direct solve; current SCC CI runs them, and the local mapping test also exercises the SCC cache path.

  • Merge: needs changes (fix scalar-state restoration, establish H1 major-release handling, and update the three public algorithm docstrings). High risk requires Chris's release decision even after technical findings are resolved.


🤖 Posted by an AI agent — harness: Codex CLI 0.162.1 · model: gpt-6.1-sol
Conversation: local headless review run (no public session URL); transcript kept by the fleet master.

🤖 Automated review section from an AI agent running as Chris Rackauckas's account — not written or reviewed by Chris.

Fresh independent review of dc54954

CHANGES/highH1. Original scalar/matrix initial states throw during mapped solution restoration: Vector-to-Float64 and Vector-to-Matrix conversion errors at src/solve.jl:137. Identical public-API analytic-root probes succeed with4/4 state-type assertions on dependency-matched clean current master. The intended mapping tests pass54/54 on head but discriminate against base. Reconstruct original shape/container and add regressions. Rendered SSRootfind, DynamicSS and SICNM docstrings still promise lowering order, contradicting the implemented original-state mapping; document actual state/residual/save_idxs/original/fallback behavior. Public output contract and version2.18.0 require Chris major-release decision. Independent Core2412, developer5, precompile8, SCC567, SICNM33 and unchanged QA/format40 pass; docs builds. Existing autodiff file contains0assertions and solver warnings, so no derivative-accuracy claim. Prerequisite initialization PR173 remains open; no merge before prerequisites, code repair and Chris decision. Review intake retained prior labels; fresh executed evidence supports findings.

Risk assessment

  • Risk: high (H1).
  • Blast radius: the affected solver/test/benchmark entrypoints described above; no additional coverage is inferred.
  • Evidence: separate reviewer executions and negative controls described above; limitations retained.
  • Independent review: Codex CLI 0.163.0 (gpt-6.1-sol), local session 01a15671-e6ff-7ed0-836e-9f12b3b79f45 (no session URL), rated it high (H1), high confidence in the observed findings; VERDICT: CHANGES at head dc54954.
  • Merge: needs human review for medium/high; otherwise hold until findings and live gates are resolved.

Links:


🤖 Posted by an AI agent — harness: Codex CLI 0.163.0 · model: gpt-6.1-sol
Conversation: local session 01a154f0-8986-78e1-8af2-1119e8cdea4a (no session URL)

🤖 Automated review section from an AI agent running as Chris Rackauckas's account — not written or reviewed by Chris.

Assigned correction of dc54954

This correction is queued to a fresh Codex CLI worker, model gpt-6.1-sol, under the SciML submaster’s pipeline ownership. Scope: Fresh CHANGES/H1: scalar/matrix original states error in restoration atsolve.jl137 whilecurrentbase4/4passes; fixshape/container preservingmapping54cases and originalresidual/save_idxs semantics. Correct rendered three algorithm docstrings. API/release decision remains Chris; noversion/tag/release. Prerequisite173 staysseparate.

The revision must preserve existing assertions and demonstrate the concrete failing-before/passing-after behavior using independent references. Default-branch failures, API or compatibility-policy decisions, and unavailable hardware remain separate findings. No test suppression, unvalidated push or unrelated repair.

The worker does not merge, approve or close PRs. Any pushed revision needs a fresh independent counted review; existing source reviews are not silently repinned. This assignment does not change the PR’s risk level or resolve any finding.

Links:


🤖 Posted by an AI agent — harness: Codex CLI 0.163.0 · model: gpt-6.1-sol
Conversation: local session 01a154f0-8986-78e1-8af2-1119e8cdea4a (no session URL)

🤖 Automated review section from an AI agent running as Chris Rackauckas's account — not written or reviewed by Chris.

Fix by Codex gpt-6.1-sol (head 8f443af)

Mapped scalar states retain their scalar type. Array states are reshaped to the original geometry before conversion to the original container type. The three rendered algorithm docstrings describe original-state mapping, original residual evaluation, original, symbolic-mapping fallback, and output selection, including the existing SSRootfind limitation.

Added 395 assertions covering all three algorithms, scalar/matrix/tensor states, in-place and out-of-place residuals, reversed symbolic ordering, failed solves, save_idxs, and mutation isolation across repeated cached solves. All 567 existing SCC assertions also pass. No dependency, compat, version, tag, or release change.

Executed validation

S denotes private scratch storage. The job-local run-julia.sh invokes timeout 3600 julia +1.12.4, sets four Julia threads, one BLAS thread, one precompile task, a private temporary directory and writable depot overlay, and uses existing depots as fallbacks. Only one top-level Julia command ran at a time; the unchanged official Pkg/Aqua harness spawned its required children while parents waited. The before/after environments use the same dependencies (SciMLBase 3.57.0, ModelingToolkit 11.45.3, NonlinearSolve 4.32.0).

Discriminating regression. The probe extracts the final regression testset from test/scc.jl; its assertion body matches the official testset modulo Runic formatting. Unfixed source is an untouched archive of dc54954.

bash "$S/run-julia.sh" --compile=min -O0 --compiled-modules=existing --project="$S/unfixed-env" "$S/container-tests-final.jl"
bash "$S/run-julia.sh" -O0 --compiled-modules=existing --project="$S/env" "$S/container-tests-final.jl"

Before, exit 1:

Test Summary:                                   | Error  Total   Time
SCC-lowered solution preserves state containers |    30     30  33.6s

The failures are scalar, matrix and tensor restoration errors. After, exit 0:

Test Summary:                                   | Pass  Total   Time
SCC-lowered solution preserves state containers |  395    395  33.1s

The review's public-API scalar/matrix analytic-root control was also executed against clean current default 471a4a0 with matched dependencies:

bash "$S/run-julia.sh" --compiled-modules=existing -O0 --project="$S/base-env" "$S/review-state-types.jl"
Test Summary:                            | Pass  Total  Time
non-vector states with symbolic lowering |    4      4  5.0s

Official groups, both exit 0:

GROUP=Core bash "$S/run-julia.sh" -O0 --compiled-modules=existing --project=. -e 'using InteractiveUtils, LinearAlgebra; versioninfo(); @show Threads.nthreads() BLAS.get_num_threads(); using Pkg; Pkg.test(; allow_reresolve=false, julia_args=["--compiled-modules=existing", "-O0"])'
GROUP=QA bash "$S/run-julia.sh" -O0 --compiled-modules=existing --project=. -e 'using Pkg; Pkg.test(; allow_reresolve=false, julia_args=["--compiled-modules=existing", "-O0"])'
Test Summary:    | Total     Time
Core/autodiff.jl |     0  7m29.1s
Test Summary: | Pass  Total     Time
Core/core.jl  | 2412   2412  1m14.8s
Test Summary:                | Pass  Total  Time
Core/developer_interfaces.jl |    5      5  0.2s
Test Summary:               | Pass  Total  Time
Core/precompile_workload.jl |    8      8  4.1s
Test Summary: | Pass  Total     Time
Core/scc.jl   |  962    962  4m15.0s
Test Summary: | Pass  Total   Time
Core/sicnm.jl |   33     33  39.2s
Testing SteadyStateDiffEq tests passed
Test Summary: | Pass  Total      Time
QA/qa.jl      |   37     37  15m32.1s
Testing SteadyStateDiffEq tests passed
Threads.nthreads() = 4
BLAS.get_num_threads() = 1

The autodiff file contains zero assertions; this is not derivative-accuracy evidence.

Documentation, formatting and spelling, all exit 0:

bash "$S/run-julia.sh" -O0 --compiled-modules=existing --project=docs docs/make.jl
bash "$S/run-julia.sh" -O0 --compiled-modules=existing --project="$S/tools-env" -m Runic --check src/solve.jl src/algorithms.jl test/scc.jl
typos --config .typos.toml "$S/added-lines.txt"
git diff --check
[ Info: CrossReferences: building cross-references.
[ Info: CheckDocument: running document checks.
[ Info: Populate: populating indices.
[ Info: RenderDocument: rendering document.
[ Info: HTMLWriter: rendering HTML pages.

Deployment was skipped outside CI. A separate HTML inspection confirms the state/container, original-residual and fallback contracts in all three rendered algorithm docstrings, the unsliced-original contract in both integration algorithms, and the SSRootfind output-selection limitation. Runic, typos over added diff lines, and whitespace checking produced no output.

G1: analytic roots and independently written original residuals. G2: the changed restoration paths execute for all three algorithms, both original-residual calling conventions, and both plain and SCC lowerings. G3: mutate a returned unsliced state, solve the same cache again, and assert the new result and original initial state remain correct. No skipped/broken tests, swallowed errors, relaxed tolerances, or warning-only build configuration were added.

Current-head CI had no failing test check at intake; current default's Core/QA/downgrade/docs checks were green. The historical downgrade failure log was read; its corrected current-head run is green. No unrelated baseline repair was included. Not verified locally: LTS/32-bit/pre-release/downgrade matrices or downstream packages. Remote CI after this push is not claimed complete.

Remaining gates

Risk remains high (H1): the public output-contract/release decision remains pending, and the initialization prerequisite remains separate. This correction does not make a release or merge decision.

Next counted review must come from a smart model in a fresh session, not this fix session (risk.md § Independent reviews).

Links:


🤖 Posted by an AI agent — harness: Codex CLI 0.163.0 · model: gpt-6.1-sol
Conversation: local session 01a15cba-455f-7840-a71e-61b5d3b9fca9 (no session URL)

🤖 Automated review section from an AI agent running as Chris Rackauckas's account — not written or reviewed by Chris.

Independent review of head 8f443af

Independent executed probes find a remaining view-backed initial-state restoration regression; existing public-contract risk remains high.

At8f443af3, view(zeros(2,2),1:1,:) with analytic equilibrium[2,3] solves on cleanmaster for allthreealgorithms(base6/6), but headthrows3Matrix-to-SubArray conversionerrors atsolve.jl140. Reconstructoriginalgeometry withoutaliasingprob.u0, supportviews acrosssolve/init/solve!, preserveoriginalmapping/residual/save_idxs/failure contracts andrendereddocs. No lowered-order fallback.

Findings-only, NOT a counted merge rating: reviewer disclosed priorlabels leaked through supplied intake. Rawfinding/probes arefreshsession distinctfromauthor, but completeblinding notclaimed. Headincludes173onlyaabe095; latest553combinedstacknotverified. H1publicoutputcontract/releasedecision remainsChris; localLTS/pre/x86/downgrade/downstreamcorpusunverified.

Risk assessment

  • Risk: high (H1).
  • Blast radius: Independent executed probes find a remaining view-backed initial-state restoration regression; existing public-contract risk remains high.
  • Evidence: OfficialCorepasses2322+5+8+962+33 assertions, QA37/37, docsbuild and3renderedalgorithmcontracts checked. Same extractedregressions: shapeparent30errors, prerequisite158pass170fail115errors, head449/449; independenttargeted72+24+21 pass. Newviewprobe cleanmaster6/6→head3errors withsameversions/assertions. Runic/typos/diffpass. CurrentCI13success; noGPU/ADaccuracyclaim fromzero-assertionautodifffile.
  • Independent review: Codex CLI0.163.0 (gpt-6.1-sol), local session 01a15d2a-dee0-74e0-a7ea-e2060698a206 (no session URL), rated it high (H1); VERDICT:CHANGES at head 8f443af. Fresh session distinct from author; prior-rating exposure disclosed. Executed findings only, NOT a counted merge rating.
  • Merge: Needs changes or verification; medium/high and permission/license decisions remain with Chris through the master.

Links:


🤖 Posted by an AI agent — harness: Codex CLI0.163.0 · model:gpt-6.1-sol
Conversation: local session 01a154f0-8986-78e1-8af2-1119e8cdea4a (no session URL)

🤖 Automated review section from an AI agent running as Chris Rackauckas's account — not written or reviewed by Chris.

Fix by Codex gpt-6.1-sol (head f2fa91e)

View-backed initial states now map into independent array storage in the original geometry. Array reconstruction uses map on the original state and converts each mapped value to its element type, avoiding conversion of a dense result into a concrete SubArray. Scalar restoration, symbolic ordering, original residual evaluation, metadata, and slicing/cache contracts retain their regression coverage. All three rendered algorithm docstrings describe the actual allocation contract. Repository instructions record the repeated container-reconstruction constraint.

The expanded official testset retains all 395 previous assertions and adds 528 view assertions, covering all three algorithms, four matrix/tensor shapes, both original-residual calling conventions, save_idxs, applicable init/solve! paths, and isolation from the view/backing storage and earlier cached solutions.

Executed validation

S denotes private scratch storage. view-run-julia.sh invokes timeout 3600 julia +1.12.4, explicitly sets four Julia threads, BLAS/OMP/MKL threads to one, one precompile task, a job-local temporary directory and writable depot overlay, and existing depots as fallbacks. One top-level Julia command ran at a time; the unchanged official Pkg/Aqua harness used its required child processes while parents waited. The matched before/after control and Core environment use SciMLBase 3.58.0, ModelingToolkit 11.45.5, NonlinearSolve 4.32.0, and SymbolicIndexingInterface 0.3.55.

Discriminating official regression: the probe extracts the final official testset; its assertion body matches test/scc.jl modulo Runic formatting. The unfixed source is an untouched archive of the parent commit linked below.

bash "$S/view-run-julia.sh" -O0 --compiled-modules=existing --project="$S/view-unfixed-env" "$S/view-container-tests.jl"
bash "$S/view-run-julia.sh" -O0 --compiled-modules=existing --project="$S/view-env" "$S/view-container-tests.jl"

Before, exit 1:

Test Summary:                                         | Pass  Error  Total   Time
SCC-lowered solution preserves state containers       |  395     24    419  42.6s

All 24 view variants error during dense-array-to-SubArray restoration; the 395 prior assertions pass. After, exit 0:

Test Summary:                                   | Pass  Total   Time
SCC-lowered solution preserves state containers |  923    923  48.3s

The exact executed finding's view(zeros(2,2), 1:1, :) analytic-root probe also passes 6/6 on clean current default and 6/6 on this correction, with matched dependencies and unchanged assertions/tolerances:

bash "$S/view-run-julia.sh" -O0 --compiled-modules=existing --project="$S/view-base-env" "$S/review-view-probe.jl"
bash "$S/view-run-julia.sh" -O0 --compiled-modules=existing --project="$S/view-env" "$S/review-view-probe.jl"
Test Summary:                          | Pass  Total   Time
view initial-state solve compatibility |    6      6  31.9s
Test Summary:                          | Pass  Total   Time
view initial-state solve compatibility |    6      6  24.6s

Official Core and QA, both exit 0:

GROUP=Core bash "$S/view-run-julia.sh" -O0 --compiled-modules=existing --project=. -e 'using InteractiveUtils, LinearAlgebra; versioninfo(); @show Threads.nthreads() BLAS.get_num_threads(); using Pkg; Pkg.test(; allow_reresolve=false, julia_args=["--compiled-modules=existing", "-O0"])'
GROUP=QA bash "$S/view-run-julia.sh" -O0 --compiled-modules=existing --project=. -e 'using Pkg; Pkg.test(; allow_reresolve=false, julia_args=["--compiled-modules=existing", "-O0"])'
Test Summary:    | Total     Time
Core/autodiff.jl |     0  6m53.2s
Test Summary: | Pass  Total   Time
Core/core.jl  | 2412   2412  58.2s
Test Summary:                | Pass  Total  Time
Core/developer_interfaces.jl |    5      5  0.2s
Test Summary:               | Pass  Total  Time
Core/precompile_workload.jl |    8      8  2.7s
Test Summary: | Pass  Total     Time
Core/scc.jl   | 1490   1490  3m49.6s
Test Summary: | Pass  Total   Time
Core/sicnm.jl |   33     33  33.8s
Testing SteadyStateDiffEq tests passed
Test Summary: | Pass  Total     Time
QA/qa.jl      |   37     37  5m07.6s
Testing SteadyStateDiffEq tests passed
Threads.nthreads() = 4
BLAS.get_num_threads() = 1

Core totals 3948 passing assertions, including 1490 SCC. The existing autodiff file has zero assertions, so this is not derivative-accuracy evidence.

Docs, formatting and spelling, all exit 0:

bash "$S/view-run-julia.sh" -O0 --compiled-modules=existing --project=docs docs/make.jl
bash "$S/view-run-julia.sh" -O0 --compiled-modules=existing --project="$S/tools-env" -m Runic --check src/solve.jl src/algorithms.jl test/scc.jl
typos --config .typos.toml AGENTS.md "$S/view-added-lines.txt"
git diff --check
[ Info: CheckDocument: running document checks.
[ Info: Populate: populating indices.
[ Info: RenderDocument: rendering document.
[ Info: HTMLWriter: rendering HTML pages.

Deployment was skipped outside CI. HTML inspection confirms all 3/3 algorithm docstrings describe independent view storage, shape/order, original residuals and symbolic fallback; both integration algorithms describe unsliced original, and SSRootfind's selection limitation is retained. Runic, typos over the added diff and instruction file, and whitespace checks produced no output.

G1: analytic equilibria and independent original-residual checks. G2: all three solve paths, in-place/out-of-place original residuals, matrix/tensor views, and applicable initialized caches execute. G3: storage-alias checks plus mutation/repeated-solve assertions establish isolation from initial/backing storage and earlier results. No skipped/broken tests, loosened tolerances, swallowed exceptions, or warning-only configuration; no new source comments, runtime dependency, compat or version change.

Remaining gates

The supplied executed review is findings-only, not a counted merge rating because of its disclosed intake-label exposure. This fix makes no independent-review or merge decision. H1 public-contract/release policy remains unresolved. The tree includes the initialization prerequisite only through the commit linked below; newer prerequisite revisions and the combined stack are neither integrated nor approved/validated here. No version/tag/release/merge/settings action.

Current-head intake checks and latest-default test CI were green; no unrelated baseline repair was included. Not verified locally: LTS/pre-release/32-bit/downgrade matrices, downstream corpora, derivative accuracy, or the newer combined prerequisite tree. Remote CI completion after this push is not claimed.

Next counted review must come from a smart model in a fresh session, not this fix session (risk.md § Independent reviews).

Links:


🤖 Posted by an AI agent — harness: Codex CLI 0.163.0 · model: gpt-6.1-sol
Conversation: local session 01a15cba-455f-7840-a71e-61b5d3b9fca9 (no session URL)

🤖 Automated review section from an AI agent running as Chris Rackauckas's account — not written or reviewed by Chris.

Executed findings at head f2fa91e — session 01a15db3-9dcf-7713-b93d-599577e39b55

Reconstructing state in symbol-enumeration order can return the wrong successful solution. The public indexing interface permits symbol enumeration to differ from variable indices. With an otherwise valid provider enumerating [:y, :x] while indices remain x=1, y=2, the independent root is [2, 3]; all three algorithms instead return approximately [3, 2], original residual [-1, 1], and Success. The exact matched-dependency probe passes6/6 on clean base and fails0/6 on this head. Use the original provider's variable_index to place values, preserve state geometry/view isolation, and add this counterexample permanently.

Risk assessment

  • Risk: high (H1: documented public solution problem/state-order/shape/original-solution contracts change).
  • Blast radius: Mapped SCC solutions for the three wrapped algorithms, including valid symbolic providers with different enumeration and index order; the initialization prerequisite also changes cache types.
  • Evidence: Independent analytic-root/order probe: base6pass versus head6fail, including nonzero original residual and false Success. Official Core3948 assertions including SCC1490, QA37/37, docs build, Runic, typos and whitespace checks pass. View-negative-control395pass24errors becomes923/923; independent view probes12/12 and simple analytic derivative3/3 pass. These successes do not cover the failing ordering counterexample. No assertions were weakened.
  • Independent review: Findings only, not counted toward merging: parent inspection confirms historical risk labels/CHANGES appeared in the supplied intake. Codex CLI 0.163.0, gpt-6.1-sol, local session 01a15db3-9dcf-7713-b93d-599577e39b55 (no session URL), reports CHANGES/high H1 at head f2fa91e based on independently executed source/base controls.
  • Merge: Needs the mapping correction and Chris's H1 major-release/version decision; the current version remains2.18.0. No release, version bump, tag or registration is authorized by this review. Latest initialization-prerequisite/combined-stack integration, downstream/LTS/prerelease/32-bit/downgrade corpus and general derivative accuracy remain unverified. A green zero-assertion AD fixture is not derivative-accuracy evidence.

Links:


🤖 Posted by an AI agent — harness: Codex CLI 0.163.0 · model: gpt-6.1-sol
Conversation: local session 01a154f0-8986-78e1-8af2-1119e8cdea4a (no session URL)

🤖 Automated review section from an AI agent running as Chris Rackauckas's account — not written or reviewed by Chris.

Fix by Codex gpt-6.1-sol (head ddb3e7c; session 01a15cba-455f-7840-a71e-61b5d3b9fca9)

Mapped values now follow the original provider's public variable_index, normalized through LinearIndices in the original geometry. variable_symbols enumeration is copied and ordered by these indices before retrieving lowered values. Array allocation, original residual evaluation, metadata and slicing/cache behavior retain their existing implementation.

The permanent official regression exercises all 36 independent enumeration/lowering permutation pairs across three shapes, linear and Cartesian indices, dense arrays and reversed strided views, both residual calling conventions and all three algorithms: 2,592 cases, 65,664 assertions. Applicable initialized solves check save_idxs, full original states, mutation isolation and repeated caches. Expected roots are independently specified as [2, 3, 5]. The existing 923-assertion container testset is byte-for-byte unchanged.

Executed validation

S denotes private scratch storage. The local wrapper runs Julia 1.12.4 with timeout 3600, four Julia threads, BLAS/OMP/MKL threads set to one, one precompile task, and job-local temporary storage and writable depot overlay. One top-level Julia command ran at a time; the unchanged official Pkg/Aqua harness used its required child processes. Subsequent commands waited for resource admission when needed. Matched environments use SciMLBase 3.58.0, NonlinearSolve 4.32.0 and SymbolicIndexingInterface 0.3.55.

Exact independent finding, unchanged analytic [2, 3] assertions:

bash "$S/view-run-julia.sh" -O0 --compiled-modules=existing --project="$S/view-base-env" "$S/ordering-review-probes.jl"
bash "$S/view-run-julia.sh" -O0 --compiled-modules=existing --project="$S/view-env" "$S/ordering-review-probes.jl"

Clean default: 6/6 pass. Unfixed parent: 0 pass, 6 fail, all three algorithms returning approximately [3, 2], residual [-1, 1], and Success. Corrected source: 6/6 pass, approximately [2, 3] with near-zero original residuals.

Same permanent test body against an exact archive of the unfixed parent and the corrected source:

bash "$S/view-run-julia.sh" -O0 --compiled-modules=existing --project="$S/ordering-unfixed-env" "$S/ordering-permanent-tests.jl"
bash "$S/view-run-julia.sh" -O0 --compiled-modules=existing --project="$S/view-env" "$S/ordering-permanent-tests.jl"

Before, exit 1, no test errors:

SCC reconstruction uses original variable indices                                                              | 50544  15120  65664  1m09.8s

After, exit 0:

Test Summary:                                     |  Pass  Total     Time
SCC reconstruction uses original variable indices | 65664  65664  1m06.8s

The extracted assertion body matches the committed official test body exactly; source/archive and test-body hashes are recorded privately.

Independent view and analytic derivative controls, exit 0:

bash "$S/view-run-julia.sh" -O0 --compiled-modules=existing --project="$S/view-env" "$S/ordering-review-more-probes.jl"
Test Summary:               | Pass  Total   Time
noncontiguous view geometry |   12     12  23.3s
ANALYTIC_DERIVATIVE SSRootfind 4.0
ANALYTIC_DERIVATIVE DynamicSS 3.9999999998402576
ANALYTIC_DERIVATIVE SICNM 4.000000000000913
Test Summary:                 | Pass  Total   Time
analytic parameter derivative |    3      3  20.8s

The derivative control is only u=p² at p=2, with analytic derivative 4; it is not a general AD accuracy claim.

Official groups, exit 0:

GROUP=Core bash "$S/view-run-julia.sh" -O0 --compiled-modules=existing --project=. -e 'using InteractiveUtils, LinearAlgebra; versioninfo(); @show Threads.nthreads() BLAS.get_num_threads(); using Pkg; Pkg.test(; allow_reresolve=false, julia_args=["--compiled-modules=existing", "-O0"])'
GROUP=QA bash "$S/view-run-julia.sh" -O0 --compiled-modules=existing --project=. -e 'using Pkg; Pkg.test(; allow_reresolve=false, julia_args=["--compiled-modules=existing", "-O0"])'
Test Summary:    | Total     Time
Core/autodiff.jl |     0  8m05.8s
Test Summary: | Pass  Total     Time
Core/core.jl  | 2412   2412  1m23.1s
Test Summary:                | Pass  Total  Time
Core/developer_interfaces.jl |    5      5  0.3s
Test Summary:               | Pass  Total  Time
Core/precompile_workload.jl |    8      8  4.8s
Test Summary: |  Pass  Total     Time
Core/scc.jl   | 67154  67154  6m30.3s
Test Summary: | Pass  Total   Time
Core/sicnm.jl |   33     33  51.6s
Testing SteadyStateDiffEq tests passed
Test Summary: | Pass  Total     Time
QA/qa.jl      |   37     37  1m30.7s
Testing SteadyStateDiffEq tests passed

Core totals 69,612 passing assertions. Its existing autodiff fixture has zero assertions. The parent runtime reports Threads.nthreads() = 4 and BLAS.get_num_threads() = 1.

Docs, formatting and spelling, exit 0:

bash "$S/view-run-julia.sh" -O0 --compiled-modules=existing --project=docs docs/make.jl
bash "$S/view-run-julia.sh" -O0 --compiled-modules=existing --project="$S/tools-env" -m Runic --check src/SteadyStateDiffEq.jl src/solve.jl test/scc.jl
typos --config .typos.toml AGENTS.md src/SteadyStateDiffEq.jl src/solve.jl test/scc.jl "$S/ordering-added-lines.txt"
git diff --check

The docs build completed document checks and HTML rendering; deployment was skipped outside CI. All three rendered algorithm contracts were inspected. Runic, typos and whitespace checks produced no failure. Algorithm docstring source is unchanged.

G1: analytic roots and independent residual/derivative controls. G2: all three algorithms, both original residual conventions, permutations and multidimensional/strided-view geometry. G3: alias checks, mutation isolation and repeated initialized solves. No assertions, tolerances, existing broken annotations or warning policies were weakened. No new dependency, version or prerequisite change.

Remaining gates

At intake, all 13 reported checks succeeded. The ancillary Dependabot Auto-merge run linked below has startup_failure, zero check runs and no available failed-job log. No default-branch run exists for this pull-request workflow, so baseline equivalence is unverified; this source correction does not repair that workflow.

The supplied fourth-round review contains independent executed findings but is not a counted merge rating because its intake exposed old labels. This author session supplies correction evidence only. H1 public-output/API compatibility and release/version policy remain unresolved; version 2.18.0 is unchanged. A fresh independent review is required; no reviewer was launched by this session.

Integration covers the prerequisite only through aabe095. Newer prerequisite revisions, their reset/getter contract and the combined stack remain unverified. Local LTS/prerelease/32-bit/downgrade matrices, downstream/GPU corpora and general derivative accuracy were not run. No release, registration, tag, merge, approval, closure or settings action. Remote CI completion after the correction is not claimed.

Next counted review must come from a smart model in a fresh session, not this fix session (risk.md § Independent reviews).

Links:


🤖 Posted by an AI agent — harness: Codex CLI 0.163.0 · model: gpt-6.1-sol
Conversation: local session 01a15cba-455f-7840-a71e-61b5d3b9fca9 (no session URL)

ChrisRackauckas and others added 4 commits October 7, 2026 12:01
…inearSolve

DynamicSS and SICNM only had __solve; init(ssprob, DynamicSS(...)) had no
matching __init here, so it fell through to NonlinearSolveBase's generic
fallback, which treats "no algorithm-specific __init exists" the same as "no
algorithm given" and rewrites the call to NonlinearSolve's default-algorithm
converter with ::Nothing in place of the algorithm - silently discarding
DynamicSS/SICNM. For a plain SteadyStateProblem that converter still finds a
NonlinearProblem to solve, so init() "worked" but ignored the requested
algorithm; for a SteadyStateProblem whose lowered_problem is an
SCCNonlinearProblem, the converter's NonlinearProblem(prob) call hits a
FieldError (SCCNonlinearProblem has no u0 field) - the reported CI failure,
init(ssprob, DynamicSS(Tsit5())) in SciMLBase.jl's downstream
SymbolicIndexingInterface test group.

Add SciMLBase.__init(prob::AbstractSteadyStateProblem, alg::DynamicSS/SICNM,
...), mirroring the existing __solve methods (extracted into shared
__dynamicss_ode_setup/__sicnm_ode_setup helpers): for a plain problem it
returns the live ODE/DAE integrator so init/solve!/step! compose normally; for
an SCC lowering there is no single integrator to hand back (blocks solve
sequentially), so it eagerly solves via the existing __solve_scc_lowering and
returns the finished solution, matching how a stored NonlinearProblem/
LinearProblem lowering is already handled verbatim elsewhere in this file.

Being a more specific method than NonlinearSolveBase's generic fallback, this
is picked by ordinary dispatch and the buggy conversion path is never reached
for these two algorithms - independent of SciML/SciMLBase.jl#1614 (the
generic converter itself is fixed separately there and in
SciML/NonlinearSolve.jl#1327).

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Sonnet <noreply@anthropic.com>
Agent-Harness: Claude Code 2.1.251 (subagent)
Agent-Model: claude-sonnet
Agent-Session: https://claude.ai/code/session_01NB3oFTNzGW79UtDt8AyjsM
The comment still described SciMLBase's NonlinearProblem(::SCCNonlinearProblem)
conversion as an identity that could recurse forever; it now raises an
ArgumentError, and this repo's new __init methods are more specific than the
default path anyway, so it is never reached.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Sonnet <noreply@anthropic.com>
Agent-Harness: Claude Code 2.1.251 (subagent)
Agent-Model: claude-sonnet
Agent-Session: https://claude.ai/code/session_01NB3oFTNzGW79UtDt8AyjsM
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Agent-Harness: Claude Code (subagent of ci-green head)
Agent-Model: claude-opus-5-5[1m]
Agent-Session: https://claude.ai/code/session_011aK5Xvp9NpdkzKuEwtiMGd
A `SteadyStateProblem` built by ModelingToolkit records an SCC lowering that is
compiled from the residual system separately, so its state can differ from the
problem's in length and order (for a `complete`d but uncompiled system,
`mtkcompile` can eliminate unknowns as observed). `DynamicSS`, `SICNM` and
`SSRootfind` returned the lowering's solution as-is, so `sol.u` no longer lined
up with `prob.u0` and indexing by the system's variable indices failed with a
BoundsError (SciMLBase test/downstream/comprehensive_indexing.jl).

When the problem is symbolic and the lowering resolves every unknown of the
problem, the solution is now built on `prob` in its own state ordering, with
the SCC solution kept as `original`. Problems without symbolic information keep
the solution on the lowering.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Agent-Harness: Claude Code (subagent of ci-green head)
Agent-Model: claude-opus-5-5[1m]
Agent-Session: https://claude.ai/code/session_011aK5Xvp9NpdkzKuEwtiMGd
ChrisRackauckas and others added 4 commits October 8, 2026 09:57
`init(prob, ::DynamicSS/SICNM)` returned the raw ODE integrator, so
`solve!` skipped the termination-condition finalization of `solve`:
finite-time nonconvergence reported Success and a protective safe
termination reported Terminated instead of Unstable. For SCC lowerings
`init` returned a finished solution that `solve!` could not accept.

`init` now returns a `SteadyStateODECache` (ODE integrator plus the
setup holding the termination cache) or a `SteadyStateSCCCache`; `solve!`
on either runs the same finalization as `__solve`, and `step!` advances
the ODE integrator. Documents the init/solve! contract, including
SICNM's extended `[y; z]` integrator state.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Agent-Harness: Claude Code (subagent of ci-green head)
Agent-Model: claude-opus-5-5[1m]
Agent-Session: https://claude.ai/code/session_011aK5Xvp9NpdkzKuEwtiMGd
Resolves the import conflicts in src/SteadyStateDiffEq.jl and
test/scc.jl, keeps both MTK init assertions, and checks that
solve!(init(...)) maps the SCC solution onto the problem's state.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Agent-Harness: Claude Code (subagent of ci-green head)
Agent-Model: claude-opus-5-5[1m]
Agent-Session: https://claude.ai/code/session_011aK5Xvp9NpdkzKuEwtiMGd
The mapped state is converted to `typeof(prob.u0)`, so an observed
integer literal no longer yields a `Vector{Int}` for a Float64 problem.
The mapped solution carries the lowering solution's `stats`, `left` and
`right`, and a `save_idxs` slice keeps the unsliced solution as
`original`.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Agent-Harness: Claude Code (subagent of ci-green head)
Agent-Model: claude-opus-5-5[1m]
Agent-Session: https://claude.ai/code/session_011aK5Xvp9NpdkzKuEwtiMGd
ModelingToolkit 11.44 cannot build an SCC lowering for a system whose
unknowns are all eliminated (empty `var_sccs`); 11.45.0 handles it, so
the ModelingToolkit test compat floor moves to 11.45. At the floor
NonlinearSolve stack, Newton cannot form a Jacobian for the resulting
zero-length problem, so the eltype check uses Jacobian-free `Broyden`.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Agent-Harness: Claude Code (subagent of ci-green head)
Agent-Model: claude-opus-5-5[1m]
Agent-Session: https://claude.ai/code/session_011aK5Xvp9NpdkzKuEwtiMGd
@ChrisRackauckas

Copy link
Copy Markdown
Member

🤖 Automated comment from an AI agent running as @ChrisRackauckas — not written or reviewed by Chris.

Independent review (Devin CLI 3000.11.3, model fusion-claude-opus-5-5-high-sidekick-swe-2-medium): MERGE, risk medium.

Full review

VERDICT: MERGE
RISK: medium

Merge only after #173 lands: this PR is stacked on it. The risk is medium because sol.u/sol.prob/sol.resid change for every MTK-built SteadyStateProblem with a lowering solved by DynamicSS/SICNM/SSRootfind. That puts the solution back in the problem's coordinates, the pre-2.17 contract, so it is a fix, but code written against 2.17/2.18 output will see the change.

Blocking findings

None.

Non-blocking findings

  1. The fix covers only part of the public solve surface. Confirmed by running. src/solve.jl:177 (the SciMLBase.solve(prob::SteadyStateProblem, ...) override) still returns the unmapped lowering solution for solve(prob) and solve(prob, NewtonRaphson()). scratch/probe3.jl uses the 2-state completed system from SciMLBase's comprehensive_indexing.jl. At the PR head it prints:

    default        prob=SCCNonlinearProblem length(u)=1 u=[5.0]
    NewtonRaphson  prob=SCCNonlinearProblem length(u)=1 u=[5.0]
    SSRootfind     prob=SteadyStateProblem length(u)=2 u=[5.0, 1.5]
    DynamicSS      prob=SteadyStateProblem length(u)=2 u=[5.0, 1.5]
    

    So SSRootfind(NewtonRaphson()) and NewtonRaphson() now return differently shaped sol.u for the same problem. The PR body says this is deferred to solve(::SteadyStateProblem) returns the lowered problem's solution: sol.u reordered/empty, sol.prob.f.sys time-independent #171, and the PR does not claim to close it. Still, the body's general framing ("solve(prob) should return a solution of prob", "breaks the common ODEProblem(sys, ...; u0 = sssol.u) pattern") is true only for the three wrapped algorithms. Suggested follow-up: route the line-177 result through __scc_solution_on_problem as well, which would close solve(::SteadyStateProblem) returns the lowered problem's solution: sol.u reordered/empty, sol.prob.f.sys time-independent #171.

  2. Overhead per solve. Confirmed by running. __scc_solution_on_problem builds a fresh getsym(sccsol, syms) on every solve. With BenchmarkTools on the 2-state completed system (scratch/probe2.jl):

    • mapping: median 129 µs, 1999 allocations, almost all of it from constructing the getsym
    • full solve(ssprob, DynamicSS(Tsit5())): median 524 µs, so the mapping is about 25% of it

    In a 50-solve loop (scratch/probe.jl), per-solve time went from about 400 µs at base aabe095 to about 770 µs at head, for both DynamicSS and SSRootfind. This is acceptable for correctness, but it is a real slowdown for parameter sweeps on small MTK steady-state problems. The getter could be built once per lowering, or MTK's observed-function cache used. Not required for this PR.

  3. Silent fallback. Suspected from reading. In src/solve.jl:132-135, when prob has a sys but some unknown of prob cannot be resolved on the lowering, the function returns nothing. The solution then silently stays in lowering coordinates, which is the same shape mismatch this PR fixes, with no warning. I could not build an MTK problem that reaches this branch, and every probe took the mapped path. A @warn or an error for "has sys but cannot map" would make that failure visible.

  4. Ordering assumption. Suspected from reading, harmless for MTK. src/solve.jl:133-137 assumes variable_symbols(prob)[i] is the symbol at state index i. That holds for MTK systems and SymbolCache, but the SymbolicIndexingInterface interface does not require it. Placing values by variable_index(prob, s) would be robust. scratch/probe.jl confirms positional correctness against an independent oracle (see What I ran), including a case where mtkcompile reorders unknowns.

  5. Docs. docs/src/index.md (changed in Add __init for DynamicSS/SICNM so init() doesn't fall through to NonlinearSolve's default #173) says nothing about which coordinates the returned solution uses when a lowering exists: sol.u follows prob.u0, sol.original is the SCC solution, and the no-sys case stays on the lowering. One sentence there would record the contract this PR establishes.

  6. Return type is a small Union. Confirmed by running. Base.return_types(SciMLBase.__solve, (typeof(ssprob), DynamicSS)) returns a 2-member Union: the SCC-problem solution or the SteadyStateProblem solution. The base's solprob choice was already unstable in the same way, so this is not a regression.

What I ran

All runs used Julia 1.12 against registered packages, not the author's dev'd SciMLBase#1624: SciMLBase 3.57.0, ModelingToolkit 11.45.3, NonlinearSolve 4.32.0, SCCNonlinearSolve 1.15.4, SymbolicIndexingInterface 0.3.55, OrdinaryDiffEq 7.8.1. There were two envs, scratch/env_head (PR head dc54954) and scratch/env_base (#173 head aabe095, via a temporary worktree that I have since removed).

  • test/scc.jl at the PR head: exit 0, every testset passes. That includes "SCC-lowered solution is expressed in the problem's state" (11/11, 11/11, 8/8) and "SCC-lowered solution metadata" (24/24). Log: scratch/scc_head.log.
  • Fail-before: the PR head's test/scc.jl against the aabe095 source fails at scc.jl:403, sol.prob isa SteadyStateProblem, which evaluates to SCCNonlinearProblem. The file aborts there, so the later new testsets did not run in this config. Log: scratch/scc_base.log.
  • Independent oracle (scratch/probe.jl). A 4-state nonlinear chain (y^3, w^2 terms), built both ways (mtkcompile, which reorders the unknowns, and complete), solved with DynamicSS, SICNM and SSRootfind. The reference solution is the same problem with lowered_problem = nothing.
    • At head: max|sol.u - ref.u| is 0.9–1.4e-10 and ‖resid‖∞ is ≤ 8e-11 for all 6 combinations. After remake(prob; p = [a => 2.0]) the difference is ≤ 1.7e-10 with resid ≤ 6e-11, so the lowering and resid both use the new p.
    • At base: every positional comparison throws DimensionMismatch (2 vs 4).
  • ForwardDiff through the solve. I computed d x_ss/da (analytically 1) with positional sol.u[variable_index(sys, x)], and d y_ss/da with symbolic sol[y] (analytically 0.222112591113).
    • At head: 1.0 and about 0.2221126 for all algorithms and both system kinds. The convert(typeof(prob.u0), …) does not break Dual propagation, because u0 is promoted.
    • At base: the mtkcompile system gives BoundsError. The complete system silently returns 0.2221 for d x/da, a wrong value read from the lowering's index. This is the bug this PR fixes.
  • DAE (mtkcompile, algebraic 0 ~ r^2 - q - 3). At head, SSRootfind and SICNM map to the 3-state ordering (q, r, p) = (1, 2, 1), which is correct, with resid ≤ 5e-11. At base, SSRootfind returned a length-1 u on a NonlinearProblem. DynamicSS(Tsit5()) errors on the mass matrix at both commits. That error predates this PR and is unrelated to it.
  • A failed DynamicSS (short tspan) keeps retcode = Failure at both commits.
  • Mirror of SciMLBase test/downstream/comprehensive_indexing.jl lines 18–220 for ssprob/sssol (scratch/probe2.jl). It covers state indexing with @inferred on the getter, observed XY/X*Y, and parameters.
    • At head: 17/17 pass for both, and init(ssprob, DynamicSS(Tsit5())) returns a SteadyStateSCCCache.
    • At base: BoundsError: attempt to access 1-element Vector{Float64} at index [[1, 2]], which is the CI failure the PR body describes.
  • SVector u0 via u0_constructor: errors at both commits, inside MTK/SciMLBase (in-place function with an immutable u0). This PR does not touch that path.
  • Checks:
    • Runic --check src test: clean.
    • typos on the diff: one hit, ded, inside a git index hash. False positive.
    • Base.ispublic(SciMLBase, :has_sys) and :build_solution are both true. The new SymbolicIndexingInterface imports are exported API. SymbolicIndexingInterface was already a direct dependency (MIT); the PR only adds it to test [targets].
  • CI on dc54954 (from gh pr checks): green, including Downgrade, QA, Core lts/1/x86. Core pre passed in 32 s, which suggests it ran little or nothing. I read this rather than reran it.
  • Test quality, from reading. Expected values are analytic (2×2 linear solve, closed forms), not taken from the code's own output. The tolerances (atol 1e-8 against solver tol 1e-10, resid 1e-7) are reasonable. The PR adds no skipped, broken or loosened tests. The five changed assertions encode the intended contract change and are not weakened.
  • Comments in the diff are few and describe the current behaviour, with no retrospective narration.

What I did not verify

  • SciMLBase GROUP=SymbolicIndexingInterface with SciMLBase#1624 dev'd, and the exact counts in the PR body. I only ran the mirror of the ssprob/sssol section against registered SciMLBase 3.57.
  • The GROUP=Core counts in the PR body for core.jl, sicnm.jl, developer_interfaces.jl and precompile_workload.jl. I ran only test/scc.jl; the other files are not touched by this PR's commits.
  • GROUP=QA locally (relied on the green CI check).
  • The PR body's downgrade-environment run on Julia 1.10 (relied on the green CI Downgrade check).
  • MTK's own test/scc_nonlinear_problem.jl against this branch.
  • init with SSRootfind on an SCC lowering.

Links:


🤖 Posted by an AI agent — harness: Devin CLI 3000.11.3 (review), Claude Code 2.1.285 (posting) · model: fusion-claude-opus-5-5-high-sidekick-swe-2-medium
Conversation: local Claude session 3c311569-dda6-4687-bc9f-65febb212c33 (fleet master)

ChrisRackauckas and others added 3 commits October 21, 2026 01:24
Restore mapped values in the original state's geometry before converting its
container type. Exercise scalar, matrix and tensor states across all three
algorithms, including original residuals, output selection and cache isolation.
Document the mapped solution and fallback contract in the algorithm docstrings.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Codex <noreply@openai.com>
Agent-Harness: Codex CLI 0.163.0
Agent-Model: gpt-6.1-sol
Agent-Session: local session 01a15cba-455f-7840-a71e-61b5d3b9fca9 (no session URL)
Allocate mapped arrays through map in the original geometry and convert each
mapped value to the original element type. Cover views across all three
algorithms, residual conventions, output slicing and repeated cached solves.
Document the allocation contract and retain container coverage in repository
instructions.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Codex <noreply@openai.com>
Agent-Harness: Codex CLI 0.163.0
Agent-Model: gpt-6.1-sol
Agent-Session: local session 01a15cba-455f-7840-a71e-61b5d3b9fca9 (no session URL)
Order symbols by their original state indices before retrieving lowered
solution values, using the original array geometry for Cartesian indices.
Cover independent enumeration and lowering permutations, strided views,
original residuals, and repeated cached slicing with analytic roots.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Codex <noreply@openai.com>
Agent-Harness: Codex CLI 0.163.0
Agent-Model: gpt-6.1-sol
Agent-Session: local session 01a15cba-455f-7840-a71e-61b5d3b9fca9 (no session URL)
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.

2 participants