Skip to content

Commit 4e0f43f

Browse files
committed
add and test SCRX model
1 parent 5dbb3e2 commit 4e0f43f

File tree

8 files changed

+327
-2
lines changed

8 files changed

+327
-2
lines changed

src/Library/Library.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,9 @@ include("OpenIPSL/Loads/PSSE_Load.jl")
174174
export PSSE_IEEET1, PSSE_ESST4B, PSSE_EXST1, PSSE_ESST1A
175175
include("OpenIPSL/Controls/PSSE_Excitation.jl")
176176

177+
export PSSE_SCRX
178+
include("OpenIPSL/Controls/PSSE_SCRX.jl")
179+
177180
export PSSE_IEEEG1
178181
include("OpenIPSL/Controls/PSSE_TurbineGovernors.jl")
179182

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# PSSE Static Excitation System Components
2+
#
3+
# Original work Copyright (c) 2016-2022 Luigi Vanfretti, ALSETLab, and contributors
4+
# Original work licensed under BSD 3-Clause License
5+
# Original source: https://github.com/OpenIPSL/OpenIPSL
6+
#
7+
# This Julia/PowerDynamics port maintains the same mathematical formulation
8+
# while adapting to PowerDynamics/ModelingToolkit framework conventions.
9+
10+
@mtkmodel PSSE_SCRX begin
11+
@structural_parameters begin
12+
# Optional input enabling flags (following naming convention from other exciter models)
13+
vothsg_input=false # Other signal input (typically zero if disabled)
14+
vuel_input=false # Under-excitation limiter input (typically zero if disabled)
15+
voel_input=false # Over-excitation limiter input (typically zero if disabled)
16+
C_SWITCH=false # Feeding selection: false for bus fed, true for solid fed
17+
end
18+
19+
@parameters begin
20+
# Excitation system parameters with OpenIPSL defaults
21+
T_AT_B=0.1, [description="Ratio between regulator numerator (lead) and denominator (lag) time constants [pu]"]
22+
T_B=1, [description="Regulator denominator (lag) time constant [s]"]
23+
K=100, [description="Excitation power source output gain [pu]"]
24+
T_E=0.005, [description="Excitation power source output time constant [s]"]
25+
E_MIN=-10, [description="Minimum exciter output [pu]"]
26+
E_MAX=10, [description="Maximum exciter output [pu]"]
27+
r_cr_fd=10, [description="Ratio between crowbar circuit resistance and field circuit resistance [pu]"]
28+
29+
# Initialization parameters (determined during initialization)
30+
V_REF, [guess=1, description="Voltage reference setpoint [pu]"]
31+
end
32+
33+
@components begin
34+
# Required inputs/outputs
35+
ECOMP_in = RealInput() # Terminal voltage measurement input
36+
XADIFD_in = RealInput() # Machine field current input
37+
EFD_out = RealOutput() # Field voltage output to generator
38+
39+
# Optional inputs (created conditionally based on structural parameters)
40+
if vothsg_input
41+
VOTHSG_in = RealInput() # Other signal input
42+
end
43+
if vuel_input
44+
VUEL_in = RealInput() # Under-excitation limiter input
45+
end
46+
if voel_input
47+
VOEL_in = RealInput() # Over-excitation limiter input
48+
end
49+
50+
# Building block components
51+
leadlag = LeadLag(K=1, T1=T_AT_B*T_B, T2=T_B, guess=1)
52+
amplifier = SimpleLagLim(K=K, T=T_E, outMin=E_MIN, outMax=E_MAX, guess=1)
53+
end
54+
55+
@variables begin
56+
# Signal processing variables
57+
voltage_error(t), [description="Voltage error signal [pu]"]
58+
sum_signal(t), [description="Combined signal before leadlag [pu]"]
59+
60+
# Amplifier and switching variables
61+
amplifier_output(t), [description="Amplifier output before switching [pu]"]
62+
switch_output(t), [description="Switched output (bus fed or solid fed) [pu]"]
63+
64+
# NegCurLogic variables
65+
crowbar_voltage(t), [description="Crowbar circuit voltage [pu]"]
66+
field_current(t), [description="Field current input [pu]"]
67+
EFD(t), [description="Final exciter output [pu]"]
68+
end
69+
70+
@equations begin
71+
# Signal combination following OpenIPSL Add3 and DiffV1 logic
72+
voltage_error ~ V_REF - ECOMP_in.u
73+
sum_signal ~ voltage_error + (vothsg_input ? VOTHSG_IN.u : 0) + (vuel_input ? VUEL_IN.u : 0) - (voel_input ? VOEL_IN.u : 0)
74+
75+
# LeadLag compensator
76+
leadlag.in ~ sum_signal
77+
78+
# Amplifier with limits
79+
amplifier.in ~ leadlag.out
80+
amplifier_output ~ amplifier.out
81+
82+
if C_SWITCH
83+
# Solid fed: use amplifier output directly
84+
switch_output ~ amplifier_output
85+
else
86+
# Bus fed: multiply by terminal voltage
87+
switch_output ~ amplifier_output * ECOMP_in.u
88+
end
89+
90+
# Field current input
91+
field_current ~ XADIFD_in.u
92+
93+
# Integrated NegCurLogic functionality
94+
crowbar_voltage ~ ifelse(abs(r_cr_fd) < eps(), 0.0, -r_cr_fd * field_current)
95+
EFD ~ ifelse(field_current < 0, crowbar_voltage, switch_output)
96+
97+
# Output connection
98+
EFD_out.u ~ EFD
99+
end
100+
end

