From f1643604477286a1e7462132e1af8b6575beb699 Mon Sep 17 00:00:00 2001 From: Jose Daniel Lara Date: Mon, 31 Aug 2026 19:28:19 -0700 Subject: [PATCH 1/3] Follow the power_units rename from the 0.1.0 schemas Power-bearing components carry power_units in place of parameter_units, and the OpenAPI read/write paths and their fixtures follow the renamed field. Pins the schema-generated packages at PowerOpenAPIModels jd/schemas-0.1.0 and PowerSystems at jd/power-units-rename, which carry the matching rename. --- Project.toml | 14 +++-- scripts/inspect_14bus_json.jl | 12 ++--- src/openapi/build.jl | 6 +-- src/openapi/container.jl | 48 ++++++++++++----- src/openapi/cost.jl | 13 +++-- src/openapi/device_base.jl | 94 ++++++++++++++++++++++++++------- src/openapi/units.jl | 18 +++++++ test/Project.toml | 13 ++--- test/test_openapi_branch.jl | 8 +-- test/test_openapi_generation.jl | 16 +++--- test/test_openapi_identity.jl | 14 ++--- test/test_openapi_load.jl | 2 +- test/test_openapi_serialize.jl | 34 ++++++------ 13 files changed, 194 insertions(+), 98 deletions(-) diff --git a/Project.toml b/Project.toml index 3d2ad78..86e1119 100644 --- a/Project.toml +++ b/Project.toml @@ -18,15 +18,13 @@ PowerOperationsOpenAPIModels = "a372b6d7-45a2-44c2-8199-6a724b72e8ff" PowerTimeSeriesOpenAPIModels = "8c2f6a0d-6b0e-4d0a-9d8f-2b5e6a4f9c31" 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 = "main", subdir = "PowerCoreOpenAPIModels.jl"} -PowerOperationsOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "main", subdir = "PowerOperationsOpenAPIModels.jl"} -PowerOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "main", subdir = "PowerOpenAPIModels.jl"} -PowerDynamicsOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "main", subdir = "PowerDynamicsOpenAPIModels.jl"} -PowerInvestmentsOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "main", subdir = "PowerInvestmentsOpenAPIModels.jl"} -PowerTimeSeriesOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "main", subdir = "PowerTimeSeriesOpenAPIModels.jl"} InfrastructureSystems = {rev = "IS4", url = "https://github.com/Sienna-Platform/InfrastructureSystems.jl"} +PowerCoreOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "jd/schemas-0.1.0", subdir = "PowerCoreOpenAPIModels.jl"} +PowerOperationsOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "jd/schemas-0.1.0", subdir = "PowerOperationsOpenAPIModels.jl"} +PowerOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "jd/schemas-0.1.0", subdir = "PowerOpenAPIModels.jl"} +PowerDynamicsOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "jd/schemas-0.1.0", subdir = "PowerDynamicsOpenAPIModels.jl"} +PowerInvestmentsOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "jd/schemas-0.1.0", subdir = "PowerInvestmentsOpenAPIModels.jl"} +PowerTimeSeriesOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "jd/schemas-0.1.0", subdir = "PowerTimeSeriesOpenAPIModels.jl"} [compat] DataStructures = "0.19.3" diff --git a/scripts/inspect_14bus_json.jl b/scripts/inspect_14bus_json.jl index 5976ad7..5569792 100644 --- a/scripts/inspect_14bus_json.jl +++ b/scripts/inspect_14bus_json.jl @@ -8,8 +8,8 @@ # Produces three files in the output directory (default `inspection_output/`): # # .pm.json the parsed PowerModels dict -# .NATURAL_UNITS.json OpenAPI document, unit_system = NATURAL_UNITS -# .DEVICE_BASE.json OpenAPI document, unit_system = DEVICE_BASE +# .NATURAL_UNITS.json OpenAPI document, power_units = NATURAL_UNITS +# .COMPONENT_BASE.json OpenAPI document, power_units = COMPONENT_BASE # # Both OpenAPI documents are built via `build_openapi_system` and carry real components — # buses, loads, generators, branches, transformers, dc lines, shunts, plus @@ -22,7 +22,7 @@ import JSON using PowerFlowFileParser const DEFAULT_CASE = joinpath(@__DIR__, "..", "test", "modified_14bus_system.raw") -const UNIT_SYSTEMS = ("NATURAL_UNITS", "DEVICE_BASE") +const UNIT_SYSTEMS = ("NATURAL_UNITS", "COMPONENT_BASE") function parse_args(argv) case = DEFAULT_CASE @@ -117,9 +117,9 @@ function main(argv) # The OpenAPI envelope in both unit systems, built from the same parsed case via the # full emit layer. - for unit_system in UNIT_SYSTEMS - sys = build_openapi_system(pm; unit_system = unit_system) - path = joinpath(opts.out, "$stem.$unit_system.json") + for power_units in UNIT_SYSTEMS + sys = build_openapi_system(pm; power_units = power_units) + path = joinpath(opts.out, "$stem.$power_units.json") to_json(sys, path; force = true, pretty = true) println(" wrote $(relpath(path)) ($(round(filesize(path) / 1024; digits = 1)) KiB)") println( diff --git a/src/openapi/build.jl b/src/openapi/build.jl index cfd0120..398daee 100644 --- a/src/openapi/build.jl +++ b/src/openapi/build.jl @@ -69,7 +69,7 @@ already-registered transformer ids by name. Before returning, errors on every no pm dict section no reader touched and `KNOWN_UNCONSUMED_PM_SECTIONS` does not excuse, so a caller never mistakes a partial document for a complete one. -`unit_system` selects the convention the values are stored in, same as +`power_units` 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; `"COMPONENT_BASE"` additionally runs [`apply_device_base_conversion!`](@ref) over the built document, converting every power-family field into per-unit on the @@ -80,14 +80,14 @@ reader unconsumed. """ function build_openapi_system( pm_data::PowerModelsData; - unit_system::AbstractString = "NATURAL_UNITS", + power_units::AbstractString = "NATURAL_UNITS", kwargs..., ) data = pm_data.data if isempty(data["bus"]) throw(IS.DataFormatError("pm_data has no buses")) end - sys = OpenAPISystem(Float64(data["baseMVA"]); unit_system = unit_system) + sys = OpenAPISystem(Float64(data["baseMVA"]); power_units = power_units) read_loadzones!(sys, data; kwargs...) read_bus!(sys, data; kwargs...) diff --git a/src/openapi/container.jl b/src/openapi/container.jl index 1b9cacc..203129d 100644 --- a/src/openapi/container.jl +++ b/src/openapi/container.jl @@ -1,10 +1,13 @@ """ 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 -only the lookup indices (by name, bus number, arc) the document has no use for once -built. +`document` carries the components, the association tables, and `ext`. `base_power` +is the system MVA base pm dict readers scale against; it is not part of the +serialized document — each component states its own `base_power`. `power_units` is +this run's chosen basis, stamped onto every emitted component whose PO type declares +the field (see [`add_component!`](@ref)); it too is not carried on the document +itself. `registry` is build-time scaffolding, holding only the lookup indices (by +name, bus number, arc) the document has no use for once built. `time_series` mirrors PowerTableDataParser's field shape for a consistent `OpenAPISystem` API across parsers, but stays permanently empty here — PSS/E and @@ -14,10 +17,12 @@ struct OpenAPISystem document::PD.SystemDocument registry::IdRegistry time_series::Vector{IS.TimeSeriesData} + base_power::Float64 + power_units::String end """ -Unit conventions a document may be written in, from the schemas' `UnitSystem`. +Unit conventions a component's `power_units` field may take, from the schemas' enum. 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 @@ -27,17 +32,23 @@ const UNIT_SYSTEMS = ("NATURAL_UNITS", "COMPONENT_BASE") function OpenAPISystem( base_power::Float64; - unit_system::AbstractString = "NATURAL_UNITS", + power_units::AbstractString = "NATURAL_UNITS", ) - if !(unit_system in UNIT_SYSTEMS) + if !(power_units in UNIT_SYSTEMS) throw( IS.DataFormatError( - "unit_system must be one of $(join(UNIT_SYSTEMS, ", ")); got $unit_system", + "power_units must be one of $(join(UNIT_SYSTEMS, ", ")); got $power_units", ), ) end - document = PD.SystemDocument(base_power; unit_system = unit_system) - return OpenAPISystem(document, IdRegistry(document), Vector{IS.TimeSeriesData}()) + document = PD.SystemDocument() + return OpenAPISystem( + document, + IdRegistry(document), + Vector{IS.TimeSeriesData}(), + base_power, + String(power_units), + ) end get_document(sys::OpenAPISystem) = sys.document @@ -66,10 +77,10 @@ end get_ext(sys::OpenAPISystem, component_id::Int) = PD.get_ext(get_document(sys), component_id) -get_base_power(sys::OpenAPISystem) = PD.get_base_power(get_document(sys)) +get_base_power(sys::OpenAPISystem) = sys.base_power get_registry(sys::OpenAPISystem) = sys.registry -get_unit_system(sys::OpenAPISystem) = PD.get_unit_system(get_document(sys)) +get_power_units(sys::OpenAPISystem) = sys.power_units """ Whether values are stored per unit rather than in the schemas' natural units. @@ -77,11 +88,20 @@ Whether values are stored per unit rather than in the schemas' natural units. `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. +why each component states the convention it was written in. """ -uses_per_unit(sys::OpenAPISystem) = PD.uses_per_unit(get_document(sys)) +uses_per_unit(sys::OpenAPISystem) = sys.power_units == "COMPONENT_BASE" +""" +Add `component` to the document, first stamping this run's `power_units` onto it when +its PO type declares the field — the per-component wire-contract requirement every +power-bearing type carries (a component with none, e.g. a pure topology row, is +untouched). +""" function add_component!(sys::OpenAPISystem, component::T) where {T <: OpenAPI.APIModel} + if hasfield(T, :power_units) + setproperty!(component, :power_units, sys.power_units) + end PD.add_component!(get_document(sys), component) return end diff --git a/src/openapi/cost.jl b/src/openapi/cost.jl index 5f8b405..1703444 100644 --- a/src/openapi/cost.jl +++ b/src/openapi/cost.jl @@ -85,7 +85,7 @@ function make_thermal_cost(gen_name::AbstractString, pm_gen::Dict, sys_mbase::Fl if !haskey(pm_gen, "model") @warn "Generator cost data not included for Generator: $gen_name" return PC.ThermalGenerationCost(; - variable = _zero_cost_curve(), + variable_operation_cost = _zero_cost_curve(), fixed = 0.0, start_up = 0.0, shut_down = 0.0, @@ -102,7 +102,7 @@ function make_thermal_cost(gen_name::AbstractString, pm_gen::Dict, sys_mbase::Fl throw(IS.DataFormatError("$gen_name: unsupported generator cost model=$model")) end return PC.ThermalGenerationCost(; - variable = PC.CostCurve(; + variable_operation_cost = PC.CostCurve(; power_units = "COMPONENT_BASE", value_curve = PC.ValueCurve( PC.InputOutputCurve(; @@ -117,10 +117,13 @@ function make_thermal_cost(gen_name::AbstractString, pm_gen::Dict, sys_mbase::Fl end """Curtailment cost for a hydro generator: PSCB never derives one from pm data.""" -make_hydro_cost() = PC.HydroGenerationCost(; variable = _zero_cost_curve(), fixed = 0.0) +make_hydro_cost() = + PC.HydroGenerationCost(; variable_operation_cost = _zero_cost_curve(), fixed = 0.0) """Operating cost for a renewable generator: PSCB never derives one from pm data.""" -make_renewable_cost() = PC.RenewableGenerationCost(; variable = _zero_cost_curve()) +make_renewable_cost() = + PC.RenewableGenerationCost(; variable_operation_cost = _zero_cost_curve()) """Operating cost for an interruptible load: PSCB never derives one from pm data.""" -make_load_cost() = PC.LoadCost(; variable = _zero_cost_curve(), fixed = 0.0) +make_load_cost() = + PC.LoadCost(; variable_operation_cost = _zero_cost_curve(), fixed = 0.0) diff --git a/src/openapi/device_base.jl b/src/openapi/device_base.jl index 93cb960..5940420 100644 --- a/src/openapi/device_base.jl +++ b/src/openapi/device_base.jl @@ -2,14 +2,12 @@ # # 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 = "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` -# importer does (`src/openapi/import_handwritten.jl`/`src/models/generated/*.jl` there), +# regardless of the run's `power_units` — `set_value!` (units.jl) only converts between +# compatible physical units (kW -> MW), never into a per-unit convention. This pass closes +# that gap: when the run 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 system base) — the exact inverse of what PowerSystems' own +# `NaturalUnit` importer does (`src/openapi/import_handwritten.jl`/`src/models/generated/*.jl` there), # so that PowerSystems' `DeviceBaseUnit` importer reading this document's numbers directly # reproduces the same System a `NATURAL_UNITS` document produces through `NaturalUnit`. # @@ -57,9 +55,9 @@ # 1. A **representation switch** between per-unit and natural for the SAME field # (`parameter_units`/`admittance_units`/`voltage_setpoint_units`/`dc_voltage_units` # choosing between e.g. "pu" and "ohm"/"kV"). Every reader in this package writes a -# FIXED value for these discriminators, independent of the document's `unit_system` — +# FIXED value for these discriminators, independent of the run's `power_units` — # confirmed by grepping every `set_value!(_, :*_units, ...)` call in this directory — -# so the field's own representation never depends on the document convention either, +# so the field's own representation never depends on the run's convention either, # and PowerSystems' own converters confirm it is identical between `DeviceBaseUnit`/ # `NaturalUnit` in every case checked. These are `:skip`. # 2. A **natural-unit choice** among sibling units of the SAME quantity @@ -142,7 +140,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 COMPONENT_BASE cases above. + # run's power_units, the mirror image of the COMPONENT_BASE cases above. ("TwoTerminalLCCLine", :r) => :skip, ("TwoTerminalLCCLine", :rectifier_rc) => :skip, ("TwoTerminalLCCLine", :rectifier_xc) => :skip, @@ -174,13 +172,16 @@ const _DEVICEBASE_INSTANCE_DISPATCHED = Dict{Tuple{String, Symbol}, Symbol}( ) """Whether `T.prop`'s declared unit is fixed — resolvable from the Type alone, rather than -depending on a runtime discriminator field only the instance-level method reads.""" +depending on a runtime discriminator field only the instance-level method reads. A missing +Type-level method (every power-family field: its unit now depends on the component's own +`power_units`) raises `MethodError` rather than the schema's own `ErrorException`; both mean +"not fixed" here.""" function _has_fixed_declared_unit(::Type{T}, prop::Symbol) where {T} try PC.declared_unit(T, Val(prop)) return true catch e - e isa ErrorException || rethrow() + (e isa ErrorException || e isa MethodError) || rethrow() return false end end @@ -248,13 +249,61 @@ function _devicebase_dynamic(key::AbstractString, prop::Symbol, po) return verdict end +"""A copy of `o` with `power_units` set to `power_units` and every other field held +exactly as-is — probes a field's instance dispatch without mutating the real component +(see [`_power_units_only_dispatch`](@ref)). `nothing` passes the generated constructor's +own enum validation (unlike an invalid string, which would raise `OpenAPI.ValidationException` +there instead of letting the probe reach the declared-quantity method it targets).""" +function _with_power_units( + o::T, + power_units::Union{Nothing, AbstractString}, +) where {T <: OpenAPI.APIModel} + return T(; + ( + f => (f === :power_units ? power_units : getfield(o, f)) for f in fieldnames(T) + )..., + ) +end + +""" +Whether `prop`'s instance-level dispatch actually branches on `power_units`, rather than +depending only on some other, unaccounted-for discriminator that happens to resolve +today regardless of what `power_units` holds. `representative`'s own fields (other than +`power_units`) are already whatever the real reader that built it set them to; poisoning +`power_units` alone to `nothing` and checking whether resolution fails BECAUSE of it (the +schema's generated error always names the deciding field, e.g. `"...no unit declared for +power_units=..."`) is the only test that distinguishes the two without requiring every one +of this schema's other discriminators to be listed here. Every ordinary power-family field +across the 32 power-bearing types resolves this way (confirmed against the current +schema); a field genuinely gated by another discriminator (`power_mode`, +`parameter_units`, `dc_control_from`, ...) instead must go through the explicit +`_DEVICEBASE_INSTANCE_DISPATCHED` registry and its loud error on a miss, never through +this fallback. +""" +function _power_units_only_dispatch(representative::T, prop::Symbol) where {T} + poisoned = _with_power_units(representative, nothing) + try + PC.declared_quantity(poisoned, Val(prop)) + catch e + e isa ErrorException || rethrow() + return occursin("power_units=", e.msg) + end + return false +end + """ -Classify `key.prop` (PO type `T`) for the COMPONENT_BASE pass: `:convert_own` (divide by the +Classify `key.prop` (`representative`: any one instance of `key` — every component of a +given type carries the same run-wide `power_units`, stamped uniformly by +[`add_component!`](@ref)) 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. """ -function _devicebase_classification(::Type{T}, key::AbstractString, prop::Symbol) where {T} +function _devicebase_classification( + representative::T, + key::AbstractString, + prop::Symbol, +) where {T} # `base_power`/`base_power_12`/`base_power_23`/`base_power_31`: anchors themselves # (`ThreeWindingTransformer`'s three pairwise bases), never scaled by their own value. if startswith(string(prop), "base_power") @@ -263,10 +312,15 @@ function _devicebase_classification(::Type{T}, key::AbstractString, prop::Symbol if !PC.has_declared_unit(T, Val(prop)) || PC.has_unit_base(T, Val(prop)) return :skip end - if !_has_fixed_declared_unit(T, prop) + if _has_fixed_declared_unit(T, prop) + quantity = PC.declared_quantity(T, Val(prop)) + elseif !haskey(_DEVICEBASE_INSTANCE_DISPATCHED, (String(key), prop)) && + _power_units_only_dispatch(representative, prop) + quantity = PC.declared_quantity(representative, Val(prop)) + else return _devicebase_instance_dispatched(key, prop) end - if !(PC.declared_quantity(T, Val(prop)) in _DEVICEBASE_POWER_QUANTITIES) + if !(quantity in _DEVICEBASE_POWER_QUANTITIES) return :skip end if (String(key), prop) in _DEVICEBASE_FIXED_NATURAL @@ -307,8 +361,8 @@ end apply_device_base_conversion!(sys::OpenAPISystem) 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 = "COMPONENT_BASE"`. A no-op for `"NATURAL_UNITS"`. See this file's header for the +units into per-unit-on-device-base, in place, when `sys`'s run is +`power_units = "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) @@ -321,7 +375,7 @@ function apply_device_base_conversion!(sys::OpenAPISystem) isempty(components) && continue T = eltype(components) for prop in fieldnames(T) - classification = _devicebase_classification(T, key, prop) + classification = _devicebase_classification(first(components), key, prop) classification === :skip && continue for po in components resolved = if classification === :dynamic diff --git a/src/openapi/units.jl b/src/openapi/units.jl index c920f56..61157f4 100644 --- a/src/openapi/units.jl +++ b/src/openapi/units.jl @@ -34,6 +34,23 @@ function _compound_type(o::OpenAPI.APIModel, prop::Symbol) return only(concrete) end +""" +A power-bearing component's own `power_units` governs the declared unit of every +power-family field on it (schema instance dispatch), so it must hold a real value +before any such field is assigned — but a freshly constructed component has it unset, +and every reader in this package always computes and assigns natural-unit values +first (`add_component!` restamps `power_units` to the run's actual convention once the +component is complete; see `device_base.jl`). Default it to `"NATURAL_UNITS"` here, +the first time it is needed, rather than requiring every construction site to stamp it. +""" +function _default_power_units!(o::OpenAPI.APIModel) + T = typeof(o) + if hasfield(T, :power_units) && getproperty(o, :power_units) === nothing + setproperty!(o, :power_units, "NATURAL_UNITS") + end + return +end + function _declared(o::OpenAPI.APIModel, prop::Symbol) T = typeof(o) if !PC.has_declared_unit(T, Val(prop)) @@ -43,6 +60,7 @@ function _declared(o::OpenAPI.APIModel, prop::Symbol) ), ) end + _default_power_units!(o) # Instance dispatch: for discriminated properties both the unit and the # quantity depend on a sibling field. return PC.declared_unit(o, Val(prop)), PC.declared_quantity(o, Val(prop)) diff --git a/test/Project.toml b/test/Project.toml index 7aa0341..b82beb6 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -15,12 +15,13 @@ PowerTimeSeriesOpenAPIModels = "8c2f6a0d-6b0e-4d0a-9d8f-2b5e6a4f9c31" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [sources] PowerFlowFileParser = {path = ".."} -PowerCoreOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "main", subdir = "PowerCoreOpenAPIModels.jl"} -PowerOperationsOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "main", subdir = "PowerOperationsOpenAPIModels.jl"} -PowerOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "main", subdir = "PowerOpenAPIModels.jl"} -PowerDynamicsOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "main", subdir = "PowerDynamicsOpenAPIModels.jl"} -PowerInvestmentsOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "main", subdir = "PowerInvestmentsOpenAPIModels.jl"} -PowerTimeSeriesOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "main", subdir = "PowerTimeSeriesOpenAPIModels.jl"} +InfrastructureSystems = {rev = "IS4", url = "https://github.com/Sienna-Platform/InfrastructureSystems.jl"} +PowerCoreOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "jd/schemas-0.1.0", subdir = "PowerCoreOpenAPIModels.jl"} +PowerOperationsOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "jd/schemas-0.1.0", subdir = "PowerOperationsOpenAPIModels.jl"} +PowerOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "jd/schemas-0.1.0", subdir = "PowerOpenAPIModels.jl"} +PowerDynamicsOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "jd/schemas-0.1.0", subdir = "PowerDynamicsOpenAPIModels.jl"} +PowerInvestmentsOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "jd/schemas-0.1.0", subdir = "PowerInvestmentsOpenAPIModels.jl"} +PowerTimeSeriesOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "jd/schemas-0.1.0", subdir = "PowerTimeSeriesOpenAPIModels.jl"} [compat] julia = "^1.6" diff --git a/test/test_openapi_branch.jl b/test/test_openapi_branch.jl index 98f6f2f..8352962 100644 --- a/test/test_openapi_branch.jl +++ b/test/test_openapi_branch.jl @@ -84,7 +84,7 @@ end # 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 = "COMPONENT_BASE") + sys = PFP.build_openapi_system(pm; power_units = "COMPONENT_BASE") d = only( v for v in values(pm.data["branch"]) if v["f_bus"] == 102 && v["t_bus"] == 104, @@ -148,11 +148,11 @@ end # 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 COMPONENT_BASE pu already (their own `parameter_units`/ - # `admittance_units` discriminators, independent of the document's unit_system) and + # `admittance_units` discriminators, independent of the run's power_units) 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 = "COMPONENT_BASE") + sys_device = PFP.build_openapi_system(pm; power_units = "COMPONENT_BASE") d = only( v for v in values(pm.data["branch"]) if v["f_bus"] == 109 && v["t_bus"] == 104, @@ -291,7 +291,7 @@ end # 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 = "COMPONENT_BASE") + sys = PFP.OpenAPISystem(100.0; power_units = "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_generation.jl b/test/test_openapi_generation.jl index 92f9356..331321a 100644 --- a/test/test_openapi_generation.jl +++ b/test/test_openapi_generation.jl @@ -80,7 +80,7 @@ end # 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 = "COMPONENT_BASE") + sys = PFP.OpenAPISystem(100.0; power_units = "COMPONENT_BASE") reg = PFP.get_registry(sys) bus = _register_test_bus!(sys) d = Dict{String, Any}( @@ -121,7 +121,7 @@ end @test cost.fixed == 0.0 @test cost.start_up == 0.0 @test cost.shut_down == 0.0 - variable = cost.variable + variable = cost.variable_operation_cost @test variable.power_units == "COMPONENT_BASE" function_data = variable.value_curve.value.function_data.value @test function_data.quadratic_term == 0.0 @@ -143,7 +143,7 @@ end c1, c0 = d["cost"] expected_proportional = c1 / sys_mbase cost = PFP.get_value(gen, :operation_cost).value - fd = cost.variable.value_curve.value.function_data.value + fd = cost.variable_operation_cost.value_curve.value.function_data.value @test fd.proportional_term ≈ expected_proportional @test fd.constant_term == 0.0 @test cost.fixed == 0.0 @@ -169,7 +169,7 @@ end cost = PFP.get_value(gen, :operation_cost).value @test cost.fixed ≈ fixed - fd = cost.variable.value_curve.value.function_data.value + fd = cost.variable_operation_cost.value_curve.value.function_data.value @test fd.function_type == "PIECEWISE_LINEAR" @test length(fd.points) == length(points) for (p, (x, y)) in zip(fd.points, points) @@ -192,8 +192,8 @@ end @test cost.fixed == 0.0 @test cost.start_up == 0.0 @test cost.shut_down == 0.0 - @test cost.variable.power_units == "NATURAL_UNITS" - fd = cost.variable.value_curve.value.function_data.value + @test cost.variable_operation_cost.power_units == "NATURAL_UNITS" + fd = cost.variable_operation_cost.value_curve.value.function_data.value @test fd.function_type == "LINEAR" @test fd.proportional_term == 0.0 @test fd.constant_term == 0.0 @@ -336,7 +336,7 @@ end # reactive_power_limits = (qmin, qmax) .* thermal_rating # 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 = "COMPONENT_BASE") + sys = PFP.OpenAPISystem(100.0; power_units = "COMPONENT_BASE") reg = PFP.get_registry(sys) bus = _register_test_bus!(sys) d = Dict{String, Any}( @@ -373,7 +373,7 @@ end # voltage-control gap (`_vsc_voltage_control_unsupported`), so it is exactly the kind # of "not yet classified" field the guard exists for. @test_throws ErrorException PFP._devicebase_classification( - PFP.PO.TwoTerminalVSCLine, + PFP.PO.TwoTerminalVSCLine(), "TwoTerminalVSCLine", :dc_setpoint_from, ) diff --git a/test/test_openapi_identity.jl b/test/test_openapi_identity.jl index d826c7c..b7f2fd3 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.PD.SystemDocument(100.0)) + reg = PFP.IdRegistry(PFP.PD.SystemDocument()) @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.PD.SystemDocument(100.0)) + reg = PFP.IdRegistry(PFP.PD.SystemDocument()) 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.PD.SystemDocument(100.0)) + reg = PFP.IdRegistry(PFP.PD.SystemDocument()) 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.PD.SystemDocument(100.0)) + reg = PFP.IdRegistry(PFP.PD.SystemDocument()) 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.PD.SystemDocument(100.0)) + reg = PFP.IdRegistry(PFP.PD.SystemDocument()) @test PFP.register!(reg, "Area", "1") != PFP.register!(reg, "LoadZone", "1") end @testset "arc_id! deduplicates and respects direction" begin - reg = PFP.IdRegistry(PFP.PD.SystemDocument(100.0)) + reg = PFP.IdRegistry(PFP.PD.SystemDocument()) 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.PD.SystemDocument(100.0)) + reg = PFP.IdRegistry(PFP.PD.SystemDocument()) 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 1657f22..b88919c 100644 --- a/test/test_openapi_load.jl +++ b/test/test_openapi_load.jl @@ -40,7 +40,7 @@ end # 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 = "COMPONENT_BASE") + sys = PFP.build_openapi_system(fourteen_bus_pm_data(); power_units = "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 82bab2d..3a20fcf 100644 --- a/test/test_openapi_serialize.jl +++ b/test/test_openapi_serialize.jl @@ -1,5 +1,5 @@ -function _serialize_test_system() - sys = PFP.OpenAPISystem(100.0) +function _serialize_test_system(; power_units::AbstractString = "NATURAL_UNITS") + sys = PFP.OpenAPISystem(100.0; power_units = power_units) reg = PFP.get_registry(sys) bus = PFP.PO.ACBus() @@ -34,34 +34,35 @@ end @testset "document top-level shape" begin doc = _round_trip(_serialize_test_system()) - @test doc["base_power"] == 100.0 - @test doc["unit_system"] == "NATURAL_UNITS" + @test doc["components"]["Area"][1]["base_power"] == 100.0 @test isnothing(get(doc, "time_series_storage_file", nothing)) @test isempty(doc["time_series_associations"]) @test haskey(doc, "supplemental_attributes") @test haskey(doc, "supplemental_attribute_associations") end -@testset "unit_system defaults to NATURAL_UNITS" begin +@testset "power_units defaults to NATURAL_UNITS" begin sys = PFP.OpenAPISystem(100.0) - @test PFP.get_unit_system(sys) == "NATURAL_UNITS" + @test PFP.get_power_units(sys) == "NATURAL_UNITS" @test !PFP.uses_per_unit(sys) end -@testset "unit_system accepts COMPONENT_BASE" begin - sys = PFP.OpenAPISystem(100.0; unit_system = "COMPONENT_BASE") - @test PFP.get_unit_system(sys) == "COMPONENT_BASE" +@testset "power_units accepts COMPONENT_BASE" begin + sys = PFP.OpenAPISystem(100.0; power_units = "COMPONENT_BASE") + @test PFP.get_power_units(sys) == "COMPONENT_BASE" @test PFP.uses_per_unit(sys) end -@testset "unit_system rejects invalid values" begin - @test_throws IS.DataFormatError PFP.OpenAPISystem(100.0; unit_system = "PER_UNIT") +@testset "power_units rejects invalid values" begin + @test_throws IS.DataFormatError PFP.OpenAPISystem(100.0; power_units = "PER_UNIT") end -@testset "unit_system is carried into the document" begin +@testset "power_units is stamped onto every component that declares the field" begin # The NATURAL_UNITS side is covered by "document top-level shape" above. - device_base = _round_trip(PFP.OpenAPISystem(100.0; unit_system = "COMPONENT_BASE")) - @test device_base["unit_system"] == "COMPONENT_BASE" + component_base = _round_trip(_serialize_test_system(; power_units = "COMPONENT_BASE")) + @test component_base["components"]["Area"][1]["power_units"] == "COMPONENT_BASE" + # ACBus declares no power_units field, so it carries none either way. + @test !haskey(component_base["components"]["ACBus"][1], "power_units") end @testset "components are grouped by type name in sorted order" begin @@ -93,7 +94,7 @@ end path = joinpath(mktempdir(), "case.json") @test PFP.to_json(sys, path) == path parsed = JSON.parsefile(path) - @test parsed["base_power"] == 100.0 + @test parsed["components"]["Area"][1]["base_power"] == 100.0 @test parsed["components"]["ACBus"][1]["name"] == "Abel" @test parsed["components"]["ACBus"][1]["base_voltage"] == 138.0 @test !haskey(parsed["components"]["ACBus"][1], "angle") @@ -143,7 +144,8 @@ end path = joinpath(mktempdir(), "case.json") PFP.to_json(sys, path) doc = PFP.PD.read_document(path) - @test PFP.PD.get_base_power(doc) == 100.0 + area = only(PFP.PD.get_components(doc, "Area")) + @test PFP.get_value(area, :base_power) == 100.0 @test length(PFP.PD.get_components(doc, "ACBus")) == 1 @test length(PFP.PD.get_components(doc, "Area")) == 1 end From 6d163baf13d3227dce34022e8ed4d3a56bc5aa54 Mon Sep 17 00:00:00 2001 From: Jose Daniel Lara Date: Mon, 31 Aug 2026 19:37:16 -0700 Subject: [PATCH 2/3] Emit the required vom_cost on every cost curve The CostCurve schema marks vom_cost required and declares a zero linear input-output curve as its default, but the 0.1.0 regeneration no longer carries object defaults into the generated models, so a writer that omits the field emits a document that fails validation. --- src/openapi/cost.jl | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/openapi/cost.jl b/src/openapi/cost.jl index 1703444..f429077 100644 --- a/src/openapi/cost.jl +++ b/src/openapi/cost.jl @@ -3,6 +3,21 @@ # table-driven cost.jl there is no fuel-price/heat-rate separation here: a MATPOWER-shaped # generator cost becomes a `CostCurve`, never a `FuelCurve`. +"""The schema's declared `vom_cost` default for `CostCurve`/`FuelCurve`: a zero +linear input-output curve (Core/common.json defs.CostCurve.properties.vom_cost.default). +`vom_cost` is schema-`required`, so every emitted curve must carry it explicitly — +the generated `CostCurve`/`FuelCurve` constructors default it to `nothing`.""" +function _zero_vom_cost() + return PC.InputOutputCurve(; + function_data = PC.InputOutputCurveFunctionData( + PC.LinearFunctionData(; + proportional_term = 0.0, + constant_term = 0.0, + ), + ), + ) +end + """A `CostCurve` with a zero linear value curve, matching PSCB's `zero(CostCurve)` (`NaturalUnit`, not `DeviceBaseUnit`) — the fallback for every generator/load type that has no cost data to read from a PowerModels dict.""" @@ -19,6 +34,7 @@ function _zero_cost_curve() ), ), ), + vom_cost = _zero_vom_cost(), ) end @@ -109,6 +125,7 @@ function make_thermal_cost(gen_name::AbstractString, pm_gen::Dict, sys_mbase::Fl function_data = PC.InputOutputCurveFunctionData(function_data), ), ), + vom_cost = _zero_vom_cost(), ), fixed = fixed, start_up = pm_gen["startup"], From b27661e00b1aa626fe4d9380addba28e8abe5d0e Mon Sep 17 00:00:00 2001 From: Jose Daniel Lara Date: Mon, 31 Aug 2026 22:34:26 -0700 Subject: [PATCH 3/3] Follow the OpenAPI package split Domain-neutral value shapes and the document registry moved to InfrastructureCoreOpenAPIModels; qualified references follow (IC alias), the time-series package rename lands in the pins, and the pins track the split branches until they merge. Full suite: 2419/2419; emitted documents pass the cross-language compatibility check both directions. --- Project.toml | 17 +++++++++++++---- src/PowerFlowFileParser.jl | 2 ++ src/openapi/attributes.jl | 10 +++++----- src/openapi/container.jl | 2 +- src/openapi/cost.jl | 10 +++++----- src/openapi/dc_branch.jl | 6 +++--- src/openapi/device_base.jl | 18 +++++++++--------- src/openapi/generation.jl | 4 ++-- src/openapi/shunt.jl | 2 +- src/openapi/units.jl | 32 ++++++++++++++++---------------- test/Project.toml | 10 +++++++--- test/test_openapi_container.jl | 2 +- test/test_openapi_serialize.jl | 4 ++-- 13 files changed, 67 insertions(+), 52 deletions(-) diff --git a/Project.toml b/Project.toml index 86e1119..a09c010 100644 --- a/Project.toml +++ b/Project.toml @@ -6,7 +6,9 @@ authors = ["Sienna Team"] [deps] DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" +InfrastructureCoreOpenAPIModels = "1f5e1c8d-e0cc-4dbf-8c6d-f2a3d2ae70a8" InfrastructureSystems = "2cd47ed4-ca9b-11e9-27f2-ab636a7671f1" +InfrastructureTimeSeriesOpenAPIModels = "37a216c8-a490-47cf-89d7-db2cb9618199" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" OpenAPI = "d5e62ea6-ddf3-4d43-8e4c-ad5e6c8bfd7d" @@ -15,21 +17,29 @@ PowerDynamicsOpenAPIModels = "044a0b22-31f8-4ef6-8282-c9a61b3013f6" PowerInvestmentsOpenAPIModels = "33cb4396-f4d9-4f59-8585-787ebb56cb1b" PowerOpenAPIModels = "0730f07c-cff6-4c3b-a9df-c546153be50a" PowerOperationsOpenAPIModels = "a372b6d7-45a2-44c2-8199-6a724b72e8ff" -PowerTimeSeriesOpenAPIModels = "8c2f6a0d-6b0e-4d0a-9d8f-2b5e6a4f9c31" Unicode = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" [sources] -InfrastructureSystems = {rev = "IS4", url = "https://github.com/Sienna-Platform/InfrastructureSystems.jl"} +# LOCAL PATH CO-DEV PINS (temporary): PowerOpenAPIModels split InfrastructureCore/ +# InfrastructureTimeSeries out on local disk only; committed git pins below still point at +# `jd/schemas-0.1.0`, which predates the split. Revert to git-url pins (with subdirs matching +# the new package names) once the split is pushed. InfrastructureSystems must also point at +# the local IS4 checkout (not the remote branch) because that checkout already carries this +# same split (uncommitted) and the remote IS4 branch does not yet. +InfrastructureSystems = {url = "https://github.com/Sienna-Platform/InfrastructureSystems.jl.git", rev = "jd/openapi-package-split"} +InfrastructureCoreOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "jd/schemas-0.1.0", subdir = "InfrastructureCoreOpenAPIModels.jl"} +InfrastructureTimeSeriesOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "jd/schemas-0.1.0", subdir = "InfrastructureTimeSeriesOpenAPIModels.jl"} PowerCoreOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "jd/schemas-0.1.0", subdir = "PowerCoreOpenAPIModels.jl"} PowerOperationsOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "jd/schemas-0.1.0", subdir = "PowerOperationsOpenAPIModels.jl"} PowerOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "jd/schemas-0.1.0", subdir = "PowerOpenAPIModels.jl"} PowerDynamicsOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "jd/schemas-0.1.0", subdir = "PowerDynamicsOpenAPIModels.jl"} PowerInvestmentsOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "jd/schemas-0.1.0", subdir = "PowerInvestmentsOpenAPIModels.jl"} -PowerTimeSeriesOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "jd/schemas-0.1.0", subdir = "PowerTimeSeriesOpenAPIModels.jl"} [compat] DataStructures = "0.19.3" DocStringExtensions = "~0.8, ~0.9" +InfrastructureCoreOpenAPIModels = "0.1" InfrastructureSystems = "^3.2" +InfrastructureTimeSeriesOpenAPIModels = "0.1" JSON = "1" LinearAlgebra = "1" OpenAPI = "0.2" @@ -38,6 +48,5 @@ PowerDynamicsOpenAPIModels = "0.1" PowerInvestmentsOpenAPIModels = "0.1" PowerOpenAPIModels = "0.1" PowerOperationsOpenAPIModels = "0.1" -PowerTimeSeriesOpenAPIModels = "0.1" Unicode = "1" julia = "^1.10" diff --git a/src/PowerFlowFileParser.jl b/src/PowerFlowFileParser.jl index 373e07f..893214c 100644 --- a/src/PowerFlowFileParser.jl +++ b/src/PowerFlowFileParser.jl @@ -19,9 +19,11 @@ import DataStructures: SortedDict import Unicode: normalize import JSON import OpenAPI +import InfrastructureCoreOpenAPIModels import PowerCoreOpenAPIModels import PowerOpenAPIModels import PowerOperationsOpenAPIModels +const IC = InfrastructureCoreOpenAPIModels const PC = PowerCoreOpenAPIModels const PD = PowerOpenAPIModels const PO = PowerOperationsOpenAPIModels diff --git a/src/openapi/attributes.jl b/src/openapi/attributes.jl index f2647a3..09f34a9 100644 --- a/src/openapi/attributes.jl +++ b/src/openapi/attributes.jl @@ -21,7 +21,7 @@ pre-expansion — [`_attach_impedance_correction!`](@ref) does that lazily, only (table, winding) pairs a transformer actually references. """ function _impedance_correction_curves(data::Dict) - curves = Dict{Int, Tuple{PC.PiecewiseLinearData, String}}() + curves = Dict{Int, Tuple{IC.PiecewiseLinearData, String}}() for (_, d) in _sorted_pm_entries(get(data, "impedance_correction", Dict{String, Any}())) table_number = Int(d["table_number"]) @@ -39,8 +39,8 @@ function _impedance_correction_curves(data::Dict) @warn "Skipping impedance correction table $table_number: insufficient data points ($(length(x)) < 2)." continue end - curve = PC.PiecewiseLinearData(; - points = [PC.XYCoords(; x = x[i], y = y[i]) for i in eachindex(x)], + curve = IC.PiecewiseLinearData(; + points = [IC.XYCoords(; x = x[i], y = y[i]) for i in eachindex(x)], ) control_mode = if PSSE_PARSER_TAP_RATIO_LBOUND <= x[1] <= PSSE_PARSER_TAP_RATIO_UBOUND @@ -61,7 +61,7 @@ Returns the attribute, so later sightings of the same pair associate against it """ function _new_impedance_correction_attribute!( sys::OpenAPISystem, - curves::Dict{Int, Tuple{PC.PiecewiseLinearData, String}}, + curves::Dict{Int, Tuple{IC.PiecewiseLinearData, String}}, table_number::Int, winding::AbstractString, transformer_id::Int, @@ -90,7 +90,7 @@ shared attribute and multiple associations — see the file header. function _attach_impedance_correction!( sys::OpenAPISystem, cache::Dict{Tuple{Int, String}, PO.ImpedanceCorrectionData}, - curves::Dict{Int, Tuple{PC.PiecewiseLinearData, String}}, + curves::Dict{Int, Tuple{IC.PiecewiseLinearData, String}}, d::Dict, table_key::AbstractString, winding::AbstractString, diff --git a/src/openapi/container.jl b/src/openapi/container.jl index 203129d..6da348a 100644 --- a/src/openapi/container.jl +++ b/src/openapi/container.jl @@ -145,7 +145,7 @@ function add_supplemental_attribute_association!( ) push!( get_document(sys).supplemental_attribute_associations, - PC.SupplementalAttributeAssociation(; + IC.SupplementalAttributeAssociation(; component_id = component_id, component_type = String(component_type), attribute_id = get_value(attribute, :id), diff --git a/src/openapi/cost.jl b/src/openapi/cost.jl index f429077..fb53870 100644 --- a/src/openapi/cost.jl +++ b/src/openapi/cost.jl @@ -10,7 +10,7 @@ the generated `CostCurve`/`FuelCurve` constructors default it to `nothing`.""" function _zero_vom_cost() return PC.InputOutputCurve(; function_data = PC.InputOutputCurveFunctionData( - PC.LinearFunctionData(; + IC.LinearFunctionData(; proportional_term = 0.0, constant_term = 0.0, ), @@ -27,7 +27,7 @@ function _zero_cost_curve() value_curve = PC.ValueCurve( PC.InputOutputCurve(; function_data = PC.InputOutputCurveFunctionData( - PC.LinearFunctionData(; + IC.LinearFunctionData(; proportional_term = 0.0, constant_term = 0.0, ), @@ -53,8 +53,8 @@ function _piecewise_linear_cost(cost_component::Vector{Float64}) (first_x, first_y), (second_x, second_y) = points[1], points[2] first_slope = (second_y - first_y) / (second_x - first_x) fixed = max(0.0, first_y - first_slope * first_x) - shifted = [PC.XYCoords(; x = x, y = y - fixed) for (x, y) in points] - return PC.PiecewiseLinearData(; points = shifted), fixed + shifted = [IC.XYCoords(; x = x, y = y - fixed) for (x, y) in points] + return IC.PiecewiseLinearData(; points = shifted), fixed end """ @@ -82,7 +82,7 @@ function _polynomial_cost(gen_name::AbstractString, cost_component::Vector{Float end quadratic_term, proportional_term, constant_term = (get(coeffs, deg, 0.0) for deg in quadratic_degrees) - return PC.QuadraticFunctionData(; + return IC.QuadraticFunctionData(; quadratic_term = quadratic_term, proportional_term = proportional_term, constant_term = constant_term, diff --git a/src/openapi/dc_branch.jl b/src/openapi/dc_branch.jl index 5aa74c4..f4d2720 100644 --- a/src/openapi/dc_branch.jl +++ b/src/openapi/dc_branch.jl @@ -16,7 +16,7 @@ function _two_terminal_loss(d::Dict) return PC.TwoTerminalLoss( PC.InputOutputCurve(; function_data = PC.InputOutputCurveFunctionData( - PC.LinearFunctionData(; + IC.LinearFunctionData(; proportional_term = d["loss1"], constant_term = d["loss0"], ), @@ -229,7 +229,7 @@ function make_vscline!( :converter_loss_from, PC.InputOutputCurve(; function_data = PC.InputOutputCurveFunctionData( - PC.LinearFunctionData(; + IC.LinearFunctionData(; proportional_term = IS.get_proportional_term(d["converter_loss_from"]), constant_term = IS.get_constant_term(d["converter_loss_from"]), ), @@ -265,7 +265,7 @@ function make_vscline!( :converter_loss_to, PC.InputOutputCurve(; function_data = PC.InputOutputCurveFunctionData( - PC.LinearFunctionData(; + IC.LinearFunctionData(; proportional_term = IS.get_proportional_term(d["converter_loss_to"]), constant_term = IS.get_constant_term(d["converter_loss_to"]), ), diff --git a/src/openapi/device_base.jl b/src/openapi/device_base.jl index 5940420..4df1c96 100644 --- a/src/openapi/device_base.jl +++ b/src/openapi/device_base.jl @@ -14,11 +14,11 @@ # ── Field classification, mechanical path ─────────────────────────────────────── # # A field converts when, mechanically: -# - it declares a unit (`PC.has_declared_unit`); -# - that unit is not relative to a sibling field (`!PC.has_unit_base` — e.g. +# - it declares a unit (`IC.has_declared_unit`); +# - that unit is not relative to a sibling field (`!IC.has_unit_base` — e.g. # `ACBus.magnitude` is already pu on `base_voltage` regardless of document unit # system, so it is untouched here); -# - its Type-level (not instance-level) `PC.declared_unit`/`PC.declared_quantity` +# - its Type-level (not instance-level) `IC.declared_unit`/`IC.declared_quantity` # resolve without error (`_has_fixed_declared_unit`) — see the next section for what # happens when they do not; # - that unit's quantity is power-family (`ActivePower`/`ReactivePower`/`ApparentPower`/ @@ -178,7 +178,7 @@ Type-level method (every power-family field: its unit now depends on the compone "not fixed" here.""" function _has_fixed_declared_unit(::Type{T}, prop::Symbol) where {T} try - PC.declared_unit(T, Val(prop)) + IC.declared_unit(T, Val(prop)) return true catch e (e isa ErrorException || e isa MethodError) || rethrow() @@ -238,7 +238,7 @@ function _devicebase_dynamic(key::AbstractString, prop::Symbol, po) "_DEVICEBASE_DYNAMIC_QUANTITIES", ) end - quantity = PC.declared_quantity(po, Val(prop)) + quantity = IC.declared_quantity(po, Val(prop)) verdict = get(quantities, quantity, nothing) if verdict === nothing error( @@ -283,7 +283,7 @@ this fallback. function _power_units_only_dispatch(representative::T, prop::Symbol) where {T} poisoned = _with_power_units(representative, nothing) try - PC.declared_quantity(poisoned, Val(prop)) + IC.declared_quantity(poisoned, Val(prop)) catch e e isa ErrorException || rethrow() return occursin("power_units=", e.msg) @@ -309,14 +309,14 @@ function _devicebase_classification( if startswith(string(prop), "base_power") return :skip end - if !PC.has_declared_unit(T, Val(prop)) || PC.has_unit_base(T, Val(prop)) + if !IC.has_declared_unit(T, Val(prop)) || IC.has_unit_base(T, Val(prop)) return :skip end if _has_fixed_declared_unit(T, prop) - quantity = PC.declared_quantity(T, Val(prop)) + quantity = IC.declared_quantity(T, Val(prop)) elseif !haskey(_DEVICEBASE_INSTANCE_DISPATCHED, (String(key), prop)) && _power_units_only_dispatch(representative, prop) - quantity = PC.declared_quantity(representative, Val(prop)) + quantity = IC.declared_quantity(representative, Val(prop)) else return _devicebase_instance_dispatched(key, prop) end diff --git a/src/openapi/generation.jl b/src/openapi/generation.jl index d66db32..d78decd 100644 --- a/src/openapi/generation.jl +++ b/src/openapi/generation.jl @@ -443,7 +443,7 @@ function make_storage!( set_value!( component, :storage_level_limits, - PC.MinMax(; min = 0.0, max = _natural_value(energy_rating, thermal_rating)), + IC.MinMax(; min = 0.0, max = _natural_value(energy_rating, thermal_rating)), ) set_value!(component, :initial_storage_capacity_level, d["energy"] / energy_rating, "1") set_value!(component, :rating, _natural_value(thermal_rating, thermal_rating), "MVA") @@ -463,7 +463,7 @@ function make_storage!( set_value!( component, :efficiency, - PC.InOut(; in = d["charge_efficiency"], out = d["discharge_efficiency"]), + IC.InOut(; in = d["charge_efficiency"], out = d["discharge_efficiency"]), ) set_value!(component, :reactive_power, _natural_value(d["qs"], thermal_rating), "MVAr") set_value!( diff --git a/src/openapi/shunt.jl b/src/openapi/shunt.jl index b622ae9..a419b5d 100644 --- a/src/openapi/shunt.jl +++ b/src/openapi/shunt.jl @@ -70,7 +70,7 @@ function _set_y_increase!( ) target, quantity = _declared(component, :Y_increase) converted = [ - PC.ComplexNumber(; + IC.ComplexNumber(; real = _convert(component, :Y_increase, real(v), source_unit, target, quantity), imag = _convert(component, :Y_increase, imag(v), source_unit, target, quantity), ) for v in values diff --git a/src/openapi/units.jl b/src/openapi/units.jl index 61157f4..a8ad8c2 100644 --- a/src/openapi/units.jl +++ b/src/openapi/units.jl @@ -53,7 +53,7 @@ end function _declared(o::OpenAPI.APIModel, prop::Symbol) T = typeof(o) - if !PC.has_declared_unit(T, Val(prop)) + if !IC.has_declared_unit(T, Val(prop)) throw( IS.DataFormatError( "$(nameof(T)).$prop declares no unit; use the 3-argument set_value!", @@ -63,13 +63,13 @@ function _declared(o::OpenAPI.APIModel, prop::Symbol) _default_power_units!(o) # Instance dispatch: for discriminated properties both the unit and the # quantity depend on a sibling field. - return PC.declared_unit(o, Val(prop)), PC.declared_quantity(o, Val(prop)) + return IC.declared_unit(o, Val(prop)), IC.declared_quantity(o, Val(prop)) end function _reject_declared(o::OpenAPI.APIModel, prop::Symbol) T = typeof(o) - if PC.has_declared_unit(T, Val(prop)) - unit = PC.declared_unit(o, Val(prop)) + if IC.has_declared_unit(T, Val(prop)) + unit = IC.declared_unit(o, Val(prop)) throw( IS.DataFormatError( "$(nameof(T)).$prop declares unit \"$unit\"; use the 4-argument set_value!", @@ -91,7 +91,7 @@ function _convert_by_factor( return value end T = typeof(o) - if !PC.has_conversion_factor(quantity, source_unit) + if !IC.has_conversion_factor(quantity, source_unit) throw( IS.DataFormatError( "$(nameof(T)).$prop is $quantity in \"$target\"; " * @@ -99,7 +99,7 @@ function _convert_by_factor( ), ) end - if !PC.has_conversion_factor(quantity, target) + if !IC.has_conversion_factor(quantity, target) throw( IS.DataFormatError( "$(nameof(T)).$prop: the unit vocabulary records no conversion factor " * @@ -107,8 +107,8 @@ function _convert_by_factor( ), ) end - return value * PC.conversion_factor(quantity, source_unit) / - PC.conversion_factor(quantity, target) + return value * IC.conversion_factor(quantity, source_unit) / + IC.conversion_factor(quantity, target) end """Whether the sibling property holding a per-unit value's base has been assigned.""" @@ -154,7 +154,7 @@ function _convert_onto_base( quantity::AbstractString, ) T = typeof(o) - base_prop = PC.unit_base(T, Val(prop)) + base_prop = IC.unit_base(T, Val(prop)) if !_base_is_set(o, base_prop) throw( IS.DataFormatError( @@ -179,7 +179,7 @@ function _convert( if source_unit == target return value end - if PC.has_unit_base(typeof(o), Val(prop)) + if IC.has_unit_base(typeof(o), Val(prop)) return _convert_onto_base(o, prop, value, source_unit, target, quantity) end return _convert_by_factor(o, prop, value, source_unit, target, quantity) @@ -283,8 +283,8 @@ function get_value(o::OpenAPI.APIModel, prop::Symbol, unit::AbstractString) return value end T = typeof(o) - if PC.has_unit_base(T, Val(prop)) - base_prop = PC.unit_base(T, Val(prop)) + if IC.has_unit_base(T, Val(prop)) + base_prop = IC.unit_base(T, Val(prop)) if !_base_is_set(o, base_prop) throw( IS.DataFormatError( @@ -303,14 +303,14 @@ function get_value(o::OpenAPI.APIModel, prop::Symbol, unit::AbstractString) base_quantity, ) end - if !PC.has_conversion_factor(quantity, unit) || - !PC.has_conversion_factor(quantity, source) + if !IC.has_conversion_factor(quantity, unit) || + !IC.has_conversion_factor(quantity, source) throw( IS.DataFormatError( "$(nameof(T)).$prop is $quantity in \"$source\"; cannot express in \"$unit\"", ), ) end - return value * PC.conversion_factor(quantity, source) / - PC.conversion_factor(quantity, unit) + return value * IC.conversion_factor(quantity, source) / + IC.conversion_factor(quantity, unit) end diff --git a/test/Project.toml b/test/Project.toml index b82beb6..f696205 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,6 +1,8 @@ [deps] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +InfrastructureCoreOpenAPIModels = "1f5e1c8d-e0cc-4dbf-8c6d-f2a3d2ae70a8" InfrastructureSystems = "2cd47ed4-ca9b-11e9-27f2-ab636a7671f1" +InfrastructureTimeSeriesOpenAPIModels = "37a216c8-a490-47cf-89d7-db2cb9618199" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" LazyArtifacts = "4af54fe1-eca0-43a8-85a7-787d91b784e3" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" @@ -11,17 +13,19 @@ PowerFlowFileParser = "bed98974-b02e-5e2f-9ee0-a103f5c450dd" PowerInvestmentsOpenAPIModels = "33cb4396-f4d9-4f59-8585-787ebb56cb1b" PowerOpenAPIModels = "0730f07c-cff6-4c3b-a9df-c546153be50a" PowerOperationsOpenAPIModels = "a372b6d7-45a2-44c2-8199-6a724b72e8ff" -PowerTimeSeriesOpenAPIModels = "8c2f6a0d-6b0e-4d0a-9d8f-2b5e6a4f9c31" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [sources] PowerFlowFileParser = {path = ".."} -InfrastructureSystems = {rev = "IS4", url = "https://github.com/Sienna-Platform/InfrastructureSystems.jl"} +# LOCAL PATH CO-DEV PINS (temporary): see ../Project.toml for the split rationale; revert to +# git-url pins once the split is pushed. +InfrastructureSystems = {url = "https://github.com/Sienna-Platform/InfrastructureSystems.jl.git", rev = "jd/openapi-package-split"} +InfrastructureCoreOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "jd/schemas-0.1.0", subdir = "InfrastructureCoreOpenAPIModels.jl"} +InfrastructureTimeSeriesOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "jd/schemas-0.1.0", subdir = "InfrastructureTimeSeriesOpenAPIModels.jl"} PowerCoreOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "jd/schemas-0.1.0", subdir = "PowerCoreOpenAPIModels.jl"} PowerOperationsOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "jd/schemas-0.1.0", subdir = "PowerOperationsOpenAPIModels.jl"} PowerOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "jd/schemas-0.1.0", subdir = "PowerOpenAPIModels.jl"} PowerDynamicsOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "jd/schemas-0.1.0", subdir = "PowerDynamicsOpenAPIModels.jl"} PowerInvestmentsOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "jd/schemas-0.1.0", subdir = "PowerInvestmentsOpenAPIModels.jl"} -PowerTimeSeriesOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "jd/schemas-0.1.0", subdir = "PowerTimeSeriesOpenAPIModels.jl"} [compat] julia = "^1.6" diff --git a/test/test_openapi_container.jl b/test/test_openapi_container.jl index 57b68b7..b000c3f 100644 --- a/test/test_openapi_container.jl +++ b/test/test_openapi_container.jl @@ -62,7 +62,7 @@ end @testset "add_supplemental_attribute! records the attribute and its link" begin sys = PFP.OpenAPISystem(100.0) PFP.add_component!(sys, _bus(1, "Abel")) - geo = PFP.PC.GeographicInfo() + geo = PFP.IC.GeographicInfo() PFP.set_value!(geo, :id, 2) PFP.set_value!(geo, :geo_json, Dict{String, Any}("type" => "Point")) PFP.add_supplemental_attribute!(sys, geo, 1) diff --git a/test/test_openapi_serialize.jl b/test/test_openapi_serialize.jl index 3a20fcf..0a50880 100644 --- a/test/test_openapi_serialize.jl +++ b/test/test_openapi_serialize.jl @@ -17,7 +17,7 @@ function _serialize_test_system(; power_units::AbstractString = "NATURAL_UNITS") PFP.set_value!(area, :base_power, PFP.get_base_power(sys), "MVA") PFP.add_component!(sys, area) - geo = PFP.PC.GeographicInfo() + geo = PFP.IC.GeographicInfo() PFP.set_value!(geo, :id, PFP.next_id!(reg)) PFP.set_value!(geo, :geo_json, Dict{String, Any}("type" => "Point")) PFP.add_supplemental_attribute!(sys, geo, PFP.get_value(bus, :id)) @@ -104,7 +104,7 @@ end path = joinpath(mktempdir(), "case.json") PFP.to_json(_serialize_test_system(), path) # 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_throws PFP.IC.DocumentFormatError PFP.to_json(_serialize_test_system(), path) @test PFP.to_json(_serialize_test_system(), path; force = true) == path end