From 766148bc439e60364a750fd329562defab2dc4f3 Mon Sep 17 00:00:00 2001 From: Thomas Bittar Date: Thu, 7 May 2026 17:42:08 +0200 Subject: [PATCH 1/3] Equality refacto --- src/gems/model/constraint.py | 8 ++++++++ src/gems/simulation/optimization.py | 27 +++++++++++++-------------- uv.lock | 2 +- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/gems/model/constraint.py b/src/gems/model/constraint.py index 889b227e..1f53a924 100644 --- a/src/gems/model/constraint.py +++ b/src/gems/model/constraint.py @@ -75,6 +75,14 @@ def __post_init__( if is_unbounded(self.upper_bound) and not is_non_negative(self.upper_bound): raise ValueError("Upper bound should not be -Inf") + @property + def is_equality(self) -> bool: + return ( + not is_unbounded(self.lower_bound) + and not is_unbounded(self.upper_bound) + and expressions_equal(self.lower_bound, self.upper_bound) + ) + def replicate(self, /, **changes: Any) -> "Constraint": return replace(self, **changes) diff --git a/src/gems/simulation/optimization.py b/src/gems/simulation/optimization.py index b40fc4cb..40265525 100644 --- a/src/gems/simulation/optimization.py +++ b/src/gems/simulation/optimization.py @@ -691,23 +691,22 @@ def _create_constraints_for_model( # Sanitize constraint name for LP format (spaces → underscores) safe_name = constraint.name.replace(" ", "_").replace("-", "_") - # Lower bound constraint: lhs >= lb (if lb != -inf) - if not is_unbounded(constraint.lower_bound): + if constraint.is_equality: lb = visit(constraint.lower_bound, builder) if validity_mask is not None: lb = _apply_validity_mask(lb, validity_mask) - name = f"{prefix}__{safe_name}__lb" - con_lb = lhs >= lb # type: ignore[operator] - self.linopy_model.add_constraints(con_lb, name=name) # type: ignore[arg-type] - - # Upper bound constraint: lhs <= ub (if ub != +inf) - if not is_unbounded(constraint.upper_bound): - ub = visit(constraint.upper_bound, builder) - if validity_mask is not None: - ub = _apply_validity_mask(ub, validity_mask) - name = f"{prefix}__{safe_name}__ub" - con_ub = lhs <= ub # type: ignore[operator] - self.linopy_model.add_constraints(con_ub, name=name) # type: ignore[arg-type] + self.linopy_model.add_constraints(lhs == lb, name=f"{prefix}__{safe_name}__eq") # type: ignore[operator,arg-type] + else: + if not is_unbounded(constraint.lower_bound): + lb = visit(constraint.lower_bound, builder) + if validity_mask is not None: + lb = _apply_validity_mask(lb, validity_mask) + self.linopy_model.add_constraints(lhs >= lb, name=f"{prefix}__{safe_name}__lb") # type: ignore[operator,arg-type] + if not is_unbounded(constraint.upper_bound): + ub = visit(constraint.upper_bound, builder) + if validity_mask is not None: + ub = _apply_validity_mask(ub, validity_mask) + self.linopy_model.add_constraints(lhs <= ub, name=f"{prefix}__{safe_name}__ub") # type: ignore[operator,arg-type] def _add_objectives_for_model( self, diff --git a/uv.lock b/uv.lock index c465b551..44df108c 100644 --- a/uv.lock +++ b/uv.lock @@ -678,7 +678,7 @@ wheels = [ [[package]] name = "gemspy" -version = "0.0.6" +version = "0.1.0" source = { editable = "." } dependencies = [ { name = "antlr4-python3-runtime" }, From 4104aa08d7a2063fce22ffa02985bba4fb72c283 Mon Sep 17 00:00:00 2001 From: Thomas Bittar Date: Thu, 7 May 2026 17:52:50 +0200 Subject: [PATCH 2/3] Add and update tests --- .../test_out_of_bounds_processing.py | 8 ++--- tests/unittests/system/test_model.py | 35 +++++++++++++++++++ 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/tests/e2e/functional/test_out_of_bounds_processing.py b/tests/e2e/functional/test_out_of_bounds_processing.py index 84bd0a89..fad8cebe 100644 --- a/tests/e2e/functional/test_out_of_bounds_processing.py +++ b/tests/e2e/functional/test_out_of_bounds_processing.py @@ -140,7 +140,7 @@ def test_out_of_bounds_processing(study_id: str, expected_objective: float) -> N # Constraints: 2 components (gen_1, gen_2) × 3 timesteps = 6 potential instances each. # # system_cyclic_with_param_in_shift — no drop mode, all instances present: -# is_on_dynamics (lb + ub): 6 each +# is_on_dynamics (eq): 6 # min_up_duration (ub only): 6 # min_down_duration (ub only): 6 # @@ -151,14 +151,12 @@ def test_out_of_bounds_processing(study_id: str, expected_objective: float) -> N # min_down_duration: both have d_min_down=1 → range [0,0] → never dropped → 6 _EXPECTED_CONSTRAINT_COUNTS: Dict[str, Dict[str, int]] = { "system_cyclic_with_param_in_shift": { - f"{_GEN_PREFIX}__is_on_dynamics__lb": 6, - f"{_GEN_PREFIX}__is_on_dynamics__ub": 6, + f"{_GEN_PREFIX}__is_on_dynamics__eq": 6, f"{_GEN_PREFIX}__min_up_duration__ub": 6, f"{_GEN_PREFIX}__min_down_duration__ub": 6, }, "system_drop_with_param_in_shift": { - f"{_GEN_PREFIX}__is_on_dynamics__lb": 4, - f"{_GEN_PREFIX}__is_on_dynamics__ub": 4, + f"{_GEN_PREFIX}__is_on_dynamics__eq": 4, f"{_GEN_PREFIX}__min_up_duration__ub": 5, f"{_GEN_PREFIX}__min_down_duration__ub": 6, }, diff --git a/tests/unittests/system/test_model.py b/tests/unittests/system/test_model.py index 6eb0cbc0..6c4c2241 100644 --- a/tests/unittests/system/test_model.py +++ b/tests/unittests/system/test_model.py @@ -219,6 +219,41 @@ def test_constraint_equals() -> None: ) +def test_is_equality_true_for_equality_comparison() -> None: + c = Constraint(name="c", expression=var("x") == param("p")) + assert c.is_equality is True + + +def test_is_equality_false_for_inequality_comparison() -> None: + assert Constraint(name="c", expression=var("x") <= param("p")).is_equality is False + assert Constraint(name="c", expression=var("x") >= param("p")).is_equality is False + + +def test_is_equality_false_for_range_constraint() -> None: + c = Constraint( + name="c", expression=var("x"), lower_bound=literal(0), upper_bound=literal(10) + ) + assert c.is_equality is False + + +def test_is_equality_true_for_equal_explicit_bounds() -> None: + c = Constraint( + name="c", expression=var("x"), lower_bound=param("p"), upper_bound=param("p") + ) + assert c.is_equality is True + + +def test_is_equality_false_for_one_sided_constraint() -> None: + assert ( + Constraint(name="c", expression=var("x"), lower_bound=literal(0)).is_equality + is False + ) + assert ( + Constraint(name="c", expression=var("x"), upper_bound=literal(0)).is_equality + is False + ) + + # --- Issue #76: tolerate absence of expec() in objective contributions --- From f2f70f69b846f4a270d191fb6b30b1ef924d64c2 Mon Sep 17 00:00:00 2001 From: Thomas Bittar Date: Thu, 7 May 2026 18:05:26 +0200 Subject: [PATCH 3/3] Update reference MPS --- .../13_1/expected_outputs/subproblem.mps | 14 ++++---------- .../studies/13_2/expected_outputs/master.mps | 5 +---- .../13_2/expected_outputs/subproblem.mps | 19 ++++++------------- 3 files changed, 11 insertions(+), 27 deletions(-) diff --git a/tests/e2e/functional/studies/13_1/expected_outputs/subproblem.mps b/tests/e2e/functional/studies/13_1/expected_outputs/subproblem.mps index 48698416..d4b55848 100644 --- a/tests/e2e/functional/studies/13_1/expected_outputs/subproblem.mps +++ b/tests/e2e/functional/studies/13_1/expected_outputs/subproblem.mps @@ -1,30 +1,24 @@ NAME ROWS N Obj - G c0 + E c0 L c1 L c2 - L c3 COLUMNS x0 Obj 1 x0 c0 -1 - x0 c1 -1 x1 Obj 501 x1 c0 1 - x1 c1 1 x2 Obj 45 x2 c0 1 x2 c1 1 - x2 c2 1 - x3 c3 -1 + x3 c2 -1 x4 Obj 10 x4 c0 1 - x4 c1 1 - x4 c3 1 + x4 c2 1 RHS RHS_V c0 400 - RHS_V c1 400 - RHS_V c2 200 + RHS_V c1 200 BOUNDS UP BOUND x0 1000000 UP BOUND x1 1000000 diff --git a/tests/e2e/functional/studies/13_2/expected_outputs/master.mps b/tests/e2e/functional/studies/13_2/expected_outputs/master.mps index 697a9563..3e53b487 100644 --- a/tests/e2e/functional/studies/13_2/expected_outputs/master.mps +++ b/tests/e2e/functional/studies/13_2/expected_outputs/master.mps @@ -1,16 +1,13 @@ NAME ROWS N Obj - G c0 - L c1 + E c0 COLUMNS x0 Obj 490 x1 Obj 200 x1 c0 1 - x1 c1 1 MARK0000 'MARKER' 'INTORG' x2 c0 -10 - x2 c1 -10 MARK0001 'MARKER' 'INTEND' RHS BOUNDS diff --git a/tests/e2e/functional/studies/13_2/expected_outputs/subproblem.mps b/tests/e2e/functional/studies/13_2/expected_outputs/subproblem.mps index 7d5ce1f2..318fcdc8 100644 --- a/tests/e2e/functional/studies/13_2/expected_outputs/subproblem.mps +++ b/tests/e2e/functional/studies/13_2/expected_outputs/subproblem.mps @@ -1,36 +1,29 @@ NAME ROWS N Obj - G c0 + E c0 L c1 L c2 L c3 - L c4 COLUMNS x0 Obj 1 x0 c0 -1 - x0 c1 -1 x1 Obj 501 x1 c0 1 - x1 c1 1 x2 Obj 45 x2 c0 1 x2 c1 1 - x2 c2 1 - x3 c3 -1 + x3 c2 -1 x4 Obj 10 x4 c0 1 - x4 c1 1 - x4 c3 1 - x5 c4 -1 + x4 c2 1 + x5 c3 -1 x6 Obj 10 x6 c0 1 - x6 c1 1 - x6 c4 1 + x6 c3 1 RHS RHS_V c0 400 - RHS_V c1 400 - RHS_V c2 200 + RHS_V c1 200 BOUNDS UP BOUND x0 1000000 UP BOUND x1 1000000