-
Notifications
You must be signed in to change notification settings - Fork 0
add tests for lower bound constraints #149
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -414,7 +414,19 @@ function add_parameterized_bound_range_constraints( | |
| return | ||
| end | ||
|
|
||
| # Backwards-compatible wrappers | ||
| # Direction-specific wrappers over `add_parameterized_bound_range_constraints`. | ||
| """ | ||
| 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]` | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Musing out loud: these three paths are pretty similar, but we're dispatching at a higher level in
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had AI do some of that in moderation. I don't want to go too deep, though: test behavior, not code. |
||
| - `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, | ||
| ::Type{T}, | ||
|
|
@@ -444,6 +456,18 @@ function add_parameterized_lower_bound_range_constraints( | |
| return | ||
| end | ||
|
|
||
| """ | ||
| 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, | ||
| ::Type{T}, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,211 @@ | ||
| """ | ||
| Unit tests for the parameterized range constraints | ||
| (`add_parameterized_lower_bound_range_constraints` / | ||
| `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 | ||
| 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) | ||
| 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 `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, | ||
| 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] = param_range_value(name, t) | ||
| mult_array[name, t] = PARAM_RANGE_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, | ||
| ), | ||
| ) | ||
|
|
||
| @testset "Bounds from a variable value parameter" begin | ||
| container = _make_parameterized_range_container(devices, time_steps) | ||
| _add_value_parameter!(container, names, time_steps) | ||
| 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") | ||
| var = IOM.get_variable(container, TestVariableType, MockThermalGen) | ||
| # 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 | ||
| @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 "Exercise expression codepath" begin | ||
| container = _make_parameterized_range_container(devices, time_steps) | ||
| _add_value_parameter!(container, names, time_steps) | ||
| 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) | ||
| # The expression is 2 * var, so the LHS coefficient must be 2, not 1. | ||
| for name in names, t in time_steps | ||
| @test JuMP.normalized_coefficient(con[name, t], var[name, t]) ≈ 2.0 | ||
| end | ||
| end | ||
|
|
||
| @testset "Time series parameter constrains only devices owning the time series" begin | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This does not check that |
||
| 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_range_value(name, t) | ||
| mult_array[name, t] = PARAM_RANGE_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 | ||
| # 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]) ≈ | ||
| PARAM_RANGE_MULTIPLIER * param_range_value(name, t) | ||
| end | ||
| mock_clear_time_series!() | ||
| end | ||
|
|
||
| @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, | ||
| 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_range_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_range_value(name, t) | ||
| @test JuMP.normalized_rhs(con[name, t]) ≈ expected | ||
| end | ||
| end | ||
| end | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Docstring is identical to the upper_bound version, do they both need it? Could we put just this one docstring on
add_parameterized_bound_range_constraints? Or even further: do we need these helpers? A caller could easily pass the LowerBound() or UpperBound() themselves.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see the deleted comment says "backwards compatible", but that's probably from PSI.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I share your sentiments...but @jd-lara says they're needed and should have tests. See issue #130 for disucssion: after 2 rounds of back-and-forth, I decided to take his word for it, even if it doesn't make sense to me.