OK – here is an “academic” example with my “linearize” function linearize_partial_model
.
- I assume that the model is an Index 1 DAE, so not for any problem
- Base the algorithm on a “partial”/underdetermined model
- “Trusts” that
states(mod)
and equations(mod)
have not changed the variables and equations through the ODESystem
constructor.
# Packages -- not sure if everything is used
using ModelingToolkit
using ModelingToolkit: inputs, outputs
using DifferentialEquations
using Plots, LaTeXStrings
using ControlSystems, ControlSystemsMTK
using LinearAlgebra
using Symbolics
# My linearization function
function linearize_partial_model(mod_p, u_mod, y_mod; x_opt = [])
#
# This function assumes that `mod_p` is an index 1 DAE
#
# mod_p: ModelingToolkit partial model, i.e., with inputs undefined
# u_mod: vector of variables which are inputs in `states(mod_p)`
# y_mod: vector of variables in `state(mod_p)` which are outputs (inputs not allowed)
# x_opt: optional state vector ξ if other than differential variables x
#
# Unknown dependent variables v
v_mod = states(mod_p)
# Finding vector of states and vector of equations
eqs = equations(mod_p)
n_v = length(v_mod)
n_eqs = length(eqs)
n_y = length(y_mod)
#
# Finding indices of differential and algebraic equations
# -- extracting lhs and rhs of equations
eqs_lhs = [eq.lhs for eq in eqs]
eqs_rhs = [eq.rhs for eq in eqs]
# -- vector of differential of unknown variables
dvdt_mod = Differential(t).(v_mod)
# -- assuming lhs of equations has only one occurrence of
# -- differential of a given variable
id = [findfirst(isequal.(eq,dvdt_mod)) for eq in eqs_lhs]
# -- finding indices of differential and algebraic equations
idxs_eq_d = id[id .!== nothing]
idxs_eq_a = setdiff(1:n_eqs,idxs_eq_d)
#
# Finding vector field for DAE
eq_field = [eqs_rhs[idxs_eq_d]; eqs_rhs[idxs_eq_a].-eqs_lhs[idxs_eq_a]]
# Finding Jacobian
J = Symbolics.jacobian(eq_field,v_mod)
#
# Finding indices for differential variables, inputs,
# algebraic variables, and outputs
# -- differential variables
id = [findfirst(isequal.(Differential(t)(var),eqs_lhs[idxs_eq_d])) for var in v_mod]
idxs_x = id[id .!== nothing]
# -- inputs
idxs_u = [findmax(isequal.(u,v_mod))[2] for u in u_mod]
# -- finding algebraic variables
idxs_z = setdiff(1:n_v,idxs_x,idxs_u)
# -- outputs
idxs_y = [findmax(isequal.(y,v_mod))[2] for y in y_mod]
# -- checking that outputs are not inputs
if !isdisjoint(idxs_y,idxs_u)
println("Outputs can not be inputs.")
println("Outputs: $(y_mod)")
println("Inputs: $(u_mod)")
end
#
# Deconstructing Jacobian
J_d_x = J[idxs_eq_d,idxs_x]
J_d_z = J[idxs_eq_d,idxs_z]
J_d_u = J[idxs_eq_d,idxs_u]
J_a_x = J[idxs_eq_a,idxs_x]
J_a_z = J[idxs_eq_a,idxs_z]
J_a_u = J[idxs_eq_a,idxs_u]
#
# LTI assuming states are differential variables
# -- A and B matrices
AA = simplify.(J_d_x - J_d_z*(J_a_z\J_a_x))
BB = simplify.(J_d_u - J_d_z*(J_a_z\J_a_u))
# --Jacobian matrix for outputs; contains ones and zeros
J_y = zeros(n_y,n_v)
for i in 1:n_y
J_y[i,:] = isequal.(y_mod[i],v_mod)
end
J_y_x = J_y[:,idxs_x]
J_y_z = J_y[:,idxs_z]
J_y_u = J_y[:,idxs_u]
# -- C and D matrices
CC = simplify.(J_y_x - J_y_z*(J_a_z\J_a_x))
DD = simplify.(J_y_u - J_y_z*(J_a_z\J_a_u))
#
# Checking whether optional states are requested
n_x = length(idxs_x)
n_ξ = length(x_opt)
#
if n_ξ == 0
return AA, BB, CC, DD, v_mod[idxs_x]
elseif n_ξ != n_x
println("Incorrect size of optional state")
return AA, BB, CC, DD, v_mod[idxs_x]
else
# Finding transformation matrix TT from original to new state
# -- indices for new state
idxs_ξ = [findmax(isequal.(x,v_mod))[2] for x in x_opt]
# Jacobian matrix for new state; contains ones and zeros
J_ξ = zeros(n_ξ,n_v)
for i in 1:n_ξ
J_ξ[i,:] = isequal.(x_opt[i],v_mod)
end
J_ξ_x = J_ξ[:,idxs_x]
J_ξ_z = J_ξ[:,idxs_z]
J_ξ_u = J_ξ[:,idxs_u]
#
TT = simplify.(J_ξ_x - J_ξ_z*(J_a_z\J_a_x))
TT_u = simplify.(J_ξ_u - J_ξ_z*(J_a_z\J_a_u))
# -- not allowed to have inputs in ξ, i.e., must require TT_u ≡ 0
if sum(TT_u) != 0
println("Optional states lead to input derivatives")
return AA, BB, CC, DD, v_mod[idxs_x]
end
AA_ = simplify.(TT*AA*inv(TT))
BB_ = simplify.(TT*BB)
CC_ = simplify.(CC*inv(TT))
DD_ = DD
#
return AA_, BB_, CC_, DD_, x_opt
end
end
# External forcing functions
u_ṁ_step(t) = t<2 ? 1 : 1.5
u_u_step(t) = t<15 ? 1 : 0.8
u_T_i_step(t) = t<10 ? 288.15 : 293.15
u_Q̇_step(t) = t<5 ? 0 : 1e5
u_ṁ_const(t) = 1
u_u_const(t) = 1
u_T_i_const(t) = 288.15
u_Q̇_const(t) = 0
# Parameters, variables, registered functions
@parameters ρ=1e3 A=1.5e-2 K_e=2 h_ς=3e-1 Ĥ_o=0 ĉ_p=4.2e3 T_o=298.15 p_o=1.01e5 p_a=1.01e5 g=9.81
@variables t h(t)=1.5e-1 m(t)=ρ*h*A ṁ_i(t) ṁ_e(t) p(t)=1.01e5 T(t)=293.15
@variables Ĥ(t)=Ĥ_o + ĉ_p*(T - T_o) + (p - p_o)/ρ
@variables U(t)= m*Ĥ - p*m/ρ Ḣ_i(t) Ḣ_e(t) Ẇ_v(t) Q̇(t)
@variables V(t) u(t)
@variables Ĥ_i(t) T_i(t) Q̇_(t)
D = Differential(t)
@register_symbolic u_ṁ(t)
@register_symbolic u_u(t)
@register_symbolic u_T_i(t)
@register_symbolic u_Q̇_(t)
# DAE Model: partial for linearization, and complete for simulation
# DAE Formulation
eqs_p = [D(m) ~ ṁ_i - ṁ_e,
D(U) ~ Ḣ_i - Ḣ_e + Ẇ_v + Q̇,
m ~ ρ*V,
V ~ A*h,
ṁ_e ~ K_e*u*sqrt(h/h_ς),
U ~ m*Ĥ - p*V,
Ĥ ~ Ĥ_o + ĉ_p*(T-T_o) + (p-p_o)/ρ,
Ĥ_i ~ Ĥ_o + ĉ_p*(T_i-T_o) + (p_a-p_o)/ρ,
Ḣ_i ~ ṁ_i*Ĥ_i,
Ḣ_e ~ ṁ_e*Ĥ,
Ẇ_v ~ -p*(ṁ_i - ṁ_e)/ρ,
Q̇ ~ Q̇_*h,
p ~ p_a + ρ*g*h]
@named sys_p = ODESystem(eqs_p)
eqs_i = [ṁ_i ~ u_ṁ(t), u ~ u_u(t), T_i ~ u_T_i(t), Q̇_ ~ u_Q̇_(t)]
@named sys_i = ODESystem(eqs_i)
sys = extend(sys_i, sys_p)
sys_simp = structural_simplify(sys)
states(sys_simp)
# ODE formulation of system
# ODE Formulation
eqs_ode_p = [D(h) ~ (ṁ_i - K_e*u*sqrt(h/h_ς))/(ρ*A),
D(T) ~ (ṁ_i*(ĉ_p*(T_i - T) - g*h) + Q̇_*h)/(ρ*A*h*ĉ_p)]
@named sys_ode_p = ODESystem(eqs_ode_p)
sys_ode = extend(sys_i, sys_ode_p)
sys_ode_simp = structural_simplify(sys_ode)
states(sys_ode_simp)
# "Partial" equations -- these are assumed to be the same as `eqs_p`:
equations(sys_p)
# "States" of partial model -- these are assumed to be the same as the variables used in `eqs_p`
v_sys = states(sys_p)
# Specifying vectors of inputs and outputs for the linearization
u_sys = [ṁ_i,u,T_i,Q̇_]
y_sys = [h,T]
# Linearization -- uses differential variables (here: `[m, U]`) as states in result
AA, BB, CC, DD, state = linearize_partial_model(sys_p, u_sys,y_sys)
state
# Linearization -- with specification of states to be `x_opt = [h,T]` instead of default
AA, BB, CC, DD, state = linearize_partial_model(sys_p, u_sys,y_sys; x_opt=[h,T])
state
# Resulting matrices
AA
BB
CC
DD
# Running numeric model (complete model) to find steady states
# -- specifying driving functions
u_ṁ(t) = u_ṁ_const(t)
u_u(t) = u_u_const(t)
u_T_i(t) = u_T_i_const(t)
u_Q̇_(t) = u_Q̇_const(t)
# -- finding steady states
tspan = (0.0,1e3)
prob = ODEProblem(sys_simp, [], tspan)
sol = solve(prob)
name_s = states(sys)
s_ss = sol(tspan[2]; idxs = name_s)
m_ss = sol(tspan[2]; idxs = m)
U_ss = sol(tspan[2]; idxs = U)
Ĥ_ss = sol(tspan[2]; idxs = Ĥ)
h_ss = sol(tspan[2]; idxs = h)
T_ss = sol(tspan[2]; idxs = T)
# -- evaluating numerical values of AA, BB, CC, DD
default_vals = ModelingToolkit.defaults(sys_p)
linsys_symb_A = substitute(AA, Dict(name_s .=> s_ss)) |> x -> substitute(x, default_vals) |> Symbolics.value
linsys_symb_B = substitute(BB, Dict(name_s .=> s_ss)) |> x -> substitute(x, default_vals)
linsys_symb_C = substitute(CC, Dict(name_s .=> s_ss)) |> x -> substitute(x, default_vals)
linsys_symb_D = substitute(DD, Dict(name_s .=> s_ss)) |> x -> substitute(x, default_vals)
# Linearizing the model using MTK
sys_in = [ṁ_i, u, T_i, Q̇_]
sys_out = [h, T]
op_0 = Dict(m=>m_ss, U=>U_ss, Ĥ=>Ĥ_ss, ṁ_i=>u_ṁ(0), u=>u_u(0), T_i=>u_T_i(0), Q̇_=>u_Q̇_(0))
op_ode_0 = Dict(h=>h_ss, T=>T_ss, ṁ_i=>u_ṁ(0), u=>u_u(0), T_i=>u_T_i(0), Q̇_=>u_Q̇_(0))
mats, sys_ = linearize(sys_p, sys_in, sys_out, op=op_0)
linsys_mtk = ss(mats...)
# Linearizing the model with ControlSystemsMTK
linsys_csmtk = named_ss(sys_p, sys_in, sys_out; op=op_0)
Some comments:
- MTK finds a linear model with 3 states based on
sys_p
: m
, U
, and Ĥ
.
- ControlSystemsMTK finds exactly the same 3 state model as MTK in this case.
- Both MTK and ControlSystemsMTK finds the same 2 state model for the
sys_ode_p
model, i.e., the ODE formulation.
- Function
linearize_partial_model
finds 2 states based on sys_p
both when the default states are used (= differential variables [m, U]
), and when optional states [h, T]
are given; the models are symbolic. When substituting in numerical values, the version with state [h, T]
is identical to the one found from MTK and ControlSystemsMTK based on sys_ode_p
.
- Function
linearize_partial_model
finds 2 states based on sys_ode_p
. When the default states are used ([h, T]
), the symbolic model appears to be the same as the one found based on the DAE sys_p
with optional states ([h, T]
) [there are some terms that have not been simplified away, so they appear to be slightly different].
- The numeric evaluation of the matrices found from
linearize_partial_model
are of type Num
, and I have not been able to insert them into ControlSystem’s ss
function.
- Function
linearize_partial_model
is extremely poorly tested, so I’m sure there are some bugs in it. Also, it is relatively limited in scope.
- I’m sure
linearize_partial_model
can be made to run more efficiently…
- I have assumed that
ODESystem()
does not change the order of equations, remove equations, or remove states. If this assumption is false, some minor modifications must be introduced.
- A slight surprise when it comes to symbolic linear algebra…
A\B
works, but B/A
does not work. In the latter case, I had to do B*inv(A)
.