diff --git a/README.md b/README.md index 5a36329..3f951a4 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ * Clean and idiomatic API for modeling and solving LPs. * Focused on numerical stability and usability. * Performs basic branch-and-bound techniques for pure integer problems. +* Provides native multi-objective optimization support. +* Builtin bindings for C and Python (coming soon). `gspl` is ideal for embedding linear optimization into Go-based software. @@ -28,95 +30,7 @@ go get github.com/chriso345/gspl ## Usage -```go -package main - -import ( - "fmt" - - "github.com/chriso345/gspl/lp" - "github.com/chriso345/gspl/solver" -) - -func main() { - // Create decision variables - variables := []lp.LpVariable{ - lp.NewVariable("x1"), - lp.NewVariable("x2"), - lp.NewVariable("x3"), - } - - x1 := &variables[0] - x2 := &variables[1] - x3 := &variables[2] - - // Objective function: Minimize -6 * x1 + 7 * x2 + 4 * x3 - objective := lp.NewExpression([]lp.LpTerm{ - lp.NewTerm(-6, *x1), - lp.NewTerm(7, *x2), - lp.NewTerm(4, *x3), - }) - - // Set up the LP problem - example := lp.NewLinearProgram("README Example", variables) - example.AddObjective(lp.LpMinimise, objective) - - // Add constraints - example.AddConstraint(lp.NewExpression([]lp.LpTerm{ - lp.NewTerm(2, *x1), - lp.NewTerm(5, *x2), - lp.NewTerm(-1, *x3), - }), lp.LpConstraintLE, 18) - - example.AddConstraint(lp.NewExpression([]lp.LpTerm{ - lp.NewTerm(1, *x1), - lp.NewTerm(-1, *x2), - lp.NewTerm(-2, *x3), - }), lp.LpConstraintLE, -14) - - example.AddConstraint(lp.NewExpression([]lp.LpTerm{ - lp.NewTerm(3, *x1), - lp.NewTerm(2, *x2), - lp.NewTerm(2, *x3), - }), lp.LpConstraintEQ, 26) - - // Print the current problem - fmt.Printf("%s\n", example.String()) - - // Solve it - sol, err := solver.Solve(&example) - if err != nil { - fmt.Println("solve error:", err) - return - } - fmt.Printf("Optimal Objective Value: %.2f\n", sol.ObjectiveValue) - fmt.Printf("Primal Solution: %v\n", sol.PrimalSolution.RawVector().Data) -} -``` - -### What's Happening? - -We're setting up a linear program to: - -1. **Minimize**: - $-6x_1 + 7x_2 + 4x_3$ -2. **Subject to constraints**: - - * $2x_1 + 5x_2 - x_3 \leq 18$ - * $x_1 - x_2 - 2x_3 \leq -14$ - * $3x_1 + 2x_2 + 2x_3 = 26$ - -The solution will print the optimal values of the variables and the minimized objective value. - -See the [examples](examples) directory for other scenarios. - ---- - -## Performance - -A small benchmark is included (go test -bench ./solver) that measures solve performance on a tiny LP; results will vary by CPU and Go version. The solver is optimized for minimal allocations in the revised simplex implementation and supports cancellation via context passed with SolverOption. For concurrent use, provide each goroutine its own *lp.LinearProgram; Solve may temporarily reference fields inside the provided program for performance and thus the same program must not be mutated concurrently during a Solve call. - -## API Overview +Full examples are available in the [gspl_examples_test.go](gspl_examples_test.go) file and the [examples/](examples/) directory. ### Variables @@ -129,13 +43,13 @@ variables := []lp.LpVariable{ } ``` -You can access and pass them as pointers: +You can access and pass them by value or take their address when needed: ```go x1 := &variables[0] ``` -Forcing integer constraints is done at the variable level: +Forcing integer constraints is done when creating the variable: ```go variables := []lp.LpVariable{ @@ -150,16 +64,16 @@ Build the objective using terms: ```go objective := lp.NewExpression([]lp.LpTerm{ - lp.NewTerm(5, *x1), - lp.NewTerm(3, *x2), + lp.NewTerm(5, variables[0]), + lp.NewTerm(3, variables[1]), }) ``` Add it to the LP: ```go -lp := lp.NewLinearProgram("My LP", variables) -lp.AddObjective(lp.LpMaximise, objective) +prog := lp.NewLinearProgram("My LP", variables) +prog.AddObjective(lp.LpMaximise, objective) ``` ### Constraints @@ -167,9 +81,9 @@ lp.AddObjective(lp.LpMaximise, objective) Each constraint uses an expression, a comparison type, and a right-hand side: ```go -lp.AddConstraint(lp.NewExpression([]lp.LpTerm{ - lp.NewTerm(2, *x1), - lp.NewTerm(3, *x2), +prog.AddConstraint(lp.NewExpression([]lp.LpTerm{ + lp.NewTerm(2, variables[0]), + lp.NewTerm(3, variables[1]), }), lp.LpConstraintLE, 10) ``` @@ -182,15 +96,34 @@ Constraint types: ### Solving ```go -solution, err := solver.Solve(&lp) +solution, err := solver.Solve(&prog) if err != nil { // handle error } -solution.PrintSolution() +fmt.Printf("obj=%.2f, x=%.2f\n", solution.ObjectiveValue, solution.PrimalSolution.AtVec(0)) ``` This solves the model and prints variable values and the objective result. +### Multi-Objective Support + +gspl provides native multi-objective optimization capabilities via the `mop` package. + +```go +x := lp.NewVariable("x") +y := lp.NewVariable("y") +p := lp.NewLinearProgram("Pareto", []lp.LpVariable{x, y}) +p.AddObjective(lp.LpMaximise, lp.NewExpression([]lp.LpTerm{lp.NewTerm(1, x)})) +p.AddObjective(lp.LpMaximise, lp.NewExpression([]lp.LpTerm{lp.NewTerm(1, y)})) +p.AddConstraint(lp.NewExpression([]lp.LpTerm{lp.NewTerm(1, x), lp.NewTerm(1, y)}), lp.LpConstraintLE, 10) + +sols, err := mop.SolvePareto(&p, mop.ParetoWeightedSum, mop.WithWeightedSums([][]float64{{1, 0}, {0, 1}})) +if err != nil { + fmt.Println("err", err) + return +} +``` + --- ## License diff --git a/internal/lang/doc.go b/internal/lang/doc.go new file mode 100644 index 0000000..80543f0 --- /dev/null +++ b/internal/lang/doc.go @@ -0,0 +1,4 @@ +// Package lang provides parsing and utilities for the GSPL language. +// It contains parsers, AST types, and helpers used across the solver and CLI. +// This file holds package documentation. +package lang diff --git a/justfile b/justfile index 51d5334..c829e3e 100644 --- a/justfile +++ b/justfile @@ -81,5 +81,5 @@ run *args: # Open the documentation in the browser [group("Documentation")] -docs: +site: pkgsite diff --git a/lp/examples_test.go b/lp/examples_test.go new file mode 100644 index 0000000..ea905be --- /dev/null +++ b/lp/examples_test.go @@ -0,0 +1,60 @@ +package lp + +import ( + "fmt" +) + +func ExampleNewLinearProgram() { + x := NewVariable("x") + y := NewVariable("y") + p := NewLinearProgram("Example LP", []LpVariable{x, y}) + p.AddObjective(LpMinimise, NewExpression([]LpTerm{NewTerm(1, x), NewTerm(2, y)})) + p.AddConstraint(NewExpression([]LpTerm{NewTerm(1, x), NewTerm(1, y)}), LpConstraintGE, 3) + + fmt.Println(p.Description) + // Output: Example LP +} + +func ExampleNewVariable() { + x := NewVariable("x") + y := NewVariable("y", LpCategoryInteger) + z := NewVariable("z", LpCategoryBinary) + fmt.Printf("%s %d %s %d %s %d\n", x.Name, int(x.Category), y.Name, int(y.Category), z.Name, int(z.Category)) + // Output: x 0 y 1 z 2 +} + +func ExampleNewTerm() { + x := NewVariable("x") + t := NewTerm(3, x) + fmt.Printf("%.0f %s\n", t.Coefficient, t.Variable.Name) + // Output: 3 x +} + +func ExampleNewExpression() { + x := NewVariable("x") + t := NewTerm(2, x) + expr := NewExpression([]LpTerm{t}) + fmt.Printf("%d\n", len(expr.Terms)) + // Output: 1 +} + +func ExampleLinearProgram_AddObjective() { + x := NewVariable("x") + y := NewVariable("y") + p := NewLinearProgram("o", []LpVariable{x, y}) + p.AddObjective(LpMaximise, NewExpression([]LpTerm{NewTerm(2, x), NewTerm(-3, y)})) + // Coefficients are negated for maximisation + fmt.Printf("%.0f %.0f %t\n", p.Objective.AtVec(0), p.Objective.AtVec(1), p.ObjectiveIsNegated) + // Output: -2 3 true +} + +func ExampleLinearProgram_AddConstraint() { + x := NewVariable("x") + y := NewVariable("y") + p := NewLinearProgram("c", []LpVariable{x, y}) + p.AddObjective(LpMinimise, NewExpression([]LpTerm{NewTerm(1, x), NewTerm(1, y)})) + p.AddConstraint(NewExpression([]LpTerm{NewTerm(1, x), NewTerm(2, y)}), LpConstraintGE, 3) + // Constraint row: 1 2 -1 (surplus) and objective expanded to length 3 + fmt.Printf("%.0f %.0f %.0f %d\n", p.Constraints.At(0, 0), p.Constraints.At(0, 1), p.Constraints.At(0, 2), p.Objective.Len()) + // Output: 1 2 -1 3 +} diff --git a/lp/formulate.go b/lp/formulate.go index 7c70c4a..0dbbde3 100644 --- a/lp/formulate.go +++ b/lp/formulate.go @@ -5,7 +5,11 @@ import ( "gonum.org/v1/gonum/mat" ) -// AddObjective adds a new objective function to the LP model. +// AddObjective sets the objective function of the [LinearProgram]. +// The provided expression defines the coefficients; sense selects [LpMinimise] +// or [LpMaximise]. If [LpMaximise] is used coefficients are negated to convert +// the problem to the solver's minimisation form and [LinearProgram].ObjectiveIsNegated +// is set to avoid double-negation. func (lp *LinearProgram) AddObjective(sense LpSense, expr LpExpression) { lp.Sense = sense @@ -45,7 +49,12 @@ func (lp *LinearProgram) AddObjective(sense LpSense, expr LpExpression) { } } -// AddConstraint adds a new constraint to the LP model. +// AddConstraint appends a constraint to the [LinearProgram]. +// expr is the left-hand side, conType is one of [LpConstraintLE], [LpConstraintEQ] +// or [LpConstraintGE], and rhs is the right-hand side. The objective must be +// defined before adding constraints. If rhs is negative the constraint is +// inverted. For LE/GE constraints a slack or surplus variable is created and +// the program matrices and objective are expanded. func (lp *LinearProgram) AddConstraint(expr LpExpression, conType LpConstraintType, rhs float64) { if lp.Objective == nil { panic("objective function must be defined before adding constraints") diff --git a/lp/lp.go b/lp/lp.go index 13ab054..e41a73c 100644 --- a/lp/lp.go +++ b/lp/lp.go @@ -32,7 +32,8 @@ type LinearProgram struct { ObjectiveIsNegated bool } -// NewLinearProgram Create a new Linear Program +// NewLinearProgram creates a new [LinearProgram] with the provided description and variables. +// The returned program's Status is initialized to [common.SolverStatusNotSolved]. func NewLinearProgram(desc string, vars []LpVariable) LinearProgram { lp := LinearProgram{ diff --git a/lp/types.go b/lp/types.go index b74d1e6..5c1dbe7 100644 --- a/lp/types.go +++ b/lp/types.go @@ -2,28 +2,30 @@ package lp import "github.com/chriso345/gspl/internal/common" -// LpExpression represents the LHS of a linear expression +// LpExpression represents the left-hand side of a linear expression and +// holds a slice of [LpTerm]. type LpExpression struct { Terms []LpTerm } -// NewExpression creates a new LpExpression with the given terms +// NewExpression creates a new [LpExpression] with the given terms. func NewExpression(terms []LpTerm) LpExpression { return LpExpression{terms} } -// LpTerm represents a term in a linear expression, consisting of a coefficient and a variable. +// LpTerm represents a term in a linear expression, consisting of a coefficient and a [LpVariable]. type LpTerm struct { Coefficient float64 Variable LpVariable // These get added to the variable list in the LinearProgram?? } -// NewTerm creates a new LpTerm with the given coefficient and variable. +// NewTerm returns a new [LpTerm] with the given coefficient and [LpVariable]. func NewTerm(coefficient float64, variable LpVariable) LpTerm { return LpTerm{coefficient, variable} } -// LpVariable represents a variable in a linear programming problem. +// LpVariable represents a variable in a linear programming problem and +// includes metadata such as slack/artificial flags and its [LpCategory]. type LpVariable struct { Name string IsSlack bool @@ -31,7 +33,7 @@ type LpVariable struct { Category LpCategory } -// NewVariable creates a new LpVariable with the given name. +// NewVariable creates a new [LpVariable] with the given name and optional [LpCategory]. func NewVariable(name string, category ...LpCategory) LpVariable { if len(category) > 1 { panic("Only one LpCategory can be specified for a variable") @@ -42,7 +44,8 @@ func NewVariable(name string, category ...LpCategory) LpVariable { return LpVariable{name, false, false, category[0]} } -// LpCategory represents the category of a variable in a linear programming problem. +// LpCategory is an alias for [common.VarCategory] and classifies variables +// (continuous, integer, binary). type LpCategory = common.VarCategory const ( @@ -51,7 +54,8 @@ const ( LpCategoryBinary ) -// LpSense represents the sense of the linear programming problem, either minimization or maximization. +// LpSense represents the problem sense for a [LinearProgram]. +// Possible values are [LpMinimise] and [LpMaximise]. type LpSense int const ( @@ -59,7 +63,8 @@ const ( LpMaximise ) -// LpStatus represents the current status of solving the linear programming problem. +// LpStatus represents the current status of solving a [LinearProgram]. +// Values include [LpStatusNotSolved], [LpStatusOptimal], [LpStatusInfeasible], and [LpStatusUnbounded]. type LpStatus int const ( @@ -79,7 +84,8 @@ func (s LpStatus) String() string { }[s] } -// LpConstraintType represents the type of a constraint in a linear programming problem. +// LpConstraintType represents the relation used in a constraint (<=, =, >=). +// Constants are [LpConstraintLE], [LpConstraintEQ], and [LpConstraintGE]. type LpConstraintType int const ( diff --git a/solver/examples_test.go b/solver/examples_test.go new file mode 100644 index 0000000..bfd4349 --- /dev/null +++ b/solver/examples_test.go @@ -0,0 +1,92 @@ +package solver + +import ( + "context" + "fmt" + + "github.com/chriso345/gspl/lp" +) + +func ExampleSolve() { + // Build a trivial LP: minimize x subject to x >= 1 + x := lp.NewVariable("x") + p := lp.NewLinearProgram("Example LP", []lp.LpVariable{x}) + p.AddObjective(lp.LpMinimise, lp.NewExpression([]lp.LpTerm{lp.NewTerm(1, x)})) + p.AddConstraint(lp.NewExpression([]lp.LpTerm{lp.NewTerm(1, x)}), lp.LpConstraintGE, 1) + + // Run solve with a background context + sol, err := Solve(&p, WithContext(context.Background())) + if err != nil { + fmt.Println("solve failed:", err) + return + } + fmt.Printf("obj=%.0f, x=%.0f, status=%v\n", sol.ObjectiveValue, sol.PrimalSolution.AtVec(0), sol.Status) + // Output: obj=1, x=1, status=Optimal +} + +func ExampleNewSolverConfig() { + x := lp.NewVariable("x") + p := lp.NewLinearProgram("t", []lp.LpVariable{x}) + p.AddObjective(lp.LpMinimise, lp.NewExpression([]lp.LpTerm{lp.NewTerm(1, x)})) + p.AddConstraint(lp.NewExpression([]lp.LpTerm{lp.NewTerm(1, x)}), lp.LpConstraintGE, 0) + + sol, _ := Solve(&p, WithTolerance(0.0001), WithMaxIterations(100), WithGapSensitivity(0.001), WithLogging(true)) + fmt.Printf("%.4f %v\n", sol.ObjectiveValue, sol.Status) + // Output: 0.0000 Optimal +} + +func ExampleWithContext() { + x := lp.NewVariable("x") + p := lp.NewLinearProgram("t", []lp.LpVariable{x}) + p.AddObjective(lp.LpMinimise, lp.NewExpression([]lp.LpTerm{lp.NewTerm(1, x)})) + p.AddConstraint(lp.NewExpression([]lp.LpTerm{lp.NewTerm(1, x)}), lp.LpConstraintGE, 0) + + sol, _ := Solve(&p, WithContext(context.Background())) + fmt.Println(sol != nil) + // Output: true +} + +func ExampleWithTolerance() { + // trivial LP minimize x subject to x >= 0 + x := lp.NewVariable("x") + p := lp.NewLinearProgram("t", []lp.LpVariable{x}) + p.AddObjective(lp.LpMinimise, lp.NewExpression([]lp.LpTerm{lp.NewTerm(1, x)})) + p.AddConstraint(lp.NewExpression([]lp.LpTerm{lp.NewTerm(1, x)}), lp.LpConstraintGE, 0) + + sol, _ := Solve(&p, WithTolerance(1e-5)) + fmt.Printf("%.5f\n", sol.ObjectiveValue) + // Output: 0.00000 +} + +func ExampleWithMaxIterations() { + x := lp.NewVariable("x") + p := lp.NewLinearProgram("t", []lp.LpVariable{x}) + p.AddObjective(lp.LpMinimise, lp.NewExpression([]lp.LpTerm{lp.NewTerm(1, x)})) + p.AddConstraint(lp.NewExpression([]lp.LpTerm{lp.NewTerm(1, x)}), lp.LpConstraintGE, 0) + + sol, _ := Solve(&p, WithMaxIterations(10)) + fmt.Println(sol.Status) + // Output: Optimal +} + +func ExampleWithGapSensitivity() { + x := lp.NewVariable("x") + p := lp.NewLinearProgram("t", []lp.LpVariable{x}) + p.AddObjective(lp.LpMinimise, lp.NewExpression([]lp.LpTerm{lp.NewTerm(1, x)})) + p.AddConstraint(lp.NewExpression([]lp.LpTerm{lp.NewTerm(1, x)}), lp.LpConstraintGE, 0) + + sol, _ := Solve(&p, WithGapSensitivity(0.5)) + fmt.Printf("%.1f\n", sol.ObjectiveValue) + // Output: 0.0 +} + +func ExampleWithLogging() { + x := lp.NewVariable("x") + p := lp.NewLinearProgram("t", []lp.LpVariable{x}) + p.AddObjective(lp.LpMinimise, lp.NewExpression([]lp.LpTerm{lp.NewTerm(1, x)})) + p.AddConstraint(lp.NewExpression([]lp.LpTerm{lp.NewTerm(1, x)}), lp.LpConstraintGE, 0) + + sol, _ := Solve(&p, WithLogging(true)) + fmt.Println(sol.Status) + // Output: Optimal +} diff --git a/solver/mop/doc.go b/solver/mop/doc.go index 7186381..af5d1db 100644 --- a/solver/mop/doc.go +++ b/solver/mop/doc.go @@ -1,7 +1,7 @@ // Package mop provides helpers for solving multi-objective linear programs. // -// It offers several algorithms for generating Pareto-optimal solution sets and -// supports lexicographic optimisation where objectives are solved in priority -// order. The package is intended to operate on lp.LinearProgram instances from -// the lp package and to use the solver package for single-objective solves. +// The package offers algorithms for generating Pareto-optimal solution sets +// and for lexicographic optimisation where objectives are solved in priority +// order. The functions operate on [lp.LinearProgram] instances and use the +// [solver] package for single-objective solves. package mop diff --git a/solver/mop/examples_test.go b/solver/mop/examples_test.go new file mode 100644 index 0000000..256162d --- /dev/null +++ b/solver/mop/examples_test.go @@ -0,0 +1,80 @@ +package mop + +import ( + "fmt" + + "github.com/chriso345/gspl/lp" + "gonum.org/v1/gonum/mat" +) + +func ExampleSolveLexicographic() { + x := lp.NewVariable("x") + y := lp.NewVariable("y") + p := lp.NewLinearProgram("Lex", []lp.LpVariable{x, y}) + // primary: minimise x + 10y + p.AddObjective(lp.LpMinimise, lp.NewExpression([]lp.LpTerm{lp.NewTerm(1, x), lp.NewTerm(10, y)})) + // constraints: x + y >= 10 and x <= 8 + p.AddConstraint(lp.NewExpression([]lp.LpTerm{lp.NewTerm(1, x), lp.NewTerm(1, y)}), lp.LpConstraintGE, 10) + p.AddConstraint(lp.NewExpression([]lp.LpTerm{lp.NewTerm(1, x)}), lp.LpConstraintLE, 8) + // secondary: minimise x + v := lp.NewExpression([]lp.LpTerm{lp.NewTerm(1, x)}) + p.SecondaryObjectives = append(p.SecondaryObjectives, matCopyVec(v, p.Vars)) + + sol, err := SolveLexicographic(&p) + if err != nil { + fmt.Println("err", err) + return + } + fmt.Printf("primary=%.0f secondary=%.0f status=%v\n", sol.ObjectiveValues[0], sol.ObjectiveValues[1], sol.Status) + // Output: primary=28 secondary=8 status=Optimal +} + +func ExampleSolvePareto_weightedSum() { + x := lp.NewVariable("x") + y := lp.NewVariable("y") + p := lp.NewLinearProgram("Pareto", []lp.LpVariable{x, y}) + p.AddObjective(lp.LpMaximise, lp.NewExpression([]lp.LpTerm{lp.NewTerm(1, x)})) + p.AddObjective(lp.LpMaximise, lp.NewExpression([]lp.LpTerm{lp.NewTerm(1, y)})) + p.AddConstraint(lp.NewExpression([]lp.LpTerm{lp.NewTerm(1, x), lp.NewTerm(1, y)}), lp.LpConstraintLE, 10) + + // Provide simple weights to sample the front + sols, err := SolvePareto(&p, ParetoWeightedSum, WithWeightedSums([][]float64{{1, 0}, {0, 1}})) + if err != nil { + fmt.Println("err", err) + return + } + fmt.Println(len(sols) >= 2) + // Output: true +} + +func ExampleSolvePareto_epsilonConstraint() { + x := lp.NewVariable("x") + y := lp.NewVariable("y") + p := lp.NewLinearProgram("Pareto", []lp.LpVariable{x, y}) + p.AddObjective(lp.LpMaximise, lp.NewExpression([]lp.LpTerm{lp.NewTerm(1, x)})) + p.AddObjective(lp.LpMaximise, lp.NewExpression([]lp.LpTerm{lp.NewTerm(1, y)})) + p.AddConstraint(lp.NewExpression([]lp.LpTerm{lp.NewTerm(1, x), lp.NewTerm(1, y)}), lp.LpConstraintLE, 10) + + // Provide epsilon steps to sample the front + sols, err := SolvePareto(&p, ParetoEpsilonConstraint, WithEpsilonSteps(3)) + if err != nil { + fmt.Println("err", err) + return + } + fmt.Println(len(sols) >= 2) + // Output: true +} + +// helper to copy an expression into a *mat.VecDense of length n +func matCopyVec(expr lp.LpExpression, vars []lp.LpVariable) *mat.VecDense { + v := mat.NewVecDense(len(vars), nil) + for _, t := range expr.Terms { + for i, vv := range vars { + if vv.Name == t.Variable.Name { + v.SetVec(i, t.Coefficient) + break + } + } + } + return v +} diff --git a/solver/mop/lexicographic.go b/solver/mop/lexicographic.go index 2981b65..219d056 100644 --- a/solver/mop/lexicographic.go +++ b/solver/mop/lexicographic.go @@ -7,11 +7,10 @@ import ( "gonum.org/v1/gonum/mat" ) -// SolveLexicographic solves the given multi-objective linear program using -// a lexicographic approach. This method optimises the objectives in a predefined -// order, ensuring that the optimal value of each objective is achieved before -// moving on to the next. It returns a single MopSolution representing the optimal -// solution that respects the priority of objectives. +// SolveLexicographic solves the given multi-objective [lp.LinearProgram] using +// a lexicographic approach. Objectives are optimised in priority order so that +// each objective's optimal value is fixed before proceeding to the next. It +// returns a single [MopSolution] that respects the provided objective ordering. func SolveLexicographic(prog *lp.LinearProgram, opts ...solver.SolverOption) (*MopSolution, error) { if prog.Objective == nil { return nil, errors.New(errors.ErrInvalidInput, "primary objective must be defined", nil) diff --git a/solver/mop/options.go b/solver/mop/options.go index 9848346..f9fbcef 100644 --- a/solver/mop/options.go +++ b/solver/mop/options.go @@ -7,21 +7,24 @@ import ( type MopOption = solver.SolverOption -// WithEpsilonSteps sets the number of epsilon steps for the epsilon-constraint method. +// WithEpsilonSteps sets the number of epsilon steps used by [SolvePareto] +// when the [ParetoEpsilonConstraint] method is selected. func WithEpsilonSteps(steps int) MopOption { return func(cfg *common.SolverConfig) { cfg.EpsilonSteps = steps } } -// WithWeightedSums sets the weight vectors for the weighted sum method. +// WithWeightedSums sets weight vectors used by [SolvePareto] when the +// [ParetoWeightedSum] method is selected. func WithWeightedSums(weights [][]float64) MopOption { return func(cfg *common.SolverConfig) { cfg.WeightedSums = weights } } -// with sets the entire SolverConfig (for internal use only). +// with sets the entire SolverConfig and is used internally to pass modified +// configs into internal solver calls. func with(cfg_ common.SolverConfig) MopOption { return func(cfg *common.SolverConfig) { *cfg = cfg_ diff --git a/solver/mop/pareto.go b/solver/mop/pareto.go index d5158fb..45a6af7 100644 --- a/solver/mop/pareto.go +++ b/solver/mop/pareto.go @@ -7,10 +7,10 @@ import ( "gonum.org/v1/gonum/mat" ) -// SolvePareto solves the given multi-objective linear program using a Pareto -// approach. This method seeks to find a set of solutions that represent the best -// trade-offs among the objectives, known as the Pareto front. It returns a slice -// of MopSolution, each representing a non-dominated solution in the objective space. +// SolvePareto solves the given multi-objective [lp.LinearProgram] using a Pareto +// approach. The function returns a slice of [*MopSolution], each representing a +// non-dominated solution on the Pareto front. The behavior depends on the +// selected [ParetoMethod]. func SolvePareto( prog *lp.LinearProgram, method ParetoMethod, diff --git a/solver/mop/types.go b/solver/mop/types.go index 45baed8..84ef405 100644 --- a/solver/mop/types.go +++ b/solver/mop/types.go @@ -6,6 +6,9 @@ import ( ) // MopSolution contains the result of a multi-objective optimization. +// +// The [MopSolution] holds objective values (in the same order as provided +// in the program), the primal solution vector, and the [common.SolverStatus]. type MopSolution struct { ObjectiveValues []float64 PrimalSolution *mat.VecDense diff --git a/solver/options.go b/solver/options.go index ba20125..1c9d3ae 100644 --- a/solver/options.go +++ b/solver/options.go @@ -6,43 +6,50 @@ import ( "github.com/chriso345/gspl/internal/common" ) -// SolverOption defines a function that modifies SolverConfig. +// SolverOption is a functional option that modifies a SolverConfig. type SolverOption func(*common.SolverConfig) -// WithTolerance sets the tolerance. +// WithTolerance sets the numerical tolerance used by the solver. +// It controls convergence criteria for linear solves and floating-point checks. +// It does not affect integer gap sensitivity; use [WithGapSensitivity] for that. func WithTolerance(t float64) SolverOption { return func(cfg *common.SolverConfig) { cfg.Tolerance = t } } -// WithContext sets a context for cancellation of long-running solves. +// WithContext attaches a context used to cancel or time out solver operations. func WithContext(ctx context.Context) SolverOption { return func(cfg *common.SolverConfig) { cfg.Ctx = ctx } } -// WithMaxIterations sets the maximum number of iterations. +// WithMaxIterations sets an upper bound on the number of solver iterations. +// A value <= 0 usually means no iteration limit. func WithMaxIterations(max int) SolverOption { return func(cfg *common.SolverConfig) { cfg.MaxIterations = max } } -// WithGapSensitivity sets the gap sensitivity. +// WithGapSensitivity sets the sensitivity used when comparing objective gaps +// in integer problems. Smaller values make the solver treat small differences +// as significant. func WithGapSensitivity(gap float64) SolverOption { return func(cfg *common.SolverConfig) { cfg.GapSensitivity = gap } } -// WithThreads sets the number of threads to use. +// WithThreads configures the number of OS threads the solver may use. +// Currently not implemented and will panic if used. func WithThreads(n int) SolverOption { panic("multi-threading not yet implemented") } -// WithLogging enables or disables logging. +// WithLogging enables or disables internal solver logging. +// Pass true to enable verbose runtime information useful for debugging. func WithLogging(enabled bool) SolverOption { return func(cfg *common.SolverConfig) { cfg.Logging = enabled @@ -51,24 +58,28 @@ func WithLogging(enabled bool) SolverOption { /// Strategy Functions Options -// WithBranch sets the branching strategy function. +// WithBranch sets a custom branching strategy used by the solver. +// By default the solver uses [brancher.DefaultBranch] if no branch function is supplied. func WithBranch(common.BranchFunc) SolverOption { panic("branching not yet implemented") } -// WithHeuristic sets the heuristic strategy function. +// WithHeuristic sets the heuristic function that produces incumbent solutions. +// If unspecified the solver uses [brancher.DefaultHeuristic]. func WithHeuristic(common.HeuristicFunc) SolverOption { panic("heuristics not yet implemented") } -// WithCut sets the cut generation function. +// WithCut sets the cut-generation function used to add cutting planes. +// The solver falls back to [brancher.DefaultCut] when none is provided. func WithCut(common.CutFunc) SolverOption { panic("cutting planes not yet implemented") } /// Helpers -// NewSolverConfig builds a SolverConfig applying all options on defaults. +// NewSolverConfig returns a SolverConfig built from the default configuration +// with each provided SolverOption applied in order. func NewSolverConfig(opts ...SolverOption) *common.SolverConfig { cfg := common.DefaultSolverConfig() for _, opt := range opts { diff --git a/solver/solver.go b/solver/solver.go index a162077..82669c4 100644 --- a/solver/solver.go +++ b/solver/solver.go @@ -12,18 +12,19 @@ import ( "gonum.org/v1/gonum/mat" ) -// Solution contains the results returned by Solve. +// Solution contains the results returned by [Solve]. // -// The returned Solution provides a snapshot of the computed objective value, +// The returned [Solution] provides a snapshot of the computed objective value, // the primal solution vector, and a status code describing optimality, -// infeasibility, or unboundedness. The Solution contains copies of data +// infeasibility, or unboundedness. The [Solution] contains copies of data // that callers can safely inspect without referencing the original -// lp.LinearProgram. +// [lp.LinearProgram]. // // Note: for performance the solver may temporarily reuse internal SCF fields -// that point into the provided LinearProgram; therefore callers MUST NOT mutate -// the provided *lp.LinearProgram concurrently with a call to Solve. -// To cancel a long-running solve pass a context using the WithContext option. +// that point into the provided [lp.LinearProgram]; therefore callers MUST NOT +// mutate the provided [*lp.LinearProgram] concurrently with a call to +// [Solve]. To cancel a long-running solve pass a context using the +// [WithContext] option. type Solution struct { ObjectiveValue float64 PrimalSolution *mat.VecDense @@ -32,12 +33,12 @@ type Solution struct { // Solve solves the given linear program and returns a Solution and an error. // -// The function returns a populated *Solution on success, or a non-nil error if +// The function returns a populated [*Solution] on success, or a non-nil error if // the solve failed. Solve respects context cancellation when a context is -// provided via SolverOption (WithContext). It may temporarily link into fields -// of the provided LinearProgram for efficiency; therefore the provided program +// provided via SolverOption ([WithContext]). It may temporarily link into fields +// of the provided [lp.LinearProgram] for efficiency; therefore the provided program // must not be mutated concurrently. Solve is safe to call concurrently as long -// as each goroutine uses a distinct *lp.LinearProgram. +// as each goroutine uses a distinct [*lp.LinearProgram]. func Solve(prog *lp.LinearProgram, opts ...SolverOption) (*Solution, error) { // Apply options options := NewSolverConfig(opts...) @@ -132,7 +133,7 @@ func Solve(prog *lp.LinearProgram, opts ...SolverOption) (*Solution, error) { return sol, nil } -// newSCF creates a new SCF instance for the linear program +// newSCF constructs a [*common.StandardComputationalForm] for the given [lp.LinearProgram]. func newSCF(prog *lp.LinearProgram) *common.StandardComputationalForm { slackIndices := make([]int, len(prog.Vars)) numPrimals := 0 @@ -177,7 +178,8 @@ func newSCF(prog *lp.LinearProgram) *common.StandardComputationalForm { } } -// newIP creates a new IP instance for the linear program +// newIP constructs a [*common.IntegerProgram] for the given [lp.LinearProgram] +// and initializes BestObj according to the program sense. func newIP(prog *lp.LinearProgram) *common.IntegerProgram { ip := &common.IntegerProgram{ SCF: newSCF(prog),