Skip to content

Commit 3d0275d

Browse files
constraints: sanitize infinity values (#370)
* constraints: sanitize infinity values * constraints: move check of invalid rhs to constraint assignment
1 parent 40a27f9 commit 3d0275d

File tree

6 files changed

+69
-9
lines changed

6 files changed

+69
-9
lines changed

doc/release_notes.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Upcoming Version
55
----------------
66

77
* When writing out an LP file, large variables and constraints are now chunked to avoid memory issues. This is especially useful for large models with constraints with many terms. The chunk size can be set with the `slice_size` argument in the `solve` function.
8+
* Constraints which of the form `<= infinity` and `>= -infinity` are now automatically filtered out when solving. The `solve` function now has a new argument `sanitize_infinities` to control this feature. Default is set to `True`.
89

910
Version 0.3.15
1011
--------------

linopy/constraints.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,14 @@
5151
to_polars,
5252
)
5353
from linopy.config import options
54-
from linopy.constants import EQUAL, HELPER_DIMS, TERM_DIM, SIGNS_pretty
54+
from linopy.constants import (
55+
EQUAL,
56+
GREATER_EQUAL,
57+
HELPER_DIMS,
58+
LESS_EQUAL,
59+
TERM_DIM,
60+
SIGNS_pretty,
61+
)
5562
from linopy.types import ConstantLike
5663

5764
if TYPE_CHECKING:
@@ -851,17 +858,17 @@ def equalities(self) -> "Constraints":
851858
"""
852859
return self[[n for n, s in self.items() if (s.sign == EQUAL).all()]]
853860

854-
def sanitize_zeros(self):
861+
def sanitize_zeros(self) -> None:
855862
"""
856863
Filter out terms with zero and close-to-zero coefficient.
857864
"""
858-
for name in list(self):
865+
for name in self:
859866
not_zero = abs(self[name].coeffs) > 1e-10
860867
constraint = self[name]
861868
constraint.vars = self[name].vars.where(not_zero, -1)
862869
constraint.coeffs = self[name].coeffs.where(not_zero)
863870

864-
def sanitize_missings(self):
871+
def sanitize_missings(self) -> None:
865872
"""
866873
Set constraints labels to -1 where all variables in the lhs are
867874
missing.
@@ -872,6 +879,19 @@ def sanitize_missings(self):
872879
contains_non_missing, -1
873880
)
874881

