Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ flexible data-handling features:
- `HiGHS <https://www.maths.ed.ac.uk/hall/HiGHS/>`__
- `MindOpt <https://solver.damo.alibaba.com/doc/en/html/index.html>`__
- `Gurobi <https://www.gurobi.com/>`__
- `Xpress <https://www.fico.com/en/products/fico-xpress-solver>`__
- `Xpress <https://www.fico.com/en/fico-xpress-trial-and-licensing-options>`__
- `Cplex <https://www.ibm.com/de-de/analytics/cplex-optimizer>`__
- `MOSEK <https://www.mosek.com/>`__
- `COPT <https://www.shanshu.ai/copt>`__
Expand Down
2 changes: 1 addition & 1 deletion doc/prerequisites.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Linopy won't work without a solver. Currently, the following solvers are support
- `GLPK <https://www.gnu.org/software/glpk/>`__ - open source, free, not very fast
- `HiGHS <https://www.maths.ed.ac.uk/hall/HiGHS/>`__ - open source, free, fast
- `Gurobi <https://www.gurobi.com/>`__ - closed source, commercial, very fast
- `Xpress <https://www.fico.com/en/products/fico-xpress-solver>`__ - closed source, commercial, very fast
- `Xpress <https://www.fico.com/en/fico-xpress-trial-and-licensing-options>`__ - closed source, commercial, very fast
- `Cplex <https://www.ibm.com/de-de/analytics/cplex-optimizer>`__ - closed source, commercial, very fast
- `MOSEK <https://www.mosek.com/>`__
- `MindOpt <https://solver.damo.alibaba.com/doc/en/html/index.html>`__ -
Expand Down
9 changes: 8 additions & 1 deletion linopy/constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand Down
76 changes: 53 additions & 23 deletions linopy/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
77 changes: 52 additions & 25 deletions linopy/solvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1559,63 +1559,90 @@ 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)
sense = read_sense_from_problem_file(problem_fn)

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