Skip to content

add tests for lower bound constraints - #149

Merged
jd-lara merged 3 commits into
mainfrom
lk/issue-130
Aug 15, 2026
Merged

add tests for lower bound constraints#149
jd-lara merged 3 commits into
mainfrom
lk/issue-130

Conversation

@luke-kiernan

Copy link
Copy Markdown
Collaborator

Address issue #130. Add tests for the lower-bound parameterized range-constraint helpers. (One of these functions--add_parameterized_lower_bound_range_constraints--looks like dead code to me, but I left it in and gave it a test anyway. See the issue for discussion.)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds unit-test coverage for the lower-bound parameterized range-constraint helpers discussed in issue #130, ensuring the lower-bound wrapper remains correct despite having no in-tree callers today.

Changes:

  • Add a comprehensive new test file covering lower/upper bound symmetry, variable vs expression LHS, time-series filtering by ownership, and event-parameter behavior.
  • Update mocks to support time-series ownership checks (IS.has_time_series) and to correctly extend IOM.get_max_active_power for mock devices.
  • Export add_parameterized_lower_bound_range_constraints and add docstrings to both direction-specific wrappers.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
test/verify_mocks.jl Makes the mock verification use IOM.get_max_active_power explicitly.
test/test_parameterized_range_constraints.jl New unit tests covering lower-bound parameterized range constraints and related edge cases.
test/mocks/mock_time_series.jl Makes MockDeterministic a IS.TimeSeriesData subtype and adds a registry-backed IS.has_time_series for mocks.
test/mocks/mock_components.jl Defines IOM.get_max_active_power methods for mock devices used by event-parameter bounds.
test/InfrastructureOptimizationModelsTests.jl Includes the new parameterized range-constraints test file in the unit test suite.
src/InfrastructureOptimizationModels.jl Exports add_parameterized_lower_bound_range_constraints.
src/common_models/range_constraint.jl Adds docstrings for lower/upper bound wrappers.
Suppressed comments (1)

src/common_models/range_constraint.jl:458

  • Same issue as the lower-bound wrapper: for P <: EventParameter the RHS is not multiplier[name, t] * parameter[name, t] (it is scaled by get_max_active_power(device)), so the docstring is misleading for event-parameter bounds.
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 Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +418 to +424
"""
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).
"""
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Performance Results
Main

Network: 10 nodes, 13 edges, 3 cost segments
Generators: 5, Demands: 5
Loss coefficients (a, b, c) per generator:
  n1: a=0.007287  b=0.005968  c=0.003850
  n2: a=0.001094  b=0.009002  c=0.007028
  n3: a=0.007897  b=0.008552  c=0.003001
  n4: a=0.008179  b=0.002530  c=0.009637
  n5: a=0.009778  b=0.009934  c=0.009099

Solver logs: /home/runner/work/InfrastructureOptimizationModels.jl/InfrastructureOptimizationModels.jl/main/test/performance/logs/solver_2026-08-07T20-48-41.log

==============================================================================================================================================
Bilinear Approximation Benchmarks
  Refinement = depth for all methods
==============================================================================================================================================
Method          R   Vars Constrs   Bins    Objective   Gap(%) MIPGap(%)     LowerBnd  rmse δbi   max δbi   rmse δq    max δq  build_t  solve_t
----------------------------------------------------------------------------------------------------------------------------------------------
NLP (Ipopt)     -     40     105      0     0.956760        -         -            -  0.00e+00  0.00e+00  0.00e+00  0.00e+00   0.0313   0.0047

NLP (Uno)       -     40     105      0     0.956760   0.0000         -            -  0.00e+00  0.00e+00  0.00e+00  0.00e+00   0.0011   0.0011

Bin2+sSOS       4    190     535      0     1.180700  23.4061    0.0000     1.180700  8.07e-02  1.41e-01  2.49e+01  5.48e+01   0.0050   1.7647
Bin2+sSOS       6    250     655      0     1.098175  14.7806    0.0000     1.098175  1.26e-01  2.86e-01  5.47e+00  9.07e+00   0.0056   7.3530
Bin2+sSOS       8    310     775      0     1.053069  10.0661    0.0088     1.052976  8.62e-02  2.43e-01  4.18e+01  9.32e+01   0.0055  26.0381

Bin2+mSOS       4    310     805    120     1.180700  23.4061    0.0000     1.180700  8.07e-02  1.41e-01  2.49e+01  5.48e+01   0.0022   1.7227
Bin2+mSOS       6    430    1045    180     1.098175  14.7806    0.0092     1.098074  1.26e-01  2.86e-01  5.47e+00  9.07e+00   0.0024   7.3328
Bin2+mSOS       8    550    1285    240     1.053069  10.0661    0.0045     1.053021  8.62e-02  2.43e-01  4.18e+01  9.32e+01   0.0025  17.6154

Bin2+Saw        4    310    1075    120     0.985823   3.0376    0.0000     0.985823  1.08e-01  2.49e-01  1.11e+02  2.44e+02   0.0030   5.7259
Bin2+Saw        6    430    1495    180     0.958677   0.2004    0.0077     0.958603  7.52e-02  1.68e-01  8.94e+01  1.71e+02   0.0036  13.1055
Bin2+Saw        8    550    1915    240     0.956996   0.0247    0.0072     0.956927  7.52e-02  1.68e-01  1.37e+02  2.09e+02   0.0043  49.3052

