Skip to content

FIBERALLOC-62: Add a HiGHS solver backend - #32

Open
monodera wants to merge 8 commits into
masterfrom
tickets/FIBERALLOC-62
Open

FIBERALLOC-62: Add a HiGHS solver backend#32
monodera wants to merge 8 commits into
masterfrom
tickets/FIBERALLOC-62

Conversation

@monodera

@monodera monodera commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

https://pfs-jira.naoj.org/jira/browse/FIBERALLOC-62

Adds HiGHS (through highspy) as a third solver backend
for the netflow MILP, alongside Gurobi and PuLP — an open-source option that
does not require a Gurobi licence.

Changes

  • HighsProblem backend (ets_fiber_assigner/netflow.py), following the
    same LPProblem interface as GurobiProblem and PulpProblem. Columns are
    reserved on addVar() and created in a single _flush() batch, because
    adding them one at a time through highspy costs ~50 us each.
  • solve() raises when no solution was produced. Unlike Gurobi, HiGHS
    hands back an all-zero column vector for an infeasible or unsolved model,
    which is indistinguishable from a feasible solution that assigns nothing;
    the model status is now checked so callers cannot silently read a failure
    as an empty assignment.
  • New buildProblem() parameters solver and solverOptions.
    solver="gurobi" | "pulp" | "highs" selects the backend, and
    solverOptions passes options in that backend's own parameter names.
    When solver is None the existing gurobi flag behaves exactly as
    before, so current callers are unaffected.
  • README: the solver paragraph in the prerequisites now lists HiGHS.

highspy is imported lazily inside HighsProblem.__init__, the same way
gurobipy and pulp are, so it is only needed when that backend is used and
no new hard dependency is introduced.

Benchmark

Benchmarked on 22 real target lists: HiGHS finished the same 20 that Gurobi
did, at 1.08x the total runtime, with pointing counts agreeing to within the
spread a single solver shows across repeated runs of the same input.

Notes

  • The ChangeLog is intentionally left untouched here; entries are added
    separately on release.
  • Known and deliberately not addressed: the demo scripts read binary variable
    values with > 0, which is not tolerance-safe for the near-integral doubles
    a MIP solver returns. The demo scripts are considered obsolete, so no fix is
    planned for now.

🤖 Generated with Claude Code

monodera and others added 8 commits August 23, 2026 02:47
HighsProblem(LPProblem), reached through highspy, as an open-source
alternative to Gurobi that needs no licence. Benchmarked on 22 real
target lists from the PFS target uploader: HiGHS finished the same 20 of
them Gurobi did, at 1.08x the total runtime, with pointing counts
agreeing to within the spread a single solver shows across repeated runs
of the same input.

Additive throughout. buildProblem() gains solver= and solverOptions=,
both defaulting to None, and the existing `gurobi` flag keeps selecting
between Gurobi and PuLP whenever solver is not given -- so callers that
do not pass it take exactly the path they took before. GurobiProblem and
PulpProblem are untouched.

Two implementation notes, both measured rather than assumed:

Columns are created in one batch. Adding them individually through
highspy costs ~50 us each, minutes of overhead on the million-variable
problems this module builds; addCols takes the batch at once and
measures ~180x faster. buildProblem() creates every variable before its
first constraint, so a single deferred flush catches all of them.

Solutions are read from one cached vector. Highs.val() recomputes per
call at O(numCol) -- 295 us per variable on a 20k-column model, 1083 us
on an 80k one -- so reading a solution back variable by variable is
quadratic, and needs about an hour on a 500k-column problem whose solve
takes 20 s. getSolution() costs a millisecond, once.

HiGHS has no lazy-constraint hint, so add_lazy_constraint() adds an
ordinary constraint. That costs nothing here: the collision constraints
are built up front rather than generated in a callback, and marking them
lazy for a backend that does support it left both runtime and objective
unchanged on a 1.7M-variable instance.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YKQTw7ZXAZDjgKoaigTGQA
A Gurobi variable has no value to read when the solve failed, so the
caller notices at once. HiGHS instead hands back an all-zero column
vector for an infeasible or unsolved model, which is indistinguishable
from a feasible solution that assigns nothing -- callers reading the
solution back would silently treat the failure as an empty assignment.

Check getModelStatus() in solve() and raise unless the model was solved
to optimality or a limit stopped the search after an incumbent had been
found, matching what PulpProblem already does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The comment described a binary/integer split the code does not make: every
column is created integral, and a 0/1 range is simply bounded to [0, 1].

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- _cacheSolution's try/except never caught anything: HiGHS returns an
  all-zero vector rather than raising when there is no solution, and the
  status check added to solve() is what rules that case out. Drop it and
  say so, instead of claiming value() will fall back to val().
- varBounds() now reports floats whatever the caller passed in, matching
  what the Gurobi and PuLP backends return.
- Note that `name` is accepted only for interface parity: highspy exposes
  no model-name API to set it on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
#	ets_fiber_assigner/netflow.py
The prerequisites still described the solver choice as PuLP versus Gurobi.
Add HiGHS to that list so the third backend is discoverable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hLuRYpDagE8nVbkGMm96k
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant