From 7de16e22010d24b4c8eb93dd3699d47434e47087 Mon Sep 17 00:00:00 2001 From: Bruno Vieira Date: Mon, 29 Dec 2025 14:23:04 +0000 Subject: [PATCH] Add Xpress 9.8+ API support with backward compatibility --- doc/index.rst | 2 +- doc/prerequisites.rst | 2 +- linopy/constraints.py | 9 ++++- linopy/model.py | 76 +++++++++++++++++++++++++++++------------- linopy/solvers.py | 77 +++++++++++++++++++++++++++++-------------- 5 files changed, 115 insertions(+), 51 deletions(-) diff --git a/doc/index.rst b/doc/index.rst index 3cf488ba..4825722e 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -44,7 +44,7 @@ flexible data-handling features: - `HiGHS `__ - `MindOpt `__ - `Gurobi `__ - - `Xpress `__ + - `Xpress `__ - `Cplex `__ - `MOSEK `__ - `COPT `__ diff --git a/doc/prerequisites.rst b/doc/prerequisites.rst index 6ff446df..88c95ba3 100644 --- a/doc/prerequisites.rst +++ b/doc/prerequisites.rst @@ -34,7 +34,7 @@ Linopy won't work without a solver. Currently, the following solvers are support - `GLPK `__ - open source, free, not very fast - `HiGHS `__ - open source, free, fast - `Gurobi `__ - closed source, commercial, very fast -- `Xpress `__ - closed source, commercial, very fast +- `Xpress `__ - closed source, commercial, very fast - `Cplex `__ - closed source, commercial, very fast - `MOSEK `__ - `MindOpt `__ - diff --git a/linopy/constraints.py b/linopy/constraints.py index 6ddb9b2e..291beb1d 100644 --- a/linopy/constraints.py +++ b/linopy/constraints.py @@ -1015,7 +1015,14 @@ def print_labels( if display_max_terms is not None: opts.set_value(display_max_terms=display_max_terms) res = [print_single_constraint(self.model, v) for v in values] - print("\n".join(res)) + + output = "\n".join(res) + try: + print(output) + except UnicodeEncodeError: + # Replace Unicode math symbols with ASCII equivalents for Windows console + output = output.replace("≤", "<=").replace("≥", ">=").replace("≠", "!=") + print(output) def set_blocks(self, block_map: np.ndarray) -> None: """ diff --git a/linopy/model.py b/linopy/model.py index 81c069ab..7e028441 100644 --- a/linopy/model.py +++ b/linopy/model.py @@ -1458,7 +1458,10 @@ def _compute_infeasibilities_gurobi(self, solver_model: Any) -> list[int]: def _compute_infeasibilities_xpress(self, solver_model: Any) -> list[int]: """Compute infeasibilities for Xpress solver.""" # Compute all IIS - solver_model.iisall() + try: # Try new API first + solver_model.IISAll() + except AttributeError: # Fallback to old API + solver_model.iisall() # Get the number of IIS found num_iis = solver_model.attributes.numiis @@ -1502,28 +1505,55 @@ def _extract_iis_constraints(self, solver_model: Any, iis_num: int) -> list[Any] list[Any] List of xpress.constraint objects in the IIS """ - # Prepare lists to receive IIS data - miisrow: list[Any] = [] # xpress.constraint objects in the IIS - miiscol: list[Any] = [] # xpress.variable objects in the IIS - constrainttype: list[str] = [] # Constraint types ('L', 'G', 'E') - colbndtype: list[str] = [] # Column bound types - duals: list[float] = [] # Dual values - rdcs: list[float] = [] # Reduced costs - isolationrows: list[str] = [] # Row isolation info - isolationcols: list[str] = [] # Column isolation info - - # Get IIS data from Xpress - solver_model.getiisdata( - iis_num, - miisrow, - miiscol, - constrainttype, - colbndtype, - duals, - rdcs, - isolationrows, - isolationcols, - ) + # Declare variables before try/except to avoid mypy redefinition errors + miisrow: list[Any] + miiscol: list[Any] + constrainttype: list[str] + colbndtype: list[str] + duals: list[float] + rdcs: list[float] + isolationrows: list[str] + isolationcols: list[str] + + try: # Try new API first + ( + miisrow, + miiscol, + constrainttype, + colbndtype, + duals, + rdcs, + isolationrows, + isolationcols, + ) = solver_model.getIISData(iis_num) + + # Transform list of indices to list of constraint objects + for i in range(len(miisrow)): + miisrow[i] = solver_model.getConstraint(miisrow[i]) + + except AttributeError: # Fallback to old API + # Prepare lists to receive IIS data + miisrow = [] # xpress.constraint objects in the IIS + miiscol = [] # xpress.variable objects in the IIS + constrainttype = [] # Constraint types ('L', 'G', 'E') + colbndtype = [] # Column bound types + duals = [] # Dual values + rdcs = [] # Reduced costs + isolationrows = [] # Row isolation info + isolationcols = [] # Column isolation info + + # Get IIS data from Xpress + solver_model.getiisdata( + iis_num, + miisrow, + miiscol, + constrainttype, + colbndtype, + duals, + rdcs, + isolationrows, + isolationcols, + ) return miisrow diff --git a/linopy/solvers.py b/linopy/solvers.py index 2783e7b8..278eb5b9 100644 --- a/linopy/solvers.py +++ b/linopy/solvers.py @@ -1559,13 +1559,11 @@ def solve_problem_from_file( Result """ CONDITION_MAP = { - "lp_optimal": "optimal", - "mip_optimal": "optimal", - "lp_infeasible": "infeasible", - "lp_infeas": "infeasible", - "mip_infeasible": "infeasible", - "lp_unbounded": "unbounded", - "mip_unbounded": "unbounded", + xpress.SolStatus.NOTFOUND: "unknown", + xpress.SolStatus.OPTIMAL: "optimal", + xpress.SolStatus.FEASIBLE: "terminated_by_limit", + xpress.SolStatus.INFEASIBLE: "infeasible", + xpress.SolStatus.UNBOUNDED: "unbounded", } io_api = read_io_api_from_problem_file(problem_fn) @@ -1573,49 +1571,78 @@ def solve_problem_from_file( m = xpress.problem() - m.read(path_to_string(problem_fn)) - m.setControl(self.solver_options) + try: # Try new API first + m.readProb(path_to_string(problem_fn)) + except AttributeError: # Fallback to old API + m.read(path_to_string(problem_fn)) + + # Set solver options - new API uses setControl per option, old API accepts dict + if self.solver_options is not None: + m.setControl(self.solver_options) if log_fn is not None: - m.setlogfile(path_to_string(log_fn)) + try: # Try new API first + m.setLogFile(path_to_string(log_fn)) + except AttributeError: # Fallback to old API + m.setlogfile(path_to_string(log_fn)) if warmstart_fn is not None: - m.readbasis(path_to_string(warmstart_fn)) + try: # Try new API first + m.readBasis(path_to_string(warmstart_fn)) + except AttributeError: # Fallback to old API + m.readbasis(path_to_string(warmstart_fn)) - m.solve() + m.optimize() # if the solver is stopped (timelimit for example), postsolve the problem - if m.getAttrib("solvestatus") == xpress.solvestatus_stopped: - m.postsolve() + if m.attributes.solvestatus == xpress.enums.SolveStatus.STOPPED: + try: # Try new API first + m.postSolve() + except AttributeError: # Fallback to old API + m.postsolve() if basis_fn is not None: try: - m.writebasis(path_to_string(basis_fn)) - except Exception as err: + try: # Try new API first + m.writeBasis(path_to_string(basis_fn)) + except AttributeError: # Fallback to old API + m.writebasis(path_to_string(basis_fn)) + except (xpress.SolverError, xpress.ModelError) as err: logger.info("No model basis stored. Raised error: %s", err) if solution_fn is not None: try: - m.writebinsol(path_to_string(solution_fn)) - except Exception as err: + try: # Try new API first + m.writeBinSol(path_to_string(solution_fn)) + except AttributeError: # Fallback to old API + m.writebinsol(path_to_string(solution_fn)) + except (xpress.SolverError, xpress.ModelError) as err: logger.info("Unable to save solution file. Raised error: %s", err) - condition = m.getProbStatusString() + condition = m.attributes.solstatus 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() + objective = m.attributes.objval - var = m.getnamelist(xpress_Namespaces.COLUMN, 0, m.attributes.cols - 1) + try: # Try new API first + var = m.getNameList(xpress_Namespaces.COLUMN, 0, m.attributes.cols - 1) + except AttributeError: # Fallback to old API + var = m.getnamelist(xpress_Namespaces.COLUMN, 0, m.attributes.cols - 1) sol = pd.Series(m.getSolution(), index=var, dtype=float) try: - _dual = m.getDual() - constraints = m.getnamelist( - xpress_Namespaces.ROW, 0, m.attributes.rows - 1 - ) + _dual = m.getDuals() + try: # Try new API first + constraints = m.getNameList( + xpress_Namespaces.ROW, 0, m.attributes.rows - 1 + ) + except AttributeError: # Fallback to old API + constraints = m.getnamelist( + xpress_Namespaces.ROW, 0, m.attributes.rows - 1 + ) dual = pd.Series(_dual, index=constraints, dtype=float) except (xpress.SolverError, xpress.ModelError, SystemError): logger.warning("Dual values of MILP couldn't be parsed")