Skip to content

Let postcondition correctors declare the coordinates they act in - #1148

Closed
ChrisRackauckas-Claude wants to merge 5 commits into
SciML:masterfrom
ChrisRackauckas-Claude:postcondition-transform-space
Closed

ChrisRackauckas-Claude wants to merge 5 commits into
SciML:masterfrom
ChrisRackauckas-Claude:postcondition-transform-space

Conversation

@ChrisRackauckas-Claude

Copy link
Copy Markdown
Member

Important

This PR should be ignored until reviewed by @ChrisRackauckas.

Follow-up to #1084, which added the precondition/postcondition solver options.

The problem

lb/ub bounds are handled by reparametrizing the iterate to an unconstrained variable, and that transform runs after the conditioning transform — so an iterate corrector was applied to the unconstrained coordinate. A limiting rule like SPICE pnjlim is written for a junction voltage in volts; applied to the logit of a scaled voltage it means nothing. #1084 only warned about this.

The change

Which space the corrector acts in is now the user's choice, defaulting to the problem's own variable:

solve(prob, NewtonRaphson(); postcondition = H)                                        # :original (default)
solve(prob, NewtonRaphson(); postcondition = PostconditionSpecifier(H; space = :transformed))
  • No new SciMLBase keyword. PostconditionSpecifier wraps the corrector value rather than adding a solve keyword, so allowedkeywords in SciMLBase is untouched and this stays a one-repo change. A bare corrector means :original.
  • :original maps each committed iterate back through the bounds transform, applies H in the user's coordinates, and maps it forward again.
  • The ±Inf hazard is handled. A correction landing exactly on a bound is at infinity under the logit, so _clamp_to_bounds nudges it into the interior before the inverse map. Clamping onto a bound is exactly what a limiting/projection corrector does, so this is the common case, not a corner case.
  • The initial-guess correction now runs on bounded problems under :original (u0 is still in original coordinates at that point). It stays skipped for :transformed, which is now also the only case that reports the postcondition_bounds_transform verbosity toggle — the warning fires only when the corrector really is acting in transformed coordinates.

Unbounded problems are unaffected: with no lb/ub there is no transform and both spaces coincide.

Why :original is the default

Correctors are written against physical quantities — a junction voltage, a concentration, a saturation — and every generated corrector we produce is of that kind (ModelingToolkit's limited(...) operator compiles limiters for model variables). Defaulting to the coordinate the user wrote the rule in is the least surprising behaviour; :transformed remains available for correctors deliberately expressed in solver coordinates.

Verification (local, Julia 1.12.4, registry NonlinearSolveBase 2.41.0 base)

All conditioning tests pass, and the suite grew where the new behaviour needed cover:

Test file Assertions
lib/NonlinearSolveBase/test/conditioning.jl 44 (was 28)
lib/NonlinearSolveFirstOrder/test/conditioning_tests__item1.jl 11
lib/NonlinearSolveFirstOrder/test/conditioning_tests__item2.jl 27 (was 18)
lib/NonlinearSolveQuasiNewton/test/core_tests__item10.jl 4
lib/NonlinearSolveSpectralMethods/test/core_tests__item6.jl 2
lib/SimpleNonlinearSolve/test/core/conditioning_tests__item1.jl 5
test/Core/core_tests__item23.jl 9 (was 5)

The load-bearing test is a clamping corrector combined with bounds, asserting the clamp lands on the intended physical value under :original and on a different value under :transformed.

Note on CI

Spell Check on this branch is red for a reason inherited from #1084 and unrelated to this change: the typo-rejection test deliberately misspells a keyword (postcondtion) to assert the keyword whitelist rejects it. #1141 fixes that with a narrow typos-config entry. This branch will go green once that lands or is rebased in.

🤖 Generated with Claude Code

https://claude.ai/code/session_01B9GHoo4pFa3DU9yhyTkevL

`lb`/`ub` bounds are handled by reparametrizing the iterate to an unconstrained
variable, and that transform runs after the conditioning transform, so an iterate
corrector was applied to the unconstrained coordinate. A limiting rule like SPICE
`pnjlim` is written for a junction voltage in volts; applied to the logit of a
scaled voltage it means nothing. Which space the corrector acts in is now the
user's choice, defaulting to the problem's own variable.

- `PostconditionSpecifier(H; space = :original | :transformed)` wraps the
  corrector value instead of adding a solve keyword, so no new SciMLBase
  `allowedkeywords` entry is needed. A bare corrector is `:original`.
- `:original` maps each committed iterate back through the bounds transform,
  applies `H` there, and maps it forward again. A correction landing exactly on a
  bound is at infinity under the logit, so `_clamp_to_bounds` nudges it into the
  interior before the inverse map.
- The initial-guess correction now runs on bounded problems in `:original` (`u0`
  is still in the original coordinates at that point). It stays skipped for
  `:transformed`, which is also the only case that reports the
  `postcondition_bounds_transform` verbosity toggle now.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Address review feedback: declare the postcondition coordinate space with
EnumX (`PostconditionSpace.Original` / `PostconditionSpace.Transformed`)
rather than bare symbols.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
The original-space bounded corrector path was broadcasting into new
`u_orig`/`u_prev_orig` arrays on every commit. Reuse the
`BoundedWrapper` temps (add a second `u_prev_cache`) so the in-place
path is allocation-free.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Default is Original space, which matches user expectations, and
Transformed is an explicit opt-in, so warning about it is unnecessary.
Keep skipping the initial-guess correction for Transformed + bounds.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
@ChrisRackauckas-Claude

Copy link
Copy Markdown
Member Author

Closing as a duplicate — my error. This work is already on master via #1142 (merged 2026-08-07, c6922fcb), including the follow-up commits that switched PostconditionSpace to EnumX, removed the now-redundant postcondition_bounds_transform toggle, and fixed the in-place allocation. I opened this without realising #1142 existed because I searched only is:open PRs and so never saw the merged one.

The only diff between this branch and master is a one-line BracketingNonlinearSolve version bump that arrived via an unrelated master merge. Nothing here needs to land.

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