Skip to content
Closed
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
10 changes: 10 additions & 0 deletions src/InfrastructureSystems.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ export PiecewisePointCurve, PiecewiseIncrementalCurve, PiecewiseAverageCurve
export TimeSeriesLinearCurve, TimeSeriesQuadraticCurve, TimeSeriesPiecewisePointCurve
export TimeSeriesPiecewiseIncrementalCurve, TimeSeriesPiecewiseAverageCurve

# Units interface: declared here, methods implemented by domain packages
# (e.g., PowerSystems.jl provides power-domain `get_value`/`set_value` methods).
"Get a field value with optional unit conversion. Methods are provided by domain packages."
function get_value end
"Set a field value with optional unit conversion. Methods are provided by domain packages."
function set_value end
export get_value, set_value

import Base: @kwdef
import CSV
import DataFrames
Expand Down Expand Up @@ -134,6 +142,7 @@ end
get_internal(value::InfrastructureSystemsComponent) = value.internal

include("common.jl")
include("relative_units.jl")
include("random_seed.jl")
include("utils/timers.jl")
include("utils/assert_op.jl")
Expand Down Expand Up @@ -208,4 +217,5 @@ include("function_data/make_convex.jl")
include("deprecated.jl")
include("Optimization/Optimization.jl")
include("Simulation/Simulation.jl")

end # module
21 changes: 13 additions & 8 deletions src/cost_aliases.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
# methods being defined for all the `ValueCurve{FunctionData}` types, not just the ones we
# have here nicely packaged and presented to the user.

# Default `is_cost_alias` is defined in value_curve.jl so it's available to
# time_series_value_curve.jl show methods (included before this file).
"Whether there is a cost alias for the instance or type under consideration"
is_cost_alias(::Union{ValueCurve, Type{<:ValueCurve}}) = false

