Skip to content

Loop openings: allow missing for the broken inputs together with a steady-state condition for the broken outputs? #4830

Description

@baggepinnen

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions