diff --git a/src/openapi/generation.jl b/src/openapi/generation.jl index fcc62c2..defdd4d 100644 --- a/src/openapi/generation.jl +++ b/src/openapi/generation.jl @@ -292,6 +292,7 @@ function _make_hydro_dispatch_body!( "MW/min") set_value!(component, :base_power, mbase, "MVA") add_component!(sys, component) + set_component_ext!(sys, component, get(pm_gen, "ext", Dict{String, Any}())) return end @@ -368,6 +369,7 @@ function make_renewable_dispatch!( set_value!(component, :power_factor, 1.0, "1") set_value!(component, :base_power, mbase, "MVA") add_component!(sys, component) + set_component_ext!(sys, component, get(pm_gen, "ext", Dict{String, Any}())) return end @@ -402,6 +404,7 @@ function make_renewable_nondispatch!( set_value!(component, :power_factor, 1.0, "1") set_value!(component, :base_power, mbase, "MVA") add_component!(sys, component) + set_component_ext!(sys, component, get(pm_gen, "ext", Dict{String, Any}())) return end @@ -485,11 +488,8 @@ function make_storage!( set_value!(component, :storage_technology_type, "OTHER_CHEM") set_value!(component, :storage_capacity, _natural_value(energy_rating, thermal_rating), "MWh") - set_value!( - component, - :storage_level_limits, - IC.MinMax(; min = 0.0, max = _natural_value(energy_rating, thermal_rating)), - ) + # A fraction of storage_capacity; pm data carries no derate, so the full band. + set_value!(component, :storage_level_limits, IC.MinMax(; min = 0.0, max = 1.0)) set_value!(component, :initial_storage_capacity_level, d["energy"] / energy_rating, "1") set_value!(component, :rating, _natural_value(thermal_rating, thermal_rating), "MVA") set_value!(component, :active_power, _natural_value(d["ps"], thermal_rating), "MW") @@ -519,6 +519,7 @@ function make_storage!( ) set_value!(component, :base_power, thermal_rating, "MVA") add_component!(sys, component) + set_component_ext!(sys, component, get(d, "ext", Dict{String, Any}())) return end diff --git a/src/openapi/load.jl b/src/openapi/load.jl index a419454..cdebde2 100644 --- a/src/openapi/load.jl +++ b/src/openapi/load.jl @@ -85,6 +85,7 @@ function _make_standard_load!( set_value!(load, :conformity, _conformity_string(Int(d["conformity"]))) _set_zip_fields!(load, d, base_power) add_component!(sys, load) + set_component_ext!(sys, load, get(d, "ext", Dict{String, Any}())) return end @@ -103,6 +104,7 @@ function _make_interruptible_standardload!( set_value!(load, :conformity, _conformity_string(Int(d["conformity"]))) _set_zip_fields!(load, d, base_power) add_component!(sys, load) + set_component_ext!(sys, load, get(d, "ext", Dict{String, Any}())) return end @@ -122,6 +124,7 @@ function _make_power_load!( set_value!(load, :max_reactive_power, d["qd"] * base_power, "MVAr") set_value!(load, :conformity, _conformity_string(Int(d["conformity"]))) add_component!(sys, load) + set_component_ext!(sys, load, get(d, "ext", Dict{String, Any}())) return end diff --git a/src/openapi/topology.jl b/src/openapi/topology.jl index 8830675..9f743f9 100644 --- a/src/openapi/topology.jl +++ b/src/openapi/topology.jl @@ -211,6 +211,7 @@ function read_bus!(sys::OpenAPISystem, data::Dict; kwargs...) set_value!(bus, :bustype, "SLACK") end add_component!(sys, bus) + set_component_ext!(sys, bus, get(d, "ext", Dict{String, Any}())) end return end diff --git a/test/test_openapi_generation.jl b/test/test_openapi_generation.jl index c131775..d7d05cc 100644 --- a/test/test_openapi_generation.jl +++ b/test/test_openapi_generation.jl @@ -338,6 +338,9 @@ end @test PFP.get_value(storage, :reactive_power_limits).min ≈ -0.0075 @test PFP.get_value(storage, :reactive_power_limits).max ≈ 0.0075 @test PFP.get_value(storage, :initial_storage_capacity_level) == 0.5 + # A fraction of storage_capacity: no derate means the full [0, 1] band. + @test PFP.get_value(storage, :storage_level_limits).min == 0.0 + @test PFP.get_value(storage, :storage_level_limits).max == 1.0 @test PFP.get_value(storage, :prime_mover_type) == "BA" @test PFP.get_value(storage, :storage_technology_type) == "OTHER_CHEM" end @@ -386,6 +389,8 @@ end @test PFP.get_value(storage, :reactive_power_limits).max ≈ d["qmax"] # Dimensionless / ratio fields: untouched by COMPONENT_BASE either way. @test PFP.get_value(storage, :initial_storage_capacity_level) == 0.5 + @test PFP.get_value(storage, :storage_level_limits).min == 0.0 + @test PFP.get_value(storage, :storage_level_limits).max == 1.0 end @testset "COMPONENT_BASE conversion errors loudly on an unregistered instance-dispatched field" begin @@ -451,6 +456,41 @@ end @test PFP.get_value(hydro, :active_power) ≈ 500.0 end +@testset "hydro, renewable and storage makers carry their pm dict entry's ext" begin + sys = PFP.OpenAPISystem(100.0) + reg = PFP.get_registry(sys) + bus = _register_test_bus!(sys) + gen(type, name) = Dict{String, Any}( + "mbase" => 100.0, "gen_status" => true, "pg" => 5.0, "qg" => 1.0, + "pmax" => 20.0, "pmin" => 0.0, "qmax" => 10.0, "qmin" => -10.0, + "type" => type, "ext" => Dict{String, Any}("mRID" => name), + ) + PFP.make_hydro_dispatch!(sys, reg, bus, gen("ROR", "hy1"), "hy1", 100.0) + PFP.make_hydro_reservoir!(sys, reg, bus, gen("HYDRO", "hy2"), "hy2", 100.0) + PFP.make_renewable_dispatch!(sys, reg, bus, gen("WIND", "wind1"), "wind1", 100.0) + PFP.make_renewable_nondispatch!(sys, reg, bus, gen("RTPV", "pv1"), "pv1", 100.0) + storage = Dict{String, Any}( + "energy_rating" => 50.0, "energy" => 25.0, "status" => true, + "thermal_rating" => 1.0, "ps" => 0.02, "charge_rating" => 0.05, + "discharge_rating" => 0.05, "charge_efficiency" => 0.9, + "discharge_efficiency" => 0.9, "qs" => 0.0, "qmin" => -0.05, "qmax" => 0.05, + "ext" => Dict{String, Any}("mRID" => "bat1"), + ) + PFP.make_storage!(sys, reg, bus, storage, "bat1", 100.0) + + components = vcat( + PFP.get_components(sys, "HydroDispatch"), + PFP.get_components(sys, "RenewableDispatch"), + PFP.get_components(sys, "RenewableNonDispatch"), + PFP.get_components(sys, "EnergyReservoirStorage"), + ) + @test length(components) == 5 + for component in components + @test PFP.get_ext(sys, PFP.get_value(component, :id)) == + Dict{String, Any}("mRID" => PFP.get_value(component, :name)) + end +end + @testset "get_generator_type resolving to EnergyReservoirStorage from \"gen\" is an error, not a skip" begin sys = PFP.OpenAPISystem(100.0) reg = PFP.get_registry(sys) diff --git a/test/test_openapi_load.jl b/test/test_openapi_load.jl index b88919c..0a534e7 100644 --- a/test/test_openapi_load.jl +++ b/test/test_openapi_load.jl @@ -143,3 +143,43 @@ end @test PFP.get_value(load, :conformity) == "CONFORMING" end end + +@testset "every load type carries its pm dict entry's ext" begin + pm = fourteen_bus_pm_data() + data = deepcopy(pm.data) + data["load"][first(keys(data["load"]))]["interruptible"] = 1 + for (key, d) in data["load"] + d["ext"]["mRID"] = "load-$key" + end + sys = PFP.OpenAPISystem(Float64(data["baseMVA"])) + PFP.read_loadzones!(sys, data) + PFP.read_bus!(sys, data) + PFP.read_loads!(sys, data) + loads = vcat( + PFP.get_components(sys, "StandardLoad"), + PFP.get_components(sys, "InterruptibleStandardLoad"), + ) + @test length(loads) == 13 + for load in loads + d = only( + v for v in values(data["load"]) if + strip(join(v["source_id"])) == PFP.get_value(load, :name) + ) + @test PFP.get_ext(sys, PFP.get_value(load, :id)) == d["ext"] + end + + pm = PFP.PowerModelsData(joinpath(MATPOWER_DIR, "case5.m")) + for (key, d) in pm.data["load"] + d["ext"] = Dict{String, Any}("mRID" => "load-$key") + end + sys = PFP.build_openapi_system(pm) + loads = PFP.get_components(sys, "PowerLoad") + @test length(loads) == 3 + for load in loads + d = only( + v for v in values(pm.data["load"]) if + strip(join(v["source_id"])) == PFP.get_value(load, :name) + ) + @test PFP.get_ext(sys, PFP.get_value(load, :id)) == d["ext"] + end +end diff --git a/test/test_openapi_topology.jl b/test/test_openapi_topology.jl index 29cfc7c..d669242 100644 --- a/test/test_openapi_topology.jl +++ b/test/test_openapi_topology.jl @@ -158,6 +158,18 @@ end @test all(b -> PFP.get_value(b, :bustype) != "SLACK", other) end +@testset "ACBus carries its pm dict entry's ext" begin + pm = fourteen_bus_pm_data() + for (number, d) in pm.data["bus"] + d["ext"] = Dict{String, Any}("mRID" => "bus-$number") + end + sys = PFP.build_openapi_system(pm) + for bus in PFP.get_components(sys, "ACBus") + ext = PFP.get_ext(sys, PFP.get_value(bus, :id)) + @test ext == Dict{String, Any}("mRID" => "bus-$(PFP.get_value(bus, :number))") + end +end + @testset "Area.ext carries PSS/E AREA DATA metadata matched by area_number" begin pm_data = PFP.PowerModelsData(joinpath(@__DIR__, "fixtures", "v35_area_slack_variants.raw"))