"""
LinearCurve(proportional_term::Float64)
Expand All @@ -29,6 +29,7 @@ curve = LinearCurve(50.0, 100.0)
const LinearCurve = InputOutputCurve{LinearFunctionData}

is_cost_alias(::Union{LinearCurve, Type{LinearCurve}}) = true
simple_type_name(::LinearCurve) = "LinearCurve"

InputOutputCurve{LinearFunctionData}(proportional_term::Real) =
InputOutputCurve(LinearFunctionData(proportional_term))
Expand All @@ -44,7 +45,7 @@ get_constant_term(vc::LinearCurve) = get_constant_term(get_function_data(vc))

Base.show(io::IO, vc::LinearCurve) =
if isnothing(get_input_at_zero(vc))
print(io, "$(typeof(vc))($(get_proportional_term(vc)), $(get_constant_term(vc)))")
print(io, "LinearCurve($(get_proportional_term(vc)), $(get_constant_term(vc)))")
else
Base.show_default(io, vc)
end
Expand All @@ -67,6 +68,7 @@ curve = QuadraticCurve(0.002, 25.0, 150.0)
const QuadraticCurve = InputOutputCurve{QuadraticFunctionData}

is_cost_alias(::Union{QuadraticCurve, Type{QuadraticCurve}}) = true
simple_type_name(::QuadraticCurve) = "QuadraticCurve"

InputOutputCurve{QuadraticFunctionData}(quadratic_term, proportional_term, constant_term) =
InputOutputCurve(
Expand All @@ -86,7 +88,7 @@ Base.show(io::IO, vc::QuadraticCurve) =
if isnothing(get_input_at_zero(vc))
print(
io,
"$(typeof(vc))($(get_quadratic_term(vc)), $(get_proportional_term(vc)), $(get_constant_term(vc)))",
"QuadraticCurve($(get_quadratic_term(vc)), $(get_proportional_term(vc)), $(get_constant_term(vc)))",
)
else
Base.show_default(io, vc)
Expand All @@ -112,6 +114,7 @@ curve = PiecewisePointCurve([(100.0, 400.0), (200.0, 900.0), (300.0, 1500.0)])
const PiecewisePointCurve = InputOutputCurve{PiecewiseLinearData}

is_cost_alias(::Union{PiecewisePointCurve, Type{PiecewisePointCurve}}) = true
simple_type_name(::PiecewisePointCurve) = "PiecewisePointCurve"

InputOutputCurve{PiecewiseLinearData}(points::Vector) =
InputOutputCurve(PiecewiseLinearData(points))
Expand All @@ -131,7 +134,7 @@ get_slopes(vc::PiecewisePointCurve) = get_slopes(get_function_data(vc))
# Here we manually circumvent the @NamedTuple{x::Float64, y::Float64} type annotation, but we keep things looking like named tuples
Base.show(io::IO, vc::PiecewisePointCurve) =
if isnothing(get_input_at_zero(vc))
print(io, "$(typeof(vc))([$(join(get_points(vc), ", "))])")
print(io, "PiecewisePointCurve([$(join(get_points(vc), ", "))])")
else
Base.show_default(io, vc)
end
Expand Down Expand Up @@ -159,6 +162,7 @@ curve = PiecewiseIncrementalCurve(500.0, [100.0, 150.0, 200.0], [30.0, 35.0])
const PiecewiseIncrementalCurve = IncrementalCurve{PiecewiseStepData}

is_cost_alias(::Union{PiecewiseIncrementalCurve, Type{PiecewiseIncrementalCurve}}) = true
simple_type_name(::PiecewiseIncrementalCurve) = "PiecewiseIncrementalCurve"

IncrementalCurve{PiecewiseStepData}(initial_input, x_coords::Vector, slopes::Vector) =
IncrementalCurve(PiecewiseStepData(x_coords, slopes), initial_input)
Expand All @@ -181,9 +185,9 @@ Base.show(io::IO, vc::PiecewiseIncrementalCurve) =
print(
io,
if isnothing(get_input_at_zero(vc))
"$(typeof(vc))($(get_initial_input(vc)), $(get_x_coords(vc)), $(get_slopes(vc)))"
"PiecewiseIncrementalCurve($(get_initial_input(vc)), $(get_x_coords(vc)), $(get_slopes(vc)))"
else
"$(typeof(vc))($(get_input_at_zero(vc)), $(get_initial_input(vc)), $(get_x_coords(vc)), $(get_slopes(vc)))"
"PiecewiseIncrementalCurve($(get_input_at_zero(vc)), $(get_initial_input(vc)), $(get_x_coords(vc)), $(get_slopes(vc)))"
end,
)

Expand All @@ -202,6 +206,7 @@ input). If your data gives incremental/marginal rates instead, use
const PiecewiseAverageCurve = AverageRateCurve{PiecewiseStepData}

is_cost_alias(::Union{PiecewiseAverageCurve, Type{PiecewiseAverageCurve}}) = true
simple_type_name(::PiecewiseAverageCurve) = "PiecewiseAverageCurve"

AverageRateCurve{PiecewiseStepData}(initial_input, x_coords::Vector, y_coords::Vector) =
AverageRateCurve(PiecewiseStepData(x_coords, y_coords), initial_input)
Expand All @@ -216,7 +221,7 @@ Base.show(io::IO, vc::PiecewiseAverageCurve) =
if isnothing(get_input_at_zero(vc))
print(
io,
"$(typeof(vc))($(get_initial_input(vc)), $(get_x_coords(vc)), $(get_average_rates(vc)))",
"PiecewiseAverageCurve($(get_initial_input(vc)), $(get_x_coords(vc)), $(get_average_rates(vc)))",
)
else
Base.show_default(io, vc)
Expand Down
126 changes: 126 additions & 0 deletions src/relative_units.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
###############################
# Relative (per-unit) markers and RelativeQuantity wrapper.
#
# These types are domain-agnostic — they express "device base" / "system base"
# / "natural unit" without assuming any particular physical domain. Downstream
# packages (e.g. PowerSystems) attach domain-specific meaning via categories
# and conversions.
###############################

"""
Supertype of per-unit (relative) unit markers.
"""
abstract type AbstractRelativeUnit end

"""
Device base per-unit. Values are normalized to the component's own base.
"""
struct DeviceBaseUnit <: AbstractRelativeUnit end

"""
System base per-unit. Values are normalized to the system's base.
"""
struct SystemBaseUnit <: AbstractRelativeUnit end

"""
Natural units. When used as a target, returns the value with the
domain-appropriate unit attached (e.g. MW for power, Ω for impedance).
Deliberately *not* `<: AbstractRelativeUnit` — "convert to NU" yields a
`Unitful.Quantity`, not a `RelativeQuantity`.
"""
struct NaturalUnit end

const DU = DeviceBaseUnit()
const SU = SystemBaseUnit()
const NU = NaturalUnit()

"""
RelativeQuantity{T<:Number, U<:AbstractRelativeUnit} <: Number

