Skip to content

fix(solvers): probe the Xpress licence, run solving tests on licensed solvers - #995

Merged
FabianHofmann merged 4 commits into
masterfrom
fix-xpress-availability
Oct 20, 2026
Merged

FabianHofmann merged 4 commits into
masterfrom
fix-xpress-availability

Conversation

@FabianHofmann

Copy link
Copy Markdown
Collaborator

Closes #994.

Note

The following content was generated by AI.

Changes proposed in this Pull Request

Xpress was the only solver without a licence probe, so licensed_solvers listed it whenever import xpress worked. With a licence that does not cover the installed release (e.g. a licence for <= 9.4 with xpress 9.9.1 installed), every solver-parametrized test failed with xpress.InterfaceError.

  • Xpress._license_probe calls xpress.init(). A rejected licence now shows up as ok=False in check_solver_licenses("xpress"), and Xpress drops out of licensed_solvers. available_solvers still only checks the import and acquires no licence.
  • Tests that solve, or build a solver model (to_xpress, to_gurobipy), now loop over licensed_solvers instead of available_solvers (18 files). Tests that only check installation, or raise before any solve, keep available_solvers.
  • New test test_xpress_license_probe_failure_marks_unlicensed, mocking only xpress.init, like the existing probe tests.

This also makes the -k "not xpress" workaround unnecessary; it deselected every test with "expression" in its name.

Verification (48828a2)
# XPAUTH_PATH pointing at a licence for <= 9.4, xpress 9.9.1 installed
uv run pytest -q --ignore=benchmarks --ignore=test/remote
8195 passed, 1403 skipped
# env -u XPAUTH_PATH (licence shipped with the wheel)
8934 passed, 1388 skipped
uv run ruff check . && uv run ruff format --check .
All checks passed!

