solving

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 hit p.MaxIterations before 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 with err != 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 Minimize problem a positive coefficient pins the variable at its lower bound and a negative coefficient pins it at its upper bound; for Maximize the 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 a Result with Status == Unbounded without 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 as 0; if it is not (0 ≥ 5) presolve returns a Result with Status == 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:

SituationReturn
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.