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
4 changes: 4 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
Mustache = "ffc61752-8dc7-55ee-8c37-f3e9cdd09e70"
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
PowerSystemsUnits = "68e3e300-ad78-400a-9448-9f67be5a6d39"
PrettyTables = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d"
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Expand All @@ -31,6 +32,7 @@ TerminalLoggers = "5d786b92-1e48-4d6f-9151-6b4477ca9bed"
TimeSeries = "9e3dc215-6440-5c97-bce1-76c03772f85e"
TimerOutputs = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f"
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d"
YAML = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6"

[compat]
Expand Down Expand Up @@ -61,5 +63,7 @@ TerminalLoggers = "~0.1"
TimeSeries = "^0.24, ^0.25"
TimerOutputs = "^0.5"
UUIDs = "1"
PowerSystemsUnits = "^0.1"
Unitful = "^1.12"
YAML = "~0.4"
julia = "^1.6"
16 changes: 16 additions & 0 deletions src/InfrastructureSystems.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@ export PiecewisePointCurve, PiecewiseIncrementalCurve, PiecewiseAverageCurve
export TimeSeriesLinearCurve, TimeSeriesQuadraticCurve, TimeSeriesPiecewisePointCurve
export TimeSeriesPiecewiseIncrementalCurve, TimeSeriesPiecewiseAverageCurve

# Re-export unit types from PowerSystemsUnits
using PowerSystemsUnits
export MW, Mvar, MVA, kV, OHMS, SIEMENS
export DU, SU, NU, DeviceBaseUnit, SystemBaseUnit, NaturalUnit
export AbstractRelativeUnit, RelativeQuantity
export ustrip
export UnitCategory,
PowerCategory, ImpedanceCategory, AdmittanceCategory,
VoltageCategory, CurrentCategory
export POWER, IMPEDANCE, ADMITTANCE, VOLTAGE, CURRENT
export convert_units, base_value, system_base_value, natural_unit, DEFAULT_UNITS
export get_device_base_power, get_system_base_power, get_base_voltage
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file contains a hard rule comment saying not to add exports due to name clashes, but this change re-exports several functions/constants from PowerSystemsUnits (e.g., convert_units, ustrip, DEFAULT_UNITS). Either update/remove the "Do not add export statements" guidance to reflect the intended public API, or avoid re-exporting functions and require consumers to qualify them via PowerSystemsUnits to prevent collisions.

Suggested change
export get_device_base_power, get_system_base_power, get_base_voltage

Copilot uses AI. Check for mistakes.

import Base: @kwdef
import CSV
import DataFrames
Expand Down Expand Up @@ -208,4 +221,7 @@ include("function_data/make_convex.jl")
include("deprecated.jl")
include("Optimization/Optimization.jl")
include("Simulation/Simulation.jl")

# Custom Unitful units (Mvar, MVA) are registered by PowerSystemsUnits.__init__

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
89 changes: 75 additions & 14 deletions src/utils/generate_structs.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@

import Mustache

# Map conversion_unit to the default natural Unitful unit for getters
const NATURAL_UNIT_MAP = Dict{String, String}(
":mva" => "MW", # Power → MW (default for :mva)
":ohm" => "OHMS", # Impedance → Ohms
":siemens" => "SIEMENS", # Admittance → Siemens
)

# Determine the natural unit based on conversion_unit and field name
function get_natural_unit(conversion_unit::String, field_name::String)
if conversion_unit == ":mva"
# Reactive power fields use Mvar instead of MW
if occursin("reactive", lowercase(field_name))
return "Mvar"
else
return "MW"
end
end
return get(NATURAL_UNIT_MAP, conversion_unit, "MW")
end

Comment on lines +4 to +23
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NATURAL_UNIT_MAP, get_natural_unit, and the computed natural_unit field are currently unused in the Mustache STRUCT_TEMPLATE (no {{natural_unit}} reference). This adds dead code/complexity to the generator. Either wire natural_unit into the generated getter/docstring behavior (and add coverage) or remove these definitions until they’re needed.

Suggested change
# Map conversion_unit to the default natural Unitful unit for getters
const NATURAL_UNIT_MAP = Dict{String, String}(
":mva" => "MW", # Power → MW (default for :mva)
":ohm" => "OHMS", # Impedance → Ohms
":siemens" => "SIEMENS", # Admittance → Siemens
)
# Determine the natural unit based on conversion_unit and field name
function get_natural_unit(conversion_unit::String, field_name::String)
if conversion_unit == ":mva"
# Reactive power fields use Mvar instead of MW
if occursin("reactive", lowercase(field_name))
return "Mvar"
else
return "MW"
end
end
return get(NATURAL_UNIT_MAP, conversion_unit, "MW")
end

