Skip to content

Commit 6cd2263

Browse files
authored
add option to simplify causality in a different way (#73)
* add option to simplify causality in a different way * provide some variables for operating points * fixes in batchlin * init fixes * rm import * more inits
1 parent dafbbfc commit 6cd2263

File tree

6 files changed

+100
-42
lines changed

6 files changed

+100
-42
lines changed

docs/Project.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
66
ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78"
77
ModelingToolkitStandardLibrary = "16a59e39-deab-5bd0-87e4-056b12336739"
88
MonteCarloMeasurements = "0987c9cc-fe09-11e8-30f0-b96dd679fdca"
9-
OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed"
9+
OrdinaryDiffEqNonlinearSolve = "127b3ac7-2247-4354-8eb6-78cf4e7c58e8"
10+
OrdinaryDiffEqRosenbrock = "43230ef6-c299-4910-a778-202eb28ce4ce"
1011
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
1112
RobustAndOptimalControl = "21fd56a4-db03-40ee-82ee-a87907bee541"
1213
SymbolicControlSystems = "886cb795-8fd3-4b11-92f6-8071e46f71c5"

docs/src/batch_linearization.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ This example will demonstrate how to linearize a nonlinear ModelingToolkit model
88
The model will be a simple Duffing oscillator:
99
```@example BATCHLIN
1010
using ControlSystemsMTK, ModelingToolkit, MonteCarloMeasurements, ModelingToolkitStandardLibrary.Blocks
11-
using ModelingToolkit: getdefault
1211
unsafe_comparisons(true)
1312
1413
# Create a model
@@ -26,15 +25,15 @@ eqs = [D(x) ~ v
2625
y.u ~ x]
2726
2827
29-
@named duffing = ODESystem(eqs, t, systems=[y, u], defaults=[u.u => 0])
28+
@named duffing = ODESystem(eqs, t, systems=[y, u])
3029
```
3130

3231
## Batch linearization
3332
To perform batch linearization, we create a vector of operating points, and then linearize the model around each of these points. The function [`batch_ss`](@ref) does this for us, and returns a vector of `StateSpace` models, one for each operating point. An operating point is a `Dict` that maps variables in the MTK model to numerical values. In the example below, we simply sample the variables uniformly within their bounds specified when we created the variables (normally, we might want to linearize on stationary points)
3433
```@example BATCHLIN
3534
N = 16 # Number of samples
3635
xs = range(getbounds(x)[1], getbounds(x)[2], length=N)
37-
ops = Dict.(x .=> xs)
36+
ops = Dict.(u.u => 0, x .=> xs)
3837
```
3938

4039
Just like [`ModelingToolkit.linearize`](@ref), [`batch_ss`](@ref) takes the set of inputs and the set of outputs to linearize between.
@@ -96,7 +95,7 @@ If you plot the Nyquist curve using the `plotly()` backend rather than the defau
9695
Above, we tuned one controller for each operating point, wouldn't it be nice if we had some features to simulate a [gain-scheduled controller](https://en.wikipedia.org/wiki/Gain_scheduling) that interpolates between the different controllers depending on the operating pont? [`GainScheduledStateSpace`](@ref) is such a thing, we show how to use it below. For fun, we simulate some reference step responses for each individual controller in the array `Cs` and end with simulating the gain-scheduled controller.
9796

9897
```@example BATCHLIN
99-
using OrdinaryDiffEq
98+
using OrdinaryDiffEqNonlinearSolve, OrdinaryDiffEqRosenbrock
10099
using DataInterpolations # Required to interpolate between the controllers
101100
@named fb = Blocks.Add(k2=-1)
102101
@named ref = Blocks.Square(frequency=1/6, amplitude=0.5, offset=0.5, start_time=1)
@@ -134,7 +133,7 @@ eqs = [
134133
]
135134
@named closed_loop = ODESystem(eqs, t, systems=[duffing, Cgs, fb, ref, F])
136135
prob = ODEProblem(structural_simplify(closed_loop), [F.xd => 0], (0.0, 8.0))
137-
sol = solve(prob, Rodas5P(), abstol=1e-8, reltol=1e-8, initializealg=NoInit())
136+
sol = solve(prob, Rodas5P(), abstol=1e-8, reltol=1e-8, initializealg=SciMLBase.NoInit())
138137
plot!(sol, idxs=[duffing.y.u, duffing.u.u], l=(2, :red), lab="Gain scheduled")
139138
plot!(sol, idxs=F.output.u, l=(1, :black, :dash, 0.5), lab="Ref")
140139
```
@@ -155,7 +154,7 @@ The generated code starts by defining the interpolation vector `xs`, this variab
155154
We can linearize around a trajectory obtained from `solve` using the function [`trajectory_ss`](@ref). We provide it with a vector of time points along the trajectory at which to linearize, and in this case we specify the inputs and outputs to linearize between as analysis points `r` and `y`.
156155
```@example BATCHLIN
157156
timepoints = 0:0.01:8
158-
Ps2, ssys = trajectory_ss(closed_loop, closed_loop.r, closed_loop.y, sol; t=timepoints)
157+
Ps2, ssys = trajectory_ss(closed_loop, closed_loop.r, closed_loop.y, sol; t=timepoints, initialize=true, verbose=true)
159158
bodeplot(Ps2, w, legend=false)
160159
```
161160

docs/src/index.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ ModelingToolkit has facilities for symbolic linearization that can be used on su
139139
We start by building a system mode, we'll use a model of two masses connected by a flexible transmission
140140
```@example LINEAIZE_SYMBOLIC
141141
using ControlSystemsMTK, ControlSystemsBase
142-
using ModelingToolkit, OrdinaryDiffEq, LinearAlgebra
142+
using ModelingToolkit, LinearAlgebra
143143
using ModelingToolkitStandardLibrary.Mechanical.Rotational
144144
using ModelingToolkitStandardLibrary.Blocks: Sine
145145
using ModelingToolkit: connect
@@ -178,7 +178,8 @@ model = SystemModel() |> complete
178178
### Numeric linearization
179179
We can linearize this model numerically using `named_ss`, this produces a `NamedStateSpace{Continuous, Float64}`
180180
```@example LINEAIZE_SYMBOLIC
181-
lsys = named_ss(model, [model.torque.tau.u], [model.inertia1.phi, model.inertia2.phi], op = Dict(model.torque.tau.u => 0))
181+
op = Dict(model.inertia1.flange_b.phi => 0.0, model.torque.tau.u => 0)
182+
lsys = named_ss(model, [model.torque.tau.u], [model.inertia1.phi, model.inertia2.phi]; op)
182183
```
183184
### Symbolic linearization
184185
If we instead call `linearize_symbolic` and pass the jacobians into `ss`, we get a `StateSpace{Continuous, Num}`

src/ode_system.jl

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -154,19 +154,22 @@ end
154154

155155

156156
"""
157-
RobustAndOptimalControl.named_ss(sys::ModelingToolkit.AbstractTimeDependentSystem, inputs, outputs; kwargs...)
157+
RobustAndOptimalControl.named_ss(sys::ModelingToolkit.AbstractTimeDependentSystem, inputs, outputs; descriptor=true, kwargs...)
158158
159159
Convert an `ODESystem` to a `NamedStateSpace` using linearization. `inputs, outputs` are vectors of variables determining the inputs and outputs respectively. See docstring of `ModelingToolkit.linearize` for more info on `kwargs`.
160160
161-
This method automatically converts systems that MTK has failed to produce a proper form for into a proper linear statespace system. Learn more about how that is done here:
161+
If `descriptor = true` (default), this method automatically converts systems that MTK has failed to produce a proper form for into a proper linear statespace system using the method described here:
162162
https://juliacontrol.github.io/ControlSystemsMTK.jl/dev/#Internals:-Transformation-of-non-proper-models-to-proper-statespace-form
163+
If `descriptor = false`, the system is instead converted to a statespace realization using `sys[:,uinds] + sys[:,duinds]*tf('s')`, which tends to result in a larger realization on which the user may want to call `minreal(sys, tol)` with a carefully selected tolerance.
164+
163165
164166
See also [`ModelingToolkit.linearize`](@ref) which is the lower-level function called internally. The functions [`get_named_sensitivity`](@ref), [`get_named_comp_sensitivity`](@ref), [`get_named_looptransfer`](@ref) similarily provide convenient ways to compute sensitivity functions while retaining signal names in the same way as `named_ss`. The corresponding lower-level functions `get_sensitivity`, `get_comp_sensitivity` and `get_looptransfer` are available in ModelingToolkitStandardLibrary.Blocks and are documented in [MTKstdlib: Linear analysis](https://docs.sciml.ai/ModelingToolkitStandardLibrary/stable/API/linear_analysis/).
165167
"""
166168
function RobustAndOptimalControl.named_ss(
167169
sys::ModelingToolkit.AbstractTimeDependentSystem,
168170
inputs,
169171
outputs;
172+
descriptor = true,
170173
kwargs...,
171174
)
172175

@@ -206,7 +209,7 @@ function RobustAndOptimalControl.named_ss(
206209
# This indicates that input derivatives are present
207210
duinds = findall(any(!iszero, eachcol(matrices.B[:, nu+1:end]))) .+ nu
208211
u2du = (1:nu) .=> duinds # This maps inputs to their derivatives
209-
lsys = causal_simplification(matrices, u2du)
212+
lsys = causal_simplification(matrices, u2du; descriptor)
210213
else
211214
lsys = ss(matrices...)
212215
end
@@ -219,26 +222,33 @@ function RobustAndOptimalControl.named_ss(
219222
)
220223
end
221224

222-
function causal_simplification(sys, u2duinds::Vector{Pair{Int, Int}})
225+
function causal_simplification(sys, u2duinds::Vector{Pair{Int, Int}}; descriptor=true)
226+
fm(x) = convert(Matrix{Float64}, x)
223227
nx = size(sys.A, 1)
224228
ny = size(sys.C, 1)
225229
ndu = length(u2duinds)
226230
nu = size(sys.B, 2) - ndu
227231
u_with_du_inds = first.(u2duinds)
228232
duinds = last.(u2duinds)
229-
= sys.B[:, duinds]
230233
B = sys.B[:, 1:nu]
231-
Iu = u_with_du_inds .== (1:nu)'
232-
E = [I(nx) -B̄; zeros(ndu, nx+ndu)]
233-
234-
fm(x) = convert(Matrix{Float64}, x)
234+
= sys.B[:, duinds]
235+
D = sys.D[:, 1:nu]
236+
= sys.D[:, duinds]
237+
iszero(fm(D̄)) || error("Nonzero feedthrough matrix from input derivative not supported")
238+
if descriptor
239+
Iu = u_with_du_inds .== (1:nu)'
240+
E = [I(nx) -B̄; zeros(ndu, nx+ndu)]
235241

236-
Ae = cat(sys.A, -I(ndu), dims=(1,2))
237-
Be = [B; Iu]
238-
Ce = [fm(sys.C) zeros(ny, ndu)]
239-
De = fm(sys.D[:, 1:nu])
240-
dsys = dss(Ae, E, Be, Ce, De)
241-
ss(RobustAndOptimalControl.DescriptorSystems.dss2ss(dsys)[1])
242+
Ae = cat(sys.A, -I(ndu), dims=(1,2))
243+
# Ae[1:nx, nx+1:end] .= B
244+
Be = [B; Iu]
245+
Ce = [fm(sys.C) zeros(ny, ndu)]
246+
De = fm(D)
247+
dsys = dss(Ae, E, Be, Ce, De)
248+
return ss(RobustAndOptimalControl.DescriptorSystems.dss2ss(dsys)[1])
249+
else
250+
ss(sys.A, B, sys.C, D) + ss(sys.A, B̄, sys.C, D̄)*tf('s')
251+
end
242252
end
243253

244254
for f in [:sensitivity, :comp_sensitivity, :looptransfer]
@@ -513,7 +523,11 @@ Linearize `sys` around the trajectory `sol` at times `t`. Returns a vector of `S
513523
function trajectory_ss(sys, inputs, outputs, sol; t = _max_100(sol.t), allow_input_derivatives = false, fuzzer = nothing, verbose = true, kwargs...)
514524
maximum(t) > maximum(sol.t) && @warn("The maximum time in `t`: $(maximum(t)), is larger than the maximum time in `sol.t`: $(maximum(sol.t)).")
515525
minimum(t) < minimum(sol.t) && @warn("The minimum time in `t`: $(minimum(t)), is smaller than the minimum time in `sol.t`: $(minimum(sol.t)).")
516-
lin_fun, ssys = linearization_function(sys, inputs, outputs; kwargs...)
526+
527+
# NOTE: we call linearization_funciton twice :( The first call is to get x=unknowns(ssys), the second call provides the operating points.
528+
# lin_fun, ssys = linearization_function(sys, inputs, outputs; warn_initialize_determined = false, kwargs...)
529+
lin_fun, ssys = linearization_function(sys, inputs, outputs; warn_initialize_determined = false, kwargs...)
530+
517531
x = unknowns(ssys)
518532
defs = ModelingToolkit.defaults(sys)
519533
ops = map(t) do ti
@@ -526,8 +540,10 @@ function trajectory_ss(sys, inputs, outputs, sol; t = _max_100(sol.t), allow_inp
526540
ops = reduce(vcat, opsv)
527541
t = repeat(t, inner = length(ops) ÷ length(t))
528542
end
543+
# lin_fun, ssys = linearization_function(sys, inputs, outputs; op=ops[1], initialize, kwargs...)
529544
lins = map(zip(ops, t)) do (op, t)
530545
linearize(ssys, lin_fun; op, t, allow_input_derivatives)
546+
# linearize(sys, inputs, outputs; op, t, allow_input_derivatives, initialize=false)[1]
531547
end
532548
(; linsystems = [ss(l...) for l in lins], ssys, ops)
533549
end
@@ -662,7 +678,7 @@ function GainScheduledStateSpace(systems, vt; interpolator, x = zeros(systems[1]
662678
@variables x(t)[1:nx]=x [
663679
description = "State variables of gain-scheduled statespace system $name",
664680
]
665-
@variables v(t) = 0 [
681+
@variables v(t) [
666682
description = "Scheduling variable of gain-scheduled statespace system $name",
667683
]
668684

test/test_ODESystem.jl

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
using ControlSystemsMTK,
2-
ControlSystemsBase, ModelingToolkit, RobustAndOptimalControl
2+
ControlSystemsBase, ModelingToolkit, RobustAndOptimalControl, Test
33
import ModelingToolkitStandardLibrary.Blocks as Blocks
44
using OrdinaryDiffEqNonlinearSolve, OrdinaryDiffEqRosenbrock
5+
using LinearAlgebra
56
conn = ModelingToolkit.connect
67
connect = ModelingToolkit.connect
78
## Test SISO (single input, single output) system
@@ -40,33 +41,34 @@ sol2 = solve(prob, Rodas5())
4041
isinteractive() && plot(sol2)
4142

4243
Pc = complete(P)
43-
Q = ControlSystemsMTK.build_quadratic_cost_matrix(Pc, [Pc.input.u], [Pc.x[1] => 2.0])
44+
op = Dict(Pc.input.u => 0.0)
45+
Q = ControlSystemsMTK.build_quadratic_cost_matrix(Pc, [Pc.input.u], [Pc.x[1] => 2.0]; op)
4446
@test Q[] 2.0
4547

46-
Q = ControlSystemsMTK.build_quadratic_cost_matrix(Pc, [Pc.input.u], [Pc.output.u => 2.0])
48+
Q = ControlSystemsMTK.build_quadratic_cost_matrix(Pc, [Pc.input.u], [Pc.output.u => 2.0]; op)
4749
@test Q[] 2.0
4850

4951
#Mix states and outputs
50-
Q = ControlSystemsMTK.build_quadratic_cost_matrix(Pc, [Pc.input.u], [Pc.x[1] => 2.0, Pc.output.u => 3])
52+
Q = ControlSystemsMTK.build_quadratic_cost_matrix(Pc, [Pc.input.u], [Pc.x[1] => 2.0, Pc.output.u => 3]; op)
5153
@test Q[] 2.0 + 3
5254

53-
matrices, ssys = linearize(Pc, [Pc.input.u], [Pc.output.u])
55+
matrices, ssys = linearize(Pc, [Pc.input.u], [Pc.output.u]; op)
5456

5557
Q = ControlSystemsMTK.build_quadratic_cost_matrix(matrices, ssys, [Pc.x[1] => 2.0])
5658
@test Q[] 2.0
5759

5860
Q = ControlSystemsMTK.build_quadratic_cost_matrix(matrices, ssys, [Pc.output.u => 2.0])
5961
@test Q[] 2.0
6062

61-
P1 = ss(Pc, [Pc.input.u], [Pc.output.u])
63+
P1 = ss(Pc, [Pc.input.u], [Pc.output.u]; op)
6264
@test P1 == P0
6365

6466

6567

6668
# === Go the other way, ODESystem -> StateSpace ================================
6769
x = unknowns(P) # I haven't figured out a good way to access states, so this is a bit manual and ugly
6870
@unpack input, output = P
69-
P02_named = named_ss(P, [input.u], [output.u])
71+
P02_named = named_ss(P, [input.u], [output.u]; op)
7072
@test P02_named.x == [Symbol("(x(t))[1]")]
7173
@test P02_named.u == [Symbol("input₊u(t)")]
7274
@test P02_named.y == [Symbol("output₊u(t)")]
@@ -76,7 +78,7 @@ P02 = ss(P02_named)
7678

7779
# same for controller
7880
x = unknowns(C)
79-
@nonamespace C02 = named_ss(C, [C.input], [C.output])
81+
@nonamespace C02 = named_ss(C, [C.input], [C.output]; op)
8082
@test ss(C02) == C0
8183

8284

@@ -216,7 +218,8 @@ end
216218

217219
model = SystemModel() |> complete
218220

219-
lsys = named_ss(model, [model.torque.tau.u], [model.inertia1.phi, model.inertia2.phi])
221+
op = Dict(model.inertia1.flange_b.phi => 0.0, model.torque.tau.u => 0)
222+
lsys = named_ss(model, [model.torque.tau.u], [model.inertia1.phi, model.inertia2.phi]; op)
220223
@test -1000 lsys.A
221224
@test -10 lsys.A
222225
@test 1000 lsys.A
@@ -295,7 +298,7 @@ using ModelingToolkitStandardLibrary.Mechanical.TranslationalPosition
295298
using Test
296299

297300
using ControlSystemsMTK
298-
using ControlSystemsMTK.ControlSystemsBase: sminreal, minreal, poles
301+
using ControlSystemsMTK.ControlSystemsBase: sminreal, minreal, poles, ss, tf
299302
connect = ModelingToolkit.connect
300303

301304
@independent_variables t
@@ -348,3 +351,38 @@ op2[cart.f] = 0
348351
@test G.nx == 4
349352
@test G.nu == length(lin_inputs)
350353
@test G.ny == length(lin_outputs)
354+
355+
## Test difficult `named_ss` simplification
356+
using ControlSystemsMTK, ControlSystemsBase, RobustAndOptimalControl
357+
lsys = (A = [0.0 2.778983834717109e8 1.4122312296634873e6 0.0; 0.0 0.0 0.0 0.037848975765016724; 0.0 24.837541148074962 0.12622006230897712 0.0; -0.0 -4.620724819774693 -0.023481719514324866 -0.6841991610512456], B = [-5.042589978197361e8 0.0; -0.0 0.0; -45.068824982602656 -0.0; 8.384511049369085 54.98555939873381], C = [0.0 0.0 0.954929658551372 0.0], D = [0.0 0.0])
358+
359+
# lsys = (A = [-0.0075449237853825925 1.6716817118020731e-6 0.0; 1864.7356343162514 -0.4131578457122937 0.0; 0.011864343330426718 -2.6287085638214332e-6 0.0], B = [0.0 0.0; 0.0 52566.418015009294; 0.0 0.3284546792274811], C = [1.4683007399899215e8 0.0 0.0], D = [-9.157636303058283e7 0.0])
360+
361+
G = ControlSystemsMTK.causal_simplification(lsys, [1=>2])
362+
G2 = ControlSystemsMTK.causal_simplification(lsys, [1=>2], descriptor=false)
363+
G2 = minreal(G2, 1e-12)
364+
365+
@test dcgain(G, 1e-5)[] dcgain(G2, 1e-5)[] rtol=1e-3
366+
@test freqresp(G, 1)[] freqresp(G2, 1)[]
367+
@test freqresp(G, 10)[] freqresp(G2, 10)[]
368+
369+
z = 0.462726166562343204837317130554462562
370+
371+
@test minimum(abs, tzeros(G) .- z) < sqrt(eps())
372+
@test minimum(abs, tzeros(G2) .- z) < sqrt(eps())
373+
374+
# using GenericSchur
375+
376+
Gb = balance_statespace(G)[1]
377+
Gb = minreal(Gb, 1e-8)
378+
@test Gb.nx == 2
379+
@test minimum(abs, tzeros(Gb) .- z) < sqrt(eps())
380+
381+
w = exp10.(LinRange(-12, 2, 2000))
382+
# ControlSystemsBase.bodeplot([G, G2, minreal(G, 1e-8)], w)
383+
384+
385+
##
386+
387+
# S = schur(A,B)
388+
# V = eigvecs(S)

test/test_batchlin.jl

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using ControlSystemsMTK, ModelingToolkit, RobustAndOptimalControl, MonteCarloMeasurements
22
using OrdinaryDiffEqNonlinearSolve, OrdinaryDiffEqRosenbrock
3+
using ModelingToolkitStandardLibrary.Blocks
34
using ModelingToolkit: getdefault
5+
using Test
46
unsafe_comparisons(true)
57

68
# Create a model
@@ -24,7 +26,7 @@ sample_within_bounds((l, u)) = (u - l) * rand() + l
2426
# Create a vector of operating points
2527
N = 10
2628
xs = range(getbounds(x)[1], getbounds(x)[2], length=N)
27-
ops = Dict.(x .=> xs)
29+
ops = Dict.(u.u => 0, x .=> xs)
2830

2931
inputs, outputs = [u.u], [y.u]
3032
Ps, ssys = batch_ss(duffing, inputs, outputs , ops)
@@ -55,11 +57,11 @@ import ModelingToolkitStandardLibrary.Blocks
5557

5658

5759
closed_loop_eqs = [
58-
connect(ref.output, :r, F.input)
59-
connect(F.output, fb.input1)
60-
connect(duffing.y, :y, fb.input2)
61-
connect(fb.output, C.input)
62-
connect(C.output, duffing.u)
60+
ModelingToolkit.connect(ref.output, :r, F.input)
61+
ModelingToolkit.connect(F.output, fb.input1)
62+
ModelingToolkit.connect(duffing.y, :y, fb.input2)
63+
ModelingToolkit.connect(fb.output, C.input)
64+
ModelingToolkit.connect(C.output, duffing.u)
6365
]
6466

6567
@named closed_loop = ODESystem(closed_loop_eqs, t, systems=[duffing, C, fb, ref, F])
@@ -73,6 +75,7 @@ sol = solve(prob, Rodas5P(), abstol=1e-8, reltol=1e-8)
7375

7476
time = 0:0.1:8
7577
inputs, outputs = [duffing.u.u], [duffing.y.u]
78+
op = Dict(u.u => 0)
7679
Ps2, ssys = trajectory_ss(closed_loop, closed_loop.r, closed_loop.y, sol; t=time)
7780
@test length(Ps2) == length(time)
7881
# bodeplot(Ps2)

0 commit comments

Comments
 (0)