From 7a68636dfa06e14a4a66fd3f765f8fab14b74163 Mon Sep 17 00:00:00 2001 From: Robbie Muir Date: Tue, 23 Dec 2025 18:05:48 +0100 Subject: [PATCH 01/15] allow constant in objective cost function --- linopy/expressions.py | 3 ++ linopy/model.py | 57 ++++++++++++++++++++++++++++++- linopy/objective.py | 3 -- test/test_linear_expression.py | 15 ++++++++ test/test_optimization.py | 14 ++++++++ test/test_quadratic_expression.py | 14 ++++++++ 6 files changed, 102 insertions(+), 4 deletions(-) diff --git a/linopy/expressions.py b/linopy/expressions.py index 10e243de..008f5b82 100644 --- a/linopy/expressions.py +++ b/linopy/expressions.py @@ -1097,6 +1097,9 @@ def empty(self) -> EmptyDeprecationWrapper: """ return EmptyDeprecationWrapper(not self.size) + def drop_constant(self: GenericExpression) -> GenericExpression: + return self - self.const # type: ignore + def densify_terms(self: GenericExpression) -> GenericExpression: """ Move all non-zero term entries to the front and cut off all-zero diff --git a/linopy/model.py b/linopy/model.py index 81c069ab..e313325b 100644 --- a/linopy/model.py +++ b/linopy/model.py @@ -10,9 +10,11 @@ import os import re from collections.abc import Callable, Mapping, Sequence +from functools import wraps from pathlib import Path from tempfile import NamedTemporaryFile, gettempdir -from typing import Any, Literal, overload +from typing import Any, Literal, ParamSpec, TypeVar, overload +from warnings import warn import numpy as np import pandas as pd @@ -77,6 +79,58 @@ logger = logging.getLogger(__name__) +P = ParamSpec("P") +R = TypeVar("R") + + +class ConstantInObjectiveWarning(UserWarning): ... + + +def strip_and_replace_constant_objective(func: Callable[P, R]) -> Callable[P, R]: + """ + Decorates a Model instance method. + + If the model objective contains a constant term, this decorator will: + - Remove the constant term from the model objective + - Call the decorated method + - Add the constant term back to the model objective + """ + + @wraps(func) + def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: + assert args, "Expected at least one argument (self)" + self = args[0] + assert isinstance(self, Model), ( + f"First argument must be a Model instance, got {type(self)}" + ) + if not bool(self.objective.expression.has_constant): + return func(*args, **kwargs) + + warn( + "The objective function contains a constant term. This will be temporarily removed", + ConstantInObjectiveWarning, + ) + + # Modify the model objective to drop the constant term + model = self + constant = self.objective.expression.const + model.objective.expression = self.objective.expression.drop_constant() + args = (model, *args[1:]) # type: ignore + result = func(*args, **kwargs) + + # Re-add the constant term + model.objective.expression = model.objective.expression + constant + if model.objective.value is not None: + model.objective.set_value(model.objective.value + constant) + warn( + "The objective function constant term has been re-added to the result", + ConstantInObjectiveWarning, + ) + return result + + return wrapper + + class Model: """ Linear optimization model. @@ -1107,6 +1161,7 @@ def get_problem_file( ) as f: return Path(f.name) + @strip_and_replace_constant_objective def solve( self, solver_name: str | None = None, diff --git a/linopy/objective.py b/linopy/objective.py index b1449270..7d0daa95 100644 --- a/linopy/objective.py +++ b/linopy/objective.py @@ -189,9 +189,6 @@ def expression( if len(expr.coord_dims): expr = expr.sum() - if (expr.const != 0.0) and not np.isnan(expr.const): - raise ValueError("Constant values in objective function not supported.") - self._expression = expr @property diff --git a/test/test_linear_expression.py b/test/test_linear_expression.py index a75ace3f..dfed5f7c 100644 --- a/test/test_linear_expression.py +++ b/test/test_linear_expression.py @@ -1230,6 +1230,21 @@ def test_cumsum(m: Model, multiple: float) -> None: cumsum.nterm == 2 +def test_drop_constant(x: Variable) -> None: + """Test that constants are removed""" + expr_a = 2 * x + expr_b = expr_a + [1, 2] + expr_c = expr_b + float("nan") + for expr in [expr_a, expr_b, expr_c]: + expr = 2 * x + 10 + expr_2 = expr.drop_constant() + + assert all(expr_2.const.values == 0.0), ( + f"Expected constant 0.0, got {expr_2.const.values}" + ) + assert not bool(expr_2.has_constant) + + def test_simplify_basic(x: Variable) -> None: """Test basic simplification with duplicate terms.""" expr = 2 * x + 3 * x + 1 * x diff --git a/test/test_optimization.py b/test/test_optimization.py index 12399a4e..416136c6 100644 --- a/test/test_optimization.py +++ b/test/test_optimization.py @@ -20,6 +20,7 @@ from linopy import GREATER_EQUAL, LESS_EQUAL, Model, solvers from linopy.common import to_path from linopy.expressions import LinearExpression +from linopy.model import ConstantInObjectiveWarning from linopy.solver_capabilities import ( SolverFeature, get_available_solvers_with_feature, @@ -955,6 +956,19 @@ def test_model_resolve( assert np.isclose(model.objective.value or 0, 5.25) +def test_model_with_constant_in_objective(model: Model) -> None: + objective = model.objective.expression + 1 + model.add_objective(expr=objective, overwrite=True) + + with pytest.warns(ConstantInObjectiveWarning): + status, _ = model.solve(solver_name="highs") + assert status == "ok" + # x = -0.1, y = 1.7 + assert model.objective.value == 4.3 + assert model.objective.expression.const == 1 + assert model.objective.expression.solution == 4.3 + + @pytest.mark.parametrize( "solver,io_api,explicit_coordinate_names", [p for p in params if "direct" not in p] ) diff --git a/test/test_quadratic_expression.py b/test/test_quadratic_expression.py index fc1bb25f..a1904e24 100644 --- a/test/test_quadratic_expression.py +++ b/test/test_quadratic_expression.py @@ -312,6 +312,20 @@ def test_quadratic_expression_constant_to_polars() -> None: assert all(arr.to_numpy() == df["const"].to_numpy()) +def test_drop_constant(x: Variable) -> None: + """Test that constants are removed""" + expr_a = 2 * x * x + expr_b = expr_a + 1 + for expr in [expr_a, expr_b]: + expr = 2 * x + 10 + expr_2 = expr.drop_constant() + + assert all(expr_2.const.values == 0.0), ( + f"Expected constant 0.0, got {expr_2.const.values}" + ) + assert not bool(expr_2.has_constant) + + def test_quadratic_expression_to_matrix(model: Model, x: Variable, y: Variable) -> None: expr: QuadraticExpression = x * y + x + 5 # type: ignore From f51b340482f7d1d5571e7865b61204e5271bc737 Mon Sep 17 00:00:00 2001 From: Robbie Muir Date: Tue, 23 Dec 2025 18:07:10 +0100 Subject: [PATCH 02/15] update docs --- doc/release_notes.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 730d3e04..1583a4fd 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -3,6 +3,7 @@ Release Notes .. Upcoming Version +* Allow constant values in objective cost function * Add support for SOS1 and SOS2 (Special Ordered Sets) constraints via ``Model.add_sos_constraints()`` and ``Model.remove_sos_constraints()`` * Add simplify method to LinearExpression to combine duplicate terms * Add convenience function to create LinearExpression from constant From a7522d6b634768b1a91dead9041bb38bd63c6066 Mon Sep 17 00:00:00 2001 From: Robbie Muir Date: Tue, 23 Dec 2025 18:17:33 +0100 Subject: [PATCH 03/15] fix tests --- linopy/model.py | 10 ++++++++-- test/test_model.py | 5 ++--- test/test_objective.py | 3 +-- test/test_optimization.py | 31 ++++++++++++++++++++++++++++++- 4 files changed, 41 insertions(+), 8 deletions(-) diff --git a/linopy/model.py b/linopy/model.py index e313325b..d7f3cef9 100644 --- a/linopy/model.py +++ b/linopy/model.py @@ -116,9 +116,15 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: constant = self.objective.expression.const model.objective.expression = self.objective.expression.drop_constant() args = (model, *args[1:]) # type: ignore - result = func(*args, **kwargs) - # Re-add the constant term + try: + result = func(*args, **kwargs) + except Exception as e: + # Even if there is an exception, make sure the model returns to it's original state + model.objective.expression = model.objective.expression + constant + raise e + + # Re-add the constant term to return the model objective to the original expression model.objective.expression = model.objective.expression + constant if model.objective.value is not None: model.objective.set_value(model.objective.value + constant) diff --git a/test/test_model.py b/test/test_model.py index c363fe4c..063b70dd 100644 --- a/test/test_model.py +++ b/test/test_model.py @@ -82,9 +82,8 @@ def test_objective() -> None: assert m.objectiverange.min() == 2 assert m.objectiverange.max() == 2 - # test objective with constant which is not supported - with pytest.raises(ValueError): - m.objective = m.objective + 3 + # test objective with constant which is supported + m.objective = m.objective + 3 def test_remove_variable() -> None: diff --git a/test/test_objective.py b/test/test_objective.py index d869175a..80b2021a 100644 --- a/test/test_objective.py +++ b/test/test_objective.py @@ -192,5 +192,4 @@ def test_repr(linear_objective: Objective, quadratic_objective: Objective) -> No def test_objective_constant() -> None: m = Model() linear_expr = LinearExpression(None, m) + 1 - with pytest.raises(ValueError): - m.objective = Objective(linear_expr, m) + m.objective = Objective(linear_expr, m) diff --git a/test/test_optimization.py b/test/test_optimization.py index 416136c6..ec4e653a 100644 --- a/test/test_optimization.py +++ b/test/test_optimization.py @@ -956,7 +956,7 @@ def test_model_resolve( assert np.isclose(model.objective.value or 0, 5.25) -def test_model_with_constant_in_objective(model: Model) -> None: +def test_model_with_constant_in_objective_feasible(model: Model) -> None: objective = model.objective.expression + 1 model.add_objective(expr=objective, overwrite=True) @@ -969,6 +969,35 @@ def test_model_with_constant_in_objective(model: Model) -> None: assert model.objective.expression.solution == 4.3 +def test_model_with_constant_in_objective_infeasible(model: Model) -> None: + objective = model.objective.expression + 1 + model.add_objective(expr=objective, overwrite=True) + model.add_constraints([(1, "x")], "<=", 0) + model.add_constraints([(1, "y")], "<=", 0) + + with pytest.warns(ConstantInObjectiveWarning): + status, condition = model.solve(solver_name="highs") + + assert condition == "infeasible" + # Even though the problem was not solved, the constant term should still be accessible + assert model.objective.expression.const == 1 + + +def test_model_with_constant_in_objective_error(model: Model) -> None: + objective = model.objective.expression + 1 + model.add_objective(expr=objective, overwrite=True) + model.add_constraints([(1, "x")], "<=", 0) + model.add_constraints([(1, "y")], "<=", 0) + + try: + status, condition = model.solve(solver_name="apples") + except AssertionError: + pass + + # Even if something goes wrong, the model objective should return to the correct state + assert model.objective.expression.const == 1 + + @pytest.mark.parametrize( "solver,io_api,explicit_coordinate_names", [p for p in params if "direct" not in p] ) From 14b603c034326f321bf7a085cac424e055feafb8 Mon Sep 17 00:00:00 2001 From: Robbie Muir Date: Tue, 23 Dec 2025 18:28:24 +0100 Subject: [PATCH 04/15] fix mypy --- linopy/model.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/linopy/model.py b/linopy/model.py index d7f3cef9..0aac9ebb 100644 --- a/linopy/model.py +++ b/linopy/model.py @@ -104,6 +104,7 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: f"First argument must be a Model instance, got {type(self)}" ) if not bool(self.objective.expression.has_constant): + # Continue as normal if there is no constant term return func(*args, **kwargs) warn( @@ -113,7 +114,7 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: # Modify the model objective to drop the constant term model = self - constant = self.objective.expression.const + constant = float(self.objective.expression.const.values) model.objective.expression = self.objective.expression.drop_constant() args = (model, *args[1:]) # type: ignore From 14497bf21022bd9132a6a7efb386c7cc4ab9a8c5 Mon Sep 17 00:00:00 2001 From: Robbie Muir Date: Thu, 25 Dec 2025 15:57:22 +0100 Subject: [PATCH 05/15] made objective constnat logic more strict --- linopy/model.py | 42 +++++++++++++++++++++++++++++---------- linopy/objective.py | 7 +++++++ test/test_optimization.py | 16 ++++++++++----- 3 files changed, 50 insertions(+), 15 deletions(-) diff --git a/linopy/model.py b/linopy/model.py index 0aac9ebb..fec3855f 100644 --- a/linopy/model.py +++ b/linopy/model.py @@ -86,6 +86,9 @@ class ConstantInObjectiveWarning(UserWarning): ... +class ConstantObjectiveError(Exception): ... + + def strip_and_replace_constant_objective(func: Callable[P, R]) -> Callable[P, R]: """ Decorates a Model instance method. @@ -103,14 +106,16 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: assert isinstance(self, Model), ( f"First argument must be a Model instance, got {type(self)}" ) - if not bool(self.objective.expression.has_constant): + model = self + if not self.objective.has_constant: # Continue as normal if there is no constant term return func(*args, **kwargs) - warn( - "The objective function contains a constant term. This will be temporarily removed", - ConstantInObjectiveWarning, - ) + # The objective contains a constant term + if not model.allow_constant_objective: + raise ConstantObjectiveError( + "Objective function contains constant terms. Please use LinearExpression.drop_constants()/QuadraticExpression.drop_constants() or set Model.allow_constant_objective=True." + ) # Modify the model objective to drop the constant term model = self @@ -129,10 +134,7 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: model.objective.expression = model.objective.expression + constant if model.objective.value is not None: model.objective.set_value(model.objective.value + constant) - warn( - "The objective function constant term has been re-added to the result", - ConstantInObjectiveWarning, - ) + return result return wrapper @@ -164,6 +166,7 @@ class Model: _dual: Dataset _status: str _termination_condition: str + _allow_constant_objective: bool _xCounter: int _cCounter: int _varnameCounter: int @@ -185,6 +188,7 @@ class Model: # hidden attributes "_status", "_termination_condition", + "_allow_constant_objective", # TODO: move counters to Variables and Constraints class "_xCounter", "_cCounter", @@ -236,6 +240,7 @@ def __init__( self._status: str = "initialized" self._termination_condition: str = "" + self._allow_constant_objective: bool = False self._xCounter: int = 0 self._cCounter: int = 0 self._varnameCounter: int = 0 @@ -788,6 +793,17 @@ def add_constraints( self.constraints.add(constraint) return constraint + @property + def allow_constant_objective(self) -> bool: + """ + Whether constant terms in the objective function are allowed. + """ + return self._allow_constant_objective + + @allow_constant_objective.setter + def allow_constant_objective(self, allow: bool) -> None: + self._allow_constant_objective = allow + def add_objective( self, expr: Variable @@ -809,7 +825,7 @@ def add_objective( Returns ------- - linopy.LinearExpression + linopy.LinearExpression, linopy.QuadraticExpression The objective function assigned to the model. """ if not overwrite: @@ -819,8 +835,14 @@ def add_objective( ) if isinstance(expr, Variable): expr = 1 * expr + self.objective.expression = expr self.objective.sense = sense + if not self.allow_constant_objective and self.objective.has_constant: + warn( + "Objective function contains constant terms but this is not allowed as Model.allow_constant_objective=False, running solve will result in an error. Please either remove constants from the expression with expr.drop_constants() or set Model.allow_constant_objective=True.", + ConstantInObjectiveWarning, + ) def remove_variables(self, name: str) -> None: """ diff --git a/linopy/objective.py b/linopy/objective.py index 7d0daa95..11e00b49 100644 --- a/linopy/objective.py +++ b/linopy/objective.py @@ -191,6 +191,13 @@ def expression( self._expression = expr + @property + def has_constant(self) -> bool: + """ + Returns whether the objective has a constant term. + """ + return bool(self.expression.has_constant) + @property def model(self) -> Model: """ diff --git a/test/test_optimization.py b/test/test_optimization.py index ec4e653a..4d0cbc06 100644 --- a/test/test_optimization.py +++ b/test/test_optimization.py @@ -20,7 +20,7 @@ from linopy import GREATER_EQUAL, LESS_EQUAL, Model, solvers from linopy.common import to_path from linopy.expressions import LinearExpression -from linopy.model import ConstantInObjectiveWarning +from linopy.model import ConstantInObjectiveWarning, ConstantObjectiveError from linopy.solver_capabilities import ( SolverFeature, get_available_solvers_with_feature, @@ -958,10 +958,15 @@ def test_model_resolve( def test_model_with_constant_in_objective_feasible(model: Model) -> None: objective = model.objective.expression + 1 - model.add_objective(expr=objective, overwrite=True) with pytest.warns(ConstantInObjectiveWarning): + model.add_objective(expr=objective, overwrite=True) + + with pytest.raises(ConstantObjectiveError): status, _ = model.solve(solver_name="highs") + + model.allow_constant_objective = True + status, _ = model.solve(solver_name="highs") assert status == "ok" # x = -0.1, y = 1.7 assert model.objective.value == 4.3 @@ -975,8 +980,8 @@ def test_model_with_constant_in_objective_infeasible(model: Model) -> None: model.add_constraints([(1, "x")], "<=", 0) model.add_constraints([(1, "y")], "<=", 0) - with pytest.warns(ConstantInObjectiveWarning): - status, condition = model.solve(solver_name="highs") + model.allow_constant_objective = True + _, condition = model.solve(solver_name="highs") assert condition == "infeasible" # Even though the problem was not solved, the constant term should still be accessible @@ -985,12 +990,13 @@ def test_model_with_constant_in_objective_infeasible(model: Model) -> None: def test_model_with_constant_in_objective_error(model: Model) -> None: objective = model.objective.expression + 1 + model.allow_constant_objective = True model.add_objective(expr=objective, overwrite=True) model.add_constraints([(1, "x")], "<=", 0) model.add_constraints([(1, "y")], "<=", 0) try: - status, condition = model.solve(solver_name="apples") + _ = model.solve(solver_name="apples") except AssertionError: pass From 9e7eea555020ea3b8df59bd0d349604abb81d178 Mon Sep 17 00:00:00 2001 From: Robbie Muir Date: Sun, 28 Dec 2025 15:03:28 +0100 Subject: [PATCH 06/15] minor changes --- linopy/expressions.py | 4 ++-- linopy/model.py | 13 +++++++------ linopy/objective.py | 2 +- test/test_linear_expression.py | 2 +- test/test_optimization.py | 12 ++++-------- test/test_quadratic_expression.py | 2 +- 6 files changed, 16 insertions(+), 19 deletions(-) diff --git a/linopy/expressions.py b/linopy/expressions.py index 008f5b82..a9b65455 100644 --- a/linopy/expressions.py +++ b/linopy/expressions.py @@ -726,8 +726,8 @@ def const(self, value: DataArray) -> None: self._data = assign_multiindex_safe(self.data, const=value) @property - def has_constant(self) -> DataArray: - return self.const.any() + def has_constant(self) -> bool: + return bool(self.const.any()) # create a dummy for a mask, which can be implemented later @property diff --git a/linopy/model.py b/linopy/model.py index fec3855f..cc33b52c 100644 --- a/linopy/model.py +++ b/linopy/model.py @@ -14,7 +14,6 @@ from pathlib import Path from tempfile import NamedTemporaryFile, gettempdir from typing import Any, Literal, ParamSpec, TypeVar, overload -from warnings import warn import numpy as np import pandas as pd @@ -83,9 +82,6 @@ R = TypeVar("R") -class ConstantInObjectiveWarning(UserWarning): ... - - class ConstantObjectiveError(Exception): ... @@ -812,6 +808,7 @@ def add_objective( | Sequence[tuple[ConstantLike, VariableLike]], overwrite: bool = False, sense: str = "min", + allow_constant: bool | None = None, ) -> None: """ Add an objective function to the model. @@ -822,6 +819,8 @@ def add_objective( Expression describing the objective function. overwrite : False, optional Whether to overwrite the existing objective. The default is False. + allow_constant : bool, optional + Set the `Model.allow_constant_objective` attribute. If True, the objective is allowed to contain a constant term. Returns ------- @@ -838,10 +837,12 @@ def add_objective( self.objective.expression = expr self.objective.sense = sense + if allow_constant is not None: + self.allow_constant_objective = allow_constant + if not self.allow_constant_objective and self.objective.has_constant: - warn( + raise ConstantObjectiveError( "Objective function contains constant terms but this is not allowed as Model.allow_constant_objective=False, running solve will result in an error. Please either remove constants from the expression with expr.drop_constants() or set Model.allow_constant_objective=True.", - ConstantInObjectiveWarning, ) def remove_variables(self, name: str) -> None: diff --git a/linopy/objective.py b/linopy/objective.py index 11e00b49..a810064a 100644 --- a/linopy/objective.py +++ b/linopy/objective.py @@ -196,7 +196,7 @@ def has_constant(self) -> bool: """ Returns whether the objective has a constant term. """ - return bool(self.expression.has_constant) + return self.expression.has_constant @property def model(self) -> Model: diff --git a/test/test_linear_expression.py b/test/test_linear_expression.py index dfed5f7c..4969e964 100644 --- a/test/test_linear_expression.py +++ b/test/test_linear_expression.py @@ -1242,7 +1242,7 @@ def test_drop_constant(x: Variable) -> None: assert all(expr_2.const.values == 0.0), ( f"Expected constant 0.0, got {expr_2.const.values}" ) - assert not bool(expr_2.has_constant) + assert not expr_2.has_constant def test_simplify_basic(x: Variable) -> None: diff --git a/test/test_optimization.py b/test/test_optimization.py index 4d0cbc06..5ea88e25 100644 --- a/test/test_optimization.py +++ b/test/test_optimization.py @@ -20,7 +20,7 @@ from linopy import GREATER_EQUAL, LESS_EQUAL, Model, solvers from linopy.common import to_path from linopy.expressions import LinearExpression -from linopy.model import ConstantInObjectiveWarning, ConstantObjectiveError +from linopy.model import ConstantObjectiveError from linopy.solver_capabilities import ( SolverFeature, get_available_solvers_with_feature, @@ -958,9 +958,7 @@ def test_model_resolve( def test_model_with_constant_in_objective_feasible(model: Model) -> None: objective = model.objective.expression + 1 - - with pytest.warns(ConstantInObjectiveWarning): - model.add_objective(expr=objective, overwrite=True) + model.add_objective(expr=objective, overwrite=True) with pytest.raises(ConstantObjectiveError): status, _ = model.solve(solver_name="highs") @@ -980,8 +978,7 @@ def test_model_with_constant_in_objective_infeasible(model: Model) -> None: model.add_constraints([(1, "x")], "<=", 0) model.add_constraints([(1, "y")], "<=", 0) - model.allow_constant_objective = True - _, condition = model.solve(solver_name="highs") + _, condition = model.solve(solver_name="highs", allow_constant=True) assert condition == "infeasible" # Even though the problem was not solved, the constant term should still be accessible @@ -990,8 +987,7 @@ def test_model_with_constant_in_objective_infeasible(model: Model) -> None: def test_model_with_constant_in_objective_error(model: Model) -> None: objective = model.objective.expression + 1 - model.allow_constant_objective = True - model.add_objective(expr=objective, overwrite=True) + model.add_objective(expr=objective, overwrite=True, allow_constant=True) model.add_constraints([(1, "x")], "<=", 0) model.add_constraints([(1, "y")], "<=", 0) diff --git a/test/test_quadratic_expression.py b/test/test_quadratic_expression.py index a1904e24..cb203b15 100644 --- a/test/test_quadratic_expression.py +++ b/test/test_quadratic_expression.py @@ -323,7 +323,7 @@ def test_drop_constant(x: Variable) -> None: assert all(expr_2.const.values == 0.0), ( f"Expected constant 0.0, got {expr_2.const.values}" ) - assert not bool(expr_2.has_constant) + assert not expr_2.has_constant def test_quadratic_expression_to_matrix(model: Model, x: Variable, y: Variable) -> None: From 9e5847a0dd166132801e82daab163282bb489770 Mon Sep 17 00:00:00 2001 From: Robbie Muir Date: Sun, 28 Dec 2025 15:29:54 +0100 Subject: [PATCH 07/15] fix test --- linopy/model.py | 12 ++++++------ test/test_optimization.py | 17 ++++++++++++----- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/linopy/model.py b/linopy/model.py index cc33b52c..c7c84577 100644 --- a/linopy/model.py +++ b/linopy/model.py @@ -110,7 +110,7 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: # The objective contains a constant term if not model.allow_constant_objective: raise ConstantObjectiveError( - "Objective function contains constant terms. Please use LinearExpression.drop_constants()/QuadraticExpression.drop_constants() or set Model.allow_constant_objective=True." + "Objective function contains constant terms. Please use expr.drop_constants() or set model.allow_constant_objective=True." ) # Modify the model objective to drop the constant term @@ -808,7 +808,7 @@ def add_objective( | Sequence[tuple[ConstantLike, VariableLike]], overwrite: bool = False, sense: str = "min", - allow_constant: bool | None = None, + allow_constant_objective: bool | None = None, ) -> None: """ Add an objective function to the model. @@ -819,7 +819,7 @@ def add_objective( Expression describing the objective function. overwrite : False, optional Whether to overwrite the existing objective. The default is False. - allow_constant : bool, optional + allow_constant_objective : bool, optional Set the `Model.allow_constant_objective` attribute. If True, the objective is allowed to contain a constant term. Returns @@ -837,12 +837,12 @@ def add_objective( self.objective.expression = expr self.objective.sense = sense - if allow_constant is not None: - self.allow_constant_objective = allow_constant + if allow_constant_objective is not None: + self.allow_constant_objective = allow_constant_objective if not self.allow_constant_objective and self.objective.has_constant: raise ConstantObjectiveError( - "Objective function contains constant terms but this is not allowed as Model.allow_constant_objective=False, running solve will result in an error. Please either remove constants from the expression with expr.drop_constants() or set Model.allow_constant_objective=True.", + "Objective function contains constant terms but this is not allowed as Model.allow_constant_objective=False. Either remove constants from the expression with expr.drop_constants() or pass allow_constant_objective=True.", ) def remove_variables(self, name: str) -> None: diff --git a/test/test_optimization.py b/test/test_optimization.py index 5ea88e25..c1a877eb 100644 --- a/test/test_optimization.py +++ b/test/test_optimization.py @@ -948,7 +948,7 @@ def test_model_resolve( # add another constraint after solve model.add_constraints(model.variables.y >= 3) - status, condition = model.solve( + status, _ = model.solve( solver, io_api=io_api, explicit_coordinate_names=explicit_coordinate_names ) assert status == "ok" @@ -958,7 +958,14 @@ def test_model_resolve( def test_model_with_constant_in_objective_feasible(model: Model) -> None: objective = model.objective.expression + 1 - model.add_objective(expr=objective, overwrite=True) + + with pytest.raises(ConstantObjectiveError): + model.add_objective( + expr=objective, overwrite=True, allow_constant_objective=False + ) + + model.add_objective(expr=objective, overwrite=True, allow_constant_objective=True) + model.allow_constant_objective = False with pytest.raises(ConstantObjectiveError): status, _ = model.solve(solver_name="highs") @@ -974,11 +981,11 @@ def test_model_with_constant_in_objective_feasible(model: Model) -> None: def test_model_with_constant_in_objective_infeasible(model: Model) -> None: objective = model.objective.expression + 1 - model.add_objective(expr=objective, overwrite=True) + model.add_objective(expr=objective, overwrite=True, allow_constant_objective=True) model.add_constraints([(1, "x")], "<=", 0) model.add_constraints([(1, "y")], "<=", 0) - _, condition = model.solve(solver_name="highs", allow_constant=True) + _, condition = model.solve(solver_name="highs") assert condition == "infeasible" # Even though the problem was not solved, the constant term should still be accessible @@ -987,7 +994,7 @@ def test_model_with_constant_in_objective_infeasible(model: Model) -> None: def test_model_with_constant_in_objective_error(model: Model) -> None: objective = model.objective.expression + 1 - model.add_objective(expr=objective, overwrite=True, allow_constant=True) + model.add_objective(expr=objective, overwrite=True, allow_constant_objective=True) model.add_constraints([(1, "x")], "<=", 0) model.add_constraints([(1, "y")], "<=", 0) From ee6f1ec69a542ac88d43ca7b63534cdfb7a6707c Mon Sep 17 00:00:00 2001 From: Robbie Muir Date: Sun, 28 Dec 2025 16:28:36 +0100 Subject: [PATCH 08/15] refactor --- linopy/model.py | 44 +++++++++++---------------------------- test/test_optimization.py | 29 ++++++++++++++------------ 2 files changed, 28 insertions(+), 45 deletions(-) diff --git a/linopy/model.py b/linopy/model.py index c7c84577..98848509 100644 --- a/linopy/model.py +++ b/linopy/model.py @@ -108,11 +108,6 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: return func(*args, **kwargs) # The objective contains a constant term - if not model.allow_constant_objective: - raise ConstantObjectiveError( - "Objective function contains constant terms. Please use expr.drop_constants() or set model.allow_constant_objective=True." - ) - # Modify the model objective to drop the constant term model = self constant = float(self.objective.expression.const.values) @@ -162,7 +157,6 @@ class Model: _dual: Dataset _status: str _termination_condition: str - _allow_constant_objective: bool _xCounter: int _cCounter: int _varnameCounter: int @@ -184,7 +178,6 @@ class Model: # hidden attributes "_status", "_termination_condition", - "_allow_constant_objective", # TODO: move counters to Variables and Constraints class "_xCounter", "_cCounter", @@ -236,7 +229,6 @@ def __init__( self._status: str = "initialized" self._termination_condition: str = "" - self._allow_constant_objective: bool = False self._xCounter: int = 0 self._cCounter: int = 0 self._varnameCounter: int = 0 @@ -277,9 +269,10 @@ def objective( self, obj: Objective | LinearExpression | QuadraticExpression ) -> Objective: if not isinstance(obj, Objective): - obj = Objective(obj, self) - - self._objective = obj + expr = obj + else: + expr = obj.expression + self.add_objective(expr=expr, overwrite=True, allow_constant=False) return self._objective @property @@ -789,17 +782,6 @@ def add_constraints( self.constraints.add(constraint) return constraint - @property - def allow_constant_objective(self) -> bool: - """ - Whether constant terms in the objective function are allowed. - """ - return self._allow_constant_objective - - @allow_constant_objective.setter - def allow_constant_objective(self, allow: bool) -> None: - self._allow_constant_objective = allow - def add_objective( self, expr: Variable @@ -808,7 +790,7 @@ def add_objective( | Sequence[tuple[ConstantLike, VariableLike]], overwrite: bool = False, sense: str = "min", - allow_constant_objective: bool | None = None, + allow_constant: bool = False, ) -> None: """ Add an objective function to the model. @@ -819,8 +801,8 @@ def add_objective( Expression describing the objective function. overwrite : False, optional Whether to overwrite the existing objective. The default is False. - allow_constant_objective : bool, optional - Set the `Model.allow_constant_objective` attribute. If True, the objective is allowed to contain a constant term. + allow_constant: bool, optional + If True, the objective is allowed to contain a constant term. The default is False Returns ------- @@ -835,16 +817,14 @@ def add_objective( if isinstance(expr, Variable): expr = 1 * expr - self.objective.expression = expr - self.objective.sense = sense - if allow_constant_objective is not None: - self.allow_constant_objective = allow_constant_objective - - if not self.allow_constant_objective and self.objective.has_constant: + if not allow_constant and expr.has_constant: raise ConstantObjectiveError( - "Objective function contains constant terms but this is not allowed as Model.allow_constant_objective=False. Either remove constants from the expression with expr.drop_constants() or pass allow_constant_objective=True.", + "Objective contains constant term. Either remove constants from the expression with expr.drop_constants() or use model.add_objective(..., allow_constant=True).", ) + objective = Objective(expression=expr, model=self, sense=sense) + self._objective = objective + def remove_variables(self, name: str) -> None: """ Remove all variables stored under reference name `name` from the model. diff --git a/test/test_optimization.py b/test/test_optimization.py index c1a877eb..7bb05be8 100644 --- a/test/test_optimization.py +++ b/test/test_optimization.py @@ -956,21 +956,24 @@ def test_model_resolve( assert np.isclose(model.objective.value or 0, 5.25) -def test_model_with_constant_in_objective_feasible(model: Model) -> None: +def test_constant_not_allowed_unless_specified_explicitly(model: Model) -> None: objective = model.objective.expression + 1 with pytest.raises(ConstantObjectiveError): - model.add_objective( - expr=objective, overwrite=True, allow_constant_objective=False - ) - - model.add_objective(expr=objective, overwrite=True, allow_constant_objective=True) - model.allow_constant_objective = False + model.add_objective(expr=objective, overwrite=True, allow_constant=False) + with pytest.raises(ConstantObjectiveError): + model.add_objective(expr=objective, overwrite=True) with pytest.raises(ConstantObjectiveError): - status, _ = model.solve(solver_name="highs") + model.objective = objective + + model.add_objective(expr=objective, overwrite=True, allow_constant=True) + + +def test_constant_feasible(model: Model) -> None: + objective = model.objective.expression + 1 + model.add_objective(expr=objective, overwrite=True, allow_constant=True) - model.allow_constant_objective = True status, _ = model.solve(solver_name="highs") assert status == "ok" # x = -0.1, y = 1.7 @@ -979,9 +982,9 @@ def test_model_with_constant_in_objective_feasible(model: Model) -> None: assert model.objective.expression.solution == 4.3 -def test_model_with_constant_in_objective_infeasible(model: Model) -> None: +def test_constant_infeasible(model: Model) -> None: objective = model.objective.expression + 1 - model.add_objective(expr=objective, overwrite=True, allow_constant_objective=True) + model.add_objective(expr=objective, overwrite=True, allow_constant=True) model.add_constraints([(1, "x")], "<=", 0) model.add_constraints([(1, "y")], "<=", 0) @@ -992,9 +995,9 @@ def test_model_with_constant_in_objective_infeasible(model: Model) -> None: assert model.objective.expression.const == 1 -def test_model_with_constant_in_objective_error(model: Model) -> None: +def test_constant_error(model: Model) -> None: objective = model.objective.expression + 1 - model.add_objective(expr=objective, overwrite=True, allow_constant_objective=True) + model.add_objective(expr=objective, overwrite=True, allow_constant=True) model.add_constraints([(1, "x")], "<=", 0) model.add_constraints([(1, "y")], "<=", 0) From 8eb9fdc37e4f7285938d8080db29192e0900ea05 Mon Sep 17 00:00:00 2001 From: Robbie Muir Date: Sun, 28 Dec 2025 17:06:23 +0100 Subject: [PATCH 09/15] more changes --- linopy/model.py | 46 +++++++++++++++++++++++++++++---------- linopy/objective.py | 13 ++++++++--- test/test_model.py | 26 +++++++++++++++++++--- test/test_objective.py | 6 ----- test/test_optimization.py | 15 ------------- 5 files changed, 68 insertions(+), 38 deletions(-) diff --git a/linopy/model.py b/linopy/model.py index 98848509..da5aac10 100644 --- a/linopy/model.py +++ b/linopy/model.py @@ -268,11 +268,7 @@ def objective(self) -> Objective: def objective( self, obj: Objective | LinearExpression | QuadraticExpression ) -> Objective: - if not isinstance(obj, Objective): - expr = obj - else: - expr = obj.expression - self.add_objective(expr=expr, overwrite=True, allow_constant=False) + self.add_objective(expr=obj, overwrite=True, allow_constant=False) return self._objective @property @@ -782,14 +778,36 @@ def add_constraints( self.constraints.add(constraint) return constraint + @overload + def add_objective( + self, + expr: Objective, + sense: None = None, + overwrite: bool = False, + allow_constant: bool = False, + ) -> None: ... + + @overload def add_objective( self, expr: Variable | LinearExpression | QuadraticExpression | Sequence[tuple[ConstantLike, VariableLike]], + sense: Literal["min", "max"] | None = None, + overwrite: bool = False, + allow_constant: bool = False, + ) -> None: ... + + def add_objective( + self, + expr: Variable + | LinearExpression + | QuadraticExpression + | Sequence[tuple[ConstantLike, VariableLike]] + | Objective, + sense: Literal["min", "max"] | None = None, overwrite: bool = False, - sense: str = "min", allow_constant: bool = False, ) -> None: """ @@ -797,8 +815,9 @@ def add_objective( Parameters ---------- - expr : linopy.LinearExpression, linopy.QuadraticExpression + expr : linopy.Variable, linopy.LinearExpression, linopy.QuadraticExpression, Objective Expression describing the objective function. + sense: "min" or "max", the sense to optimize for. Defaults to min. Cannot be set if passing Objective directly overwrite : False, optional Whether to overwrite the existing objective. The default is False. allow_constant: bool, optional @@ -814,15 +833,20 @@ def add_objective( "Objective already defined." " Set `overwrite` to True to force overwriting." ) - if isinstance(expr, Variable): - expr = 1 * expr - if not allow_constant and expr.has_constant: + if isinstance(expr, Objective): + assert sense is None, "Cannot set sense if objective object is passed" + objective = expr + assert objective.model == self + else: + sense = sense or "min" + objective = Objective(expression=expr, model=self, sense=sense) + + if not allow_constant and objective.expression.has_constant: raise ConstantObjectiveError( "Objective contains constant term. Either remove constants from the expression with expr.drop_constants() or use model.add_objective(..., allow_constant=True).", ) - objective = Objective(expression=expr, model=self, sense=sense) self._objective = objective def remove_variables(self, name: str) -> None: diff --git a/linopy/objective.py b/linopy/objective.py index a810064a..fcd2709c 100644 --- a/linopy/objective.py +++ b/linopy/objective.py @@ -9,7 +9,7 @@ import functools from collections.abc import Callable, Sequence -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Literal import numpy as np import polars as pl @@ -24,6 +24,7 @@ from linopy import expressions from linopy.types import ConstantLike +from linopy.variables import Variable if TYPE_CHECKING: from linopy.expressions import LinearExpression, QuadraticExpression @@ -64,13 +65,19 @@ class Objective: def __init__( self, - expression: expressions.LinearExpression | expressions.QuadraticExpression, + expression: Variable + | expressions.LinearExpression + | expressions.QuadraticExpression, model: Model, - sense: str = "min", + sense: Literal["min", "max"] = "min", ) -> None: self._model: Model = model self._value: float | None = None + if isinstance(expression, Variable): + expression = 1 * expression + + assert sense in ["min", "max"] self.sense: str = sense self.expression: ( expressions.LinearExpression | expressions.QuadraticExpression diff --git a/test/test_model.py b/test/test_model.py index 063b70dd..494118ad 100644 --- a/test/test_model.py +++ b/test/test_model.py @@ -9,10 +9,12 @@ from tempfile import gettempdir import numpy as np +import pandas as pd import pytest import xarray as xr from linopy import EQUAL, Model +from linopy.model import ConstantObjectiveError from linopy.testing import assert_model_equal target_shape: tuple[int, int] = (10, 10) @@ -67,7 +69,7 @@ def test_objective() -> None: y = m.add_variables(lower, upper, name="y") obj1 = (10 * x + 5 * y).sum() - m.add_objective(obj1) + m.add_objective(obj1, allow_constant=True) assert m.objective.vars.size == 200 # test overwriting @@ -82,8 +84,9 @@ def test_objective() -> None: assert m.objectiverange.min() == 2 assert m.objectiverange.max() == 2 - # test objective with constant which is supported - m.objective = m.objective + 3 + # test setting constant term in objective with explicitly allowing it + with pytest.raises(ConstantObjectiveError): + m.objective = m.objective + 3 def test_remove_variable() -> None: @@ -162,3 +165,20 @@ def test_assert_model_equal() -> None: m.add_objective(obj) assert_model_equal(m, m) + + +def test_constant_not_allowed_in_objective_unless_specified_explicitly() -> None: + model = Model() + days = pd.Index(["Mon", "Tue", "Wed", "Thu", "Fri"], name="day") + x = model.add_variables(name="x", coords=[days]) + non_linear = x + 1 + + with pytest.raises(ConstantObjectiveError): + model.add_objective(expr=non_linear, overwrite=True, allow_constant=False) + with pytest.raises(ConstantObjectiveError): + model.add_objective(expr=non_linear, overwrite=True) + + with pytest.raises(ConstantObjectiveError): + model.objective = non_linear + + model.add_objective(expr=non_linear, overwrite=True, allow_constant=True) diff --git a/test/test_objective.py b/test/test_objective.py index 80b2021a..cffbaa29 100644 --- a/test/test_objective.py +++ b/test/test_objective.py @@ -187,9 +187,3 @@ def test_repr(linear_objective: Objective, quadratic_objective: Objective) -> No assert "Linear" in linear_objective.__repr__() assert "Quadratic" in quadratic_objective.__repr__() - - -def test_objective_constant() -> None: - m = Model() - linear_expr = LinearExpression(None, m) + 1 - m.objective = Objective(linear_expr, m) diff --git a/test/test_optimization.py b/test/test_optimization.py index 7bb05be8..846b292a 100644 --- a/test/test_optimization.py +++ b/test/test_optimization.py @@ -20,7 +20,6 @@ from linopy import GREATER_EQUAL, LESS_EQUAL, Model, solvers from linopy.common import to_path from linopy.expressions import LinearExpression -from linopy.model import ConstantObjectiveError from linopy.solver_capabilities import ( SolverFeature, get_available_solvers_with_feature, @@ -956,20 +955,6 @@ def test_model_resolve( assert np.isclose(model.objective.value or 0, 5.25) -def test_constant_not_allowed_unless_specified_explicitly(model: Model) -> None: - objective = model.objective.expression + 1 - - with pytest.raises(ConstantObjectiveError): - model.add_objective(expr=objective, overwrite=True, allow_constant=False) - with pytest.raises(ConstantObjectiveError): - model.add_objective(expr=objective, overwrite=True) - - with pytest.raises(ConstantObjectiveError): - model.objective = objective - - model.add_objective(expr=objective, overwrite=True, allow_constant=True) - - def test_constant_feasible(model: Model) -> None: objective = model.objective.expression + 1 model.add_objective(expr=objective, overwrite=True, allow_constant=True) From 7f89c068db37ac7b371f4e95c6ed05e5ba1911ed Mon Sep 17 00:00:00 2001 From: Robbie Muir Date: Sun, 28 Dec 2025 17:26:55 +0100 Subject: [PATCH 10/15] fix mypy issues maybe --- doc/release_notes.rst | 2 +- linopy/model.py | 9 ++++++--- linopy/objective.py | 4 ++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 1583a4fd..60904063 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -3,7 +3,7 @@ Release Notes .. Upcoming Version -* Allow constant values in objective cost function +* Allow constant values in objective cost function. Refactored objective setting. * Add support for SOS1 and SOS2 (Special Ordered Sets) constraints via ``Model.add_sos_constraints()`` and ``Model.remove_sos_constraints()`` * Add simplify method to LinearExpression to combine duplicate terms * Add convenience function to create LinearExpression from constant diff --git a/linopy/model.py b/linopy/model.py index da5aac10..862de47d 100644 --- a/linopy/model.py +++ b/linopy/model.py @@ -266,7 +266,7 @@ def objective(self) -> Objective: @objective.setter def objective( - self, obj: Objective | LinearExpression | QuadraticExpression + self, obj: Objective | Variable | LinearExpression | QuadraticExpression ) -> Objective: self.add_objective(expr=obj, overwrite=True, allow_constant=False) return self._objective @@ -779,7 +779,7 @@ def add_constraints( return constraint @overload - def add_objective( + def add_objective( # Set objective as Objective object self, expr: Objective, sense: None = None, @@ -788,7 +788,7 @@ def add_objective( ) -> None: ... @overload - def add_objective( + def add_objective( # Set objective as expression-like with sense self, expr: Variable | LinearExpression @@ -834,6 +834,9 @@ def add_objective( " Set `overwrite` to True to force overwriting." ) + if isinstance(expr, list | tuple): + expr: LinearExpression = self.linexpr(*expr) + if isinstance(expr, Objective): assert sense is None, "Cannot set sense if objective object is passed" objective = expr diff --git a/linopy/objective.py b/linopy/objective.py index fcd2709c..87fd76ef 100644 --- a/linopy/objective.py +++ b/linopy/objective.py @@ -213,14 +213,14 @@ def model(self) -> Model: return self._model @property - def sense(self) -> str: + def sense(self) -> Literal["min", "max"]: """ Returns the sense of the objective. """ return self._sense @sense.setter - def sense(self, sense: str) -> None: + def sense(self, sense: Literal["min", "max"]) -> None: """ Sets the sense of the objective. """ From 931b194c448014ce47c5e5a106a31b2826527490 Mon Sep 17 00:00:00 2001 From: Robbie Muir Date: Sun, 28 Dec 2025 17:32:47 +0100 Subject: [PATCH 11/15] fix types --- linopy/model.py | 13 +++++++------ test/test_objective.py | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/linopy/model.py b/linopy/model.py index 862de47d..f28bb8d2 100644 --- a/linopy/model.py +++ b/linopy/model.py @@ -272,14 +272,14 @@ def objective( return self._objective @property - def sense(self) -> str: + def sense(self) -> Literal["min", "max"]: """ Sense of the objective function. """ return self.objective.sense @sense.setter - def sense(self, value: str) -> None: + def sense(self, value: Literal["min", "max"]) -> None: self.objective.sense = value @property @@ -834,16 +834,17 @@ def add_objective( " Set `overwrite` to True to force overwriting." ) - if isinstance(expr, list | tuple): - expr: LinearExpression = self.linexpr(*expr) - if isinstance(expr, Objective): assert sense is None, "Cannot set sense if objective object is passed" objective = expr assert objective.model == self else: sense = sense or "min" - objective = Objective(expression=expr, model=self, sense=sense) + if isinstance(expr, list | tuple): + expr_2: LinearExpression = self.linexpr(*expr) + else: + expr_2 = expr + objective = Objective(expression=expr_2, model=self, sense=sense) if not allow_constant and objective.expression.has_constant: raise ConstantObjectiveError( diff --git a/test/test_objective.py b/test/test_objective.py index cffbaa29..f886b9c4 100644 --- a/test/test_objective.py +++ b/test/test_objective.py @@ -69,7 +69,7 @@ def test_set_sense_via_model( def test_sense_setter_error(linear_objective: Objective) -> None: with pytest.raises(ValueError): - linear_objective.sense = "not min or max" + linear_objective.sense = "not min or max" # type: ignore def test_variables_inherited_properties(linear_objective: Objective) -> None: From ea1ebaed707b7f41c9af54177f12486737043694 Mon Sep 17 00:00:00 2001 From: Robbie Muir Date: Sun, 28 Dec 2025 17:38:11 +0100 Subject: [PATCH 12/15] maybe fix types --- linopy/model.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/linopy/model.py b/linopy/model.py index f28bb8d2..1be4e598 100644 --- a/linopy/model.py +++ b/linopy/model.py @@ -838,13 +838,12 @@ def add_objective( assert sense is None, "Cannot set sense if objective object is passed" objective = expr assert objective.model == self + elif isinstance(expr, (Variable, LinearExpression, QuadraticExpression)): + sense = sense or "min" + objective = Objective(expression=expr, model=self, sense=sense) else: sense = sense or "min" - if isinstance(expr, list | tuple): - expr_2: LinearExpression = self.linexpr(*expr) - else: - expr_2 = expr - objective = Objective(expression=expr_2, model=self, sense=sense) + objective = Objective(expression=self.linexpr(*expr), model=self, sense=sense) if not allow_constant and objective.expression.has_constant: raise ConstantObjectiveError( From 1d23943bfbe94f192dfb8c7239c63b2a7c8f3629 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 28 Dec 2025 16:38:22 +0000 Subject: [PATCH 13/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- linopy/model.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/linopy/model.py b/linopy/model.py index 1be4e598..22860208 100644 --- a/linopy/model.py +++ b/linopy/model.py @@ -843,7 +843,9 @@ def add_objective( objective = Objective(expression=expr, model=self, sense=sense) else: sense = sense or "min" - objective = Objective(expression=self.linexpr(*expr), model=self, sense=sense) + objective = Objective( + expression=self.linexpr(*expr), model=self, sense=sense + ) if not allow_constant and objective.expression.has_constant: raise ConstantObjectiveError( From 4f07a35388d31475fb04e3eb0566bfb76abb2557 Mon Sep 17 00:00:00 2001 From: Robbie Muir Date: Sun, 28 Dec 2025 17:41:32 +0100 Subject: [PATCH 14/15] make precommit happy --- linopy/model.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/linopy/model.py b/linopy/model.py index 1be4e598..9404eb62 100644 --- a/linopy/model.py +++ b/linopy/model.py @@ -838,12 +838,14 @@ def add_objective( assert sense is None, "Cannot set sense if objective object is passed" objective = expr assert objective.model == self - elif isinstance(expr, (Variable, LinearExpression, QuadraticExpression)): + elif isinstance(expr, Variable | LinearExpression | QuadraticExpression): sense = sense or "min" objective = Objective(expression=expr, model=self, sense=sense) else: sense = sense or "min" - objective = Objective(expression=self.linexpr(*expr), model=self, sense=sense) + objective = Objective( + expression=self.linexpr(*expr), model=self, sense=sense + ) if not allow_constant and objective.expression.has_constant: raise ConstantObjectiveError( From c184189b067ea84e80b0fc2c9b94d9489d6893ee Mon Sep 17 00:00:00 2001 From: Robbie Muir Date: Mon, 29 Dec 2025 08:35:28 +0100 Subject: [PATCH 15/15] update docstring --- linopy/model.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/linopy/model.py b/linopy/model.py index 9404eb62..7ac7d58c 100644 --- a/linopy/model.py +++ b/linopy/model.py @@ -102,22 +102,21 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: assert isinstance(self, Model), ( f"First argument must be a Model instance, got {type(self)}" ) - model = self - if not self.objective.has_constant: + model: Model = self + if not model.objective.has_constant: # Continue as normal if there is no constant term return func(*args, **kwargs) # The objective contains a constant term # Modify the model objective to drop the constant term - model = self - constant = float(self.objective.expression.const.values) - model.objective.expression = self.objective.expression.drop_constant() + constant = float(model.objective.expression.const.values) + model.objective.expression = model.objective.expression.drop_constant() args = (model, *args[1:]) # type: ignore try: result = func(*args, **kwargs) except Exception as e: - # Even if there is an exception, make sure the model returns to it's original state + # Even if there is an exception, make sure the model returns to its original state model.objective.expression = model.objective.expression + constant raise e