Skip to content

Commit 6b60b1a

Browse files
committed
add input-affine equation factorization
1 parent 68f5e73 commit 6b60b1a

File tree

4 files changed

+192
-0
lines changed

4 files changed

+192
-0
lines changed

src/ModelingToolkit.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ include("structural_transformation/StructuralTransformations.jl")
224224

225225
@reexport using .StructuralTransformations
226226
include("inputoutput.jl")
227+
include("input_affine_form.jl")
227228

228229
include("adjoints.jl")
229230
include("deprecations.jl")
@@ -259,6 +260,7 @@ export JumpProblem
259260
export alias_elimination, flatten
260261
export connect, domain_connect, @connector, Connection, AnalysisPoint, Flow, Stream,
261262
instream
263+
export input_affine_form
262264
export initial_state, transition, activeState, entry, ticksInState, timeInState
263265
export @component, @mtkmodel, @mtkcompile, @mtkbuild
264266
export isinput, isoutput, getbounds, hasbounds, getguess, hasguess, isdisturbance,

src/input_affine_form.jl

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""
2+
input_affine_form(eqs, inputs)
3+
4+
Extract control-affine (input-affine) form from symbolic equations.
5+
6+
Given a system of equations of the form `ẋ = F(x, u)` where `x` is the state
7+
and `u` are the inputs, this function extracts the control-affine representation:
8+
`ẋ = f(x) + g(x)*u`
9+
10+
where:
11+
- `f(x)` is the drift term (dynamics without inputs)
12+
- `g(x)` is the input matrix
13+
14+
# Arguments
15+
- `eqs`: Vector of symbolic equations representing the system dynamics
16+
- `inputs`: Vector of input/control variables
17+
18+
# Returns
19+
- `f`: The drift vector f(x)
20+
- `g`: The input matrix g(x)
21+
22+
# Example
23+
```julia
24+
using ModelingToolkit, Symbolics
25+
26+
@variables x1 x2 u1 u2
27+
state = [x1, x2]
28+
inputs = [u1, u2]
29+
30+
# Define system equations: ẋ = F(x, u)
31+
eqs = [
32+
-x1 + 2*x2 + u1,
33+
x1*x2 - x2 + u1 + 2*u2
34+
]
35+
36+
# Extract control-affine form
37+
f, g = input_affine_form(eqs, inputs)
38+
```
39+
40+
# Notes
41+
The function assumes that the equations are affine in the inputs. If the equations
42+
are nonlinear in the inputs, the result may not be meaningful.
43+
"""
44+
function input_affine_form(eqs, inputs)
45+
# Extract the input matrix g(x) by taking coefficients of each input
46+
g = [Symbolics.coeff(Symbolics.simplify(eq, expand=true), u) for eq in eqs, u in inputs]
47+
g = Symbolics.simplify.(g, expand=true)
48+
49+
# Extract the drift term f(x) by substituting inputs = 0
50+
f = Symbolics.substitute.(eqs, Ref(Dict(inputs .=> 0)))
51+
f = Symbolics.simplify.(f, expand=true)
52+
53+
return f, g
54+
end

test/input_affine_form.jl

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
using ModelingToolkit, Test
2+
using Symbolics
3+
using StaticArrays
4+
5+
@testset "input_affine_form" begin
6+
# Test with simple linear system
7+
@testset "Simple linear system" begin
8+
@variables x1 x2 u1 u2
9+
state = [x1, x2]
10+
inputs = [u1, u2]
11+
12+
eqs = [
13+
-x1 + 2*x2 + u1,
14+
x1*x2 - x2 + u1 + 2*u2
15+
]
16+
17+
f, g = input_affine_form(eqs, inputs)
18+
19+
# Verify reconstruction
20+
eqs_reconstructed = f + g * inputs
21+
@test isequal(Symbolics.simplify.(eqs_reconstructed), Symbolics.simplify.(eqs))
22+
23+
# Check dimensions
24+
@test length(f) == length(eqs)
25+
@test size(g) == (length(eqs), length(inputs))
26+
end
27+
28+
# Test with Segway dynamics example
29+
@testset "Segway dynamics" begin
30+
# Segway parameters
31+
grav = 9.81
32+
R = 0.195
33+
M = 2 * 2.485
34+
Jc = 2 * 0.0559
35+
L = 0.169
36+
m = 44.798
37+
Jg = 3.836
38+
m0 = 52.710
39+
J0 = 5.108
40+
Km = 2 * 1.262
41+
bt = 2 * 1.225
42+
43+
# Dynamics of Segway in Euler-Lagrange form
44+
D(q) = [m0 m*L*cos(q[2]); m*L*cos(q[2]) J0]
45+
function H(q, q̇)
46+
return SA[
47+
-m * L * sin(q[2]) * q̇[2] + bt * (q̇[1] - R * q̇[2]) / R,
48+
-m * grav * L * sin(q[2]) - bt * (q̇[1] - R * q̇[2]),
49+
]
50+
end
51+
B(q) = SA[Km / R, -Km]
52+
53+
# Convert to control affine form
54+
function f_seg(x)
55+
q, q̇ = x[SA[1,2]], x[SA[3,4]]
56+
return [q̇; -D(q) \ H(q, q̇)]
57+
end
58+
function g_seg(x)
59+
q, q̇ = x[SA[1,2]], x[SA[3,4]]
60+
return [SA[0,0]; D(q) \ B(q)]
61+
end
62+
63+
# Trace dynamics symbolically
64+
@variables q1 q2 qd1 qd2 u
65+
x = [q1; q2; qd1; qd2]
66+
inputs = [u]
67+
eqs = f_seg(x) + g_seg(x) * u
68+
69+
# Extract control-affine form
70+
fe, ge = input_affine_form(eqs, inputs)
71+
72+
# Test reconstruction
73+
eqs2 = fe + ge * inputs
74+
diff = Symbolics.simplify.(eqs2 - eqs, expand=true)
75+
76+
# The difference should be zero or very close to zero symbolically
77+
# We test numerically since symbolic simplification might not be perfect
78+
f2, _ = build_function(fe, x, expression=false)
79+
g2, _ = build_function(ge, x, expression=false)
80+
81+
for i = 1:10
82+
x_val = rand(length(x))
83+
@test f2(x_val) f_seg(x_val) rtol=1e-10
84+
@test g2(x_val) g_seg(x_val) rtol=1e-10
85+
end
86+
end
87+
88+
# Test with multiple inputs
89+
@testset "Multiple inputs" begin
90+
@variables x1 x2 x3 u1 u2
91+
state = [x1, x2, x3]
92+
inputs = [u1, u2]
93+
94+
eqs = [
95+
x2,
96+
x3,
97+
-x1 - 2*x2 - x3 + u1 + 3*u2
98+
]
99+
100+
f, g = input_affine_form(eqs, inputs)
101+
102+
# Expected results
103+
f_expected = [x2, x3, -x1 - 2*x2 - x3]
104+
g_expected = [0 0; 0 0; 1 3]
105+
106+
@test isequal(Symbolics.simplify.(f), Symbolics.simplify.(f_expected))
107+
108+
# Test g matrix elements
109+
for i in 1:size(g, 1), j in 1:size(g, 2)
110+
@test isequal(Symbolics.simplify(g[i,j]), g_expected[i,j])
111+
end
112+
end
113+
114+
# Test with nonlinear state dynamics
115+
@testset "Nonlinear state dynamics" begin
116+
@variables x1 x2 u
117+
state = [x1, x2]
118+
inputs = [u]
119+
120+
eqs = [
121+
x2,
122+
-sin(x1) - x2 + u
123+
]
124+
125+
f, g = input_affine_form(eqs, inputs)
126+
127+
# Expected results
128+
f_expected = [x2, -sin(x1) - x2]
129+
g_expected = reshape([0, 1], 2, 1)
130+
131+
@test isequal(Symbolics.simplify.(f), Symbolics.simplify.(f_expected))
132+
@test isequal(g, g_expected)
133+
end
134+
135+
end

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ end
3434
@safetestset "Direct Usage Test" include("direct.jl")
3535
@safetestset "System Linearity Test" include("linearity.jl")
3636
@safetestset "Input Output Test" include("input_output_handling.jl")
37+
@safetestset "Input Affine Form Test" include("input_affine_form.jl")
3738
@safetestset "Clock Test" include("clock.jl")
3839
@safetestset "ODESystem Test" include("odesystem.jl")
3940
@safetestset "Dynamic Quantities Test" include("dq_units.jl")

0 commit comments

Comments
 (0)