Skip to content

Commit 923e6a5

Browse files
BatchZeroChrisRackauckasAayushSabharwal
authored
Add the homotopy operator and HomotopyProblem construction (#4601)
* Add the homotopy operator and HomotopyProblem construction Implement the `homotopy(actual, simplified)` operator as a general nonlinear-solving construct: a `System` whose equations carry `homotopy` annotations is lowered into a `SciMLBase.HomotopyProblem` whose residual is the convex blend `(1 - λ)*simplified + λ*actual`, compiled as `f(u, p, λ)` and solved by natural-parameter continuation (`NonlinearSolveBase.HomotopySweep`). - systems/homotopy_operator.jl: `@register_symbolic homotopy` with the numeric fallback `homotopy(a, s) = a`; nodewise derivatives kept as opaque `homotopy(1, 0)`/`homotopy(0, 1)` nodes so jacobians and index reduction stay consistent under the blend; the `has_*homotopy` detection family; the fixed continuation sentinel `HOMOTOPY_LAMBDA` (not `gensym`, to keep codegen precompile-stable) with a collision guard; the structure-preserving `lower_homotopy` shadow pass; and `generate_homotopy_residual`. - problems/homotopyproblem.jl: `SciMLBase.HomotopyProblem(sys, op)` builds the standard residual to obtain `u0`/`p`/observed, then swaps in the swept `f(u, p, λ)`. `λ` is a trailing argument, never added to `p`. - problems/nonlinearproblem.jl: `SciMLBase.AbstractNonlinearProblem(sys, op)` auto-selects `HomotopyProblem` when `sys` contains `homotopy` nodes, otherwise `NonlinearProblem`. - systems/codegen.jl: `generate_rhs` gains an additive `extra_args` to thread `λ` as a trailing scalar argument; empty by default, leaving the standard codegen path byte-identical. - docs (basics/Homotopy.md, problems API reference) and tests for the operator, lowering, broadcasting, problem construction, auto-selection, the codegen guards, and pressure-drop parity. Initialization wiring (injecting the continuation solver as the default `initializealg`) is intentionally left to a follow-up PR. * docs: show default solver in homotopy examples, don't privilege HomotopySweep --------- Co-authored-by: Christopher Rackauckas <accounts@chrisrackauckas.com> Co-authored-by: Aayush Sabharwal <aayush.sabharwal@gmail.com>
1 parent e32fa88 commit 923e6a5

13 files changed

Lines changed: 1012 additions & 7 deletions

File tree

docs/pages.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ pages = [
4747
"Basics" => Any[
4848
"basics/Composition.md",
4949
"basics/Events.md",
50+
"basics/Homotopy.md",
5051
"basics/Linearization.md",
5152
"basics/InputOutput.md",
5253
"basics/MTKLanguage.md",

docs/src/API/problems.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ SciMLBase.ImplicitDiscreteProblem
3636
```@docs
3737
SciMLBase.NonlinearFunction
3838
SciMLBase.NonlinearProblem
39+
SciMLBase.AbstractNonlinearProblem(::System, ::Any)
40+
SciMLBase.HomotopyProblem
3941
SciMLBase.SCCNonlinearProblem
4042
SciMLBase.NonlinearLeastSquaresProblem
4143
SciMLBase.SteadyStateProblem

docs/src/basics/Homotopy.md

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# [Homotopy](@id homotopy)
2+
3+
ModelingToolkit implements the Modelica `homotopy(actual, simplified)` operator
4+
([Modelica Specification 3.7.4.2](https://specification.modelica.org/master/operators-and-expressions.html#homotopy))
5+
as a way to robustly solve nonlinear systems that are hard to solve from a cold
6+
start. It is most commonly reached for during initialization, but it is a general
7+
nonlinear-solving construct: any nonlinear system whose equations carry a
8+
`homotopy` annotation can be solved by continuation.
9+
10+
## What Is Homotopy?
11+
12+
The `simplified` equations are a set of equations for which the nonlinear system
13+
is easier to get a convergent (Newton) iteration for, and the `actual` equations
14+
are the more complex equations you actually want to solve. A homotopy solver
15+
starts by solving the nonlinear system with the `simplified` equations, and uses
16+
that solution as the starting point for solving the `actual` problem, deforming
17+
continuously from one to the other.
18+
19+
There are many ways this can be used. For example, if you have equations with
20+
multiple solutions — like a quadratic equation with a positive and a negative
21+
root — you can simplify it down to an approximating linear problem that has a
22+
single (say, positive) solution, and then deform it to the actual equation to
23+
stabilise the process of converging to that positive solution.
24+
25+
The operator encodes both expressions in a single annotation:
26+
27+
```julia
28+
homotopy(actual, simplified)
29+
```
30+
31+
Concretely, the continuation introduces a scalar parameter ``\lambda`` and solves
32+
33+
```math
34+
(1 - \lambda)\,\text{simplified} + \lambda\,\text{actual}
35+
```
36+
37+
sweeping ``\lambda`` from `0` (the easy `simplified` system) to `1` (the
38+
`actual` system), warm-starting each step from the previous solution.
39+
40+
## Runtime Semantics
41+
42+
The operator stays an opaque symbolic function through `System` construction,
43+
`mtkcompile`, and runtime code generation — no continuation parameter is added to
44+
the system. Wherever the operator is evaluated numerically outside a continuation
45+
solve, the generated code calls the numeric fallback
46+
47+
```julia
48+
homotopy(actual::Real, simplified::Real) = actual
49+
```
50+
51+
so the operator evaluates to `actual`, as the Modelica specification prescribes.
52+
Note the honest cost: the `simplified` argument expression is still evaluated and
53+
its value discarded (arguments are evaluated before the call). This small
54+
overhead is borne only by systems that use `homotopy` — systems without the
55+
operator go through a byte-identical pipeline and are completely unaffected.
56+
57+
Symbolic differentiation works through the operator: nodewise derivative rules
58+
keep symbolic jacobians, `tgrad`, and index reduction consistent. At runtime,
59+
differentiated equations reproduce `actual`'s derivative; along the continuation
60+
they follow the derivative of the blended expression above.
61+
62+
## Building a `HomotopyProblem`
63+
64+
A system whose equations contain `homotopy` nodes is built into a
65+
[`SciMLBase.HomotopyProblem`](@ref) whose residual is the blended expression
66+
above, compiled as `f(u, p, λ)`. `λ` is an explicit trailing argument — it is
67+
never added to the system's parameters, and your parameter object `p` passes
68+
through untouched. All `homotopy` calls in a system share the single `λ`, per the
69+
Modelica spec's recommendation of (conceptually) one homotopy iteration over the
70+
whole model; this includes nested `homotopy` calls.
71+
72+
There are two ways to construct it:
73+
74+
```julia
75+
# Explicit: always returns a HomotopyProblem (errors if `sys` has no `homotopy`).
76+
prob = HomotopyProblem(sys, op)
77+
78+
# Automatic: returns a HomotopyProblem when `sys` contains `homotopy` nodes, and
79+
# a plain NonlinearProblem otherwise.
80+
prob = AbstractNonlinearProblem(sys, op)
81+
```
82+
83+
The `HomotopyProblem`'s `λspan` defaults to `(0.0, 1.0)`. It can be solved with
84+
any algorithm that supports the problem type; `solve(prob)` with no algorithm
85+
picks a suitable default. The current default is natural-parameter continuation
86+
(`NonlinearSolveBase.HomotopySweep`), which sweeps `λ` from `0` to `1`, solving a
87+
standard nonlinear problem at each step and warm-starting from the previous
88+
step's solution.
89+
90+
## Example: Out-of-Basin Rescue
91+
92+
The equation `0 = atan(y - 3)` has a root at `y = 3`, but a Newton solver
93+
starting from `y = 12` diverges because `atan` saturates. Using `homotopy` with
94+
`simplified = y` (whose root is `y = 0`) lets the continuation walk from the easy
95+
root to the true one:
96+
97+
```julia
98+
using ModelingToolkit, NonlinearSolve
99+
100+
@variables y
101+
@mtkcompile sys = System([0 ~ homotopy(atan(y - 3), y)])
102+
prob = HomotopyProblem(sys, [y => 12.0])
103+
sol = solve(prob)
104+
sol[y] # ≈ 3.0 — the continuation rescued the out-of-basin guess
105+
```
106+
107+
The operating point (`[y => 12.0]`) provides the starting point of the
108+
continuation; the sweep deforms the equations so the solver reaches `y ≈ 3` at
109+
`λ = 1`.
110+
111+
## Broadcasting Over Arrays
112+
113+
`homotopy` is a scalar operator. For array equations, broadcast it elementwise:
114+
115+
```julia
116+
eqs = 0 .~ homotopy.(actual_array, simplified_array)
117+
```
118+
119+
This creates one `homotopy` node per element; the continuation lowering rewrites
120+
each node independently, and all of them share the single continuation parameter
121+
`λ`.
122+
123+
## Customizing the Continuation Solver
124+
125+
To tune the sweep, pass your own continuation algorithm to `solve`:
126+
127+
```julia
128+
sol = solve(prob, HomotopySweep(nsteps = 30))
129+
```
130+
131+
`HomotopySweep` accepts the keyword arguments `inner` (the nonlinear algorithm
132+
used at each step), `nsteps`, `adaptive`, `initial_step_factor`, and `min_dλ`;
133+
see the `NonlinearSolveBase.HomotopySweep` docstring for their meanings and
134+
defaults.
135+
136+
## Limitations
137+
138+
- **`expression = Val{true}` is not yet supported** for the homotopy
139+
constructor; build the problem directly (the default
140+
`expression = Val{false}`). This can be added in a future PR.
141+
- **The jacobian/sparsity of the standard build are dropped.** They encode the
142+
`λ = 1` (opaque-`actual`) system and would be wrong mid-sweep; continuation
143+
steps solve with a freshly differentiated residual. Per-problem analytic
144+
jacobians for the swept residual are future work.
145+
- **Scalar `Real` expressions only** for a single `homotopy` node, matching
146+
Modelica's restriction; use broadcasting (above) for arrays.
147+
- **Only equations and observed equations are rewritten.** A `homotopy` call
148+
inside a parameter binding or default value is left as-is and evaluates as
149+
`actual` at all `λ`.
150+
151+
## API Reference
152+
153+
```@docs
154+
ModelingToolkit.homotopy
155+
```
156+
157+
## See Also
158+
159+
- [Modelica Specification 3.7.4.2](https://specification.modelica.org/master/operators-and-expressions.html#homotopy)
160+
— the upstream specification this operator implements.

lib/ModelingToolkitBase/Project.toml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,8 @@ ModelingToolkitStandardLibrary = "2.20"
143143
Mooncake = "0.5"
144144
Moshi = "0.3.6"
145145
NaNMath = "0.3, 1"
146-
NonlinearSolve = "4.3"
146+
NonlinearSolve = "4.19"
147+
NonlinearSolveBase = "2.31"
147148
OffsetArrays = "1.3.0"
148149
OptimizationBase = "5"
149150
OptimizationIpopt = "1"
@@ -170,12 +171,12 @@ ReferenceTests = "0.10"
170171
RuntimeGeneratedFunctions = "0.5.12"
171172
SCCNonlinearSolve = "1.13"
172173
SafeTestsets = "0.1"
173-
SciMLBase = "3.18"
174+
SciMLBase = "3.19"
174175
SciMLPublic = "1.0.0"
175176
SciMLStructures = "1.7"
176177
Serialization = "1"
177178
Setfield = "0.7, 0.8, 1"
178-
SimpleNonlinearSolve = "0.1.0, 1, 2"
179+
SimpleNonlinearSolve = "2.11.1"
179180
SparseArrays = "1"
180181
SpecialFunctions = "1, 2"
181182
StableRNGs = "1"
@@ -209,6 +210,7 @@ LinearSolve = "7ed4a6bd-45f5-4d41-b270-4a48e9bafcae"
209210
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
210211
ModelingToolkitStandardLibrary = "16a59e39-deab-5bd0-87e4-056b12336739"
211212
NonlinearSolve = "8913a72c-1f9b-4ce2-8d82-65094dcecaec"
213+
NonlinearSolveBase = "be0214bd-f91f-a760-ac4e-3421ce2b2da0"
212214
Optimization = "7f7a1694-90dd-40f0-9382-eb1efda571ba"
213215
OptimizationBase = "bca83a33-5cc9-4baa-983d-23429ab6bcbb"
214216
OptimizationIpopt = "43fad042-7963-4b32-ab19-e2a4f9a67124"
@@ -234,4 +236,4 @@ Sundials = "c3572dad-4567-51f8-b174-8c6c989267f4"
234236
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
235237

236238
[targets]
237-
test = ["BenchmarkTools", "BoundaryValueDiffEqMIRK", "BoundaryValueDiffEqAscher", "ControlSystemsBase", "DataInterpolations", "DelayDiffEq", "NonlinearSolve", "ForwardDiff", "ModelingToolkitStandardLibrary", "Optimization", "OptimizationIpopt", "OptimizationOptimJL", "OrdinaryDiffEq", "OrdinaryDiffEqDefault", "REPL", "Random", "ReferenceTests", "SafeTestsets", "StableRNGs", "Statistics", "SteadyStateDiffEq", "Test", "StochasticDiffEq", "Sundials", "Pkg", "OrdinaryDiffEqNonlinearSolve", "Logging", "OptimizationBase", "LinearSolve", "Latexify", "Distributed", "DiffEqNoiseProcess", "DynamicQuantities", "OrdinaryDiffEqSDIRK", "OrdinaryDiffEqRosenbrock", "OrdinaryDiffEqBDF", "OrdinaryDiffEqFunctionMap", "OrdinaryDiffEqTsit5"]
239+
test = ["BenchmarkTools", "BoundaryValueDiffEqMIRK", "BoundaryValueDiffEqAscher", "ControlSystemsBase", "DataInterpolations", "DelayDiffEq", "NonlinearSolve", "ForwardDiff", "ModelingToolkitStandardLibrary", "NonlinearSolveBase", "Optimization", "OptimizationIpopt", "OptimizationOptimJL", "OrdinaryDiffEq", "OrdinaryDiffEqDefault", "REPL", "Random", "ReferenceTests", "SafeTestsets", "StableRNGs", "Statistics", "SteadyStateDiffEq", "Test", "StochasticDiffEq", "Sundials", "Pkg", "OrdinaryDiffEqNonlinearSolve", "Logging", "OptimizationBase", "LinearSolve", "Latexify", "Distributed", "DiffEqNoiseProcess", "DynamicQuantities", "OrdinaryDiffEqSDIRK", "OrdinaryDiffEqRosenbrock", "OrdinaryDiffEqBDF", "OrdinaryDiffEqFunctionMap", "OrdinaryDiffEqTsit5"]

lib/ModelingToolkitBase/src/ModelingToolkitBase.jl

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ using Compat
5858
using AbstractTrees
5959
using SciMLBase: StandardODEProblem, StandardNonlinearProblem, handle_varmap, TimeDomain,
6060
PeriodicClock, Clock, SolverStepClock, ContinuousClock, OverrideInit,
61-
NoInit
61+
NoInit, AbstractNonlinearProblem
6262
import Moshi
6363
using Moshi.Data: @data
6464
using Reexport
@@ -196,6 +196,9 @@ include("systems/codegen_utils.jl")
196196
include("problems/docs.jl")
197197
include("systems/codegen.jl")
198198
include("systems/problem_utils.jl")
199+
# Operator + lowering layer; must load before the problem constructors that
200+
# consume it (problems/nonlinearproblem.jl selector + problems/homotopyproblem.jl).
201+
include("systems/homotopy_operator.jl")
199202

200203
include("problems/compatibility.jl")
201204
include("problems/odeproblem.jl")
@@ -204,6 +207,7 @@ include("problems/daeproblem.jl")
204207
include("problems/sdeproblem.jl")
205208
include("problems/sddeproblem.jl")
206209
include("problems/nonlinearproblem.jl")
210+
include("problems/homotopyproblem.jl")
207211
include("problems/intervalnonlinearproblem.jl")
208212
include("problems/implicitdiscreteproblem.jl")
209213
include("problems/discreteproblem.jl")
@@ -251,6 +255,10 @@ export ImplicitDiscreteProblem, ImplicitDiscreteFunction
251255
export ODEProblem, SDEProblem
252256
export NonlinearFunction
253257
export NonlinearProblem
258+
# `AbstractNonlinearProblem(sys, op)` is the automatic constructor that selects a
259+
# `HomotopyProblem` when `sys` contains `homotopy(...)` nodes (see
260+
# `problems/homotopyproblem.jl`); re-exported so it is reachable unqualified.
261+
export AbstractNonlinearProblem
254262
export IntervalNonlinearFunction
255263
export IntervalNonlinearProblem
256264
export OptimizationProblem, constraints
@@ -305,6 +313,8 @@ export generate_initializesystem, Initial, isinitial, InitializationProblem
305313
export alg_equations, diff_equations, has_alg_equations, has_diff_equations
306314
export get_alg_eqs, get_diff_eqs, has_alg_eqs, has_diff_eqs
307315

316+
export homotopy
317+
308318
export @variables, @parameters, @independent_variables, @constants, @brownians, @brownian,
309319
@poissonians, @discretes
310320
export @named, @nonamespace, @namespace, extend, compose, complete, toggle_namespacing
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"""
2+
SciMLBase.HomotopyProblem(sys::System, op; λspan = (0.0, 1.0), kwargs...)
3+
4+
Build a [`SciMLBase.HomotopyProblem`](@ref) from a `System` whose equations
5+
contain Modelica `homotopy(actual, simplified)` nodes (Modelica spec 3.7.4.2).
6+
7+
The standard nonlinear residual is built from `sys` (and used only to obtain
8+
`u0`, `p`, the observed function, and the residual prototype), then the residual
9+
is regenerated with every `homotopy(actual, simplified)` replaced by the convex
10+
blend `(1 - λ)*simplified + λ*actual` and compiled as `f(u, p, λ)`. `λ` is an
11+
explicit trailing argument — it is never added to the system's parameters, and
12+
`p` passes through untouched — so the result solves by natural-parameter
13+
continuation (`NonlinearSolveBase.HomotopySweep`), sweeping `λ` across `λspan`
14+
(default `(0.0, 1.0)`, i.e. from `simplified` to `actual`).
15+
16+
`sys` must be `complete`d and must contain at least one `homotopy` node (use
17+
[`SciMLBase.NonlinearProblem`](@ref) otherwise, or `AbstractNonlinearProblem` to
18+
select automatically). The jacobian/sparsity of the standard build are
19+
deliberately dropped: they encode the `λ = 1` (opaque-`actual`) system and would
20+
be wrong mid-sweep.
21+
22+
!!! note
23+
24+
`expression = Val{true}` (codegen-to-`Expr`) is not yet supported for the
25+
homotopy constructor; this can be added in a future PR.
26+
"""
27+
function SciMLBase.HomotopyProblem(
28+
sys::System, op;
29+
expression = Val{false}, λspan = (0.0, 1.0),
30+
check_length = true, check_compatibility = true,
31+
eval_expression = false, eval_module = @__MODULE__,
32+
checkbounds = false, cse = true, kwargs...
33+
)
34+
if expression !== Val{false}
35+
throw(
36+
ArgumentError(
37+
"`HomotopyProblem(sys, op)` does not yet support " *
38+
"`expression = Val{true}`; build the problem directly " *
39+
"(the default `expression = Val{false}`)."
40+
)
41+
)
42+
end
43+
check_complete(sys, SciMLBase.HomotopyProblem)
44+
if is_time_dependent(sys)
45+
sys = NonlinearSystem(sys)
46+
end
47+
check_compatibility && check_compatible_system(SciMLBase.NonlinearProblem, sys)
48+
if !has_any_homotopy(sys)
49+
throw(
50+
ArgumentError(
51+
"`HomotopyProblem` requires a system containing " *
52+
"`homotopy(actual, simplified)` nodes; none were found. Use " *
53+
"`NonlinearProblem` for systems without `homotopy`."
54+
)
55+
)
56+
end
57+
58+
_iip = resolve_iip(Both, op)
59+
f, u0,
60+
p = process_SciMLProblem(
61+
SciMLBase.NonlinearFunction{_iip}, sys, op;
62+
check_length, check_compatibility, expression,
63+
eval_expression, eval_module, checkbounds, cse, kwargs...
64+
)
65+
66+
# Swap the opaque-`actual` residual for the homotopy-swept `f(u, p, λ)`. The
67+
# observed function and residual prototype carry over; `initialization_data`,
68+
# jacobian, and sparsity are deliberately not carried (the latter two encode
69+
# the `λ = 1` system and would be wrong mid-sweep).
70+
shadow, λ = lower_homotopy(sys)
71+
hf = generate_homotopy_residual(
72+
shadow, λ; eval_expression, eval_module, checkbounds, cse
73+
)
74+
swept_f = SciMLBase.NonlinearFunction{_iip}(
75+
hf; sys = f.sys, observed = f.observed, resid_prototype = f.resid_prototype
76+
)
77+
78+
kwargs = process_kwargs(sys; kwargs...)
79+
args = (; f = swept_f, u0, p)
80+
return maybe_codegen_scimlproblem(
81+
expression, SciMLBase.HomotopyProblem{_iip}, args; λspan, kwargs...
82+
)
83+
end

lib/ModelingToolkitBase/src/problems/nonlinearproblem.jl

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,31 @@ end
132132
)
133133
end
134134

135+
"""
136+
get_nonlinear_problem_type(sys::System)
137+
138+
The concrete `AbstractNonlinearProblem` type that `AbstractNonlinearProblem(sys, op)`
139+
builds for `sys`: [`SciMLBase.HomotopyProblem`](@ref) when `sys` contains Modelica
140+
`homotopy(actual, simplified)` nodes, otherwise [`SciMLBase.NonlinearProblem`](@ref).
141+
"""
142+
function get_nonlinear_problem_type(sys::System)
143+
return has_any_homotopy(sys) ? SciMLBase.HomotopyProblem : SciMLBase.NonlinearProblem
144+
end
145+
146+
"""
147+
SciMLBase.AbstractNonlinearProblem(sys::System, op; kwargs...)
148+
149+
Build a nonlinear problem from `sys`, automatically selecting the concrete type via
150+
`get_nonlinear_problem_type`: a [`SciMLBase.HomotopyProblem`](@ref) when `sys`
151+
contains Modelica `homotopy(actual, simplified)` nodes (so the equations are solved by
152+
continuation from the `simplified` form), otherwise a plain
153+
[`SciMLBase.NonlinearProblem`](@ref). Keyword arguments are forwarded to the selected
154+
constructor; `λspan` only applies to the `HomotopyProblem` branch.
155+
"""
156+
function SciMLBase.AbstractNonlinearProblem(sys::System, op; kwargs...)
157+
return get_nonlinear_problem_type(sys)(sys, op; kwargs...)
158+
end
159+
135160
@fallback_iip_specialize function SciMLBase.NonlinearLeastSquaresProblem{iip, spec}(
136161
sys::System, op; check_length = false, lb = nothing, ub = nothing,
137162
check_compatibility = true, expression = Val{false}, kwargs...

0 commit comments

Comments
 (0)