modeling

Problems, variables, constraints.

Every grove program is the same four steps in a different arrangement: create a Problem, declare Vars, hand the problem an objective, attach Constraints. This page is the whole modeling DSL on one screen.

Creating a problem

A Problem owns the variables and constraints you’ll hang off it. Construct one with a name and an optimization sense.

prob := grove.NewProblem("nurse_schedule", grove.Minimize)
// or: grove.NewProblem("value", grove.Maximize)

The name is used for error messages and file-format exports. Use the Go-idiomatic name-the-decision-problem convention (nurse_schedule, vm_allocation, diet). You can change the sense later by reassigning the field, but it’s a footgun — do it before you call Solve.

Variables

A variable is created through the Problem so that grove can track it, assign it a column index, and later feed you the value back:

x := prob.NewVar("x", grove.Continuous, grove.Bounds(0, grove.Inf))

Kinds

kind is one of three values, all of type grove.VarKind:

KindDomainStatus in v0.1
grove.Continuous real numbers within bounds solved directly
grove.Integer integers within bounds LP relaxation only
grove.Binary {0, 1} LP relaxation only

If you care about the Integer/Binary row, read Integer variables — grove runs a post-solve check and emits a warning if the LP relaxation didn’t happen to land on an integer vertex, but the caveat is important enough that the whole page is about it.

Bounds

grove.Bounds(low, high) is the only VarOption in v0.1. Both arguments are float64. For unbounded sides, pass grove.Inf or -grove.Inf:

a := prob.NewVar("a", grove.Continuous, grove.Bounds(0, grove.Inf))           // a ≥ 0
b := prob.NewVar("b", grove.Continuous, grove.Bounds(-grove.Inf, grove.Inf))  // free
c := prob.NewVar("c", grove.Continuous, grove.Bounds(0, 1))                   // [0, 1]
d := prob.NewVar("d", grove.Continuous, grove.Bounds(-5, 5))                  // [-5, 5]

If you omit Bounds entirely you get the default [0, +∞), which is what most LP formulations want.

Internally, bounded variables are substituted so the standard form sees only non-negative variables; free variables are split into a positive and a negative component. You don’t have to know any of that to use the library — the point is that you don’t need to do any of it by hand.

Expressions

grove.Expr is a Go map from *Var to coefficient. The map itself is the linear expression — there is no wrapper type to remember:

e := grove.Expr{x: 3, y: -2, z: 1} // 3x - 2y + z

Two helpers exist for when the expression comes from data:

e := grove.Expr{x: 1}
e.Add(grove.Expr{y: 2})          // e is now {x: 1, y: 2}
e.Scale(0.5)                     // e is now {x: 0.5, y: 1}

Because Expr is just a map, the ergonomic way to build a big constraint is the range-over-slice idiom:

obj := grove.Expr{}
for i := range foods {
    obj[x[i]] = foods[i].pricePerGram
}
prob.SetObjective(obj)

Objectives

Set the objective by handing the problem one Expr:

prob.SetObjective(grove.Expr{x: 1, y: 1})

The sense (Maximize / Minimize) is on the Problem, not on the objective — one less sign to track. grove negates internally for Maximize and un-negates the returned objective for you.

A fixed constant offset is optional:

prob.SetObjectiveConstant(5) // z → z + 5

This is convenient when the mathematically clean objective is something like “minimise total cost with a flat $5 admin fee on top”.

Constraints

Each AddConstraint call returns a *Constraint handle. Hold onto it if you want to read the shadow price back from the Result:

budget := prob.AddConstraint(
    "budget",
    grove.Expr{day: 1200, night: 1500},
    grove.LTE,
    18000,
)
// later:
fmt.Println(res.Dual(budget)) // ∂z*/∂rhs of `budget`

The relation is grove.LTE, grove.GTE, or grove.EQ. Right-hand side is a single float64. Negative RHS, non-zero RHS, and two-sided inequalities composed of a pair of rows all work.

Constraint names must be unique per problem; duplicates will cause Validate to fail. Names show up in error messages, LP/MPS exports, and the sensitivity report.

Validation

Problem.Validate runs automatically at the top of Solve, but you can call it yourself earlier if you’re building a model in pieces. It returns []error with every problem found — it never short-circuits on the first failure, so a single call surfaces the whole class of issues at once:

if errs := prob.Validate(); len(errs) > 0 {
    for _, err := range errs {
        log.Println("bad model:", err)
    }
    return errors.Join(errs...)
}

The full list of checks:

KindWhat it catches
ValidationNoVariables The problem has no variables at all.
ValidationNoObjective SetObjective was never called (the objective map is empty).
ValidationZeroObjective Every objective coefficient is finite and zero. If any coefficient is NaN or ±Inf, only ValidationBadObjectiveCoef is reported for those terms. grove treats an all-finite-zero objective as a modeling mistake — add a non-zero coefficient on at least one variable, or (for user-built models) drop the objective call entirely and hit ValidationNoObjective. Presolve-reduced problems may legally have an empty objective map when the linear part was folded into SetObjectiveConstant.
ValidationBadBound A variable has a NaN bound. ±Inf is still valid — that’s the sentinel for an unbounded side.
ValidationInvertedBounds low > high on Bounds, which would leave the variable’s domain empty.
ValidationBadObjectiveCoef The objective has a NaN or ±Inf coefficient.
ValidationBadRHS A constraint has a NaN or ±Inf right-hand side.
ValidationEmptyRow A constraint’s LHS is the empty expression Expr{}.
ValidationZeroRow A constraint has a non-empty LHS and every coefficient is finite and zero (Expr{x: 0, y: 0}). Rows with any NaN/±Inf coefficient only report ValidationBadCoefficient, not ValidationZeroRow. These would be dropped by presolve anyway; rejecting up front surfaces the likely modeling typo.
ValidationBadCoefficient A constraint has a NaN or ±Inf coefficient on one of its variables.
ValidationDuplicateConstraint Two or more constraints share the same name. (AddConstraint panics on duplicates at registration time; Validate is the backstop for direct-slice construction and file readers.)

Each entry in the returned slice is a *ValidationError with a stable Kind field — handy when tooling wants to react to a specific class of issue rather than grepping strings:

var verr *grove.ValidationError
for _, err := range prob.Validate() {
    if errors.As(err, &verr) && verr.Kind == grove.ValidationDuplicateConstraint {
        fixDuplicateName(verr.Target)
    }
}

Both NewVar and AddConstraint still panic on duplicate names at registration time; Validate exists to catch the same class of problem when a model is built any other way — a file reader, code inside the grove package that appends to the internal constraint slice, or a test that mutates the live slice returned by Constraints() (the backing array is shared; treat it as read-only unless you know what you’re doing).

Putting it together

A complete model is short. This is the nurse-scheduling LP from the intro, with the shape of every section above made explicit:

prob := grove.NewProblem("nurses", grove.Maximize)           // Problem

day   := prob.NewVar("day",   grove.Continuous, grove.Bounds(0, grove.Inf))  // Variables
night := prob.NewVar("night", grove.Continuous, grove.Bounds(0, grove.Inf))

prob.SetObjective(grove.Expr{day: 1, night: 1})              // Objective

prob.AddConstraint("day_min",   grove.Expr{day: 1},                 grove.GTE, 4)
prob.AddConstraint("night_min", grove.Expr{night: 1},               grove.GTE, 2)
prob.AddConstraint("budget",    grove.Expr{day: 1200, night: 1500}, grove.LTE, 18000)

res, _ := prob.Solve()                                        // Solve

With a model in hand, the next page is how to read the Result object.