Solve and Result.
prob.Solve() returns a (*Result, error).
The error is non-nil only for configuration mistakes; logical
outcomes live on the Result. Understanding that
split — and the fields of Result — is most of
using grove in production.
Calling Solve
res, err := prob.Solve()
if err != nil {
// You built something structurally wrong: no objective, a Var
// from a different Problem, inverted Bounds, etc.
return fmt.Errorf("grove: bad model: %w", err)
}
switch res.Status {
case grove.Optimal:
// Normal path.
case grove.Infeasible, grove.Unbounded:
// Model is self-contradictory or open. Handle.
case grove.IterationLimit, grove.NumericalError:
// Solver gave up. Try p.MaxIterations, or re-check the data.
}
This distinction matters: an “infeasible” LP isn’t
a bug, it’s information. grove returns
(res, nil) with res.Status ==
Infeasible so you can treat it like any other outcome.
Only genuine misuse — no variables, no objective, a
*Var that isn’t part of this problem —
surfaces as a non-nil error.
The Result object
Result is a plain struct with accessor methods
for the map-backed fields. In full:
type Result struct {
Status grove.Status // Optimal / Infeasible / Unbounded / IterationLimit / NumericalError / NotSolved
Objective float64 // Evaluated in the user-facing sense (Maximize returns positive values)
Iterations int // Total simplex pivots, Phase I + Phase II
Message string // Optional human-readable detail (failures, LP-relaxation caveat)
Warnings []string // Advisory notes; v0.1 emits WarnLPRelaxationOnly when an ILP was silently relaxed
// Accessor methods read the internal maps:
// Value(v) float64 value of variable v
// Dual(c) float64 shadow price of constraint c (∂z*/∂rhs)
// Reduced(v) float64 reduced cost of variable v
// NonIntegerIntegerVars(p) []*Var integer/binary vars whose LP optimum is non-integer
}
Status
grove.Status is a small enum with a
String method, so fmt.Println(res.Status)
prints a readable label. The values:
Optimal— the returned vertex is feasible and optimal.Infeasible— no feasible solution exists. Phase I couldn’t drive the artificials to zero.Unbounded— there is a direction in which the objective can improve without limit. grove detects this during Phase II ratio tests.IterationLimit— the solver hitp.MaxIterationsbefore converging. Default is effectively unbounded; set it only when you want a timeout.NumericalError— the simplex produced values grove can’t trust (typically a NaN or a blow-up from extreme coefficient ratios). Try scaling your data.NotSolved— returned inside error-return paths; you’ll see this witherr != nil.
Objective and variable values
fmt.Println("objective:", res.Objective)
for _, v := range []*grove.Var{day, night} {
fmt.Printf("%-6s %g\n", v.Name()+":", res.Value(v))
}
res.Objective is already reported in your
original sense — if you said Maximize, you’ll
see a positive number for a maximisation problem, not the
negated internal form. Same for res.Value(v):
bounded-variable substitutions are un-applied before the
number reaches you.
Asking for a variable that doesn’t belong to the
problem returns 0. Asking for a variable on a
non-Optimal result also returns 0.
Verbose mode
When a solve is misbehaving the fastest way to see what the simplex is doing is to turn on verbose logging:
prob.Verbose = true
res, _ := prob.Solve()
The solver will stream, to stderr:
- The standard-form tableau immediately after construction (with artificial columns flagged).
- Each Phase I pivot, showing entering column, leaving row, and the resulting artificial-sum residual.
- Each Phase II pivot.
- The final basis and the reason the solver stopped.
For a 2-variable test problem this is three screenfuls. For anything bigger, pipe it into a file — the output is genuinely diagnostic, but it isn’t a thing you leave on.
Solver options
Three settings live on the Problem itself:
prob.Verbose = true // stream pivots to stderr (see above)
prob.MaxIterations = 10000 // hard pivot cap; default is unbounded
prob.SkipPresolve = true // bypass the pre-simplex reduction pass (v0.2)
Use MaxIterations when you’re running
grove inside a request handler and you want a fixed
worst-case budget rather than a wall-clock timeout.
The fourth dial is prob.Solver — a slot for an
alternative implementation of the grove.Solver
interface (the pure-Go simplex is the default). That’s
covered in File I/O and solvers.
Presolve
Before the simplex runs, Solve invokes a cheap
presolve pass that shrinks the model in three ways:
-
Fixed variables. A variable declared with
Bounds(c, c)is substituted out: its constant contribution is folded into every constraint’s RHS and into the objective constant. -
Empty columns. A variable that — after
fixed-variable substitution — appears in no constraint is
settled at whichever bound improves the user-sense
objective. For a
Minimizeproblem a positive coefficient pins the variable at its lower bound and a negative coefficient pins it at its upper bound; forMaximizethe signs flip. A zero coefficient leaves the variable at its lower bound (or the upper bound if the lower is-Inf). If the improving direction is unbounded — for example, a free variable with a nonzero objective coefficient that never appears in a constraint — presolve returns aResultwithStatus == Unboundedwithout ever touching the simplex. -
Empty rows. A constraint whose LHS
contains only fixed variables collapses to a scalar
comparison (e.g.
0 ≤ 5). If it is consistent the constraint is dropped and its dual is reported as0; if it is not (0 ≥ 5) presolve returns aResultwithStatus == Infeasible.
Presolve is transparent: it never mutates the
Problem you built, and the Result
that Solve returns is re-expanded into your
original index space. Fixed and empty-column variables
carry their settled value on res.Value(v); dropped
constraints carry a zero res.Dual(c).
If you need to drive the pass by hand — for debugging, or to
inspect the reduced model before it reaches the solver — call
Presolve directly:
reduced, undo, err := prob.Presolve(nil)
if err != nil {
return err
}
if undo.Terminal != grove.NotSolved {
// Presolve proved Infeasible / Unbounded / all-fixed-Optimal
// before the simplex ever needed to run.
return handleTerminal(undo)
}
// ... run reduced.Solve() or inspect reduced.Constraints() ...
Turn presolve off by setting
prob.SkipPresolve = true. You almost never
want to — the pass is O(nnz) cheap
and frequently saves the simplex a handful of degenerate
pivots — but the dial exists for anyone comparing grove
against a reference that solves the unreduced model.
Errors vs. logical outcomes
To summarise the contract:
| Situation | Return |
|---|---|
| Model is fine and has a solution | (res, nil), res.Status == Optimal |
| Model is fine but infeasible | (res, nil), res.Status == Infeasible |
| Model is fine but unbounded | (res, nil), res.Status == Unbounded |
| Ran out of iteration budget | (res, nil), res.Status == IterationLimit |
| Numerical breakdown (NaN, blow-up) | (res, nil), res.Status == NumericalError |
| Model itself is structurally invalid | (res, err), res.Status == NotSolved |
In practice this means you always check
err first, then switch on res.Status.
Don’t early-return on err != nil and skip
the Infeasible case — infeasibility is almost
always a result you want to report to the human running the
model, not a panic.
A note on determinism
grove’s simplex uses Bland’s rule for pivot
selection, which guarantees termination on degenerate
problems. A useful side-effect is that the number of
iterations is deterministic for a given model —
identical inputs give identical res.Iterations.
If you see an iteration count change between runs without a
model change, something else is varying (map iteration order
if you’re building Expr from a map,
typically).
With a result in hand, the next question is usually “what do the numbers mean?” — which is exactly the sensitivity chapter.