diff --git a/Project.toml b/Project.toml index a58c8b4..7bea4b6 100644 --- a/Project.toml +++ b/Project.toml @@ -11,14 +11,16 @@ JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" OpenAPI = "d5e62ea6-ddf3-4d43-8e4c-ad5e6c8bfd7d" PowerCoreOpenAPIModels = "b7b40286-e793-417d-a9a0-b1583e4da1cb" +PowerOpenAPIModels = "0730f07c-cff6-4c3b-a9df-c546153be50a" PowerOperationsOpenAPIModels = "a372b6d7-45a2-44c2-8199-6a724b72e8ff" Unicode = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" [sources] # These packages are generated and not yet released, so they resolve from the branch # rather than the registry. Switch to a tagged rev once one exists. -PowerCoreOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "jd/openapi_regen", subdir = "PowerCoreOpenAPIModels.jl"} -PowerOperationsOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "jd/openapi_regen", subdir = "PowerOperationsOpenAPIModels.jl"} +PowerCoreOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "feat/infrastore-integration", subdir = "PowerCoreOpenAPIModels.jl"} +PowerOperationsOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "feat/infrastore-integration", subdir = "PowerOperationsOpenAPIModels.jl"} +InfrastructureSystems = {rev = "feat/infrastore-integration", url = "https://github.com/Sienna-Platform/InfrastructureSystems.jl"} [compat] DataStructures = "0.19.3" @@ -28,6 +30,7 @@ JSON = "1" LinearAlgebra = "1" OpenAPI = "0.2" PowerCoreOpenAPIModels = "0.1" +PowerOpenAPIModels = "0.1" PowerOperationsOpenAPIModels = "0.1" Unicode = "1" julia = "^1.10" diff --git a/docs/Project.toml b/docs/Project.toml index 059f6a4..278f10f 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -11,8 +11,8 @@ PowerOperationsOpenAPIModels = "a372b6d7-45a2-44c2-8199-6a724b72e8ff" # pin them. [sources] PowerFlowFileParser = {path = ".."} -PowerCoreOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "jd/openapi_regen", subdir = "PowerCoreOpenAPIModels.jl"} -PowerOperationsOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "jd/openapi_regen", subdir = "PowerOperationsOpenAPIModels.jl"} +PowerCoreOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "feat/infrastore-integration", subdir = "PowerCoreOpenAPIModels.jl"} +PowerOperationsOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "feat/infrastore-integration", subdir = "PowerOperationsOpenAPIModels.jl"} [compat] julia = "^1.10" diff --git a/src/PowerFlowFileParser.jl b/src/PowerFlowFileParser.jl index 0720f90..373e07f 100644 --- a/src/PowerFlowFileParser.jl +++ b/src/PowerFlowFileParser.jl @@ -20,8 +20,10 @@ import Unicode: normalize import JSON import OpenAPI import PowerCoreOpenAPIModels +import PowerOpenAPIModels import PowerOperationsOpenAPIModels const PC = PowerCoreOpenAPIModels +const PD = PowerOpenAPIModels const PO = PowerOperationsOpenAPIModels import InfrastructureSystems diff --git a/src/openapi/attributes.jl b/src/openapi/attributes.jl index 14a1f66..f2647a3 100644 --- a/src/openapi/attributes.jl +++ b/src/openapi/attributes.jl @@ -16,7 +16,7 @@ """ One piecewise-linear curve and control-mode per impedance-correction table number. -Ported from PSCB's `_impedance_correction_table_lookup` (:213-261), minus the per-winding +Ported from PSCB's `_impedance_correction_table_lookup`, minus the per-winding pre-expansion — [`_attach_impedance_correction!`](@ref) does that lazily, only for (table, winding) pairs a transformer actually references. """ @@ -78,7 +78,8 @@ function _new_impedance_correction_attribute!( end """ -Attach the `ImpedanceCorrectionData` for `(d[table_key], winding)` to `transformer_id`, +Attach the `ImpedanceCorrectionData` for `(d[table_key], winding)` to `transformer_id` +(a `transformer_type` component: `"TwoWindingTransformer"` or `"ThreeWindingTransformer"`), if `d[table_key]` names a table `curves` has an entry for. `table_key` absent, or naming table `0` (PSS/E's "no correction table" default, per `pti.jl`'s TAB1/TAB2/TAB3 defaults), is a no-op — matching PSCB's `_attach_single_ict!`, which only ever finds a @@ -94,6 +95,7 @@ function _attach_impedance_correction!( table_key::AbstractString, winding::AbstractString, transformer_id::Int, + transformer_type::AbstractString, ) if isempty(curves) || !haskey(d, table_key) return @@ -105,7 +107,9 @@ function _attach_impedance_correction!( key = (table_number, winding) if haskey(cache, key) # The attribute already exists, so only the association row is new. - add_supplemental_attribute_association!(sys, cache[key], transformer_id) + add_supplemental_attribute_association!( + sys, cache[key], transformer_id, transformer_type, + ) else cache[key] = _new_impedance_correction_attribute!(sys, curves, table_number, winding, @@ -142,7 +146,7 @@ function read_substations!(sys::OpenAPISystem, data::Dict; kwargs...) set_value!(attribute, :grounding_resistance, d["grounding_resistance"], "ohm") add_supplemental_attribute!(sys, attribute, first(bus_ids)) for bus_id in Iterators.drop(bus_ids, 1) - add_supplemental_attribute_association!(sys, attribute, bus_id) + add_supplemental_attribute_association!(sys, attribute, bus_id, "ACBus") end end return @@ -151,7 +155,7 @@ end """ Attach `ImpedanceCorrectionData` supplemental attributes to every `TwoWindingTransformer`/ `ThreeWindingTransformer` that references an impedance-correction table. Ported from -PSCB's `_attach_impedance_correction_tables!` (:294-330), but driven by re-walking +PSCB's `_attach_impedance_correction_tables!`, but driven by re-walking `data["branch"]`/`data["3w_transformer"]` rather than called inline from the transformer readers — so the name derivation below must match [`read_branches!`](@ref)/[`read_3w_transformers!`](@ref) exactly, same formatter kwargs @@ -179,6 +183,7 @@ function read_impedance_corrections!(sys::OpenAPISystem, data::Dict; kwargs...) transformer_id = get_id(reg, "TwoWindingTransformer", name) _attach_impedance_correction!( sys, cache, curves, d, "correction_table", "TR2W_WINDING", transformer_id, + "TwoWindingTransformer", ) end @@ -197,15 +202,15 @@ function read_impedance_corrections!(sys::OpenAPISystem, data::Dict; kwargs...) transformer_id = get_id(reg, "ThreeWindingTransformer", name) _attach_impedance_correction!( sys, cache, curves, d, "primary_correction_table", "PRIMARY_WINDING", - transformer_id, + transformer_id, "ThreeWindingTransformer", ) _attach_impedance_correction!( sys, cache, curves, d, "secondary_correction_table", "SECONDARY_WINDING", - transformer_id, + transformer_id, "ThreeWindingTransformer", ) _attach_impedance_correction!( sys, cache, curves, d, "tertiary_correction_table", "TERTIARY_WINDING", - transformer_id, + transformer_id, "ThreeWindingTransformer", ) end return diff --git a/src/openapi/branch.jl b/src/openapi/branch.jl index 379f5ca..8c39fd8 100644 --- a/src/openapi/branch.jl +++ b/src/openapi/branch.jl @@ -46,15 +46,6 @@ function _get_rating(name::AbstractString, d::Dict, key::AbstractString) return d[key] end -"""A `_get_rating`-style optional value (`nothing` when the pm dict carries no such key), -scaled onto the circuit's own `base_power`.""" -function _scaled_or_nothing(value, base_power::Real) - if isnothing(value) - return nothing - end - return value * base_power -end - """Bus name and ISOLATED status by pm bus number. Shared by the branch/transformer/ dc-line/shunt readers, all of which resolve endpoints by pm bus number rather than by document id.""" @@ -267,7 +258,7 @@ function _make_transformer_circuit!( set_value!(circuit, :arc, arc_id) set_value!(circuit, :tap, get(d, tap_key, 1.0), "1") set_value!(circuit, :alpha, d[angle_key], "rad") - set_value!(circuit, :parameter_units, "DEVICE_BASE") + set_value!(circuit, :parameter_units, "COMPONENT_BASE") set_value!(circuit, :r, r, "pu") set_value!(circuit, :x, x, "pu") _set_transformer_control_fields!(circuit, d, control_suffix, record) @@ -315,9 +306,9 @@ function make_line!( set_value!(line, :b, (from = d["b_fr"], to = d["b_to"]), "pu") set_value!(line, :rating, _get_rating(name, d, "rate_a") * sys_mbase, "MVA") set_optional_value!(line, :rating_b, - _scaled_or_nothing(_get_rating(name, d, "rate_b"), sys_mbase), "MVA") + _natural_value(_get_rating(name, d, "rate_b"), sys_mbase), "MVA") set_optional_value!(line, :rating_c, - _scaled_or_nothing(_get_rating(name, d, "rate_c"), sys_mbase), "MVA") + _natural_value(_get_rating(name, d, "rate_c"), sys_mbase), "MVA") set_value!(line, :angle_limits, (min = d["angmin"], max = d["angmax"]), "rad") add_component!(sys, line) set_component_ext!(sys, line, get(d, "ext", Dict{String, Any}())) @@ -325,7 +316,7 @@ function make_line!( end """A zero-impedance pm branch, converted to a `DiscreteControlledACBranch` of type -`SWITCH`. Ported from PSCB's `_make_switch_from_zero_impedance_line` (:1270-1301) — a +`SWITCH`. Ported from PSCB's `_make_switch_from_zero_impedance_line` — a real PSS/E data shape (a modeled switching device recorded as a zero-r/x branch), not one of the four named bug-compatible sites.""" function make_switch_from_zero_impedance_branch!( @@ -370,7 +361,7 @@ function _branch_type_matpower(d::Dict) shift = d["shift"] is_transformer = d["transformer"] if !is_transformer - is_transformer = (tap != 0.0 && tap != 1.0) || shift != 0.0 + is_transformer = (!iszero(tap) && tap != 1.0) || !iszero(shift) end if is_transformer return :transformer @@ -385,7 +376,7 @@ function _branch_type_psse(d::Dict, name::AbstractString) is_transformer = d["transformer"] tap = d["tap"] if !is_transformer - if tap != 0.0 && tap != 1.0 + if !iszero(tap) && tap != 1.0 @warn "Transformer $name has tap ratio $tap, which is not 0.0 or 1.0; this is not a valid value for a Line. Parsing entry as a Transformer" else return :line @@ -395,7 +386,7 @@ function _branch_type_psse(d::Dict, name::AbstractString) end """Two-winding transformer + its `TransformerCircuit`. Ported from PSCB's -`make_transformer_2w` (:1503-1553). See the file header for the rating/flow base +`make_transformer_2w`. See the file header for the rating/flow base (circuit's own `base_power`, not `sys_mbase`) and the magnetizing shunt basis (`g_fr`/`b_fr` are already device-base pu for PSS/E-origin data; identical to system base for MATPOWER, where `base_power == sys_mbase` unconditionally).""" @@ -420,9 +411,9 @@ function make_transformer_2w!( tap_key = "tap", angle_key = "shift", control_suffix = 1, available = available, r = d["br_r"], x = d["br_x"], - rating = _scaled_or_nothing(rate_a, base_power), - rating_b = _scaled_or_nothing(rate_b, base_power), - rating_c = _scaled_or_nothing(rate_c, base_power), + rating = _natural_value(rate_a, base_power), + rating_b = _natural_value(rate_b, base_power), + rating_c = _natural_value(rate_c, base_power), base_power = base_power, base_voltage_primary = _base_voltage_or_nothing(d["base_voltage_from"]), base_voltage_secondary = _base_voltage_or_nothing(d["base_voltage_to"]), @@ -434,7 +425,7 @@ function make_transformer_2w!( set_value!(component, :id, register!(reg, "TwoWindingTransformer", name)) set_value!(component, :name, name) set_value!(component, :circuit, circuit_id) - set_value!(component, :admittance_units, "DEVICE_BASE") + set_value!(component, :admittance_units, "COMPONENT_BASE") set_value!(component, :magnetizing_shunt, (real = d["g_fr"], imag = d["b_fr"]), "pu") add_component!(sys, component) set_component_ext!(sys, component, get(d, "ext", Dict{String, Any}())) @@ -505,7 +496,7 @@ function make_3w_transformer!( set_value!(component, :secondary_circuit, secondary_circuit) set_value!(component, :tertiary_circuit, tertiary_circuit) set_value!(component, :star_bus, star_id) - set_value!(component, :parameter_units, "DEVICE_BASE") + set_value!(component, :parameter_units, "COMPONENT_BASE") set_value!(component, :r_12, d["r_12"], "pu") set_value!(component, :x_12, d["x_12"], "pu") set_value!(component, :r_23, d["r_23"], "pu") @@ -515,7 +506,7 @@ function make_3w_transformer!( set_value!(component, :base_power_12, d["base_power_12"], "MVA") set_value!(component, :base_power_23, d["base_power_23"], "MVA") set_value!(component, :base_power_31, d["base_power_31"], "MVA") - set_value!(component, :admittance_units, "DEVICE_BASE") + set_value!(component, :admittance_units, "COMPONENT_BASE") set_value!(component, :magnetizing_shunt, (real = d["g"], imag = d["b"]), "pu") add_component!(sys, component) set_component_ext!(sys, component, get(d, "ext", Dict{String, Any}())) @@ -578,7 +569,7 @@ end """ Create one `ThreeWindingTransformer` per `data["3w_transformer"]` entry. Ported from -PSCB's `read_3w_transformer!` (:1685-1721). `data["3w_transformer"]`'s star bus is a +PSCB's `read_3w_transformer!`. `data["3w_transformer"]`'s star bus is a regular pm dict bus entry (already an `ACBus` by the time this reader runs, via `read_bus!`), not created here. """ diff --git a/src/openapi/build.jl b/src/openapi/build.jl index eb51571..cfd0120 100644 --- a/src/openapi/build.jl +++ b/src/openapi/build.jl @@ -71,7 +71,7 @@ a caller never mistakes a partial document for a complete one. `unit_system` selects the convention the values are stored in, same as [`OpenAPISystem`](@ref): `"NATURAL_UNITS"` (the default) leaves every reader's MW/MVAr/MVA -values as computed; `"DEVICE_BASE"` additionally runs [`apply_device_base_conversion!`](@ref) +values as computed; `"COMPONENT_BASE"` additionally runs [`apply_device_base_conversion!`](@ref) over the built document, converting every power-family field into per-unit on the component's own device base (or the system base, for the few types with none of their own) — the document PowerSystems' `DeviceBaseUnit` importer expects. Keyword arguments — the diff --git a/src/openapi/container.jl b/src/openapi/container.jl index d026f17..1b9cacc 100644 --- a/src/openapi/container.jl +++ b/src/openapi/container.jl @@ -1,5 +1,5 @@ """ -The document PowerFlowFileParser emits, a thin wrapper over `PC.SystemDocument`. +The document PowerFlowFileParser emits, a thin wrapper over `PD.SystemDocument`. `document` is the only serialized artifact: components, the association tables, `ext` and the unit convention all live on it. `registry` is build-time scaffolding, holding @@ -11,7 +11,7 @@ built. Matpower carry no time series. """ struct OpenAPISystem - document::PC.SystemDocument + document::PD.SystemDocument registry::IdRegistry time_series::Vector{IS.TimeSeriesData} end @@ -21,9 +21,9 @@ Unit conventions a document may be written in, from the schemas' `UnitSystem`. The schemas offer no system-base option: per-unit data historically on the system base records that base in the component's own `base_power` and rides as -`DEVICE_BASE`. +`COMPONENT_BASE`. """ -const UNIT_SYSTEMS = ("NATURAL_UNITS", "DEVICE_BASE") +const UNIT_SYSTEMS = ("NATURAL_UNITS", "COMPONENT_BASE") function OpenAPISystem( base_power::Float64; @@ -36,7 +36,7 @@ function OpenAPISystem( ), ) end - document = PC.SystemDocument(base_power; unit_system = unit_system) + document = PD.SystemDocument(base_power; unit_system = unit_system) return OpenAPISystem(document, IdRegistry(document), Vector{IS.TimeSeriesData}()) end @@ -49,7 +49,7 @@ Kept beside the components rather than inside them: the schemas describe what a component is, and this is whatever else the source data happened to state. """ function set_ext!(sys::OpenAPISystem, component_id::Int, extras::Dict{String, Any}) - PC.set_ext!(get_document(sys), component_id, extras) + PD.set_ext!(get_document(sys), component_id, extras) return end @@ -64,62 +64,71 @@ function set_component_ext!(sys::OpenAPISystem, component, extras::Dict{String, return end -get_ext(sys::OpenAPISystem, component_id::Int) = PC.get_ext(get_document(sys), component_id) +get_ext(sys::OpenAPISystem, component_id::Int) = PD.get_ext(get_document(sys), component_id) -get_base_power(sys::OpenAPISystem) = PC.get_base_power(get_document(sys)) +get_base_power(sys::OpenAPISystem) = PD.get_base_power(get_document(sys)) get_registry(sys::OpenAPISystem) = sys.registry -get_unit_system(sys::OpenAPISystem) = PC.get_unit_system(get_document(sys)) +get_unit_system(sys::OpenAPISystem) = PD.get_unit_system(get_document(sys)) """ Whether values are stored per unit rather than in the schemas' natural units. -`DEVICE_BASE` reproduces PowerSystems' storage convention. The `x-unit` annotations +`COMPONENT_BASE` reproduces PowerSystems' storage convention. The `x-unit` annotations still name the natural unit either way, so a per-unit document is for comparison against PowerSystems rather than for a consumer that reads the annotations — which is why the document states the convention it was written in. """ -uses_per_unit(sys::OpenAPISystem) = PC.uses_per_unit(get_document(sys)) +uses_per_unit(sys::OpenAPISystem) = PD.uses_per_unit(get_document(sys)) function add_component!(sys::OpenAPISystem, component::T) where {T <: OpenAPI.APIModel} - PC.add_component!(get_document(sys), component) + PD.add_component!(get_document(sys), component) return end """ -Record a supplemental attribute and the entity it describes. +Record a supplemental attribute and the component it describes. Plant-family groupings and service memberships are recorded in their own tables, not this one — see `add_service_association!` below — so this parser only ever emits a plain -attribute row. +attribute row. `component_id`'s type name is resolved from the document itself, so the +caller need not know it. """ function add_supplemental_attribute!( sys::OpenAPISystem, attribute::OpenAPI.APIModel, - entity_id::Int, + component_id::Int, ) - PC.add_supplemental_attribute!(get_document(sys), attribute, entity_id) + PD.add_supplemental_attribute!(get_document(sys), attribute, component_id) return end """ -Describe `entity_id` with an attribute [`add_supplemental_attribute!`](@ref) already -recorded — one attribute shared across several entities takes one row per extra entity. +Describe `component_id` with an attribute [`add_supplemental_attribute!`](@ref) already +recorded — one attribute shared across several components takes one row per extra +component. + +`component_type` names `component_id`'s type (e.g. `"ACBus"`); unlike +[`add_supplemental_attribute!`](@ref), which resolves it by scanning the document for the +first component, the caller passes it directly here since it already knows it from having +looked `component_id` up. `attribute_type` is derived from the attribute rather than passed in, matching what -`add_supplemental_attribute!` writes for the first entity; a literal would let the two +`add_supplemental_attribute!` writes for the first component; a literal would let the two disagree. """ function add_supplemental_attribute_association!( sys::OpenAPISystem, attribute::OpenAPI.APIModel, - entity_id::Int, + component_id::Int, + component_type::AbstractString, ) push!( get_document(sys).supplemental_attribute_associations, PC.SupplementalAttributeAssociation(; + component_id = component_id, + component_type = String(component_type), attribute_id = get_value(attribute, :id), - entity_id = entity_id, attribute_type = string(nameof(typeof(attribute))), ), ) @@ -161,12 +170,12 @@ end """Attributes of one type, in the order they were added.""" function get_supplemental_attributes(sys::OpenAPISystem, type_name::AbstractString) - return PC.get_supplemental_attributes(get_document(sys), type_name) + return PD.get_supplemental_attributes(get_document(sys), type_name) end function get_components(sys::OpenAPISystem, type_name::AbstractString) - return PC.get_components(get_document(sys), type_name) + return PD.get_components(get_document(sys), type_name) end """Type names in sorted order, so serialized output is deterministic.""" -component_type_names(sys::OpenAPISystem) = PC.component_type_names(get_document(sys)) +component_type_names(sys::OpenAPISystem) = PD.component_type_names(get_document(sys)) diff --git a/src/openapi/cost.jl b/src/openapi/cost.jl index a54552e..5f8b405 100644 --- a/src/openapi/cost.jl +++ b/src/openapi/cost.jl @@ -78,7 +78,7 @@ Thermal generation cost from a MATPOWER-shaped `pm_gen`'s `"model"`/`"cost"` fie Model `1` is PIECEWISE_LINEAR, `2` is POLYNOMIAL (MATPOWER manual Table B-4). A generator carrying neither key gets a zero natural-unit cost curve, matching PSCB's own fallback -(and its warning). The resulting variable cost is `DEVICE_BASE` per-unit — PSCB's +(and its warning). The resulting variable cost is `COMPONENT_BASE` per-unit — PSCB's `CostCurve(_, IS.DU)` — never natural units, unlike the zero-cost fallback. """ function make_thermal_cost(gen_name::AbstractString, pm_gen::Dict, sys_mbase::Float64) @@ -103,7 +103,7 @@ function make_thermal_cost(gen_name::AbstractString, pm_gen::Dict, sys_mbase::Fl end return PC.ThermalGenerationCost(; variable = PC.CostCurve(; - power_units = "DEVICE_BASE", + power_units = "COMPONENT_BASE", value_curve = PC.ValueCurve( PC.InputOutputCurve(; function_data = PC.InputOutputCurveFunctionData(function_data), diff --git a/src/openapi/dc_branch.jl b/src/openapi/dc_branch.jl index adf9799..5aa74c4 100644 --- a/src/openapi/dc_branch.jl +++ b/src/openapi/dc_branch.jl @@ -10,6 +10,21 @@ # `data["vscline"]` and `data["interarea_transfer"]` are NOT native PowerModels sections; # see the per-maker docstrings below for how PFFP's own `psse.jl` pre-scales their fields. +"""A linear `TwoTerminalLoss` curve from a pm dict's `loss0`/`loss1` fields, shared by +`TwoTerminalLCCLine` and `TwoTerminalGenericHVDCLine`.""" +function _two_terminal_loss(d::Dict) + return PC.TwoTerminalLoss( + PC.InputOutputCurve(; + function_data = PC.InputOutputCurveFunctionData( + PC.LinearFunctionData(; + proportional_term = d["loss1"], + constant_term = d["loss0"], + ), + ), + ), + ) +end + """Two-terminal LCC HVDC line (PSS/E).""" function make_lcc_line!( sys::OpenAPISystem, @@ -74,20 +89,7 @@ function make_lcc_line!( set_value!(component, :inverter_extinction_angle, d["inverter_extinction_angle"], "rad") set_value!(component, :inverter_capacitor_reactance, d["inverter_capacitor_reactance"], "ohm") - set_value!( - component, - :loss, - PC.TwoTerminalLoss( - PC.InputOutputCurve(; - function_data = PC.InputOutputCurveFunctionData( - PC.LinearFunctionData(; - proportional_term = d["loss1"], - constant_term = d["loss0"], - ), - ), - ), - ), - ) + set_value!(component, :loss, _two_terminal_loss(d)) set_value!(component, :base_power, sys_mbase, "MVA") add_component!(sys, component) set_component_ext!(sys, component, get(d, "ext", Dict{String, Any}())) @@ -119,20 +121,7 @@ function make_generic_hvdc_line!( (min = d["qminf"] * sys_mbase, max = d["qmaxf"] * sys_mbase), "MVAr") set_value!(component, :reactive_power_limits_to, (min = d["qmint"] * sys_mbase, max = d["qmaxt"] * sys_mbase), "MVAr") - set_value!( - component, - :loss, - PC.TwoTerminalLoss( - PC.InputOutputCurve(; - function_data = PC.InputOutputCurveFunctionData( - PC.LinearFunctionData(; - proportional_term = d["loss1"], - constant_term = d["loss0"], - ), - ), - ), - ), - ) + set_value!(component, :loss, _two_terminal_loss(d)) set_value!(component, :base_power, sys_mbase, "MVA") add_component!(sys, component) return @@ -166,7 +155,7 @@ end """ Voltage-source-converter HVDC line (PSS/E `VOLTAGE SOURCE CONVERTER`). Ported from -PSCB's `make_vscline` (:1820-1874). +PSCB's `make_vscline`. Every numeric field `psse.jl` derives from a per-bridge PSS/E record (`rating`/ `rating_from`/`rating_to`, `active_power_limits_from`/`to`, `reactive_power_limits_from`/ @@ -180,15 +169,16 @@ are already natural (Amperes / a bare fraction) and pass through unscaled. voltage, or on `sys_mbase` when it controls DC power. `setpoint_voltage_units` (decoupled from `voltage_units`, which tags only -`voltage_limits_from`/`to`) is set unconditionally to `DEVICE_BASE`: PSS/E always reports a +`voltage_limits_from`/`to`) is set unconditionally to `COMPONENT_BASE`: PSS/E always reports a voltage-controlling side's DC setpoint as p.u. of `rated_dc_voltage` (`psse.jl` pre-divides `DCSET` by `base_voltage`) and a voltage-controlling AC setpoint (`ACSET`) as p.u. of the AC bus's own base voltage — never kV. The `DC_POWER`/`AC_REACTIVE_POWER` branches have their own fixed units (`MW`/`1`) and ignore this discriminator, so setting it unconditionally is safe regardless of which sides actually control voltage. -No fixture on hand carries a `vscline` entry; this maker is exercised by synthetic dicts -in the test suite. +`psse.jl` also captures each converter's own AC bus base kV as `base_voltage_from`/ +`base_voltage_to`, threaded onto the document as `rated_ac_voltage_from`/ +`rated_ac_voltage_to` — the AC-side counterpart of `rated_dc_voltage`. """ function make_vscline!( sys::OpenAPISystem, @@ -218,7 +208,7 @@ function make_vscline!( set_value!(component, :dc_current, get(d, "if", 0.0), "A") set_value!(component, :reactive_power_from, get(d, "qf", 0.0) * sys_mbase, "MVAr") # See the docstring: PSS/E's voltage-controlling setpoints are always already p.u. - set_value!(component, :setpoint_voltage_units, "DEVICE_BASE") + set_value!(component, :setpoint_voltage_units, "COMPONENT_BASE") if d["dc_voltage_control_from"] set_value!(component, :dc_control_from, "DC_VOLTAGE") set_value!(component, :dc_setpoint_from, d["dc_setpoint_from"], "pu") @@ -233,6 +223,7 @@ function make_vscline!( set_value!(component, :ac_control_from, "AC_REACTIVE_POWER") set_value!(component, :ac_setpoint_from, d["ac_setpoint_from"], "1") end + set_value!(component, :rated_ac_voltage_from, d["base_voltage_from"], "kV") set_value!( component, :converter_loss_from, @@ -268,6 +259,7 @@ function make_vscline!( set_value!(component, :ac_control_to, "AC_REACTIVE_POWER") set_value!(component, :ac_setpoint_to, d["ac_setpoint_to"], "1") end + set_value!(component, :rated_ac_voltage_to, d["base_voltage_to"], "kV") set_value!( component, :converter_loss_to, @@ -331,7 +323,7 @@ end """ Create one `TwoTerminalVSCLine` per `data["vscline"]` entry. Ported from PSCB's -`read_vscline!` (:1876-1902), including the undefined-bus warn-and-skip (a real, already +`read_vscline!`, including the undefined-bus warn-and-skip (a real, already logged skip in the oracle, ported as-is — not one of this reader's own silent skips). """ function read_vsc_lines!(sys::OpenAPISystem, data::Dict; kwargs...) @@ -360,10 +352,10 @@ end """ Create one `AreaInterchange` per `data["interarea_transfer"]` entry. Ported from the -`interarea_transfer` block inside PSCB's `read_bus!` (:481-529), grouped here with the -other DC/interchange readers rather than with `topology.jl`'s bus reader. +`interarea_transfer` block inside PSCB's `read_bus!`, grouped here with the other +DC/interchange readers rather than with `topology.jl`'s bus reader. -# Bug-compatible with PSCB power_models_data.jl:509 — `active_power_flow = +# Bug-compatible with PSCB — `active_power_flow = d["power_transfer"]` assigns PFFP's raw `PTRAN` value (already natural MW, since `interarea_transfer` is not a native PowerModels section) into a field PSY declares `SU`, with no division by `sys_mbase` first, so a real `get_active_power_flow(interchange, @@ -407,7 +399,7 @@ function read_area_interchanges!(sys::OpenAPISystem, data::Dict; kwargs...) set_value!(component, :id, register!(reg, "AreaInterchange", name)) set_value!(component, :name, name) set_value!(component, :available, true) - # Bug-compatible with PSCB power_models_data.jl:509 — see docstring. + # Bug-compatible with PSCB — see docstring. set_value!(component, :active_power_flow, d["power_transfer"] * sys_mbase, "MW") set_value!(component, :from_area, get_id(reg, "Area", area_from_name)) set_value!(component, :to_area, get_id(reg, "Area", area_to_name)) diff --git a/src/openapi/device_base.jl b/src/openapi/device_base.jl index 57f618d..79677a1 100644 --- a/src/openapi/device_base.jl +++ b/src/openapi/device_base.jl @@ -1,11 +1,11 @@ -# Hand-written: the DEVICE_BASE post-build conversion pass. +# Hand-written: the COMPONENT_BASE post-build conversion pass. # # Every reader in this directory (load.jl, generation.jl, branch.jl, ...) computes and # assigns natural-unit values (MW/MVAr/MVA) onto the OpenAPI components it builds, # regardless of `unit_system` — `set_value!` (units.jl) only converts between compatible # physical units (kW -> MW), never into a per-unit convention. Before this pass existed, -# a `unit_system = "DEVICE_BASE"` document stamped the flag but carried the same natural -# values as `"NATURAL_UNITS"`. This pass closes that gap: when the document is DEVICE_BASE, +# a `unit_system = "COMPONENT_BASE"` document stamped the flag but carried the same natural +# values as `"NATURAL_UNITS"`. This pass closes that gap: when the document is COMPONENT_BASE, # it walks every built component afterward and divides each power-family field by the # component's own device base (or, for a type with no device base of its own, the # document's system base) — the exact inverse of what PowerSystems' own `NaturalUnit` @@ -94,9 +94,9 @@ const _DEVICEBASE_SYSTEM_BASE_TYPES = Set(["OnlineReserve", "OfflineReserve", "GroupReserve"]) const _DEVICEBASE_INSTANCE_DISPATCHED = Dict{Tuple{String, Symbol}, Symbol}( - # parameter_units/admittance_units/voltage_setpoint_units always "DEVICE_BASE" + # parameter_units/admittance_units/voltage_setpoint_units always "COMPONENT_BASE" # (branch.jl, shunt.jl) -- pu on the component's own base_power (or, for the shunt - # admittance fields, DEVICE_MVAR, see below) already, identical in both document + # admittance fields, COMPONENT_MVAR, see below) already, identical in both document # conventions. ("TransformerCircuit", :r) => :skip, ("TransformerCircuit", :x) => :skip, @@ -128,7 +128,7 @@ const _DEVICEBASE_INSTANCE_DISPATCHED = Dict{Tuple{String, Symbol}, Symbol}( # VOLTAGE-family, already-`:skip` branch either way). Static `:skip`, matching # `control_limits`. ("TransformerCircuit", :controlled_quantity_limits) => :skip, - # admittance_units always "DEVICE_MVAR" (shunt.jl) -- PowerSystems' own to_openapi + # admittance_units always "COMPONENT_MVAR" (shunt.jl) -- PowerSystems' own to_openapi # confirms this is fixed-natural, multiplied by the SYSTEM base in both document # conventions (export_handwritten.jl's FixedAdmittance section), not document-unit- # system-governed at all (same shape as Area/LoadZone's peak fields). @@ -138,7 +138,7 @@ const _DEVICEBASE_INSTANCE_DISPATCHED = Dict{Tuple{String, Symbol}, Symbol}( ("SwitchedAdmittance", :admittance_limits) => :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 - # document's unit_system, the mirror image of the DEVICE_BASE cases above. + # document's unit_system, the mirror image of the COMPONENT_BASE cases above. ("TwoTerminalLCCLine", :r) => :skip, ("TwoTerminalLCCLine", :rectifier_rc) => :skip, ("TwoTerminalLCCLine", :rectifier_xc) => :skip, @@ -191,10 +191,10 @@ function _devicebase_instance_dispatched(key::AbstractString, prop::Symbol) verdict = get(_DEVICEBASE_INSTANCE_DISPATCHED, (String(key), prop), nothing) if verdict === nothing error( - "DEVICE_BASE conversion: $key.$prop has an instance-level unit discriminator " * + "COMPONENT_BASE conversion: $key.$prop has an instance-level unit discriminator " * "not accounted for in _DEVICEBASE_INSTANCE_DISPATCHED — classify it as " * ":convert_own, :skip, or :dynamic (see device_base.jl's header) before " * - "building a DEVICE_BASE document containing this type", + "building a COMPONENT_BASE document containing this type", ) end return verdict @@ -229,7 +229,7 @@ function _devicebase_dynamic(key::AbstractString, prop::Symbol, po) quantities = get(_DEVICEBASE_DYNAMIC_QUANTITIES, (key, prop), nothing) if quantities === nothing error( - "DEVICE_BASE conversion: $key.$prop is registered :dynamic with no entry in " * + "COMPONENT_BASE conversion: $key.$prop is registered :dynamic with no entry in " * "_DEVICEBASE_DYNAMIC_QUANTITIES", ) end @@ -237,7 +237,7 @@ function _devicebase_dynamic(key::AbstractString, prop::Symbol, po) verdict = get(quantities, quantity, nothing) if verdict === nothing error( - "DEVICE_BASE conversion: $key.$prop resolved quantity \"$quantity\", not " * + "COMPONENT_BASE conversion: $key.$prop resolved quantity \"$quantity\", not " * "accounted for in _DEVICEBASE_DYNAMIC_QUANTITIES[($key, :$prop)]", ) end @@ -245,7 +245,7 @@ function _devicebase_dynamic(key::AbstractString, prop::Symbol, po) end """ -Classify `key.prop` (PO type `T`) for the DEVICE_BASE pass: `:convert_own` (divide by the +Classify `key.prop` (PO type `T`) for the COMPONENT_BASE pass: `:convert_own` (divide by the component's own `base_power`), `:convert_system` (divide by the document's system base), `:dynamic` (resolved per component by [`_devicebase_dynamic`](@ref)), or `:skip`. See this file's header for the full rule. @@ -292,7 +292,7 @@ to classify, rather than a silent skip.""" function _devicebase_own_base(po, key::AbstractString, prop::Symbol) if !hasfield(typeof(po), :base_power) error( - "DEVICE_BASE conversion: $key.$prop is a power-family field with no own " * + "COMPONENT_BASE conversion: $key.$prop is a power-family field with no own " * "base_power field and $key is not in _DEVICEBASE_SYSTEM_BASE_TYPES", ) end @@ -304,17 +304,16 @@ end Convert every power-family field [`build_openapi_system`](@ref)'s readers wrote in natural units into per-unit-on-device-base, in place, when `sys`'s document is -`unit_system = "DEVICE_BASE"`. A no-op for `"NATURAL_UNITS"`. See this file's header for the +`unit_system = "COMPONENT_BASE"`. A no-op for `"NATURAL_UNITS"`. See this file's header for the field-classification rule and its exceptions. """ function apply_device_base_conversion!(sys::OpenAPISystem) - doc = get_document(sys) - if !PC.uses_per_unit(doc) + if !uses_per_unit(sys) return sys end - system_base = PC.get_base_power(doc) - for key in PC.component_type_names(doc) - components = PC.get_components(doc, key) + system_base = get_base_power(sys) + for key in component_type_names(sys) + components = get_components(sys, key) isempty(components) && continue T = eltype(components) for prop in fieldnames(T) diff --git a/src/openapi/generation.jl b/src/openapi/generation.jl index 510a677..d66db32 100644 --- a/src/openapi/generation.jl +++ b/src/openapi/generation.jl @@ -172,6 +172,23 @@ function _generator_ext(pm_gen::Dict) return extras end +"""Device base, base conversion factor, and the active/reactive power limits, rating, and +ramp limits every per-unit generator maker derives from them — shared by +`make_thermal_generator!` and `_make_hydro_dispatch_body!`, which compute this set +identically.""" +function _gen_base_and_limits(pm_gen::Dict, gen_name::AbstractString, sys_mbase::Float64) + mbase = _device_base_power(pm_gen, gen_name, sys_mbase) + base_conversion = sys_mbase / mbase + active_power_limits = + (min = pm_gen["pmin"] * base_conversion, max = pm_gen["pmax"] * base_conversion) + reactive_power_limits = + (min = pm_gen["qmin"] * base_conversion, max = pm_gen["qmax"] * base_conversion) + rating = calculate_gen_rating(pm_gen["pmax"], pm_gen["qmax"], base_conversion) + ramp_limits = calculate_ramp_limit(pm_gen, gen_name) + return mbase, base_conversion, active_power_limits, reactive_power_limits, rating, + ramp_limits +end + """Thermal generator; the cost branch lives in `make_thermal_cost` (cost.jl).""" function make_thermal_generator!( sys::OpenAPISystem, @@ -181,16 +198,9 @@ function make_thermal_generator!( gen_name::AbstractString, sys_mbase::Float64, ) - mbase = _device_base_power(pm_gen, gen_name, sys_mbase) - base_conversion = sys_mbase / mbase + mbase, base_conversion, active_power_limits, reactive_power_limits, rating, + ramp_limits = _gen_base_and_limits(pm_gen, gen_name, sys_mbase) _is_likely_motor_load(pm_gen, gen_name) - - active_power_limits = - (min = pm_gen["pmin"] * base_conversion, max = pm_gen["pmax"] * base_conversion) - reactive_power_limits = - (min = pm_gen["qmin"] * base_conversion, max = pm_gen["qmax"] * base_conversion) - rating = calculate_gen_rating(pm_gen["pmax"], pm_gen["qmax"], base_conversion) - ramp_limits = calculate_ramp_limit(pm_gen, gen_name) extras = _generator_ext(pm_gen) component = PO.ThermalStandard() @@ -233,15 +243,8 @@ function _make_hydro_dispatch_body!( gen_name::AbstractString, sys_mbase::Float64, ) - mbase = _device_base_power(pm_gen, gen_name, sys_mbase) - base_conversion = sys_mbase / mbase - - active_power_limits = - (min = pm_gen["pmin"] * base_conversion, max = pm_gen["pmax"] * base_conversion) - reactive_power_limits = - (min = pm_gen["qmin"] * base_conversion, max = pm_gen["qmax"] * base_conversion) - rating = calculate_gen_rating(pm_gen["pmax"], pm_gen["qmax"], base_conversion) - ramp_limits = calculate_ramp_limit(pm_gen, gen_name) + mbase, base_conversion, active_power_limits, reactive_power_limits, rating, + ramp_limits = _gen_base_and_limits(pm_gen, gen_name, sys_mbase) component = PO.HydroDispatch() set_value!(component, :id, register!(reg, "HydroDispatch", gen_name)) @@ -268,7 +271,7 @@ function _make_hydro_dispatch_body!( end """Hydro generator without a reservoir (`fuel: HYDRO, type: ROR`). Ported from PSCB's -`make_hydro_dispatch` (:774-812).""" +`make_hydro_dispatch`.""" make_hydro_dispatch!(sys::OpenAPISystem, reg::IdRegistry, bus_id::Int, pm_gen::Dict, gen_name::AbstractString, sys_mbase::Float64) = _make_hydro_dispatch_body!(sys, reg, bus_id, pm_gen, gen_name, sys_mbase) @@ -277,7 +280,7 @@ make_hydro_dispatch!(sys::OpenAPISystem, reg::IdRegistry, bus_id::Int, pm_gen::D Hydro generator with a reservoir (`fuel: HYDRO, type: HYDRO`/`type: null`), mapped to the `HydroTurbine` generator class by `generator_mapping_pm.yaml`. -# Bug-compatible with PSCB power_models_data.jl:814-852 — PowerModels carries no storage +# Bug-compatible with PSCB — PowerModels carries no storage parameters for a generator ("No way to define storage parameters for gens in PM", PSCB's own comment), so `make_hydro_reservoir` there produces a plain `HydroDispatch`, silently dropping the reservoir, instead of a `HydroTurbine`/`HydroReservoir` pair. Fix tracked @@ -290,7 +293,7 @@ make_hydro_reservoir!(sys::OpenAPISystem, reg::IdRegistry, bus_id::Int, pm_gen:: """ Curtailable renewable generator. -# Bug-compatible with PSCB power_models_data.jl:872,885 — `calculate_gen_rating` already +# Bug-compatible with PSCB — `calculate_gen_rating` already multiplies by `base_conversion`; PSCB's `RenewableDispatch(...)` call then multiplies its own already-converted `rating` local by `base_conversion` a second time, so the stored device-base-pu rating carries `base_conversion^2`. Fix tracked upstream; not fixed here. @@ -314,7 +317,7 @@ function make_renewable_dispatch!( @warn "rating is larger than base power for $gen_name, setting to $mbase" rating = mbase end - # Bug-compatible with PSCB power_models_data.jl:885 — `rating` above is already + # Bug-compatible with PSCB — `rating` above is already # device-base per-unit (calculate_gen_rating applied `base_conversion` once); this # second multiply is the double-application the docstring names. rating = rating * base_conversion @@ -408,7 +411,7 @@ end """ Generic battery storage from a `data["storage"]` entry. -# Bug-compatible with PSCB power_models_data.jl:944,951 — `rating` and `base_power` are +# Bug-compatible with PSCB — `rating` and `base_power` are both assigned the raw `"thermal_rating"` value, itself PowerModels system per-unit and never converted. Every other field in this file uses the component's own base to reach natural units; here that base *is* the same unconverted `thermal_rating` value, so @@ -613,11 +616,10 @@ end Create one generator per `data["gen"]` entry and one storage device per `data["storage"]` entry. -Ported from PSCB's `read_gen!` (:1141-1197) and `read_storage!` (:2038-2060), run -together since both populate injector components and the sub-task brief groups them as -one stage. `data["gen"]` must exist (mirrors `read_loads!`'s stance on `data["load"]`); -`data["storage"]` is genuinely optional — plain Matpower cases never carry one — so its -absence is not an error. +Ported from PSCB's `read_gen!` and `read_storage!`, run together since both populate +injector components. `data["gen"]` must exist (mirrors `read_loads!`'s stance on +`data["load"]`); `data["storage"]` is genuinely optional — plain Matpower cases never +carry one — so its absence is not an error. """ function read_generation!(sys::OpenAPISystem, data::Dict; kwargs...) if !haskey(data, "gen") diff --git a/src/openapi/identity.jl b/src/openapi/identity.jl index ebcf6da..ec98d36 100644 --- a/src/openapi/identity.jl +++ b/src/openapi/identity.jl @@ -14,13 +14,13 @@ drift from the document's own. Not serialized: every lookup index here is recove from the emitted document. """ struct IdRegistry - document::PC.SystemDocument + document::PD.SystemDocument by_name::Dict{Tuple{String, String}, Int} by_bus_number::Dict{Int, Int} arcs::Dict{Tuple{Int, Int}, Int} end -function IdRegistry(document::PC.SystemDocument) +function IdRegistry(document::PD.SystemDocument) return IdRegistry( document, Dict{Tuple{String, String}, Int}(), @@ -32,7 +32,7 @@ end """Allocate an id without associating it with a name. For types the schemas give no `name` field, such as `Arc`.""" function next_id!(reg::IdRegistry) - return PC.next_id!(reg.document) + return PD.next_id!(reg.document) end """Allocate an id for `name` within `type_name`. Throws if that pair is taken.""" diff --git a/src/openapi/load.jl b/src/openapi/load.jl index 2577088..0b05ac1 100644 --- a/src/openapi/load.jl +++ b/src/openapi/load.jl @@ -5,7 +5,7 @@ # documents. Every MW/MVAr field this file writes is `raw_pu * base_power` before # `set_value!` — mirroring topology.jl's `_zone_peak_loads`, not a PSCB peculiarity. # -# `make_interruptible_powerload` (PSCB :534-553) is not ported: `read_loads!`'s own +# `make_interruptible_powerload` (PSCB) is not ported: `read_loads!`'s own # if/elseif/else never calls it, so it is unreachable dead code in the oracle. """ @@ -53,6 +53,25 @@ function _set_zip_fields!(component, d::Dict, base_power::Float64) return end +"""Id, name, availability, bus, and base power — the identity fields every load maker +below sets identically, before its own type-specific fields.""" +function _set_load_identity!( + load, + reg::IdRegistry, + type_name::AbstractString, + name::AbstractString, + bus_id::Int, + status, + base_power::Float64, +) + set_value!(load, :id, register!(reg, type_name, name)) + set_value!(load, :name, name) + set_value!(load, :available, Bool(status)) + set_value!(load, :bus, bus_id) + set_value!(load, :base_power, base_power, "MVA") + return +end + function _make_standard_load!( sys::OpenAPISystem, reg::IdRegistry, @@ -62,11 +81,7 @@ function _make_standard_load!( base_power::Float64, ) load = PO.StandardLoad() - set_value!(load, :id, register!(reg, "StandardLoad", name)) - set_value!(load, :name, name) - set_value!(load, :available, Bool(d["status"])) - set_value!(load, :bus, bus_id) - set_value!(load, :base_power, base_power, "MVA") + _set_load_identity!(load, reg, "StandardLoad", name, bus_id, d["status"], base_power) set_value!(load, :conformity, _conformity_string(Int(d["conformity"]))) _set_zip_fields!(load, d, base_power) add_component!(sys, load) @@ -82,11 +97,8 @@ function _make_interruptible_standardload!( base_power::Float64, ) load = PO.InterruptibleStandardLoad() - set_value!(load, :id, register!(reg, "InterruptibleStandardLoad", name)) - set_value!(load, :name, name) - set_value!(load, :available, Bool(d["status"])) - set_value!(load, :bus, bus_id) - set_value!(load, :base_power, base_power, "MVA") + _set_load_identity!(load, reg, "InterruptibleStandardLoad", name, bus_id, d["status"], + base_power) set_value!(load, :operation_cost, make_load_cost()) set_value!(load, :conformity, _conformity_string(Int(d["conformity"]))) _set_zip_fields!(load, d, base_power) @@ -103,15 +115,11 @@ function _make_power_load!( base_power::Float64, ) load = PO.PowerLoad() - set_value!(load, :id, register!(reg, "PowerLoad", name)) - set_value!(load, :name, name) - set_value!(load, :available, Bool(d["status"])) - set_value!(load, :bus, bus_id) + _set_load_identity!(load, reg, "PowerLoad", name, bus_id, d["status"], base_power) set_value!(load, :active_power, d["pd"] * base_power, "MW") set_value!(load, :reactive_power, d["qd"] * base_power, "MVAr") set_value!(load, :max_active_power, d["pd"] * base_power, "MW") set_value!(load, :max_reactive_power, d["qd"] * base_power, "MVAr") - set_value!(load, :base_power, base_power, "MVA") set_value!(load, :conformity, _conformity_string(Int(d["conformity"]))) add_component!(sys, load) return diff --git a/src/openapi/serialize.jl b/src/openapi/serialize.jl index 1ae8b9e..485386a 100644 --- a/src/openapi/serialize.jl +++ b/src/openapi/serialize.jl @@ -1,7 +1,7 @@ """ Write `sys` to `filename` as the OpenAPI JSON document. Returns `filename`. -`PC.write_document` validates before writing, so a malformed document is reported +`PD.write_document` validates before writing, so a malformed document is reported before anything reaches disk. `force = false` refuses to overwrite an existing `filename`; `pretty = true` indents the output, the default is compact. """ @@ -11,6 +11,6 @@ function to_json( force::Bool = false, pretty::Bool = false, ) - PC.write_document(get_document(sys), filename; pretty = pretty, force = force) + PD.write_document(get_document(sys), filename; pretty = pretty, force = force) return filename end diff --git a/src/openapi/shunt.jl b/src/openapi/shunt.jl index c46e628..04b9f01 100644 --- a/src/openapi/shunt.jl +++ b/src/openapi/shunt.jl @@ -1,7 +1,7 @@ # None of `"shunt"`/`"switched_shunt"`/`"facts"` are native PowerModels # sections, so `_make_per_unit!` never touches them; every field PFFP's own psse.jl # parser writes is used exactly as written (either PSS/E-native per-unit-at-unity-voltage, -# `ShuntAdmittanceUnitBasis.DEVICE_MVAR`, for shunt admittances, or a plain natural value +# `ShuntAdmittanceUnitBasis.COMPONENT_MVAR`, for shunt admittances, or a plain natural value # for everything else) — no `sys_mbase` scaling anywhere in this file. """Fixed admittance (PSS/E `FIXED SHUNT`).""" @@ -18,7 +18,7 @@ function make_fixed_admittance!( set_value!(component, :available, Bool(d["status"])) set_value!(component, :bus, bus_id) set_value!(component, :base_power, get_base_power(sys), "MVA") - set_value!(component, :admittance_units, "DEVICE_MVAR") + set_value!(component, :admittance_units, "COMPONENT_MVAR") set_value!(component, :Y, (real = d["gs"], imag = d["bs"]), "MVAr") add_component!(sys, component) return @@ -88,7 +88,7 @@ function make_switched_admittance!( set_value!(component, :name, name) set_value!(component, :available, Bool(d["status"])) set_value!(component, :bus, bus_id) - set_value!(component, :admittance_units, "DEVICE_MVAR") + set_value!(component, :admittance_units, "COMPONENT_MVAR") set_value!(component, :Y, (real = d["gs"], imag = d["bs"]), "MVAr") set_value!(component, :number_of_steps, d["step_number"]) _set_y_increase!(component, d["y_increment"], "MVAr") @@ -146,7 +146,7 @@ function make_facts!( set_value!(component, :bus, bus_id) set_value!(component, :base_power, get_base_power(sys), "MVA") set_value!(component, :control_mode, control_mode) - set_value!(component, :voltage_setpoint_units, "DEVICE_BASE") + set_value!(component, :voltage_setpoint_units, "COMPONENT_BASE") set_value!(component, :voltage_setpoint, d["voltage_setpoint"], "pu") set_value!(component, :max_shunt_current, d["max_shunt_current"], "MVA") set_value!(component, :reactive_power_required, get(d, "reactive_power_required", 0.0), @@ -165,8 +165,7 @@ end """ Create one `FixedAdmittance` per `data["shunt"]` entry, one `SwitchedAdmittance` per `data["switched_shunt"]` entry, and one `FACTSControlDevice` per `data["facts"]` entry. -Ported from PSCB's `read_shunt!`/`read_switched_shunt!`/`read_facts!` (:1935-2036), run -together as one stage. +Ported from PSCB's `read_shunt!`/`read_switched_shunt!`/`read_facts!`, run together. Deviates from the oracle in one place: PSCB's `read_facts!` reads its name formatter under `:bus_name_formatter`, the same kwarg `read_bus!` uses — a copy-paste artifact, since a diff --git a/src/openapi/switch_breaker.jl b/src/openapi/switch_breaker.jl index 0817d06..e3f1da1 100644 --- a/src/openapi/switch_breaker.jl +++ b/src/openapi/switch_breaker.jl @@ -42,7 +42,7 @@ end """ A switch, breaker, or generic connector as a `DiscreteControlledACBranch`. Ported from -PSCB's `make_switch_breaker` (:1349-1363). `base_power` is set explicitly here because +PSCB's `make_switch_breaker`. `base_power` is set explicitly here because the oracle's constructor leaves it to PSY's `add_component!`, which back-fills the system base for every `BasePowerKind::SystemBasePower` type. """ diff --git a/src/openapi/topology.jl b/src/openapi/topology.jl index 12041af..60eae1b 100644 --- a/src/openapi/topology.jl +++ b/src/openapi/topology.jl @@ -57,19 +57,30 @@ function _unique_bus_names(bus_data) end """ -PSS/E AREA DATA per-area metadata (name, slack bus, desired/tolerance interchange) for -the `Area` named `area_name`, in PSCB's own `ext` shape. Ported from the inline -area_interchange lookup inside PSCB's `read_bus!` (:437-458), including its match rule's -`area_name_formatter`-dependent behavior: a non-default formatter makes this comparison -never match, the same silent gap the oracle has. - -Returns `nothing` for "no match" rather than the oracle's unconditional all-`""` dict, so -the caller only calls `set_ext!` when there is real data to record. +Whether PSS/E AREA DATA carries per-area metadata (name, slack bus, desired/tolerance +interchange) for the `Area` named `area_name`. Matches the inline area_interchange lookup +inside PSCB's `read_bus!`, including its match rule's `area_name_formatter`-dependent +behavior: a non-default formatter makes this comparison never match, the same silent gap +the oracle has. """ -function _area_interchange_ext(data::Dict, area_name::AbstractString) +function _has_area_interchange(data::Dict, area_name::AbstractString) if get(data, "source_type", nothing) != "pti" || !haskey(data, "area_interchange") - return nothing + return false + end + for (_, area_data) in data["area_interchange"] + if haskey(area_data, "area_number") && + string(area_data["area_number"]) == area_name + return true + end end + return false +end + +""" +PSS/E AREA DATA per-area metadata for the `Area` named `area_name`, in PSCB's own `ext` +shape. Call only after [`_has_area_interchange`](@ref) confirms a match exists. +""" +function _area_interchange_ext(data::Dict, area_name::AbstractString) for (_, area_data) in data["area_interchange"] if haskey(area_data, "area_number") && string(area_data["area_number"]) == area_name @@ -82,7 +93,7 @@ function _area_interchange_ext(data::Dict, area_name::AbstractString) ) end end - return nothing + error("_area_interchange_ext: no area_interchange record for area $area_name") end """ @@ -104,9 +115,8 @@ function _ensure_area!(sys::OpenAPISystem, data::Dict, name::AbstractString) # Area has no device base; base_power records the system base. set_value!(area, :base_power, get_base_power(sys), "MVA") add_component!(sys, area) - extras = _area_interchange_ext(data, name) - if !isnothing(extras) - set_ext!(sys, id, extras) + if _has_area_interchange(data, name) + set_ext!(sys, id, _area_interchange_ext(data, name)) end return id end diff --git a/src/pm_io/node_breaker.jl b/src/pm_io/node_breaker.jl index ac8dc6e..730d6e6 100644 --- a/src/pm_io/node_breaker.jl +++ b/src/pm_io/node_breaker.jl @@ -47,7 +47,7 @@ function _representative_node(nodes::Vector, src::Dict) get(n, "va", nothing) == get(src, "va", 0.0)) pool = filter(inherits_bus_voltage, nodes) isempty(pool) && (pool = nodes) - return pool[argmin([n["number"] for n in pool])] + return argmin(n -> n["number"], pool) end # Turns on the isolated-bus bookkeeping that `_psse2pm_bus!` turns on for an IDE=4 BUS @@ -102,7 +102,11 @@ function _prepare_node_breaker!(pm_data::Dict) # service the BUS record's IDE still governs: the retained record is left # exactly as the file declared it rather than being reconciled here. in_service = filter(n -> get(n, "status", 1) == 1, nodes) - rep = _representative_node(isempty(in_service) ? nodes : in_service, src) + if isempty(in_service) + rep = _representative_node(nodes, src) + else + rep = _representative_node(in_service, src) + end for n in nodes if n === rep num = I @@ -210,7 +214,7 @@ identifier. Falls back to the declared bus number when no terminal claims it. function _nb_target(nb, busno::Int, typ::String, other, id) busno in nb.nb_bus_numbers || return busno ni = get(nb.terminal_node, (busno, typ, something(other, 0), _nb_id(id)), nothing) - return ni === nothing ? busno : ni + return something(ni, busno) end # A type-"3" terminal's key carries only one of the winding's two sibling buses diff --git a/src/pm_io/psse.jl b/src/pm_io/psse.jl index aa4ce5a..e64a591 100644 --- a/src/pm_io/psse.jl +++ b/src/pm_io/psse.jl @@ -588,9 +588,9 @@ function _psse2pm_area_slack!(pm_data::Dict) end isw_bus = pm_data["bus"][isw] bus_type = isw_bus["bus_type"] - if !(bus_type in (2, 3)) + if !(bus_type in (PM_BUS_TYPE_PV, PM_BUS_TYPE_REF)) moved_to = get( - get(isw_bus, "ext", Dict{String, Any}()), + get(() -> Dict{String, Any}(), isw_bus, "ext"), "nb_bus_type_moved_to", nothing, ) @@ -599,9 +599,9 @@ function _psse2pm_area_slack!(pm_data::Dict) bus_type = isw_bus["bus_type"] end end - if bus_type == 2 + if bus_type == PM_BUS_TYPE_PV isw_bus["area_slack"] = true - elseif bus_type == 3 + elseif bus_type == PM_BUS_TYPE_REF @debug "Area $area_number interchange-control bus (ISW=$isw) is the system REF bus" else @warn "Area $area_number interchange-control bus (ISW=$isw) has bus_type $bus_type; expected PV (2) or REF (3). Skipping area-slack assignment." @@ -812,7 +812,7 @@ function _psse2pm_load!(pm_data::Dict, pti_data::Dict, import_all::Bool, nb) "bus" => sub_data["load_bus"], "pg" => dgenp, "qg" => dgenq, - "status" => dgenm == 0 ? 0 : 1, + "status" => Int(!iszero(dgenm)), "source_id" => [ "distributed_generation", bus_number, @@ -2066,6 +2066,10 @@ function _psse2pm_dcline!(pm_data::Dict, pti_data::Dict, import_all::Bool) # VSC intended to be one or bi-directional? sub_data["f_bus"] = from_bus["IBUS"] sub_data["t_bus"] = to_bus["IBUS"] + sub_data["base_voltage_from"] = + _get_bus_value(sub_data["f_bus"], "base_kv", pm_data) + sub_data["base_voltage_to"] = + _get_bus_value(sub_data["t_bus"], "base_kv", pm_data) if pm_data["has_isolated_type_buses"] push!(pm_data["connected_buses"], sub_data["f_bus"]) push!(pm_data["connected_buses"], sub_data["t_bus"]) diff --git a/src/pm_io/pti.jl b/src/pm_io/pti.jl index 24a69cd..75ad372 100644 --- a/src/pm_io/pti.jl +++ b/src/pm_io/pti.jl @@ -2110,6 +2110,17 @@ function _parse_substation_section!( return line_index end +"""The v35 / v29-v30 / default variant of a per-version PTI table, selected by `version`.""" +function _pti_version_select(version::Int, v35, v2930, default) + if version == 35 + return v35 + elseif version in (29, 30) + return v2930 + else + return default + end +end + """ Determine the PSS(R)E revision number of a raw file from its CASE IDENTIFICATION header line (the REV field), falling back to 30 when the field is absent or @@ -2122,7 +2133,7 @@ function _resolve_pti_version(data_lines, is_v35) fields, _ = _get_line_elements(data_lines[header]) length(fields) < 3 && return 30 rev = tryparse(Int, strip(fields[3])) - return rev === nothing ? 30 : rev + return something(rev, 30) end """ @@ -2144,18 +2155,15 @@ function _parse_pti_data(data_io::IO) throw(IS.DataFormatError("Unsupported PSS(R)E raw version: $version")) end - active_sections = if version == 35 - deepcopy(_pti_sections_v35) - elseif version in (29, 30) - deepcopy(_pti_sections_v30) - else - deepcopy(_pti_sections) - end + active_sections = + deepcopy( + _pti_version_select(version, _pti_sections_v35, _pti_sections_v30, + _pti_sections), + ) pti_data = Dict{String, Array{Dict}}() section = popfirst!(active_sections) - section_v35 = section section_data = Dict{String, Any}() header_line_start = is_v35 ? 2 : 1 # Start in second line due to @! @@ -2195,13 +2203,8 @@ function _parse_pti_data(data_io::IO) 4 # Start for all v33 files end - current_dtypes = if version == 35 - _pti_dtypes_v35 - elseif version in (29, 30) - _pti_dtypes_v30 - else - _pti_dtypes - end + current_dtypes = + _pti_version_select(version, _pti_dtypes_v35, _pti_dtypes_v30, _pti_dtypes) line_index = 1 while line_index <= length(data_lines) @@ -2218,7 +2221,7 @@ function _parse_pti_data(data_io::IO) first_element = _unquote(elements[1]) if is_v35 && (line_index == 3 || line_index == 4) && - section_v35 == "CASE IDENTIFICATION" + section == "CASE IDENTIFICATION" comment_line = strip(line) comment_key = line_index == 3 ? "Comment_Line_1" : "Comment_Line_2" @@ -2811,21 +2814,15 @@ Internal function. Populates empty fields with PSS(R)E PTI v33 default values """ function _populate_defaults!(data::Dict) rev = get(data["CASE IDENTIFICATION"][1], "REV", 30) - version = rev == "" ? 30 : rev - sections = if version == 35 - _pti_sections_v35 - elseif version in (29, 30) - _pti_sections_v30 - else - _pti_sections - end - defaults = if version == 35 - _pti_defaults_v35 - elseif version in (29, 30) - _pti_defaults_v30 + if rev == "" + version = 30 else - _pti_defaults + version = rev end + sections = + _pti_version_select(version, _pti_sections_v35, _pti_sections_v30, _pti_sections) + defaults = + _pti_version_select(version, _pti_defaults_v35, _pti_defaults_v30, _pti_defaults) for section in sections if haskey(data, section) diff --git a/test/Project.toml b/test/Project.toml index d427a3d..2114d4e 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,4 +1,5 @@ [deps] +PowerOpenAPIModels = "0730f07c-cff6-4c3b-a9df-c546153be50a" Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" InfrastructureSystems = "2cd47ed4-ca9b-11e9-27f2-ab636a7671f1" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" @@ -12,8 +13,9 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [sources] PowerFlowFileParser = {path = ".."} -PowerCoreOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "jd/openapi_regen", subdir = "PowerCoreOpenAPIModels.jl"} -PowerOperationsOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "jd/openapi_regen", subdir = "PowerOperationsOpenAPIModels.jl"} +PowerCoreOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "feat/infrastore-integration", subdir = "PowerCoreOpenAPIModels.jl"} +PowerOperationsOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "feat/infrastore-integration", subdir = "PowerOperationsOpenAPIModels.jl"} +PowerOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "feat/infrastore-integration", subdir = "PowerOpenAPIModels.jl"} [compat] julia = "^1.6" diff --git a/test/fixtures/synthetic_v35_vsc_line.raw b/test/fixtures/synthetic_v35_vsc_line.raw new file mode 100644 index 0000000..09160c7 --- /dev/null +++ b/test/fixtures/synthetic_v35_vsc_line.raw @@ -0,0 +1,39 @@ +@!IC,SBASE,REV,XFRRAT,NXFRAT,BASFRQ +0, 100.00, 35, 0, 1, 60.00 +Synthetic fictional v35 case for VSC line AC-side base voltage capture +Second comment line +0 / END OF SYSTEM-WIDE DATA, BEGIN BUS DATA +@! I,'NAME ', BASKV, IDE,AREA,ZONE,OWNER, VM, VA, NVHI, NVLO, EVHI, EVLO + 1,'BUSONE ', 200.0000,3, 1, 1, 1,1.00000, 0.0000,1.10000,0.90000,1.10000,0.90000 + 2,'BUSTWO ', 200.0000,2, 1, 1, 1,1.00000, 0.0000,1.10000,0.90000,1.10000,0.90000 + 3,'BUSTHREE ', 138.0000,1, 1, 1, 1,1.00000, 0.0000,1.10000,0.90000,1.10000,0.90000 +0 / END OF BUS DATA, BEGIN LOAD DATA + 3,'1 ', 1, 1, 1, 50.000, 10.000, 0.000, 0.000, 0.000, 0.000, 1, 1, 0, 0.000, 0.000, 0,' V' +0 / END OF LOAD DATA, BEGIN FIXED SHUNT DATA +0 / END OF FIXED SHUNT DATA, BEGIN GENERATOR DATA + 1,'1 ', 80.000, 0.000, 80.000, -80.000,1.00000, 0, 0, 100.000, 0.00000E+0, 1.00000E-1, 0.00000E+0, 0.00000E+0,1.00000,1, 100.0, 120.000, 0.000, 0,1,1.0000 + 2,'1 ', 30.000, 0.000, 50.000, -50.000,1.00000, 0, 0, 100.000, 0.00000E+0, 1.00000E-1, 0.00000E+0, 0.00000E+0,1.00000,1, 100.0, 120.000, 0.000, 0,1,1.0000 +0 / END OF GENERATOR DATA, BEGIN BRANCH DATA + 1, 2,'1 ', 1.00000E-02, 1.00000E-01,0.02000,'BRANCH_1_2 ', 500.00, 500.00, 500.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 1.00, 1,1.0000 + 2, 3,'1 ', 1.00000E-02, 1.00000E-01,0.02000,'BRANCH_2_3 ', 500.00, 500.00, 500.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 1.00, 1,1.0000 +0 / END OF BRANCH DATA, BEGIN SYSTEM SWITCHING DEVICE DATA +0 / END OF SYSTEM SWITCHING DEVICE DATA, BEGIN TRANSFORMER DATA +0 / END OF TRANSFORMER DATA, BEGIN AREA DATA +0 / END OF AREA DATA, BEGIN TWO-TERMINAL DC DATA +0 / END OF TWO-TERMINAL DC DATA, BEGIN VSC DC LINE DATA +'VSCLINE1 ', 1, 0.0000, 1, 1.0, 0, 1.0, 0, 1.0, 0, 1.0 + 1, 1, 1, 150.000,1.05000,1000.0000,1.5000, 0.00, 250.00, 1000.00,0.5000, 100.00,-100.00, 0,100.00 + 3, 2, 1, -20.000,1.02500,1000.0000,1.5000, 0.00, 250.00, 1000.00,0.5000, 100.00,-100.00, 0,100.00 +0 / END OF VSC DC LINE DATA, BEGIN IMPEDANCE CORRECTION DATA +0 / END OF IMPEDANCE CORRECTION DATA, BEGIN MULTI-TERMINAL DC DATA +0 / END OF MULTI-TERMINAL DC DATA, BEGIN MULTI-SECTION LINE DATA +0 / END OF MULTI-SECTION LINE DATA, BEGIN ZONE DATA +0 / END OF ZONE DATA, BEGIN INTER-AREA TRANSFER DATA +0 / END OF INTER-AREA TRANSFER DATA, BEGIN OWNER DATA +0 / END OF OWNER DATA, BEGIN FACTS DEVICE DATA +0 / END OF FACTS DEVICE DATA, BEGIN SWITCHED SHUNT DATA +0 / END OF SWITCHED SHUNT DATA, BEGIN GNE DATA +0 / END OF GNE DATA, BEGIN INDUCTION MACHINE DATA +0 / END OF INDUCTION MACHINE DATA, BEGIN SUBSTATION DATA +0 / END OF SUBSTATION DATA +Q diff --git a/test/test_openapi_attributes.jl b/test/test_openapi_attributes.jl index fee0875..61e4ce2 100644 --- a/test/test_openapi_attributes.jl +++ b/test/test_openapi_attributes.jl @@ -31,7 +31,10 @@ shared_id = PFP.get_value(shared, :id) shared_rows = [a for a in associations if PFP.get_value(a, :attribute_id) == shared_id] @test length(shared_rows) == 2 - entity_ids = sort([PFP.get_value(a, :entity_id) for a in shared_rows]) + @test all( + PFP.get_value(a, :component_type) == "ThreeWindingTransformer" for a in shared_rows + ) + component_ids = sort([PFP.get_value(a, :component_id) for a in shared_rows]) primary_3w = PFP.get_value( only( t for t in PFP.get_components(sys, "ThreeWindingTransformer") if @@ -46,7 +49,7 @@ ), :id, ) - @test entity_ids == sort([primary_3w, other_3w]) + @test component_ids == sort([primary_3w, other_3w]) end @testset "ImpedanceCorrectionData: table_number/curve/control_mode hand-derived from a single 2W table" begin @@ -95,8 +98,16 @@ end a for a in doc.supplemental_attribute_associations if PFP.get_value(a, :attribute_id) == PFP.get_value(ict, :id), ) - @test PFP.get_value(row, :entity_id) == PFP.get_value(two_w, :id) + @test PFP.get_value(row, :component_id) == PFP.get_value(two_w, :id) + @test PFP.get_value(row, :component_type) == "TwoWindingTransformer" @test PFP.get_value(row, :attribute_type) == "ImpedanceCorrectionData" + # A plain attribute link emits nothing in the plant-family or service tables. This used + # to assert `group_index`/`role` were unset on the row itself; those columns are gone + # with the un-consolidation, so the row type enforces that much structurally and what is + # left worth checking is that no row landed in the tables that replaced them. + @test isempty(doc.plant_associations) + @test isempty(doc.combined_cycle_associations) + @test isempty(doc.service_associations) end @testset "read_attributes! is a no-op when impedance_correction is absent" begin @@ -132,8 +143,8 @@ end sys = PFP.build_openapi_system(fourteen_bus_pm_data()) path = joinpath(mktempdir(), "fourteen_bus_attributes.json") PFP.to_json(sys, path) - doc = PFP.PC.read_document(path) - icts = PFP.PC.get_supplemental_attributes(doc, "ImpedanceCorrectionData") + doc = PFP.PD.read_document(path) + icts = PFP.PD.get_supplemental_attributes(doc, "ImpedanceCorrectionData") @test length(icts) == 8 - PFP.PC.validate_document(doc) + PFP.PD.validate_document(doc) end diff --git a/test/test_openapi_branch.jl b/test/test_openapi_branch.jl index 9946e98..98f6f2f 100644 --- a/test/test_openapi_branch.jl +++ b/test/test_openapi_branch.jl @@ -78,13 +78,13 @@ end @test PFP.get_value(line, :reactive_power_flow) == 0.0 end -@testset "Line: rating is per-unit-on-own-base_power under DEVICE_BASE" begin +@testset "Line: rating is per-unit-on-own-base_power under COMPONENT_BASE" begin # NATURAL_UNITS stores rating = d["rate_a"] * base_power (own base_power, always the - # system base for a Line -- see `_resolve_base_power`). DEVICE_BASE divides that back + # system base for a Line -- see `_resolve_base_power`). COMPONENT_BASE divides that back # by the same base_power, so the document should carry PowerModels' raw per-unit # `rate_a` verbatim; r/x/b are pu-by-convention and untouched either way. pm = fourteen_bus_pm_data() - sys = PFP.build_openapi_system(pm; unit_system = "DEVICE_BASE") + sys = PFP.build_openapi_system(pm; unit_system = "COMPONENT_BASE") d = only( v for v in values(pm.data["branch"]) if v["f_bus"] == 102 && v["t_bus"] == 104, @@ -108,7 +108,7 @@ end circuit = _transformer_circuit_between(sys, 109, 104) @test PFP.get_value(circuit, :available) - @test PFP.get_value(circuit, :parameter_units) == "DEVICE_BASE" + @test PFP.get_value(circuit, :parameter_units) == "COMPONENT_BASE" @test PFP.get_value(circuit, :r) == d["br_r"] @test PFP.get_value(circuit, :x) == d["br_x"] @test PFP.get_value(circuit, :tap) == d["tap"] @@ -136,23 +136,23 @@ end @test PFP.get_value(circuit, :number_of_tap_positions) == Int(d["NTP1"]) transformer = _two_winding_transformer_for(sys, circuit) - @test PFP.get_value(transformer, :admittance_units) == "DEVICE_BASE" + @test PFP.get_value(transformer, :admittance_units) == "COMPONENT_BASE" @test _matches_nt( PFP.get_value(transformer, :magnetizing_shunt), (real = d["g_fr"], imag = d["b_fr"]), ) end -@testset "TransformerCircuit: rating/flow are per-unit-on-own-base_power under DEVICE_BASE, r/x/magnetizing_shunt untouched" begin +@testset "TransformerCircuit: rating/flow are per-unit-on-own-base_power under COMPONENT_BASE, r/x/magnetizing_shunt untouched" begin # NATURAL_UNITS stores rating = d["rate_a"] * d["base_power"] (the circuit's own base, - # not necessarily sys_mbase). DEVICE_BASE divides that back by the same base_power, so + # not necessarily sys_mbase). COMPONENT_BASE divides that back by the same base_power, so # the document should carry PowerModels' raw per-unit `rate_a` verbatim. r/x/ - # magnetizing_shunt are always DEVICE_BASE pu already (their own `parameter_units`/ + # magnetizing_shunt are always COMPONENT_BASE pu already (their own `parameter_units`/ # `admittance_units` discriminators, independent of the document's unit_system) and # must not move at all between the two documents. pm = fourteen_bus_pm_data() sys_natural = PFP.build_openapi_system(pm) - sys_device = PFP.build_openapi_system(pm; unit_system = "DEVICE_BASE") + sys_device = PFP.build_openapi_system(pm; unit_system = "COMPONENT_BASE") d = only( v for v in values(pm.data["branch"]) if v["f_bus"] == 109 && v["t_bus"] == 104, @@ -211,10 +211,10 @@ end @test PFP.get_value(transformer, :star_bus) == PFP.get_bus_id(PFP.get_registry(sys), d["star_bus"]) - @test PFP.get_value(transformer, :parameter_units) == "DEVICE_BASE" + @test PFP.get_value(transformer, :parameter_units) == "COMPONENT_BASE" @test PFP.get_value(transformer, :r_12) == d["r_12"] @test PFP.get_value(transformer, :base_power_12) == d["base_power_12"] - @test PFP.get_value(transformer, :admittance_units) == "DEVICE_BASE" + @test PFP.get_value(transformer, :admittance_units) == "COMPONENT_BASE" @test _matches_nt( PFP.get_value(transformer, :magnetizing_shunt), (real = d["g"], imag = d["b"]), @@ -271,11 +271,11 @@ end (PFP.get_value(switch, :available) ? "CLOSED" : "OPEN") end -@testset "TransformerCircuit.controlled_quantity_limits passes through UNSCALED under DEVICE_BASE for a power-flow-family control_objective" begin +@testset "TransformerCircuit.controlled_quantity_limits passes through UNSCALED under COMPONENT_BASE for a power-flow-family control_objective" begin # Regression: `controlled_quantity_limits`'s schema quantity DOES switch with # `control_objective` (pu for VOLTAGE-family objectives, MW/MVAr for ACTIVE_POWER_FLOW/ # REACTIVE_POWER_FLOW/CONTROL_OF_DC_LINE-family ones), which made a first cut of the - # DEVICE_BASE registry classify it `:dynamic` (converting the power-flow-family + # COMPONENT_BASE registry classify it `:dynamic` (converting the power-flow-family # branches by the circuit's own base_power). That was wrong: PowerSystems' own # `to_openapi` calls the SAME unscaled `_minmax_po(get_controlled_quantity_limits(...))` # in BOTH `DeviceBaseUnit` and `NaturalUnit` (export_handwritten.jl:166-167, :195-196) — @@ -286,12 +286,12 @@ end # the bug was in. # # base_power = 50, sys_mbase = 100 (base_conversion-sensitive, same discipline as the - # storage/generator DEVICE_BASE tests): active_power_flow/reactive_power_flow DO + # storage/generator COMPONENT_BASE tests): active_power_flow/reactive_power_flow DO # convert (10.0/50.0 = 0.2, 5.0/50.0 = 0.1) so this test also proves the fix did not # collaterally stop scaling this circuit's other power fields. controlled_quantity_limits # must come out exactly (50.0, 150.0) -- if it were wrongly divided by base_power = 50 # it would read (1.0, 3.0) instead, a clearly different and wrong number. - sys = PFP.OpenAPISystem(100.0; unit_system = "DEVICE_BASE") + sys = PFP.OpenAPISystem(100.0; unit_system = "COMPONENT_BASE") reg = PFP.get_registry(sys) from_id = _register_bus!(sys, 1, "b1") to_id = _register_bus!(sys, 2, "b2") diff --git a/test/test_openapi_container.jl b/test/test_openapi_container.jl index 4d73fb2..57b68b7 100644 --- a/test/test_openapi_container.jl +++ b/test/test_openapi_container.jl @@ -69,7 +69,8 @@ end assoc = only(PFP.get_document(sys).supplemental_attribute_associations) @test PFP.get_value(assoc, :attribute_id) == 2 - @test PFP.get_value(assoc, :entity_id) == 1 + @test PFP.get_value(assoc, :component_id) == 1 + @test PFP.get_value(assoc, :component_type) == "ACBus" @test only(PFP.get_supplemental_attributes(sys, "GeographicInfo")) === geo end @@ -80,5 +81,6 @@ end assoc = only(PFP.get_document(sys).service_associations) @test PFP.get_value(assoc, :service_id) == 99 @test PFP.get_value(assoc, :entity_id) == 1 + @test isempty(PFP.get_document(sys).supplemental_attribute_associations) @test_throws IS.DataFormatError PFP.add_service_association!(sys, 99, 1) end diff --git a/test/test_openapi_dc_shunt.jl b/test/test_openapi_dc_shunt.jl index c3c3f2e..73a5de2 100644 --- a/test/test_openapi_dc_shunt.jl +++ b/test/test_openapi_dc_shunt.jl @@ -55,7 +55,7 @@ end @test PFP.get_value(line, :base_power) == base end -@testset "FixedAdmittance: PSS/E-native DEVICE_MVAR Y, no scaling" begin +@testset "FixedAdmittance: PSS/E-native COMPONENT_MVAR Y, no scaling" begin pm = fourteen_bus_pm_data() sys = PFP.build_openapi_system(pm) d = only(v for v in values(pm.data["shunt"]) if v["shunt_bus"] == 111) @@ -64,7 +64,7 @@ end PFP.get_value(s, :bus) == PFP.get_bus_id(PFP.get_registry(sys), 111) ) @test PFP.get_value(shunt, :available) == d["status"] - @test PFP.get_value(shunt, :admittance_units) == "DEVICE_MVAR" + @test PFP.get_value(shunt, :admittance_units) == "COMPONENT_MVAR" @test _matches_nt(PFP.get_value(shunt, :Y), (real = d["gs"], imag = d["bs"])) end @@ -108,7 +108,7 @@ end facts = only(PFP.get_components(sys, "FACTSControlDevice")) @test PFP.get_value(facts, :control_mode) == "NML" @test PFP.get_value(facts, :available) == d["available"] - @test PFP.get_value(facts, :voltage_setpoint_units) == "DEVICE_BASE" + @test PFP.get_value(facts, :voltage_setpoint_units) == "COMPONENT_BASE" @test PFP.get_value(facts, :voltage_setpoint) == d["voltage_setpoint"] @test PFP.get_value(facts, :max_shunt_current) == d["max_shunt_current"] @test PFP.get_value(facts, :reactive_power_required) == 0.0 @@ -213,6 +213,7 @@ function _synthetic_vscline_dict() "power_factor_weighting_fraction_from" => 1.0, "power_factor_weighting_fraction_to" => 1.0, "rated_dc_voltage" => 100.0, + "base_voltage_from" => 138.0, "base_voltage_to" => 138.0, ) end @@ -255,14 +256,28 @@ end @test PFP.get_value(vsc, :g) ≈ 1.0 / 0.5 @test PFP.get_value(vsc, :max_dc_current_from) == 100.0 @test PFP.get_value(vsc, :rated_dc_voltage) == 100.0 + @test PFP.get_value(vsc, :rated_ac_voltage_from) == 138.0 + @test PFP.get_value(vsc, :rated_ac_voltage_to) == 138.0 end -@testset "TwoTerminalVSCLine: make_vscline! stores DC_VOLTAGE/AC_VOLTAGE setpoints as DEVICE_BASE pu" begin +@testset "TwoTerminalVSCLine: build_openapi_system threads each converter's AC base kV onto rated_ac_voltage_from/to" begin + # The AC-side base kV (base_voltage_from/base_voltage_to, see test_parse_psse.jl) is + # captured into the pm dict and threaded onto the document as + # rated_ac_voltage_from/to (see make_vscline!'s docstring). + file = joinpath(@__DIR__, "fixtures", "synthetic_v35_vsc_line.raw") + sys = PFP.build_openapi_system(PFP.PowerModelsData(file)) + vsc = only(PFP.get_components(sys, "TwoTerminalVSCLine")) + @test PFP.get_value(vsc, :rated_dc_voltage) == 150.0 + @test PFP.get_value(vsc, :rated_ac_voltage_from) == 200.0 + @test PFP.get_value(vsc, :rated_ac_voltage_to) == 138.0 +end + +@testset "TwoTerminalVSCLine: make_vscline! stores DC_VOLTAGE/AC_VOLTAGE setpoints as COMPONENT_BASE pu" begin # SiennaSchemas decouples setpoint_voltage_units (dc_setpoint_from/to, # ac_setpoint_from/to) from voltage_units (voltage_limits_from/to only), so tagging a - # voltage-controlling setpoint DEVICE_BASE no longer relabels the untouched + # voltage-controlling setpoint COMPONENT_BASE no longer relabels the untouched # voltage_limits_from/to defaults. make_vscline! sets setpoint_voltage_units = - # "DEVICE_BASE" unconditionally and stores the already-p.u. PSS/E value with unit "pu" — + # "COMPONENT_BASE" unconditionally and stores the already-p.u. PSS/E value with unit "pu" — # an identity conversion, so the stored value equals the input. data = Dict{String, Any}( "baseMVA" => 100.0, "source_type" => "pti", @@ -298,7 +313,7 @@ end ), ) @test PFP.get_value(vsc_dc, :dc_control_from) == "DC_VOLTAGE" - @test PFP.get_value(vsc_dc, :setpoint_voltage_units) == "DEVICE_BASE" + @test PFP.get_value(vsc_dc, :setpoint_voltage_units) == "COMPONENT_BASE" @test PFP.get_value(vsc_dc, :dc_setpoint_from) == 1.03 @test PFP.get_value(vsc_dc, :voltage_units) == "NATURAL_UNITS" @test _matches_nt(PFP.get_value(vsc_dc, :voltage_limits_from), (min = 0.0, max = 999.9)) @@ -314,7 +329,7 @@ end ), ) @test PFP.get_value(vsc_ac, :ac_control_from) == "AC_VOLTAGE" - @test PFP.get_value(vsc_ac, :setpoint_voltage_units) == "DEVICE_BASE" + @test PFP.get_value(vsc_ac, :setpoint_voltage_units) == "COMPONENT_BASE" @test PFP.get_value(vsc_ac, :ac_setpoint_from) == 1.02 @test PFP.get_value(vsc_ac, :voltage_units) == "NATURAL_UNITS" @test _matches_nt(PFP.get_value(vsc_ac, :voltage_limits_to), (min = 0.0, max = 999.9)) @@ -328,12 +343,12 @@ end # base_voltage (rated_dc_voltage) = 500.0 kV => 515.0 / 500.0 = 1.03 p.u. # That division already produces the number PSY expects, so passing it # through set_value! with unit "pu" is an identity conversion: source - # unit "pu" equals the DEVICE_BASE-branch declared unit "pu", and "pu" + # unit "pu" equals the COMPONENT_BASE-branch declared unit "pu", and "pu" # carries no fixed conversion factor (to_default: null in # Core/units.json) -- there is nothing left to scale. vsc = PFP.PO.TwoTerminalVSCLine() PFP.set_value!(vsc, :dc_control_from, "DC_VOLTAGE") - PFP.set_value!(vsc, :setpoint_voltage_units, "DEVICE_BASE") + PFP.set_value!(vsc, :setpoint_voltage_units, "COMPONENT_BASE") PFP.set_value!(vsc, :dc_setpoint_from, 515.0 / 500.0, "pu") @test PFP.get_value(vsc, :dc_setpoint_from) == 1.03 @@ -349,7 +364,7 @@ end # DC_VOLTAGE in TwoTerminalVSCLine.json's dc_setpoint_from annotation; # confirm the emitter's recursive walk produced the same result for it. PFP.set_value!(vsc, :dc_control_from, "DC_VOLTAGE_DROOP") - PFP.set_value!(vsc, :setpoint_voltage_units, "DEVICE_BASE") + PFP.set_value!(vsc, :setpoint_voltage_units, "COMPONENT_BASE") PFP.set_value!(vsc, :dc_setpoint_from, 1.03, "pu") @test PFP.get_value(vsc, :dc_setpoint_from) == 1.03 @@ -365,11 +380,11 @@ end # ACSET for a VSC converter bus is already per-unit of the AC bus's own # base voltage (the same PSS/E convention as bus VM), so no scaling # happens before this value reaches PSY. Passing "pu" here is again an - # identity: source unit "pu" equals the AC_VOLTAGE/DEVICE_BASE-branch + # identity: source unit "pu" equals the AC_VOLTAGE/COMPONENT_BASE-branch # declared unit "pu". vsc = PFP.PO.TwoTerminalVSCLine() PFP.set_value!(vsc, :ac_control_from, "AC_VOLTAGE") - PFP.set_value!(vsc, :setpoint_voltage_units, "DEVICE_BASE") + PFP.set_value!(vsc, :setpoint_voltage_units, "COMPONENT_BASE") PFP.set_value!(vsc, :ac_setpoint_from, 1.02, "pu") @test PFP.get_value(vsc, :ac_setpoint_from) == 1.02 diff --git a/test/test_openapi_generation.jl b/test/test_openapi_generation.jl index 9c68ef3..92f9356 100644 --- a/test/test_openapi_generation.jl +++ b/test/test_openapi_generation.jl @@ -69,18 +69,18 @@ end end end -@testset "ThermalStandard fields are per-unit-on-own-mbase under DEVICE_BASE, mbase != sys_mbase" begin +@testset "ThermalStandard fields are per-unit-on-own-mbase under COMPONENT_BASE, mbase != sys_mbase" begin # sys_mbase=100, mbase=50: every 14-bus generator states mbase == sys_mbase (see this - # file's header), so DEVICE_BASE's own-base division is only distinguishable from the + # file's header), so COMPONENT_BASE's own-base division is only distinguishable from the # system base on a synthetic generator, same reason the "Bug-compatible" tests below # build one directly rather than through build_openapi_system. # # NATURAL_UNITS would store natural_MW = pg * base_conversion * mbase, where # base_conversion = sys_mbase / mbase (see the mbase == sys_mbase testset above). - # DEVICE_BASE divides that by the generator's own mbase: pu = natural_MW / mbase = + # COMPONENT_BASE divides that by the generator's own mbase: pu = natural_MW / mbase = # pg * base_conversion = pg * (sys_mbase / mbase) -- i.e. the raw system-per-unit value # rescaled onto the device's own base, independent of `mbase`'s absolute value. - sys = PFP.OpenAPISystem(100.0; unit_system = "DEVICE_BASE") + sys = PFP.OpenAPISystem(100.0; unit_system = "COMPONENT_BASE") reg = PFP.get_registry(sys) bus = _register_test_bus!(sys) d = Dict{String, Any}( @@ -102,7 +102,7 @@ end # power-family quantity set: with no ramp_agc/ramp_10/ramp_30 in `d`, # calculate_ramp_limit falls back to (up = down = abs(pmax)) *without* base_conversion # (`calculate_ramp_limit`'s own documented inconsistency) -- natural_MW = pmax * mbase, - # so DEVICE_BASE's pu = natural_MW / mbase collapses back to plain `pmax`. + # so COMPONENT_BASE's pu = natural_MW / mbase collapses back to plain `pmax`. @test PFP.get_value(gen, :ramp_limits).up ≈ d["pmax"] @test PFP.get_value(gen, :ramp_limits).down ≈ d["pmax"] end @@ -122,7 +122,7 @@ end @test cost.start_up == 0.0 @test cost.shut_down == 0.0 variable = cost.variable - @test variable.power_units == "DEVICE_BASE" + @test variable.power_units == "COMPONENT_BASE" function_data = variable.value_curve.value.function_data.value @test function_data.quadratic_term == 0.0 @test function_data.proportional_term == 1.0 @@ -317,9 +317,9 @@ end @test PFP.get_value(storage, :storage_technology_type) == "OTHER_CHEM" end -@testset "EnergyReservoirStorage fields are per-unit-on-own-thermal_rating under DEVICE_BASE, device base != system base" begin +@testset "EnergyReservoirStorage fields are per-unit-on-own-thermal_rating under COMPONENT_BASE, device base != system base" begin # Regression: storage_capacity's quantity (ElectricalEnergy) is only resolvable - # through `energy_units`' instance-level discriminator. A DEVICE_BASE pass that + # through `energy_units`' instance-level discriminator. A COMPONENT_BASE pass that # treats every instance-dispatched field as "leave untouched" skips this conversion # silently instead of erroring. `own base_power` here is # `thermal_rating` (PSCB's bug-compatible battery convention, same as the @@ -334,9 +334,9 @@ end # input/output_active_power_limits.max = charge/discharge_rating * thermal_rating # reactive_power = qs * thermal_rating = 0.1 * 50.0 = 5.0 # reactive_power_limits = (qmin, qmax) .* thermal_rating - # DEVICE_BASE divides every one of those back by the SAME thermal_rating = 50.0, + # COMPONENT_BASE divides every one of those back by the SAME thermal_rating = 50.0, # collapsing each to the raw un-multiplied input value. - sys = PFP.OpenAPISystem(100.0; unit_system = "DEVICE_BASE") + sys = PFP.OpenAPISystem(100.0; unit_system = "COMPONENT_BASE") reg = PFP.get_registry(sys) bus = _register_test_bus!(sys) d = Dict{String, Any}( @@ -359,11 +359,11 @@ end @test PFP.get_value(storage, :reactive_power) ≈ d["qs"] @test PFP.get_value(storage, :reactive_power_limits).min ≈ d["qmin"] @test PFP.get_value(storage, :reactive_power_limits).max ≈ d["qmax"] - # Dimensionless / ratio fields: untouched by DEVICE_BASE either way. + # Dimensionless / ratio fields: untouched by COMPONENT_BASE either way. @test PFP.get_value(storage, :initial_storage_capacity_level) == 0.5 end -@testset "DEVICE_BASE conversion errors loudly on an unregistered instance-dispatched field" begin +@testset "COMPONENT_BASE conversion errors loudly on an unregistered instance-dispatched field" begin # Pins the structural guarantee: an # instance-level-discriminated field with no verdict in # `_DEVICEBASE_INSTANCE_DISPATCHED` must error, not silently fall through unconverted. @@ -463,10 +463,10 @@ end @testset "a fresh 14-bus document with loads and generators round-trips through PC" begin sys = PFP.build_openapi_system(fourteen_bus_pm_data()) - PFP.PC.validate_document(PFP.get_document(sys)) + PFP.PD.validate_document(PFP.get_document(sys)) path = joinpath(mktempdir(), "fourteen_bus_gen.json") PFP.to_json(sys, path) - doc = PFP.PC.read_document(path) - @test length(PFP.PC.get_components(doc, "StandardLoad")) == 13 - @test length(PFP.PC.get_components(doc, "ThermalStandard")) == 7 + doc = PFP.PD.read_document(path) + @test length(PFP.PD.get_components(doc, "StandardLoad")) == 13 + @test length(PFP.PD.get_components(doc, "ThermalStandard")) == 7 end diff --git a/test/test_openapi_identity.jl b/test/test_openapi_identity.jl index 04ec388..d826c7c 100644 --- a/test/test_openapi_identity.jl +++ b/test/test_openapi_identity.jl @@ -1,5 +1,5 @@ @testset "IdRegistry assigns one global id space" begin - reg = PFP.IdRegistry(PFP.PC.SystemDocument(100.0)) + reg = PFP.IdRegistry(PFP.PD.SystemDocument(100.0)) @test PFP.register!(reg, "Area", "1") == 1 @test PFP.register_bus!(reg, 101, "Abel") == 2 @test PFP.register!(reg, "ThermalStandard", "101_STEAM_3") == 3 @@ -7,7 +7,7 @@ end @testset "IdRegistry lookups" begin - reg = PFP.IdRegistry(PFP.PC.SystemDocument(100.0)) + reg = PFP.IdRegistry(PFP.PD.SystemDocument(100.0)) PFP.register_bus!(reg, 101, "Abel") @test PFP.has_bus_id(reg, 101) @test PFP.get_bus_id(reg, 101) == 1 @@ -20,24 +20,24 @@ end end @testset "IdRegistry rejects duplicates within a type" begin - reg = PFP.IdRegistry(PFP.PC.SystemDocument(100.0)) + reg = PFP.IdRegistry(PFP.PD.SystemDocument(100.0)) PFP.register!(reg, "Area", "1") @test_throws IS.DataFormatError PFP.register!(reg, "Area", "1") end @testset "IdRegistry rejects duplicate bus numbers" begin - reg = PFP.IdRegistry(PFP.PC.SystemDocument(100.0)) + reg = PFP.IdRegistry(PFP.PD.SystemDocument(100.0)) PFP.register_bus!(reg, 101, "Abel") @test_throws IS.DataFormatError PFP.register_bus!(reg, 101, "Adams") end @testset "IdRegistry allows the same name across types" begin - reg = PFP.IdRegistry(PFP.PC.SystemDocument(100.0)) + reg = PFP.IdRegistry(PFP.PD.SystemDocument(100.0)) @test PFP.register!(reg, "Area", "1") != PFP.register!(reg, "LoadZone", "1") end @testset "arc_id! deduplicates and respects direction" begin - reg = PFP.IdRegistry(PFP.PC.SystemDocument(100.0)) + reg = PFP.IdRegistry(PFP.PD.SystemDocument(100.0)) from = PFP.register_bus!(reg, 101, "Abel") to = PFP.register_bus!(reg, 102, "Adams") id1, created1 = PFP.arc_id!(reg, from, to) @@ -51,7 +51,7 @@ end end @testset "find_by_name narrows by candidate types" begin - reg = PFP.IdRegistry(PFP.PC.SystemDocument(100.0)) + reg = PFP.IdRegistry(PFP.PD.SystemDocument(100.0)) area = PFP.register!(reg, "Area", "1") zone = PFP.register!(reg, "LoadZone", "1") @test PFP.find_by_name(reg, ["LoadZone"], "1") == ("LoadZone", zone) diff --git a/test/test_openapi_load.jl b/test/test_openapi_load.jl index 0ffdabf..1657f22 100644 --- a/test/test_openapi_load.jl +++ b/test/test_openapi_load.jl @@ -35,12 +35,12 @@ end end end -@testset "StandardLoad fields are per-unit-on-device-base under DEVICE_BASE" begin +@testset "StandardLoad fields are per-unit-on-device-base under COMPONENT_BASE" begin # A StandardLoad's own `base_power` is always the system base (read_loads! has no - # per-load mbase concept -- unlike a generator's `mbase`), so DEVICE_BASE's + # per-load mbase concept -- unlike a generator's `mbase`), so COMPONENT_BASE's # natural_MW / base_power collapses to (raw_pu * base_power) / base_power == raw_pu: # the document should carry PowerModels' own system-per-unit numbers back verbatim. - sys = PFP.build_openapi_system(fourteen_bus_pm_data(); unit_system = "DEVICE_BASE") + sys = PFP.build_openapi_system(fourteen_bus_pm_data(); unit_system = "COMPONENT_BASE") data = fourteen_bus_pm_data().data for load in PFP.get_components(sys, "StandardLoad") name = PFP.get_value(load, :name) diff --git a/test/test_openapi_serialize.jl b/test/test_openapi_serialize.jl index 5a7cb2c..82bab2d 100644 --- a/test/test_openapi_serialize.jl +++ b/test/test_openapi_serialize.jl @@ -48,9 +48,9 @@ end @test !PFP.uses_per_unit(sys) end -@testset "unit_system accepts DEVICE_BASE" begin - sys = PFP.OpenAPISystem(100.0; unit_system = "DEVICE_BASE") - @test PFP.get_unit_system(sys) == "DEVICE_BASE" +@testset "unit_system accepts COMPONENT_BASE" begin + sys = PFP.OpenAPISystem(100.0; unit_system = "COMPONENT_BASE") + @test PFP.get_unit_system(sys) == "COMPONENT_BASE" @test PFP.uses_per_unit(sys) end @@ -60,8 +60,8 @@ end @testset "unit_system is carried into the document" begin # The NATURAL_UNITS side is covered by "document top-level shape" above. - device_base = _round_trip(PFP.OpenAPISystem(100.0; unit_system = "DEVICE_BASE")) - @test device_base["unit_system"] == "DEVICE_BASE" + device_base = _round_trip(PFP.OpenAPISystem(100.0; unit_system = "COMPONENT_BASE")) + @test device_base["unit_system"] == "COMPONENT_BASE" end @testset "components are grouped by type name in sorted order" begin @@ -83,7 +83,8 @@ end @testset "supplemental attribute associations serialize as id pairs" begin doc = _round_trip(_serialize_test_system()) assoc = only(doc["supplemental_attribute_associations"]) - @test assoc["entity_id"] == 1 + @test assoc["component_id"] == 1 + @test assoc["component_type"] == "ACBus" @test assoc["attribute_type"] == "GeographicInfo" end @@ -101,7 +102,7 @@ end @testset "to_json refuses to overwrite without force" begin path = joinpath(mktempdir(), "case.json") PFP.to_json(_serialize_test_system(), path) - # PC.write_document owns the "already exists" check for the JSON path now. + # PD.write_document owns the "already exists" check for the JSON path now. @test_throws PFP.PC.DocumentFormatError PFP.to_json(_serialize_test_system(), path) @test PFP.to_json(_serialize_test_system(), path; force = true) == path end @@ -137,12 +138,12 @@ end end end -@testset "a written document reads back through PC.read_document" begin +@testset "a written document reads back through PD.read_document" begin sys = _serialize_test_system() path = joinpath(mktempdir(), "case.json") PFP.to_json(sys, path) - doc = PFP.PC.read_document(path) - @test PFP.PC.get_base_power(doc) == 100.0 - @test length(PFP.PC.get_components(doc, "ACBus")) == 1 - @test length(PFP.PC.get_components(doc, "Area")) == 1 + doc = PFP.PD.read_document(path) + @test PFP.PD.get_base_power(doc) == 100.0 + @test length(PFP.PD.get_components(doc, "ACBus")) == 1 + @test length(PFP.PD.get_components(doc, "Area")) == 1 end diff --git a/test/test_openapi_topology.jl b/test/test_openapi_topology.jl index 9bbb709..f8bedab 100644 --- a/test/test_openapi_topology.jl +++ b/test/test_openapi_topology.jl @@ -175,14 +175,14 @@ end @test ext["PTOL"] == area1_d["tol_interchange"] end -@testset "_area_interchange_ext returns nothing without a source_type/area_interchange/matching area_number" begin +@testset "_has_area_interchange is false without a source_type/area_interchange/matching area_number" begin pm_data = PFP.PowerModelsData(joinpath(@__DIR__, "fixtures", "v35_area_slack_variants.raw")) data = pm_data.data - @test isnothing(PFP._area_interchange_ext(data, "999")) # no area_number == "999" - @test isnothing(PFP._area_interchange_ext(Dict{String, Any}(), "1")) # no source_type + @test !PFP._has_area_interchange(data, "999") # no area_number == "999" + @test !PFP._has_area_interchange(Dict{String, Any}(), "1") # no source_type matpower_data = merge(data, Dict{String, Any}("source_type" => "matpower")) - @test isnothing(PFP._area_interchange_ext(matpower_data, "1")) # oracle-matched: pti only + @test !PFP._has_area_interchange(matpower_data, "1") # oracle-matched: pti only end @testset "_bustype_name rejects a code outside PowerModels' 1-4 range" begin @@ -231,24 +231,24 @@ end sys = PFP.build_openapi_system(fourteen_bus_pm_data()) path = joinpath(mktempdir(), "fourteen_bus.json") PFP.to_json(sys, path) - doc = PFP.PC.read_document(path) - @test length(PFP.PC.get_components(doc, "ACBus")) == 22 - @test length(PFP.PC.get_components(doc, "Area")) == 1 - @test length(PFP.PC.get_components(doc, "LoadZone")) == 1 - @test length(PFP.PC.get_components(doc, "StandardLoad")) == 13 - @test length(PFP.PC.get_components(doc, "ThermalStandard")) == 7 - @test length(PFP.PC.get_components(doc, "Line")) == 20 - @test length(PFP.PC.get_components(doc, "TwoWindingTransformer")) == 3 - @test length(PFP.PC.get_components(doc, "ThreeWindingTransformer")) == 2 - @test length(PFP.PC.get_components(doc, "TwoTerminalLCCLine")) == 1 - @test length(PFP.PC.get_components(doc, "FixedAdmittance")) == 4 - @test length(PFP.PC.get_components(doc, "SwitchedAdmittance")) == 2 - @test length(PFP.PC.get_components(doc, "FACTSControlDevice")) == 1 - @test length(PFP.PC.get_components(doc, "DiscreteControlledACBranch")) == 2 - @test length(PFP.PC.get_supplemental_attributes(doc, "ImpedanceCorrectionData")) == 8 + doc = PFP.PD.read_document(path) + @test length(PFP.PD.get_components(doc, "ACBus")) == 22 + @test length(PFP.PD.get_components(doc, "Area")) == 1 + @test length(PFP.PD.get_components(doc, "LoadZone")) == 1 + @test length(PFP.PD.get_components(doc, "StandardLoad")) == 13 + @test length(PFP.PD.get_components(doc, "ThermalStandard")) == 7 + @test length(PFP.PD.get_components(doc, "Line")) == 20 + @test length(PFP.PD.get_components(doc, "TwoWindingTransformer")) == 3 + @test length(PFP.PD.get_components(doc, "ThreeWindingTransformer")) == 2 + @test length(PFP.PD.get_components(doc, "TwoTerminalLCCLine")) == 1 + @test length(PFP.PD.get_components(doc, "FixedAdmittance")) == 4 + @test length(PFP.PD.get_components(doc, "SwitchedAdmittance")) == 2 + @test length(PFP.PD.get_components(doc, "FACTSControlDevice")) == 1 + @test length(PFP.PD.get_components(doc, "DiscreteControlledACBranch")) == 2 + @test length(PFP.PD.get_supplemental_attributes(doc, "ImpedanceCorrectionData")) == 8 # GeographicInfo stays empty: "substation" is allow-listed # (`KNOWN_UNCONSUMED_PM_SECTIONS`, build.jl), not implemented. - @test isempty(PFP.PC.get_components(doc, "GeographicInfo")) + @test isempty(PFP.PD.get_components(doc, "GeographicInfo")) end @testset "build_openapi_system rejects a pm dict with no buses" begin diff --git a/test/test_openapi_transformer_discriminators.jl b/test/test_openapi_transformer_discriminators.jl index 179da66..7635ab4 100644 --- a/test/test_openapi_transformer_discriminators.jl +++ b/test/test_openapi_transformer_discriminators.jl @@ -183,13 +183,13 @@ end sys = PFP.build_openapi_system(_transformer_discriminator_pm_data()) path = joinpath(mktempdir(), "transformer_discriminators.json") PFP.to_json(sys, path) - doc = PFP.PC.read_document(path) + doc = PFP.PD.read_document(path) # 3, not 2: bus 2 (200 kV) -> bus 3 (138 kV) is a plain `Line` record in the raw file, # but PFFP's own >1%-voltage-mismatch detection (`power_models_data.jl`) reclassifies # it as a third (CZ=1/CW=1/CM=1, unremarkable) transformer — incidental to this # fixture's DC-line buses, borrowed from `synthetic_v35_two_terminal_dc.raw`'s # template, not one of T1/T2. - @test length(PFP.PC.get_components(doc, "TwoWindingTransformer")) == 3 - @test length(PFP.PC.get_components(doc, "TwoTerminalLCCLine")) == 1 - PFP.PC.validate_document(doc) + @test length(PFP.PD.get_components(doc, "TwoWindingTransformer")) == 3 + @test length(PFP.PD.get_components(doc, "TwoTerminalLCCLine")) == 1 + PFP.PD.validate_document(doc) end diff --git a/test/test_openapi_units.jl b/test/test_openapi_units.jl index 32e2dd5..e580904 100644 --- a/test/test_openapi_units.jl +++ b/test/test_openapi_units.jl @@ -170,7 +170,7 @@ end @test line.r == 5.0 other = PFP.PO.TwoTerminalLCCLine() - PFP.set_value!(other, :parameter_units, "DEVICE_BASE") + PFP.set_value!(other, :parameter_units, "COMPONENT_BASE") @test_throws IS.DataFormatError PFP.set_value!(other, :r, 5.0, "ohm") PFP.set_value!(other, :r, 0.01, "pu") @test other.r == 0.01 diff --git a/test/test_parse_psse.jl b/test/test_parse_psse.jl index 34f6842..8c2c52f 100644 --- a/test/test_parse_psse.jl +++ b/test/test_parse_psse.jl @@ -98,6 +98,17 @@ end @test blocked_dcline["r"] ≈ 5.0 / (200.0^2 / 100.0) end +@testset "PSSE VSC line captures each converter's own AC bus base_kv" begin + file = joinpath(@__DIR__, "fixtures", "synthetic_v35_vsc_line.raw") + pm_data = PowerModelsData(file).data + vscline = only(values(pm_data["vscline"])) + @test vscline["f_bus"] == 1 + @test vscline["t_bus"] == 3 + @test vscline["base_voltage_from"] == 200.0 + @test vscline["base_voltage_to"] == 138.0 + @test vscline["rated_dc_voltage"] == 150.0 +end + @testset "PSSE ISW area-slack flag" begin file = joinpath(@__DIR__, "fixtures", "v35_area_slack_variants.raw") pm_data = @test_logs(