From 54b66d8ca411f35f4e4e375ec7506d449b2cc92c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 11:25:43 +0000 Subject: [PATCH 1/4] feat: add XpressSolver for FICO Xpress support Adds fx.solvers.XpressSolver, mapping mip_gap, time_limit_seconds and log_to_console to the Xpress controls MIPRELSTOP, TIMELIMIT and OUTPUTLOG. Linopy already supports Xpress, so this only adds the flixopt-side configuration. Tests run against Xpress when the xpress package is installed, and the docs cover installation. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QZ8AnG2jhX3n9Q2s2inYPm --- docs/home/installation.md | 11 +++++++++++ docs/user-guide/optimization/index.md | 1 + flixopt/solvers.py | 22 ++++++++++++++++++++++ tests/conftest.py | 8 +++++++- 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/docs/home/installation.md b/docs/home/installation.md index d61022074..76ee3a539 100644 --- a/docs/home/installation.md +++ b/docs/home/installation.md @@ -48,6 +48,17 @@ 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). 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()` + ## Verification Verify your installation by running: diff --git a/docs/user-guide/optimization/index.md b/docs/user-guide/optimization/index.md index 12571ad67..fdc50b0b6 100644 --- a/docs/user-guide/optimization/index.md +++ b/docs/user-guide/optimization/index.md @@ -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. diff --git a/flixopt/solvers.py b/flixopt/solvers.py index e5db61192..73513b2dd 100644 --- a/flixopt/solvers.py +++ b/flixopt/solvers.py @@ -87,3 +87,25 @@ def _options(self) -> dict[str, Any]: 'threads': self.threads, 'log_to_console': self.log_to_console, } + + +class XpressSolver(_Solver): + """ + FICO Xpress solver configuration. + + 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 (e.g. `{'THREADS': 4}`) merged into `options`. + """ + + name: ClassVar[str] = 'xpress' + + @property + def _options(self) -> dict[str, Any]: + return { + 'MIPRELSTOP': self.mip_gap, + 'TIMELIMIT': self.time_limit_seconds, + 'OUTPUTLOG': 1 if self.log_to_console else 0, + } diff --git a/tests/conftest.py b/tests/conftest.py index b3950cc35..8f574c3b0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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__) From 8e2667b9f10bb85dff029990eb1b080d105673b4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 13:07:44 +0000 Subject: [PATCH 2/4] fix: make solver subclasses dataclasses so threads works; add threads to XpressSolver HighsSolver declared a threads field, but the subclass was not a dataclass, so HighsSolver(threads=4) raised a TypeError. All solver subclasses are now dataclasses. XpressSolver gains threads, mapped to the Xpress THREADS control. threads comes after the base fields, so positional calls like HighsSolver(0.01, 60) are unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QZ8AnG2jhX3n9Q2s2inYPm --- flixopt/solvers.py | 8 ++++++- tests/utilities/test_solvers.py | 38 +++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 tests/utilities/test_solvers.py diff --git a/flixopt/solvers.py b/flixopt/solvers.py index 73513b2dd..484b287bf 100644 --- a/flixopt/solvers.py +++ b/flixopt/solvers.py @@ -42,6 +42,7 @@ def _options(self) -> dict[str, Any]: raise NotImplementedError +@dataclass class GurobiSolver(_Solver): """ Gurobi solver configuration. @@ -64,6 +65,7 @@ def _options(self) -> dict[str, Any]: } +@dataclass class HighsSolver(_Solver): """ HiGHS solver configuration. @@ -89,6 +91,7 @@ def _options(self) -> dict[str, Any]: } +@dataclass class XpressSolver(_Solver): """ FICO Xpress solver configuration. @@ -97,9 +100,11 @@ class XpressSolver(_Solver): 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 (e.g. `{'THREADS': 4}`) merged into `options`. + 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 @@ -107,5 +112,6 @@ 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, } diff --git a/tests/utilities/test_solvers.py b/tests/utilities/test_solvers.py new file mode 100644 index 000000000..a06259a05 --- /dev/null +++ b/tests/utilities/test_solvers.py @@ -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} From aaceee5488b800e1cbae0d8ae29df7020db15205 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 13:10:11 +0000 Subject: [PATCH 3/4] docs: document Xpress and linopy version requirements for XpressSolver Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QZ8AnG2jhX3n9Q2s2inYPm --- docs/home/installation.md | 3 +++ flixopt/solvers.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/docs/home/installation.md b/docs/home/installation.md index 76ee3a539..12a411360 100644 --- a/docs/home/installation.md +++ b/docs/home/installation.md @@ -59,6 +59,9 @@ Xpress ships with a free Community license for small models (up to 5000 rows and 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: diff --git a/flixopt/solvers.py b/flixopt/solvers.py index 484b287bf..b1eed755d 100644 --- a/flixopt/solvers.py +++ b/flixopt/solvers.py @@ -96,6 +96,8 @@ 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`. From edd70ea8a1be6a6ba9971198398f38cd5a0de4d6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 13:14:26 +0000 Subject: [PATCH 4/4] docs: clarify Xpress Community license limit is rows and columns combined Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QZ8AnG2jhX3n9Q2s2inYPm --- docs/home/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/home/installation.md b/docs/home/installation.md index 12a411360..2c01833ab 100644 --- a/docs/home/installation.md +++ b/docs/home/installation.md @@ -50,7 +50,7 @@ For academic use, Gurobi offers free licenses: ### FICO Xpress (Optional) -Xpress ships with a free Community license for small models (up to 5000 rows and columns). Academic and commercial licenses are available from [FICO](https://www.fico.com/en/fico-xpress-community-license): +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