uv run mypy linopy reports 28 pre-existing errors in linopy/solvers.py when xpress is installed (old-API fallback names, see #533); unchanged by this PR.

Checklist

  • AI-generated content is marked (see AGENTS.md).
  • Code changes are sufficiently documented; i.e. new functions contain docstrings and further explanations may be given in doc.
  • Unit tests for new features were added (if applicable).
  • A note for the release notes doc/release_notes.rst of the upcoming release is included.
  • I consent to the release of this PR's code under the MIT license.

@FabianHofmann FabianHofmann added bug Something isn't working solver interface labels Sep 28, 2026
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Build cost — v1 vs legacy

v1 build peak & time relative to legacy, on this commit — not a comparison against master (that is CodSpeed).

peak — v1 / legacy time — v1 / legacy
peak v1/legacy time v1/legacy
Full table (time + peak, mean)
benchmarks/drivers/test_build.py::test_build[basic-n=10]
                  time (s)         peak (KiB) 
 name                 mean   │           mean 
──────────────────────────────────────────────
 (legacy)   0.09889 (1.20)   │   15.03 (1.00) 
 (v1)        0.08236 (1.0)   │    15.00 (1.0) 

benchmarks/drivers/test_build.py::test_build[basic-n=250]
                 time (s)         peak (MiB) 
 name                mean   │           mean 
─────────────────────────────────────────────
 (legacy)   0.1021 (1.17)   │   12.04 (1.00) 
 (v1)       0.08721 (1.0)   │    12.04 (1.0) 

benchmarks/drivers/test_build.py::test_build[cumsum-severity=0]
                  time (s)        peak (KiB) 
 name                 mean   │          mean 
─────────────────────────────────────────────
 (legacy)   0.04037 (1.14)   │   15.20 (1.0) 
 (v1)        0.03531 (1.0)   │   15.20 (1.0) 

benchmarks/drivers/test_build.py::test_build[cumsum-severity=100]
                  time (s)        peak (MiB) 
 name                 mean   │          mean 
─────────────────────────────────────────────
 (legacy)   0.05757 (1.10)   │   44.93 (1.0) 
 (v1)        0.05251 (1.0)   │   44.93 (1.0) 

benchmarks/drivers/test_build.py::test_build[cumsum-severity=50]
                  time (s)        peak (MiB) 
 name                 mean   │          mean 
─────────────────────────────────────────────
 (legacy)   0.04331 (1.11)   │   11.51 (1.0) 
 (v1)        0.03905 (1.0)   │   11.51 (1.0) 

benchmarks/drivers/test_build.py::test_build[expression_arithmetic-n=10]
                 time (s)         peak (KiB) 
 name                mean   │           mean 
─────────────────────────────────────────────
 (legacy)   0.1066 (1.14)   │   24.45 (1.06) 
 (v1)       0.09352 (1.0)   │    23.04 (1.0) 

benchmarks/drivers/test_build.py::test_build[expression_arithmetic-n=250]
                 time (s)         peak (MiB) 
 name                mean   │           mean 
─────────────────────────────────────────────
 (legacy)   0.1175 (1.14)   │   16.12 (1.00) 
 (v1)        0.1035 (1.0)   │    16.12 (1.0) 

benchmarks/drivers/test_build.py::test_build[knapsack-n=10000]
                  time (s)          peak (KiB) 
 name                 mean   │            mean 
───────────────────────────────────────────────
 (legacy)   0.02504 (1.12)   │   752.18 (1.10) 
 (v1)        0.02229 (1.0)   │    685.15 (1.0) 

benchmarks/drivers/test_build.py::test_build[knapsack-n=100]
                  time (s)        peak (KiB) 
 name                 mean   │          mean 
─────────────────────────────────────────────
 (legacy)   0.02452 (1.12)   │   3.12 (1.33) 
 (v1)        0.02192 (1.0)   │    2.34 (1.0) 

benchmarks/drivers/test_build.py::test_build[kvl_cycles-severity=0]
                  time (s)          peak (MiB) 
 name                 mean   │            mean 
───────────────────────────────────────────────
 (legacy)   0.06587 (1.24)   │   126.16 (1.44) 
 (v1)        0.05306 (1.0)   │     87.71 (1.0) 

benchmarks/drivers/test_build.py::test_build[kvl_cycles-severity=100]
                  time (s)          peak (MiB) 
 name                 mean   │            mean 
───────────────────────────────────────────────
 (legacy)   0.06507 (1.24)   │   126.16 (1.44) 
 (v1)        0.05259 (1.0)   │     87.71 (1.0) 

benchmarks/drivers/test_build.py::test_build[kvl_cycles-severity=50]
                  time (s)          peak (MiB) 
 name                 mean   │            mean 
───────────────────────────────────────────────
 (legacy)   0.06565 (1.24)   │   126.16 (1.44) 
 (v1)          0.053 (1.0)   │     87.71 (1.0) 

benchmarks/drivers/test_build.py::test_build[masked-n=100]
                  time (s)          peak (KiB) 
 name                 mean   │            mean 
───────────────────────────────────────────────
 (legacy)   0.05786 (1.07)   │    712.45 (1.0) 
 (v1)        0.05416 (1.0)   │   787.73 (1.11) 

benchmarks/drivers/test_build.py::test_build[masked-n=10]
                  time (s)        peak (KiB) 
 name                 mean   │          mean 
─────────────────────────────────────────────
 (legacy)   0.05663 (1.16)   │   4.62 (1.29) 
 (v1)        0.04897 (1.0)   │    3.57 (1.0) 

benchmarks/drivers/test_build.py::test_build[merge_balance-severity=0]
                 time (s)          peak (KiB) 
 name                mean   │            mean 
──────────────────────────────────────────────
 (legacy)   0.3797 (1.07)   │   707.91 (1.10) 
 (v1)        0.3564 (1.0)   │    643.85 (1.0) 

benchmarks/drivers/test_build.py::test_build[merge_balance-severity=100]
                 time (s)        peak (MiB) 
 name                mean   │          mean 
────────────────────────────────────────────
 (legacy)   0.3963 (1.06)   │   18.34 (1.0) 
 (v1)        0.3722 (1.0)   │   18.34 (1.0) 

benchmarks/drivers/test_build.py::test_build[merge_balance-severity=50]
                 time (s)       peak (MiB) 
 name                mean   │         mean 
───────────────────────────────────────────
 (legacy)   0.3948 (1.07)   │   9.54 (1.0) 
 (v1)        0.3695 (1.0)   │   9.54 (1.0) 

benchmarks/drivers/test_build.py::test_build[milp-n=10]
                  time (s)        peak (KiB) 
 name                 mean   │          mean 
─────────────────────────────────────────────
 (legacy)   0.07969 (1.15)   │   3.85 (1.14) 
 (v1)        0.06958 (1.0)   │    3.37 (1.0) 

benchmarks/drivers/test_build.py::test_build[milp-n=50]
                  time (s)          peak (KiB) 
 name                 mean   │            mean 
───────────────────────────────────────────────
 (legacy)   0.07995 (1.12)   │   216.98 (1.11) 
 (v1)        0.07115 (1.0)   │    196.23 (1.0) 

benchmarks/drivers/test_build.py::test_build[nodal_balance-severity=0]
                  time (s)         peak (KiB) 
 name                 mean   │           mean 
──────────────────────────────────────────────
 (legacy)   0.03985 (1.14)   │   938.49 (1.0) 
 (v1)        0.03492 (1.0)   │   938.49 (1.0) 

benchmarks/drivers/test_build.py::test_build[nodal_balance-severity=100]
                  time (s)       peak (MiB) 
 name                 mean   │         mean 
────────────────────────────────────────────
 (legacy)   0.04077 (1.12)   │   9.66 (1.0) 
 (v1)         0.0364 (1.0)   │   9.66 (1.0) 

benchmarks/drivers/test_build.py::test_build[nodal_balance-severity=50]
                  time (s)       peak (MiB) 
 name                 mean   │         mean 
────────────────────────────────────────────
 (legacy)   0.04014 (1.12)   │   5.32 (1.0) 
 (v1)        0.03579 (1.0)   │   5.32 (1.0) 

benchmarks/drivers/test_build.py::test_build[nodal_balance_sparse-severity=0]
                  time (s)       peak (MiB) 
 name                 mean   │         mean 
────────────────────────────────────────────
 (legacy)   0.01943 (1.03)   │   1.28 (1.0) 
 (v1)        0.01885 (1.0)   │   1.28 (1.0) 

benchmarks/drivers/test_build.py::test_build[nodal_balance_sparse-severity=100]
                  time (s)       peak (MiB) 
 name                 mean   │         mean 
────────────────────────────────────────────
 (legacy)   0.01924 (1.03)   │   1.28 (1.0) 
 (v1)        0.01871 (1.0)   │   1.28 (1.0) 

benchmarks/drivers/test_build.py::test_build[nodal_balance_sparse-severity=50]
                  time (s)       peak (MiB) 
 name                 mean   │         mean 
────────────────────────────────────────────
 (legacy)   0.01925 (1.01)   │   1.28 (1.0) 
 (v1)        0.01901 (1.0)   │   1.28 (1.0) 

benchmarks/drivers/test_build.py::test_build[piecewise-n=1000]
                 time (s)          peak (KiB) 
 name                mean   │            mean 
──────────────────────────────────────────────
 (legacy)   0.2058 (1.08)   │   946.85 (1.06) 
 (v1)          0.19 (1.0)   │    891.54 (1.0) 

benchmarks/drivers/test_build.py::test_build[piecewise-n=10]
                 time (s)         peak (KiB) 
 name                mean   │           mean 
─────────────────────────────────────────────
 (legacy)   0.2065 (1.11)   │   12.01 (1.00) 
 (v1)        0.1862 (1.0)   │    11.99 (1.0) 

benchmarks/drivers/test_build.py::test_build[qp-n=1000]
                  time (s)          peak (KiB) 
 name                 mean   │            mean 
───────────────────────────────────────────────
 (legacy)   0.04983 (1.11)   │   147.70 (1.06) 
 (v1)        0.04487 (1.0)   │    139.87 (1.0) 

benchmarks/drivers/test_build.py::test_build[qp-n=10]
                  time (s)        peak (KiB) 
 name                 mean   │          mean 
─────────────────────────────────────────────
 (legacy)   0.05006 (1.12)   │   2.60 (1.09) 
 (v1)        0.04462 (1.0)   │    2.38 (1.0) 

benchmarks/drivers/test_build.py::test_build[rolling-severity=0]
                  time (s)          peak (KiB) 
 name                 mean   │            mean 
───────────────────────────────────────────────
 (legacy)   0.04094 (1.13)   │   704.59 (1.05) 
 (v1)         0.0362 (1.0)   │    673.70 (1.0) 

benchmarks/drivers/test_build.py::test_build[rolling-severity=100]
                  time (s)         peak (MiB) 
 name                 mean   │           mean 
──────────────────────────────────────────────
 (legacy)   0.09018 (1.10)   │   137.97 (1.0) 
 (v1)        0.08189 (1.0)   │   137.97 (1.0) 

benchmarks/drivers/test_build.py::test_build[rolling-severity=50]
                  time (s)        peak (MiB) 
 name                 mean   │          mean 
─────────────────────────────────────────────
 (legacy)   0.06171 (1.03)   │   69.22 (1.0) 
 (v1)        0.05992 (1.0)   │   69.22 (1.0) 

benchmarks/drivers/test_build.py::test_build[sos-n=1000]
                  time (s)          peak (KiB) 
 name                 mean   │            mean 
───────────────────────────────────────────────
 (legacy)   0.04762 (1.15)   │   402.37 (1.00) 
 (v1)        0.04132 (1.0)   │    402.30 (1.0) 

benchmarks/drivers/test_build.py::test_build[sos-n=10]
                 time (s)        peak (KiB) 
 name                mean   │          mean 
────────────────────────────────────────────
 (legacy)   0.0472 (1.16)   │   3.23 (1.20) 
 (v1)       0.04074 (1.0)   │    2.69 (1.0) 

benchmarks/drivers/test_build.py::test_build[sparse_network-n=10]
                  time (s)         peak (KiB) 
 name                 mean   │           mean 
──────────────────────────────────────────────
 (legacy)   0.05123 (1.11)   │   29.00 (1.54) 
 (v1)        0.04605 (1.0)   │    18.84 (1.0) 

benchmarks/drivers/test_build.py::test_build[sparse_network-n=250]
                  time (s)         peak (MiB) 
 name                 mean   │           mean 
──────────────────────────────────────────────
 (legacy)   0.06361 (1.18)   │   37.95 (1.43) 
 (v1)        0.05381 (1.0)   │    26.51 (1.0) 

benchmarks/drivers/test_build.py::test_build[storage-n=10]
                  time (s)          peak (KiB) 
 name                 mean   │            mean 
───────────────────────────────────────────────
 (legacy)   0.09902 (1.04)   │    410.14 (1.0) 
 (v1)        0.09542 (1.0)   │   427.84 (1.04) 

benchmarks/drivers/test_build.py::test_build[storage-n=250]
                 time (s)         peak (MiB) 
 name                mean   │           mean 
─────────────────────────────────────────────
 (legacy)   0.1032 (1.00)   │     9.94 (1.0) 
 (v1)        0.1029 (1.0)   │   10.22 (1.03) 

📊 Interactive plots + CSV: download the semantics-report-v1-vs-legacy artifact from this run.

Report-only · not a gate · refreshed on every push · obsolete once legacy is dropped.

@codspeed

codspeed Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 181 untouched benchmarks
⏩ 181 skipped benchmarks1


Comparing fix-xpress-availability (18cd460) with master (4117547)

Open in CodSpeed

Footnotes

  1. 181 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@FabianHofmann
FabianHofmann force-pushed the fix-xpress-availability branch from feb370b to 18cd460 Compare October 20, 2026 07:44
@FabianHofmann
FabianHofmann merged commit 09d4fde into master Oct 20, 2026
24 checks passed
@FabianHofmann
FabianHofmann deleted the fix-xpress-availability branch October 20, 2026 08:46
@FabianHofmann FabianHofmann mentioned this pull request Oct 21, 2026
5 tasks done
FabianHofmann added a commit that referenced this pull request Oct 21, 2026
* fix: type check with numpy 2.5.4 isscalar stubs (#1031)

* fix: type check with numpy 2.5.4 isscalar stubs

numpy 2.5.4 widened the TypeIs of np.isscalar to include numbers.Number,
which xarray's DataArray.__add__ does not accept, so mypy fails on the
legacy scalar branch of _add_constant. Cast to the scalar members of
ConstantLike; no runtime change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor: route legacy scalar add through _combined_with_constant instead of cast

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Fabian <fab.hof@gmx.de>
(cherry picked from commit 86d6ad2)

* fix: keep dash-named parameters in netcdf round trip (#1025)

* fix: keep dash-named parameters, attributes and MultiIndex dims in read_netcdf

read_netcdf assigned each data variable and attribute to its container
by splitting the key at its last dash. Parameters are stored as
parameters-<name>, so a parameter named my-param was assigned to
parameters-my and dropped; the same happened to parameter attributes
and _multiindex attributes of dimensions with a dash in their name.
Keys now belong to the longest known container prefix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: type check with numpy 2.5.4 isscalar stubs

numpy 2.5.4 widened the TypeIs of np.isscalar to include numbers.Number,
which xarray's DataArray.__add__ does not accept, so mypy fails on the
legacy scalar branch of _add_constant. Cast to the scalar members of
ConstantLike; no runtime change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: move release note to upcoming version

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(io): compute netcdf key owners once

* refactor(io): compactify netcdf key owner lookup

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Fabian <fab.hof@gmx.de>
(cherry picked from commit 4117547)

* fix(solvers): probe the Xpress licence, run solving tests on licensed solvers (#995)

* fix(solvers): probe the Xpress licence, run solving tests on licensed solvers (#994)

* test(io): to_mosek builds a task without a licence, gate it on available_solvers

* test(io): name a mosek task without a licence, gate its set_names cases on available_solvers

* test(io): gate the highs direct QP solve on licensed_solvers

(cherry picked from commit 09d4fde)

* fix: keep auxiliary coordinates of dense containers in netcdf round trip (#1024)

* fix: keep auxiliary coordinates of dense containers in netcdf round trip

read_netcdf dropped every non-index coordinate except MultiIndex levels
to keep out the scalar coordinates of other containers that xarray
attaches when selecting one container's data variables. That also
dropped the auxiliary coordinates of dense variables, constraints,
expressions and parameters, such as the levels on a flat dimension that
v1 recommends instead of a MultiIndex.

Only drop scalar coordinates owned by another container, matched on the
longest prefix since names may contain dashes. Coordinates with
dimensions only attach to the container whose dimensions they share.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: drop narrative comments, cover dash-named parameter with scalar coord

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Fabian <fab.hof@gmx.de>
(cherry picked from commit 44ff525)

* docs: release notes for v0.10.1

---------

Co-authored-by: Mayk Thewessen <mayk_12@hotmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working solver interface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Xpress missing _license_probe: licensed_solvers includes xpress with an out-of-date licence, breaking local tests

1 participant