Skip to content
Merged
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
14 changes: 14 additions & 0 deletions docs/home/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,20 @@ For academic use, Gurobi offers free licenses:
```
3. Activate your license following Gurobi's instructions

### FICO Xpress (Optional)

Xpress ships with a free Community license for small models (up to 5000 rows and columns combined). Academic and commercial licenses are available from [FICO](https://www.fico.com/en/fico-xpress-community-license):

1. Install Xpress:
```bash
pip install xpress
```
2. For larger models, point Xpress to your license file (e.g. via the `XPAUTH_PATH` environment variable)
3. Use it via `fx.solvers.XpressSolver()`

!!! note "Version requirements"
`XpressSolver` needs Xpress 9.0 or newer (for the `TIMELIMIT` control). Use linopy 0.5.8 or newer: older versions cannot return a solution when Xpress stops at the time limit.

## Verification

Verify your installation by running:
Expand Down
1 change: 1 addition & 0 deletions docs/user-guide/optimization/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ Variables follow this naming pattern:
| **HiGHS** | Open-source | Fast | Free |
| **Gurobi** | Commercial | Fastest | Academic/Commercial |
| **CPLEX** | Commercial | Fastest | Academic/Commercial |
| **Xpress** | Commercial | Fastest | Free Community/Academic/Commercial |
| **GLPK** | Open-source | Slower | Free |

**Recommendation:** Start with HiGHS (included by default). Use Gurobi/CPLEX for large models or when speed matters.
Expand Down
30 changes: 30 additions & 0 deletions flixopt/solvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ def _options(self) -> dict[str, Any]:
raise NotImplementedError


@dataclass
class GurobiSolver(_Solver):
"""
Gurobi solver configuration.
Expand All @@ -64,6 +65,7 @@ def _options(self) -> dict[str, Any]:
}


@dataclass
class HighsSolver(_Solver):
"""
HiGHS solver configuration.
Expand All @@ -87,3 +89,31 @@ def _options(self) -> dict[str, Any]:
'threads': self.threads,
'log_to_console': self.log_to_console,
}


@dataclass
class XpressSolver(_Solver):
"""
FICO Xpress solver configuration.

Requires Xpress >= 9.0 (for `TIMELIMIT`) and linopy >= 0.5.8 (to retrieve solutions after hitting the time limit).

Args:
mip_gap: Acceptable relative optimality gap in [0.0, 1.0]; mapped to Xpress `MIPRELSTOP`.
time_limit_seconds: Time limit in seconds; mapped to Xpress `TIMELIMIT`.
log_to_console: If False, no output to console; mapped to Xpress `OUTPUTLOG`.
extra_options: Additional Xpress controls merged into `options`.
threads (int | None): Number of threads to use; mapped to Xpress `THREADS`. If None, Xpress chooses.
"""

threads: int | None = None
name: ClassVar[str] = 'xpress'

@property
def _options(self) -> dict[str, Any]:
return {
'MIPRELSTOP': self.mip_gap,
'TIMELIMIT': self.time_limit_seconds,
'THREADS': self.threads,
'OUTPUTLOG': 1 if self.log_to_console else 0,
}
8 changes: 7 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,13 @@ def gurobi_solver():
return fx.solvers.GurobiSolver(mip_gap=0, time_limit_seconds=300)


@pytest.fixture(params=[highs_solver, gurobi_solver], ids=['highs', 'gurobi'])
@pytest.fixture()
def xpress_solver():
pytest.importorskip('xpress', reason='Xpress not available in this environment')
return fx.solvers.XpressSolver(mip_gap=0, time_limit_seconds=300)


@pytest.fixture(params=[highs_solver, gurobi_solver, xpress_solver], ids=['highs', 'gurobi', 'xpress'])
def solver_fixture(request):
return request.getfixturevalue(request.param.__name__)

Expand Down
38 changes: 38 additions & 0 deletions tests/utilities/test_solvers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Tests for the solver configuration classes."""

import pytest

import flixopt as fx


def test_highs_threads():
solver = fx.solvers.HighsSolver(threads=2)
assert solver.threads == 2
assert solver.options['threads'] == 2


def test_xpress_threads():
solver = fx.solvers.XpressSolver(threads=2)
assert solver.threads == 2
assert solver.options['THREADS'] == 2


@pytest.mark.parametrize('solver_cls', [fx.solvers.HighsSolver, fx.solvers.XpressSolver])
def test_threads_omitted_by_default(solver_cls):
assert all(value is not None for value in solver_cls().options.values())
assert 'threads' not in solver_cls().options
assert 'THREADS' not in solver_cls().options


@pytest.mark.parametrize('solver_cls', [fx.solvers.GurobiSolver, fx.solvers.HighsSolver, fx.solvers.XpressSolver])
def test_positional_args(solver_cls):
solver = solver_cls(0.01, 60)
assert solver.mip_gap == 0.01
assert solver.time_limit_seconds == 60


def test_xpress_options():
solver = fx.solvers.XpressSolver(
mip_gap=0.05, time_limit_seconds=30, log_to_console=False, extra_options={'PRESOLVE': 0}
)
assert solver.options == {'MIPRELSTOP': 0.05, 'TIMELIMIT': 30, 'OUTPUTLOG': 0, 'PRESOLVE': 0}
Loading