Skip to content

Say that unbalanced initialization errors come from initialization - #4897

Merged
ChrisRackauckas merged 3 commits into
SciML:masterfrom
ChrisRackauckas-Claude:initialization-error-context
Aug 7, 2026
Merged

Say that unbalanced initialization errors come from initialization#4897
ChrisRackauckas merged 3 commits into
SciML:masterfrom
ChrisRackauckas-Claude:initialization-error-context

Conversation

@ChrisRackauckas-Claude

Copy link
Copy Markdown
Member

Draft — please ignore until reviewed by @ChrisRackauckas.

The problem

Reported by Michael Tiller: a Dyad model built for the JuliaCon workshop failed with

ERROR: ExtraVariablesSystemException: The system is unbalanced. There are 2 highest order derivative variables and 1 equations.
More variables than equations, here are the potential extra variable(s):
 inertia₊wˍt(t)
Note that the process of determining extra variables is a best-effort heuristic. The true extra variables are dependent on the model and may not be in this list.

The counting is correct and the fix is "supply one more equation", but nothing in the message says which system is unbalanced. The model's own equations are fine; it is the initialization system that is short an equation. The only indication of that is the stack trace. On top of that, inertia₊wˍt(t) is D(inertia₊w), a variable the user never wrote, and the message gives no hint of how to supply the missing equation.

Reproducer

Not Dyad-specific — plain MTK reproduces it, and shows the progression:

using ModelingToolkit
using ModelingToolkit: t_nounits as t, D_nounits as D

@parameters J = 1.0
@variables phi(t) w(t)
@mtkcompile sys = System([D(phi) ~ w, J * D(w) ~ 1.0 * t], t)

ODEProblem(sys, [], (0.0, 1.0); fully_determined = true)          # InvalidSystemException: structurally singular
ODEProblem(sys, [phi => 0.0], (0.0, 1.0); fully_determined = true) # ExtraVariablesSystemException, as above
ODEProblem(sys, [phi => 0.0, w => 0.0], (0.0, 1.0); fully_determined = true)  # builds

The change

InitializationProblem compiles the initialization system at exactly one place. That call is now wrapped so structural errors are rethrown with the missing context. The exception type is unchanged, so existing code (and the existing @test_throws in the suite) that catches ExtraVariablesSystemException / ExtraEquationsSystemException / InvalidSystemException still works — only the message changes.

The same case now reports:

ExtraVariablesSystemException: Initialization system is underdetermined.

This is an error in the initialization system, not in the equations being integrated. The
initialization system solves for the value of every unknown of the model, and of their
derivatives, at the initial time. It needs as many equations as there are such values to
solve for. Initial conditions and bindings given for the model become equations of this
system.

The system is unbalanced. There are 2 highest order derivative variables and 1 equations.
More variables than equations, here are the potential extra variable(s):
 inertia₊wˍt(t)

1 more equation is needed to determine the initial state.

Any of the following supplies one missing equation:

  * Give an initial value in the problem constructor, as in
    `ODEProblem(sys, [x => 1.0], tspan)`.
  * Give one in the model, via `initial_conditions` or `bindings`.
  * Add an equation relating initial values, via the `initialization_eqs` keyword argument
    of the system or of the problem constructor.

`guesses` are starting values for the initialization solve rather than constraints on it,
so adding a guess does not add an equation. To solve an underdetermined initialization in
a least squares sense instead, pass `fully_determined = false` to the problem constructor;
the guesses then decide which of the possible initial states is found.

See https://docs.sciml.ai/ModelingToolkit/stable/tutorials/initialization/ for more information.
Note that variables named with a `ˍt` suffix are derivatives at the initial time: `xˍt` is `D(x)`.

Note that the process of determining extra variables is a best-effort heuristic. The true extra variables are dependent on the model and may not be in this list.

Three variants are handled, each with its own remedy paragraph:

  • ExtraVariablesSystemException → underdetermined
  • ExtraEquationsSystemException → overdetermined
  • InvalidSystemException (singular message only) → structurally singular, i.e. balanced but with a redundant condition

Notes on two choices:

  • The deficit count ("1 more equation is needed") is obtained by recompiling the initialization system with the balance check off and taking nunknowns - neqs. This costs a second compile, but only on the path that is about to throw. Only the difference is reported, not the absolute counts: that compile simplifies further than the one that failed, so its raw counts do not line up with the ones structural analysis prints, and showing both pairs would be worse than showing one.
  • InvalidSystemException is also thrown for things that have nothing to do with the balance of a system ("Illegal unknown: …", derivative on the RHS). Only messages that report structural singularity are rewritten; the rest pass through untouched.

