Skip to content

Commit 643d29a

Browse files
Add Xpress 9.8+ API support with backward compatibility (#542)
1 parent c5c004e commit 643d29a

5 files changed

Lines changed: 115 additions & 51 deletions

File tree

doc/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ flexible data-handling features:
4444
- `HiGHS <https://www.maths.ed.ac.uk/hall/HiGHS/>`__
4545
- `MindOpt <https://solver.damo.alibaba.com/doc/en/html/index.html>`__
4646
- `Gurobi <https://www.gurobi.com/>`__
47-
- `Xpress <https://www.fico.com/en/products/fico-xpress-solver>`__
47+
- `Xpress <https://www.fico.com/en/fico-xpress-trial-and-licensing-options>`__
4848
- `Cplex <https://www.ibm.com/de-de/analytics/cplex-optimizer>`__
4949
- `MOSEK <https://www.mosek.com/>`__
5050
- `COPT <https://www.shanshu.ai/copt>`__

doc/prerequisites.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Linopy won't work without a solver. Currently, the following solvers are support
3434
- `GLPK <https://www.gnu.org/software/glpk/>`__ - open source, free, not very fast
3535
- `HiGHS <https://www.maths.ed.ac.uk/hall/HiGHS/>`__ - open source, free, fast
3636
- `Gurobi <https://www.gurobi.com/>`__ - closed source, commercial, very fast
37-
- `Xpress <https://www.fico.com/en/products/fico-xpress-solver>`__ - closed source, commercial, very fast
37+
- `Xpress <https://www.fico.com/en/fico-xpress-trial-and-licensing-options>`__ - closed source, commercial, very fast
3838
- `Cplex <https://www.ibm.com/de-de/analytics/cplex-optimizer>`__ - closed source, commercial, very fast
3939
- `MOSEK <https://www.mosek.com/>`__
4040
- `MindOpt <https://solver.damo.alibaba.com/doc/en/html/index.html>`__ -

linopy/constraints.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1015,7 +1015,14 @@ def print_labels(
10151015
if display_max_terms is not None:
10161016
opts.set_value(display_max_terms=display_max_terms)
10171017
res = [print_single_constraint(self.model, v) for v in values]
1018-
print("\n".join(res))
1018+
1019+
output = "\n".join(res)
1020+
try:
1021+
print(output)
1022+
except UnicodeEncodeError:
1023+
# Replace Unicode math symbols with ASCII equivalents for Windows console
1024+
output = output.replace("≤", "<=").replace("≥", ">=").replace("≠", "!=")
1025+
print(output)
10191026

10201027
def set_blocks(self, block_map: np.ndarray) -> None:
10211028
"""

linopy/model.py

Lines changed: 53 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1458,7 +1458,10 @@ def _compute_infeasibilities_gurobi(self, solver_model: Any) -> list[int]:
14581458
def _compute_infeasibilities_xpress(self, solver_model: Any) -> list[int]:
14591459
"""Compute infeasibilities for Xpress solver."""
14601460
# Compute all IIS
1461-
solver_model.iisall()
1461+
try: # Try new API first
1462+
solver_model.IISAll()
1463+
except AttributeError: # Fallback to old API
1464+
solver_model.iisall()
14621465

14631466
# Get the number of IIS found
14641467
num_iis = solver_model.attributes.numiis
@@ -1502,28 +1505,55 @@ def _extract_iis_constraints(self, solver_model: Any, iis_num: int) -> list[Any]
15021505
list[Any]
15031506
List of xpress.constraint objects in the IIS
15041507
"""
1505-
# Prepare lists to receive IIS data
1506-
miisrow: list[Any] = [] # xpress.constraint objects in the IIS
1507-
miiscol: list[Any] = [] # xpress.variable objects in the IIS
1508-
constrainttype: list[str] = [] # Constraint types ('L', 'G', 'E')
1509-
colbndtype: list[str] = [] # Column bound types
1510-
duals: list[float] = [] # Dual values
1511-
rdcs: list[float] = [] # Reduced costs
1512-
isolationrows: list[str] = [] # Row isolation info
1513-
isolationcols: list[str] = [] # Column isolation info
1514-
1515-
# Get IIS data from Xpress
1516-
solver_model.getiisdata(
1517-
iis_num,
1518-
miisrow,
1519-
miiscol,
1520-
constrainttype,
1521-
colbndtype,
1522-
duals,
1523-
rdcs,
1524-
isolationrows,
1525-
isolationcols,
1526-
)
1508+
# Declare variables before try/except to avoid mypy redefinition errors
1509+
miisrow: list[Any]
1510+
miiscol: list[Any]
1511+
constrainttype: list[str]
1512+
colbndtype: list[str]
1513+
duals: list[float]
1514+
rdcs: list[float]
1515+
isolationrows: list[str]
1516+
isolationcols: list[str]
1517+
1518+
try: # Try new API first
1519+
(
1520+
miisrow,
1521+
miiscol,
1522+
constrainttype,
1523+
colbndtype,
1524+
duals,
1525+
rdcs,
1526+
isolationrows,
1527+
isolationcols,
1528+
) = solver_model.getIISData(iis_num)
1529+
1530+
# Transform list of indices to list of constraint objects
1531+
for i in range(len(miisrow)):
1532+
miisrow[i] = solver_model.getConstraint(miisrow[i])
1533+
1534+
except AttributeError: # Fallback to old API
1535+
# Prepare lists to receive IIS data
1536+
miisrow = [] # xpress.constraint objects in the IIS
1537+
miiscol = [] # xpress.variable objects in the IIS
1538+
constrainttype = [] # Constraint types ('L', 'G', 'E')
1539+
colbndtype = [] # Column bound types
1540+
duals = [] # Dual values
1541+
rdcs = [] # Reduced costs
1542+
isolationrows = [] # Row isolation info
1543+
isolationcols = [] # Column isolation info
1544+
1545+
# Get IIS data from Xpress
1546+
solver_model.getiisdata(
1547+
iis_num,
1548+
miisrow,
1549+
miiscol,
1550+
constrainttype,
1551+
colbndtype,
1552+
duals,
1553+
rdcs,
1554+
isolationrows,
1555+
isolationcols,
1556+
)
15271557

15281558
return miisrow
15291559

linopy/solvers.py

Lines changed: 52 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1559,63 +1559,90 @@ def solve_problem_from_file(
15591559
Result
15601560
"""
15611561
CONDITION_MAP = {
1562-
"lp_optimal": "optimal",
1563-
"mip_optimal": "optimal",
1564-
"lp_infeasible": "infeasible",
1565-
"lp_infeas": "infeasible",
1566-
"mip_infeasible": "infeasible",
1567-
"lp_unbounded": "unbounded",
1568-
"mip_unbounded": "unbounded",
1562+
xpress.SolStatus.NOTFOUND: "unknown",
1563+
xpress.SolStatus.OPTIMAL: "optimal",
1564+
xpress.SolStatus.FEASIBLE: "terminated_by_limit",
1565+
xpress.SolStatus.INFEASIBLE: "infeasible",
1566+
xpress.SolStatus.UNBOUNDED: "unbounded",
15691567
}
15701568

15711569
io_api = read_io_api_from_problem_file(problem_fn)
15721570
sense = read_sense_from_problem_file(problem_fn)
15731571

15741572
m = xpress.problem()
15751573

1576-
m.read(path_to_string(problem_fn))
1577-
m.setControl(self.solver_options)
1574+
try: # Try new API first
1575+
m.readProb(path_to_string(problem_fn))
1576+
except AttributeError: # Fallback to old API
1577+
m.read(path_to_string(problem_fn))
1578+
1579+
# Set solver options - new API uses setControl per option, old API accepts dict
1580+
if self.solver_options is not None:
1581+
m.setControl(self.solver_options)
15781582

15791583
if log_fn is not None:
1580-
m.setlogfile(path_to_string(log_fn))
1584+
try: # Try new API first
1585+
m.setLogFile(path_to_string(log_fn))
1586+
except AttributeError: # Fallback to old API
1587+
m.setlogfile(path_to_string(log_fn))
15811588

15821589
if warmstart_fn is not None:
1583-
m.readbasis(path_to_string(warmstart_fn))
1590+
try: # Try new API first
1591+
m.readBasis(path_to_string(warmstart_fn))
1592+
except AttributeError: # Fallback to old API
1593+
m.readbasis(path_to_string(warmstart_fn))
15841594

1585-
m.solve()
1595+
m.optimize()
15861596

15871597
# if the solver is stopped (timelimit for example), postsolve the problem
1588-
if m.getAttrib("solvestatus") == xpress.solvestatus_stopped:
1589-
m.postsolve()
1598+
if m.attributes.solvestatus == xpress.enums.SolveStatus.STOPPED:
1599+
try: # Try new API first
1600+
m.postSolve()
1601+
except AttributeError: # Fallback to old API
1602+
m.postsolve()
15901603

15911604
if basis_fn is not None:
15921605
try:
1593-
m.writebasis(path_to_string(basis_fn))
1594-
except Exception as err:
1606+
try: # Try new API first
1607+
m.writeBasis(path_to_string(basis_fn))
1608+
except AttributeError: # Fallback to old API
1609+
m.writebasis(path_to_string(basis_fn))
1610+
except (xpress.SolverError, xpress.ModelError) as err:
15951611
logger.info("No model basis stored. Raised error: %s", err)
15961612

15971613
if solution_fn is not None:
15981614
try:
1599-
m.writebinsol(path_to_string(solution_fn))
1600-
except Exception as err:
1615+
try: # Try new API first
1616+
m.writeBinSol(path_to_string(solution_fn))
1617+
except AttributeError: # Fallback to old API
1618+
m.writebinsol(path_to_string(solution_fn))
1619+
except (xpress.SolverError, xpress.ModelError) as err:
16011620
logger.info("Unable to save solution file. Raised error: %s", err)
16021621

1603-
condition = m.getProbStatusString()
1622+
condition = m.attributes.solstatus
16041623
termination_condition = CONDITION_MAP.get(condition, condition)
16051624
status = Status.from_termination_condition(termination_condition)
16061625
status.legacy_status = condition
16071626

16081627
def get_solver_solution() -> Solution:
1609-
objective = m.getObjVal()
1628+
objective = m.attributes.objval
16101629

1611-
var = m.getnamelist(xpress_Namespaces.COLUMN, 0, m.attributes.cols - 1)
1630+
try: # Try new API first
1631+
var = m.getNameList(xpress_Namespaces.COLUMN, 0, m.attributes.cols - 1)
1632+
except AttributeError: # Fallback to old API
1633+
var = m.getnamelist(xpress_Namespaces.COLUMN, 0, m.attributes.cols - 1)
16121634
sol = pd.Series(m.getSolution(), index=var, dtype=float)
16131635

16141636
try:
1615-
_dual = m.getDual()
1616-
constraints = m.getnamelist(
1617-
xpress_Namespaces.ROW, 0, m.attributes.rows - 1
1618-
)
1637+
_dual = m.getDuals()
1638+
try: # Try new API first
1639+
constraints = m.getNameList(
1640+
xpress_Namespaces.ROW, 0, m.attributes.rows - 1
1641+
)
1642+
except AttributeError: # Fallback to old API
1643+
constraints = m.getnamelist(
1644+
xpress_Namespaces.ROW, 0, m.attributes.rows - 1
1645+
)
16191646
dual = pd.Series(_dual, index=constraints, dtype=float)
16201647
except (xpress.SolverError, xpress.ModelError, SystemError):
16211648
logger.warning("Dual values of MILP couldn't be parsed")

0 commit comments

Comments
 (0)