Copilot uses AI. Check for mistakes.
const STRUCT_TEMPLATE = """
#=
This file is auto-generated. Do not edit.
Expand Down Expand Up @@ -63,13 +83,26 @@ end

{{/has_null_values}}
{{#accessors}}
{{#needs_conversion}}
{{#create_docstring}}\"\"\"Get [`{{struct_name}}`](@ref) `{{name}}`. Returns value in DEFAULT_UNITS (system base per-unit).\"\"\"{{/create_docstring}}
{{accessor}}(value::{{struct_name}}) = get_value(value, Val(:{{name}}), Val({{conversion_unit}}), DEFAULT_UNITS)
{{accessor}}(value::{{struct_name}}, units) = get_value(value, Val(:{{name}}), Val({{conversion_unit}}), units)
{{/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}}`. Value must have units (e.g., `30.0MW`, `0.5DU`).\"\"\"{{/create_docstring}}
{{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}} = {{#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}} = val
{{/needs_conversion}}
{{/setters}}

{{#custom_code}}
Expand Down Expand Up @@ -140,16 +173,38 @@ 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 = get(param, "conversion_unit", "nothing")
natural_unit = get_natural_unit(conversion_unit, param["name"])
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,
"natural_unit" => natural_unit,
),
)
else
# When public getter is excluded, generate an internal _get_ accessor
# that returns the raw field value (Float64). Used for fields like
# base_power where the public getter is hand-written with units.
internal_name = "_get_" * param["name"]
push!(
accessors,
Dict(
"name" => param["name"],
"accessor" => internal_name,
"create_docstring" => false,
"needs_conversion" => false,
"conversion_unit" => "nothing",
"natural_unit" => "",
),
)
end
include_setter = !get(param, "exclude_setter", false)
if include_setter
push!(
Expand All @@ -160,12 +215,18 @@ 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 == ""
push!(unique_accessor_functions, accessor_name)
if include_getter
push!(unique_accessor_functions, accessor_name)
end
# Always export setter name even if exclude_setter is true,
# because exclude_setter means "hand-written elsewhere" not "nonexistent".
# Only suppress getter export when exclude_getter is true (meaning
# the public getter is hand-written with a different signature, e.g. unitful).
push!(unique_setter_functions, setter_name)
end

Expand Down
9 changes: 3 additions & 6 deletions src/value_curve.jl
Original file line number Diff line number Diff line change
Expand Up @@ -272,12 +272,9 @@ end
IncrementalCurve(data::AverageRateCurve) = IncrementalCurve(InputOutputCurve(data))

# PRINTING
"Whether there is a cost alias for the instance or type under consideration"
is_cost_alias(::Union{ValueCurve, Type{<:ValueCurve}}) = false

# For cost aliases, return the alias name; otherwise, return the type name without the parameter
simple_type_name(curve::ValueCurve) =
string(is_cost_alias(curve) ? typeof(curve) : nameof(typeof(curve)))
# typeof() can't recover const alias names, so we use nameof for non-aliases
# and override in cost_aliases.jl for each alias.
simple_type_name(curve::ValueCurve) = string(nameof(typeof(curve)))

function Base.show(io::IO, ::MIME"text/plain", curve::InputOutputCurve)
print(io, simple_type_name(curve))
Expand Down
14 changes: 10 additions & 4 deletions test/test_cost_functions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -286,10 +286,16 @@ end
@test zero(IS.FuelCurve) ==
IS.FuelCurve(IS.InputOutputCurve(IS.LinearFunctionData(0.0, 0.0)), 0.0)

@test repr(cc) == sprint(show, cc) ==
"InfrastructureSystems.CostCurve{QuadraticCurve}(QuadraticCurve(1.0, 2.0, 3.0), InfrastructureSystems.UnitSystemModule.UnitSystem.NATURAL_UNITS = 2, LinearCurve(0.0, 0.0))"
@test repr(fc) == sprint(show, fc) ==
"InfrastructureSystems.FuelCurve{QuadraticCurve}(QuadraticCurve(1.0, 2.0, 3.0), InfrastructureSystems.UnitSystemModule.UnitSystem.NATURAL_UNITS = 2, 4.0, LinearCurve(0.0, 0.0), LinearCurve(0.0, 0.0))"
# repr and sprint(show, ...) must agree; the type parameter may or may not
# be module-qualified depending on what's in scope, so check key content.
@test repr(cc) == sprint(show, cc)
@test occursin("CostCurve", repr(cc))
@test occursin("QuadraticCurve(1.0, 2.0, 3.0)", repr(cc))
@test occursin("LinearCurve(0.0, 0.0)", repr(cc))
@test repr(fc) == sprint(show, fc)
@test occursin("FuelCurve", repr(fc))
@test occursin("QuadraticCurve(1.0, 2.0, 3.0)", repr(fc))
Comment on lines +293 to +297
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The updated repr assertions for CostCurve/FuelCurve no longer check that the unit system (e.g., NATURAL_UNITS) is present in the representation. This makes it easier for a regression to drop or misreport units without failing tests. Consider adding an occursin assertion for the unit-system portion of the string (while still avoiding module-qualification brittleness).

Suggested change
@test occursin("QuadraticCurve(1.0, 2.0, 3.0)", repr(cc))
@test occursin("LinearCurve(0.0, 0.0)", repr(cc))
@test repr(fc) == sprint(show, fc)
@test occursin("FuelCurve", repr(fc))
@test occursin("QuadraticCurve(1.0, 2.0, 3.0)", repr(fc))
@test occursin("QuadraticCurve(1.0, 2.0, 3.0)", repr(cc))
@test occursin("NATURAL_UNITS", repr(cc))
@test occursin("LinearCurve(0.0, 0.0)", repr(cc))
@test repr(fc) == sprint(show, fc)
@test occursin("FuelCurve", repr(fc))
@test occursin("QuadraticCurve(1.0, 2.0, 3.0)", repr(fc))
@test occursin("NATURAL_UNITS", repr(fc))

Copilot uses AI. Check for mistakes.
@test occursin("4.0", repr(fc))
@test sprint(show, "text/plain", cc) ==
sprint(show, "text/plain", cc; context = :compact => false) ==
"CostCurve:\n value_curve: QuadraticCurve (a type of InfrastructureSystems.InputOutputCurve) where function is: f(x) = 1.0 x^2 + 2.0 x + 3.0\n power_units: InfrastructureSystems.UnitSystemModule.UnitSystem.NATURAL_UNITS = 2\n vom_cost: LinearCurve (a type of InfrastructureSystems.InputOutputCurve) where function is: f(x) = 0.0 x + 0.0"
Expand Down
Loading