diff --git a/src/InfrastructureSystems.jl b/src/InfrastructureSystems.jl index bbf1fa251..b61c5913d 100644 --- a/src/InfrastructureSystems.jl +++ b/src/InfrastructureSystems.jl @@ -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 @@ -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") @@ -208,4 +217,5 @@ include("function_data/make_convex.jl") include("deprecated.jl") include("Optimization/Optimization.jl") include("Simulation/Simulation.jl") + 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/relative_units.jl b/src/relative_units.jl new file mode 100644 index 000000000..564781620 --- /dev/null +++ b/src/relative_units.jl @@ -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 diff --git a/src/time_series_interface.jl b/src/time_series_interface.jl index 462f2b396..05ecfe040 100644 --- a/src/time_series_interface.jl +++ b/src/time_series_interface.jl @@ -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 """ diff --git a/src/utils/generate_struct_files.jl b/src/utils/generate_struct_files.jl index 64f1b625d..cac2e6c61 100644 --- a/src/utils/generate_struct_files.jl +++ b/src/utils/generate_struct_files.jl @@ -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 diff --git a/src/utils/generate_structs.jl b/src/utils/generate_structs.jl index 5dd0552d5..564ec5783 100644 --- a/src/utils/generate_structs.jl +++ b/src/utils/generate_structs.jl @@ -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. @@ -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}} @@ -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!( @@ -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 diff --git a/src/utils/print_pt_v2.jl b/src/utils/print_pt_v2.jl index abf293a87..d8a3fd53e 100644 --- a/src/utils/print_pt_v2.jl +++ b/src/utils/print_pt_v2.jl @@ -148,7 +148,12 @@ function show_components( val = summary(val) elseif hasproperty(parent, getter_name) getter_func = Base.getproperty(parent, getter_name) - val = getter_func(component) + arg = display_units_arg(getter_func, typeof(component)) + val = if ismissing(arg) + getter_func(component) + else + getter_func(component, arg) + end end data[i, j] = val j += 1 diff --git a/src/utils/print_pt_v3.jl b/src/utils/print_pt_v3.jl index 464c34bf4..e9d0ce8bf 100644 --- a/src/utils/print_pt_v3.jl +++ b/src/utils/print_pt_v3.jl @@ -129,7 +129,12 @@ function show_components( val = summary(val) elseif hasproperty(parent, getter_name) getter_func = Base.getproperty(parent, getter_name) - val = getter_func(component) + arg = display_units_arg(getter_func, typeof(component)) + val = if ismissing(arg) + getter_func(component) + else + getter_func(component, arg) + end end data[i, j] = val j += 1 diff --git a/src/utils/test.jl b/src/utils/test.jl index 72e4bfba4..a14195daa 100644 --- a/src/utils/test.jl +++ b/src/utils/test.jl @@ -45,7 +45,11 @@ end get_internal(component::TestComponent) = component.internal get_internal(component::AdditionalTestComponent) = component.internal get_val(component::TestComponent) = component.val +# 2-arg form so this getter can be used as a `scaling_factor_multiplier` +# (which `_make_time_array` invokes with a units marker). +get_val(component::TestComponent, _) = component.val get_val2(component::TestComponent) = component.val2 +get_val2(component::TestComponent, _) = component.val2 supports_time_series(::TestComponent) = true supports_time_series(::AdditionalTestComponent) = true supports_time_series(::SimpleTestComponent) = false 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" diff --git a/test/test_generate_structs.jl b/test/test_generate_structs.jl index 169669da1..5cad40440 100644 --- a/test/test_generate_structs.jl +++ b/test/test_generate_structs.jl @@ -37,6 +37,21 @@ end @test isfile(joinpath(output_directory, "MyComponent.jl")) end +@testset "conversion_unit validator rejects non-constant expressions" begin + @test IS._validate_conversion_unit(":active_power") == ":active_power" + @test IS._validate_conversion_unit("nothing") == "nothing" + @test IS._validate_conversion_unit("PowerSystems.PowerUnits.MW") == + "PowerSystems.PowerUnits.MW" + @test_throws ErrorException IS._validate_conversion_unit("foo(bar)") + @test_throws ErrorException IS._validate_conversion_unit("[1, 2]") + @test_throws ErrorException IS._validate_conversion_unit("a + b") + @test_throws ErrorException IS._validate_conversion_unit("a*b") + @test_throws ErrorException IS._validate_conversion_unit("a,b") + @test_throws ErrorException IS._validate_conversion_unit("\$var") + @test_throws ErrorException IS._validate_conversion_unit(".mva") + @test_throws ErrorException IS._validate_conversion_unit("trailing.") +end + @testset "Test StructField errors" begin @test_throws ErrorException IS.StructDefinition( struct_name = "MyStruct", diff --git a/test/test_relative_units.jl b/test/test_relative_units.jl new file mode 100644 index 000000000..e27dc64f5 --- /dev/null +++ b/test/test_relative_units.jl @@ -0,0 +1,37 @@ +@testset "RelativeQuantity construction and arithmetic" begin + a = 0.6 * IS.DU + b = 0.4 * IS.DU + @test a isa IS.RelativeQuantity{Float64, IS.DeviceBaseUnit} + @test IS.ustrip(a + b) ≈ 1.0 + @test IS.ustrip(a - b) ≈ 0.2 + @test IS.ustrip(-a) ≈ -0.6 + # scalar multiplication dispatches differently on each side + @test IS.ustrip(2.0 * a) ≈ 1.2 + @test IS.ustrip(a * 2.0) ≈ 1.2 + @test IS.ustrip(a / 2.0) ≈ 0.3 +end + +@testset "RelativeQuantity comparisons" begin + @test 0.6 * IS.DU < 0.7 * IS.DU + @test 0.6 * IS.DU <= 0.6 * IS.DU + @test isapprox(0.6 * IS.DU, 0.60000001 * IS.DU; atol = 1e-6) + @test isless(0.6 * IS.DU, 0.7 * IS.DU) +end + +@testset "DU and SU cannot be mixed" begin + @test_throws Exception 0.6 * IS.DU + 0.4 * IS.SU + @test_throws Exception 0.6 * IS.DU == 0.4 * IS.SU +end + +@testset "RelativeQuantity zero and one" begin + @test zero(IS.RelativeQuantity{Float64, IS.DeviceBaseUnit}) == 0.0 * IS.DU + @test one(IS.RelativeQuantity{Float64, IS.DeviceBaseUnit}) == 1.0 * IS.DU +end + +@testset "RelativeQuantity display" begin + @test sprint(show, 0.6 * IS.DU) == "0.6 DU" + @test sprint(show, 0.3 * IS.SU) == "0.3 SU" + @test sprint(show, IS.DU) == "DU" + @test sprint(show, IS.SU) == "SU" + @test sprint(show, IS.NU) == "NU" +end