882+
def sanitize_infinities(self) -> None:
883+
"""
884+
Replace infinite values in the constraints with a large value.
885+
"""
886+
for name in self:
887+
constraint = self[name]
888+
valid_infinity_values = (
889+
(constraint.sign == LESS_EQUAL) & (constraint.rhs == np.inf)
890+
) | ((constraint.sign == GREATER_EQUAL) & (constraint.rhs == -np.inf))
891+
self[name].data["labels"] = self[name].labels.where(
892+
~valid_infinity_values, -1
893+
)
894+
875895
def get_name_by_label(self, label: Union[int, float]) -> str:
876896
"""
877897
Get the constraint name of the constraint containing the passed label.

linopy/model.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,14 @@
3232
replace_by_map,
3333
to_path,
3434
)
35-
from linopy.constants import HELPER_DIMS, TERM_DIM, ModelStatus, TerminationCondition
35+
from linopy.constants import (
36+
GREATER_EQUAL,
37+
HELPER_DIMS,
38+
LESS_EQUAL,
39+
TERM_DIM,
40+
ModelStatus,
41+
TerminationCondition,
42+
)
3643
from linopy.constraints import AnonymousScalarConstraint, Constraint, Constraints
3744
from linopy.expressions import (
3845
LinearExpression,
@@ -583,6 +590,12 @@ def add_constraints(
583590
f"Invalid type of `lhs` ({type(lhs)}) or invalid combination of `lhs`, `sign` and `rhs`."
584591
)
585592

593+
invalid_infinity_values = (
594+
(data.sign == LESS_EQUAL) & (data.rhs == -np.inf)
595+
) | ((data.sign == GREATER_EQUAL) & (data.rhs == np.inf)) # noqa: F821
596+
if invalid_infinity_values.any():
597+
raise ValueError(f"Constraint {name} contains incorrect infinite values.")
598+
586599
# ensure helper dimensions are not set as coordinates
587600
if drop_dims := set(HELPER_DIMS).intersection(data.coords):
588601
# TODO: add a warning here, routines should be safe against this
@@ -953,6 +966,7 @@ def solve(
953966
keep_files: bool = False,
954967
env: None = None,
955968
sanitize_zeros: bool = True,
969+
sanitize_infinities: bool = True,
956970
slice_size: int = 2_000_000,
957971
remote: None = None,
958972
**solver_options,
@@ -1003,6 +1017,8 @@ def solve(
10031017
Whether to set terms with zero coefficient as missing.
10041018
This will remove unneeded overhead in the lp file writing.
10051019
The default is True.
1020+
sanitize_infinities : bool, optional
1021+
Whether to filter out constraints that are subject to `<= inf` or `>= -inf`.
10061022
slice_size : int, optional
10071023
Size of the slice to use for writing the lp file. The slice size
10081024
is used to split large variables and constraints into smaller
@@ -1083,6 +1099,9 @@ def solve(
10831099
if sanitize_zeros:
10841100
self.constraints.sanitize_zeros()
10851101

1102+
if sanitize_infinities:
1103+
self.constraints.sanitize_infinities()
1104+
10861105
if self.is_quadratic and solver_name not in quadratic_solvers:
10871106
raise ValueError(
10881107
f"Solver {solver_name} does not support quadratic problems."

linopy/solvers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1493,7 +1493,7 @@ def get_solver_solution() -> Solution:
14931493
dual_ = [str(d) for d in m.getConstraint()]
14941494
dual = pd.Series(m.getDual(dual_), index=dual_, dtype=float)
14951495
dual = set_int_index(dual)
1496-
except (xpress.SolverError, SystemError):
1496+
except (xpress.SolverError, xpress.ModelError, SystemError):
14971497
logger.warning("Dual values of MILP couldn't be parsed")
14981498
dual = pd.Series(dtype=float)
14991499

test/test_constraints.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,25 @@ def test_constraints_flat():
181181

182182
assert isinstance(m.constraints.flat, pd.DataFrame)
183183
assert not m.constraints.flat.empty
184+
185+
186+
def test_sanitize_infinities():
187+
m = Model()
188+
189+
lower = xr.DataArray(np.zeros((10, 10)), coords=[range(10), range(10)])
190+
upper = xr.DataArray(np.ones((10, 10)), coords=[range(10), range(10)])
191+
x = m.add_variables(lower, upper, name="x")
192+
y = m.add_variables(name="y")
193+
194+
# Test correct infinities
195+
m.add_constraints(x <= np.inf, name="con_inf")
196+
m.add_constraints(y >= -np.inf, name="con_neg_inf")
197+
m.constraints.sanitize_infinities()
198+
assert (m.constraints["con_inf"].labels == -1).all()
199+
assert (m.constraints["con_neg_inf"].labels == -1).all()
200+
201+
# Test incorrect infinities
202+
with pytest.raises(ValueError):
203+
m.add_constraints(x >= np.inf, name="con_wrong_inf")
204+
with pytest.raises(ValueError):
205+
m.add_constraints(y <= -np.inf, name="con_wrong_neg_inf")

test/test_optimization.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -498,9 +498,7 @@ def test_infeasible_model(model, solver, io_api):
498498
model.compute_infeasibilities()
499499

500500

501-
@pytest.mark.parametrize(
502-
"solver,io_api", [p for p in params if p[0] not in ["glpk", "cplex", "mindopt"]]
503-
)
501+
@pytest.mark.parametrize("solver,io_api", params)
504502
def test_model_with_inf(model_with_inf, solver, io_api):
505503
status, condition = model_with_inf.solve(solver, io_api=io_api)
506504
assert condition == "optimal"

0 commit comments

Comments
 (0)