File I/O and solvers.
grove is written to play nicely with the rest of the LP
ecosystem. You can export any model to a standard LP or MPS
file to hand to another solver, and you can swap in a
different backend behind the grove.Solver
interface — including the HiGHS cgo backend that ships in
v0.3.
CPLEX-LP format
WriteLP serialises a Problem into
the widely-supported CPLEX-LP format:
var buf bytes.Buffer
if err := prob.WriteLP(&buf); err != nil {
return err
}
fmt.Println(buf.String())
Example output for the nurse-scheduling model:
\Problem name: nurses
\Sense: Maximize
Maximize
obj: day + night
Subject To
day_min: day >= 4
night_min: night >= 2
budget: 1200 day + 1500 night <= 18000
Bounds
0 <= day
0 <= night
End
The writer respects variable kinds (Integer and
Binary are emitted under a General
or Binary section) and bounds, so a round-trip
through a MIP solver will see the same model you handed
grove.
Reading LP files
grove.ReadLP is the inverse of
WriteLP: any model you write with WriteLP
and read back with ReadLP is structurally
equivalent to the original — same sense, same variable
kinds/bounds, same objective and constraint coefficients.
f, err := os.Open("nurses.lp")
if err != nil {
return err
}
defer f.Close()
prob, err := grove.ReadLP(f)
if err != nil {
return err // parse errors carry the offending line and column
}
res, err := prob.Solve()
The reader accepts the usual CPLEX-LP tolerances: case-insensitive
keywords, abbreviated section headers (Min/Max,
s.t./st/such that,
Bound, Gen/Integer/Integers,
Bin/Binaries), expressions that span
multiple lines, backslash comments, and blank lines anywhere.
Variables that appear only in the objective or constraints —
without an explicit Bounds row — default to
[0, +Inf] continuous, matching the CPLEX default.
Parse errors are *grove.ParseError values whose
Error() includes the 1-based line (and column when
available) so mistakes surface with a useful pointer into the
file:
grove: LP parse error at line 5, column 11: expected '+' or '-' between terms
The Line, Col, and Msg
fields are exported, so tooling can pull them out with
errors.As to underline the offending spot:
var pe *grove.ParseError
if errors.As(err, &pe) {
highlight(path, pe.Line, pe.Col)
}
The reader does not yet model SOS sets (they are silently
skipped) or range constraints of the form
3 <= expr <= 10 — the latter surface a clear
diagnostic. If you need ranges today, split them into two
separate constraints.
MPS format
Fixed-column MPS is the lingua franca of academic LP/MIP. Use
WriteMPS when you’re interfacing with a tool
that prefers it over LP, or when your problem is too large for
LP format’s quirks:
f, err := os.Create("nurses.mps")
if err != nil {
return err
}
defer f.Close()
if err := prob.WriteMPS(f); err != nil {
return err
}
grove’s MPS writer names everything from your
Problem — constraint names, variable names,
problem name — and runs them through a sanitizer for the
MPS identifier rules (no spaces, 8-char-friendly where
possible, unique). If you’re relying on specific names
for a downstream tool, check the output and rename upstream.
Reading MPS files
grove.ReadMPS is the inverse of
WriteMPS. It parses every section grove emits —
NAME, ROWS, COLUMNS
(with the familiar INTORG/INTEND
integer markers), RHS, BOUNDS,
ENDATA — plus optional OBJSENSE,
RANGES, and SOS headers. The parser
has been round-tripped against a handful of MIPLIB instances
(flugpl, gr4x6, pk1) as part of the test suite.
f, err := os.Open("flugpl.mps")
if err != nil {
return err
}
defer f.Close()
prob, err := grove.ReadMPS(f)
if err != nil {
return err // parse errors carry the offending line
}
Fixed vs free-form, auto-detected
MPS has two encodings on the wire: the original fixed-column
layout (data in columns 2-3, 5-12, 15-22, 25-36, 40-47,
50-61), and the modern free-form variant that simply
whitespace-separates fields — handy when variable names
exceed the original 8-character slot. By default
ReadMPS auto-detects which you’ve given it:
it scans the first data line of COLUMNS,
RHS, RANGES, or BOUNDS
and checks whether any identifier spills into one of the
layout’s reserved whitespace gutters. If it does, the
file is parsed as free-form; otherwise as fixed-column. For
well-formed input with short names the two are
indistinguishable, and both parsers accept either.
Callers that know their input ahead of time can pin the parser to one mode or the other with an option, bypassing auto-detection:
prob, err := grove.ReadMPS(f, grove.MPSFreeInput())
// or
prob, err := grove.ReadMPS(f, grove.MPSFixedInput())
Unknown section headers (e.g. a typo like BOUNDSS)
surface a parse error rather than being silently dropped, so
malformed input fails fast. SOS sections are
silently skipped because grove doesn’t model SOS sets
yet; RANGES rows raise the same "range
constraints are not supported" diagnostic as the LP reader,
so you know to split the row into two before re-reading.
Parse errors for both readers share the
*grove.ParseError type; MPS errors set
Format = "MPS" so the rendered message reads
grove: MPS parse error at line N: ....
The Solver interface
The default pure-Go simplex is a concrete implementation of a small interface:
type Solver interface {
Solve(p *Problem) (*Result, error)
}
Assign a different implementation to prob.Solver
and Solve will dispatch to it instead of the
default:
prob.Solver = &grove.HiGHS{} // real implementation ships in v0.3
res, err := prob.Solve()
This is a hook, not a plugin system — grove has exactly one
extension point today, and that is the right number until a
second real backend exists. If you need to wrap the simplex
for logging, timing, or fallback, writing a trivial adapter
that holds a *grove.SimplexSolver and satisfies
the interface is a ten-line job.
The HiGHS backend (v0.3 preview)
HiGHS is a well-regarded
open-source LP/MIP solver. grove v0.3 will ship a cgo-linked
backend behind the highs build tag:
go build -tags highs ./...
Without the tag, grove.HiGHS.Solve returns
grove.ErrHiGHSNotBuilt, which you can detect and
fall back to the pure-Go solver:
prob.Solver = &grove.HiGHS{}
res, err := prob.Solve()
if errors.Is(err, grove.ErrHiGHSNotBuilt) {
prob.Solver = nil // back to the simplex default
res, err = prob.Solve()
}
Once enabled, HiGHS brings three things grove v0.1 doesn’t have: a true MIP solver (branch-and-bound with cuts, before grove’s own in v0.4), a sparse presolver that makes large models solve in seconds instead of minutes, and its own ranging sensitivity output.
The rest of your code — your Problem construction,
your Result inspection, your sensitivity report —
is unchanged. That symmetry is the whole point of the
Solver interface.
Interop recipe: grove → CBC
Until the HiGHS backend ships, if you need a true MIP answer the pragmatic thing is to export MPS and shell out to CBC or a HiGHS-packaged binary. This is five lines of Go:
f, _ := os.CreateTemp("", "model-*.mps")
defer os.Remove(f.Name())
_ = prob.WriteMPS(f)
f.Close()
out, err := exec.Command("cbc", f.Name(), "solve", "printi", "csv").Output()
if err != nil {
return fmt.Errorf("cbc: %w", err)
}
// Parse `out` — or read CBC's solution file and look variables up
// by name via prob.VarByName.
If that strays into hacky territory for your taste, the
cleaner option is to wait three weeks and use
grove.HiGHS.