A quantity tagged with a per-unit marker.

# Examples
```julia
0.6 * DU # 0.6 per-unit on device base
0.3 * SU # 0.3 per-unit on system base
```
"""
struct RelativeQuantity{T <: Number, U <: AbstractRelativeUnit} <: Number
value::T
unit::U
end

# Construction via multiplication
Base.:*(a::Number, b::AbstractRelativeUnit) = RelativeQuantity(a, b)
Base.:*(b::AbstractRelativeUnit, a::Number) = RelativeQuantity(a, b)

# Arithmetic — same unit type only
Base.:+(a::RelativeQuantity{T, U}, b::RelativeQuantity{S, U}) where {T, S, U} =
RelativeQuantity(a.value + b.value, a.unit)
Base.:-(a::RelativeQuantity{T, U}, b::RelativeQuantity{S, U}) where {T, S, U} =
RelativeQuantity(a.value - b.value, a.unit)
Base.:-(a::RelativeQuantity{T, U}) where {T, U} = RelativeQuantity(-a.value, a.unit)

# Scalar mul/div (Real to avoid ambiguity with unit-bearing types)
Base.:*(a::Real, b::RelativeQuantity{T, U}) where {T, U} =
RelativeQuantity(a * b.value, b.unit)
Base.:*(a::RelativeQuantity{T, U}, b::Real) where {T, U} =
RelativeQuantity(a.value * b, a.unit)
Base.:/(a::RelativeQuantity{T, U}, b::Real) where {T, U} =
RelativeQuantity(a.value / b, a.unit)

# Comparisons
Base.:(==)(a::RelativeQuantity{T, U}, b::RelativeQuantity{S, U}) where {T, S, U} =
a.value == b.value
Base.:(<)(a::RelativeQuantity{T, U}, b::RelativeQuantity{S, U}) where {T, S, U} =
a.value < b.value
Base.:(<=)(a::RelativeQuantity{T, U}, b::RelativeQuantity{S, U}) where {T, S, U} =
a.value <= b.value
Base.isless(a::RelativeQuantity{T, U}, b::RelativeQuantity{S, U}) where {T, S, U} =
isless(a.value, b.value)
Base.isapprox(
a::RelativeQuantity{T, U},
b::RelativeQuantity{S, U};
kwargs...,
) where {T, S, U} = isapprox(a.value, b.value; kwargs...)

"""
ustrip(q::RelativeQuantity)

