integration

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.