ExtraVariablesSystemException and friends are defined both in ModelingToolkitBase and in StateSelection.jl, which ModelingToolkitBase does not depend on, so the two are matched by type name. The alternative is to thread a "this is an initialization system" flag from mtkcompile down into StateSelection.check_consistency and build the message at the source, which would also let it print D(w) instead of wˍt and drop the second compile. That is the better long-term shape but spans two repos; happy to do it that way instead if preferred.

Tests

Added to lib/ModelingToolkitBase/test/initializationsystem.jl, alongside the existing @test_throws cases that already cover both the under- and over-determined paths:

  • the raised error still has the same type, its message names the initialization system, and the original structural message is retained
  • a direct test of the message builder for all three variants, including that the deficit sentence is omitted when the counts are unknown or do not corroborate the reported imbalance

Not addressed here

Michael also noted that the AI assistant diagnosed this as "just specify an initial condition for phi" and claimed to have verified it without running the analysis. That is an agent-side issue, not a ModelingToolkit one.

🤖 Generated with Claude Code

https://claude.ai/code/session_019XZQMmzJbrqB9aJomKAB9z

ChrisRackauckas and others added 2 commits August 6, 2026 17:13
Structural analysis has no way of knowing that the system handed to it is
an initialization system, so an unbalanced or singular initialization
system was reported in terms that read as if the model itself were at
fault:

    ERROR: ExtraVariablesSystemException: The system is unbalanced. There
    are 2 highest order derivative variables and 1 equations.
    More variables than equations, here are the potential extra variable(s):
     inertia₊wˍt(t)

Nothing in that names initialization, says how many equations are missing,
or says how to supply them, and the `ˍt` names are derivatives the user
never wrote. The only hint is the stack trace.

Wrap the `mtkcompile` of the initialization system so these errors are
rethrown with that context: which system is unbalanced and in which
direction, how many equations short it is, and the ways of supplying them
(initial values in the problem constructor or the model,
`initialization_eqs`, or `fully_determined = false` to solve in a least
squares sense). The exception type is unchanged, so code catching these
still works.

The deficit is measured by recompiling without the balance check, which
happens only on the error path. Only the difference is reported: that
compile simplifies further, so its absolute counts do not line up with the
ones structural analysis reports.

`InvalidSystemException` also reports problems unrelated to the balance of
a system, so only its structural singularity message is rewritten.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XZQMmzJbrqB9aJomKAB9z
The remedies in an initialization error name ModelingToolkit functions and
keyword arguments: pass this to `ODEProblem`, set `initialization_eqs`.
A front end which presents a modelling language of its own has none of
those, so the advice sends its users looking for something that does not
exist in their language.

Split each remedy into the part that is about the model and the part that
is about the ModelingToolkit API, and add `show_api_guidance!(false)` to
drop the latter. What went wrong is still described in full. A front end
calls it once when it loads and adds guidance of its own.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XZQMmzJbrqB9aJomKAB9z
@ChrisRackauckas-Claude

Copy link
Copy Markdown
Member Author

Added show_api_guidance! so front ends can drop the ModelingToolkit-specific part (687a1c8).

Each remedy is now split in two: what is true of the model, and what names ModelingToolkit functions and keyword arguments. ModelingToolkit.show_api_guidance!(false) — called once when the front end loads — keeps the first and drops the second. What went wrong is still described in full, so the diagnosis is unchanged; only the "pass this to ODEProblem" half goes away, along with the link to the ModelingToolkit initialization docs.

Same error, guidance on (the default):

ExtraVariablesSystemException: Initialization system is underdetermined.

This is an error in the initialization system, not in the equations being integrated. The
initialization system solves for the value of every unknown of the model, and of their
derivatives, at the initial time. It needs as many equations as there are such values to
solve for. Initial values given for the model become equations of this system.

The system is unbalanced. There are 2 highest order derivative variables and 1 equations.
More variables than equations, here are the potential extra variable(s):
 inertia₊wˍt(t)

1 more equation is needed to determine the initial state.

Each missing equation is supplied by giving one more initial value, or one more equation
relating initial values. Guesses are starting values for the initialization solve rather
than constraints on it, so adding a guess does not supply one.