Extract the numeric value from a `RelativeQuantity`.
"""
ustrip(q::RelativeQuantity) = q.value

# Type conversions
Base.convert(::Type{RelativeQuantity{T, U}}, q::RelativeQuantity{S, U}) where {T, S, U} =
RelativeQuantity(convert(T, q.value), q.unit)
Base.promote_rule(
::Type{RelativeQuantity{T, U}},
::Type{RelativeQuantity{S, U}},
) where {T, S, U} = RelativeQuantity{promote_type(T, S), U}

# Display
Base.show(io::IO, q::RelativeQuantity{T, DeviceBaseUnit}) where {T} =
print(io, q.value, " DU")
Base.show(io::IO, q::RelativeQuantity{T, SystemBaseUnit}) where {T} =
print(io, q.value, " SU")
Base.show(io::IO, ::DeviceBaseUnit) = print(io, "DU")
Base.show(io::IO, ::SystemBaseUnit) = print(io, "SU")
Base.show(io::IO, ::NaturalUnit) = print(io, "NU")

Base.zero(::Type{RelativeQuantity{T, U}}) where {T, U} = RelativeQuantity(zero(T), U())
Base.one(::Type{RelativeQuantity{T, U}}) where {T, U} = RelativeQuantity(one(T), U())

"""
display_units_arg(f, ::Type{T}) -> Union{AbstractRelativeUnit, Missing}

Trait returning the units argument a getter `f` expects when called on a
component of type `T` for display/tabular output, or `missing` if the getter
takes no units argument. Keyed on both function and type because the same
getter name can appear on both unit-bearing and non-unit-bearing structs
(e.g. `get_b` on `Line` vs. `DynamicExponentialLoad`). Downstream packages
set this per-struct (typically via the struct-generator template); consumers
like `show_components` dispatch on the result to avoid runtime method
introspection.
"""
display_units_arg(_, ::Type) = missing
5 changes: 4 additions & 1 deletion src/time_series_interface.jl
Original file line number Diff line number Diff line change
Expand Up @@ -950,7 +950,10 @@ function _make_time_array(owner, time_series, start_time, len, ignore_scaling_fa
return ta
end

return ta .* multiplier(owner)
# Scaling-factor multipliers (e.g. `get_max_active_power`) are unit-aware
# accessors from downstream packages; pass `SU` so the result is in the
# system base that consumers of the time series expect.
return ta .* multiplier(owner, SU)
end

"""
Expand Down
23 changes: 23 additions & 0 deletions src/utils/generate_struct_files.jl
Original file line number Diff line number Diff line change
@@ -1,3 +1,26 @@
"""
_validate_conversion_unit(s::AbstractString)

`conversion_unit` is interpolated into `Val(...)` in the generated getter, so
it MUST evaluate to a compile-time constant. Accept only:

* The literal `"nothing"` (no conversion).
* A symbol literal: `:active_power_unit`, `:mva`, `:x_unit`
* A dotted constant path: `PowerSystems.PowerUnits.MW`

Anything else is rejected — including arithmetic operators (`+`, `*`, `-`),
commas, `\$` interpolation, function calls, array/dict literals, whitespace,
leading/trailing dots, and other runtime expressions.
"""
function _validate_conversion_unit(s::AbstractString)
s == "nothing" && return s
occursin(r"^:?\w+(\.\w+)*$", s) && return s
error(
"conversion_unit must be a compile-time constant (symbol literal " *
"or const-bound dotted path); got: $(repr(s))",
)
end

struct StructField
name::String
data_type::String
Expand Down
62 changes: 50 additions & 12 deletions src/utils/generate_structs.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@

import Mustache

