From 66498865e0d910866143a428a8751e66866c2fd2 Mon Sep 17 00:00:00 2001 From: Fabian Date: Fri, 29 Sep 2023 12:09:54 +0200 Subject: [PATCH 1/2] add objective module and objective class in order to allow maximization problems --- README.md | 2 +- benchmark/scripts/benchmark_jump.jl | 4 +- benchmark/scripts/benchmark_linopy.py | 4 +- doc/release_notes.rst | 7 +- linopy/__init__.py | 1 + linopy/expressions.py | 3 + linopy/io.py | 10 +- linopy/matrices.py | 4 +- linopy/model.py | 81 +++++------ linopy/objective.py | 198 ++++++++++++++++++++++++++ linopy/solvers.py | 20 +++ test/test_io.py | 2 +- test/test_objective.py | 143 +++++++++++++++++++ test/test_optimization.py | 39 ++++- 14 files changed, 453 insertions(+), 65 deletions(-) create mode 100644 linopy/objective.py create mode 100644 test/test_objective.py diff --git a/README.md b/README.md index 744fb011..66fa3950 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ Finally, we can solve the problem and get the optimal solution: ```python >>> m.solve() ->>> m.objective_value +>>> m.objective.value ``` ``` 17.166 diff --git a/benchmark/scripts/benchmark_jump.jl b/benchmark/scripts/benchmark_jump.jl index eacf7bdb..02e09168 100755 --- a/benchmark/scripts/benchmark_jump.jl +++ b/benchmark/scripts/benchmark_jump.jl @@ -14,7 +14,7 @@ function basic_model(n, solver) @constraint(m, x + y .>= 0) @objective(m, Min, 2 * sum(x) + sum(y)) optimize!(m) - return objective_value(m) + return objective.value(m) end function knapsack_model(n, solver) @@ -25,7 +25,7 @@ function knapsack_model(n, solver) @constraint(m, weight' * x <= 200) @objective(m, Max, value' * x) optimize!(m) - return objective_value(m) + return objective.value(m) end diff --git a/benchmark/scripts/benchmark_linopy.py b/benchmark/scripts/benchmark_linopy.py index a6bd5990..2e7ee4d5 100755 --- a/benchmark/scripts/benchmark_linopy.py +++ b/benchmark/scripts/benchmark_linopy.py @@ -27,7 +27,7 @@ def basic_model(n, solver): m.add_objective(2 * x.sum() + y.sum()) # m.to_file(f"linopy-model.lp") m.solve(solver) - return m.objective_value + return m.objective.value def knapsack_model(n, solver): @@ -38,7 +38,7 @@ def knapsack_model(n, solver): m.add_constraints((weight * packages).sum() <= 200) m.add_objective(-(value * packages).sum()) # use minus because of minimization m.solve(solver_name=solver) - return -m.objective_value + return -m.objective.value if __name__ == "__main__": diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 4249c323..8a0d628e 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -1,8 +1,11 @@ Release Notes ============= -.. Upcoming Release -.. ---------------- +Upcoming Release +---------------- + +* It is now possible to set the sense of the objective function to `minimize` or `maximize`. Therefore, a new class `Objective` was introduced which is used in `Model.objective`. It supports the same arithmetic operations as `LinearExpression` and `QuadraticExpression` and contains a `sense` attribute which can be set to `minimize` or `maximize`. + Version 0.2.5 ------------- diff --git a/linopy/__init__.py b/linopy/__init__.py index c25778ab..e646ffbd 100644 --- a/linopy/__init__.py +++ b/linopy/__init__.py @@ -16,5 +16,6 @@ from linopy.expressions import LinearExpression, QuadraticExpression, merge from linopy.io import read_netcdf from linopy.model import Model, Variable, available_solvers +from linopy.objective import Objective from linopy.remote import RemoteHandler from linopy.version import version as __version__ diff --git a/linopy/expressions.py b/linopy/expressions.py index 93aab4f0..f5adf27a 100644 --- a/linopy/expressions.py +++ b/linopy/expressions.py @@ -1102,6 +1102,9 @@ class QuadraticExpression(LinearExpression): def __init__(self, data, model): super().__init__(data, model) + if data is None: + da = xr.DataArray([[], []], dims=[FACTOR_DIM, TERM_DIM]) + data = Dataset({"coeffs": da, "vars": da, "const": 0}) if FACTOR_DIM not in data.vars.dims: raise ValueError(f"Data does not include dimension {FACTOR_DIM}") elif data.sizes[FACTOR_DIM] != 2: diff --git a/linopy/io.py b/linopy/io.py index b3c7fffe..64575c76 100644 --- a/linopy/io.py +++ b/linopy/io.py @@ -93,7 +93,8 @@ def objective_to_file(m, f, log=False, batch_size=10000): if log: logger.info("Writing objective.") - f.write("min\n\nobj:\n\n") + sense = m.objective.sense + f.write(f"{sense}\n\nobj:\n\n") df = m.objective.flat if np.isnan(df.coeffs).any(): @@ -354,6 +355,9 @@ def to_gurobipy(m, env=None): else: model.setObjective(M.c @ x) + if m.objective.sense == "max": + model.ModelSense = -1 + if len(m.constraints): names = "c" + M.clabels.astype(str).astype(object) c = model.addMConstr(M.A, x, M.sense, M.b) @@ -421,6 +425,10 @@ def to_highspy(m): num_vars = Q.shape[0] h.passHessian(num_vars, Q.nnz, 1, Q.indptr, Q.indices, Q.data) + # change objective sense + if m.objective.sense == "max": + h.changeObjectiveSense(highspy.highs_bindings.ObjSense.kMaximize) + return h diff --git a/linopy/matrices.py b/linopy/matrices.py index f206043c..f0b8534a 100644 --- a/linopy/matrices.py +++ b/linopy/matrices.py @@ -110,7 +110,7 @@ def c(self): "Vector of objective coefficients of all non-missing variables." m = self._parent ds = m.objective.flat - if isinstance(m.objective, expressions.QuadraticExpression): + if isinstance(m.objective.expression, expressions.QuadraticExpression): ds = ds[(ds.vars1 == -1) | (ds.vars2 == -1)] ds["vars"] = ds.vars1.where(ds.vars1 != -1, ds.vars2) @@ -125,4 +125,4 @@ def Q(self): if m.is_linear: return None - return m.objective.to_matrix()[self.vlabels][:, self.vlabels] + return m.objective.expression.to_matrix()[self.vlabels][:, self.vlabels] diff --git a/linopy/model.py b/linopy/model.py index 8a31f8cd..c3e0a671 100644 --- a/linopy/model.py +++ b/linopy/model.py @@ -8,7 +8,6 @@ import logging import os import re -import warnings from pathlib import Path from tempfile import NamedTemporaryFile, gettempdir @@ -20,13 +19,7 @@ from xarray import DataArray, Dataset from linopy import solvers -from linopy.common import ( - as_dataarray, - best_int, - maybe_replace_signs, - replace_by_map, - save_join, -) +from linopy.common import as_dataarray, best_int, maybe_replace_signs, replace_by_map from linopy.constants import TERM_DIM, ModelStatus, TerminationCondition from linopy.constraints import AnonymousScalarConstraint, Constraint, Constraints from linopy.expressions import ( @@ -36,6 +29,7 @@ ) from linopy.io import to_block_files, to_file, to_gurobipy, to_highspy, to_netcdf from linopy.matrices import MatrixAccessor +from linopy.objective import Objective from linopy.solvers import available_solvers, quadratic_solvers from linopy.variables import ScalarVariable, Variable, Variables @@ -75,7 +69,6 @@ class Model: "_varnameCounter", "_connameCounter", "_blocks", - "_objective_value", # TODO: check if these should not be mutable "_chunk", "_force_dim_names", @@ -110,11 +103,9 @@ def __init__(self, solver_dir=None, chunk=None, force_dim_names=False): """ self._variables = Variables(model=self) self._constraints = Constraints(model=self) - self._objective = LinearExpression(None, self) + self._objective = Objective(LinearExpression(None, self), self) self._parameters = Dataset() - self._objective_value = nan - self._status = "initialized" self._termination_condition = "" self._xCounter = 0 @@ -151,8 +142,23 @@ def objective(self): return self._objective @objective.setter - def objective(self, value) -> LinearExpression: - self.add_objective(value, overwrite=True) + def objective(self, obj) -> Objective: + if not isinstance(obj, Objective): + obj = Objective(obj, self) + + self._objective = obj + return self._objective + + @property + def sense(self): + """ + Sense of the objective function. + """ + return self.objective.sense + + @sense.setter + def sense(self, value): + self.objective.sense = value @property def parameters(self): @@ -205,15 +211,16 @@ def termination_condition(self, value): self._termination_condition = TerminationCondition[value].value @property + @deprecated("0.2.7", "Use `objective.value` instead.") def objective_value(self): """ Objective value of the model. """ - return self._objective_value + return self._objective.value @objective_value.setter def objective_value(self, value): - self._objective_value = float(value) + self._objective.value = value @property def chunk(self): @@ -276,7 +283,6 @@ def dataset_attrs(self): @property def scalar_attrs(self): return [ - "objective_value", "status", "_xCounter", "_cCounter", @@ -586,14 +592,14 @@ def assert_sign_rhs_not_None(lhs, sign, rhs): self.constraints.add(constraint) return constraint - def add_objective(self, expr, overwrite=False): + def add_objective(self, expr, overwrite=False, sense="min"): """ - Add a linear objective function to the model. + Add an objective function to the model. Parameters ---------- - expr : linopy.LinearExpression - Linear Expressions describing the objective function. + expr : linopy.LinearExpression, linopy.QuadraticExpression + Expression describing the objective function. overwrite : False, optional Whether to overwrite the existing objective. The default is False. @@ -603,31 +609,12 @@ def add_objective(self, expr, overwrite=False): The objective function assigned to the model. """ if not overwrite: - assert self.objective.empty(), ( + assert self.objective.expression.empty(), ( "Objective already defined." " Set `overwrite` to True to force overwriting." ) - - if isinstance(expr, (list, tuple)): - expr = self.linexpr(*expr) - - if not isinstance(expr, (LinearExpression, QuadraticExpression)): - raise ValueError( - f"Invalid type of `expr` ({type(expr)})." - " Must be a LinearExpression or QuadraticExpression." - ) - - if self.chunk is not None: - expr = expr.chunk(self.chunk) - - if len(expr.coord_dims): - expr = expr.sum() - - if expr.const != 0: - raise ValueError("Constant values in objective function not supported.") - - self._objective = expr - return self._objective + self.objective.expression = expr + self.objective.sense = sense def remove_variables(self, name): """ @@ -697,11 +684,11 @@ def integers(self): @property def is_linear(self): - return type(self.objective) is LinearExpression + return self.objective.is_linear @property def is_quadratic(self): - return type(self.objective) is QuadraticExpression + return self.objective.is_quadratic @property def type(self): @@ -995,7 +982,7 @@ def solve( **solver_options, ) - self.objective_value = solved.objective_value + self.objective.value = solved.objective.value self.status = solved.status self.termination_condition = solved.termination_condition for k, v in self.variables.items(): @@ -1057,7 +1044,7 @@ def solve( result.info() - self.objective_value = result.solution.objective + self.objective._value = result.solution.objective self.status = result.status.status.value self.termination_condition = result.status.termination_condition.value self.solver_model = result.solver_model diff --git a/linopy/objective.py b/linopy/objective.py new file mode 100644 index 00000000..cbfdd84a --- /dev/null +++ b/linopy/objective.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Linopy objective module. + +This module contains definition related to objective expressions. +""" + +import functools +from typing import Union + +import numpy as np + +from linopy import expressions +from linopy.common import forward_as_properties + + +def objwrap(method, *default_args, **new_default_kwargs): + @functools.wraps(method) + def _objwrap(obj, *args, **kwargs): + for k, v in new_default_kwargs.items(): + kwargs.setdefault(k, v) + return obj.__class__( + method(obj.expression, *default_args, *args, **kwargs), obj.model, obj.sense + ) + + _objwrap.__doc__ = ( + f"Wrapper for the expression {method} function for linopy.Objective." + ) + if new_default_kwargs: + _objwrap.__doc__ += f" with default arguments: {new_default_kwargs}" + + return _objwrap + + +@forward_as_properties( + expression=[ + "attrs", + "coords", + "indexes", + "sizes", + "flat", + "coeffs", + "vars", + "data", + "nterm", + ] +) +class Objective: + """ + An objective expression containing all relevant information. + """ + + __slots__ = ("_expression", "_model", "_sense", "_value") + __array_ufunc__ = None + __array_priority__ = 10000 + + _fill_value = {"vars": -1, "coeffs": np.nan, "const": 0} + + def __init__( + self, + expression: Union[ + expressions.LinearExpression, expressions.QuadraticExpression + ], + model, + sense="min", + ): + from linopy.model import Model + + self._model = model + self._value = None + + self.sense = sense + self.expression = expression + + def __repr__(self) -> str: + sense_string = f"Sense: {self.sense}" + expr_string = self.expression.__repr__() + expr_string = "\n".join( + expr_string.split("\n")[2:] + ) # drop first two lines of expression string + expr_string = self.expression.type + ": " + expr_string + value_string = f"Value: {self.value}" + + return f"Objective:\n----------\n{expr_string}\n{sense_string}\n{value_string}" + + @property + def expression(self): + """ + Returns the expression of the objective. + """ + return self._expression + + @expression.setter + def expression(self, expr): + """ + Sets the expression of the objective. + """ + if isinstance(expr, (list, tuple)): + expr = self.model.linexpr(*expr) + + if isinstance(expr, Objective): + expr = expr.expression + + if not isinstance( + expr, (expressions.LinearExpression, expressions.QuadraticExpression) + ): + raise ValueError( + f"Invalid type of `expr` ({type(expr)})." + " Must be a LinearExpression or QuadraticExpression." + ) + + if len(expr.coord_dims): + expr = expr.sum() + + if expr.const != 0: + raise ValueError("Constant values in objective function not supported.") + + self._expression = expr + + @property + def model(self): + """ + Returns the model of the objective. + """ + return self._model + + @property + def sense(self): + """ + Returns the sense of the objective. + """ + return self._sense + + @sense.setter + def sense(self, sense): + """ + Sets the sense of the objective. + """ + if sense not in ("min", "max"): + raise ValueError("Invalid sense. Must be 'min' or 'max'.") + self._sense = sense + + @property + def value(self): + """ + Returns the value of the objective. + """ + return self._value + + def set_value(self, value: float): + """ + Sets the value of the objective. + """ + self._value = float(value) + + @property + def is_linear(self): + return type(self.expression) is expressions.LinearExpression + + @property + def is_quadratic(self): + return type(self.expression) is expressions.QuadraticExpression + + def to_matrix(self, *args, **kwargs): + "Wrapper for expression.to_matrix" + if self.is_linear: + raise ValueError("Cannot convert linear objective to matrix.") + return self.expression.to_matrix(*args, **kwargs) + + assign = objwrap(expressions.LinearExpression.assign) + + sel = objwrap(expressions.LinearExpression.sel) + + def __add__(self, expr): + if not isinstance(expr, Objective): + expr = Objective(expr, self.model, self.sense) + return Objective(self.expression + expr.expression, self.model, self.sense) + + def __sub__(self, expr): + if not isinstance(expr, Objective): + expr = Objective(expr, self.model) + return Objective(self.expression - expr.expression, self.model, self.sense) + + def __mul__(self, expr): + # only allow scalar multiplication + if not isinstance(expr, (int, float, np.floating, np.integer)): + raise ValueError("Invalid type for multiplication.") + return Objective(self.expression * expr, self.model, self.sense) + + def __neg__(self): + return Objective(-self.expression, self.model, self.sense) + + def __truediv__(self, expr): + # only allow scalar division + if not isinstance(expr, (int, float, np.floating, np.integer)): + raise ValueError("Invalid type for division.") + return Objective(self.expression / expr, self.model, self.sense) diff --git a/linopy/solvers.py b/linopy/solvers.py index 64be343e..65e2a0a8 100644 --- a/linopy/solvers.py +++ b/linopy/solvers.py @@ -87,6 +87,20 @@ def safe_get_solution(status, func): return Solution() +def maybe_adjust_objective_sign(solution, sense, io_api, solver_name): + if sense == "min": + return + + if np.isnan(solution.objective): + return + + if io_api == "mps": + logger.info( + "Adjusting objective sign due to switched coefficients in MPS file." + ) + solution.objective *= -1 + + def set_int_index(series): """ Convert string index to int index. @@ -193,6 +207,7 @@ def get_solver_solution(): return Solution(sol, dual, objective) solution = safe_get_solution(status, get_solver_solution) + maybe_adjust_objective_sign(solution, model.objective.sense, io_api, "cbc") return Result(status, solution) @@ -303,6 +318,7 @@ def get_solver_solution() -> Solution: return Solution(sol, dual, objective) solution = safe_get_solution(status, get_solver_solution) + maybe_adjust_objective_sign(solution, model.objective.sense, io_api, "glpk") return Result(status, solution) @@ -393,6 +409,7 @@ def get_solver_solution() -> Solution: return Solution(sol, dual, objective) solution = safe_get_solution(status, get_solver_solution) + maybe_adjust_objective_sign(solution, model.objective.sense, io_api, "highs") return Result(status, solution, h) @@ -497,6 +514,7 @@ def get_solver_solution() -> Solution: return Solution(solution, dual, objective) solution = safe_get_solution(status, get_solver_solution) + maybe_adjust_objective_sign(solution, model.objective.sense, io_api, "cplex") return Result(status, solution, m) @@ -596,6 +614,7 @@ def get_solver_solution() -> Solution: return Solution(sol, dual, objective) solution = safe_get_solution(status, get_solver_solution) + maybe_adjust_objective_sign(solution, model.objective.sense, io_api, "gurobi") return Result(status, solution, m) @@ -688,6 +707,7 @@ def get_solver_solution() -> Solution: return Solution(sol, dual, objective) solution = safe_get_solution(status, get_solver_solution) + maybe_adjust_objective_sign(solution, model.objective.sense, io_api, "xpress") return Result(status, solution, m) diff --git a/test/test_io.py b/test/test_io.py index 2792c7ec..f7775d4a 100644 --- a/test/test_io.py +++ b/test/test_io.py @@ -34,7 +34,7 @@ def test_to_netcdf(m, tmp_path): p = read_netcdf(fn) for k in m.scalar_attrs: - if k != "objective_value": + if k != "objective.value": assert getattr(m, k) == getattr(p, k) for k in m.dataset_attrs: diff --git a/test/test_objective.py b/test/test_objective.py new file mode 100644 index 00000000..7cded4eb --- /dev/null +++ b/test/test_objective.py @@ -0,0 +1,143 @@ +import pytest +from scipy.sparse import csc_matrix + +from linopy import Model +from linopy.expressions import LinearExpression, QuadraticExpression +from linopy.objective import Objective + + +@pytest.fixture +def linear_objective(): + m = Model() + linear_expr = LinearExpression(None, m) + m.objective = Objective(linear_expr, m, sense="min") + return m.objective + + +@pytest.fixture +def quadratic_objective(): + m = Model() + quadratic_expr = QuadraticExpression(None, m) + m.objective = Objective(quadratic_expr, m, sense="max") + return m.objective + + +def test_model(linear_objective, quadratic_objective): + assert isinstance(linear_objective.model, Model) + assert isinstance(quadratic_objective.model, Model) + + +def test_sense(linear_objective, quadratic_objective): + assert linear_objective.sense == "min" + assert quadratic_objective.sense == "max" + + assert linear_objective.model.sense == "min" + assert quadratic_objective.model.sense == "max" + + +def test_set_sense(linear_objective, quadratic_objective): + linear_objective.sense = "max" + quadratic_objective.sense = "min" + + assert linear_objective.sense == "max" + assert quadratic_objective.sense == "min" + + assert linear_objective.model.sense == "max" + assert quadratic_objective.model.sense == "min" + + +def test_set_sense_via_model(linear_objective, quadratic_objective): + linear_objective.model.sense = "max" + quadratic_objective.model.sense = "min" + + assert linear_objective.sense == "max" + assert quadratic_objective.sense == "min" + + +def test_sense_setter_error(linear_objective): + with pytest.raises(ValueError): + linear_objective.sense = "not min or max" + + +def test_expression(linear_objective, quadratic_objective): + assert isinstance(linear_objective.expression, LinearExpression) + assert isinstance(quadratic_objective.expression, QuadraticExpression) + + +def test_value(linear_objective, quadratic_objective): + assert linear_objective.value is None + assert quadratic_objective.value is None + + +def test_set_value(linear_objective, quadratic_objective): + linear_objective.set_value(1) + quadratic_objective.set_value(2) + assert linear_objective.value == 1 + assert quadratic_objective.value == 2 + + +def test_set_value_error(linear_objective): + with pytest.raises(ValueError): + linear_objective.set_value("not a number") + + +def test_assign(linear_objective): + assert isinstance(linear_objective.assign(one=1), Objective) + + +def test_sel(linear_objective): + assert isinstance(linear_objective.sel(_term=[]), Objective) + + +def test_is_linear(linear_objective, quadratic_objective): + assert linear_objective.is_linear == True + assert quadratic_objective.is_linear == False + + +def test_is_quadratic(linear_objective, quadratic_objective): + assert linear_objective.is_quadratic == False + assert quadratic_objective.is_quadratic == True + + +def test_to_matrix(linear_objective, quadratic_objective): + with pytest.raises(ValueError): + linear_objective.to_matrix() + assert isinstance(quadratic_objective.to_matrix(), csc_matrix) + + +def test_add(linear_objective, quadratic_objective): + obj = linear_objective + quadratic_objective + assert isinstance(obj, Objective) + assert isinstance(obj.expression, QuadraticExpression) + + +def test_sub(linear_objective, quadratic_objective): + obj = quadratic_objective - linear_objective + assert isinstance(obj, Objective) + assert isinstance(obj.expression, QuadraticExpression) + + +def test_mul(quadratic_objective): + obj = quadratic_objective * 2 + assert isinstance(obj, Objective) + assert isinstance(obj.expression, QuadraticExpression) + + +def test_neg(quadratic_objective): + obj = -quadratic_objective + assert isinstance(obj, Objective) + assert isinstance(obj.expression, QuadraticExpression) + + +def test_truediv(quadratic_objective): + obj = quadratic_objective / 2 + assert isinstance(obj, Objective) + assert isinstance(obj.expression, QuadraticExpression) + + +def test_repr(linear_objective, quadratic_objective): + assert isinstance(linear_objective.__repr__(), str) + assert isinstance(quadratic_objective.__repr__(), str) + + assert "Linear" in linear_objective.__repr__() + assert "Quadratic" in quadratic_objective.__repr__() diff --git a/test/test_optimization.py b/test/test_optimization.py index 5b2d7d9b..72c3d06f 100644 --- a/test/test_optimization.py +++ b/test/test_optimization.py @@ -11,7 +11,7 @@ import pytest from xarray.testing import assert_equal -from linopy import GREATER_EQUAL, Model +from linopy import GREATER_EQUAL, LESS_EQUAL, Model from linopy.constants import SolverStatus, Status, TerminationCondition from linopy.solvers import available_solvers, quadratic_solvers @@ -68,6 +68,20 @@ def model_chunked(): return m +@pytest.fixture +def model_maximization(): + m = Model() + + x = m.add_variables(name="x") + y = m.add_variables(name="y") + + m.add_constraints(2 * x + 6 * y, LESS_EQUAL, 10) + m.add_constraints(4 * x + 2 * y, LESS_EQUAL, 3) + + m.add_objective(2 * y + x, sense="max") + return m + + @pytest.fixture def model_with_inf(): m = Model() @@ -275,9 +289,10 @@ def test_model_types( @pytest.mark.parametrize("solver,io_api", params) def test_default_setting(model, solver, io_api): + assert model.objective.sense == "min" status, condition = model.solve(solver, io_api=io_api) assert status == "ok" - assert np.isclose(model.objective_value, 3.3) + assert np.isclose(model.objective.value, 3.3) @pytest.mark.parametrize("solver,io_api", params) @@ -294,17 +309,27 @@ def test_default_setting_sol_and_dual_accessor(model, solver, io_api): def test_anonymous_constraint(model, model_anonymous_constraint, solver, io_api): status, condition = model_anonymous_constraint.solve(solver, io_api=io_api) assert status == "ok" - assert np.isclose(model_anonymous_constraint.objective_value, 3.3) + assert np.isclose(model_anonymous_constraint.objective.value, 3.3) model.solve(solver, io_api=io_api) assert_equal(model.solution, model_anonymous_constraint.solution) +@pytest.mark.parametrize("solver,io_api", params) +def test_model_maximization(model_maximization, solver, io_api): + m = model_maximization + assert m.objective.sense == "max" + assert m.objective.value is None + status, condition = m.solve(solver, io_api=io_api) + assert status == "ok" + assert np.isclose(m.objective.value, 3.3) + + @pytest.mark.parametrize("solver,io_api", params) def test_default_settings_chunked(model_chunked, solver, io_api): status, condition = model_chunked.solve(solver, io_api=io_api) assert status == "ok" - assert np.isclose(model_chunked.objective_value, 3.3) + assert np.isclose(model_chunked.objective.value, 3.3) @pytest.mark.parametrize("solver,io_api", params) @@ -440,7 +465,7 @@ def test_quadratic_model(quadratic_model, solver, io_api): assert condition == "optimal" assert (quadratic_model.solution.x.round(3) == 0).all() assert (quadratic_model.solution.y.round(3) == 10).all() - assert round(quadratic_model.objective_value, 3) == 0 + assert round(quadratic_model.objective.value, 3) == 0 else: with pytest.raises(ValueError): quadratic_model.solve(solver, io_api=io_api) @@ -453,7 +478,7 @@ def test_quadratic_model_cross_terms(quadratic_model_cross_terms, solver, io_api assert condition == "optimal" assert (quadratic_model_cross_terms.solution.x.round(3) == 1.5).all() assert (quadratic_model_cross_terms.solution.y.round(3) == 8.5).all() - assert round(quadratic_model_cross_terms.objective_value, 3) == 77.5 + assert round(quadratic_model_cross_terms.objective.value, 3) == 77.5 else: with pytest.raises(ValueError): quadratic_model_cross_terms.solve(solver, io_api=io_api) @@ -466,7 +491,7 @@ def test_quadratic_model_wo_constraint(quadratic_model, solver, io_api): status, condition = quadratic_model.solve(solver, io_api=io_api) assert condition == "optimal" assert (quadratic_model.solution.x.round(3) == 0).all() - assert round(quadratic_model.objective_value, 3) == 0 + assert round(quadratic_model.objective.value, 3) == 0 else: with pytest.raises(ValueError): quadratic_model.solve(solver, io_api=io_api) From ed67bb47471a3594ae26912579bdfaf19a0cc3b8 Mon Sep 17 00:00:00 2001 From: Fabian Date: Tue, 24 Oct 2023 10:13:03 +0200 Subject: [PATCH 2/2] objective class: increase test coverage --- linopy/objective.py | 3 --- test/test_objective.py | 24 ++++++++++++++++++++++++ test/test_optimization.py | 3 +++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/linopy/objective.py b/linopy/objective.py index cbfdd84a..426a6879 100644 --- a/linopy/objective.py +++ b/linopy/objective.py @@ -99,9 +99,6 @@ def expression(self, expr): if isinstance(expr, (list, tuple)): expr = self.model.linexpr(*expr) - if isinstance(expr, Objective): - expr = expr.expression - if not isinstance( expr, (expressions.LinearExpression, expressions.QuadraticExpression) ): diff --git a/test/test_objective.py b/test/test_objective.py index 7cded4eb..f3ec9524 100644 --- a/test/test_objective.py +++ b/test/test_objective.py @@ -111,12 +111,24 @@ def test_add(linear_objective, quadratic_objective): assert isinstance(obj.expression, QuadraticExpression) +def test_add_expr(linear_objective, quadratic_objective): + obj = linear_objective + quadratic_objective.expression + assert isinstance(obj, Objective) + assert isinstance(obj.expression, QuadraticExpression) + + def test_sub(linear_objective, quadratic_objective): obj = quadratic_objective - linear_objective assert isinstance(obj, Objective) assert isinstance(obj.expression, QuadraticExpression) +def test_sub_epxr(linear_objective, quadratic_objective): + obj = quadratic_objective - linear_objective.expression + assert isinstance(obj, Objective) + assert isinstance(obj.expression, QuadraticExpression) + + def test_mul(quadratic_objective): obj = quadratic_objective * 2 assert isinstance(obj, Objective) @@ -135,9 +147,21 @@ def test_truediv(quadratic_objective): assert isinstance(obj.expression, QuadraticExpression) +def test_truediv_false(quadratic_objective): + with pytest.raises(ValueError): + quadratic_objective / quadratic_objective + + def test_repr(linear_objective, quadratic_objective): assert isinstance(linear_objective.__repr__(), str) assert isinstance(quadratic_objective.__repr__(), str) assert "Linear" in linear_objective.__repr__() assert "Quadratic" in quadratic_objective.__repr__() + + +def test_objective_constant(): + m = Model() + linear_expr = LinearExpression(None, m) + 1 + with pytest.raises(ValueError): + m.objective = Objective(linear_expr, m) diff --git a/test/test_optimization.py b/test/test_optimization.py index 72c3d06f..4322c5d7 100644 --- a/test/test_optimization.py +++ b/test/test_optimization.py @@ -294,6 +294,9 @@ def test_default_setting(model, solver, io_api): assert status == "ok" assert np.isclose(model.objective.value, 3.3) + with pytest.warns(DeprecationWarning): + assert np.isclose(model.objective_value, 3.3) + @pytest.mark.parametrize("solver,io_api", params) def test_default_setting_sol_and_dual_accessor(model, solver, io_api):