Skip to content
Merged
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
90 changes: 55 additions & 35 deletions src/pm_io/psse.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1050,6 +1050,23 @@ function apply_tap_correction!(
return windv_value
end

"""
Return winding `winding`'s `(RMI, RMA)` in the units of its modeled turns ratio, where
`turns_ratio_scale` maps the record's CW-unit WINDV onto that ratio. Phase-shift objectives
(|COD| 3 or 5) bracket an angle in degrees instead, which CW does not touch.
"""
function _tap_ratio_limits(
transformer::Dict{String, Any},
winding::Int,
turns_ratio_scale::Float64,
)
rmi, rma = transformer["RMI$winding"], transformer["RMA$winding"]
if abs(transformer["COD$winding"]) ∈ (3, 5)
return rmi, rma
end
return rmi * turns_ratio_scale, rma * turns_ratio_scale
end

# Two-winding records name their buses "f_bus"/"t_bus", three-winding ones
# "bus_primary"/"bus_secondary"/"bus_tertiary"; both reach the warning below.
function _transformer_bus_label(sub_data::Dict)
Expand Down Expand Up @@ -1340,11 +1357,13 @@ function _psse2pm_transformer!(pm_data::Dict, pti_data::Dict, import_all::Bool,
transformer["CW"],
"primary",
)
sub_data["tap"] = windv1 / pop!(transformer, "WINDV2")
# Maps a winding 1 turns ratio in CW units onto the modeled tap, so the
# RMA1/RMI1 that bracket WINDV1 bracket `tap` in the same units.
tap_scale = 1.0 / pop!(transformer, "WINDV2")
sub_data["shift"] = pop!(transformer, "ANG1")

if transformer["CW"] != 1 # NOT "for off-nominal turns ratio in pu of winding bus base voltage"
sub_data["tap"] *=
tap_scale *=
_get_bus_value(transformer["J"], "base_kv", pm_data) /
_get_bus_value(transformer["I"], "base_kv", pm_data)
if transformer["CW"] == 3 # "for off-nominal turns ratio in pu of nominal winding voltage, NOMV1, NOMV2 and NOMV3."
Expand All @@ -1362,10 +1381,10 @@ function _psse2pm_transformer!(pm_data::Dict, pti_data::Dict, import_all::Bool,
winding2_nominal_voltage = transformer["NOMV2"]
end

sub_data["tap"] *=
winding1_nominal_voltage / winding2_nominal_voltage
tap_scale *= winding1_nominal_voltage / winding2_nominal_voltage
end
end
sub_data["tap"] = windv1 * tap_scale

if import_all
sub_data["cw"] = transformer["CW"]
Expand Down Expand Up @@ -1395,8 +1414,8 @@ function _psse2pm_transformer!(pm_data::Dict, pti_data::Dict, import_all::Bool,
sub_data["index"] = length(pm_data["branch"]) + 1
sub_data["COD1"] = transformer["COD1"]
sub_data["CONT1"] = transformer["CONT1"]
sub_data["RMA1"] = transformer["RMA1"]
sub_data["RMI1"] = transformer["RMI1"]
sub_data["RMI1"], sub_data["RMA1"] =
_tap_ratio_limits(transformer, 1, tap_scale)
sub_data["VMA1"] = transformer["VMA1"]
sub_data["VMI1"] = transformer["VMI1"]
sub_data["NTP1"] = transformer["NTP1"]
Expand Down Expand Up @@ -1805,43 +1824,28 @@ function _psse2pm_transformer!(pm_data::Dict, pti_data::Dict, import_all::Bool,
"tertiary",
)

if transformer["CW"] == 1
sub_data["primary_turns_ratio"] = windv1
sub_data["secondary_turns_ratio"] = windv2
sub_data["tertiary_turns_ratio"] = windv3
elseif transformer["CW"] == 2
sub_data["primary_turns_ratio"] =
windv1 / _get_bus_value(transformer["I"], "base_kv", pm_data)
sub_data["secondary_turns_ratio"] =
windv2 / _get_bus_value(transformer["J"], "base_kv", pm_data)
sub_data["tertiary_turns_ratio"] =
windv3 / _get_bus_value(transformer["K"], "base_kv", pm_data)
else
# Maps each winding's turns ratio in CW units onto its per-unit ratio, so
# that winding's RMA/RMI bracket its turns ratio in the same units.
winding_buses = (transformer["I"], transformer["J"], transformer["K"])
turns_ratio_scales = ntuple(3) do i
transformer["CW"] == 1 && return 1.0
bus_base_kv = _get_bus_value(winding_buses[i], "base_kv", pm_data)
transformer["CW"] == 2 && return 1.0 / bus_base_kv
@assert transformer["CW"] == 3
sub_data["primary_turns_ratio"] =
windv1 * (
sub_data["base_voltage_primary"] /
_get_bus_value(transformer["I"], "base_kv", pm_data)
)
sub_data["secondary_turns_ratio"] =
windv2 * (
sub_data["base_voltage_secondary"] /
_get_bus_value(transformer["J"], "base_kv", pm_data)
)
sub_data["tertiary_turns_ratio"] =
windv3 * (
sub_data["base_voltage_tertiary"] /
_get_bus_value(transformer["K"], "base_kv", pm_data)
)
return sub_data["base_voltage_$(WINDING_NAMES[i])"] / bus_base_kv
end
for (i, windv) in enumerate((windv1, windv2, windv3))
sub_data["$(WINDING_NAMES[i])_turns_ratio"] =
windv * turns_ratio_scales[i]
end
sub_data["circuit"] = strip(transformer["CKT"])
sub_data["COD1"] = transformer["COD1"]
sub_data["COD2"] = transformer["COD2"]
sub_data["COD3"] = transformer["COD3"]
for i in 1:3
sub_data["CONT$i"] = transformer["CONT$i"]
sub_data["RMA$i"] = transformer["RMA$i"]
sub_data["RMI$i"] = transformer["RMI$i"]
sub_data["RMI$i"], sub_data["RMA$i"] =
_tap_ratio_limits(transformer, i, turns_ratio_scales[i])
sub_data["VMA$i"] = transformer["VMA$i"]
sub_data["VMI$i"] = transformer["VMI$i"]
sub_data["NTP$i"] = transformer["NTP$i"]
Expand Down Expand Up @@ -2628,7 +2632,20 @@ function _psse2pm_substation_data!(pm_data::Dict, pti_data::Dict, import_all::Bo
return
end

# A substation declaring no nodes attaches to no bus, so it has nothing to contribute to
# the bus-branch model; the record stays in `pti_data`.
nodeless_count = 0
for substation in pti_data["SUBSTATION DATA"]
if isempty(substation["NODES"])
nodeless_count += 1
n_devices = length(substation["SWITCHING DEVICES"])
n_terminals = length(substation["TERMINALS"])
if n_devices + n_terminals > 0
@warn "Substation $(substation["IS"]) declares no nodes but has $n_devices switching device(s) and $n_terminals terminal(s); dropping it."
end
continue
end

sub_data = Dict{String, Any}()
sub_data["number"] = pop!(substation, "IS")
sub_data["name"] = pop!(substation, "NAME")
Expand Down Expand Up @@ -2674,6 +2691,9 @@ function _psse2pm_substation_data!(pm_data::Dict, pti_data::Dict, import_all::Bo
push!(pm_data["substation"], sub_data)
end

if nodeless_count > 0
@info "Dropped $nodeless_count PSS(R)E substation(s) that declare no nodes and so attach to no bus."
end
return
end

Expand Down
2 changes: 1 addition & 1 deletion test/fixtures/synthetic_v35_transformer_discriminators.raw
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Two 2W transformers with device base (SBASE1-2) != system base (100 MVA): T1 CZ=
0 / END OF SYSTEM SWITCHING DEVICE DATA, BEGIN TRANSFORMER DATA
201, 202, 0, '1 ', 2, 2, 1, 0.001, 0.002, 2, 'XFMR_T1 ', 1, 1, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, ' '
0.01, 0.05, 50.0
105.0, 0.0, 0.0, 40.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 0, 0, 1.1, 0.9, 1.1, 0.9, 33, 0, 0.0, 0.0, 0.0
105.0, 0.0, 0.0, 40.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 0, 0, 110.0, 90.0, 1.1, 0.9, 33, 0, 0.0, 0.0, 0.0
21.0, 0.0
203, 204, 0, '1 ', 3, 3, 2, 80000.0, 0.005, 2, 'XFMR_T2 ', 1, 1, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, ' '
800.0, 0.05, 80.0
Expand Down
9 changes: 9 additions & 0 deletions test/test_openapi_transformer_discriminators.jl
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ _transformer_discriminator_pm_data() =
# exercised via T2 below, whose ratio does NOT cancel.
@test d["tap"] ≈ 1.0

# RMA1/RMI1 = 110/90 kV bracket WINDV1 and take the same CW=2 conversion as the tap:
# (110/21) * (20/100) and (90/21) * (20/100).
@test d["RMA1"] ≈ 110.0 / 105.0
@test d["RMI1"] ≈ 90.0 / 105.0

# rate_a: RATE11 = 40.0 MVA raw, divided by sys_mbase (PowerModels' generic branch
# per-unit correction, applied uniformly to every "branch" entry) = 0.4 system-pu.
@test d["rate_a"] ≈ 40.0 / sys_mbase
Expand Down Expand Up @@ -80,6 +85,10 @@ _transformer_discriminator_pm_data() =
# 0.4*100=40.0 (wrong) vs 0.4*50=20.0 (right).
@test PFP.get_value(circuit, :rating) ≈ d["rate_a"] * d["base_power"]
@test PFP.get_value(circuit, :rating) ≈ 20.0
@test _matches_nt(
PFP.get_value(circuit, :control_limits),
(min = d["RMI1"], max = d["RMA1"]),
)
end

@testset "T2 (CZ=3, CW=3, CM=2, SBASE1-2=80 != sys_mbase=100): hand-derived br_r/br_x/g_fr/b_fr/tap/rating" begin
Expand Down
71 changes: 71 additions & 0 deletions test/test_parse_psse.jl
Original file line number Diff line number Diff line change
Expand Up @@ -346,3 +346,74 @@ end
@test transformer_3w["g"] == 0.0
@test transformer_3w["b"] == 0.0
end

@testset "PSSE CW=2 tap-ratio limits are converted with the tap" begin
# Under CW=2 WINDV and the RMA/RMI that bracket it are winding voltages in kV. Both must
# reach the pm dict in the tap's own units, so a tap inside its band stays inside it.
raw = read_fixture(FOURTEEN_BUS_FIXTURE)

# "TRAFO 2W 1" runs 230 kV bus 106 to 138 kV bus 105 and regulates voltage (COD1=1).
# WINDV1 = 232.875 kV sits on step 18 of the 33-step 207..253 kV ladder, and
# WINDV2 = 141.45 kV is 1.025 pu, so the tap and its band all divide by 1.025.
# "TRAFO 2W 3" is a CW=2 phase shifter (COD1=3), whose limits are degrees.
# "TRAFO 3W 1" winds 230/230/500 kV and exercises one objective per winding.
cw2 = replace(
raw,
" 106, 105, 0,'1 ',1,1,1, 0.00000E+0, 0.00000E+0,2,'TRAFO 2W 1 ',1, 1,1.0000, 0,1.0000, 0,1.0000, 0,1.0000,' '\n" *
" 0.00000E+0, 1.00000E-4, 100.00\n" *
"1.00000, 0.000, 0.000, 0.00, 0.00, 0.00, 0, 0, 1.10000, 0.90000, 1.10000, 0.90000, 33, 7, 0.00000, 0.00000, 0.000\n" *
"1.00000, 0.000\n" =>
" 106, 105, 0,'1 ',2,1,1, 0.00000E+0, 0.00000E+0,2,'TRAFO 2W 1 ',1, 1,1.0000, 0,1.0000, 0,1.0000, 0,1.0000,' '\n" *
" 0.00000E+0, 1.00000E-4, 100.00\n" *
"232.875, 0.000, 0.000, 0.00, 0.00, 0.00, 1, 105, 253.000, 207.000, 1.10000, 0.90000, 33, 7, 0.00000, 0.00000, 0.000\n" *
"141.450, 0.000\n",
" 109, 104, 0,'1 ',1,1,1, 0.00000E+0, 0.00000E+0,2,'TRAFO 2W 3 ',1, 1,1.0000, 0,1.0000, 0,1.0000, 0,1.0000,' '\n" *
" 0.00000E+0, 1.00000E-4, 100.00\n" *
"1.00000, 0.000, 0.000, 0.00, 0.00, 0.00, 0, 0, 1.10000, 0.90000, 1.10000, 0.90000, 33, 4, 0.00000, 0.00000, 0.000\n" *
"1.00000, 0.000\n" =>
" 109, 104, 0,'1 ',2,1,1, 0.00000E+0, 0.00000E+0,2,'TRAFO 2W 3 ',1, 1,1.0000, 0,1.0000, 0,1.0000, 0,1.0000,' '\n" *
" 0.00000E+0, 1.00000E-4, 100.00\n" *
"500.000, 0.000, 0.000, 0.00, 0.00, 0.00, 3, 0, 30.0000, -30.000, 1.10000, 0.90000, 33, 4, 0.00000, 0.00000, 0.000\n" *
"138.000, 0.000\n",
" 113, 110, 114,'1 ',1,1,1, 0.00000E+0, 0.00000E+0,2,'TRAFO 3W 1 ',1, 1,1.0000, 0,1.0000, 0,1.0000, 0,1.0000,' '\n" *
" 0.00000E+0, 2.00000E-4, 100.00, 0.00000E+0, 2.00000E-4, 100.00, 0.00000E+0, 2.00000E-4, 100.00,0.99967, -3.0658\n" *
"1.00000, 0.000, 0.000, 0.00, 0.00, 0.00, 0, 0, 1.10000, 0.90000, 1.10000, 0.90000, 33, 9, 0.00000, 0.00000, 0.000\n" *
"1.00000, 0.000, 0.000, 0.00, 0.00, 0.00, 0, 0, 1.10000, 0.90000, 1.10000, 0.90000, 33, 8, 0.00000, 0.00000, 0.000\n" *
"1.00000, 0.000, 0.000, 0.00, 0.00, 0.00, 0, 0, 1.10000, 0.90000, 1.10000, 0.90000, 33, 9, 0.00000, 0.00000, 0.000\n" =>
" 113, 110, 114,'1 ',2,1,1, 0.00000E+0, 0.00000E+0,2,'TRAFO 3W 1 ',1, 1,1.0000, 0,1.0000, 0,1.0000, 0,1.0000,' '\n" *
" 0.00000E+0, 2.00000E-4, 100.00, 0.00000E+0, 2.00000E-4, 100.00, 0.00000E+0, 2.00000E-4, 100.00,0.99967, -3.0658\n" *
"232.875, 0.000, 0.000, 0.00, 0.00, 0.00, 1, 113, 253.000, 207.000, 1.10000, 0.90000, 33, 9, 0.00000, 0.00000, 0.000\n" *
"230.000, 0.000, 0.000, 0.00, 0.00, 0.00, 0, 0, 253.000, 207.000, 1.10000, 0.90000, 33, 8, 0.00000, 0.00000, 0.000\n" *
"500.000, 0.000, 0.000, 0.00, 0.00, 0.00, 3, 0, 30.0000, -30.000, 1.10000, 0.90000, 33, 9, 0.00000, 0.00000, 0.000\n",
)
@test cw2 != raw
pm_data = parse_file(IOBuffer(cw2); filetype = "raw")
by_name(section, name) = only(
v for v in values(pm_data[section]) if
get(get(v, "ext", Dict()), "psse_name", "") == name
)

regulating = by_name("branch", "TRAFO 2W 1 ")
@test regulating["tap"] ≈ 1.0125 / 1.025
@test regulating["RMI1"] ≈ 0.9 / 1.025
@test regulating["RMA1"] ≈ 1.1 / 1.025
@test regulating["RMI1"] < regulating["tap"] < regulating["RMA1"]
# ext keeps the record's own values.
@test regulating["ext"]["RMA1"] == 253.0

phase_shifter = by_name("branch", "TRAFO 2W 3 ")
@test phase_shifter["tap"] ≈ 1.0
@test phase_shifter["RMI1"] == -30.0
@test phase_shifter["RMA1"] == 30.0

transformer_3w = by_name("3w_transformer", "TRAFO 3W 1 ")
@test transformer_3w["primary_turns_ratio"] ≈ 1.0125
@test transformer_3w["RMI1"] ≈ 0.9
@test transformer_3w["RMA1"] ≈ 1.1
@test transformer_3w["secondary_turns_ratio"] ≈ 1.0
@test transformer_3w["RMI2"] ≈ 0.9
@test transformer_3w["RMA2"] ≈ 1.1
@test transformer_3w["tertiary_turns_ratio"] ≈ 1.0
@test transformer_3w["RMI3"] == -30.0
@test transformer_3w["RMA3"] == 30.0
end
60 changes: 60 additions & 0 deletions test/test_parse_substation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1280,3 +1280,63 @@ end
PowerModelsData(file)
)
end

@testset "a substation declaring no nodes has no PowerModels representation" begin
# A SUBSTATION record whose node block is empty attaches to no bus. pti.jl keeps it,
# but it has nothing to contribute to the pm dict, so the conversion drops it.
raw = read_fixture(V35_SUBSTATION_FIXTURE)
nodeless =
"@! BEGIN SUBSTATION DATA BLOCK\n" *
" 3,'CHARLIE ', 0.0000000, 0.0000000, 0.0000\n" *
" 0 / END OF SUBSTATION NODE DATA, BEGIN SUBSTATION SWITCHING DEVICE DATA\n" *
" 0 / END OF SUBSTATION SWITCHING DEVICE DATA, BEGIN SUBSTATION TERMINAL DATA\n" *
" 0 / END OF SUBSTATION TERMINAL DATA\n"
patched = replace(
raw,
"0 / END OF SUBSTATION DATA\n" => nodeless * "0 / END OF SUBSTATION DATA\n",
)
@test patched != raw
path = joinpath(mktempdir(), "nodeless_substation.raw")
write(path, patched)

@test length(PFP.parse_pti(path)["SUBSTATION DATA"]) == 3

pm = @test_logs(
(:info, r"Dropped 1 PSS\(R\)E substation\(s\) that declare no nodes"),
match_mode = :any,
PowerModelsData(path)
)
substations = pm.data["substation"]
@test sort([s["number"] for s in values(substations)]) == [1, 2]
@test sort([s["index"] for s in values(substations)]) == [1, 2]

sys = PFP.build_openapi_system(pm)
@test sort([
PFP.get_value(s, :number) for
s in PFP.get_supplemental_attributes(sys, "Substation")
]) == [1, 2]

# Switching devices between nodes that do not exist are malformed rather than a
# placeholder, so dropping that record is announced on its own.
with_devices =
"@! BEGIN SUBSTATION DATA BLOCK\n" *
" 4,'DELTA ', 0.0000000, 0.0000000, 0.0000\n" *
" 0 / END OF SUBSTATION NODE DATA, BEGIN SUBSTATION SWITCHING DEVICE DATA\n" *
" 1, 2, '1 ','DELTA\$138\$CB\$0001 ', 2, 1, 1, 0.00010, 0.00, 0.00, 0.00\n" *
" 0 / END OF SUBSTATION SWITCHING DEVICE DATA, BEGIN SUBSTATION TERMINAL DATA\n" *
" 0 / END OF SUBSTATION TERMINAL DATA\n"
write(
path,
replace(
patched,
"0 / END OF SUBSTATION DATA\n" => with_devices * "0 / END OF SUBSTATION DATA\n",
),
)
pm = @test_logs(
(:warn, r"Substation 4 declares no nodes but has 1 switching device\(s\)"),
(:info, r"Dropped 2 PSS\(R\)E substation\(s\) that declare no nodes"),
match_mode = :any,
PowerModelsData(path)
)
@test sort([s["number"] for s in values(pm.data["substation"])]) == [1, 2]
end
Loading