Skip to content

Commit db1b16f

Browse files
authored
add generic transfer function (#233)
* add generic transfer function * formatting * add test
1 parent 5ee7453 commit db1b16f

File tree

3 files changed

+151
-1
lines changed

3 files changed

+151
-1
lines changed

src/Blocks/Blocks.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ include("sources.jl")
2525
export Limiter, DeadZone, SlewRateLimiter
2626
include("nonlinear.jl")
2727

28-
export Integrator, Derivative, FirstOrder, SecondOrder, StateSpace
28+
export Integrator, Derivative, FirstOrder, SecondOrder, StateSpace, TransferFunction
2929
export PI, LimPI, PID, LimPID
3030
include("continuous.jl")
3131

src/Blocks/continuous.jl

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,3 +553,85 @@ linearized around the operating point `x₀, u₀`, we have `y0, u0 = h(x₀, u
553553
end
554554

555555
StateSpace(A, B, C, D = nothing; kwargs...) = StateSpace(; A, B, C, D, kwargs...)
556+
557+
symbolic_eps(t) = eps(t)
558+
@register_symbolic symbolic_eps(t)
559+
560+
"""
561+
TransferFunction(; b, a, name)
562+
563+
A single input, single output, linear time-invariant system provided as a transfer-function.
564+
```
565+
Y(s) = b(s) / a(s) U(s)
566+
```
567+
where `b` and `a` are vectors of coefficients of the numerator and denominator polynomials, respectively, ordered such that the coefficient of the highest power of `s` is first.
568+
569+
The internal state realization is on controller canonical form, with state variable `x`, output variable `y` and input variable `u`. For numerical robustness, the realization used by the integrator is scaled by the last entry of the `a` parameter. The internally scaled state variable is available as `x_scaled`.
570+
571+
To set the initial state, it's recommended to set the initial condition for `x`, and let that of `x_scaled` be computed automatically.
572+
573+
# Parameters:
574+
- `b`: Numerator polynomial coefficients, e.g., `2s + 3` is specified as `[2, 3]`
575+
- `a`: Denomenator polynomial coefficients, e.g., `s² + 2ωs + ω^2` is specified as `[1, 2ω, ω^2]`
576+
577+
# Connectors:
578+
- `input`
579+
- `output`
580+
581+
See also [`StateSpace`](@ref) which handles MIMO systems, as well as [ControlSystemsMTK.jl](https://juliacontrol.github.io/ControlSystemsMTK.jl/stable/) for an interface between [ControlSystems.jl](https://juliacontrol.github.io/ControlSystems.jl/stable/) and ModelingToolkit.jl for advanced manipulation of transfer functions and linear statespace systems. For linearization, see [`linearize`](@ref) and [Linear Analysis](https://docs.sciml.ai/ModelingToolkitStandardLibrary/stable/API/linear_analysis/).
582+
"""
583+
@component function TransferFunction(; b = [1], a = [1, 1], name)
584+
nb = length(b)
585+
na = length(a)
586+
nb <= na ||
587+
error("Transfer function is not proper, the numerator must not be longer than the denominator")
588+
nx = na - 1
589+
nbb = max(0, na - nb)
590+
591+
@named begin
592+
input = RealInput()
593+
output = RealOutput()
594+
end
595+
596+
@parameters begin
597+
b[1:nb] = b,
598+
[
599+
description = "Numerator coefficients of transfer function (e.g., 2s + 3 is specified as [2,3])",
600+
]
601+
a[1:na] = a,
602+
[
603+
description = "Denominator coefficients of transfer function (e.g., `s² + 2ωs + ω^2` is specified as [1, 2ω, ω^2])",
604+
]
605+
bb[1:(nbb + nb)] = [zeros(nbb); b]
606+
d = bb[1] / a[1], [description = "Direct feedthrough gain"]
607+
end
608+
609+
a = collect(a)
610+
@parameters a_end = ifelse(a[end] > 100 * symbolic_eps(sqrt(a' * a)), a[end], 1.0)
611+
612+
pars = [collect(b); a; collect(bb); d; a_end]
613+
@variables begin
614+
x(t)[1:nx] = zeros(nx),
615+
[description = "State of transfer function on controller canonical form"]
616+
x_scaled(t)[1:nx] = collect(x) * a_end, [description = "Scaled vector x"]
617+
u(t), [description = "Input of transfer function"]
618+
y(t), [description = "Output of transfer function"]
619+
end
620+
621+
x = collect(x)
622+
x_scaled = collect(x_scaled)
623+
624+
sts = [x; x_scaled; y; u]
625+
626+
if nx == 0
627+
eqs = [y ~ d * u]
628+
else
629+
eqs = [D(x_scaled[1]) ~ (-a[2:na]'x_scaled + a_end * u) / a[1]
630+
D.(x_scaled[2:nx]) .~ x_scaled[1:(nx - 1)]
631+
y ~ ((bb[2:na] - d * a[2:na])'x_scaled) / a_end + d * u
632+
x .~ x_scaled ./ a_end]
633+
end
634+
push!(eqs, input.u ~ u)
635+
push!(eqs, output.u ~ y)
636+
compose(ODESystem(eqs, t, sts, pars; name = name), input, output)
637+
end

test/Blocks/continuous.jl

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using ModelingToolkit, ModelingToolkitStandardLibrary, OrdinaryDiffEq
22
using ModelingToolkitStandardLibrary.Blocks
33
using OrdinaryDiffEq: ReturnCode.Success
4+
using Test
45

56
@parameters t
67

@@ -408,4 +409,71 @@ end
408409
@test sol[plant.output.u][end]re_val atol=1e-3 # zero control error after 100s
409410
@test all(-1.5 .<= sol[pid_controller.ctr_output.u] .<= 1.5) # test limit
410411
end
412+
413+
@testset "TransferFunction" begin
414+
pt1_func(t, k, T) = k * (1 - exp(-t / T)) # Known solution to first-order system
415+
416+
@named c = Constant(; k = 1)
417+
@named pt1 = TransferFunction(b = [1.2], a = [3.14, 1])
418+
@named iosys = ODESystem(connect(c.output, pt1.input), t, systems = [pt1, c])
419+
sys = structural_simplify(iosys)
420+
prob = ODEProblem(sys, Pair[pt1.a_end => 1], (0.0, 100.0))
421+
sol = solve(prob, Rodas4())
422+
@test sol.retcode == Success
423+
@test sol[pt1.output.u]pt1_func.(sol.t, 1.2, 3.14) atol=1e-3
424+
425+
# Test logic for a_end by constructing an integrator
426+
@named c = Constant(; k = 1)
427+
@named pt1 = TransferFunction(b = [1.2], a = [3.14, 0])
428+
@named iosys = ODESystem(connect(c.output, pt1.input), t, systems = [pt1, c])
429+
sys = structural_simplify(iosys)
430+
prob = ODEProblem(sys, Pair[pt1.a_end => 1], (0.0, 100.0))
431+
sol = solve(prob, Rodas4())
432+
@test sol.retcode == Success
433+
@test sol[pt1.output.u] sol.t .* (1.2 / 3.14)
434+
@test sol[pt1.x[1]] sol.t .* (1 / 3.14) # Test that scaling of state works properly
435+
436+
# Test higher order
437+
438+
function pt2_func(t, k, w, d)
439+
y = if d == 0
440+
-k * (-1 + cos(t * w))
441+
else
442+
d = complex(d)
443+
real(k * (1 +
444+
(-cosh(sqrt(-1 + d^2) * t * w) -
445+
(d * sinh(sqrt(-1 + d^2) * t * w)) / sqrt(-1 + d^2)) /
446+
exp(d * t * w)))
447+
end
448+
end
449+
450+
k, w, d = 1.0, 1.0, 0.5
451+
@named pt1 = TransferFunction(b = [w^2], a = [1, 2d * w, w^2])
452+
@named iosys = ODESystem(connect(c.output, pt1.input), t, systems = [pt1, c])
453+
sys = structural_simplify(iosys)
454+
prob = ODEProblem(sys, Pair[pt1.a_end => 1], (0.0, 100.0))
455+
sol = solve(prob, Rodas4())
456+
@test sol.retcode == Success
457+
@test sol[pt1.output.u]pt2_func.(sol.t, k, w, d) atol=1e-3
458+
459+
# test zeros (high-pass version of first test)
460+
@named c = Constant(; k = 1)
461+
@named pt1 = TransferFunction(b = [1, 0], a = [1, 1])
462+
@named iosys = ODESystem(connect(c.output, pt1.input), t, systems = [pt1, c])
463+
sys = structural_simplify(iosys)
464+
prob = ODEProblem(sys, Pair[pt1.a_end => 1], (0.0, 100.0))
465+
sol = solve(prob, Rodas4())
466+
@test sol.retcode == Success
467+
@test sol[pt1.output.u]1 .- pt1_func.(sol.t, 1, 1) atol=1e-3
468+
@test sol[pt1.x[1]]pt1_func.(sol.t, 1, 1) atol=1e-3 # Test that scaling of state works properly
469+
470+
# Test with no state
471+
@named pt1 = TransferFunction(b = [2.7], a = [pi])
472+
@named iosys = ODESystem(connect(c.output, pt1.input), t, systems = [pt1, c])
473+
sys = structural_simplify(iosys)
474+
prob = ODEProblem(sys, Pair[pt1.a_end => 1], (0.0, 100.0))
475+
sol = solve(prob, Rodas4())
476+
@test sol.retcode == Success
477+
@test all(==(2.7 / pi), sol[pt1.output.u])
478+
end
411479
end

0 commit comments

Comments
 (0)