Documentation ¶
Overview ¶
Package store provides a modeling kit for decision automation problems. It is based on the paradigm of "decisions as code". The base interface is the Store: a space defined by Variables and logic. Hop is an engine provided to search that space and find the best solution possible, this is, the best collection of Variable assignments. The Store is the root node of a search tree. Child Stores (nodes) inherit both logic and Variables from the parent and may also add new Variables and logic, or overwrite existing ones. Changes to a child do not impact its parent.
A new Store is defined.
s := store.New()
Variables are stored in the Store.
x := store.NewVar(s, 1) y := store.NewSlice(s, 2, 3, 4) z := store.NewMap[string, int](s)
The Format of the Store can be set and one can get the value of a Variable.
s = s.Format( func(s store.Store) any { return map[string]any{ "x": x.Get(s), "y": y.Slice(s), "z": z.Map(s), } }, )
The Value of the Store can be set. When maximizing or minimizing, Variable assignments are chosen so that this value increases or decreases, respectively.
s = s.Value( func(s store.Store) int { sum := 0 for i := 0; i < y.Len(s); i++ { sum += y.Get(s, i) } return x.Get(s) + sum }, )
Changes, like setting a new value on a Variable, can be applied to the Store.
s = s.Apply( x.Set(10), y.Append(5, 6), )
To broaden the search space, new Stores can be generated.
s = s.Generate(func(s store.Store) store.Generator { value := x.Get(s) return store.Lazy( func() bool { return value <= 10 }, func() store.Store { value++ return s.Apply(x.Set(value)) }, ) })
To check the operational validity of the Store (all decisions have been made and they are valid), use the provided function.
s = s.Validate(func(s store.Store) bool { return x.Get(s)%2 == 0 })
When setting a Value, it can be maximized or minimized. Alternatively, operational validity on the Store can be satisfied, in which case setting a Value is not needed. Options are required to specify the search mechanics.
// DefaultOptions provide sensible defaults. opt := store.DefaultOptions() // Options can be modified, e.g.: changing the duration. // opt.Limits.Duration = time.Duration(4) * time.Second solver := s.Value(...).Minimizer(opt) // solver := s.Value(...).Minimizer(opt) // solver := s.Satisfier(opt)
To find the best collection of Variable assignments in the Store, the last Solution can be obtained from the given Solver. Alternatively, all Solutions can be retrieved to debug the search mechanics of the Solver.
solver := s.Maximizer(opt) last := solver.Last(context.Background()) // all := solver.All(context.Background()) best := x.Get(last.Store) stats := last.Statistics
Runners are provided for convenience when running the Store. They read data and options and manage the call to the Solver. The `NEXTMV_RUNNER` environment variable defines the type of runner used.
- "cli": (Default) Command Line Interface runner. Useful for running from a terminal. Can read from a file or stdin and write to a file or stdout.
- "http": HTTP runner. Useful for sending requests and receiving responses on the specified port.
The runner receives a handler that specifies the data type and expects a Solver.
package main import ( "github.com/nextmv-io/sdk/run" "github.com/nextmv-io/sdk/store" ) func main() { handler := func(v int, opt store.Options) (store.Solver, error) { s := store.New() x := store.NewVar(s, v) // Initialized from the runner. s = s.Value(...).Format(...) // Modify the Store. return s.Maximizer(opt), nil // Options are passed by the runner. } run.Run(handler) }
Compile the binary and use the -h flag to see available options to configure a runner. You can use command-line flags or environment variables. When using environment variables, use all caps and snake case. For example, using the command-line flag `-hop.solver.limits.duration` is equivalent to setting the environment variable `HOP_SOLVER_LIMITS_DURATION`.
Using the cli runner for example:
echo 0 | go run main.go -hop.solver.limits.duration 2s
Writes this output to stdout:
{ "hop": { "version": "..." }, "options": { "diagram": { "expansion": { "limit": 0 }, "width": 10 }, "limits": { "duration": "2s" }, "search": { "buffer": 100 }, "sense": "maximizer" }, "store": { "x": 10 }, "statistics": { "bounds": { "lower": 10, "upper": 9223372036854776000 }, "search": { "generated": 10, "filtered": 0, "expanded": 10, "reduced": 0, "restricted": 10, "deferred": 0, "explored": 1, "solutions": 5 }, "time": { "elapsed": "93.417µs", "elapsed_seconds": 9.3417e-05, "start": "..." }, "value": 10 } }
Example (KnightsTour) ¶
A knight's tour is a sequence of moves of a knight on an nxn chessboard such that the knight visits every square exactly once. This example implements an open knight's tour, given that the last position will not necessarily be one move away from the first.
package main import ( "context" "encoding/json" "fmt" "reflect" "sort" "strconv" "strings" "github.com/nextmv-io/sdk/store" ) // position tracks the knight on the board. type position struct { row int col int } // offsets for obtaining the eight different positions a knight can reach from // any given square (inside the board or not). var offsets = []struct { row int col int }{ {2, 1}, {1, 2}, {-1, 2}, {-2, 1}, {-2, -1}, {-1, -2}, {1, -2}, {2, -1}, } // onward implements sort.Interface for []Position based on the moves field. type onward struct { moves []int candidates []position } func (o onward) Len() int { return len(o.moves) } func (o onward) Swap(i, j int) { o.moves[i], o.moves[j] = o.moves[j], o.moves[i] o.candidates[i], o.candidates[j] = o.candidates[j], o.candidates[i] } func (o onward) Less(i, j int) bool { return o.moves[i] < o.moves[j] } // positions returns the positions that a knight can move to from the given row // and column, asserting that they are inside the board and have not been // visited yet. func positions(n, row, col int, tour []position) []position { // Create all the possible movement options. options := make([]position, len(offsets)) for i := range options { options[i] = position{ row: row + offsets[i].row, col: col + offsets[i].col, } } // Create positions that are inside the board and have not been visited // yet. var positions []position for _, opt := range options { // Assert the option is inside the board. if opt.row >= 0 && opt.row < n && opt.col >= 0 && opt.col < n { if !visited(opt, tour) { positions = append(positions, opt) } } } return positions } // visited asserts that the option has not been visited in the tour. func visited(opt position, tour []position) bool { visited := false for _, move := range tour { if reflect.DeepEqual(opt, move) { visited = true break } } return visited } /* format defines the JSON formatting of the store as a board, e.g. { "0": "00 -- 02 -- -- ", "1": "-- -- -- -- 03 ", "2": "06 01 -- -- -- ", "3": "-- -- 07 04 -- ", "4": "-- 05 -- -- -- " } */ func format(tour store.Slice[position], n int) func(s store.Store) any { return func(s store.Store) any { // Empty board. board := map[string]string{} for i := 0; i < n; i++ { board[strconv.Itoa(i)] = strings.Repeat("-- ", n) } // Loop over the knight's tour to fill the board with positions. for i, p := range tour.Slice(s) { // Make every number a double digit. num := strconv.Itoa(i) if i < 10 { num = "0" + num } // Set the visited position on the board. cols := strings.Split(board[strconv.Itoa(p.row)], " ") cols[p.col] = num board[strconv.Itoa(p.row)] = strings.Join(cols, " ") } return board } } // A knight's tour is a sequence of moves of a knight on an nxn chessboard such // that the knight visits every square exactly once. This example implements an // open knight's tour, given that the last position will not necessarily be one // move away from the first. func main() { // Board size and initial position. n := 5 p := position{row: 0, col: 0} // Create the knight's tour model. knight := store.New() // Track the sequence of moves. tour := store.NewSlice(knight, p) // Define the output format. knight = knight.Format(format(tour, n)) // The store is operationally valid if the tour is complete. knight = knight.Validate(func(s store.Store) bool { return tour.Len(s) == n*n }) // Define the generation of the tour. knight = knight.Generate(func(s store.Store) store.Generator { // Gets the last move made and all the candidate positions from // there. lastMove := tour.Get(s, tour.Len(s)-1) candidates := positions(n, lastMove.row, lastMove.col, tour.Slice(s)) // Obtain the number of onward moves per candidate, excluding // visited squares. Sort candidates increasingly by the number // of onward moves. moves := make([]int, len(candidates)) for i, candidate := range candidates { moves[i] = len(positions(n, candidate.row, candidate.col, tour.Slice(s))) } onward := onward{moves: moves, candidates: candidates} sort.Sort(onward) // Starting from the most constrained candidate, create a store // queue by adding each candidate to the tour. stores := make([]store.Store, len(onward.candidates)) for i, candidate := range onward.candidates { stores[i] = s.Apply(tour.Append(candidate)) } return store.Eager(stores...) }) // The solver type is a satisfier because only operationally valid tours // are needed, there is no value associated. opt := store.DefaultOptions() opt.Limits.Solutions = 1 solver := knight.Satisfier(opt) // Get the last solution of the problem and print it. last := solver.Last(context.Background()) b, err := json.MarshalIndent(last.Store, "", " ") if err != nil { panic(err) } fmt.Println(string(b)) }
Output: { "0": "00 17 06 11 20 ", "1": "07 12 19 16 05 ", "2": "18 01 04 21 10 ", "3": "13 08 23 02 15 ", "4": "24 03 14 09 22 " }
Example (LongestUncrossedKnightsPath) ¶
The longest uncrossed (or nonintersecting) knight's path is a mathematical problem involving a knight on a square n×n chess board. The problem is to find the longest path the knight can take on the given board, such that the path does not intersect itself. Definitions reused from the knight's tour example: position, offsets, format, visited.
package main import ( "context" "encoding/json" "fmt" "github.com/nextmv-io/sdk/store" ) // intersects returns true if the segment p0->p1 intersects with the segment // p2->p3. It is based on this link: // https://stackoverflow.com/a/14795484/15559724 func intersects(p0, p1, p2, p3 position) bool { s10x := p1.row - p0.row s10y := p1.col - p0.col s32x := p3.row - p2.row s32y := p3.col - p2.col denom := s10x*s32y - s32x*s10y if denom == 0 { return false } denomPositive := denom > 0 s02x := p0.row - p2.row s02y := p0.col - p2.col sNumer := s10x*s02y - s10y*s02x if (sNumer < 0) == denomPositive { return false } tNumer := s32x*s02y - s32y*s02x if (tNumer < 0) == denomPositive { return false } if ((sNumer > denom) == denomPositive) || ((tNumer > denom) == denomPositive) { return false } return true } // intersection asserts that the option does not intersect the tour. func intersection(opt position, tour []position, visited bool) bool { intersection := false if len(tour) >= 3 && !visited { for i := 0; i < len(tour)-1; i++ { if intersects(tour[i], tour[i+1], tour[len(tour)-1], opt) { intersection = true break } } } return intersection } // unintersected returns the positions that a knight can move to from the given // row and column, asserting that they are inside the board, have not been // visited yet and do not intersect the knight's path. func unintersected(n, row, col int, tour []position) []position { // Create all the possible movement options. options := make([]position, len(offsets)) for i := range options { options[i] = position{ row: row + offsets[i].row, col: col + offsets[i].col, } } // Create positions that are feasible candidates. var positions []position for _, opt := range options { // Assert the option is inside the board, has not been visited and does // not intersect the path. if opt.row >= 0 && opt.row < n && opt.col >= 0 && opt.col < n { visited := visited(opt, tour) if !visited && !intersection(opt, tour, visited) { positions = append(positions, opt) } } } return positions } // The longest uncrossed (or nonintersecting) knight's path is a mathematical // problem involving a knight on a square n×n chess board. The problem is to // find the longest path the knight can take on the given board, such that the // path does not intersect itself. Definitions reused from the knight's tour // example: position, offsets, format, visited. func main() { // Board size and initial position. n := 5 p := position{row: 0, col: 0} // Create the knight's tour model. knight := store.New() // Track the sequence of moves. tour := store.NewSlice(knight, p) // Define the output format. knight = knight.Format(format(tour, n)) // Define the value to maximize: the number of jumps made. knight = knight.Value(func(s store.Store) int { return tour.Len(s) - 1 }) // Define the generation of the tour. The store is always operationally // valid. knight = knight.Generate(func(s store.Store) store.Generator { // Gets the last move made and all the candidate positions from // there. lastMove := tour.Get(s, tour.Len(s)-1) candidates := unintersected( n, lastMove.row, lastMove.col, tour.Slice(s), ) // Create new stores by adding each candidate to the tour. stores := make([]store.Store, len(candidates)) for i, candidate := range candidates { stores[i] = s.Apply(tour.Append(candidate)) } return store.Eager(stores...) }) // The solver type is a maximizer because the store is searching for the // highest number of moves. solver := knight.Maximizer(store.DefaultOptions()) // Get the last solution of the problem and print it. last := solver.Last(context.Background()) b, err := json.MarshalIndent(last.Store, "", " ") if err != nil { panic(err) } fmt.Println(string(b)) }
Output: { "0": "00 -- 02 -- -- ", "1": "-- -- -- -- 03 ", "2": "06 01 -- -- -- ", "3": "-- -- 07 04 -- ", "4": "-- 05 -- -- -- " }
Index ¶
- func False(s Store) bool
- func True(s Store) bool
- type Bounder
- type Bounds
- type Change
- type Condition
- type Diagram
- type Domain
- type Domains
- type Formatter
- type Generator
- type Key
- type Limits
- type Map
- type Options
- type Propagator
- type Search
- type Sense
- type Slice
- type Solution
- type Solver
- type Statistics
- type Store
- type Time
- type Valuer
- type Variable
Examples ¶
- Package (KnightsTour)
- Package (LongestUncrossedKnightsPath)
- And
- Change
- Condition
- DefaultOptions
- Domain (Add)
- Domain (AtLeast)
- Domain (AtMost)
- Domain (Cmp)
- Domain (Contains)
- Domain (Domain)
- Domain (Empty)
- Domain (Len)
- Domain (Max)
- Domain (Min)
- Domain (Remove)
- Domain (Slice)
- Domain (Value)
- Domains (Add)
- Domains (Assign)
- Domains (AtLeast)
- Domains (AtMost)
- Domains (Cmp)
- Domains (Domain)
- Domains (Domains)
- Domains (Empty)
- Domains (First)
- Domains (Largest)
- Domains (Last)
- Domains (Len)
- Domains (Maximum)
- Domains (Minimum)
- Domains (Remove)
- Domains (Singleton)
- Domains (Slices)
- Domains (Smallest)
- Domains (Values)
- Eager
- False
- Lazy
- Map (Delete)
- Map (Get)
- Map (Len)
- Map (Map)
- Map (Set)
- Multiple
- NewDomain
- NewDomains
- NewMap
- NewSlice
- NewVar
- Not
- Or
- Repeat
- Singleton
- Slice (Append)
- Slice (Get)
- Slice (Insert)
- Slice (Len)
- Slice (Prepend)
- Slice (Remove)
- Slice (Set)
- Slice (Slice)
- Solver (All)
- Solver (Last)
- Store (Apply)
- Store (Bound)
- Store (Format)
- Store (Generate)
- Store (Maximizer)
- Store (Minimizer)
- Store (Satisfier)
- Store (Validate)
- Store (Value)
- True
- Variable (Get)
- Variable (Set)
- Xor
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type Bounder ¶
Bounder maps a Store to monotonically tightening bounds. It is meant to be used with the store.Bound function.
type Bounds ¶
Bounds on an objective value at some node in the search tree consist of a lower value and an upper value. If the lower and upper value are the same, the bounds have converged.
type Change ¶
type Change func(Store)
Change a Store.
Example ¶
Changes can be applied to a store.
package main import ( "fmt" "github.com/nextmv-io/sdk/store" ) func main() { // Original value. s := store.New() x := store.NewVar(s, 15) fmt.Println(x.Get(s)) // Value after store changed. s = s.Apply(x.Set(42)) fmt.Println(x.Get(s)) }
Output: 15 42
type Condition ¶
Condition represents a logical condition on a Store.
Example ¶
Custom conditions can be defined.
package main import ( "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() a := func(store.Store) bool { return 1 < 2 } b := func(store.Store) bool { return 1 > 2 } c := store.And(a, b)(s) fmt.Println(c) }
Output: false
func And ¶
And uses the conditional "AND" logical operator on all given conditions. It returns true if all conditions are true.
Example ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() c := store.And(store.True, store.False)(s) fmt.Println(c) c = store.And(store.True, store.True)(s) fmt.Println(c) c = store.And(store.False, store.True)(s) fmt.Println(c) c = store.And(store.False, store.False)(s) fmt.Println(c) c = store.And(store.False, store.True, store.False)(s) fmt.Println(c) c = store.And(store.True, store.True, store.True)(s) fmt.Println(c) }
Output: false true false false false true
func Not ¶
Not negates the given condition.
Example ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() f := store.Not(store.True)(s) fmt.Println(f) t := store.Not(store.False)(s) fmt.Println(t) }
Output: false true
func Or ¶
Or uses the conditional "OR" logical operator on all given conditions. It returns true if at least one condition is true.
Example ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() c := store.Or(store.True, store.False)(s) fmt.Println(c) c = store.Or(store.True, store.True)(s) fmt.Println(c) c = store.Or(store.False, store.True)(s) fmt.Println(c) c = store.Or(store.False, store.False)(s) fmt.Println(c) c = store.Or(store.False, store.True, store.False)(s) fmt.Println(c) c = store.Or(store.True, store.True, store.True)(s) fmt.Println(c) }
Output: true true true false true true
func Xor ¶
Xor uses the conditional "Exclusive OR" logical operator on all given conditions. It returns true if, and only if, the conditions are different.
Example ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() c := store.Xor(store.True, store.False)(s) fmt.Println(c) c = store.Xor(store.True, store.True)(s) fmt.Println(c) c = store.Xor(store.False, store.True)(s) fmt.Println(c) c = store.Xor(store.False, store.False)(s) fmt.Println(c) }
Output: true false true false
type Diagram ¶
type Diagram struct { // Maximum Width of the Decision Diagram. Width int // Maximum Expansion that can be generated from a Store. Expansion struct { // Limit represents the maximum number of children Stores that can // be generated from a parent. Limit int `json:"limit"` } }
Diagram options. The Store search is based on Decision Diagrams. These options configure the mechanics of using DD.
type Domain ¶
type Domain interface { /* Add values to a Domain. s1 := store.New() d := store.Multiple(s1, 1, 3, 5) s2 := s1.Apply(d.Add(2, 4)) d.Domain(s1) // {1, 3, 5}} d.Domain(s2) // [1, 5]] */ Add(...int) Change /* AtLeast updates the Domain to the sub-Domain of at least some value. s1 := store.New() d := store.NewDomain(s1, model.NewRange(1, 10), model.NewRange(101, 110)) s2 := s1.Apply(d.AtLeast(50)) d.Domain(s1) // {[1, 10], [101, 110]} d.Domain(s2) // [101, 110] */ AtLeast(int) Change /* AtMost updates the Domain to the sub-Domain of at most some value. s1 := store.New() d := store.NewDomain(s1, model.NewRange(1, 10), model.NewRange(101, 110)) s2 := s1.Apply(d.AtMost(50)) d.Domain(s1) // {[1, 10], [101, 110]} d.Domain(s2) // [1, 10] */ AtMost(int) Change /* Cmp lexically compares two integer Domains. It returns a negative value if the receiver is less, 0 if they are equal, and a positive value if the receiver Domain is greater. s := store.New() d1 := store.NewDomain(s, model.NewRange(1, 5), model.NewRange(8, 10)) d2 := store.Multiple(s, -1, 1) d1.Cmp(s, d2) // > 0 */ Cmp(Store, Domain) int /* Contains returns true if a Domain contains a given value. s := store.New() d := store.NewDomain(s, model.NewRange(1, 10)) d.Contains(s, 5) // true d.Contains(s, 15) // false */ Contains(Store, int) bool /* Domain returns a Domain unattached to a Store. s := store.New() d := store.NewDomain(s, model.NewRange(1, 10)) d.Domain(s) // model.NewDomain(model.NewRange(1, 10)) */ Domain(Store) model.Domain /* Empty is true if a Domain is empty for a Store. s := store.New() d1 := store.NewDomain(s) d2 := store.Singleton(s, 42) d1.Empty(s) // true d2.Empty(s) // false */ Empty(Store) bool /* Len of a Domain, counting all values within ranges. s := store.New() d := store.NewDomain(s, model.NewRange(1, 10), model.NewRange(-5, -1)) d.Len(s) // 15 */ Len(Store) int /* Max of a Domain and a boolean indicating it is non-empty. s := store.New() d1 := store.NewDomain(s) d2 := store.NewDomain(s, model.NewRange(1, 10), model.NewRange(-5, -1)) d1.Max(s) // returns (_, false) d2.Max(s) // returns (10, true) */ Max(Store) (int, bool) /* Min of a Domain and a boolean indicating it is non-empty. s := store.New() d1 := store.NewDomain(s) d2 := store.NewDomain(s, model.NewRange(1, 10), model.NewRange(-5, -1)) d1.Min(s) // returns (_, false) d2.Min(s) // returns (-5, true) */ Min(Store) (int, bool) /* Remove values from a Domain. s1 := store.New() d := store.NewDomain(s1, model.NewRange(1, 5)) s2 := s1.Apply(d.Remove(2, 4)) d.Domain(s1) // [1, 5] d.Domain(s2) // {1, 3, 5} */ Remove(...int) Change /* Slice representation of a Domain. s := store.New() d := store.NewDomain(s, model.NewRange(1, 5)) d.Slice(s) // [1, 2, 3, 4, 5] */ Slice(Store) []int /* Value returns an int and true if a Domain is Singleton. s := store.New() d1 := store.NewDomain(s) d2 := store.Singleton(s, 42) d3 := store.Multiple(s, 1, 3, 5) d1.Value(s) // returns (0, false) d2.Value(s) // returns (42, true) d3.Value(s) // returns (0, false) */ Value(Store) (int, bool) }
A Domain of integers.
Example (Add) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s1 := store.New() d := store.Multiple(s1, 1, 3, 5) s2 := s1.Apply(d.Add(2, 4)) fmt.Println(d.Domain(s1)) fmt.Println(d.Domain(s2)) }
Output: {[{1 1} {3 3} {5 5}]} {[{1 5}]}
Example (AtLeast) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/model" "github.com/nextmv-io/sdk/store" ) func main() { s1 := store.New() d := store.NewDomain(s1, model.NewRange(1, 10), model.NewRange(101, 110)) s2 := s1.Apply(d.AtLeast(50)) fmt.Println(d.Domain(s1)) fmt.Println(d.Domain(s2)) }
Output: {[{1 10} {101 110}]} {[{101 110}]}
Example (AtMost) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/model" "github.com/nextmv-io/sdk/store" ) func main() { s1 := store.New() d := store.NewDomain(s1, model.NewRange(1, 10), model.NewRange(101, 110)) s2 := s1.Apply(d.AtMost(50)) fmt.Println(d.Domain(s1)) fmt.Println(d.Domain(s2)) }
Output: {[{1 10} {101 110}]} {[{1 10}]}
Example (Cmp) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/model" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() d1 := store.NewDomain(s, model.NewRange(1, 5), model.NewRange(8, 10)) d2 := store.Multiple(s, -1, 1) fmt.Println(d1.Cmp(s, d2)) }
Output: 1
Example (Contains) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/model" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() d := store.NewDomain(s, model.NewRange(1, 10)) fmt.Println(d.Contains(s, 5)) fmt.Println(d.Contains(s, 15)) }
Output: true false
Example (Domain) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/model" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() d := store.NewDomain(s, model.NewRange(1, 10)) fmt.Println(d.Domain(s)) }
Output: {[{1 10}]}
Example (Empty) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() d1 := store.NewDomain(s) d2 := store.Singleton(s, 42) fmt.Println(d1.Empty(s)) fmt.Println(d2.Empty(s)) }
Output: true false
Example (Len) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/model" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() d := store.NewDomain(s, model.NewRange(1, 10), model.NewRange(-5, -1)) fmt.Println(d.Len(s)) }
Output: 15
Example (Max) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/model" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() d1 := store.NewDomain(s) d2 := store.NewDomain(s, model.NewRange(1, 10), model.NewRange(-5, -1)) fmt.Println(d1.Max(s)) fmt.Println(d2.Max(s)) }
Output: 9223372036854775807 false 10 true
Example (Min) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/model" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() d1 := store.NewDomain(s) d2 := store.NewDomain(s, model.NewRange(1, 10), model.NewRange(-5, -1)) fmt.Println(d1.Min(s)) fmt.Println(d2.Min(s)) }
Output: -9223372036854775808 false -5 true
Example (Remove) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/model" "github.com/nextmv-io/sdk/store" ) func main() { s1 := store.New() d := store.NewDomain(s1, model.NewRange(1, 5)) s2 := s1.Apply(d.Remove(2, 4)) fmt.Println(d.Domain(s1)) fmt.Println(d.Domain(s2)) }
Output: {[{1 5}]} {[{1 1} {3 3} {5 5}]}
Example (Slice) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/model" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() d := store.NewDomain(s, model.NewRange(1, 5)) fmt.Println(d.Slice(s)) }
Output: [1 2 3 4 5]
Example (Value) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() d1 := store.NewDomain(s) d2 := store.Singleton(s, 42) d3 := store.Multiple(s, 1, 3, 5) fmt.Println(d1.Value(s)) fmt.Println(d2.Value(s)) fmt.Println(d3.Value(s)) }
Output: 0 false 42 true 0 false
func Multiple ¶
Multiple creates a Domain containing multiple integer values and stores it in a Store.
s := store.New() even := store.Multiple(s, 2, 4, 6, 8)
Example ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() even := store.Multiple(s, 2, 4, 6, 8) fmt.Println(even.Domain(s)) }
Output: {[{2 2} {4 4} {6 6} {8 8}]}
func NewDomain ¶
NewDomain creates a Domain of integers and stores it in a Store.
s := store.New() d1 := store.NewDomain(s, model.NewRange(1, 10)) // 1 through 10 d2 := store.NewDomain( // 1 through 10 and 20 through 29 s, model.NewRange(1, 10), model.NewRange(20, 29), )
Example ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/model" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() d1 := store.NewDomain(s, model.NewRange(1, 10)) d2 := store.NewDomain(s, model.NewRange(1, 10), model.NewRange(20, 29)) fmt.Println(d1.Domain(s)) fmt.Println(d2.Domain(s)) }
Output: {[{1 10}]} {[{1 10} {20 29}]}
func Singleton ¶
Singleton creates a Domain containing one integer value and stores it in a Store.
s := store.New() fortyTwo := store.Singleton(s, 42)
Example ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() fortyTwo := store.Singleton(s, 42) fmt.Println(fortyTwo.Domain(s)) }
Output: {[{42 42}]}
type Domains ¶
type Domains interface { /* Add values to a Domain by index. s1 := store.New() d := store.Repeat(s1, 3, model.Singleton(42)) // [42, 42, 42] s2 := s1.Apply(d.Add(1, 41, 43)) d.Domains(s2) // [42, [41,43], 42] */ Add(int, ...int) Change /* Assign a Singleton value to a Domain by index. s1 := store.New() d := store.Repeat(s1, 3, model.Singleton(42)) // [42, 42, 42] s2 := s1.Apply(d.Assign(0, 10)) d.Domains(s2) // [10, 42, 42] */ Assign(int, int) Change /* AtLeast updates the Domain to the sub-Domain of at least some value. s1 := store.New() d := store.Repeat( // [[1, 100], [1, 100]] s1, 2, model.NewDomain(model.NewRange(1, 100)), ) s2 := s1.Apply(d.AtLeast(1, 50)) d.Domains(s2) // [[1, 100], [50, 100]] */ AtLeast(int, int) Change /* AtMost updates the Domain to the sub-Domain of at most some value. s1 := store.New() d := store.Repeat( // [[1, 100], [1, 100]] s1, 2, model.NewDomain(model.NewRange(1, 100)), ) s2 := s1.Apply(d.AtMost(1, 50)) d.Domains(s2) // [[1, 100], [1, 50]] */ AtMost(int, int) Change /* Cmp lexically compares two sequences of integer Domains. It returns a negative value if the receiver is less, 0 if they are equal, and a positive value if the receiver Domain is greater. s := store.New() d1 := store.Repeat(s, 2, model.Singleton(42)) // [42, 42, 42] d2 := store.Repeat(s, 3, model.Singleton(43)) // [43, 43]] d1.Cmp(s, d2) // < 0 */ Cmp(Store, Domains) int /* Domain by index. s := store.New() d := store.NewDomains(s, model.NewDomain(), model.Singleton(42)) d.Domain(s, 0) // {} d.Domain(s, 1) // 42 */ Domain(Store, int) model.Domain /* Domains in the sequence. s := store.New() d := store.NewDomains(s, model.NewDomain(), model.Singleton(42)) d.Domains(s) // [{}, 42} */ Domains(Store) model.Domains /* Empty is true if all Domains are empty. s := store.New() d := store.NewDomains(s, model.NewDomain()) d.Empty(s) // true */ Empty(Store) bool /* Len returns the number of Domains. s := store.New() d := store.Repeat(s, 5, model.NewDomain()) d.Len(s) // 5 */ Len(Store) int /* Remove values from a Domain by index. s1 := store.New() d := store.NewDomains(s1, model.Multiple(42, 13)) // {13, 42} s2 := s1.Apply(d.Remove(0, 13)) d.Domains(s2) // {42} */ Remove(int, ...int) Change /* Singleton is true if all Domains are Singleton. s := store.New() d := store.Repeat(s, 5, model.Singleton(42)) d.Singleton(s) // true */ Singleton(Store) bool /* Slices converts Domains to a slice of int slices. s := store.New() d := store.NewDomains(s, model.NewDomain(), model.Multiple(1, 3)) d.Slices(s) // [[], [1, 2, 3]] */ Slices(Store) [][]int /* Values returns the values of a sequence of Singleton Domains. s1 := store.New() d := store.Repeat(s1, 3, model.Singleton(42)) s2 := s1.Apply(d.Add(0, 41)) d.Values(s1) // ([42, 42, 42], true) d.Values(s2) // ([], false) */ Values(Store) ([]int, bool) /* First returns the first Domain index with length above 1 and true if it is found. If no Domain has a length above 1, the function returns 0 and false. s := store.New() d := store.NewDomains( s, model.Singleton(88), // Length 1 model.Multiple(1, 3), // Length above 1 model.Multiple(4, 76), // Length above 1 ) d.First(s) // (1, true) */ First(Store) (int, bool) /* Largest returns the index of the largest Domain with length above 1 by number of elements and true if it is found. If no Domain has a length above 1, the function returns 0 and false. s := store.New() d := store.NewDomains( s, model.Singleton(88), // Length 1 model.Multiple(1, 3), // Length 2 model.Multiple(4, 76, 97), // Length 3 ) d.Largest(s) // (2, true) */ Largest(Store) (int, bool) /* Last returns the last Domain index with length above 1 and true if it is found. If no Domain has a length above 1, the function returns 0 and false. s := store.New() d := store.NewDomains( s, model.Singleton(88), // Length 1 model.Multiple(1, 3), // Length above 1 model.Multiple(4, 76, 97), // Length above 1 model.Singleton(45), // Length 1 ) d.Last(s) // (2, true) */ Last(Store) (int, bool) /* Maximum returns the index of the Domain containing the maximum value with length above 1 and true if it is found. If no Domain has a length above 1, the function returns 0 and false. s := store.New() d := store.NewDomains( s, model.Singleton(88), // Length 1 model.Multiple(4, 76, 97), // Length above 1 model.Multiple(1, 3), // Length above 1 model.Singleton(45), // Length 1 ) d.Maximum(s) // (1, true) */ Maximum(Store) (int, bool) /* Minimum returns the index of the Domain containing the minimum value with length above 1 and true if it is found. If no Domain has a length above 1, the function returns 0 and false. s := store.New() d := store.NewDomains( s, model.Singleton(88), // Length 1 model.Multiple(4, 76, 97), // Length above 1 model.Multiple(1, 3), // Length above 1 model.Singleton(45), // Length 1 ) d.Minimum(s) // (2, true) */ Minimum(Store) (int, bool) /* Smallest returns the index of the smallest Domain with length above 1 by number of elements and true if it is found. If no Domain has a length above 1, the function returns 0 and false. s := store.New() d := store.NewDomains( s, model.Singleton(88), // Length 1 model.Multiple(1, 3), // Length 2 model.Multiple(4, 76, 97), // Length 3 ) d.Smallest(s) // (1, true) */ Smallest(Store) (int, bool) }
Domains of integers.
Example (Add) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/model" "github.com/nextmv-io/sdk/store" ) func main() { s1 := store.New() d := store.Repeat(s1, 3, model.Singleton(42)) s2 := s1.Apply(d.Add(1, 41, 43)) fmt.Println(d.Domains(s2)) }
Output: [{[{42 42}]} {[{41 43}]} {[{42 42}]}]
Example (Assign) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/model" "github.com/nextmv-io/sdk/store" ) func main() { s1 := store.New() d := store.Repeat(s1, 3, model.Singleton(42)) s2 := s1.Apply(d.Assign(0, 10)) fmt.Println(d.Domains(s2)) }
Output: [{[{10 10}]} {[{42 42}]} {[{42 42}]}]
Example (AtLeast) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/model" "github.com/nextmv-io/sdk/store" ) func main() { s1 := store.New() d := store.Repeat(s1, 2, model.NewDomain(model.NewRange(1, 100))) s2 := s1.Apply(d.AtLeast(1, 50)) fmt.Println(d.Domains(s2)) }
Output: [{[{1 100}]} {[{50 100}]}]
Example (AtMost) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/model" "github.com/nextmv-io/sdk/store" ) func main() { s1 := store.New() d := store.Repeat(s1, 2, model.NewDomain(model.NewRange(1, 100))) s2 := s1.Apply(d.AtMost(1, 50)) fmt.Println(d.Domains(s2)) }
Output: [{[{1 100}]} {[{1 50}]}]
Example (Cmp) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/model" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() d1 := store.Repeat(s, 2, model.Singleton(42)) d2 := store.Repeat(s, 3, model.Singleton(43)) fmt.Println(d1.Cmp(s, d2)) }
Output: -1
Example (Domain) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/model" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() d := store.NewDomains(s, model.NewDomain(), model.Singleton(42)) fmt.Println(d.Domain(s, 0)) fmt.Println(d.Domain(s, 1)) }
Output: {[]} {[{42 42}]}
Example (Domains) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/model" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() d := store.NewDomains(s, model.NewDomain(), model.Singleton(42)) fmt.Println(d.Domains(s)) }
Output: [{[]} {[{42 42}]}]
Example (Empty) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/model" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() d := store.NewDomains(s, model.NewDomain()) fmt.Println(d.Empty(s)) }
Output: true
Example (First) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/model" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() d := store.NewDomains( s, model.Singleton(88), model.Multiple(1, 3), model.Multiple(4, 76), ) fmt.Println(d.First(s)) }
Output: 1 true
Example (Largest) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/model" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() d := store.NewDomains( s, model.Singleton(88), model.Multiple(1, 3), model.Multiple(4, 76, 97), ) fmt.Println(d.Largest(s)) }
Output: 2 true
Example (Last) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/model" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() d := store.NewDomains( s, model.Singleton(88), model.Multiple(1, 3), model.Multiple(4, 76, 97), model.Singleton(45), ) fmt.Println(d.Last(s)) }
Output: 2 true
Example (Len) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/model" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() d := store.Repeat(s, 5, model.NewDomain()) fmt.Println(d.Len(s)) }
Output: 5
Example (Maximum) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/model" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() d := store.NewDomains( s, model.Singleton(88), model.Multiple(4, 76, 97), model.Multiple(1, 3), model.Singleton(45), ) fmt.Println(d.Maximum(s)) }
Output: 1 true
Example (Minimum) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/model" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() d := store.NewDomains( s, model.Singleton(88), model.Multiple(4, 76, 97), model.Multiple(1, 3), model.Singleton(45), ) fmt.Println(d.Minimum(s)) }
Output: 2 true
Example (Remove) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/model" "github.com/nextmv-io/sdk/store" ) func main() { s1 := store.New() d := store.NewDomains(s1, model.Multiple(42, 13)) s2 := s1.Apply(d.Remove(0, 13)) fmt.Println(d.Domains(s2)) }
Output: [{[{42 42}]}]
Example (Singleton) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/model" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() d := store.Repeat(s, 5, model.Singleton(42)) fmt.Println(d.Singleton(s)) }
Output: true
Example (Slices) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/model" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() d := store.NewDomains(s, model.NewDomain(), model.Multiple(1, 3)) fmt.Println(d.Slices(s)) }
Output: [[] [1 3]]
Example (Smallest) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/model" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() d := store.NewDomains( s, model.Singleton(88), model.Multiple(1, 3), model.Multiple(4, 76, 97), ) fmt.Println(d.Smallest(s)) }
Output: 1 true
Example (Values) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/model" "github.com/nextmv-io/sdk/store" ) func main() { s1 := store.New() d := store.Repeat(s1, 3, model.Singleton(42)) s2 := s1.Apply(d.Add(0, 41)) fmt.Println(d.Values(s1)) fmt.Println(d.Values(s2)) }
Output: [42 42 42] true [] false
func NewDomains ¶
NewDomains creates a sequence of Domains and stores the sequence in a Store.
s := store.New() d := store.NewDomains( // [1 to 10, 42, odds] s, model.NewDomain(model.NewRange(1, 10)), model.Singleton(42), model.Multiple(1, 3, 5, 7), )
Example ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/model" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() d := store.NewDomains( // [1 to 10, 42, odds] s, model.NewDomain(model.NewRange(1, 10)), model.Singleton(42), model.Multiple(1, 3, 5, 7), ) fmt.Println(d.Domains(s)) }
Output: [{[{1 10}]} {[{42 42}]} {[{1 1} {3 3} {5 5} {7 7}]}]
func Repeat ¶
Repeat a Domain n times and store the sequence in a Store.
s := store.New() d := store.Repeat(s, 3, model.NewDomain(model.NewRange(1, 10)))
Example ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/model" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() d := store.Repeat(s, 3, model.NewDomain(model.NewRange(1, 10))) fmt.Println(d.Domains(s)) }
Output: [{[{1 10}]} {[{1 10}]} {[{1 10}]}]
type Formatter ¶
Formatter maps a Store to any type with a JSON representation. It is meant to be used with the store.Format function.
type Generator ¶
type Generator any
A Generator is used to generate new Stores (children) from an existing one (parent). It is meant to be used with the store.Generate function.
func Eager ¶
Eager way of generating new Stores. The Generator uses the list of Stores upfront in the order they are provided.
Example ¶
Generate stores eagerly: create all stores from an integer variable by increasing its value in 1 each time. The value should never be greater than 5. Eager implementation of the Lazy example.
package main import ( "context" "encoding/json" "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() x := store.NewVar(s, 0) solver := s. Generate(func(s store.Store) store.Generator { value := x.Get(s) var stores []store.Store for value <= 5 { value++ if value > 5 { break } stores = append(stores, s.Apply(x.Set(value))) } return store.Eager(stores...) }). Value(func(s store.Store) int { return x.Get(s) }). Format(func(s store.Store) any { return x.Get(s) }). Maximizer(store.DefaultOptions()) // Get the last solution of the problem and print it. last := solver.Last(context.Background()) b, err := json.MarshalIndent(last.Store, "", " ") if err != nil { panic(err) } fmt.Println(string(b)) }
Output: 5
func Lazy ¶
Lazy way of generating new Stores. While the condition holds, the function is called to generate new Stores. If the condition is no longer true or a nil Store is returned, the generator is not used anymore by the current parent.
Example ¶
Generate stores lazily: while an integer variable is less than or equal to 5, increase its value by 1. Lazy implementation of the Eager example.
package main import ( "context" "encoding/json" "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() x := store.NewVar(s, 0) solver := s. Generate(func(s store.Store) store.Generator { value := x.Get(s) return store.Lazy( func() bool { return value <= 5 }, func() store.Store { value++ return s.Apply(x.Set(value)) }, ) }). Value(func(s store.Store) int { return x.Get(s) }). Format(func(s store.Store) any { return x.Get(s) }). Maximizer(store.DefaultOptions()) // Get the last solution of the problem and print it. last := solver.Last(context.Background()) b, err := json.MarshalIndent(last.Store, "", " ") if err != nil { panic(err) } fmt.Println(string(b)) }
Output: 6
type Limits ¶
type Limits struct { // Time Duration. Duration time.Duration // Nodes reprent active Stores in the search. Nodes int // Solutions represent operationally valid Stores. Solutions int }
Limits when performing a search. The search will stop if any one of these limits are encountered.
type Map ¶
type Map[K Key, V any] interface { /* Delete a Key from the Map. s1 := store.New() m := store.NewMap[int, string](s1) s1 = s1.Apply( // {42: foo, 13: bar} m.Set(42, "foo"), m.Set(13, "bar"), ) s2 := s1.Apply(m.Delete(42)) // {13: bar} */ Delete(K) Change /* Get a value for a Key. If the Key is not present in the Map for the given Store, the zero value and false are returned. s1 := store.New() m := store.NewMap[int, string](s1) s2 := s1.Apply(m.Set(42, "foo")) m.Get(s2, 42) // (foo, true) m.Get(s2, 88) // (_, false) */ Get(Store, K) (V, bool) /* Len returns the number of Keys in a Map. s1 := store.New() m := store.NewMap[int, string](s1) s2 := s1.Apply( m.Set(42, "foo"), m.Set(13, "bar"), ) m.Len(s1) // 0 m.Len(s2) // 2 */ Len(Store) int /* Map representation that is mutable. s1 := store.New() m := store.NewMap[int, string](s1) s2 := s1.Apply( m.Set(42, "foo"), m.Set(13, "bar"), ) m.Map(s2) // map[int]string{42: "foo", 13: "bar"} */ Map(Store) map[K]V /* Set a Key to a Value. s1 := store.New() m := store.NewMap[int, string](s1) s2 := s1.Apply(m.Set(42, "foo")) // 42 -> foo s3 := s2.Apply(m.Set(42, "bar")) // 42 -> bar */ Set(K, V) Change }
A Map stores key-value pairs in a Store.
Example (Delete) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s1 := store.New() m := store.NewMap[int, string](s1) s1 = s1.Apply( m.Set(42, "foo"), m.Set(13, "bar"), ) s2 := s1.Apply(m.Delete(42)) fmt.Println(m.Map(s1)) fmt.Println(m.Map(s2)) }
Output: map[13:bar 42:foo] map[13:bar]
Example (Get) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s1 := store.New() m := store.NewMap[int, string](s1) s2 := s1.Apply(m.Set(42, "foo")) fmt.Println(m.Get(s2, 42)) fmt.Println(m.Get(s2, 88)) }
Output: foo true false
Example (Len) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s1 := store.New() m := store.NewMap[int, string](s1) s2 := s1.Apply( m.Set(42, "foo"), m.Set(13, "bar"), ) fmt.Println(m.Len(s1)) fmt.Println(m.Len(s2)) }
Output: 0 2
Example (Map) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s1 := store.New() m := store.NewMap[int, string](s1) s2 := s1.Apply( m.Set(42, "foo"), m.Set(13, "bar"), ) fmt.Println(m.Map(s2)) }
Output: map[13:bar 42:foo]
Example (Set) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s1 := store.New() m := store.NewMap[int, string](s1) s2 := s1.Apply(m.Set(42, "foo")) s3 := s2.Apply(m.Set(42, "bar")) fmt.Println(m.Map(s2)) fmt.Println(m.Map(s3)) }
Output: map[42:foo] map[42:bar]
func NewMap ¶
NewMap returns a new NewMap and stores it in a Store.
s := store.New() m1 := store.NewMap[int, [2]float64](s) // map of {int -> [2]float64} m2 := store.NewMap[string, int](s) // map of {string -> int}
Example ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() m1 := store.NewMap[int, [2]float64](s) m2 := store.NewMap[string, int](s) s1 := s.Apply(m1.Set(2, [2]float64{0.1, 3.1416})) s2 := s.Apply(m2.Set("a", 43)) fmt.Println(m1.Map(s1)) fmt.Println(m2.Map(s2)) }
Output: map[2:[0.1 3.1416]] map[a:43]
type Options ¶
type Options struct { Sense Sense // Tags are custom key-value pairs that the user defines for // record-keeping. Tags map[string]any Diagram Diagram // Search options. Search struct { // Buffer represents the maximum number of Stores that can be buffered // when generating more Stores. Buffer int } Limits Limits // Options for random number generation. Random struct { // Seed for generating random numbers. Seed int64 `json:"seed,omitempty"` } // Pool that is used in specific engines. Pool struct { // Maximum Size of the Pool. Size int `json:"size,omitempty"` } }
Options for a solver.
func DefaultOptions ¶
func DefaultOptions() Options
DefaultOptions for running a solver. Options can be customized after using these sensitive defaults.
opt := store.DefaultOptions() opt.Limits.Duration = time.Duration(5) * time.Second
Example ¶
DefaultOptions provide sensible defaults but they can (and should) be modified.
package main import ( "encoding/json" "fmt" "time" "github.com/nextmv-io/sdk/store" ) func main() { opt := store.DefaultOptions() b, err := json.MarshalIndent(opt, "", " ") if err != nil { panic(err) } fmt.Println(string(b)) // Modify options opt.Diagram.Expansion.Limit = 1 opt.Limits.Duration = time.Duration(4) * time.Second opt.Tags = map[string]any{"foo": 1, "bar": 2} b, err = json.MarshalIndent(opt, "", " ") if err != nil { panic(err) } fmt.Println(string(b)) }
Output: { "diagram": { "expansion": { "limit": 0 }, "width": 10 }, "limits": { "duration": "10s" }, "search": { "buffer": 100 }, "sense": "minimize" } { "diagram": { "expansion": { "limit": 1 }, "width": 10 }, "limits": { "duration": "4s" }, "search": { "buffer": 100 }, "sense": "minimize", "tags": { "bar": 2, "foo": 1 } }
type Propagator ¶
Propagator propagates Changes to a Store. It is meant to be used with the store.Propagate function.
type Search ¶
type Search struct { // Generated stores in the search. Generated int `json:"generated"` // Filtered stores in the search. Filtered int `json:"filtered"` // Expanded stores in the search. Expanded int `json:"expanded"` // Reduced stores in the search. Reduced int `json:"reduced"` // Restricted stores in the search. Restricted int `json:"restricted"` // Deferred stores in the search. Deferred int `json:"deferred"` // Explored stores in the search. Explored int `json:"explored"` // Operationally valid stores in the search. Solutions int `json:"solutions"` }
Search statistics of the Store generation.
type Sense ¶
type Sense int
Sense specifies whether one is maximizing, minimizing, or satisfying. Default is set to minimization.
const ( // Minimize indicates the solution space is being searched to find the // smallest possible value. Minimize Sense = iota // Maximize indicates the solution space is being searched to find the // biggest possible value. Maximize // Satisfy indicates the solution space is being searched to find // operationally valid Stores. Satisfy )
type Slice ¶
type Slice[T any] interface { /* Append one or more values to the end of a Slice. s1 := store.New() x := store.NewSlice(s1, 1, 2, 3) // [1, 2, 3] s2 := s1.Apply(x.Append(4, 5)) x.Slice(s2) // [1, 2, 3, 4, 5] */ Append(value T, values ...T) Change /* Get an index of a Slice. s := store.New() x := store.NewSlice(s, 1, 2, 3) x.Get(s, 2) // 3 */ Get(Store, int) T /* Insert one or more values at an index in a Slice. s1 := store.New() x := store.NewSlice(s1, "a", "b", "c") s2 := s1.Apply(x.Insert(2, "d", "e")) x.Slice(s2) // [a, b, d, e, c] */ Insert(index int, value T, values ...T) Change /* Len returns the length of a Slice. s := store.New() x := store.NewSlice(s, 1, 2, 3) x.Len(s) // 3 */ Len(Store) int /* Prepend one or more values at the beginning of a Slice. s1 := store.New() x := store.NewSlice(s1, 1, 2, 3) // [1, 2, 3] s2 := s1.Apply(x.Prepend(4, 5)) x.Slice(s2) // [4, 5, 1, 2, 3] */ Prepend(value T, values ...T) Change /* Remove a sub-Slice from a starting to an ending index. s1 := store.New() x := store.NewSlice(s1, 1, 2, 3) // [1, 2, 3] s2 := s1.Apply(x.Remove(1, 1)) x.Slice(s2) // [1, 3] */ Remove(start, end int) Change /* Set a value by index. s1 := store.New() x := store.NewSlice(s1, "a", "b", "c") // [a, b, c] s2 := s1.Apply(x.Set(1, "d")) x.Slice(s2) // [a, d, c] */ Set(int, T) Change /* Slice representation that is mutable. s := store.New() x := store.NewSlice(s, 1, 2, 3) x.Slice(s) // []int{1, 2, 3} */ Slice(Store) []T }
Slice manages an immutable slice container of some type in a Store.
Example (Append) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s1 := store.New() x := store.NewSlice(s1, 1, 2, 3) s2 := s1.Apply(x.Append(4, 5)) fmt.Println(x.Slice(s2)) }
Output: [1 2 3 4 5]
Example (Get) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() x := store.NewSlice(s, 1, 2, 3) fmt.Println(x.Get(s, 2)) }
Output: 3
Example (Insert) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s1 := store.New() x := store.NewSlice(s1, "a", "b", "c") s2 := s1.Apply(x.Insert(2, "d", "e")) fmt.Println(x.Slice(s2)) }
Output: [a b d e c]
Example (Len) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() x := store.NewSlice(s, 1, 2, 3) fmt.Println(x.Len(s)) }
Output: 3
Example (Prepend) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s1 := store.New() x := store.NewSlice(s1, 1, 2, 3) s2 := s1.Apply(x.Prepend(4, 5)) fmt.Println(x.Slice(s2)) }
Output: [4 5 1 2 3]
Example (Remove) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s1 := store.New() x := store.NewSlice(s1, 1, 2, 3) s2 := s1.Apply(x.Remove(1, 1)) fmt.Println(x.Slice(s2)) }
Output: [1 3]
Example (Set) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s1 := store.New() x := store.NewSlice(s1, "a", "b", "c") s2 := s1.Apply(x.Set(1, "d")) fmt.Println(x.Slice(s2)) }
Output: [a d c]
Example (Slice) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() x := store.NewSlice(s, 1, 2, 3) fmt.Println(x.Slice(s)) }
Output: [1 2 3]
func NewSlice ¶
NewSlice returns a new NewSlice and stores it in a Store.
s := store.New() x := store.NewSlice[int](s) // []int{} y := store.NewSlice(s, 3.14, 2.72) // []float64{3.14, 2.72}
Example ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() x := store.NewSlice[int](s) y := store.NewSlice(s, 3.14, 2.72) fmt.Println(x.Slice(s)) fmt.Println(y.Slice(s)) }
Output: [] [3.14 2.72]
type Solution ¶
type Solution struct { // Store of the Solution. If nil, it means that the solution is // operationally invalid. Store Store `json:"store"` Statistics Statistics `json:"statistics"` }
Solution of a decision automation problem. A Solution is an operationally valid Store.
type Solver ¶
type Solver interface { // All Solutions found by the Solver. Loop over the channel values to get // the solutions. All(context.Context) <-chan Solution // Last Solution found by the Solver. When running a Maximizer or // Minimizer, the last Solution is the best one found (highest or smallest // value, respectively) with the given options. Using this function is // equivalent to getting the last element when using All. Last(context.Context) Solution // Options provided to the Solver. Options() Options }
A Solver searches a space and finds the best Solution possible, this is, the best collection of Variable assignments in an operationally valid Store.
Example (All) ¶
Get all the solutions from the Generate example.
package main import ( "context" "encoding/json" "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() x := store.NewVar(s, 0) s = s.Generate(func(s store.Store) store.Generator { value := x.Get(s) return store.Lazy( func() bool { return value <= 2 }, func() store.Store { value++ return s.Apply(x.Set(value)) }, ) }) solver := s. Value(func(s store.Store) int { return x.Get(s) }). Format(func(s store.Store) any { return x.Get(s) }). Maximizer(store.DefaultOptions()) // Get all solutions. all := solver.All(context.Background()) // Loop over the channel values to get the solutions. solutions := make([]store.Store, len(all)) for solution := range all { solutions = append(solutions, solution.Store) } // Print the solutions. b, err := json.MarshalIndent(solutions, "", " ") if err != nil { panic(err) } fmt.Println(string(b)) }
Output: [ 0, 1, 2, 3 ]
Example (Last) ¶
Get the last (best) solutions from the Generate example.
package main import ( "context" "encoding/json" "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() x := store.NewVar(s, 0) s = s.Generate(func(s store.Store) store.Generator { value := x.Get(s) return store.Lazy( func() bool { return value <= 2 }, func() store.Store { value++ return s.Apply(x.Set(value)) }, ) }) solver := s. Value(func(s store.Store) int { return x.Get(s) }). Format(func(s store.Store) any { return x.Get(s) }). Maximizer(store.DefaultOptions()) // Get the last solution of the problem and print it. last := solver.Last(context.Background()) b, err := json.MarshalIndent(last.Store, "", " ") if err != nil { panic(err) } fmt.Println(string(b)) }
Output: 3
type Statistics ¶
type Statistics struct { // Bounds of the store. Nil when using a Satisfier. Bounds *Bounds `json:"bounds,omitempty"` Search Search `json:"search"` Time Time `json:"time"` // Value of the store. Nil when using a Satisfier. Value *int `json:"value,omitempty"` }
Statistics of the search.
type Store ¶
type Store interface { /* Apply changes to a Store. A change happens when a stored Variable is updated: s := store.New() x := store.NewVar(s, 3.1416) s1 := s.Apply( x.Set(x.Get(s) * 2), ) */ Apply(...Change) Store /* Bound the value of a Store. The solver can use this information to more efficiently find the best Store. The lower and upper bounds can be set: s := store.New() x := store.NewVar(s, initial) s = s.Bound(func(s store.Store) store.Bounds { return store.Bounds{ Lower: -1, Upper: 1, } }) */ Bound(Bounder) Store /* Format a Store into any structure prior to JSON encoding. s := store.New() x := store.NewVar(s, 10) s = s.Format(func(s store.Store) any { return map[string]int{"x": x.Get(s)} }) */ Format(Formatter) Store /* Generate new Stores (children) from the existing one (parent). A callback function provides a lexical scope that can be used to perform and update calculations. s := store.New() x := store.NewVar(s, 0) s = s.Generate(func(s store.Store) store.Generator { value := x.Get(s) return store.Lazy( func() bool { return value <= 2 }, func() store.Store { value++ return s.Apply(x.Set(value)) }, ) }) */ Generate(func(Store) Generator) Store /* Propagate changes into a Store until it reaches a fixed point. s := store.New() x := store.NewVar(s, 1) s = s.Propagate(func(s store.Store) []store.Change { return []store.Change{ x.Set(2), x.Set(42), } }) */ Propagate(...Propagator) Store /* Validate the Store. A Store is operationally valid if all decisions have been made and those decisions fulfill certain requirements; e.g.: all stops have been assigned to vehicles, all shifts are covered with the necessary personnel, all assignment have been made, quantity respects an alloted capacity, etc. Setting operational validity is optional and the default is true. s := store.New() x := store.NewVar(s, 1) s = s.Validate(func(s store.Store) bool { return x.Get(s)%2 == 0 }) */ Validate(Condition) Store /* Value sets the integer value of a Store. When maximizing or minimizing, this is the value that is optimized. s := store.New() x := store.NewVar(s, 6) s = s.Value(func(s store.Store) int { v := x.Get(s) return v * v }) */ Value(Valuer) Store // Maximizer builds a solver that searches the space defined by the Store // to maximize a value. Maximizer(Options) Solver // Minimizer builds a solver that searches the space defined by the Store // to minimize a value. Minimizer(Options) Solver // Satisfier builds a solver that searches the space defined by the Store // to satisfy operational validity. Satisfier(Options) Solver }
Store represents a store of Variables and logic to solve decision automation problems. Adding logic to the Store updates it (functions may be called directly and chained):
s := store.New() // s := store.New(). s = s.Apply(...) // Apply(...). s = s.Bound(...) // Generate(...). s = s.Format(...) // Bound(...). s = s.Generate(...) // Format(...)
The Variables and logic stored define a solution space. This space is searched to make decisions.
Example (Apply) ¶
Applying changes to a store updates it, e.g.: setting the value of a variable.
package main import ( "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() x := store.NewVar(s, 3.1416) s1 := s.Apply( x.Set(x.Get(s) * 2), ) fmt.Println(x.Get(s)) fmt.Println(x.Get(s1)) }
Output: 3.1416 6.2832
Example (Bound) ¶
Make an initial value approach a target by minimizing the absolute difference between them. The store is bounded near zero to help the solver look for the best solution. The resulting bounds are tightened.
package main import ( "context" "encoding/json" "fmt" "math" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() initial := 10 target := 16 x := store.NewVar(s, initial) s = s.Bound(func(s store.Store) store.Bounds { return store.Bounds{ Lower: -1, Upper: 1, } }) solver := s. Value(func(s store.Store) int { diff := float64(target - x.Get(s)) return int(math.Abs(diff)) }). Generate(func(s store.Store) store.Generator { value := x.Get(s) return store.Lazy( func() bool { return value <= 2*target }, func() store.Store { value++ return s.Apply(x.Set(value)) }, ) }). Format(func(s store.Store) any { return x.Get(s) }). Minimizer(store.DefaultOptions()) // Get the last solution of the problem and print it. last := solver.Last(context.Background()) // Override this variable to have a consistent testable example. last.Statistics.Time = store.Time{} b, err := json.MarshalIndent(last, "", " ") if err != nil { panic(err) } fmt.Println(string(b)) }
Output: { "store": 16, "statistics": { "bounds": { "lower": -1, "upper": 0 }, "search": { "generated": 23, "filtered": 0, "expanded": 23, "reduced": 0, "restricted": 10, "deferred": 13, "explored": 1, "solutions": 2 }, "time": { "elapsed": "0s", "elapsed_seconds": 0, "start": "0001-01-01T00:00:00Z" }, "value": 0 } }
Example (Format) ¶
A store can be formatted to any JSON representation.
package main import ( "encoding/json" "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() x := store.NewVar(s, 10) s = s.Format(func(s store.Store) any { return map[string]int{"x": x.Get(s)} }) b, err := json.Marshal(s) if err != nil { panic(err) } fmt.Println(string(b)) }
Output: {"x":10}
Example (Generate) ¶
Given a parent, which is simply an integer variable, children are generated by adding 1. This is done until the value reaches a certain limit.
package main import ( "context" "encoding/json" "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() x := store.NewVar(s, 0) s = s.Generate(func(s store.Store) store.Generator { value := x.Get(s) return store.Lazy( func() bool { return value <= 2 }, func() store.Store { value++ return s.Apply(x.Set(value)) }, ) }) solver := s. Value(func(s store.Store) int { return x.Get(s) }). Format(func(s store.Store) any { return x.Get(s) }). Maximizer(store.DefaultOptions()) // Get the last solution of the problem and print it. last := solver.Last(context.Background()) b, err := json.MarshalIndent(last.Store, "", " ") if err != nil { panic(err) } fmt.Println(string(b)) }
Output: 3
Example (Maximizer) ¶
Increase the value of a variable as much as possible.
package main import ( "context" "encoding/json" "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() x := store.NewVar(s, 10) maximizer := s. Value(x.Get). Format(func(s store.Store) any { return x.Get(s) }). Generate(func(s store.Store) store.Generator { value := x.Get(s) return store.Lazy( func() bool { return value <= 20 }, func() store.Store { value += 5 return s.Apply(x.Set(value)) }, ) }). Maximizer(store.DefaultOptions()) // Get the last solution of the problem and print it. last := maximizer.Last(context.Background()) b, err := json.MarshalIndent(last.Store, "", " ") if err != nil { panic(err) } fmt.Println(string(b)) }
Output: 25
Example (Minimizer) ¶
Decrease the value of a variable as much as possible.
package main import ( "context" "encoding/json" "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() x := store.NewVar(s, 10) minimizer := s. Value(x.Get). Format(func(s store.Store) any { return x.Get(s) }). Generate(func(s store.Store) store.Generator { value := x.Get(s) return store.Lazy( func() bool { return value >= 0 }, func() store.Store { value -= 5 return s.Apply(x.Set(value)) }, ) }). Minimizer(store.DefaultOptions()) // Get the last solution of the problem and print it. last := minimizer.Last(context.Background()) b, err := json.MarshalIndent(last.Store, "", " ") if err != nil { panic(err) } fmt.Println(string(b)) }
Output: -5
Example (Satisfier) ¶
Find the first number divisible by 6, starting from 100.
package main import ( "context" "encoding/json" "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() x := store.NewVar(s, 100) opt := store.DefaultOptions() opt.Limits.Solutions = 1 satisfier := s. Format(func(s store.Store) any { return x.Get(s) }). Validate(func(s store.Store) bool { return x.Get(s)%6 == 0 }). Generate(func(s store.Store) store.Generator { value := x.Get(s) return store.Lazy( func() bool { return value > 0 }, func() store.Store { value-- return s.Apply(x.Set(value)) }, ) }). Satisfier(opt) // Get the last solution of the problem and print it. last := satisfier.Last(context.Background()) b, err := json.MarshalIndent(last.Store, "", " ") if err != nil { panic(err) } fmt.Println(string(b)) }
Output: 90
Example (Validate) ¶
Validating that 1 is divisible by 2 results in an operational invalid store, represented as null.
package main import ( "context" "encoding/json" "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() x := store.NewVar(s, 1) s = s.Validate(func(s store.Store) bool { return x.Get(s)%2 == 0 }) solver := s. Format(func(s store.Store) any { return x.Get(s) }). Satisfier(store.DefaultOptions()) // Get the last solution of the problem and print it. last := solver.Last(context.Background()) b, err := json.MarshalIndent(last.Store, "", " ") if err != nil { panic(err) } fmt.Println(string(b)) }
Output: null
Example (Value) ¶
A custom value can be set on a store. Using any solver, the store has the given value.
package main import ( "context" "encoding/json" "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() x := store.NewVar(s, 6) s = s.Value(func(s store.Store) int { v := x.Get(s) return v * v }) solver := s.Minimizer(store.DefaultOptions()) // Get the last solution of the problem and print it. last := solver.Last(context.Background()) b, err := json.MarshalIndent(last.Statistics.Value, "", " ") if err != nil { panic(err) } fmt.Println(string(b)) }
Output: 36
type Valuer ¶
Valuer maps a Store to an integer value. It is meant to be used with the store.Value function.
type Variable ¶
type Variable[T any] interface { /* Get the current value of the Variable in the Store. s := store.New() x := store.NewVar(s, 10) s = s.Format(func(s store.Store) any { return map[string]int{"x": x.Get(s)} }) */ Get(Store) T /* Set a new value on the Variable. s := store.New() x := store.NewVar(s, 10) s = s.Apply(x.Set(15)) */ Set(T) Change }
Variable stored in a Store.
Example (Get) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() x := store.NewVar(s, 10) fmt.Println(x.Get(s)) }
Output: 10
Example (Set) ¶
package main import ( "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() x := store.NewVar(s, 10) fmt.Println(x.Get(s)) s1 := s.Apply(x.Set(15)) fmt.Println(x.Get(s1)) }
Output: 10 15
func NewVar ¶
NewVar stores a new Variable in a Store.
s := store.New() x := store.NewVar(s, 10) // x is stored in s.
Example ¶
Declaring a new variable adds the variable to the store.
package main import ( "encoding/json" "fmt" "github.com/nextmv-io/sdk/store" ) func main() { s := store.New() x := store.NewVar(s, 10) s = s.Apply(x.Set(15)) b, err := json.Marshal(s) if err != nil { panic(err) } fmt.Println(string(b)) }
Output: [15]