HybS+sSOS       4    310    1165      0     0.812261  15.1030    0.0000     0.812261  5.52e-01  1.00e+00  2.04e+02  2.97e+02   0.0067   2.6347
HybS+sSOS       6    410    1545      0     0.891208   6.8515    0.0000     0.891208  5.49e-01  1.00e+00  3.69e+02  7.09e+02   0.0071   4.8684
HybS+sSOS       8    510    1925      0     0.934789   2.2965    0.0087     0.934707  5.48e-01  1.00e+00  3.47e+02  5.22e+02   0.0074  15.3260

HybS+mSOS       4    390    1345     80     0.812261  15.1030    0.0046     0.812224  5.52e-01  1.00e+00  2.04e+02  2.97e+02   0.0032   2.8535
HybS+mSOS       6    530    1805    120     0.891208   6.8515    0.0000     0.891208  5.49e-01  1.00e+00  3.69e+02  7.09e+02   0.0038   6.1295
HybS+mSOS       8    670    2265    160     0.934789   2.2965    0.0047     0.934744  5.48e-01  1.00e+00  3.47e+02  5.22e+02   0.0043  15.9626

HybS+Saw        4    390    1525     80     0.951869   0.5113    0.0021     0.951849  5.48e-01  1.00e+00  2.16e+03  4.23e+03   0.0037  13.3213
HybS+Saw        6    530    2105    120     0.956386   0.0391    0.0099     0.956291  5.48e-01  1.00e+00  4.57e+02  7.57e+02   0.0045  32.9787
HybS+Saw        8    670    2685    160     0.956734   0.0027    0.0014     0.956721  5.48e-01  1.00e+00  6.08e+00  1.07e+01   0.0054 105.5036

DNMDT           4    395    1640     80     0.954878   0.1967    0.0004     0.954874  1.39e-03  3.13e-03  4.70e-04  1.05e-03   0.0030   3.8212
DNMDT           6    550    2315    120     0.956636   0.0130    0.0098     0.956541  6.75e-05  1.57e-04  4.26e+03  9.54e+03   0.0035  16.9310
DNMDT           8    705    2990    160     0.956754   0.0007    0.0098     0.956660  4.62e-06  1.14e-05  9.42e+01  1.49e+02   0.0038  48.2835

==============================================================================================================================================

This branch

Network: 10 nodes, 13 edges, 3 cost segments
Generators: 5, Demands: 5
Loss coefficients (a, b, c) per generator:
  n1: a=0.007287  b=0.005968  c=0.003850
  n2: a=0.001094  b=0.009002  c=0.007028
  n3: a=0.007897  b=0.008552  c=0.003001
  n4: a=0.008179  b=0.002530  c=0.009637
  n5: a=0.009778  b=0.009934  c=0.009099

Solver logs: /home/runner/work/InfrastructureOptimizationModels.jl/InfrastructureOptimizationModels.jl/branch/test/performance/logs/solver_2026-08-07T20-56-36.log

==============================================================================================================================================
Bilinear Approximation Benchmarks
  Refinement = depth for all methods
==============================================================================================================================================
Method          R   Vars Constrs   Bins    Objective   Gap(%) MIPGap(%)     LowerBnd  rmse δbi   max δbi   rmse δq    max δq  build_t  solve_t
----------------------------------------------------------------------------------------------------------------------------------------------
NLP (Ipopt)     -     40     105      0     0.956760        -         -            -  0.00e+00  0.00e+00  0.00e+00  0.00e+00   0.0322   0.0049

NLP (Uno)       -     40     105      0     0.956760   0.0000         -            -  0.00e+00  0.00e+00  0.00e+00  0.00e+00   0.0013   0.0012

Bin2+sSOS       4    190     535      0     1.180700  23.4061    0.0000     1.180700  8.07e-02  1.41e-01  2.49e+01  5.48e+01   0.0054   1.7779
Bin2+sSOS       6    250     655      0     1.098175  14.7806    0.0000     1.098175  1.26e-01  2.86e-01  5.47e+00  9.07e+00   0.0056   7.3890
Bin2+sSOS       8    310     775      0     1.053069  10.0661    0.0088     1.052976  8.62e-02  2.43e-01  4.18e+01  9.32e+01   0.0056  26.2442

Bin2+mSOS       4    310     805    120     1.180700  23.4061    0.0000     1.180700  8.07e-02  1.41e-01  2.49e+01  5.48e+01   0.0022   1.7355
Bin2+mSOS       6    430    1045    180     1.098175  14.7806    0.0092     1.098074  1.26e-01  2.86e-01  5.47e+00  9.07e+00   0.0024   7.3743
Bin2+mSOS       8    550    1285    240     1.053069  10.0661    0.0045     1.053021  8.62e-02  2.43e-01  4.18e+01  9.32e+01   0.0025  17.6584

