From 3cd11124a30b90a421dc7541336c8b02ac5aeec7 Mon Sep 17 00:00:00 2001 From: Luke Kiernan Date: Tue, 12 May 2026 10:19:57 -0600 Subject: [PATCH 1/2] Add FlowSign trait for variable directionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Holy-traits style: `flow_sign(V)` returns Injection/Withdrawal/Unsigned; `multiplier_from_sign` resolves ±1.0 via dispatch. Lets POM derive most power-balance multipliers from the variable type alone. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/InfrastructureOptimizationModels.jl | 3 ++ src/common_models/rateofchange_constraints.jl | 1 - src/core/standard_variables_expressions.jl | 32 +++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/InfrastructureOptimizationModels.jl b/src/InfrastructureOptimizationModels.jl index 6fa5498e..c85299b3 100644 --- a/src/InfrastructureOptimizationModels.jl +++ b/src/InfrastructureOptimizationModels.jl @@ -468,6 +468,9 @@ export ReservationVariable export PiecewiseLinearCostVariable export RateofChangeConstraintSlackUp, RateofChangeConstraintSlackDown export DCVoltage +# Flow-direction trait for variable types +export FlowSign, Injection, Withdrawal, Unsigned +export flow_sign, multiplier_from_sign # Abstract types needed by POM for type hierarchy export SparseVariableType, InterpolationVariableType, BinaryInterpolationVariableType diff --git a/src/common_models/rateofchange_constraints.jl b/src/common_models/rateofchange_constraints.jl index 3785d6de..4aaae74b 100644 --- a/src/common_models/rateofchange_constraints.jl +++ b/src/common_models/rateofchange_constraints.jl @@ -1,4 +1,3 @@ -# NOTE: not included currently. function _get_minutes_per_period(container::OptimizationContainer) resolution = get_resolution(container) if resolution > Dates.Minute(1) diff --git a/src/core/standard_variables_expressions.jl b/src/core/standard_variables_expressions.jl index 58cf8768..61008422 100644 --- a/src/core/standard_variables_expressions.jl +++ b/src/core/standard_variables_expressions.jl @@ -36,6 +36,38 @@ struct RateofChangeConstraintSlackDown <: VariableType end # HVDC Variables (used in add_pwl_methods) struct DCVoltage <: VariableType end +################################################################################# +# Flow direction trait +# +# Encodes the sign with which a variable contributes to a power-balance +# expression. Replaces scattered `get_variable_multiplier(...) = ±1.0` overrides +# whose only signal is the variable type itself. Device-driven sign overrides +# (e.g. anything on `PSY.ElectricLoad`) still live in POM as more-specific +# dispatches. +################################################################################# + +# Holy-traits style: `flow_sign` returns a *type*, and `multiplier_from_sign` +# dispatches on `::Type{<:FlowSign}`. Both layers resolve at compile time so the +# numeric multiplier folds away at every call site. +abstract type FlowSign end +struct Injection <: FlowSign end +struct Withdrawal <: FlowSign end +struct Unsigned <: FlowSign end + +# Default: no directional meaning attached to the variable type. +flow_sign(::Type{<:VariableType}) = Unsigned + +multiplier_from_sign(::Type{Injection}) = 1.0 +multiplier_from_sign(::Type{Withdrawal}) = -1.0 +# Variables without flow semantics (OnVariable, StartVariable, ...) keep the +# legacy 1.0 default so callers that don't care about sign still work. +multiplier_from_sign(::Type{Unsigned}) = 1.0 + +# Standard variable types defined here: +flow_sign(::Type{ActivePowerVariable}) = Injection +flow_sign(::Type{ActivePowerInVariable}) = Withdrawal +flow_sign(::Type{ActivePowerOutVariable}) = Injection + ################################################################################# # Standard Expression Types # These are the base expression types for aggregating terms From b8a95af250da5be0af5139036d8b9e0b4ec2d201 Mon Sep 17 00:00:00 2001 From: Luke Kiernan Date: Wed, 13 May 2026 21:10:47 -0600 Subject: [PATCH 2/2] Rename FlowSign variants and add tests Address Copilot review on #98: rename Injection/Withdrawal/Unsigned to FlowInjection/FlowWithdrawal/FlowUndirected to avoid the name clash with Base.Unsigned and reduce ambiguity for downstream `using` consumers. Add test/test_flow_sign.jl covering defaults, standard mappings, and multiplier roundtrip. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/InfrastructureOptimizationModels.jl | 2 +- src/core/standard_variables_expressions.jl | 20 ++++++------- test/InfrastructureOptimizationModelsTests.jl | 1 + test/Project.toml | 2 -- test/test_flow_sign.jl | 28 +++++++++++++++++++ 5 files changed, 40 insertions(+), 13 deletions(-) create mode 100644 test/test_flow_sign.jl diff --git a/src/InfrastructureOptimizationModels.jl b/src/InfrastructureOptimizationModels.jl index c85299b3..e62ba704 100644 --- a/src/InfrastructureOptimizationModels.jl +++ b/src/InfrastructureOptimizationModels.jl @@ -469,7 +469,7 @@ export PiecewiseLinearCostVariable export RateofChangeConstraintSlackUp, RateofChangeConstraintSlackDown export DCVoltage # Flow-direction trait for variable types -export FlowSign, Injection, Withdrawal, Unsigned +export FlowSign, FlowInjection, FlowWithdrawal, FlowUndirected export flow_sign, multiplier_from_sign # Abstract types needed by POM for type hierarchy export SparseVariableType, InterpolationVariableType, BinaryInterpolationVariableType diff --git a/src/core/standard_variables_expressions.jl b/src/core/standard_variables_expressions.jl index 61008422..6522e19d 100644 --- a/src/core/standard_variables_expressions.jl +++ b/src/core/standard_variables_expressions.jl @@ -50,23 +50,23 @@ struct DCVoltage <: VariableType end # dispatches on `::Type{<:FlowSign}`. Both layers resolve at compile time so the # numeric multiplier folds away at every call site. abstract type FlowSign end -struct Injection <: FlowSign end -struct Withdrawal <: FlowSign end -struct Unsigned <: FlowSign end +struct FlowInjection <: FlowSign end +struct FlowWithdrawal <: FlowSign end +struct FlowUndirected <: FlowSign end # Default: no directional meaning attached to the variable type. -flow_sign(::Type{<:VariableType}) = Unsigned +flow_sign(::Type{<:VariableType}) = FlowUndirected -multiplier_from_sign(::Type{Injection}) = 1.0 -multiplier_from_sign(::Type{Withdrawal}) = -1.0 +multiplier_from_sign(::Type{FlowInjection}) = 1.0 +multiplier_from_sign(::Type{FlowWithdrawal}) = -1.0 # Variables without flow semantics (OnVariable, StartVariable, ...) keep the # legacy 1.0 default so callers that don't care about sign still work. -multiplier_from_sign(::Type{Unsigned}) = 1.0 +multiplier_from_sign(::Type{FlowUndirected}) = 1.0 # Standard variable types defined here: -flow_sign(::Type{ActivePowerVariable}) = Injection -flow_sign(::Type{ActivePowerInVariable}) = Withdrawal -flow_sign(::Type{ActivePowerOutVariable}) = Injection +flow_sign(::Type{ActivePowerVariable}) = FlowInjection +flow_sign(::Type{ActivePowerInVariable}) = FlowWithdrawal +flow_sign(::Type{ActivePowerOutVariable}) = FlowInjection ################################################################################# # Standard Expression Types diff --git a/test/InfrastructureOptimizationModelsTests.jl b/test/InfrastructureOptimizationModelsTests.jl index f218b685..81adc613 100644 --- a/test/InfrastructureOptimizationModelsTests.jl +++ b/test/InfrastructureOptimizationModelsTests.jl @@ -114,6 +114,7 @@ function run_tests() # TODO service_model.jl include(joinpath(TEST_DIR, "test_settings.jl")) # standard_variables_expressions.jl: low complexity + include(joinpath(TEST_DIR, "test_flow_sign.jl")) # time_series_parameter_types.jl: low complexity # --- objective_function/ subfolder --- diff --git a/test/Project.toml b/test/Project.toml index fed99d0f..0ec6c3d2 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -9,7 +9,6 @@ DataFramesMeta = "1313f7d8-7da2-5740-9ea0-a2ca25f37964" DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" -InfrastructureOptimizationModels = "bed98974-b02a-5e2f-9ee0-a103f5c45069" InfrastructureSystems = "2cd47ed4-ca9b-11e9-27f2-ab636a7671f1" Ipopt = "b6b21f68-93f8-5de0-b562-5493be1d77c9" JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" @@ -30,7 +29,6 @@ UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" UnoSolver = "1baa60ac-02f7-4b39-a7a8-2f4f58486b05" [sources] -InfrastructureOptimizationModels = {path = ".."} InfrastructureSystems = {rev = "IS4", url = "https://github.com/Sienna-Platform/InfrastructureSystems.jl"} [compat] diff --git a/test/test_flow_sign.jl b/test/test_flow_sign.jl new file mode 100644 index 00000000..65947fb0 --- /dev/null +++ b/test/test_flow_sign.jl @@ -0,0 +1,28 @@ +@testset "FlowSign trait" begin + @testset "multiplier_from_sign maps to expected ±1.0" begin + @test IOM.multiplier_from_sign(IOM.FlowInjection) == 1.0 + @test IOM.multiplier_from_sign(IOM.FlowWithdrawal) == -1.0 + @test IOM.multiplier_from_sign(IOM.FlowUndirected) == 1.0 + end + + @testset "flow_sign defaults to FlowUndirected" begin + # Any VariableType without an explicit override falls back to FlowUndirected. + # OnVariable / StartVariable / etc. have no flow semantics by design. + @test IOM.flow_sign(OnVariable) === IOM.FlowUndirected + @test IOM.flow_sign(StartVariable) === IOM.FlowUndirected + @test IOM.flow_sign(StopVariable) === IOM.FlowUndirected + end + + @testset "Standard active-power variables carry the correct sign" begin + @test IOM.flow_sign(ActivePowerVariable) === IOM.FlowInjection + @test IOM.flow_sign(ActivePowerInVariable) === IOM.FlowWithdrawal + @test IOM.flow_sign(ActivePowerOutVariable) === IOM.FlowInjection + end + + @testset "Roundtrip: multiplier_from_sign ∘ flow_sign" begin + @test IOM.multiplier_from_sign(IOM.flow_sign(ActivePowerVariable)) == 1.0 + @test IOM.multiplier_from_sign(IOM.flow_sign(ActivePowerInVariable)) == -1.0 + @test IOM.multiplier_from_sign(IOM.flow_sign(ActivePowerOutVariable)) == 1.0 + @test IOM.multiplier_from_sign(IOM.flow_sign(OnVariable)) == 1.0 + end +end