You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
with platform markers doing the work where a wheel does not exist everywhere. Since our compat lane is a pure consumer of linopy's public API, matching this is coherent rather than merely imitative — and it settles the question in #105: highspy moves out of dependencies.
What is worth adopting
1. Two implementation paths behind one ABC.Solver declares _run_file (LP/MPS) and _build_direct / _run_direct (in-process API), with accepted_io_apis: ClassVar[frozenset[str]] saying which a given solver supports. That maps exactly onto our two sinks — lp_file and solver_direct — and is the seam we need, because solver_direct is currently highspy-specific (addCols / addRows / changeColsIntegrality are highspy APIs).
We already have this distinction and have never named it. Naming it makes "which solvers can this model reach, and how" answerable.
3. Availability probing with no import side effects. Three separate mechanisms, all worth copying:
_has_module uses importlib.util.find_spec — never executes the package's __init__.
_LazyModule is a proxy importing on first attribute access, so gurobipy.Env can be referenced at module level while deferring the import "and its license-server side effects, for mindoptpy/coptpy".
_AvailableSolvers is a lazy Sequence[str] behind functools.cached_property, with an explicit refresh().
The license-server point is the non-obvious one: for commercial solvers, importing is not free.
4. Licensing is a separate, opt-in probe. Membership in available_solvers means importable, explicitly not licensed; check_solver_licenses() is the eager check. Conflating the two would make availability slow and network-dependent.
5. SolverFeature — Track 4, already built upstream. A 15-member enum declared per solver class as ClassVar[frozenset[SolverFeature]], queried via cls.supports(feature):
That list covers precisely what ARCHITECTURE names as the sink upgrade path (sos_sets, genconstr, a semi-continuous threshold on cols) plus what ROADMAP Track 2b wants (IIS_COMPUTATION).
The one place adoption is not 1:1 — and it matters
linopy attaches capability to the solver. ARCHITECTURE.md attaches it to the sink, and is explicit that "capability is not the ceiling":
lp_file carries [SOS] as a text section and Gurobi natively
Both models are right about different things. An LP file can serialise SOS whether or not the solver eventually reading it understands it; a direct API can accept SOS whether or not any file format was involved. So capability is really a property of the (sink, solver) pair, not of either alone.
Deciding this is the substantive design question here, and it should be settled before any code:
declare features on sinks only, and treat the solver as one more sink?
declare on both and intersect?
keep linopy's solver-only model and accept that check(model, sink=...) answers a narrower question than it appears to?
What we should not adopt
Stateful Solver objects. linopy's carries status, solution, solver_model, snapshot, _rebuilds, _in_place_updates, plus track_updates persistent-update machinery. Our api.py is deliberately three verbs and the executor owns lifetime; importing that state model would fight it. Warm starts are a separate conversation.
One 4,520-line module holding 13 solver classes.
Default-by-probe-order._SOLVER_PROBE_ORDER makes the first installed solver the default in Model.solve. That means the same YAML solves with a different solver on a different machine — the opposite of "the file is the source of truth". We should require an explicit solver=, or at minimum make the implicit choice loud.
linopy hard-depends on polars>=1.31.1 and ships an lp-polars io_api — it writes LP files through polars. So the compat extra already pulls polars in transitively, and "polars is exotic in this ecosystem" is not a defensible argument against it.
Work this implies
Move highspy to [highs]; add a [solvers] bundle; nothing installed by default.
solve() with no solver installed raises naming the extras, as to_dataarray already does for xarray.
Thread A of #105, researched. Read against linopy 0.9.x as installed.
linopy already forces no solver
Its hard dependencies contain no solver at all:
Solvers live behind one extra:
with platform markers doing the work where a wheel does not exist everywhere. Since our compat lane is a pure consumer of linopy's public API, matching this is coherent rather than merely imitative — and it settles the question in #105:
highspymoves out ofdependencies.What is worth adopting
1. Two implementation paths behind one ABC.
Solverdeclares_run_file(LP/MPS) and_build_direct/_run_direct(in-process API), withaccepted_io_apis: ClassVar[frozenset[str]]saying which a given solver supports. That maps exactly onto our two sinks —lp_fileandsolver_direct— and is the seam we need, becausesolver_directis currently highspy-specific (addCols/addRows/changeColsIntegralityare highspy APIs).2.
io_apias an axis orthogonal to solver choice.We already have this distinction and have never named it. Naming it makes "which solvers can this model reach, and how" answerable.
3. Availability probing with no import side effects. Three separate mechanisms, all worth copying:
_has_moduleusesimportlib.util.find_spec— never executes the package's__init__._LazyModuleis a proxy importing on first attribute access, sogurobipy.Envcan be referenced at module level while deferring the import "and its license-server side effects, for mindoptpy/coptpy"._AvailableSolversis a lazySequence[str]behindfunctools.cached_property, with an explicitrefresh().The license-server point is the non-obvious one: for commercial solvers, importing is not free.
4. Licensing is a separate, opt-in probe. Membership in
available_solversmeans importable, explicitly not licensed;check_solver_licenses()is the eager check. Conflating the two would make availability slow and network-dependent.5.
SolverFeature— Track 4, already built upstream. A 15-member enum declared per solver class asClassVar[frozenset[SolverFeature]], queried viacls.supports(feature):That list covers precisely what ARCHITECTURE names as the sink upgrade path (
sos_sets,genconstr, a semi-continuous threshold oncols) plus what ROADMAP Track 2b wants (IIS_COMPUTATION).The one place adoption is not 1:1 — and it matters
linopy attaches capability to the solver. ARCHITECTURE.md attaches it to the sink, and is explicit that "capability is not the ceiling":
Both models are right about different things. An LP file can serialise SOS whether or not the solver eventually reading it understands it; a direct API can accept SOS whether or not any file format was involved. So capability is really a property of the (sink, solver) pair, not of either alone.
Deciding this is the substantive design question here, and it should be settled before any code:
check(model, sink=...)answers a narrower question than it appears to?What we should not adopt
Solverobjects. linopy's carriesstatus,solution,solver_model,snapshot,_rebuilds,_in_place_updates, plustrack_updatespersistent-update machinery. Ourapi.pyis deliberately three verbs and the executor owns lifetime; importing that state model would fight it. Warm starts are a separate conversation._SOLVER_PROBE_ORDERmakes the first installed solver the default inModel.solve. That means the same YAML solves with a different solver on a different machine — the opposite of "the file is the source of truth". We should require an explicitsolver=, or at minimum make the implicit choice loud.Incidental finding, relevant to #105
linopy hard-depends on
polars>=1.31.1and ships anlp-polarsio_api — it writes LP files through polars. So the compat extra already pulls polars in transitively, and "polars is exotic in this ecosystem" is not a defensible argument against it.Work this implies
highspyto[highs]; add a[solvers]bundle; nothing installed by default.solve()with no solver installed raises naming the extras, asto_dataarrayalready does for xarray..mps— with no solver by default, the file sinks become the universal interface (see Nothing should be forced: make the solver, the dataframe library, and Arrow all optional #105).solver_directbehind a small ABC before a second direct solver exists, not after.