Bin2+Saw        4    310    1075    120     0.985823   3.0376    0.0000     0.985823  1.08e-01  2.49e-01  1.11e+02  2.44e+02   0.0031   5.7355
Bin2+Saw        6    430    1495    180     0.958677   0.2004    0.0077     0.958603  7.52e-02  1.68e-01  8.94e+01  1.71e+02   0.0036  13.0972
Bin2+Saw        8    550    1915    240     0.956996   0.0247    0.0072     0.956927  7.52e-02  1.68e-01  1.37e+02  2.09e+02   0.0043  49.2751

HybS+sSOS       4    310    1165      0     0.812261  15.1030    0.0000     0.812261  5.52e-01  1.00e+00  2.04e+02  2.97e+02   0.0067   2.6292
HybS+sSOS       6    410    1545      0     0.891208   6.8515    0.0000     0.891208  5.49e-01  1.00e+00  3.69e+02  7.09e+02   0.0073   4.8453
HybS+sSOS       8    510    1925      0     0.934789   2.2965    0.0087     0.934707  5.48e-01  1.00e+00  3.47e+02  5.22e+02   0.0075  15.3307

HybS+mSOS       4    390    1345     80     0.812261  15.1030    0.0046     0.812224  5.52e-01  1.00e+00  2.04e+02  2.97e+02   0.0033   2.8550
HybS+mSOS       6    530    1805    120     0.891208   6.8515    0.0000     0.891208  5.49e-01  1.00e+00  3.69e+02  7.09e+02   0.0038   6.1337
HybS+mSOS       8    670    2265    160     0.934789   2.2965    0.0047     0.934744  5.48e-01  1.00e+00  3.47e+02  5.22e+02   0.0171  16.1209

HybS+Saw        4    390    1525     80     0.951869   0.5113    0.0021     0.951849  5.48e-01  1.00e+00  2.16e+03  4.23e+03   0.0037  13.3496
HybS+Saw        6    530    2105    120     0.956386   0.0391    0.0099     0.956291  5.48e-01  1.00e+00  4.57e+02  7.57e+02   0.0045  33.0567
HybS+Saw        8    670    2685    160     0.956734   0.0027    0.0014     0.956721  5.48e-01  1.00e+00  6.08e+00  1.07e+01   0.0055 105.7733

DNMDT           4    395    1640     80     0.954878   0.1967    0.0004     0.954874  1.39e-03  3.13e-03  4.70e-04  1.05e-03   0.0031   3.8219
DNMDT           6    550    2315    120     0.956636   0.0130    0.0098     0.956541  6.75e-05  1.57e-04  4.26e+03  9.54e+03   0.0035  16.9586
DNMDT           8    705    2990    160     0.956754   0.0007    0.0098     0.956660  4.62e-06  1.14e-05  9.42e+01  1.49e+02   0.0038  48.2452

==============================================================================================================================================

@luke-kiernan luke-kiernan linked an issue Aug 5, 2026 that may be closed by this pull request
# 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

Copy link
Copy Markdown
Member

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.

Copy link
Copy Markdown
Member

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.

@luke-kiernan luke-kiernan Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or even further: do we need these helpers? A caller could easily pass the LowerBound() or UpperBound() themselves.

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.

Comment thread test/mocks/mock_time_series.jl Outdated
Comment thread test/test_parameterized_range_constraints.jl Outdated
Comment thread test/test_parameterized_range_constraints.jl Outdated
Comment thread test/test_parameterized_range_constraints.jl Outdated
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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think maybe an informative comment here like # Test that we are exercising expression path

But IMO it feels pretty silly to be testing both variable and expression paths. In the code it's just a middle layer that goes either get_variable or get_expression and then passes array to an explicitly unified helper.

(And in line with my musings above, would be a lot simpler if we went straight to the unified helper and dispatched at a smaller level: get_array(container_type))

end
end

@testset "Lower and upper bounds are mirror images" begin

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also feels like a silly test, especially since all the helpers are direction-agnostic until you get to the very last _make_bound_constraint which does <= or >= explicitly. So this feels like it's testing 1) did we remember to pass direction 2) are those final helpers pointing the right way.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And I guess this is part of a broader question, do we want tests for the sake of absolutely ensuring coverage, or do we want to be pruning our tests for importance. I lean towards the latter.

end
end

@testset "Time series parameter constrains only devices owning the time series" begin

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not check that "B" does not have any constraints. Which is fine I think, like I'm not sure we need to test that. But then 1) this test's name is weird and 2) then it's redundant to the other tests, right?

Comment thread test/test_parameterized_range_constraints.jl Outdated
@test isempty(IOM.get_constraint_keys(container))
end

@testset "Event parameter bounds by the device's max active power" begin

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also random thought: I've seen other Julia codebases have much more normalized test names, something like bounds.parameters.events , which makes searching for tests easier (and also works quite nicely with ReTest...)

@luke-kiernan

luke-kiernan commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed most of the comments. I cut the tests down some: they still feel mildly overbuilt, but I don't think it's worth spending more time on them.

@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@jd-lara
jd-lara merged commit 5b783aa into main Aug 15, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dead code: lower-bound parameterized range-constraint helpers

5 participants