Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/ModelingToolkit.jl
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ include("structural_transformation/StructuralTransformations.jl")

@reexport using .StructuralTransformations
include("inputoutput.jl")
include("input_affine_form.jl")

include("adjoints.jl")
include("deprecations.jl")
Expand Down Expand Up @@ -259,6 +260,7 @@ export JumpProblem
export alias_elimination, flatten
export connect, domain_connect, @connector, Connection, AnalysisPoint, Flow, Stream,
instream
export input_affine_form
export initial_state, transition, activeState, entry, ticksInState, timeInState
export @component, @mtkmodel, @mtkcompile, @mtkbuild
export isinput, isoutput, getbounds, hasbounds, getguess, hasguess, isdisturbance,
Expand Down
49 changes: 49 additions & 0 deletions src/input_affine_form.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""
input_affine_form(eqs, inputs)

Extract control-affine (input-affine) form from symbolic equations.

Given a system of equations of the form `ẋ = F(x, u)` where `x` is the state
and `u` are the inputs, this function extracts the control-affine representation:
`ẋ = f(x) + g(x)*u`

where:
- `f(x)` is the drift term (dynamics without inputs)
- `g(x)` is the input matrix

# Arguments
- `eqs`: Vector of symbolic equations representing the system dynamics
- `inputs`: Vector of input/control variables

# Returns
- `f`: The drift vector f(x)
- `g`: The input matrix g(x)

# Example
```julia
using ModelingToolkit, Symbolics

@variables x1 x2 u1 u2
state = [x1, x2]
inputs = [u1, u2]

# Define system equations: ẋ = F(x, u)
eqs = [
D(x1) ~ -x1 + 2*x2 + u1,
D(x2) ~ x1*x2 - x2 + u1 + 2*u2
]

# Extract control-affine form
f, g = input_affine_form(eqs, inputs)
```

# Notes
The function assumes that the equations are affine in the inputs. If the equations
are nonlinear in the inputs, an error is thrown.
"""
function input_affine_form(eqs, inputs)
any(is_alg_equation, eqs) && error("All equations must be differential equations.")
g, f, flag = Symbolics.linear_expansion(getfield.(eqs, :rhs), inputs)
flag || error("The system is not affine in the inputs.")
return f, g
end
137 changes: 137 additions & 0 deletions test/input_affine_form.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
using ModelingToolkit, Test
using Symbolics
using StaticArrays

rhs(eq) = simplify(eq.rhs)

@testset "input_affine_form" begin
# Test with simple linear system
@testset "Simple linear system" begin
@variables x1 x2 u1 u2
state = [x1, x2]
inputs = [u1, u2]

eqs = D.(state) .~ [
-x1 + 2*x2 + u1,
x1*x2 - x2 + u1 + 2*u2
]

f, g = input_affine_form(eqs, inputs)

# Verify reconstruction
eqs_reconstructed = f + g * inputs
@test isequal(Symbolics.simplify.(eqs_reconstructed), rhs.(eqs))

# Check dimensions
@test length(f) == length(eqs)
@test size(g) == (length(eqs), length(inputs))
end

# Test with Segway dynamics example
@testset "Segway dynamics" begin
# Segway parameters
grav = 9.81
R = 0.195
M = 2 * 2.485
Jc = 2 * 0.0559
L = 0.169
m = 44.798
Jg = 3.836
m0 = 52.710
J0 = 5.108
Km = 2 * 1.262
bt = 2 * 1.225

# Dynamics of Segway in Euler-Lagrange form
Dq(q) = [m0 m*L*cos(q[2]); m*L*cos(q[2]) J0]
function H(q, q̇)
return SA[
-m * L * sin(q[2]) * q̇[2] + bt * (q̇[1] - R * q̇[2]) / R,
-m * grav * L * sin(q[2]) - bt * (q̇[1] - R * q̇[2])
]
end
B(q) = SA[Km / R, -Km]

# Convert to control affine form
function f_seg(x)
q, q̇ = x[SA[1, 2]], x[SA[3, 4]]
return [q̇; -Dq(q) \ H(q, q̇)]
end
function g_seg(x)
q, q̇ = x[SA[1, 2]], x[SA[3, 4]]
return [SA[0, 0]; Dq(q) \ B(q)]
end

# Trace dynamics symbolically
@variables q1 q2 qd1 qd2 u
x = [q1; q2; qd1; qd2]
inputs = [u]
eqs = D.(x) .~ f_seg(x) + g_seg(x) * u

# Extract control-affine form
fe, ge = input_affine_form(eqs, inputs)

# Test reconstruction
eqs2 = fe + ge * inputs
diff = Symbolics.simplify.(eqs2 - rhs.(eqs), expand = true)

# The difference should be zero or very close to zero symbolically
# We test numerically since symbolic simplification might not be perfect
f2, _ = build_function(fe, x, expression = false)
g2, _ = build_function(ge, x, expression = false)

for i in 1:10
x_val = rand(length(x))
@test f2(x_val) ≈ f_seg(x_val) rtol=1e-10
@test g2(x_val) ≈ g_seg(x_val) rtol=1e-10
end
end

# Test with multiple inputs
@testset "Multiple inputs" begin
@variables x1 x2 x3 u1 u2
state = [x1, x2, x3]
inputs = [u1, u2]

eqs = D.(state) .~ [
x2,
x3,
-x1 - 2*x2 - x3 + u1 + 3*u2
]

f, g = input_affine_form(eqs, inputs)

# Expected results
f_expected = [x2, x3, -x1 - 2*x2 - x3]
g_expected = [0 0; 0 0; 1 3]

@test isequal(Symbolics.simplify.(f), Symbolics.simplify.(f_expected))

# Test g matrix elements
for i in 1:size(g, 1), j in 1:size(g, 2)

@test isequal(Symbolics.simplify(g[i, j]), g_expected[i, j])
end
end

# Test with nonlinear state dynamics
@testset "Nonlinear state dynamics" begin
@variables x1 x2 u
state = [x1, x2]
inputs = [u]

eqs = D.(state) .~ [
x2,
-sin(x1) - x2 + u
]

f, g = input_affine_form(eqs, inputs)

# Expected results
f_expected = [x2, -sin(x1) - x2]
g_expected = reshape([0, 1], 2, 1)

@test isequal(Symbolics.simplify.(f), Symbolics.simplify.(f_expected))
@test isequal(g, g_expected)
end
end
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ end
@safetestset "Direct Usage Test" include("direct.jl")
@safetestset "System Linearity Test" include("linearity.jl")
@safetestset "Input Output Test" include("input_output_handling.jl")
@safetestset "Input Affine Form Test" include("input_affine_form.jl")
@safetestset "Clock Test" include("clock.jl")
@safetestset "ODESystem Test" include("odesystem.jl")
@safetestset "Dynamic Quantities Test" include("dq_units.jl")
Expand Down
Loading