From 7fe0ecff2cba612dd7ea891f2d7324c42683febe Mon Sep 17 00:00:00 2001 From: Luke Kiernan Date: Wed, 5 Aug 2026 13:12:17 -0600 Subject: [PATCH 1/3] add tests for lower bound constraints --- src/InfrastructureOptimizationModels.jl | 1 + src/common_models/range_constraint.jl | 16 +- test/InfrastructureOptimizationModelsTests.jl | 1 + test/mocks/mock_components.jl | 3 +- test/mocks/mock_time_series.jl | 24 +- test/test_parameterized_range_constraints.jl | 279 ++++++++++++++++++ test/verify_mocks.jl | 2 +- 7 files changed, 322 insertions(+), 4 deletions(-) create mode 100644 test/test_parameterized_range_constraints.jl diff --git a/src/InfrastructureOptimizationModels.jl b/src/InfrastructureOptimizationModels.jl index d98a6fb0..b0117ec3 100644 --- a/src/InfrastructureOptimizationModels.jl +++ b/src/InfrastructureOptimizationModels.jl @@ -344,6 +344,7 @@ export add_sparse_pwl_interpolation_variables! export JuMPOrFloat # Constraint helpers export add_range_constraints!, add_parameterized_upper_bound_range_constraints +export add_parameterized_lower_bound_range_constraints export add_reserve_bound_range_constraints!, add_commitment_bound_range_constraints! export add_semicontinuous_range_constraints!, add_semicontinuous_ramp_constraints! export add_slacked_range_constraints!, fill_slacked_range_constraints! diff --git a/src/common_models/range_constraint.jl b/src/common_models/range_constraint.jl index f6d95f91..37f3ad68 100644 --- a/src/common_models/range_constraint.jl +++ b/src/common_models/range_constraint.jl @@ -414,7 +414,14 @@ function add_parameterized_bound_range_constraints( return end -# Backwards-compatible wrappers +# Direction-specific wrappers over `add_parameterized_bound_range_constraints`. +""" +Add `array[name, t] >= multiplier[name, t] * parameter[name, t]` constraints of type `T`, +where `array` is the variable or expression `U` and the right-hand side comes from the +parameter `P`. For `P <: TimeSeriesParameter` only devices that own the time series named in +`get_time_series_names(model)[P]` are constrained. Mirror of +[`add_parameterized_upper_bound_range_constraints`](@ref). +""" function add_parameterized_lower_bound_range_constraints( container::OptimizationContainer, ::Type{T}, @@ -444,6 +451,13 @@ function add_parameterized_lower_bound_range_constraints( return end +""" +Add `array[name, t] <= multiplier[name, t] * parameter[name, t]` constraints of type `T`, +where `array` is the variable or expression `U` and the right-hand side comes from the +parameter `P`. For `P <: TimeSeriesParameter` only devices that own the time series named in +`get_time_series_names(model)[P]` are constrained. Mirror of +[`add_parameterized_lower_bound_range_constraints`](@ref). +""" function add_parameterized_upper_bound_range_constraints( container::OptimizationContainer, ::Type{T}, diff --git a/test/InfrastructureOptimizationModelsTests.jl b/test/InfrastructureOptimizationModelsTests.jl index dbf98550..35f33b71 100644 --- a/test/InfrastructureOptimizationModelsTests.jl +++ b/test/InfrastructureOptimizationModelsTests.jl @@ -135,6 +135,7 @@ function run_tests() include(joinpath(TEST_DIR, "test_jump_utils.jl")) include(joinpath(TEST_DIR, "test_pwl_methods.jl")) include(joinpath(TEST_DIR, "test_ramp_constraints.jl")) + include(joinpath(TEST_DIR, "test_parameterized_range_constraints.jl")) include(joinpath(TEST_DIR, "test_duration_constraints.jl")) include(joinpath(TEST_DIR, "test_emulation_model_store.jl")) include(joinpath(TEST_DIR, "test_model_store.jl")) diff --git a/test/mocks/mock_components.jl b/test/mocks/mock_components.jl index 2e2881c6..d54f9310 100644 --- a/test/mocks/mock_components.jl +++ b/test/mocks/mock_components.jl @@ -128,7 +128,8 @@ end get_name(l::MockLoad) = l.name get_available(l::MockLoad) = l.available get_bus(l::MockLoad) = l.bus -get_max_active_power(l::MockLoad) = l.max_active_power +IOM.get_max_active_power(l::MockLoad) = l.max_active_power +IOM.get_max_active_power(g::MockThermalGen) = g.active_power_limits.max # Mock Branch struct MockBranch <: AbstractMockDevice diff --git a/test/mocks/mock_time_series.jl b/test/mocks/mock_time_series.jl index ce7664a7..4b94dc6e 100644 --- a/test/mocks/mock_time_series.jl +++ b/test/mocks/mock_time_series.jl @@ -4,7 +4,9 @@ Minimal time series mocks for testing parameter updates. using Dates -struct MockDeterministic +# Subtypes IS.TimeSeriesData so it can stand in for a real time series type wherever one is +# required by dispatch (e.g. `TimeSeriesAttributes`), without carrying any of the machinery. +struct MockDeterministic <: IS.TimeSeriesData name::String data::Vector{Float64} resolution::Dates.Period @@ -18,3 +20,23 @@ struct MockSingleTimeSeries end get_name(ts::Union{MockDeterministic, MockSingleTimeSeries}) = ts.name + +# Mock components are immutable and hold no time series manager, so `IS.has_time_series` +# can't work off the component itself. Builders that filter devices on time series +# ownership (e.g. the parameterized range constraints) consult this registry instead. +const MOCK_TIME_SERIES_REGISTRY = Dict{String, Set{String}}() + +function mock_add_time_series!(component, ts_name::AbstractString) + push!(get!(MOCK_TIME_SERIES_REGISTRY, get_name(component), Set{String}()), ts_name) + return +end + +mock_clear_time_series!() = empty!(MOCK_TIME_SERIES_REGISTRY) + +function IS.has_time_series( + component::AbstractMockDevice, + ::Type{MockDeterministic}, + ts_name::AbstractString, +) + return ts_name in get(MOCK_TIME_SERIES_REGISTRY, get_name(component), Set{String}()) +end diff --git a/test/test_parameterized_range_constraints.jl b/test/test_parameterized_range_constraints.jl new file mode 100644 index 00000000..133387ac --- /dev/null +++ b/test/test_parameterized_range_constraints.jl @@ -0,0 +1,279 @@ +""" +Unit tests for the parameterized range constraints +(`add_parameterized_lower_bound_range_constraints` / +`add_parameterized_upper_bound_range_constraints`, issue #130). The lower-bound wrapper has +no in-tree caller today — hydro and the other POM formulations only use the upper-bound one +— so these tests are what keeps it honest. +""" + +struct TestValueParameter <: IOM.VariableValueParameter end +struct TestTimeSeriesParameter <: IOM.TimeSeriesParameter end +struct TestEventParameter <: IOM.EventParameter end + +const PARAM_RANGE_TS_NAME = "max_active_power" + +function _make_parameterized_range_container(devices, time_steps) + mock_sys = MockSystem(100.0) + settings = IOM.Settings( + mock_sys; + horizon = Dates.Hour(length(time_steps)), + resolution = Dates.Hour(1), + time_series_cache_size = 0, + ) + container = IOM.OptimizationContainer(mock_sys, settings, nothing, MockDeterministic) + IOM.set_time_steps!(container, time_steps) + jump_model = IOM.get_jump_model(container) + names = [get_name(d) for d in devices] + D = eltype(devices) + var = IOM.add_variable_container!(container, TestVariableType, D, names, time_steps) + expr = + IOM.add_expression_container!(container, TestExpressionType, D, names, time_steps) + for name in names, t in time_steps + v = JuMP.@variable(jump_model) + var[name, t] = v + # Scaled so a test can tell the expression path from the variable path. + expr[name, t] = 2.0 * v + end + return container +end + +"Add a `VariableValueParameter` container with `param[name, t] = value_fn(name, t)`." +function _add_value_parameter!( + container, + ::Type{P}, + ::Type{D}, + names, + time_steps, + value_fn, + multiplier, +) where {P <: IOM.ParameterType, D <: IS.InfrastructureSystemsComponent} + param_container = IOM.add_param_container!( + container, + P, + D, + IOM.VariableKey(TestVariableType, D), + names, + time_steps, + ) + param_array = IOM.get_parameter_array(param_container) + mult_array = IOM.get_multiplier_array(param_container) + for name in names, t in time_steps + param_array[name, t] = value_fn(name, t) + mult_array[name, t] = multiplier + end + return param_container +end + +@testset "Parameterized range constraints" begin + time_steps = 1:3 + devices = [make_mock_thermal("A"), make_mock_thermal("B")] + names = ["A", "B"] + model = IOM.DeviceModel( + MockThermalGen, + TestFormulation; + time_series_names = Dict{Type{<:IOM.ParameterType}, String}( + TestTimeSeriesParameter => PARAM_RANGE_TS_NAME, + ), + ) + param_value(name, t) = name == "A" ? 1.0 * t : 10.0 + t + multiplier = 0.5 + + @testset "Lower bound over a variable" begin + container = _make_parameterized_range_container(devices, time_steps) + _add_value_parameter!( + container, + TestValueParameter, + MockThermalGen, + names, + time_steps, + param_value, + multiplier, + ) + add_parameterized_lower_bound_range_constraints( + container, + TestConstraintType, + TestVariableType, + TestValueParameter, + devices, + model, + TestPowerModel, + ) + con = IOM.get_constraint(container, TestConstraintType, MockThermalGen, "lb") + var = IOM.get_variable(container, TestVariableType, MockThermalGen) + @test axes(con)[1] == names + @test axes(con)[2] == time_steps + for name in names, t in time_steps + constraint = JuMP.constraint_object(con[name, t]) + @test constraint.set isa MOI.GreaterThan + @test JuMP.normalized_rhs(con[name, t]) ≈ multiplier * param_value(name, t) + @test JuMP.normalized_coefficient(con[name, t], var[name, t]) ≈ 1.0 + end + end + + @testset "Lower bound over an expression" begin + container = _make_parameterized_range_container(devices, time_steps) + _add_value_parameter!( + container, + TestValueParameter, + MockThermalGen, + names, + time_steps, + param_value, + multiplier, + ) + add_parameterized_lower_bound_range_constraints( + container, + TestConstraintType, + TestExpressionType, + TestValueParameter, + devices, + model, + TestPowerModel, + ) + con = IOM.get_constraint(container, TestConstraintType, MockThermalGen, "lb") + var = IOM.get_variable(container, TestVariableType, MockThermalGen) + for name in names, t in time_steps + # The expression is 2 * var, so the LHS coefficient must be 2, not 1. + @test JuMP.normalized_coefficient(con[name, t], var[name, t]) ≈ 2.0 + @test JuMP.normalized_rhs(con[name, t]) ≈ multiplier * param_value(name, t) + end + end + + @testset "Lower and upper bounds are mirror images" begin + container = _make_parameterized_range_container(devices, time_steps) + _add_value_parameter!( + container, + TestValueParameter, + MockThermalGen, + names, + time_steps, + param_value, + multiplier, + ) + for f in ( + add_parameterized_lower_bound_range_constraints, + add_parameterized_upper_bound_range_constraints, + ) + f( + container, + TestConstraintType, + TestVariableType, + TestValueParameter, + devices, + model, + TestPowerModel, + ) + end + con_lb = IOM.get_constraint(container, TestConstraintType, MockThermalGen, "lb") + con_ub = IOM.get_constraint(container, TestConstraintType, MockThermalGen, "ub") + # Distinct containers under the "lb"/"ub" metas, same LHS and RHS, opposite senses. + for name in names, t in time_steps + lb = JuMP.constraint_object(con_lb[name, t]) + ub = JuMP.constraint_object(con_ub[name, t]) + @test lb.set isa MOI.GreaterThan + @test ub.set isa MOI.LessThan + @test lb.func == ub.func + @test JuMP.normalized_rhs(con_lb[name, t]) ≈ + JuMP.normalized_rhs(con_ub[name, t]) + end + end + + @testset "Time series parameter constrains only devices owning the time series" begin + mock_clear_time_series!() + ts_devices = + [make_mock_thermal("A"), make_mock_thermal("B"), make_mock_thermal("C")] + ts_names = ["A", "C"] # B has no time series + for device in ts_devices + get_name(device) in ts_names && + mock_add_time_series!(device, PARAM_RANGE_TS_NAME) + end + uuids = Dict(name => "uuid-$name" for name in ts_names) + container = _make_parameterized_range_container(ts_devices, time_steps) + param_container = IOM.add_param_container!( + container, + TestTimeSeriesParameter, + MockThermalGen, + MockDeterministic, + PARAM_RANGE_TS_NAME, + collect(values(uuids)), + ts_names, + (), + time_steps, + ) + attributes = IOM.get_attributes(param_container) + param_array = IOM.get_parameter_array(param_container) + mult_array = IOM.get_multiplier_array(param_container) + for name in ts_names + IOM.add_component_name!(attributes, name, uuids[name]) + for t in time_steps + param_array[uuids[name], t] = param_value(name, t) + mult_array[name, t] = multiplier + end + end + + add_parameterized_lower_bound_range_constraints( + container, + TestConstraintType, + TestVariableType, + TestTimeSeriesParameter, + ts_devices, + model, + TestPowerModel, + ) + con = IOM.get_constraint(container, TestConstraintType, MockThermalGen, "lb") + @test axes(con)[1] == ts_names + var = IOM.get_variable(container, TestVariableType, MockThermalGen) + for name in ts_names, t in time_steps + @test JuMP.normalized_rhs(con[name, t]) ≈ multiplier * param_value(name, t) + @test JuMP.normalized_coefficient(con[name, t], var[name, t]) ≈ 1.0 + end + mock_clear_time_series!() + end + + @testset "Time series parameter with no owning devices builds nothing" begin + mock_clear_time_series!() + container = _make_parameterized_range_container(devices, time_steps) + add_parameterized_lower_bound_range_constraints( + container, + TestConstraintType, + TestVariableType, + TestTimeSeriesParameter, + devices, + model, + TestPowerModel, + ) + @test isempty(IOM.get_constraint_keys(container)) + end + + @testset "Event parameter bounds by the device's max active power" begin + container = _make_parameterized_range_container(devices, time_steps) + param_container = IOM.add_param_container!( + container, + TestEventParameter, + MockThermalGen, + MockThermalGen, + names, + time_steps, + ) + param_array = IOM.get_parameter_array(param_container) + for name in names, t in time_steps + param_array[name, t] = param_value(name, t) + end + add_parameterized_lower_bound_range_constraints( + container, + TestConstraintType, + TestVariableType, + TestEventParameter, + devices, + model, + TestPowerModel, + ) + con = IOM.get_constraint(container, TestConstraintType, MockThermalGen, "lb") + for (device, name) in zip(devices, names), t in time_steps + # The event path scales by the device's max active power, not the multiplier + # array (which is left as NaN here). + expected = IOM.get_max_active_power(device) * param_value(name, t) + @test JuMP.normalized_rhs(con[name, t]) ≈ expected + end + end +end diff --git a/test/verify_mocks.jl b/test/verify_mocks.jl index 1134ee02..3b0ee9d9 100644 --- a/test/verify_mocks.jl +++ b/test/verify_mocks.jl @@ -51,7 +51,7 @@ load = MockLoad("load1", true, bus, 75.0) get_name(load) get_available(load) get_bus(load) -get_max_active_power(load) +IOM.get_max_active_power(load) # MockBranch bus2 = MockBus("bus2", 2, :PQ) From 9f25ebff723b936f6faedccf19ab658460a1d60a Mon Sep 17 00:00:00 2001 From: Luke Kiernan Date: Wed, 5 Aug 2026 17:43:59 -0600 Subject: [PATCH 2/3] copilot code review: docstring fix --- src/common_models/range_constraint.jl | 30 ++++++++++++++++++--------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/common_models/range_constraint.jl b/src/common_models/range_constraint.jl index 37f3ad68..395f5343 100644 --- a/src/common_models/range_constraint.jl +++ b/src/common_models/range_constraint.jl @@ -416,11 +416,16 @@ end # Direction-specific wrappers over `add_parameterized_bound_range_constraints`. """ -Add `array[name, t] >= multiplier[name, t] * parameter[name, t]` constraints of type `T`, -where `array` is the variable or expression `U` and the right-hand side comes from the -parameter `P`. For `P <: TimeSeriesParameter` only devices that own the time series named in -`get_time_series_names(model)[P]` are constrained. Mirror of -[`add_parameterized_upper_bound_range_constraints`](@ref). +Add `array[name, t] >= rhs[name, t]` constraints of type `T`, where `array` is the variable or +expression `U` and the right-hand side is built from the parameter `P`. The right-hand side +depends on the parameter family: + + - generic `P`: `multiplier[name, t] * parameter[name, t]` + - `P <: EventParameter`: `get_max_active_power(device) * parameter[name, t]` + - `P <: TimeSeriesParameter`: `multiplier[name, t] * parameter_column[t]`, and only devices + that own the time series named in `get_time_series_names(model)[P]` are constrained. + +Mirror of [`add_parameterized_upper_bound_range_constraints`](@ref). """ function add_parameterized_lower_bound_range_constraints( container::OptimizationContainer, @@ -452,11 +457,16 @@ function add_parameterized_lower_bound_range_constraints( end """ -Add `array[name, t] <= multiplier[name, t] * parameter[name, t]` constraints of type `T`, -where `array` is the variable or expression `U` and the right-hand side comes from the -parameter `P`. For `P <: TimeSeriesParameter` only devices that own the time series named in -`get_time_series_names(model)[P]` are constrained. Mirror of -[`add_parameterized_lower_bound_range_constraints`](@ref). +Add `array[name, t] <= rhs[name, t]` constraints of type `T`, where `array` is the variable or +expression `U` and the right-hand side is built from the parameter `P`. The right-hand side +depends on the parameter family: + + - generic `P`: `multiplier[name, t] * parameter[name, t]` + - `P <: EventParameter`: `get_max_active_power(device) * parameter[name, t]` + - `P <: TimeSeriesParameter`: `multiplier[name, t] * parameter_column[t]`, and only devices + that own the time series named in `get_time_series_names(model)[P]` are constrained. + +Mirror of [`add_parameterized_lower_bound_range_constraints`](@ref). """ function add_parameterized_upper_bound_range_constraints( container::OptimizationContainer, From b35aed17ce8094233d303af22d9c27d4a3ee1a1f Mon Sep 17 00:00:00 2001 From: Luke Kiernan Date: Fri, 7 Aug 2026 14:32:37 -0600 Subject: [PATCH 3/3] trim tests and comments --- test/mocks/mock_time_series.jl | 2 - test/test_parameterized_range_constraints.jl | 170 ++++++------------- 2 files changed, 51 insertions(+), 121 deletions(-) diff --git a/test/mocks/mock_time_series.jl b/test/mocks/mock_time_series.jl index 4b94dc6e..56b2b307 100644 --- a/test/mocks/mock_time_series.jl +++ b/test/mocks/mock_time_series.jl @@ -4,8 +4,6 @@ Minimal time series mocks for testing parameter updates. using Dates -# Subtypes IS.TimeSeriesData so it can stand in for a real time series type wherever one is -# required by dispatch (e.g. `TimeSeriesAttributes`), without carrying any of the machinery. struct MockDeterministic <: IS.TimeSeriesData name::String data::Vector{Float64} diff --git a/test/test_parameterized_range_constraints.jl b/test/test_parameterized_range_constraints.jl index 133387ac..92d9cf0a 100644 --- a/test/test_parameterized_range_constraints.jl +++ b/test/test_parameterized_range_constraints.jl @@ -1,9 +1,11 @@ """ Unit tests for the parameterized range constraints (`add_parameterized_lower_bound_range_constraints` / -`add_parameterized_upper_bound_range_constraints`, issue #130). The lower-bound wrapper has -no in-tree caller today — hydro and the other POM formulations only use the upper-bound one -— so these tests are what keeps it honest. +`add_parameterized_upper_bound_range_constraints`). + +The real branching is the parameter family: generic, `EventParameter`, and +`TimeSeriesParameter` dispatch to different `_bound_range_with_parameter!` methods, and the +time series one additionally filters devices, so those carry the bulk of the coverage. """ struct TestValueParameter <: IOM.VariableValueParameter end @@ -11,6 +13,9 @@ struct TestTimeSeriesParameter <: IOM.TimeSeriesParameter end struct TestEventParameter <: IOM.EventParameter end const PARAM_RANGE_TS_NAME = "max_active_power" +const PARAM_RANGE_MULTIPLIER = 0.5 + +param_range_value(name, t) = name == "A" ? 1.0 * t : 10.0 + t function _make_parameterized_range_container(devices, time_steps) mock_sys = MockSystem(100.0) @@ -37,29 +42,21 @@ function _make_parameterized_range_container(devices, time_steps) return container end -"Add a `VariableValueParameter` container with `param[name, t] = value_fn(name, t)`." -function _add_value_parameter!( - container, - ::Type{P}, - ::Type{D}, - names, - time_steps, - value_fn, - multiplier, -) where {P <: IOM.ParameterType, D <: IS.InfrastructureSystemsComponent} +"Add a `TestValueParameter` container with `param[name, t] = param_range_value(name, t)`." +function _add_value_parameter!(container, names, time_steps) param_container = IOM.add_param_container!( container, - P, - D, - IOM.VariableKey(TestVariableType, D), + TestValueParameter, + MockThermalGen, + IOM.VariableKey(TestVariableType, MockThermalGen), names, time_steps, ) param_array = IOM.get_parameter_array(param_container) mult_array = IOM.get_multiplier_array(param_container) for name in names, t in time_steps - param_array[name, t] = value_fn(name, t) - mult_array[name, t] = multiplier + param_array[name, t] = param_range_value(name, t) + mult_array[name, t] = PARAM_RANGE_MULTIPLIER end return param_container end @@ -75,52 +72,42 @@ end TestTimeSeriesParameter => PARAM_RANGE_TS_NAME, ), ) - param_value(name, t) = name == "A" ? 1.0 * t : 10.0 + t - multiplier = 0.5 - @testset "Lower bound over a variable" begin + @testset "Bounds from a variable value parameter" begin container = _make_parameterized_range_container(devices, time_steps) - _add_value_parameter!( - container, - TestValueParameter, - MockThermalGen, - names, - time_steps, - param_value, - multiplier, - ) - add_parameterized_lower_bound_range_constraints( - container, - TestConstraintType, - TestVariableType, - TestValueParameter, - devices, - model, - TestPowerModel, + _add_value_parameter!(container, names, time_steps) + for f in ( + add_parameterized_lower_bound_range_constraints, + add_parameterized_upper_bound_range_constraints, ) - con = IOM.get_constraint(container, TestConstraintType, MockThermalGen, "lb") + f( + container, + TestConstraintType, + TestVariableType, + TestValueParameter, + devices, + model, + TestPowerModel, + ) + end + con_lb = IOM.get_constraint(container, TestConstraintType, MockThermalGen, "lb") + con_ub = IOM.get_constraint(container, TestConstraintType, MockThermalGen, "ub") var = IOM.get_variable(container, TestVariableType, MockThermalGen) - @test axes(con)[1] == names - @test axes(con)[2] == time_steps + # The two directions land in distinct containers under the "lb"/"ub" metas. Sense is + # compile-time dispatch, invariant across names and time steps, so spot-check it + # once; the RHS arithmetic is what varies, so check that elementwise. + @test JuMP.constraint_object(con_lb["A", 1]).set isa MOI.GreaterThan + @test JuMP.constraint_object(con_ub["A", 1]).set isa MOI.LessThan for name in names, t in time_steps - constraint = JuMP.constraint_object(con[name, t]) - @test constraint.set isa MOI.GreaterThan - @test JuMP.normalized_rhs(con[name, t]) ≈ multiplier * param_value(name, t) - @test JuMP.normalized_coefficient(con[name, t], var[name, t]) ≈ 1.0 + @test JuMP.normalized_coefficient(con_lb[name, t], var[name, t]) ≈ 1.0 + @test JuMP.normalized_rhs(con_lb[name, t]) ≈ + PARAM_RANGE_MULTIPLIER * param_range_value(name, t) end end - @testset "Lower bound over an expression" begin + @testset "Exercise expression codepath" begin container = _make_parameterized_range_container(devices, time_steps) - _add_value_parameter!( - container, - TestValueParameter, - MockThermalGen, - names, - time_steps, - param_value, - multiplier, - ) + _add_value_parameter!(container, names, time_steps) add_parameterized_lower_bound_range_constraints( container, TestConstraintType, @@ -132,49 +119,9 @@ end ) con = IOM.get_constraint(container, TestConstraintType, MockThermalGen, "lb") var = IOM.get_variable(container, TestVariableType, MockThermalGen) + # The expression is 2 * var, so the LHS coefficient must be 2, not 1. for name in names, t in time_steps - # The expression is 2 * var, so the LHS coefficient must be 2, not 1. @test JuMP.normalized_coefficient(con[name, t], var[name, t]) ≈ 2.0 - @test JuMP.normalized_rhs(con[name, t]) ≈ multiplier * param_value(name, t) - end - end - - @testset "Lower and upper bounds are mirror images" begin - container = _make_parameterized_range_container(devices, time_steps) - _add_value_parameter!( - container, - TestValueParameter, - MockThermalGen, - names, - time_steps, - param_value, - multiplier, - ) - for f in ( - add_parameterized_lower_bound_range_constraints, - add_parameterized_upper_bound_range_constraints, - ) - f( - container, - TestConstraintType, - TestVariableType, - TestValueParameter, - devices, - model, - TestPowerModel, - ) - end - con_lb = IOM.get_constraint(container, TestConstraintType, MockThermalGen, "lb") - con_ub = IOM.get_constraint(container, TestConstraintType, MockThermalGen, "ub") - # Distinct containers under the "lb"/"ub" metas, same LHS and RHS, opposite senses. - for name in names, t in time_steps - lb = JuMP.constraint_object(con_lb[name, t]) - ub = JuMP.constraint_object(con_ub[name, t]) - @test lb.set isa MOI.GreaterThan - @test ub.set isa MOI.LessThan - @test lb.func == ub.func - @test JuMP.normalized_rhs(con_lb[name, t]) ≈ - JuMP.normalized_rhs(con_ub[name, t]) end end @@ -206,8 +153,8 @@ end for name in ts_names IOM.add_component_name!(attributes, name, uuids[name]) for t in time_steps - param_array[uuids[name], t] = param_value(name, t) - mult_array[name, t] = multiplier + param_array[uuids[name], t] = param_range_value(name, t) + mult_array[name, t] = PARAM_RANGE_MULTIPLIER end end @@ -222,30 +169,15 @@ end ) con = IOM.get_constraint(container, TestConstraintType, MockThermalGen, "lb") @test axes(con)[1] == ts_names - var = IOM.get_variable(container, TestVariableType, MockThermalGen) + # RHS comes from the parameter's column refs rather than the parameter array. for name in ts_names, t in time_steps - @test JuMP.normalized_rhs(con[name, t]) ≈ multiplier * param_value(name, t) - @test JuMP.normalized_coefficient(con[name, t], var[name, t]) ≈ 1.0 + @test JuMP.normalized_rhs(con[name, t]) ≈ + PARAM_RANGE_MULTIPLIER * param_range_value(name, t) end mock_clear_time_series!() end - @testset "Time series parameter with no owning devices builds nothing" begin - mock_clear_time_series!() - container = _make_parameterized_range_container(devices, time_steps) - add_parameterized_lower_bound_range_constraints( - container, - TestConstraintType, - TestVariableType, - TestTimeSeriesParameter, - devices, - model, - TestPowerModel, - ) - @test isempty(IOM.get_constraint_keys(container)) - end - - @testset "Event parameter bounds by the device's max active power" begin + @testset "Event parameter scales by the device's max active power" begin container = _make_parameterized_range_container(devices, time_steps) param_container = IOM.add_param_container!( container, @@ -257,7 +189,7 @@ end ) param_array = IOM.get_parameter_array(param_container) for name in names, t in time_steps - param_array[name, t] = param_value(name, t) + param_array[name, t] = param_range_value(name, t) end add_parameterized_lower_bound_range_constraints( container, @@ -272,7 +204,7 @@ end for (device, name) in zip(devices, names), t in time_steps # The event path scales by the device's max active power, not the multiplier # array (which is left as NaN here). - expected = IOM.get_max_active_power(device) * param_value(name, t) + expected = IOM.get_max_active_power(device) * param_range_value(name, t) @test JuMP.normalized_rhs(con[name, t]) ≈ expected end end