ModelingToolkit 11.36 made the operating-point values of loop-opened signals explicitly required when linearizing (_check_loop_opening_op): opening a loop at an analysis point turns the opened signal into a parameter whose value is no longer implied by the rest of the system. This is a good change in itself — previously a stale/guessed value could be used silently — but there does not currently seem to be a way to express the most natural choice of value: the value the signal would have at the operating point with the loop closed.
Would it make sense to allow passing missing for the new broken inputs, together with a steady-state condition for the broken outputs? That is,
op = Dict(
sys.C.u => missing, # the broken input: solve for it in the initialization
D(sys.P.y) => 0, # the broken output: steady-state condition making the system determined
)
following the usual MTK convention that p => missing means "solve for this parameter during initialization (using its guess)". Today this crashes inside linearize's operating-point application loop, because the missing is passed straight to the parameter setter:
ERROR: MethodError: Cannot `convert` an object of type Missing to an object of type Float64
Stacktrace:
[1] setindex!
[2] set_parameter! @ ModelingToolkitBase src/systems/parameter_buffer.jl:501
[3] set_parameter! @ SymbolicIndexingInterface src/value_provider_interface.jl:68
... (via the `setu(prob, k)(prob, v)` loop in `linearize`, src/linearization.jl)
MWE
using ModelingToolkit
using ModelingToolkit: t_nounits as t, D_nounits as D
import ModelingToolkit: connect
@component function FirstOrder(; name, k = 1.0)
@variables begin
u(t), [input = true]
x(t) = 0.0
y(t), [output = true]
end
@parameters k = k
System([D(x) ~ -x + k * u, y ~ x], t; name)
end
@named P = FirstOrder(k = 1.0)
@named C = FirstOrder(k = -2.0)
eqs = [connect(P.y, :y, C.u), connect(C.y, :u, P.u)]
@named sys = System(eqs, t; systems = [P, C])
csys = complete(sys) # for operating-point key access only
# Explicit value: works
m1, _ = ModelingToolkit.linearize(sys, :u, :y; loop_openings = [:y], op = Dict(csys.C.u => 0.0))
# missing: MethodError: Cannot `convert` an object of type Missing to an object of type Float64
m2, _ = ModelingToolkit.linearize(sys, :u, :y; loop_openings = [:y], op = Dict(csys.C.u => missing))
# missing + steady-state condition for the broken output: same error
m3, _ = ModelingToolkit.linearize(sys, :u, :y; loop_openings = [:y],
op = Dict(csys.C.u => missing, D(csys.P.y) => 0.0))
What works today, and why it is not quite enough
- Literal values (e.g.
signal => 0) work everywhere, but for a nonlinear system linearized at a nonzero operating point, zero is generally not the loop-closed-consistent value, so the linearization is taken at a (slightly) different point than the closed-loop one.
- Symbolic values (e.g.
opened_signal => driving_signal) resolve correctly in linearize(sys, lin_fun; op) (via _resolve_op_value/getu), which gives exactly the loop-closed value — but they error at problem construction when the expression involves unknowns without defaults, since MTKParameters evaluates operating-point values eagerly, before any initialization has run ("Could not evaluate value of parameter ... Missing values for variables in expression ..."). So they cannot be used robustly either.
Context: this came up while porting DyadControlSystems to MTK 11.36 (all linear-analysis APIs there open loops at measurement/input analysis points). We currently fall back to zero-filling the opened signals, which restores the previous behavior at the typical zero operating points but loses the loop-closed consistency in general.
Versions: ModelingToolkit 11.36.0, ModelingToolkitBase 1.56.0, Julia 1.12.6.
🤖 Generated with Claude Code
https://claude.ai/code/session_014QR4FuuvxZU9a3UrTJjg5X
ModelingToolkit 11.36 made the operating-point values of loop-opened signals explicitly required when linearizing (
_check_loop_opening_op): opening a loop at an analysis point turns the opened signal into a parameter whose value is no longer implied by the rest of the system. This is a good change in itself — previously a stale/guessed value could be used silently — but there does not currently seem to be a way to express the most natural choice of value: the value the signal would have at the operating point with the loop closed.Would it make sense to allow passing
missingfor the new broken inputs, together with a steady-state condition for the broken outputs? That is,following the usual MTK convention that
p => missingmeans "solve for this parameter during initialization (using its guess)". Today this crashes insidelinearize's operating-point application loop, because themissingis passed straight to the parameter setter:MWE
What works today, and why it is not quite enough
signal => 0) work everywhere, but for a nonlinear system linearized at a nonzero operating point, zero is generally not the loop-closed-consistent value, so the linearization is taken at a (slightly) different point than the closed-loop one.opened_signal => driving_signal) resolve correctly inlinearize(sys, lin_fun; op)(via_resolve_op_value/getu), which gives exactly the loop-closed value — but they error at problem construction when the expression involves unknowns without defaults, sinceMTKParametersevaluates operating-point values eagerly, before any initialization has run ("Could not evaluate value of parameter ... Missing values for variables in expression ..."). So they cannot be used robustly either.Context: this came up while porting DyadControlSystems to MTK 11.36 (all linear-analysis APIs there open loops at measurement/input analysis points). We currently fall back to zero-filling the opened signals, which restores the previous behavior at the typical zero operating points but loses the loop-closed consistency in general.
Versions: ModelingToolkit 11.36.0, ModelingToolkitBase 1.56.0, Julia 1.12.6.
🤖 Generated with Claude Code
https://claude.ai/code/session_014QR4FuuvxZU9a3UrTJjg5X