-
-
Notifications
You must be signed in to change notification settings - Fork 255
Modelica homotopy(actual, simplified) operator for initialization (spec 3.7.4)
#4601
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
07659d4
7aef6da
0c20856
aad7273
8cf4984
61c41e5
27377a4
bb7d70e
1ce0a1c
e731caf
bb27cb5
f2294bf
4e966cd
20912fd
b13a3c2
41ae851
8a5ee98
1c253f0
f48537a
0e5ffce
89d0f11
10f124c
3b20127
703ee65
34bbfcb
ccc61fe
6434f96
293b484
c76b6a1
d0c1019
fbfd313
7775a91
2dda803
4706e06
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| # [Homotopy Initialization](@id homotopy) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why even speak to initialization here? It's just a nonlinear solver. The fact that it shows up commonly in initialization is a detail, but there are many other uses for it. |
||
|
|
||
| ModelingToolkit implements the Modelica `homotopy(actual, simplified)` operator | ||
| ([Modelica Specification 3.7.4.2](https://specification.modelica.org/master/operators-and-expressions.html#homotopy)) | ||
| as an initialization aid for systems with hard-to-initialize algebraic equations. | ||
|
|
||
| ## What Is Homotopy Initialization? | ||
|
|
||
| Many physical models (pressure-driven pipe networks, power-flow equations, chemical | ||
| equilibria) have algebraic constraints that are easy to solve under simplified | ||
| assumptions but hard to solve from a cold start. The `homotopy` operator encodes | ||
| both the actual expression and a simplified approximation in a single annotation: | ||
|
|
||
| ```julia | ||
| homotopy(actual, simplified) | ||
| ``` | ||
|
|
||
| During initialization the solver may start from the easy `simplified` form and | ||
| continuously deform it into `actual`; outside initialization the operator | ||
| evaluates to `actual`, as the Modelica specification prescribes. | ||
|
Comment on lines
+15
to
+20
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It should be more clear. The
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This also highlights that your implementation of homotopy has a bug since it doesn't start at lambda=0 😅 |
||
|
|
||
| ## Runtime Semantics | ||
|
|
||
| The operator stays an opaque symbolic function through `System` construction, | ||
| `mtkcompile`, and runtime code generation — no continuation parameter is added to | ||
| the system. At runtime the generated code calls the numeric fallback | ||
|
|
||
| ```julia | ||
| homotopy(actual::Real, simplified::Real) = actual | ||
| ``` | ||
|
|
||
| so the operator evaluates to `actual`. Note the honest cost: the `simplified` | ||
| argument expression is still evaluated and its value discarded (arguments are | ||
| evaluated before the call). This small overhead is borne only by systems that use | ||
| `homotopy` — systems without the operator go through a byte-identical pipeline and | ||
| are completely unaffected. | ||
|
|
||
| ## Initialization Semantics | ||
|
|
||
| When the initialization problem is built (after `mtkcompile`), the compiled | ||
| initialization system is scanned for `homotopy` nodes in its equations and | ||
| observed equations. If any are found and the initialization problem is a square | ||
| `NonlinearProblem` with unknowns, the initialization residual is regenerated with | ||
| every `homotopy(actual, simplified)` replaced by the blend | ||
|
|
||
| ```math | ||
| (1 - \lambda)\,\text{simplified} + \lambda\,\text{actual} | ||
| ``` | ||
|
|
||
| and compiled as `f(u, p, λ)`, where `λ` is an explicit trailing argument of the | ||
| residual — it is never added to the system's parameters, and your parameter object | ||
| `p` passes through untouched. All `homotopy` calls in a system share the single | ||
| `λ`, per the Modelica spec's recommendation of (conceptually) one homotopy | ||
| iteration over the whole model; this includes nested `homotopy` calls. The result is wrapped as a `SciMLBase.HomotopyProblem` | ||
| (its `λspan` defaults to `(0.0, 1.0)`). | ||
|
|
||
| Problems built through the default path then receive | ||
|
|
||
| ```julia | ||
| initializealg = SciMLBase.OverrideInit(nlsolve = NonlinearSolveBase.HomotopySweep()) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any nonlinear solve of equations which have a homotopy operator should default to defining a homotopy problem.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or rather, there should be an AbstractNonlinearProblem constructor that is automatic here. |
||
| ``` | ||
|
|
||
| as their default initialization algorithm. `HomotopySweep` performs a | ||
| natural-parameter continuation: it sweeps `λ` from `0` (the easy `simplified` | ||
| equations) to `1` (`actual`), solving a standard nonlinear problem at each step and | ||
| warm-starting from the previous step's solution. `NonlinearSolveBase` is a hard | ||
| dependency of `ModelingToolkitBase`, so no extra package load is needed to | ||
| activate the sweep. | ||
|
|
||
| Symbolic differentiation works through the operator: nodewise derivative rules | ||
| keep symbolic jacobians, `tgrad`, and index reduction consistent. At runtime, | ||
| differentiated equations reproduce `actual`'s derivative; in the swept residual | ||
| they follow the derivative of the blended expression above. | ||
|
|
||
| ## When Does the Sweep Activate? | ||
|
|
||
| - A system whose compiled initialization system contains `homotopy` nodes (in | ||
| equations or observed equations) gets a swept initialization, as described | ||
| above. | ||
| - A system without `homotopy` is untouched: no detection cost beyond the scan, | ||
| no problem rewrapping, no `initializealg` injection. | ||
| - An explicit user-passed `initializealg` always wins — the default is only | ||
| injected when you did not pass one, and a solve-time `initializealg` overrides | ||
| the problem default as usual. | ||
| - If simplification eliminates every `homotopy` node before the initialization | ||
| system is built, initialization proceeds as standard (detection happens on the | ||
| compiled initialization system). | ||
| - Multiple (or nested) `homotopy` calls in one system are blended with the one | ||
| shared `λ` and swept together in a single continuation. | ||
| - If the initialization would otherwise target an `SCCNonlinearProblem`, a | ||
| homotopy-using system is forced to the monolithic `NonlinearProblem` target, | ||
| since the continuation deforms the coupled system along one `λ`. | ||
|
Comment on lines
+90
to
+92
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It should probably split that to SCCs. |
||
|
|
||
| ## Example: Out-of-Basin Rescue | ||
|
|
||
| The equation `0 = atan(y - 3)` has a root at `y = 3`, but a Newton solver starting | ||
| from `y = 12` diverges because `atan` saturates. Using `homotopy` with | ||
| `simplified = y` (whose root is `y = 0`) lets the continuation walk from the easy | ||
| root to the true one: | ||
|
|
||
| ```julia | ||
| using ModelingToolkit, NonlinearSolve, OrdinaryDiffEqRosenbrock | ||
| using ModelingToolkit: t_nounits as t, D_nounits as D | ||
|
|
||
| @variables x(t) y(t) | ||
| @named sys = System([D(x) ~ -x, 0 ~ homotopy(atan(y - 3), y)], t; guesses = [y => 12.0]) | ||
| sys = mtkcompile(sys) | ||
| prob = ODEProblem(sys, [x => 1.0], (0.0, 1.0)) | ||
| integ = init(prob, Rodas5P()) | ||
| integ[y] # ≈ 3.0 — the sweep rescued the out-of-basin guess | ||
| ``` | ||
|
|
||
| The `guesses` keyword provides the starting point of the continuation (here | ||
| `y = 12`); the sweep deforms the equations so the solver reaches `y ≈ 3` at | ||
| `λ = 1`. | ||
|
|
||
| ## Customizing the Continuation Solver | ||
|
|
||
| To tune the sweep, pass your own `initializealg` (which always takes precedence | ||
| over the injected default): | ||
|
|
||
| ```julia | ||
| integ = init(prob, Rodas5P(); | ||
| initializealg = SciMLBase.OverrideInit( | ||
| nlsolve = NonlinearSolveBase.HomotopySweep(nsteps = 30))) | ||
| ``` | ||
|
|
||
| `HomotopySweep` accepts the keyword arguments `inner` (the nonlinear algorithm | ||
| used at each step), `nsteps`, `adaptive`, `initial_step_factor`, and `min_dλ`; see | ||
| the `NonlinearSolveBase.HomotopySweep` docstring for their meanings and defaults. | ||
| Tolerances are inherited from the integrator's `abstol`/`reltol`. | ||
|
|
||
| ## Limitations | ||
|
|
||
| - **Non-square initialization cannot be swept.** If the initialization problem | ||
| is not a square `NonlinearProblem` (e.g. over-determined initialization, which | ||
| falls back to least squares), a one-time warning is emitted and initialization | ||
| solves the `actual` (`λ = 1`) form instead. Least-squares and per-SCC sweeps | ||
| are future work. | ||
| - **Trivial initialization is silently correct.** If the initialization system | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Obviously. Why is this even worth mentioning? |
||
| has no unknowns, there is nothing to solve and no sweep happens; observed | ||
| evaluation folds `homotopy` to `actual`, the correct | ||
| outside-initialization semantics. | ||
| - **`expression = Val{true}` does not auto-inject the `initializealg`.** When | ||
| generating problem-construction expressions, pass | ||
| `initializealg = SciMLBase.OverrideInit(nlsolve = NonlinearSolveBase.HomotopySweep())` | ||
| explicitly. | ||
|
Comment on lines
+144
to
+147
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a bug. |
||
| - **Parameter bindings and defaults are not rewritten.** A `homotopy` call | ||
| inside a parameter binding or default value evaluates as `actual` at all `λ`. | ||
| - **Scalar `Real` expressions only**, matching Modelica's restriction; the | ||
| numeric fallback method is `homotopy(::Real, ::Real)`. | ||
|
Comment on lines
+150
to
+151
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Recommend broadcast. |
||
| - **Failure surfaces as a failed initialization.** If the continuation cannot | ||
|
Comment on lines
+150
to
+152
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Obviously. Also, not relevant to the MTK side. |
||
| reach `λ = 1`, the sweep returns a failure return code and initialization | ||
| fails (see `NonlinearSolveBase.HomotopySweep` for the failure contract). | ||
|
|
||
| ## API Reference | ||
|
|
||
| ```@docs | ||
| ModelingToolkit.homotopy | ||
| ``` | ||
|
|
||
| ## See Also | ||
|
|
||
| - [Initialization of Systems](@ref initialization) — general DAE initialization, | ||
| bindings, guesses, and `OverrideInit`. | ||
| - [Modelica Specification 3.7.4.2](https://specification.modelica.org/master/operators-and-expressions.html#homotopy) | ||
| — the upstream specification this operator implements. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -123,7 +123,98 @@ All other keyword arguments are forwarded to the wrapped problem constructor. | |
| sys, isys; warn_initialize_determined, | ||
| kwargs... | ||
| ) | ||
| TProb{_iip}(isys, op; kwargs..., build_initializeprob = false, is_initializeprob = true) | ||
| # Modelica homotopy(actual, simplified) (spec 3.7.4.2): scan the compiled init system | ||
| # (equations ∪ observed) for homotopy nodes. If any are present, the square init | ||
| # problem is rebuilt by `_maybe_sweep_homotopy_initprob` below as a | ||
| # `SciMLBase.HomotopyProblem` whose residual `f(u, p, λ)` sweeps | ||
| # `(1 - λ)*simplified + λ*actual`, so a continuation solver can walk λ from 0 to 1. | ||
| homotopic = has_any_homotopy(isys) | ||
| if homotopic && TProb === SCCNonlinearProblem | ||
| # A continuation sweep deforms the COUPLED system along one λ; per-SCC | ||
| # sweeping is a separate future improvement. Root ModelingToolkit's | ||
| # `get_initialization_problem_type` prefers `SCCNonlinearProblem` for split | ||
| # fully-determined systems — force the monolithic target here. | ||
| TProb = NonlinearProblem | ||
| end | ||
| initprob = TProb{_iip}(isys, op; kwargs..., build_initializeprob = false, is_initializeprob = true) | ||
| if homotopic | ||
| initprob = _maybe_sweep_homotopy_initprob(initprob, isys, _iip, kwargs) | ||
| end | ||
| return initprob | ||
|
Comment on lines
+131
to
+143
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Split to the AbstractNonlinearProblem constructor. |
||
| end | ||
|
|
||
| # Codegen keyword arguments which `_maybe_sweep_homotopy_initprob` threads from | ||
| # `InitializationProblem`'s kwargs into `generate_homotopy_residual`, so the swept | ||
| # residual is compiled with the same options as the standard one. Keep in sync with | ||
| # the codegen-relevant keywords of `SciMLBase.NonlinearFunction{iip, spec}(::System)` | ||
| # (problems/nonlinearproblem.jl), which the standard init residual build receives. | ||
| const HOMOTOPY_INIT_CODEGEN_KWARGS = ( | ||
| :eval_expression, :eval_module, :checkbounds, :cse, :compiler_options, | ||
| ) | ||
|
|
||
| """ | ||
| $TYPEDSIGNATURES | ||
|
|
||
| Independent post-build pass for Modelica `homotopy(actual, simplified)` (spec 3.7.4.2), | ||
| called from `InitializationProblem` when the compiled init system `isys` contains | ||
| homotopy nodes. If `initprob` is a square `NonlinearProblem` with unknowns, rebuild | ||
| it as a `SciMLBase.HomotopyProblem` whose residual `f(u, p, λ)` sweeps | ||
| `(1 - λ)*simplified + λ*actual`, so a continuation solver can walk λ from 0 to 1. | ||
| Otherwise return `initprob` unchanged — silently when initialization is trivial | ||
| (no solve happens, so nothing is lost), with a one-time warning when a real init | ||
| solve will run on the `actual` (λ = 1) form instead of being swept. | ||
|
|
||
| `iip` is the resolved in-place flag of `initprob`. `kwargs` are | ||
| `InitializationProblem`'s keyword arguments, from which the codegen options listed | ||
| in `HOMOTOPY_INIT_CODEGEN_KWARGS` are threaded into the swept-residual build. | ||
| """ | ||
| function _maybe_sweep_homotopy_initprob(initprob, isys::AbstractSystem, iip::Bool, kwargs) | ||
| if initprob isa NonlinearProblem && state_values(initprob) !== nothing | ||
| shadow, λ = lower_homotopy_for_init(isys) | ||
| codegen_kwargs = (; | ||
| ( | ||
| kw => kwargs[kw] for kw in HOMOTOPY_INIT_CODEGEN_KWARGS | ||
| if haskey(kwargs, kw) | ||
| )..., | ||
| ) | ||
| hf = generate_homotopy_residual(shadow, λ; codegen_kwargs...) | ||
| nf = initprob.f | ||
| swept_f = SciMLBase.NonlinearFunction{iip}( | ||
| hf; sys = nf.sys, observed = nf.observed, | ||
| resid_prototype = nf.resid_prototype | ||
| ) | ||
| # Deliberately NOT carried over: jac / jac_prototype / sparsity — they | ||
| # encode the λ = 1 (opaque-actual) system and would be wrong mid-sweep. | ||
| # (The standard build did generate that λ = 1 residual — and a jac, if the | ||
| # user requested one — only for them to be replaced here: a deliberate | ||
| # layering cost on the homotopy path; the un-swept path is unaffected.) | ||
| # λspan is deliberately not passed: the `SciMLBase.HomotopyProblem` | ||
| # default is the single source of truth. | ||
| return SciMLBase.HomotopyProblem{iip}( | ||
| swept_f, initprob.u0, initprob.p; initprob.kwargs... | ||
| ) | ||
| elseif initprob isa NonlinearProblem | ||
| # No unknowns (`state_values === nothing`): initialization is trivial — | ||
| # the init solve is skipped entirely, so there is no system to sweep and | ||
| # no homotopy intent is discarded. Downstream observed evaluation folds | ||
| # `homotopy(actual, simplified)` to `actual`, which is exactly the | ||
| # Modelica outside-initialization semantics. Correct result; no warning. | ||
| @debug "`homotopy(actual, simplified)` present, but initialization is " * | ||
| "trivial (no unknowns); nothing to sweep." | ||
| else | ||
| # Remaining legs: the NLLS fallback for non-square/singular init systems, | ||
| # the linear-init path (homotopy nodes without unknowns; its build product | ||
| # is SCC-wrapped), or an `Expr` from `expression = Val{true}`. Any future | ||
| # target type lands here too — warn + λ = 1 solve is the safe default. In | ||
| # all of them the generated code's numeric fallback folds homotopy to | ||
| # `actual`. | ||
| @warn "This system contains `homotopy(actual, simplified)`, but its " * | ||
| "initialization problem (a `$(nameof(typeof(initprob)))`) is not a " * | ||
| "square `NonlinearProblem` and cannot be swept by continuation; " * | ||
| "initialization proceeds with the `actual` (λ = 1) form. A " * | ||
| "least-squares/SCC homotopy sweep is planned as a follow-up." maxlog = 1 | ||
| end | ||
| return initprob | ||
| end | ||
|
|
||
| function overdetermined_initialization_message(neqs::Integer, nunknown::Integer, extra::AbstractString) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -81,6 +81,7 @@ function generate_rhs( | |
| rhss = [_iszero(eq.lhs) ? eq.rhs : eq.rhs - eq.lhs for eq in eqs] | ||
| end | ||
| else | ||
| # NOTE: generate_homotopy_residual in homotopy.jl mirrors the time-independent path here; keep in sync. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ? |
||
| if !override_discrete && !is_discrete_system(sys) | ||
| check_operator_variables(eqs, Differential) | ||
| check_lhs(eqs, Differential, Set(dvs)) | ||
|
|
@@ -273,7 +274,7 @@ function generate_jacobian( | |
| end | ||
|
|
||
| function assert_jac_length(arr::SparseMatrixCSC, I::Vector{<:Integer}, J::Vector{<:Integer}) | ||
| @assert findnz(arr)[1:2] == (I, J) | ||
| return @assert findnz(arr)[1:2] == (I, J) | ||
| end | ||
|
|
||
| function assert_jac_length_header(sys) | ||
|
|
@@ -1103,8 +1104,10 @@ end | |
| function assemble_maj(majv::Vector{U}, unknowntoid, pmapper) where {U <: MassActionJump} | ||
| rs = [numericrstoich(maj.reactant_stoch, unknowntoid) for maj in majv] | ||
| ns = [numericnstoich(maj.net_stoch, unknowntoid) for maj in majv] | ||
| return MassActionJump(rs, ns; param_mapper = pmapper, nocopy = true, | ||
| scale_rates = false, rescale_rates_on_update = false) | ||
| return MassActionJump( | ||
| rs, ns; param_mapper = pmapper, nocopy = true, | ||
| scale_rates = false, rescale_rates_on_update = false | ||
| ) | ||
| end | ||
|
|
||
| function numericrstoich(mtrs::Vector{Pair{V, W}}, unknowntoid) where {V, W} | ||
|
|
@@ -1389,8 +1392,8 @@ Base.@nospecializeinfer function build_explicit_observed_function( | |
| p_start + wrap_delays, length(args) - length(rps) + 1 + wrap_delays, is_split(sys), | ||
| ), | ||
| }( | ||
| oop, iip | ||
| ) | ||
| oop, iip | ||
| ) | ||
| return (return_inplace isa Val{true} || return_inplace isa Bool && return_inplace) ? (f, f) : f | ||
| end | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
? it'll release right away? Why write that here?