# Template note (not rendered into output): the `needs_conversion` getter branch uses
# `Val({{conversion_unit}})`, which requires `conversion_unit` to be a compile-time
# constant (symbol literal or const-bound path). This is enforced at descriptor parse time
# by `_validate_conversion_unit` in generate_struct_files.jl.
const STRUCT_TEMPLATE = """
#=
This file is auto-generated. Do not edit.
Expand Down Expand Up @@ -63,13 +67,26 @@ end

{{/has_null_values}}
{{#accessors}}
{{#needs_conversion}}
{{#create_docstring}}\"\"\"Get [`{{struct_name}}`](@ref) `{{name}}`. The `units` argument is required (e.g. `SU`, `DU`, `MW`, or `Float64`).\"\"\"{{/create_docstring}}
{{accessor}}(value::{{struct_name}}, units) = get_value(value, Val(:{{name}}), Val({{conversion_unit}}), units)
InfrastructureSystems.display_units_arg(::typeof({{accessor}}), ::Type{ {{struct_name}} }) = InfrastructureSystems.SU
{{/needs_conversion}}
{{^needs_conversion}}
{{#create_docstring}}\"\"\"Get [`{{struct_name}}`](@ref) `{{name}}`.\"\"\"{{/create_docstring}}
{{accessor}}(value::{{struct_name}}) = {{#needs_conversion}}get_value(value, Val(:{{name}}), Val({{conversion_unit}})){{/needs_conversion}}{{^needs_conversion}}value.{{name}}{{/needs_conversion}}
{{accessor}}(value::{{struct_name}}) = value.{{name}}
{{/needs_conversion}}
{{/accessors}}

{{#setters}}
{{#needs_conversion}}
{{#create_docstring}}\"\"\"Set [`{{struct_name}}`](@ref) `{{name}}`.\"\"\"{{/create_docstring}}
{{setter}}(value::{{struct_name}}, val) = value.{{name}} = {{#needs_conversion}}set_value(value, Val(:{{name}}), val, Val({{conversion_unit}})){{/needs_conversion}}{{^needs_conversion}}val{{/needs_conversion}}
{{setter}}(value::{{struct_name}}, val) = value.{{name}} = set_value(value, Val(:{{name}}), val, Val({{conversion_unit}}))
{{/needs_conversion}}
{{^needs_conversion}}
{{#create_docstring}}\"\"\"Set [`{{struct_name}}`](@ref) `{{name}}`.\"\"\"{{/create_docstring}}
{{setter}}(value::{{struct_name}}, val) = value.{{name}} = val
{{/needs_conversion}}
{{/setters}}

{{#custom_code}}
Expand Down Expand Up @@ -140,16 +157,34 @@ function generate_structs(directory, data::Vector; print_results = true)
end
accessor_name = accessor_module * "get_" * param["name"]
setter_name = accessor_module * "set_" * param["name"] * "!"
push!(
accessors,
Dict(
"name" => param["name"],
"accessor" => accessor_name,
"create_docstring" => create_docstring,
"needs_conversion" => get(param, "needs_conversion", false),
"conversion_unit" => get(param, "conversion_unit", "nothing"),
),
conversion_unit = _validate_conversion_unit(
get(param, "conversion_unit", "nothing"),
)
include_getter = !get(param, "exclude_getter", false)
if include_getter
push!(
accessors,
Dict(
"name" => param["name"],
"accessor" => accessor_name,
"create_docstring" => create_docstring,
"needs_conversion" => get(param, "needs_conversion", false),
"conversion_unit" => conversion_unit,
),
)
else
internal_name = "_get_" * param["name"]
push!(
accessors,
Dict(
"name" => param["name"],
"accessor" => internal_name,
"create_docstring" => false,
"needs_conversion" => false,
"conversion_unit" => "nothing",
),
)
end
include_setter = !get(param, "exclude_setter", false)
if include_setter
push!(
Expand All @@ -160,11 +195,14 @@ function generate_structs(directory, data::Vector; print_results = true)
"data_type" => param["data_type"],
"create_docstring" => create_docstring,
"needs_conversion" => get(param, "needs_conversion", false),
"conversion_unit" => get(param, "conversion_unit", "nothing"),
"conversion_unit" => conversion_unit,
),
)
end
if field["name"] != "internal" && accessor_module == ""
# exclude_getter/exclude_setter mean "hand-written elsewhere" (e.g.
# unit-aware accessors with different signatures), not "nonexistent" —
# always export the public name.
push!(unique_accessor_functions, accessor_name)
push!(unique_setter_functions, setter_name)
end
Expand Down
Loading