Skip to content

Commit 7d4e1e1

Browse files
committed
IEEEST implemented and tested
1 parent 129bb48 commit 7d4e1e1

File tree

4 files changed

+284
-123
lines changed

4 files changed

+284
-123
lines changed
Lines changed: 142 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,143 +1,187 @@
1-
@mtkmodel NotchFilter_Canonical begin
1+
@mtkmodel PSSE_IEEEST begin
22
@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
74
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"]
1324
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
4034
end
4135
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)
4379
end
80+
4481
@components begin
4582
VOTHSG_out = RealOutput()
4683
INPUT_in = RealInput()
4784
V_CT_in = RealInput() #terminal voltage
4885

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)
5287
filter2 = SimpleGain(K=1) # Bypass second notch filter if A3=A4=0
5388
filter3 = LeadLag(K=1, T1=T1, T2=T2, guess=1)
5489
filter4 = LeadLag(K=1, T1=T3, T2=T4, guess=1)
5590
diff = Derivative(K=T5, T=T6)
5691
end
5792
@variables begin
5893
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])"]
6195
end
6296
@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
6699
filter4.in ~ filter3.out
67100
diff.in ~ filter4.out * Ks
68101
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)
69106
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
74108
end
75109
end
76110

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
77115

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"
105118

106119
# Symbolic system
107120
@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])
111126
end
127+
112128
∂x = Dt.(x)
113129
eqs = vcat(
114130
∂x .~ A*x .+ B*[in],
115131
[out] .~ (length(C)>0 ? C*x : 0) .+ D*[in]
116132
)
117133
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)))
119135

120136
return System(eqs, t, vcat(x, [in, out]), allp; name=name)
121137
end
122138

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)
139173
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
141183
end
142-
return r, D
184+
D = fill(bN, 1, 1)
185+
186+
return A, B, C, D
143187
end

0 commit comments

Comments
 (0)