Repository navigation
Add the limited(actual, limiter) operator: symbolic PCNR iterate limiting for nonlinear solves - #4774
Conversation
|
Verification detail (local, Julia 1.12.4). The full What was run instead, in a coherent dev environment (SciMLBase#1449 + NonlinearSolve#1084 + this branch):
Also verified the operator's derivative rules ( 🤖 Generated with Claude Code |
|
CI triage (first full run): Runic Format Check and Spell Check pass. All 83 failing jobs (tests/sublibrary-ci/Downgrade/Documentation/Benchmarks/Catalyst downstream) fail at dependency resolution with Release order for green CI here: SciMLBase#1449 → tag 3.37.0 → re-run (tests pass with limiting inert) → NonlinearSolve#1084 + release → hook-gated solver-behavior assertions activate. 🤖 Generated with Claude Code |
| Return `true` iff `sys` contains a `limited(...)` node anywhere in its equations or its | ||
| observed equations. | ||
| """ | ||
| function has_any_limited(sys) |
There was a problem hiding this comment.
This should use https://github.com/JuliaSymbolics/SymbolicUtils.jl/blob/master/src/irstructure.jl#L659 to have any hope at scaling to Multibody models. Naive recursion on an expression is strictly off the table in such cases.
| Recursively replace every `limited(actual, limiter)` node in the unwrapped expression `x` | ||
| with `actual`, discarding the limiter (and with it any `limitnew`/`limitold` sentinels). | ||
| """ | ||
| function _strip_limited(x) |
There was a problem hiding this comment.
In a similar fashion, this should use IRStructure, though we don't have an inbuilt utility for this. An alternative formulation is to propagate a cache::Base.IdDict{SymbolicT, SymbolicT} throughout the recursion, populate it with the result of maketerm and check it at the beginning to avoid duplicating work.
| return filter(v -> _sentinel_kind(v) === :none, vars) | ||
| end | ||
|
|
||
| function _canonicalize_sentinels(x) |
There was a problem hiding this comment.
Similar cache required.
|
|
||
| # Replace every node in `x` that appears as a key of `repl` (an ordered | ||
| # node => variable map keyed by `isequal`) with its replacement. | ||
| function _replace_limited_nodes(x, repl) |
There was a problem hiding this comment.
Isn't this just substitute? It should use IRSubstituter.
| return maketerm(typeof(x), operation(x), new_args, metadata(x)) | ||
| end | ||
|
|
||
| function _collect_limited_nodes!(nodes, x) |
There was a problem hiding this comment.
This is just search_variables!. It should use the IRStructureSearchBuffer version.
| for v in Iterators.flatten((unknowns(sys), parameters(sys; initial_parameters = true))) | ||
| if hasname(v) && getname(v) in reserved | ||
| throw( | ||
| ArgumentError( |
There was a problem hiding this comment.
Can't the lowering use Unicode or a name with # in it?
| # parameters only; referencing other unknowns is not supported since the hook | ||
| # sees the limited entries, not the whole state. | ||
| bad = [ | ||
| v for v in unwrap.(Symbolics.get_variables(lexpr)) |
There was a problem hiding this comment.
Using IRStructureSearchBuffer and get_variables! here will be faster.
| lims = Tuple(lims) | ||
| if iip | ||
| return let lims = lims | ||
| function limited_postcondition_iip(up, uprev, p) |
There was a problem hiding this comment.
Can these closures be made callable structs so we don't accidentally forget to add something to the let block later?
| ) | ||
| ) | ||
| end | ||
| fn = generate_custom_function( |
There was a problem hiding this comment.
Do we want to FunctionWrapper these somehow to cut down on compile times?
|
Thanks @AayushSabharwal — all addressed in 204a5ce. Every traversal now goes through the IR machinery rather than naive recursion:
On the last one — FunctionWrapper for the compiled limiters: I left them unwrapped for now, because I don't think there's a signature that survives the use sites. The limiter is called as Re-verified after the rewrite: 🤖 Generated with Claude Code |
|
Updated for the SciMLBase/NonlinearSolve redesign (commit For this PR the change is small and strictly an improvement: the corrector compiled from the limiter registry is merged into the problem's keywords by
Re-verified after the change: 🤖 Generated with Claude Code |
|
| limited | unannotated | |
|---|---|---|
dt = 1e-6, tspan = 20dt |
v = 0.0, ConvergenceFailure |
v = -5.1e16, "Success" |
adaptive to t = 1e-5 |
nf = 652, v = 0.6698509496766557 |
nf = 661, v = 0.6698509496766557 |
The first row is the limiter doing its job — the unannotated stage Newton walks off into
exp(v/Vt) and returns garbage, the limited one refuses to accept the step. The second is
the honest caveat: on a smoothly stepped RC ramp the limiter almost never binds, so it buys
~1% of f-evaluations, not the 179 → 11 iteration collapse the DC operating point shows. The
value here is robustness on hard turn-ons, not speed on easy ones.
|
The discussions from before miss that limiting is a function of operators, not variables or even equations. You don't want to limit a full equation, and the choice of limiter can be operator-specific. So doing this based on variable bounds is just a bad approach because you don't necessarily know the function form to do with that. Even if we do something more automatic, I think we want this manual form and the automatic form just lowers to this, so it still needs to be exposed. |
|
2eae0d0 to
de05533
Compare
|
Rebased onto current master (
Dependencies are all released now, so this PR no longer depends on anything unmerged: SciMLBase 3.43 (#1449), NonlinearSolveBase 2.41 (SciML/NonlinearSolve.jl#1084, #1142), and SciML/OrdinaryDiffEq.jl#4158 for the stage-solve forwarding. Verified locally after the rebase (Julia 1.12.4):
The MTK-visible run is the one that matters for this rebase: the 🤖 Generated with Claude Code |
| observed equations. | ||
| """ | ||
| function has_any_limited(sys) | ||
| has_limited_in_equations(equations(sys)) && return true |
There was a problem hiding this comment.
This still doesn't use IRStructure. It will not scale, I had to refactor the homotopy operator to make Multibody models stop stalling. Use the new implementation of the homotopy operator to implement this. It has a very similar template of functions.
| return [Equation(subber(unwrap(eq.lhs)), subber(unwrap(eq.rhs))) for eq in eqs] | ||
| end | ||
|
|
||
| # The `limitnew`/`limitold` sentinels appear inside limiter arguments, so `System`'s |
There was a problem hiding this comment.
Do we want limitnew/limitold to be namespaced? If we don't, it can easily be avoided by making them GlobalScoped.
| for s in sentinels | ||
| rules[s] = _sentinel_kind(s) === :new ? LIMIT_NEW : LIMIT_OLD | ||
| end | ||
| return SU.IRSubstituter{false}(ir, rules)(x) |
There was a problem hiding this comment.
This really shouldn't build and call the substituter every time. There should be a method that returns the relevant IRSubstituter and it should then be reused.
| ) | ||
| ) | ||
| end | ||
| b = unwrap(substitute(q, Dict(z => 0))) |
There was a problem hiding this comment.
IRSubstituter should be used here.
de05533 to
eaff1c4
Compare
| predicate and as an `is_atomic` for `search_variables!`, which then collects the | ||
| `limited` nodes themselves rather than the variables inside them. | ||
| """ | ||
| struct IsLimitedNode end |
There was a problem hiding this comment.
This should just be a normal function instead of a singleton callable struct.
|
|
||
| Return `true` iff `expr` contains at least one `limited(...)` node. | ||
| """ | ||
| function has_limited(expr) |
There was a problem hiding this comment.
Should use the IRStructure method of query.
eaff1c4 to
dcd0a68
Compare
…ting Adds symbolic, component-level declaration of SPICE-style iterate limiting, lowered to the Predictor/Corrector Newton-Raphson (PCNR) formulation of Aadithya, Keiter & Mei. Mirrors the Modelica homotopy(actual, simplified) operator: limited(actual, limiter) stays opaque through System construction, and mtkcompile lowers it. - Time-independent systems: an auxiliary irreducible unknown per limited quantity, a consistency equation, and the limiter registry in LimitedCtx metadata. NonlinearProblem construction compiles the registry into a postcondition corrector attached to the problem's keywords, so it is forwarded to solve/init like any other solver option. - Time-dependent systems: the operator is stripped, but the registry is kept in StageLimitedCtx so that nlstep problems can re-attach it. For those, the stage substitution makes each limited quantity affine in a stage unknown, so the corrector is the limiter conjugated by that map and no augmentation is needed. limitold is then the previous Newton iterate of the current stage. - bounds metadata is delivered to nlstep stage solves the same way, as a clamping corrector conjugated into stage coordinates, automatically. The coefficients are stage-parameter expressions read live, so the enforced box tracks the stepper, which a static lb/ub vector cannot. The stage problem is built with explicit lb/ub of nothing so state boxes are never attached to stage increments, which required a DeriveBounds() sentinel to distinguish 'not supplied' from an explicit nothing. Measured on the classic Vs-R-diode DC circuit with pnjlim: plain NewtonRaphson 179 steps, with limited 11 steps, both flat and through hierarchical namespaced components. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Agent-Harness: Claude Code Agent-Model: claude-fable-5-1 Agent-Session: https://claude.ai/code/session_01QnF4Nc4VpRrSjbbBTgeaUb
…e corrector Addresses the review on SciML#4774. `has_limited` now takes the `IRStructure` method of `SymbolicUtils.query`, following the template the `homotopy` operator was refactored to. The naive-recursion version was reachable from `has_any_limited`, which runs on *every* `ODEFunction`/ `NonlinearFunction` construction, so on a model with heavily shared subexpressions it re-walked the same subtrees once per path that reached them. The `limitnew`/`limitold` sentinels are `GlobalScope`d, so a limiter written inside a component still refers to the two toplevel symbols after flattening. That deletes the name-suffix recognition (`_sentinel_kind`, `IsSentinel`) and the per-call `_canonicalize_sentinels` substituter along with it; dropping the sentinels from the discovered variables is now an identity test. `attach_stage_limiters` builds one `IRSubstituter` for the whole pass instead of a fresh `substitute` call per limited quantity, and gets the affine coefficients from a cached `Symbolics.LinearExpander` (`strict`, so a stage unknown buried in an `ifelse` condition is not mistaken for an affine one) rather than a derivative plus a second substitution. `_conjugate_limiter` uses the IR substituter too. `generate_limited_postcondition` composes correctors symbolically and emits a *single* generated function for the whole registry, rather than one per limiter held in a heterogeneous tuple. The corrector is now concretely typed and its compile time no longer grows with the number of limited quantities in a device library — which is the concern behind the `FunctionWrapper` question, without giving up the generic value type that lets ForwardDiff `Dual`s flow through the hook. `IsLimitedNode` is a plain function. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Agent-Harness: Claude Code Agent-Model: claude-opus-5[1m] Agent-Session: https://claude.ai/code/session_01T2Y8c85c7AiQHEjK9KLzdA Claude-Session: https://claude.ai/code/session_01T2Y8c85c7AiQHEjK9KLzdA
Three separate breakages, all in the test/compat plumbing rather than the code: 1. `tests / InterfaceII` (root package) errored with `Package NonlinearSolveBase not found in current path`. `lib/ModelingToolkitBase/test/limited_operator.jl` imports it and is registered in the root suite too, but the root `Project.toml` never listed it. Added to `[extras]`, `[compat]` and the test target. 2. `downgrade-mtkbase (InterfaceII)` failed with 179 Newton steps instead of 11 and `calls[] == 0`: at the declared floors the resolver picked NonlinearSolveBase 2.38.1 and NonlinearSolveFirstOrder 2.2.4, neither of which knows about `postcondition`, so the corrector was silently never applied. `get_postcondition` first exists in NonlinearSolveBase 2.41 and `supports_postcondition` in NonlinearSolveFirstOrder 2.3; NonlinearSolveFirstOrder is not a declared dependency, so the floor has to be carried by NonlinearSolve, whose first version forcing NonlinearSolveFirstOrder past 2.3 is 4.29.1 — which in turn pins NonlinearSolveBase to at least 2.45. `OrdinaryDiffEqNonlinearSolve` gets 2.8, the first version that forwards the stage problem's keywords to the stage solve. The four floors are mutually satisfiable, which matters because the downgrade job pins each to `=floor`. 3. `Aqua / all_qualified_accesses_are_public` flagged `Base.setindex`, the non-mutating array update the out-of-place corrector needs for an immutable iterate. Base declares no public spelling for it, so it joins the existing Base entries in the ignore list rather than being routed around. The one QA failure that remains locally (`JET report_package`, 234 reports, all Moshi `@match` temporaries) reproduces identically on master at the rebase base and is tracked in SciML#5063. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Agent-Harness: Claude Code Agent-Model: claude-opus-5[1m] Agent-Session: https://claude.ai/code/session_01T2Y8c85c7AiQHEjK9KLzdA Claude-Session: https://claude.ai/code/session_01T2Y8c85c7AiQHEjK9KLzdA
…s against `docs/src/basics/Limiting.md` builds a limited `NonlinearProblem` and an `nlstep` `ODEProblem`, both of which need the `postcondition` machinery: NonlinearSolve 4.29.1 (the first version pulling a NonlinearSolveFirstOrder that applies the corrector) and OrdinaryDiffEqNonlinearSolve 2.8 (the first that forwards the stage problem's keywords). At the old floors the page would still build, but its two headline numbers would silently be the unlimited ones. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Agent-Harness: Claude Code Agent-Model: claude-opus-5[1m] Agent-Session: https://claude.ai/code/session_01T2Y8c85c7AiQHEjK9KLzdA Claude-Session: https://claude.ai/code/session_01T2Y8c85c7AiQHEjK9KLzdA
dcd0a68 to
9875986
Compare
|
@AayushSabharwal — rebased onto
On the last one. A What was actually costing compile time is that there was one generated function per limiter, held in a heterogeneous tuple. Separately, the first full CI run exposed three plumbing breakages, all fixed here (details and the failing-before evidence are in the PR body):
The one QA failure that remains locally ( 🤖 Generated with Claude Code (model: claude-opus-5[1m]) |
…p one floor **Downstream.** Every `IntegrationTest` job that pulls ModelingToolkitBase failed at `Pkg.develop`, not in a test: ``` Unsatisfiable requirements detected for package ModelingToolkitBase [7771a370]: ├─possible versions are: 1.0.0 - 1.69.1 or uninstalled └─restricted to versions 1.70.0 - 1 by ModelingToolkit [961ee093] — no versions left ``` The job developed the root first and the `lib/*` sublibraries second. The first call resolves the root alone, so it has to find ModelingToolkitBase 1.70.0 in the registry — which does not have it yet — and dies before the second call can supply it from the checkout. `[sources]` does not help: Pkg honours it only for the active project, not for a path-developed dependency. This is not specific to this PR; it breaks any PR that bumps a sublibrary version together with the root's floor on it. Both develops are now one call, so the root and the sublibraries resolve together. **One floor reverted.** `downgrade-scicompdsl` became unsatisfiable: that lane pins `OrdinaryDiffEqNonlinearSolve = 2.6.0` from `lib/SciCompDSL`'s own floors, computed without the root's test-target compat, and then the root's `2.8` floor arrives through the developed ModelingToolkit and leaves nothing. The root's copy of this floor buys no enforcement — the root package has no InterfaceII downgrade lane, and its test jobs always resolve the newest versions — so it goes back to master's `"2"`. The floor that is actually enforced lives in `lib/ModelingToolkitBase/Project.toml`, whose `downgrade-mtkbase (InterfaceII)` lane now passes. The `NonlinearSolve` and `NonlinearSolveBase` floors stay: the root `Downgrade` lane is green with them. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Agent-Harness: Claude Code Agent-Model: claude-opus-5[1m] Agent-Session: https://claude.ai/code/session_01T2Y8c85c7AiQHEjK9KLzdA Claude-Session: https://claude.ai/code/session_01T2Y8c85c7AiQHEjK9KLzdA
…em` constructors (#5110) * Add `adtype` passthrough to OptimizationFunction/OptimizationProblem `OptimizationFunction{iip}(sys, ...)` hardcoded `ad = SciMLBase.NoAD()`, so there was no way to select an AD backend when building an optimization problem from a `System`. Add an `adtype` keyword (default `NoAD()`) to the `OptimizationFunction` constructors, a positional `OptimizationFunction{iip} (sys, adtype)` method matching the SciMLBase signature, and let `adtype` flow through `OptimizationProblem(sys, op; adtype = ...)` to the function constructor like `grad`/`hess`. `Optimization.jl`'s `instantiate_function` dispatches on `f.adtype`, so this selects solver-side AD while `grad`/`hess` keep controlling symbolic derivative generation independently. Promotes ADTypes from a test-only extra to a real dependency of ModelingToolkitBase (it is already unconditionally loaded via SciMLBase and has no dependencies of its own) so the `adtype` annotation can use the public `ADTypes.AbstractADType` spelling. Motivated by SciML/NeuralPDE.jl#1161 Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> Agent-Harness: Devin CLI 3000.10.21 Agent-Model: SWE-2 High Agent-Session: local Devin CLI session (no shareable URL) * Remove ADTypes from the test target now that it is a real dependency `Pkg.test` requires `[targets] test` entries to resolve through `[extras]`/`[weakdeps]`; since ADTypes moved to `[deps]` it must not remain in the test target, or test-env resolution fails before any tests run. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> Agent-Harness: Devin CLI 3000.10.21 Agent-Model: SWE-2 High Agent-Session: local Devin CLI session (no shareable URL) --------- Co-authored-by: ChrisRackauckas-Claude <accounts@chrisrackauckas.com> Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: ChrisRackauckas-Claude <noreply@github.com>
#5116) Three recently merged PRs widened ModelingToolkit's surface without updating the two version-controlled QA audit lists in `test/qa/qa.jl`, which is what those lists exist to catch: - #4774 (`limited` operator) exports `limited`, `limitnew`, `limitold` from ModelingToolkitBase and reaches for `MTKBase.LimitedCtx`, `MTKBase.apply_limited_lowering` and `MTKBase.attach_stage_limiters` from the root package. - #5111 exports `constraints_to_penalties` from ModelingToolkitBase. The four exported names are documented public API of ModelingToolkitBase with rendered `@docs` entries (`docs/src/basics/Limiting.md`, `docs/src/API/problems.md`), so they are added to `REEXPORTED_API`. The three qualified accesses are MTK/MTKBase shared implementation seams -- a metadata registry key and two lowering entry points populated by the compiler, not anything a user sets -- so they join `lower_homotopy` and `has_any_homotopy`, the directly analogous names from the `homotopy` operator, in `NONPUBLIC_QUALIFIED_ACCESSES`. Agent-Harness: Claude Code 2.0.14 Agent-Model: claude-opus-5[1m] Agent-Session: https://claude.ai/code/session_01FeJYXni9MkJhPFA9yxYvfn Claude-Session: https://claude.ai/code/session_01FeJYXni9MkJhPFA9yxYvfn Co-authored-by: ChrisRackauckas-Claude <accounts@chrisrackauckas.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Note
Rebased 2026-09-10 onto
bf1efdb992, and the outstanding review is addressed — see the comment below for the per-thread mapping. Versions bumped to ModelingToolkit 11.43.0 / ModelingToolkitBase 1.70.0.Since the last push the traversals were rewritten onto
IRStructure(thehomotopyoperator's template), thelimitnew/limitoldsentinels becameGlobalScoped — which deletes the name-suffix canonicalization machinery outright — and the corrector now compiles to a single generated function instead of one per limiter.The first CI run also exposed three plumbing breakages, all fixed here: a missing
NonlinearSolveBasetest dependency on the root package,postconditionfloors that were too low for the downgrade job (so limiting was silently inert there — 179 Newton steps instead of 11), and anExplicitImportsfinding onBase.setindex.Important
This PR should be ignored until reviewed by @ChrisRackauckas.
Summary
Adds the
limited(actual, limiter)operator: symbolic, component-level declaration of SPICE-style iterate limiting, lowered automatically to the Predictor/Corrector Newton-Raphson (PCNR) formulation of Aadithya, Keiter & Mei. A device author annotates a sensitive quantity once —— and every nonlinear solve built from any model containing that component gets predictor/corrector limiting fully automatically:
mtkcompileperforms the PCNR augmentation andNonlinearProblemconstruction compiles the limiters into thepostconditioncorrector hook the problem carries.The operator follows the architecture of the Modelica
homotopy(actual, simplified)operator (#4601): the actual expression first, the helper second;limitnew/limitoldare reserved placeholder symbols (fixed sentinels, precompile-safe likeHOMOTOPY_LAMBDA) for the proposed and previously-accepted values inside the limiter expression.What the lowering does
For time-independent systems (in both the ModelingToolkitBase light pipeline and the ModelingToolkit structural pipeline, inserted post-
expand_connections):limited(actual, limiter)node gets an auxiliary irreducible unknownlimited_k; the node is replaced by it and the consistency equationlimited_k ~ actualis appended (EquationSourceInformationis padded accordingly forTearingState);limited_kis irreducible, alias elimination keeps the limited quantity as the surviving representative of its alias class — the PCNR augmentation is reduced symbolically, at no runtime cost;limited_kreceivesactualas both default and guess, so no user-provided initial value is needed;LimitedCtxsystem metadata; problem construction compiles the whole registry with onegenerate_custom_functioncall (parameters, including bound parameters, resolve through standard codegen) and merges it into the problem's keywords aspostcondition, so it rides tosolve/initlike any other solver option and an explicitly passed corrector wins over the model-declared one.For time-dependent systems the operator is stripped to
actualduringmtkcompile, so component libraries carrying limiters compile unchanged for transient simulation — but the stripped limiters are remembered inStageLimitedCtxmetadata, and anODEProblembuilt withnlstep = truere-attaches them to the implicit stage system, conjugated by the affine stage substitutionq = a*z + bintoφ(znew, zold) = (L(a*znew + b, a*zold + b) - b) / a. Un-loweredlimitednodes are rejected with clear errors atNonlinearFunction/ODEFunctionconstruction; nestedlimitedand limiters referencing other unknowns are rejected likewise.The same conjugation delivers the
boundsmetadata of an ODE state to the stage solve as a per-stage clamp. That fixes a pre-existing bug in passing:generate_ODENLStepDataused to let a state's staticlb/ubland on the stage problem, whose unknowns are the incrementsz— the wrong variable, and enough to put the problem into NonlinearSolve's transformed coordinates that the raw stage values the stepper writes back do not live in.Changes
1.69.1 → 1.70.0): newsrc/systems/limited_operator.jl(operator,GlobalScoped sentinels, IR-backed detection/strip/lowering passes,attach_stage_limiters,generate_limited_postcondition); lowering invoked from__mtkcompile;postconditionmerged into the problem keywords with guards;DeriveBoundssentinel solb = nothing, ub = nothingis distinguishable from "not supplied";nlstep_limit_boundsthreaded throughODEFunction; exportslimited,limitnew,limitold; tests intest/limited_operator.jl(registered in the InterfaceII group).11.42.1 → 11.43.0): lowering invoked from the structural__mtkcompile(with source-info padding);inner_nlsystemattaches the stage limiters;nlstep_scc = truerejects declared limiters rather than silently dropping them.docs/src/basics/Limiting.md(registered inpages.jl) with a component-based diode-circuit walkthrough, thenlstepstory, lowering semantics, and contracts.NonlinearSolveBaseadded to the root package's test target;postcondition-era floors for NonlinearSolve / NonlinearSolveBase / OrdinaryDiffEqNonlinearSolve in bothProject.tomls anddocs/Project.toml;Base.setindexadded to theall_qualified_accesses_are_publicignore list.Verification (all local, Julia 1.12.7, registered dependencies — nothing unmerged is needed)
lib/ModelingToolkitBase/test/limited_operator.jlwith ModelingToolkit loaded (CI'sgroup_interfaceii.jlpath)GROUP=InterfaceII julia --project -e 'using Pkg; Pkg.test()'Testing ModelingToolkit tests passedlib/ModelingToolkitBaseGROUP=QAbf1efdb992(JET report_package, 234 reports, every one a Moshi@matchtemporary; tracked in #5063). The extra error the previous run had here (Base.setindexis not public) is gone.homotopy_lowering.jl(shares the touched lowering pipeline)limited_operator.jl66/66 (the nlstep testsets gate off without ModelingToolkit, as indowngrade-mtkbase). Before the floor bump this run gave 179 Newton steps instead of 11 andcalls[] == 0.@example limitingblocks ofdocs/src/basics/Limiting.md, extracted and executedrunic --checkandtyposover every changed fileNot verified locally: the full docs build (only the new page's example blocks were executed), the Julia
lts/prejobs, GPU and downstream jobs.Things a reviewer should push back on
limitedquantity that is not is anArgumentError; abounds-derived clamp that is not is silently dropped. That asymmetry is deliberate — the user asked for the first and not the second — but it is a judgment call.boundsclamping on the stage solve is on by default (nlstep_limit_bounds = true). The argument is that clamping an intermediate Newton iterate cannot move the root, only the path, and it keepslog/sqrt/expright-hand sides in domain. If you disagree, the default should flip.NonlinearSolveandOrdinaryDiffEqNonlinearSolvefloors were raised (test/docs targets only) rather than the tests being made tolerant of an older stack.inner_dae_nlsystem) deliberately gets neither limiting nor a derived box, preserving its pre-limitedbehaviour. Extending it is untested work left out on purpose.Design notes
System(eqs, t)constructor's function-of-tvalidation accepts equations containing them, andGlobalScoped so a limiter written inside a component still refers to the two toplevel symbols after flattening; both lowering paths drop them from the discovered unknowns/parameters by identity.IRStructure—SU.queryfor detection,search_variables!with anis_atomicpredicate for node collection,IRSubstituterfor every rewrite — following the template thehomotopyoperator was refactored onto. This matters becausehas_any_limitedruns on everyODEFunction/NonlinearFunctionconstruction.var_to_name, and crucially theirreduciblesfield, which structural simplification consults rather than per-variable metadata — found the hard way).∂₁ = 1,∂₂ = 0: Jacobians treat the operator asactual, matching the hook semantics (the limiter is a corrector, not part of the residual).Closes the ModelingToolkit third of the SciML/NonlinearSolve.jl#351 + PCNR feature arc.
🤖 Generated with Claude Code
https://claude.ai/code/session_01T2Y8c85c7AiQHEjK9KLzdA