|
1 | | -@mtkmodel NotchFilter_Canonical begin |
| 1 | +@mtkmodel PSSE_IEEEST begin |
2 | 2 | @structural_parameters begin |
3 | | - A # numerator s term |
4 | | - B # numerator s^2 term |
5 | | - C # denominator s term |
6 | | - D # denominator s^2 term |
| 3 | + warn = true |
7 | 4 | end |
8 | | - @variables begin |
9 | | - in(t), [input=true] |
10 | | - out(t), [output=true] |
11 | | - x1(t), [guess=1] # x1 = y |
12 | | - x2(t), [guess=0] # x2 = y' |
| 5 | + @parameters begin |
| 6 | + A1=0, [description="Power system stabilizer high frequency filter coefficient"] |
| 7 | + A2=0, [description="Power system stabilizer high frequency filter coefficient"] |
| 8 | + A3=0, [description="Power system stabilizer high frequency filter coefficient"] |
| 9 | + A4=0, [description="Power system stabilizer high frequency filter coefficient"] |
| 10 | + A5=0, [description="Power system stabilizer high frequency filter coefficient"] |
| 11 | + A6=0, [description="Power system stabilizer high frequency filter coefficient"] |
| 12 | + T1=0, [description="PSS first numerator (lead) time constant"] |
| 13 | + T2=0, [description="PSS first denominator (lag) time constant"] |
| 14 | + T3=0, [description="PSS second numerator (lead) time constant"] |
| 15 | + T4=0, [description="PSS second denominator (lag) time constant"] |
| 16 | + T5=1.65, [description="Stabilizer washout numerator time constant"] |
| 17 | + T6=1.65, [description="Stabilizer washout denominator time constant"] |
| 18 | + Ks=6.2, [description="Stabilizer Gain"] |
| 19 | + Lsmax=0.26, [description="Maximum output for stabilizer washout filter"] |
| 20 | + Lsmin=-0.1, [description="Minimum output for stabilizer washout filter"] |
| 21 | + Vcu=Inf, [description="Maximum power system stabilizer output"] |
| 22 | + Vcl=-Inf, [description="Minimum power system stabilizer output"] |
| 23 | + _IEEEST_active = 1, [description="Internal flag to disable/enable the stabilizer"] |
13 | 24 | end |
14 | | - @equations begin |
15 | | - Dt(x1) ~ x2 |
16 | | - Dt(x2) ~ (in + A*x2 + B*((in - x1 - C*x2)/D) - C*x2 - x1)/D |
17 | | - out ~ x1 |
18 | | - end |
19 | | -end |
20 | | - |
21 | | -@mtkmodel PSSE_IEEEST begin |
22 | | - @structural_parameters begin |
23 | | - A1 # Notch filter parameters |
24 | | - A2 # Notch filter parameters |
25 | | - A3 # Notch filter parameters |
26 | | - A4 # Notch filter parameters |
27 | | - A5 # Notch filter parameters |
28 | | - A6 # Notch filter parameters |
29 | | - T1 # Lead/lag time constant, sec |
30 | | - T2 # Lead/lag time constant, sec |
31 | | - T3 # Lead/lag time constant, sec |
32 | | - T4 # Lead/lag time constant, sec |
33 | | - T5 # Wahsout numerator time constant, sec |
34 | | - T6 # Washout denominator time constant, sec |
35 | | - Ks # Stabilizer gains |
36 | | - Lsmax # Maximum stabilizer output, pu |
37 | | - Lsmin # Minimum stabilizer output, pu |
38 | | - Vcu # Stabilizer input cutoff threshold, pu |
39 | | - Vcl # Stabilizer input cutoff threshold, pu |
| 25 | + begin |
| 26 | + _vcu = ModelingToolkit.getdefault(Vcu) |
| 27 | + _vcl = ModelingToolkit.getdefault(Vcl) |
| 28 | + if warn && _vcu != Inf && _vcl != -Inf |
| 29 | + @warn "You set explicit VCU/VCL limits for IEEEST. The output limiting is not active \ |
| 30 | + per default! You need to add a callback, which callback which watches vct(t) state \ |
| 31 | + in comparison to VCU/VCL and sets _IEEEST_active to 0/1 accordingly. Diable this wanrning \ |
| 32 | + by passing warn=false structural parmeter o the IEEEST constructor." |
| 33 | + end |
40 | 34 | end |
41 | 35 | begin |
42 | | - @warn "PSSE_IEEEST is not fully implemented! Filter bypass logic is broken" |
| 36 | + _A1 = ModelingToolkit.getdefault(A1) |
| 37 | + _A2 = ModelingToolkit.getdefault(A2) |
| 38 | + _A3 = ModelingToolkit.getdefault(A3) |
| 39 | + _A4 = ModelingToolkit.getdefault(A4) |
| 40 | + _A5 = ModelingToolkit.getdefault(A5) |
| 41 | + _A6 = ModelingToolkit.getdefault(A6) |
| 42 | + |
| 43 | + # define coefficients (both defualts and symbolic) |
| 44 | + # A6 s² + A5 s + 1 |
| 45 | + # ------------------------------------ |
| 46 | + # (A2 s² + A1 s + 1)(A4 s² + A3 s + 1) |
| 47 | + # or equally |
| 48 | + # A6 s² + A5 s + 1 |
| 49 | + # -------------------------------- |
| 50 | + # (A2 A4) s⁴ + (A1 A4 + A2 A3) s³ + (A1 A3 + A2 + A4) s² + (A1 + A3) s + 1 |
| 51 | + # |
| 52 | + # What we want to do here is to filter out all leading zeros in |
| 53 | + # the nom/denom vectors (otherwise the system is singular) |
| 54 | + # and ignore thos variables |
| 55 | + # I.e. the filter order is actually determined by the parameters |
| 56 | + # at build time! |
| 57 | + |
| 58 | + nom = [ |
| 59 | + (A6) => _A6, |
| 60 | + (A5) => _A5, |
| 61 | + (1) => 1 |
| 62 | + ] |
| 63 | + |
| 64 | + denom = [ |
| 65 | + (A2*A4) => _A2*_A4, |
| 66 | + (A1*A4 + A2*A3) => _A1*_A4 + _A2*_A3, |
| 67 | + (A1*A3 + A2 + A4) => _A1*_A3 + _A2 + _A4, |
| 68 | + (A1 + A3) => _A1 + _A3, |
| 69 | + (1) => 1 |
| 70 | + ] |
| 71 | + |
| 72 | + leading_denom_nom = findfirst(x -> !isapprox(x[2], 0, atol=1e-10), nom) |
| 73 | + _nom = [x[2] for x in nom[leading_denom_nom:end]] |
| 74 | + leading_denom_denom = findfirst(x -> !isapprox(x[2], 0, atol=1e-10), denom) |
| 75 | + _denom = [x[2] for x in denom[leading_denom_denom:end]] |
| 76 | + |
| 77 | + A,B,C,D = siso_tf_to_ss(_nom, _denom) |
| 78 | + guesses = vcat(zeros(size(A,1)-1), 1) |
43 | 79 | end |
| 80 | + |
44 | 81 | @components begin |
45 | 82 | VOTHSG_out = RealOutput() |
46 | 83 | INPUT_in = RealInput() |
47 | 84 | V_CT_in = RealInput() #terminal voltage |
48 | 85 |
|
49 | | - # filter1 = SimpleGain(K=1) |
50 | | - filter1 = NotchFilter_Canonical(A=A5, B=A6, C=A1, D=A2) |
51 | | - # filter2 = NotchFilter_Canonical(A=A5, B=A6, C=A3, D=A4) |
| 86 | + filter12 = ss_to_mtkmodel(; A, B, C, D, guesses) |
52 | 87 | filter2 = SimpleGain(K=1) # Bypass second notch filter if A3=A4=0 |
53 | 88 | filter3 = LeadLag(K=1, T1=T1, T2=T2, guess=1) |
54 | 89 | filter4 = LeadLag(K=1, T1=T3, T2=T4, guess=1) |
55 | 90 | diff = Derivative(K=T5, T=T6) |
56 | 91 | end |
57 | 92 | @variables begin |
58 | 93 | clamped_sig(t), [description="Clamped stabilizer signal"] |
59 | | - active(t), [description="Stabilizer active flag"] |
60 | | - vct(t), [description="Voltage measurement for stabilizer input"] |
| 94 | + vct(t), [description="Voltage measurement (disables stabilizer when outsise [Vcl,Vcu])"] |
61 | 95 | end |
62 | 96 | @equations begin |
63 | | - filter1.in ~ INPUT_in.u |
64 | | - filter2.in ~ filter1.out |
65 | | - filter3.in ~ filter2.out |
| 97 | + filter12.in ~ INPUT_in.u |
| 98 | + filter3.in ~ filter12.out |
66 | 99 | filter4.in ~ filter3.out |
67 | 100 | diff.in ~ filter4.out * Ks |
68 | 101 | clamped_sig ~ clamp(diff.out, Lsmin, Lsmax) |
| 102 | + |
| 103 | + # output limiting is not implemented currently and should be done |
| 104 | + # via callback since its actually discontinuous (i.e. disables the output |
| 105 | + # once the operational reagion is left) |
69 | 106 | vct ~ V_CT_in.u |
70 | | - VOTHSG_out.u ~ clamped_sig*0 |
71 | | - # VOTHSG_out.u ~ clamped_sig |
72 | | - # active ~ ifelse(Vcl < vct, 1, 0)*ifelse(vct < Vcu, 1, 0) |
73 | | - # VOTHSG_out.u ~ clamped_sig * active |
| 107 | + VOTHSG_out.u ~ clamped_sig * _IEEEST_active |
74 | 108 | end |
75 | 109 | end |
76 | 110 |
|
| 111 | +ModelingToolkit.@component ss_to_mtkmodel(; A, B, C, D, kwargs...) = ss_to_mtkmodel(A, B, C, D; kwargs...) |
| 112 | +function ss_to_mtkmodel(A, B, C, D; name=nothing, guesses=zeros(size(A,1))) |
| 113 | + t = ModelingToolkit.t_nounits |
| 114 | + Dt = ModelingToolkit.D_nounits |
77 | 115 |
|
78 | | -function RationalTransferFunction(num::AbstractVector, den::AbstractVector; name=nothing) |
79 | | - m = length(num)-1 |
80 | | - n = length(den)-1 |
81 | | - if m > n |
82 | | - error("Numerator degree > denominator degree not allowed (non-proper).") |
83 | | - end |
84 | | - |
85 | | - # Feedthrough and remainder |
86 | | - r, D = split_proper(num, den) |
87 | | - if length(r) < n |
88 | | - r = vcat(zeros(n - length(r)), r) |
89 | | - end |
90 | | - C = reverse(r)' |
91 | | - |
92 | | - # Leading denominator coefficient |
93 | | - leading = den[1] |
94 | | - @assert !iszero(leading) |
95 | | - |
96 | | - # Companion canonical form |
97 | | - A = convert(Matrix{Any}, zeros(n,n)) |
98 | | - if n > 1 |
99 | | - A[1:end-1, 2:end] .= LinearAlgebra.I(n-1) |
100 | | - end |
101 | | - # scale last row by 1/leading |
102 | | - A[end, :] .= -reverse(den[2:end]) / leading |
103 | | - B = convert(Matrix{Any}, zeros(n,1)) |
104 | | - B[end] = 1 / leading |
| 116 | + n = size(A, 1) |
| 117 | + @assert size(D) == (1, 1) "Only SISO systems supported" |
105 | 118 |
|
106 | 119 | # Symbolic system |
107 | 120 | @variables in(t) out(t) |
108 | | - _xs_names = [ Symbol("x", NetworkDynamics.subscript(i)) for i in 1:n ] |
109 | | - x = map(_xs_names) do nm |
110 | | - Symbolics.variable(nm; T=Symbolics.FnType)(t) |
| 121 | + _xs_names = [ Symbol("x", NetworkDynamics.subscript(i)) for i in 1:n] |
| 122 | + # dont use Symbolics.variables as it does not create all necessary metadata? |
| 123 | + # also needs to set guess in @variables not with MTK.setguess |
| 124 | + x = map(zip(_xs_names, guesses)) do (_name, _guess) |
| 125 | + only(@variables $(_name)(t) [guess=_guess]) |
111 | 126 | end |
| 127 | + |
112 | 128 | ∂x = Dt.(x) |
113 | 129 | eqs = vcat( |
114 | 130 | ∂x .~ A*x .+ B*[in], |
115 | 131 | [out] .~ (length(C)>0 ? C*x : 0) .+ D*[in] |
116 | 132 | ) |
117 | 133 | eqs = Symbolics.simplify.(eqs) |
118 | | - allp = reduce(∪, Symbolics.get_variables.(vcat(num, den))) |
| 134 | + allp = mapreduce(Symbolics.get_variables, ∪, Iterators.flatten((A,B,C,D))) |
119 | 135 |
|
120 | 136 | return System(eqs, t, vcat(x, [in, out]), allp; name=name) |
121 | 137 | end |
122 | 138 |
|
123 | | -# Symbolic-safe split of numerator into feedthrough D and strictly proper remainder r(s) |
124 | | -function split_proper(num::AbstractVector, den::AbstractVector) |
125 | | - n = length(den) - 1 |
126 | | - m = length(num) - 1 |
127 | | - if m < n |
128 | | - # strictly proper: feedthrough D = 0 |
129 | | - D = zero(eltype(num)) |
130 | | - r = num |
131 | | - # pad with zeros on left to match denominator order later |
132 | | - elseif m == n |
133 | | - # proper: feedthrough D = leading coefficient ratio |
134 | | - D = num[1] / den[1] |
135 | | - # remainder r(s) = num(s) - D * den(s) |
136 | | - r_full = [ num[i] - D * den[i] for i in 1:length(num) ] |
137 | | - # drop leading coefficient (coefficient of s^n) |
138 | | - r = r_full[2:end] |
| 139 | +#= |
| 140 | +Taken and adapted from SymbolicControlSystems.jl |
| 141 | +
|
| 142 | +Copyright (c) 2020 Fredrik Bagge Carlson, MIT License |
| 143 | +=# |
| 144 | +function siso_tf_to_ss(num0, den0) |
| 145 | + T = Base.promote_type(eltype(num0), eltype(den0)) |
| 146 | + |
| 147 | + # truncate leading zeros |
| 148 | + num0 = num0[findfirst(!iszero, num0):end] |
| 149 | + den0 = den0[findfirst(!iszero, den0):end] |
| 150 | + |
| 151 | + # check if it is proper |
| 152 | + denorder = length(den0) - 1 |
| 153 | + numorder = length(num0) - 1 |
| 154 | + if numorder > denorder |
| 155 | + error("Numerator degree > denominator degree not allowed (non-proper).") |
| 156 | + end |
| 157 | + |
| 158 | + |
| 159 | + # Normalize the numerator and denominator to allow realization of transfer functions |
| 160 | + # that are proper, but not strictly proper |
| 161 | + num = num0 ./ den0[1] |
| 162 | + den = den0 ./ den0[1] |
| 163 | + |
| 164 | + N = length(den) - 1 # The order of the rational function f |
| 165 | + |
| 166 | + # Get numerator coefficient of the same order as the denominator |
| 167 | + bN = length(num) == N+1 ? num[1] : zero(eltype(num)) |
| 168 | + |
| 169 | + @views if N == 0 #|| num == zero(Polynomial{T}) |
| 170 | + A = zeros(T, 0, 0) |
| 171 | + B = zeros(T, 0, 1) |
| 172 | + C = zeros(T, 1, 0) |
139 | 173 | else |
140 | | - error("Numerator degree > denominator degree not allowed") |
| 174 | + A = LinearAlgebra.diagm(1 => ones(T, N-1)) |
| 175 | + A[end, :] .= .-reverse(den)[1:end-1] |
| 176 | + |
| 177 | + B = zeros(T, N, 1) |
| 178 | + B[end] = one(T) |
| 179 | + |
| 180 | + C = zeros(T, 1, N) |
| 181 | + C[1:min(N, length(num))] = reverse(num)[1:min(N, length(num))] |
| 182 | + C[:] .-= bN .* reverse(den)[1:end-1] # Can index into polynomials at greater inddices than their length |
141 | 183 | end |
142 | | - return r, D |
| 184 | + D = fill(bN, 1, 1) |
| 185 | + |
| 186 | + return A, B, C, D |
143 | 187 | end |
0 commit comments