diff --git a/src/openapi/device_base.jl b/src/openapi/device_base.jl index 4df1c96..a89609e 100644 --- a/src/openapi/device_base.jl +++ b/src/openapi/device_base.jl @@ -138,6 +138,10 @@ const _DEVICEBASE_INSTANCE_DISPATCHED = Dict{Tuple{String, Symbol}, Symbol}( ("SwitchedAdmittance", :Y) => :skip, ("SwitchedAdmittance", :Y_increase) => :skip, ("SwitchedAdmittance", :admittance_limits) => :skip, + # BINIT (PowerSystems.jl#1774) is the same COMPONENT_MVAR-on-system-base quantity as `Y` + # and `Y_increase` above -- PSY's own to_openapi scales it by the SYSTEM base in both + # document conventions -- so it takes their classification, not a device-base conversion. + ("SwitchedAdmittance", :solved_admittance) => :skip, # parameter_units/dc_voltage_units/admittance_units always "NATURAL_UNITS" for the # PSS/E-native LCC/VSC fields (dc_branch.jl) -- fixed ohm/kV/S regardless of the # run's power_units, the mirror image of the COMPONENT_BASE cases above. diff --git a/src/openapi/shunt.jl b/src/openapi/shunt.jl index a419b5d..c93a6ab 100644 --- a/src/openapi/shunt.jl +++ b/src/openapi/shunt.jl @@ -122,8 +122,19 @@ function make_switched_admittance!( Int(get(d, "regulated_bus_number", 0)), "1", ) - if haskey(d, "initial_status") - set_value!(component, :initial_status, d["initial_status"]) + if haskey(d, "number_engaged") + set_value!(component, :number_engaged, d["number_engaged"]) + end + # PSS/E BINIT. Always present in a SWITCHED SHUNT record (pti.jl defaults it to 0.0), so + # a parsed switched shunt always carries a solved admittance and downstream reads it + # rather than summing blocks -- which is what PSS/E itself does with the value. + if haskey(d, "solved_admittance") + set_value!( + component, + :solved_admittance, + d["solved_admittance"] * base_power, + "MVAr", + ) end add_component!(sys, component) set_component_ext!(sys, component, get(d, "ext", Dict{String, Any}())) diff --git a/src/pm_io/data.jl b/src/pm_io/data.jl index 17de7ac..0f05c5a 100644 --- a/src/pm_io/data.jl +++ b/src/pm_io/data.jl @@ -340,6 +340,9 @@ function _make_per_unit!(data::Dict{String, <:Any}, mva_base::Real) _apply_func!(sw_shunt, "gs", rescale) _apply_func!(sw_shunt, "bs", rescale) _apply_func!(sw_shunt, "y_increment", rescale) + # BINIT is an admittance like the two above and must be per-unitized with them; + # skipping it here would leave it a factor of baseMVA too large downstream. + _apply_func!(sw_shunt, "solved_admittance", rescale) end end diff --git a/src/pm_io/psse.jl b/src/pm_io/psse.jl index b3b54e7..f264b25 100644 --- a/src/pm_io/psse.jl +++ b/src/pm_io/psse.jl @@ -897,7 +897,14 @@ function _psse2pm_shunt!(pm_data::Dict, pti_data::Dict, import_all::Bool, nb) get(sub_data, "sw_id", "1"), ) sub_data["gs"] = 0.0 - sub_data["bs"] = pop!(switched_shunt, "BINIT") + # A PSS/E switched shunt has no fixed base admittance: the record carries only + # BINIT (the solved/initial total susceptance) and the per-block increments. BINIT + # is therefore NOT a `bs` -- it is the device's solved admittance, and goes to its + # own field so downstream can tell "solved total" from "fixed base + blocks" + # instead of inferring it from a zeroed block-status vector. + # See PowerSystems.jl#1774. + sub_data["bs"] = 0.0 + sub_data["solved_admittance"] = pop!(switched_shunt, "BINIT") sub_data["status"] = _determine_injector_status( switched_shunt, pm_data, @@ -944,18 +951,19 @@ function _psse2pm_shunt!(pm_data::Dict, pti_data::Dict, import_all::Bool, nb) ) initial_ss_status_sorted = sort(collect(keys(initial_ss_status)); by = x -> parse(Int, x[2:end])) - sub_data["initial_status"] = + sub_data["number_engaged"] = [initial_ss_status[k] for k in initial_ss_status_sorted] - sub_data["initial_status"] = - sub_data["initial_status"][1:length(sub_data["step_number"])] + sub_data["number_engaged"] = + sub_data["number_engaged"][1:length(sub_data["step_number"])] sub_data["ext"]["NREG"] = pop!(switched_shunt, "NREG") elseif pm_data["source_version"] ∈ ("30", "32", "33") - # Pre-v35 SWITCHED SHUNT records carry no per-block status field. BINIT - # already holds the total in-service admittance, which `bs` above passes - # on, so every block must start out of service whatever MODSW says — - # counting any of them in would double-count that same admittance. - sub_data["initial_status"] = zeros(Int, length(sub_data["y_increment"])) + # Pre-v35 SWITCHED SHUNT records carry no per-block status field, so how + # many steps of each block are engaged is simply unknown. The device's + # actual admittance is BINIT, now carried in `solved_admittance` above, so + # nothing has to be reconstructed from the blocks; zeros here record "no + # per-block information", not "every block is out of service". + sub_data["number_engaged"] = zeros(Int, length(sub_data["y_increment"])) else error("Unsupported PSS(R)E source version: $(pm_data["source_version"])") end diff --git a/test/test_openapi_dc_shunt.jl b/test/test_openapi_dc_shunt.jl index fc34569..7337f01 100644 --- a/test/test_openapi_dc_shunt.jl +++ b/test/test_openapi_dc_shunt.jl @@ -101,13 +101,16 @@ end ) # The fixture's bus-101 SWITCHED SHUNT record declares BINIT = 50.00 and B1 = 100.00, # so both must read back in the RAW's own MVAr rather than the pm dict's 0.5/1.0 pu. - @test _matches_nt(PFP.get_value(shunt, :Y), (real = 0.0, imag = 50.0)) + # BINIT is the SOLVED admittance, not a fixed base: it lands in `solved_admittance` and + # `Y` stays zero, since a PSS/E switched shunt has no fixed base term (#1774). + @test _matches_nt(PFP.get_value(shunt, :Y), (real = 0.0, imag = 0.0)) + @test PFP.get_value(shunt, :solved_admittance) == 50.0 @test only(y_increase).imag == 100.0 @test _matches_nt( PFP.get_value(shunt, :admittance_limits), (min = d["admittance_limits"][1], max = d["admittance_limits"][2]), ) - @test PFP.get_value(shunt, :initial_status) == d["initial_status"] + @test PFP.get_value(shunt, :number_engaged) == d["number_engaged"] end @testset "_switched_admittance_control_mode rejects an unrecognized MODSW code" begin diff --git a/test/test_parse_psse.jl b/test/test_parse_psse.jl index 312cdb9..c292756 100644 --- a/test/test_parse_psse.jl +++ b/test/test_parse_psse.jl @@ -146,11 +146,12 @@ end @test !haskey(pm_v33["bus"][3], "area_slack") end -@testset "PSSE pre-v35 switched shunt blocks start out of service" begin - # Pre-v35 SWITCHED SHUNT records have no per-block status field, so the parser has to - # fabricate one. BINIT already carries the total in-service admittance into `bs`, so - # every block must start at zero whatever MODSW says; an in-service block would - # double-count the admittance BINIT has already contributed. +@testset "PSSE pre-v35 switched shunts carry BINIT separately from the blocks" begin + # Pre-v35 SWITCHED SHUNT records have no per-block status field, so how many steps of + # each block are engaged is unknown and `number_engaged` is zero-filled to record that. + # BINIT is the device's actual admittance and now lands in its own `solved_admittance` + # key rather than in `bs`, so nothing has to be reconstructed from the blocks and there + # is no double-count to avoid. See PowerSystems.jl#1774. raw = read_fixture(FOURTEEN_BUS_FIXTURE) pm_data = parse_file(IOBuffer(raw); filetype = "raw") @test pm_data["source_version"] == "33" @@ -158,7 +159,11 @@ end shunts = collect(values(pm_data["switched_shunt"])) @test !isempty(shunts) for shunt in shunts - @test shunt["initial_status"] == zeros(Int, length(shunt["y_increment"])) + @test shunt["number_engaged"] == zeros(Int, length(shunt["y_increment"])) + # BINIT no longer masquerades as a fixed base admittance. + @test shunt["gs"] == 0.0 + @test shunt["bs"] == 0.0 + @test haskey(shunt, "solved_admittance") end # Bus 101's record is MODSW=1, which an earlier mode-specific patch already zeroed. @@ -171,7 +176,9 @@ end @test shunt_101["control_mode"] == 3 @test shunt_101["step_number"] == [5] @test length(shunt_101["y_increment"]) == 1 - @test shunt_101["initial_status"] == [0] + @test shunt_101["number_engaged"] == [0] + # BINIT = 50 MVAr on a 100 MVA base, per-unitized alongside bs/y_increment. + @test shunt_101["solved_admittance"] == 0.5 end @testset "PSSE transformer CM=2 magnetizing susceptance is inductive" begin