Repository navigation
Let postcondition correctors declare the coordinates they act in (bounded problems) - #1142
Conversation
`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>
23981e5 to
45457b0
Compare
| function PostconditionSpecifier(corrector; space::Symbol = :original) | ||
| space in (:original, :transformed) || throw( |
There was a problem hiding this comment.
This should be EnumX instead of symbols.
There was a problem hiding this comment.
Done in 98afe94 — space is now PostconditionSpace via EnumX:
PostconditionSpecifier(H; space = PostconditionSpace.Transformed)
# default: PostconditionSpace.OriginalPostconditionSpace is exported (and reexported through NonlinearSolve), documented, and covered by the existing conditioning tests.
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>
| u_orig = _from_unbounded.(u, lb, ub) | ||
| u_prev_orig = _from_unbounded.(u_prev, lb, ub) | ||
| post(u_orig, u_prev_orig, p, cache) | ||
| @. u = _to_unbounded(_clamp_to_bounds(u_orig, lb, ub), lb, ub) |
There was a problem hiding this comment.
This allocates the u_prev_orig and u_orig
There was a problem hiding this comment.
Fixed in the latest push. The IIP original-space path was broadcasting into new u_orig / u_prev_orig arrays on every commit (~128 B for a length-1 Float64 state).
It now reuses preallocated BoundedWrapper buffers:
u_cache(already used by residual evaluation) for the mapped proposed iterate- new
u_prev_cachefor the mapped previous iterate
Fail-before / pass-after on the new @allocated test:
BEFORE fix: allocs = 128
AFTER fix: allocs = 0
NONLINEARSOLVE_TEST_GROUP=Core NonlinearSolveBase: 45/45 in the preconditioning suite (including the new allocation assertion).
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>
Follow-up to #1084 (merged). That PR made
postconditioncorrectors compose withlb/ubbounds, but only in the solver's coordinates: bounds are handled by reparametrizing the
iterate to an unconstrained variable, the bounds transform runs after the conditioning
transform, so the corrector was applied to the unconstrained coordinate and the
postcondition_bounds_transformtoggle merely warned about it.That is backwards for the main use case. A limiting rule like SPICE
pnjlimis written fora junction voltage in volts; applied to the logit of a scaled voltage it means nothing,
and a ModelingToolkit-emitted limiter is a statement about the model's own variables. So
which space the corrector acts in is now an exposed option, defaulting to the original
(physical) variable.
The option
PostconditionSpecifier(corrector; space = :original)wraps thepostconditionvaluerather than adding a solve keyword. That is deliberate:
postcondition_space = ...) would have to be registered in SciMLBase'sallowedkeywords— a separate repo and a separate release for one enum. If you wouldrather have the registered keyword, say so and I will open the SciMLBase PR; I did not
add one unilaterally.
corrector once, tag it, and carry it on the problem, and it stays correct wherever the
problem is solved.
unchanged, and
spaceis a type parameter, so the branch resolves at compile time.Default
:original, because that is what a corrector written against the model means andbecause a bare (unwrapped) corrector should do the obviously-correct thing. This changes the
behaviour introduced in #1084 — released in NonlinearSolveBase 2.41.0 but only reachable by
combining
postconditionwithlb/ub, which the docs described as "rarely what a limitingrule means".
What
:originaldoesAt each iterate-commit point, when the cache's problem function is wrapped in a
BoundedWrapper: mapuandu_prevback with_from_unbounded, applyHthere, then_clamp_to_boundsand_to_unboundedforward again. In-place correctors mutate themapped-back buffer and the transformed iterate is written back in place; out-of-place ones
return a new iterate.
The
_clamp_to_boundsstep is load-bearing and was the main hazard: a corrector that clampsexactly onto a bound — the natural thing for a projection — is at ±Inf under the logit. It
gets the same interior nudge (relative
eps^(3/4)) the bounds transform already applies tou0, soHfloor!(up, ...) = (up[1] = lb[1])commits a finite iterate that maps back to thebound to ~1e-11 instead of an
Inf.The initial-guess correction now also runs on bounded problems in
:originalmode —u0isstill in the original coordinates when
transform_conditioned_problemsees it, so there isnothing to map. It stays skipped for
:transformed, which is now the only case that reportsthe
postcondition_bounds_transformtoggle (message reworded: it no longer fires on thedefault path, where nothing surprising happens).
While in here,
_apply_postcondition!!now dispatches on the in-place convention instead ofbranching on a
Bool, matching what #1133 did forevaluate_f. Without it the unusedconvention's return type leaked into the caller:
apply_postcondition!!inferredUnion{Vector{Float64}, Vector{Union{}}}at thecache.u = ...assignment. It now infersVector{Float64}, with 0 allocations on unbounded problems (unchanged) and 160 bytes perapplication on bounded ones (the two mapped-back temporaries). There is an
@inferredregression test.
Docs / versions
docs/src/tutorials/nonlinear_preconditioning.md: new "Correctors on bounded problems"subsection with a runnable side-by-side of the two spaces, plus the skipped initial guess
and the on-a-bound nudge. Also fixes two stale 3-argument corrector signatures at the top
of the page (they became 4-argument in Nonlinear preconditioning hooks: precondition (G) / postcondition (H) with PCNR limiting support #1084).
docs/src/basics/solve.md:@docsentry forPostconditionSpecifier— required by the QAapi-docs check for a new public name — and the
solvedocstring keyword list.NonlinearSolveFirstOrder 2.3.0 → 2.3.1, with their
NonlinearSolveBasecompat moved to"2.42".
Local test results (Julia 1.12.4)
Runic
--checkexit 0 on every changed Julia file.(the whole group: 36 testsets, 2412 passing assertions, 0 failures —
TrustRegion1176/1176,NewtonRaphson309/309, etc.)Every test file that touches the corrector path, run against this branch's working tree
(66 tests, all passing):
covering
lib/NonlinearSolveFirstOrder/test/conditioning_tests__item{1,2}.jl,lib/NonlinearSolveQuasiNewton/test/core_tests__item10.jl,lib/NonlinearSolveSpectralMethods/test/core_tests__item6.jl,lib/SimpleNonlinearSolve/test/core/conditioning_tests__item1.jl,lib/NonlinearSolveBase/test/bounds_transform.jlandtest/Core/core_tests__item23.jl.test/Core/bounds_tests__item{1,2,4}.jlalso pass (41/41).The new tutorial block was run as Documenter would:
Three red CI checks inherited from
master, not from this PR.NONLINEARSOLVE_TEST_GROUP=QAfails because nine names publicized by #1084 lack docstrings /
@docsentries, and Spell Checkfails on the deliberate
postcondtionmisspelling in the keyword-rejection test that came inwith the same PR (correcting it would invert the assertion); #1141 fixes both. `Downgrade Tests
fails on master too (runs on 4bd97b77f and 4aad48784, both predating this branch) withUndefVarError: issquare not definedunder downgraded dependencies — that one is #1143 (the downgrade-pinned LinearSolve 5.0.1's ForwardDiff extension uses a bareissquarethat only resolved while SciMLBase re-exported SciMLOperators; #1084'sSciMLBase = "3.43"floor is what exposed it). None of the three involves anything this PR adds:PostconditionSpecifierhas a docstring and a rendered@docsentry, Aqua's unbound-type-parameter check passes with the{space, F}` signature, and the one newspelling finding this branch did introduce ("reparametrization") is fixed here.
Version note. Two sibling PRs bump the same files from the same base: #1141 takes
lib/NonlinearSolveBase/Project.tomlto 2.41.1, and #1143 takes the umbrellaProject.tomlto4.25.1. Whichever lands last needs a one-line rebase on the version field. This PR keeps
NonlinearSolveBase at 2.42.0 and the umbrella at 4.26.0, since it adds public API.
New tests
lib/NonlinearSolveBase/test/conditioning.jl:PostconditionSpecifierconstruction,default/rejected
space, transparency of the wrapper; the initial guess corrected under:originaland skipped under:transformed; and a commit-point testset asserting that thesame clamp lands on the physical value under
:originaland on the transformedcoordinate under
:transformed, thatu_prevalso arrives in the original variable, thatclamping onto a bound stays finite, and that the mapped path is
@inferred.lib/NonlinearSolveFirstOrder/test/conditioning_tests__item2.jl: end-to-end —pnjlimlimiting on the bounded diode circuit now converges to the same root; a component pinned to
1.0lands on1.0under:originaland onlb + (ub - lb) * logistic(1) = 7.31under:transformed; a corrector pinning exactly tolbstays finite; plus the out-of-placebounded form.
test/Core/core_tests__item23.jl: the same through the default polyalgorithm, and thereexport of
PostconditionSpecifierfrom the umbrella package.🤖 Generated with Claude Code
https://claude.ai/code/session_01B9GHoo4pFa3DU9yhyTkevL