Skip to content

Raise a clear ArgumentError converting an SCCNonlinearProblem to NonlinearProblem - #1614

Draft
ChrisRackauckas-Claude wants to merge 3 commits into
SciML:masterfrom
ChrisRackauckas-Claude:fix/scc-nonlinearproblem-identity
Draft

ChrisRackauckas-Claude wants to merge 3 commits into
SciML:masterfrom
ChrisRackauckas-Claude:fix/scc-nonlinearproblem-identity

Conversation

@ChrisRackauckas-Claude

@ChrisRackauckas-Claude ChrisRackauckas-Claude commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

Please ignore until reviewed by @ChrisRackauckas.

What changed and why

GROUP=SymbolicIndexingInterface is (or was) red on master (CI run https://github.com/SciML/SciMLBase.jl/actions/runs/35839187919): init(ssprob, DynamicSS(Tsit5())) on an MTK-lowered SCC steady-state problem throws FieldError: type SCCNonlinearProblem has no field u0.

Root cause: NonlinearProblem(prob::AbstractNonlinearProblem) = NonlinearProblem{isinplace(prob)}(prob.f, prob.u0, prob.p) assumes every AbstractNonlinearProblem subtype has a u0 field. SCCNonlinearProblem never has one (its state is the concatenation of its sub-problems' states), so calling NonlinearProblem(x) on an SCCNonlinearProblem has always thrown this FieldError, standalone, with no other package involved:

using SciMLBase
f1 = NonlinearFunction((du, u, p) -> (du .= u .^ 2 .- p))
prob1 = NonlinearProblem(f1, [1.0], 4.0)
sccprob = SciMLBase.SCCNonlinearProblem((prob1,), (Returns(nothing),))
NonlinearProblem(sccprob)   # FieldError on master

Revision history on this PR

An earlier version of this PR made NonlinearProblem(prob::SCCNonlinearProblem) an identity (= prob), matching how a stored NonlinearProblem/LinearProblem lowered_problem is already handled verbatim elsewhere ("stored problem is used verbatim" in test/problem_building_test.jl). Review caught two problems with that:

  1. It doesn't fix the reported CI failure at all — the real cause is SteadyStateDiffEq.jl having no __init for DynamicSS/SICNM, which is fixed independently and sufficiently in Add __init for DynamicSS/SICNM so init() doesn't fall through to NonlinearSolve's default SteadyStateDiffEq.jl#173 (confirmed: that fix alone, against today's unmodified SciMLBase, resolves the reported failure).
  2. Worse, the identity is itself a footgun and a hazard: a NonlinearProblem constructor returning something that is not a NonlinearProblem breaks any caller that assumes .u0/.lb/.ub/etc., and it is exactly what let NonlinearSolve.jl's default-algorithm converter (src/default.jl, which does nlprob = NonlinearProblem(prob); return __init(nlprob, nothing, args...)) recurse forever instead of terminating, once it received nlprob === prob. Reproduced:
    Warning: detected a stack overflow; program state may be corrupted, so further execution might be unreliable.
    ERROR: LoadError: StackOverflowError:
     [1] __init(prob::SCCNonlinearProblem{...}, ::Nothing, args::DynamicSS{...}; ...) (repeats 39992 times)
    

What this PR does now

SCCNonlinearProblem solves as an ordered sequence of block solves (see SciMLBase.solve(::SCCNonlinearProblem, ::Union{DynamicSS,SICNM}, ...) and SSRootfind's handling in SteadyStateDiffEq.jl), not a single residual — there is no faithful NonlinearProblem to construct from it. So NonlinearProblem(prob::SCCNonlinearProblem) now raises a clear ArgumentError:

ArgumentError: an SCCNonlinearProblem cannot be converted to a NonlinearProblem; solve it directly or use its component problems.

An error cannot recurse, so this also removes the recursion hazard above without needing a companion guard in NonlinearSolve.jl (the earlier companion PR, SciML/NonlinearSolve.jl#1327, has been closed as unnecessary: NonlinearProblem(prob) never returns prob itself now, so the nlprob === prob case it guarded against cannot occur).

Verification

Standalone reproducer, fail-before/pass-after:

Before (unmodified master):

Caught: FieldError -- FieldError: type SCCNonlinearProblem has no field `u0`, available fields: `probs`, `explicitfuns!`, `f`, `p`, `parameters_alias`

After (this PR):

Caught: ArgumentError -- ArgumentError: an SCCNonlinearProblem cannot be converted to a NonlinearProblem; solve it directly or use its component problems.

Regression test updated to @test_throws ArgumentError NonlinearProblem(sccprob) in test/problem_building_test.jl (still discriminates: throws FieldError on unmodified master, so @test_throws ArgumentError fails there and passes here).

GROUP=Core passes locally in full, including the updated test (Problem building tests | 182 182):

     Testing SciMLBase tests passed

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

What I did not verify

  • Did not run the actual downstream GROUP=SymbolicIndexingInterface test in isolation against this PR — see Add __init for DynamicSS/SICNM so init() doesn't fall through to NonlinearSolve's default SteadyStateDiffEq.jl#173 body for why that test doesn't need this PR at all (it is fixed independently there).
  • Did not run GROUP=QA locally (PythonCall/CondaPkg fails to start on this machine, unrelated to this change).
  • Did not audit every existing caller of NonlinearProblem on an AbstractSteadyStateProblem-typed value for reliance on the old identity behavior; grepped SteadyStateDiffEq.jl's source and test suite (including its new tests in PR Add a page describing the init and solve interfaces #173) and found none that call NonlinearProblem directly on an already-built SCCNonlinearProblem — all existing callers only convert a SteadyStateProblem (which correctly returns the stored SCCNonlinearProblem verbatim; that path is untouched by this change).

Links

🤖 Generated with Claude Code

https://claude.ai/code/session_01NB3oFTNzGW79UtDt8AyjsM

Risk assessment

  • Risk: low
  • Blast radius: Only code that calls NonlinearProblem(::SCCNonlinearProblem). That call already failed on master with FieldError: type SCCNonlinearProblem has no field u0. With this PR it fails with an ArgumentError instead, so nothing that works today stops working. The one downstream path it touches is the MTK steady-state init(ssprob, DynamicSS(...)) route through NonlinearSolve's src/default.jl. That path fails before and after; only the error type changes. The PR adds one 4-line method, NonlinearProblem(prob::SCCNonlinearProblem), in src/problems/nonlinear_problems.jl, plus one @test_throws ArgumentError in test/problem_building_test.jl. It uses no non-public internals of dependencies. There's no SemVer concern, since the only change is which exception is thrown for an input that was never supported. No tests are weakened or skipped.
  • Evidence: The head commit is 7a57389 (CI ran 2026-09-24). It is 13 commits behind master and was branched from b7076b5. I compared all 8 failing checks with master runs, and every one is pre-existing:
    • tests / SymbolicIndexingInterface (julia 1, lts): pre-existing. These fail on every master "Run Tests" run from 2026-09-23 to 2026-10-03, including 37100169960 on master head 432e985, and on the merge base (35923456549). It's the same testset, "Symbol and integer based indexing of interpolated solutions". On master it throws the FieldError; on this PR it throws the new ArgumentError. So the PR changes the message but doesn't fix the test.
    • tests / QA (julia 1): pre-existing at the base. ExplicitImports flags Base.Callable at nonlinear_problems.jl:256, a line this PR doesn't touch. QA also failed on the merge base (35923456549) and on master 36036224472. Master fixed it later in Use the local Callable alias instead of non-public Base.Callable #1608, so it should clear after a rebase.
    • SciMLSensitivity.jl/Core1, Core6, Core7: pre-existing. The same testsets fail on master IntegrationTest 37100169941 (2026-10-03) and 36518786705: "Mooncake with MooncakeAdjoint", "Complex Matrix FiniteDiff Adjoint" and "Fully Out of Place adjoint sensitivity".
    • SciMLSensitivity.jl/Core8: pre-existing at the base. "Enzyme/Mooncake through init" in desauty_dae_mwe.jl also fails on master 36518786705 and 36447374379. It passes on current master since Un-break Mooncake DAE initialization observable AD test (fixes #1568) #1576, so it needs a rebase to confirm.
    • Catalyst/All/1 (ReleaseTest): pre-existing. It fails on master 37100169405 with the same infrastructure error: SystemError: opening .../Catalyst/.../test/extensions/Project.toml: Permission denied.
    • All other checks pass, including Core on julia 1, lts and x86, the Downstream groups, every ModelingToolkit and NonlinearSolve downstream group, Runic, typos and docs.
  • Independent review: Devin CLI (Mac) / fusion-claude-opus-5-5-high-sidekick-swe-2-medium rated it low, high confidence: the change swaps one exception for a clearer one on a call that always failed, and every failing check also fails on master or at the merge base.
  • Merge: needs human review. No regressions, but these points need a person:
    1. The PR doesn't fix the CI failure it was opened for. SymbolicIndexingInterface stays red, and the real fix, Add __init for DynamicSS/SICNM so init() doesn't fall through to NonlinearSolve's default SteadyStateDiffEq.jl#173, is still open and unmerged. The description says this.
    2. It's a design decision that overlaps with the still-open Route SCC-lowered SteadyStateProblems through the default init/solve NonlinearSolve.jl#1319, which another approach to the same error (a maintainer, SebastianM-C, pointed to it on the PR). It also commits SciMLBase to "an SCC problem can never be converted to a NonlinearProblem", and a maintainer should confirm that's the intended contract.
    3. The body opens with "Please ignore until reviewed by @ChrisRackauckas", and no review has been done.
    4. CI is stale: the head is 13 commits behind master, and the QA and Core8 failures were fixed on master after the branch point. It should be rebased and CI re-run before merging.
    5. Small things: 4 of the 6 added test lines are a comment, the branch name fix/scc-nonlinearproblem-identity describes the earlier identity-returning approach, and the PR body lacks the standard agent-attribution banner and footer. The description is otherwise complete. It says QA wasn't run locally and SymbolicIndexingInterface wasn't run against this PR.

🤖 Risk assessment posted by an AI agent (fleet master) — harness: Devin CLI (Mac) / fusion-claude-opus-5-5-high-sidekick-swe-2-medium; dispatched by Devin CLI 3000.11.3 (Mac) fleet-master caretaker, model Fusion (claude-opus-5-5 medium + swe-2 medium)
Conversation: local transcript ~/.local/share/devin/cli/transcripts/speckle-whale.json (Chris's Mac)

NonlinearProblem(prob::AbstractNonlinearProblem) generically rebuilds
prob via prob.f, prob.u0, prob.p, but SCCNonlinearProblem has no u0
field (its state is the concatenation of its sub-problems' states).
Calling NonlinearProblem on an SCCNonlinearProblem therefore threw a
FieldError instead of returning the (already valid) problem unchanged.

This was latent until ModelingToolkit.jl started storing an
SCCNonlinearProblem in SteadyStateProblem's lowered_problem field:
NonlinearProblem(::SteadyStateProblem) correctly returns that stored
problem verbatim, but a downstream caller normalizing the result via
NonlinearProblem() again hit the generic fallback.

Add an identity method for SCCNonlinearProblem alongside a regression
test.

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
@SebastianM-C

Copy link
Copy Markdown
Member

SciML/NonlinearSolve.jl#1319 is also related to the same error

…inearProblem

NonlinearProblem(prob::AbstractNonlinearProblem) rebuilds via prob.f/prob.u0/
prob.p, but SCCNonlinearProblem has no u0 field, so this threw an opaque
FieldError. Returning prob unchanged (an earlier version of this fix) is its
own footgun: a NonlinearProblem constructor returning something that isn't a
NonlinearProblem breaks any caller assuming .u0/.lb/.ub, and it is exactly
what let a caller that re-normalizes its result (NonlinearSolve.jl's
default-algorithm converter) recurse forever instead of terminating.

An SCCNonlinearProblem solves as an ordered sequence of block solves, not a
single residual, so there is no faithful NonlinearProblem to construct here.
Raise a clear ArgumentError instead, naming what to do (solve it directly or
use its component problems).

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
@ChrisRackauckas-Claude ChrisRackauckas-Claude changed the title Fix FieldError normalizing an SCCNonlinearProblem via NonlinearProblem Raise a clear ArgumentError converting an SCCNonlinearProblem to NonlinearProblem Sep 24, 2026
Keep the source comment to the why; the recursion-hazard and review history
belong in the PR body, not the code.

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

This branch has not been deployed

No deployments
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.

3 participants