diff --git a/Project.toml b/Project.toml index 3f89ed780..0f3d054f3 100644 --- a/Project.toml +++ b/Project.toml @@ -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" @@ -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] @@ -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" diff --git a/src/InfrastructureSystems.jl b/src/InfrastructureSystems.jl index bbf1fa251..1c7511860 100644 --- a/src/InfrastructureSystems.jl +++ b/src/InfrastructureSystems.jl @@ -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 + import Base: @kwdef import CSV import DataFrames @@ -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 diff --git a/src/cost_aliases.jl b/src/cost_aliases.jl index c42144322..552a0d3bb 100644 --- a/src/cost_aliases.jl +++ b/src/cost_aliases.jl @@ -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) @@ -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)) @@ -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 @@ -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( @@ -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) @@ -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)) @@ -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 @@ -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) @@ -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, ) @@ -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) @@ -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) diff --git a/src/utils/generate_structs.jl b/src/utils/generate_structs.jl index 5dd0552d5..a0b35cd16 100644 --- a/src/utils/generate_structs.jl +++ b/src/utils/generate_structs.jl @@ -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 + const STRUCT_TEMPLATE = """ #= This file is auto-generated. Do not edit. @@ -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}} @@ -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!( @@ -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 diff --git a/src/value_curve.jl b/src/value_curve.jl index 81efebbff..3b73246fc 100644 --- a/src/value_curve.jl +++ b/src/value_curve.jl @@ -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)) diff --git a/test/test_cost_functions.jl b/test/test_cost_functions.jl index 333a6a877..0453cb620 100644 --- a/test/test_cost_functions.jl +++ b/test/test_cost_functions.jl @@ -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)) + @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"