diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 730d3e04..e215a12b 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -2,7 +2,7 @@ Release Notes ============= .. Upcoming Version - +* xpress: Fixed for xpress v9.5 (broken since linopy v0.5.8), upgraded deprecated xpress methods * 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/solvers.py b/linopy/solvers.py index 2783e7b8..f718eb81 100644 --- a/linopy/solvers.py +++ b/linopy/solvers.py @@ -1600,21 +1600,22 @@ def solve_problem_from_file( except Exception as err: logger.info("Unable to save solution file. Raised error: %s", err) - condition = m.getProbStatusString() + condition: str = m.getAttrib("solstatus").name.lower() termination_condition = CONDITION_MAP.get(condition, condition) status = Status.from_termination_condition(termination_condition) status.legacy_status = condition - def get_solver_solution() -> Solution: - objective = m.getObjVal() + def get_solver_solution_new() -> Solution: + # For xpress >= 9.6 + objective: float = m.getAttrib("objval") - var = m.getnamelist(xpress_Namespaces.COLUMN, 0, m.attributes.cols - 1) + var = m.getnamelist(xpress.Namespaces.COLUMN, 0, m.attributes.cols - 1) sol = pd.Series(m.getSolution(), index=var, dtype=float) try: - _dual = m.getDual() + _dual = m.getDuals() constraints = m.getnamelist( - xpress_Namespaces.ROW, 0, m.attributes.rows - 1 + xpress.Namespaces.ROW, 0, m.attributes.rows - 1 ) dual = pd.Series(_dual, index=constraints, dtype=float) except (xpress.SolverError, xpress.ModelError, SystemError): @@ -1623,6 +1624,29 @@ def get_solver_solution() -> Solution: return Solution(sol, dual, objective) + def get_solver_solution_legacy() -> Solution: + # For xpress < 9.6 + objective: float = m.getAttrib("objval") + + var = [str(v) for v in m.getVariable()] + + sol = pd.Series(m.getSolution(var), index=var, dtype=float) + + try: + dual_ = [str(d) for d in m.getConstraint()] + dual = pd.Series(m.getDuals(dual_), index=dual_, dtype=float) + except (xpress.SolverError, xpress.ModelError, SystemError): + logger.warning("Dual values of MILP couldn't be parsed") + dual = pd.Series(dtype=float) + + return Solution(sol, dual, objective) + + def get_solver_solution() -> Solution: + if parse_version(xpress.__version__) >= parse_version("9.6"): + return get_solver_solution_new() + else: + return get_solver_solution_legacy() + solution = self.safe_get_solution(status=status, func=get_solver_solution) solution = maybe_adjust_objective_sign(solution, io_api, sense) diff --git a/test/test_optimization.py b/test/test_optimization.py index 12399a4e..6724158a 100644 --- a/test/test_optimization.py +++ b/test/test_optimization.py @@ -10,6 +10,7 @@ import itertools import logging from typing import Any +from unittest.mock import patch import numpy as np import pandas as pd @@ -641,6 +642,32 @@ def test_model_with_inf( assert (model_with_inf.solution.y == 10).all() +@pytest.mark.skipif( + "xpress" not in available_solvers, reason="Xpress solver not available" +) +def test_old_xpress_version_no_dual(model_with_inf: Model) -> None: + # Confirm that the old code path for xpress 9.5.x still works (when duals are not available) + with patch("xpress.__version__", "9.5.0"): + _, condition = model_with_inf.solve("xpress") + assert condition == "optimal" + assert (model_with_inf.solution.x == 0).all() + assert (model_with_inf.solution.y == 10).all() + assert len(model_with_inf.dual.keys()) == 0 + + +@pytest.mark.skipif( + "xpress" not in available_solvers, reason="Xpress solver not available" +) +def test_old_xpress_version_with_dual(model: Model) -> None: + # Confirm that the old code path for xpress 9.5.x still works (when duals are available) + with patch("xpress.__version__", "9.5.0"): + status, condition = model.solve("xpress") + assert status == "ok" + assert condition == "optimal" + assert np.isclose(model.objective.value or 0, 3.3) + assert np.isclose(model.dual["con0"].values, 0.3) + + @pytest.mark.parametrize( "solver,io_api,explicit_coordinate_names", [p for p in params if p[0] not in ["mindopt"]],