Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 18 additions & 11 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -15,23 +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]
# 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"}
# 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"}

[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"
Expand All @@ -40,6 +48,5 @@ PowerDynamicsOpenAPIModels = "0.1"
PowerInvestmentsOpenAPIModels = "0.1"
PowerOpenAPIModels = "0.1"
PowerOperationsOpenAPIModels = "0.1"
PowerTimeSeriesOpenAPIModels = "0.1"
Unicode = "1"
julia = "^1.10"
12 changes: 6 additions & 6 deletions scripts/inspect_14bus_json.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
# Produces three files in the output directory (default `inspection_output/`):
#
# <case>.pm.json the parsed PowerModels dict
# <case>.NATURAL_UNITS.json OpenAPI document, unit_system = NATURAL_UNITS
# <case>.DEVICE_BASE.json OpenAPI document, unit_system = DEVICE_BASE
# <case>.NATURAL_UNITS.json OpenAPI document, power_units = NATURAL_UNITS
# <case>.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
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions src/PowerFlowFileParser.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions src/openapi/attributes.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
Expand All @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions src/openapi/build.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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...)
Expand Down
50 changes: 35 additions & 15 deletions src/openapi/container.jl
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -66,22 +77,31 @@ 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.

`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
Expand Down Expand Up @@ -125,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),
Expand Down
38 changes: 29 additions & 9 deletions src/openapi/cost.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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(
IC.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."""
Expand All @@ -12,13 +27,14 @@ 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,
),
),
),
),
vom_cost = _zero_vom_cost(),
)
end

Expand All @@ -37,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

"""
Expand Down Expand Up @@ -66,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,
Expand All @@ -85,7 +101,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,
Expand All @@ -102,13 +118,14 @@ 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(;
function_data = PC.InputOutputCurveFunctionData(function_data),
),
),
vom_cost = _zero_vom_cost(),
),
fixed = fixed,
start_up = pm_gen["startup"],
Expand All @@ -117,10 +134,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)
6 changes: 3 additions & 3 deletions src/openapi/dc_branch.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
),
Expand Down Expand Up @@ -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"]),
),
Expand Down Expand Up @@ -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"]),
),
Expand Down
Loading
Loading