src/Library/building_blocks.jl

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ function _generate_limint_callbacks(cf::NetworkDynamics.ComponentModel, namespac
170170
end
171171
end
172172

173+
# TODO: merge both discrete conditions and move condition below function for performance
173174
discrete_condition = ComponentCondition([out, min, max], []) do u, p, t
174175
# account for nummerical innaccuracies at the boudaries
175176
u[out] < u[min] - 1e-10 || u[out] > u[max] + 1e-10
@@ -199,10 +200,29 @@ function _generate_limint_callbacks(cf::NetworkDynamics.ComponentModel, namespac
199200
error("Sanity check was wrongfully triggered!")
200201
end
201202
end
203+
discrete_unsat_condition = ComponentCondition([forcing],[satmin, satmax]) do u, p, t
204+
insatmin = !iszero(p[satmin])
205+
insatmax = !iszero(p[satmax])
206+
insatmin && u[forcing] > 0 || insatmax && u[forcing] < 0
207+
end
208+
discrete_unsat_affect = ComponentAffect([],[satmin, satmax]) do u, p, ctx
209+
insatmin = !iszero(p[satmin])
210+
insatmax = !iszero(p[satmax])
211+
if insatmin
212+
println("$namespace: _/ left lower saturation at $(round(ctx.t, digits=4))s (triggered by discrete cb)")
213+
p[satmin] = 0.0
214+
elseif insatmax
215+
println("$namespace: ⎺\\ left upper saturation at $(round(ctx.t, digits=4))s (triggered by discrete cb)")
216+
p[satmax] = 0.0
217+
else
218+
error("Sanity check was wrongfully triggered!")
219+
end
220+
end
202221

203222
(
204223
VectorContinuousComponentCallback(condition, upcrossing_affect, 3; affect_neg! = downcrossing_affect),
205-
DiscreteComponentCallback(discrete_condition, discrete_affect)
224+
DiscreteComponentCallback(discrete_condition, discrete_affect),
225+
DiscreteComponentCallback(discrete_unsat_condition, discrete_unsat_affect)
206226
)
207227
end
208228
function _SatLim_condition(_out, u, p, _)

test/OpenIPSL_test/ModelTransferRules.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ The following OpenIPSL models use the SMIB test harness and are candidates for P
212212
- *Dependencies: Requires GENROE machine model*
213213
- [ ] **SEXS** - Simplified excitation system
214214
- *Dependencies: Requires GENROU machine model*
215-
- [ ] **SCRX** - Static exciter
215+
- [X] **SCRX** - Static exciter
216216
- *Dependencies: Requires GENROU machine model*
217217
- [X] **EXST1** - Static excitation system
218218
- *Dependencies: Requires GENROE machine model*
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
using PowerDynamics
2+
PowerDynamics.load_pdtesting()
3+
using Main.PowerDynamicsTesting
4+
5+
using PowerDynamics.Library
6+
using ModelingToolkit
7+
using OrdinaryDiffEqRosenbrock
8+
using OrdinaryDiffEqNonlinearSolve
9+
10+
using CSV
11+
using DataFrames
12+
using CairoMakie
13+
14+
# Load reference data from SCRX simulation
15+
ref = CSV.read(
16+
joinpath(pkgdir(PowerDynamics),"test","OpenIPSL_test","SCRX","modelica_results.csv.gz"),
17+
DataFrame;
18+
drop=(i,name) -> contains(string(name), "nrows="),
19+
silencewarnings=true
20+
)
21+
22+
# SCRX test system parameters from OpenIPSL SCRX test case
23+
BUS = let
24+
# Power flow from OpenIPSL SCRX test
25+
v_0 = 1.0
26+
angle_0 = 0.070492225331847
27+
28+
# Create GENROU machine with EFD input enabled for exciter control
29+
@named genrou = PSSE_GENROU(;
30+
# System parameters
31+
S_b = 100e6,
32+
M_b = 100e6,
33+
# GENROU machine parameters from OpenIPSL SCRX.mo (lines 6-28)
34+
H = 4.28,
35+
D = 0,
36+
Xd = 1.84,
37+
Xq = 1.75,
38+
Xpd = 0.41,
39+
Xpq = 0.6,
40+
Xppd = 0.2,
41+
Xppq = 0.2,
42+
Xl = 0.12,
43+
Tpd0 = 5,
44+
Tppd0 = 0.07,
45+
Tpq0 = 0.9,
46+
Tppq0 = 0.09,
47+
S10 = 0.11,
48+
S12 = 0.39,
49+
R_a = 0,
50+
pmech_input = false,
51+
efd_input = true # EFD controlled by SCRX
52+
)
53+
54+
# Create SCRX exciter with parameters matching OpenIPSL test (lines 30-38)
55+
@named scrx = PSSE_SCRX(;
56+
T_AT_B = 0.1,
57+
T_B = 1,
58+
K = 100,
59+
T_E = 0.005,
60+
E_MIN = -10,
61+
E_MAX = 10,
62+
r_cr_fd = 0,
63+
C_SWITCH = false, # Bus fed mode
64+
vothsg_input = false, # No other signal input
65+
vuel_input = false, # No under-excitation limiter
66+
voel_input = false # No over-excitation limiter
67+
)
68+
69+
con = [
70+
# Connect exciter output to machine field voltage
71+
connect(scrx.EFD_out, genrou.EFD_in)
72+
73+
# Connect machine field current to exciter
74+
connect(genrou.XADIFD_out, scrx.XADIFD_in)
75+
76+
# Connect terminal voltage measurement
77+
connect(genrou.ETERM_out, scrx.ECOMP_in)
78+
]
79+
80+
# Create bus model with proper connections
81+
busmodel = MTKBus([genrou, scrx], con; name=:GEN1)
82+
bm = compile_bus(busmodel, pf=pfSlack(V=v_0, δ=angle_0))
83+
end
84+
85+
sol = OpenIPSL_SMIB(BUS);
86+
87+
## Validation tests for GENROU machine variables (core 3 variables)
88+
@test ref_rms_error(sol, ref, VIndex(:GEN1, :genrou₊w), "gENROU.w") < 1e-5
89+
@test ref_rms_error(sol, ref, VIndex(:GEN1, :genrou₊delta), "gENROU.delta") < 1e-3
90+
@test ref_rms_error(sol, ref, VIndex(:GEN1, :genrou₊Vt), "gENROU.Vt") < 1e-4
91+
92+
## Validation tests for SCRX exciter variables (4 control path variables)
93+
@test ref_rms_error(sol, ref, VIndex(:GEN1, :scrx₊leadlag₊out), "sCRX.imLeadLag.y") < 5e-5
94+
@test ref_rms_error(sol, ref, VIndex(:GEN1, :scrx₊amplifier₊out), "sCRX.simpleLagLim.y") < 5e-3
95+
@test ref_rms_error(sol, ref, VIndex(:GEN1, :genrou₊XADIFD_out₊u), "gENROU.XADIFD") < 1e-3
96+
@test ref_rms_error(sol, ref, VIndex(:GEN1, :scrx₊EFD_out₊u), "sCRX.EFD") < 2e-3
97+
98+
#=
99+
fig_scrx = let
100+
fig = Figure(size=(1400, 1200))
101+
ts = refine_timeseries(sol.t)
102+
103+
# Plot 1: Generator Rotor Angle
104+
ax1 = Axis(fig[1,1]; xlabel="Time [s]", ylabel="δ [rad]", title="Generator: Rotor Angle")
105+
lines!(ax1, ref.time, ref[!, Symbol("gENROU.delta")]; label="OpenIPSL", color=Cycled(1), linewidth=2, alpha=0.7)
106+
lines!(ax1, ts, sol(ts, idxs=VIndex(:GEN1, :genrou₊delta)).u; label="PowerDynamics", color=Cycled(1), linewidth=2, linestyle=:dash)
107+
axislegend(ax1)
108+
109+
# Plot 2: Generator Speed Deviation
110+
ax2 = Axis(fig[1,2]; xlabel="Time [s]", ylabel="ω [pu]", title="Generator: Speed Deviation")
111+
lines!(ax2, ref.time, ref[!, Symbol("gENROU.w")]; label="OpenIPSL", color=Cycled(2), linewidth=2, alpha=0.7)
112+
lines!(ax2, ts, sol(ts, idxs=VIndex(:GEN1, :genrou₊w)).u; label="PowerDynamics", color=Cycled(2), linewidth=2, linestyle=:dash)
113+
axislegend(ax2)
114+
115+
# Plot 3: Generator Terminal Voltage
116+
ax3 = Axis(fig[2,1]; xlabel="Time [s]", ylabel="Vt [pu]", title="Generator: Terminal Voltage")
117+
lines!(ax3, ref.time, ref[!, Symbol("gENROU.Vt")]; label="OpenIPSL", color=Cycled(3), linewidth=2, alpha=0.7)
118+
lines!(ax3, ts, sol(ts, idxs=VIndex(:GEN1, :genrou₊Vt)).u; label="PowerDynamics", color=Cycled(3), linewidth=2, linestyle=:dash)
119+
axislegend(ax3)
120+
121+
# Plot 4: SCRX LeadLag Output
122+
ax4 = Axis(fig[2,2]; xlabel="Time [s]", ylabel="[pu]", title="SCRX: LeadLag Output")
123+
lines!(ax4, ref.time, ref[!, Symbol("sCRX.imLeadLag.y")]; label="OpenIPSL", color=Cycled(4), linewidth=2, alpha=0.7)
124+
lines!(ax4, ts, sol(ts, idxs=VIndex(:GEN1, :scrx₊leadlag₊out)).u; label="PowerDynamics", color=Cycled(4), linewidth=2, linestyle=:dash)
125+
axislegend(ax4)
126+
127+
# Plot 5: SCRX Amplifier Output
128+
ax5 = Axis(fig[3,1]; xlabel="Time [s]", ylabel="[pu]", title="SCRX: Amplifier Output")
129+
lines!(ax5, ref.time, ref[!, Symbol("sCRX.simpleLagLim.y")]; label="OpenIPSL", color=Cycled(5), linewidth=2, alpha=0.7)
130+
lines!(ax5, ts, sol(ts, idxs=VIndex(:GEN1, :scrx₊amplifier₊out)).u; label="PowerDynamics", color=Cycled(5), linewidth=2, linestyle=:dash)
131+
axislegend(ax5)
132+
133+
# Plot 6: SCRX Final EFD Output
134+
ax6 = Axis(fig[3,2]; xlabel="Time [s]", ylabel="EFD [pu]", title="SCRX: Final Field Voltage")
135+
lines!(ax6, ref.time, ref[!, Symbol("sCRX.EFD")]; label="OpenIPSL", color=Cycled(6), linewidth=2, alpha=0.7)
136+
lines!(ax6, ts, sol(ts, idxs=VIndex(:GEN1, :scrx₊EFD_out₊u)).u; label="PowerDynamics", color=Cycled(6), linewidth=2, linestyle=:dash)
137+
axislegend(ax6)
138+
139+
fig
140+
end
141+
=#
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Load OpenIPSL library
2+
loadFile(getEnvironmentVar("OPENIPSL_PATH") + "/package.mo");
3+
4+
// Run SCRX simulation with model's native experiment settings
5+
simulate(OpenIPSL.Tests.Controls.PSSE.ES.SCRX);
6+
7+
// Extract minimal machine and exciter data for testing
8+
filterSimulationResults(
9+
"OpenIPSL.Tests.Controls.PSSE.ES.SCRX_res.mat",
10+
"modelica_results.csv",
11+
{
12+
// GENROU machine variables (key dynamics)
13+
"gENROU.delta", // Rotor angle
14+
"gENROU.w", // Speed deviation (omega)
15+
"gENROU.Vt", // Terminal voltage magnitude
16+
17+
// SCRX exciter variables (control path)
18+
"sCRX.imLeadLag.y", // LeadLag compensator output
19+
"sCRX.simpleLagLim.y", // Amplifier output
20+
"gENROU.XADIFD", // Field current (machine side)
21+
"sCRX.EFD" // Final exciter field voltage output
22+
}
23+
);
29.1 KB
Binary file not shown.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#!/bin/bash
2+
3+
# Get script directory
4+
SCRIPTDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5+
6+
# Set temporary directory
7+
TMPDIR="/tmp/scrx_sim"
8+
9+
# Create temporary directory
10+
mkdir -p "$TMPDIR" && cd "$TMPDIR"
11+
12+
# Remove existing OpenIPSL directory if it exists
13+
rm -rf OpenIPSL
14+
15+
# Clone OpenIPSL at specific commit for reproducibility
16+
git clone --depth 1 https://github.com/OpenIPSL/OpenIPSL.git
17+
cd OpenIPSL
18+
git fetch --depth 1 origin fe8aa5c
19+
git checkout fe8aa5c
20+
cd ..
21+
22+
# Set OpenIPSL library path
23+
export OPENIPSL_PATH="$TMPDIR/OpenIPSL/OpenIPSL"
24+
25+
# Run OpenModelica simulation
26+
omc "$SCRIPTDIR/model_simulation.mos"
27+
28+
# Copy results back, compress, and cleanup
29+
# Copy minimal data (for git repo)
30+
cp modelica_results.csv "$SCRIPTDIR/" || { echo "Error: Failed to copy simulation results"; exit 1; }
31+
32+
# Remove existing compressed files if they exist to avoid conflicts
33+
rm -f "$SCRIPTDIR/modelica_results.csv.gz"
34+
35+
# Compress the file
36+
gzip "$SCRIPTDIR/modelica_results.csv"
37+
38+
rm -rf "$TMPDIR"

0 commit comments

Comments
 (0)