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")