In ModelingToolkit, any of the following supplies one missing equation:

  * Give an initial value in the problem constructor, as in
    `ODEProblem(sys, [x => 1.0], tspan)`.
  * Give one in the model, via `initial_conditions` or `bindings`.
  * Add an equation relating initial values, via the `initialization_eqs` keyword argument
    of the system or of the problem constructor.

To solve an underdetermined initialization in a least squares sense instead, pass
`fully_determined = false` to the problem constructor; the guesses then decide which of
the possible initial states is found.

See https://docs.sciml.ai/ModelingToolkit/stable/tutorials/initialization/ for more information.
Note that variables named with a `ˍt` suffix are derivatives at the initial time: `xˍt` is `D(x)`.

and with show_api_guidance!(false), which is what Dyad would ship:

ExtraVariablesSystemException: Initialization system is underdetermined.

This is an error in the initialization system, not in the equations being integrated. The
initialization system solves for the value of every unknown of the model, and of their
derivatives, at the initial time. It needs as many equations as there are such values to
solve for. Initial values given for the model become equations of this system.

The system is unbalanced. There are 2 highest order derivative variables and 1 equations.
More variables than equations, here are the potential extra variable(s):
 inertia₊wˍt(t)

1 more equation is needed to determine the initial state.

Each missing equation is supplied by giving one more initial value, or one more equation
relating initial values. Guesses are starting values for the initialization solve rather
than constraints on it, so adding a guess does not supply one.

Note that variables named with a `ˍt` suffix are derivatives at the initial time: `xˍt` is `D(x)`.

Why a global toggle rather than SciMLLogging

SciMLLogging controls whether a message is emitted and at what level, keyed on a verbosity object threaded through the call. This text is not a message — it is part of an exception's message string, which is always emitted. Routing it through SciMLLogging would mean either logging the remedy separately from the error it explains (so it can be reordered or lost, and is worse for the common case), or using the verbosity object purely as a config carrier, which is not what it is for. It would also add a dependency to ModelingToolkitBase, which has neither SciMLLogging nor Preferences today.

So this is a plain documented setter with process-wide effect, which is the shape that fits a host application setting it once at startup. If MTK does adopt a SciMLLogging verbosity spec later, this is one toggle to fold into it — the call sites read show_api_guidance() and nothing else, so swapping the backing store is a one-line change.

Alternatives, if either is preferred:

  • A keyword argument on the problem constructors, plumbed to InitializationProblem. Explicit rather than global, and DyadInterface has a single setup_prob that would pass it — but it widens every constructor signature for a setting no individual call site wants to vary.
  • Preferences.jl, so it is set in LocalPreferences.toml rather than in code. Would suit a host that controls the environment, at the cost of a new dependency and no runtime override.

Documented under "Error message guidance" on the Debugging page.

@ChrisRackauckas

Copy link
Copy Markdown
Member

Make the global toggle be an experimental thing for now, the documented way will be via SciMLLogging but that hasn't landed yet so we'll want that to be the documented way to handle this, but this is good enough to get it shipped for now.

Verbosity across the SciML ecosystem is moving to SciMLLogging.jl, and this
setting belongs there once ModelingToolkit adopts it. Until then it ships as
experimental and unsupported rather than as documented API, so replacing it
is not a breaking change.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XZQMmzJbrqB9aJomKAB9z
@ChrisRackauckas-Claude

Copy link
Copy Markdown
Member Author

Marked experimental in aade10f.

Both docstrings now carry:

!!! warning "Experimental"

This is experimental and unsupported. It may change or be removed in any release, without a breaking version bump. Verbosity across the SciML ecosystem is moving to SciMLLogging.jl, and this setting is expected to be replaced by an option there once ModelingToolkit adopts it.

and the @docs block is off the Debugging page, so it is not listed as API — the page keeps only the call and the same warning, pointing at SciMLLogging as the supported mechanism to come. Replacing it later is therefore not a breaking change.

The call sites read show_api_guidance() and nothing else, so swapping the backing store for a verbosity option is a one-line change when SciMLLogging lands.

@ChrisRackauckas
ChrisRackauckas marked this pull request as ready for review August 7, 2026 05:59
@ChrisRackauckas
ChrisRackauckas merged commit 5bf5c07 into SciML:master Aug 7, 2026
77 of 93 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.

3 participants