store

package
v0.20.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 5, 2022 License: Apache-2.0 Imports: 6 Imported by: 0

README

Store

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. The underlying algorithms 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.

See godocs for package docs.

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. The underlying algorithms 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
	opt.Diagram.Expansion.Limit = 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 21 10 15 06 ",
  "1": "11 16 07 20 09 ",
  "2": "24 01 22 05 14 ",
  "3": "17 12 03 08 19 ",
  "4": "02 23 18 13 04 "
}
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

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func False

func False(s Store) bool

False is a convenience function that is always false.

Example
package main

import (
	"fmt"

	"github.com/nextmv-io/sdk/store"
)

func main() {
	s := store.New()
	f := store.False(s)
	fmt.Println(f)
}
Output:

false

func True

func True(s Store) bool

True is a convenience function that is always true.

Example
package main

import (
	"fmt"

	"github.com/nextmv-io/sdk/store"
)

func main() {
	s := store.New()
	f := store.True(s)
	fmt.Println(f)
}
Output:

true

Types

type Bounder

type Bounder func(Store) Bounds

Bounder maps a Store to monotonically tightening bounds. It is meant to be used with the store.Bound function.

type Bounds

type Bounds struct {
	Lower int `json:"lower"`
	Upper int `json:"upper"`
}

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

type Condition func(Store) bool

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

func And(c1 Condition, c2 Condition, conditions ...Condition) Condition

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

func Not(c Condition) Condition

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

func Or(c1 Condition, c2 Condition, conditions ...Condition) Condition

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

func Xor(c1, c2 Condition) Condition

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.

func (Diagram) MarshalJSON

func (d Diagram) MarshalJSON() ([]byte, error)

MarshalJSON Diagram.

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([]int{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([]int{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

func Multiple(s Store, values ...int) Domain

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

func NewDomain(s Store, ranges ...model.Range) Domain

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

func Singleton(s Store, value int) Domain

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, []int{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, []int{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

func NewDomains(s Store, domains ...model.Domain) Domains

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

func Repeat(s Store, n int, domain model.Domain) Domains

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

type Formatter func(Store) any

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

func Eager(s ...Store) Generator

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

func Lazy(c func() bool, f func() Store) Generator

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 Key

type Key interface{ int | string }

A Key for a Map.

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.

func (Limits) MarshalJSON

func (l Limits) MarshalJSON() ([]byte, error)

MarshalJSON Limits.

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

func NewMap[K Key, V any](s Store) Map[K, V]

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": "168h0m0s"
  },
  "search": {
    "buffer": 100
  },
  "sense": "minimize"
}
{
  "diagram": {
    "expansion": {
      "limit": 1
    },
    "width": 10
  },
  "limits": {
    "duration": "4s"
  },
  "search": {
    "buffer": 100
  },
  "sense": "minimize",
  "tags": {
    "bar": 2,
    "foo": 1
  }
}

func (Options) MarshalJSON

func (o Options) MarshalJSON() ([]byte, error)

MarshalJSON Options.

type Propagator

type Propagator func(Store) []Change

Propagator propagates Changes to a Store. It is meant to be used with the store.Propagate function.

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
)

func (Sense) String

func (s Sense) String() string

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

func NewSlice[T any](s Store, values ...T) Slice[T]

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 and is re-invoked until no further
		changes need to be made and an empty slice of changes is returned.

			s := store.New()
			x := store.NewVar(s, 1)
			s = s.Propagate(func(s store.Store) []store.Change {
				if x.Get(s) <= 1 {
					return []store.Change{
						x.Set(2),
						x.Set(42),
					}
				}
				return []store.Change{}
			})
	*/
	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(...)    // 	   Bound(...).
s = s.Format(...)   // 	   Format(...).
s = s.Generate(...) // 	   Generate(...)

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
	opt.Diagram.Expansion.Limit = 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:

96
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

func New

func New() Store

New returns a new Store.

type Time

type Time struct {
	Start   time.Time     `json:"start"`
	Elapsed time.Duration `json:"elapsed"`
}

Time statistics.

func (Time) MarshalJSON

func (t Time) MarshalJSON() ([]byte, error)

MarshalJSON Time.

type Valuer

type Valuer func(Store) int

Valuer maps a Store to an integer value. It is meant to be used with the store.Value function.

type Var

type Var[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
}

Var is a 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

func NewVar[T any](s Store, data T) Var[T]

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]

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL