Full API

Every main concept of the Seeker Reference Manual, in the same order and with the same section numbers, each with a short example you can run. The manual remains the place for every signature and overload; this page shows how the pieces are used; the Core API is the shorter overview by task. All examples run on the free demo.

2 The Environment Class

2.1 Environment creation and termination

An environment holds one model. It is deterministic by default and stochastic when created with True. Licensed users can run several cooperating processes that share a run ID.

  • skr.Env(license)
  • skr.Env(license, stochastic)
  • skr.Env(license, processID, runID[, stochastic])
  • env.end()
%pip install -q seekerdemo
import seekerdemo as skr

# A deterministic environment ('' = free demo license; otherwise the license file)
env = skr.Env('')
x = env.continuous(0, 10)
env.minimize(env.sqr(x - 4), 1)
print("deterministic:", round(x.get_value(), 3))
env.end()                                    # always end an environment

# A stochastic environment: pass True as second argument
senv = skr.Env('', True)
d = senv.normal(10, 2)                       # a random term
q = senv.continuous(0, 20)
senv.minimize(senv.aggregate_mean(senv.abs(q, d)), 1)
print("stochastic:  ", round(q.get_value(), 1))
senv.end()

# Parallel runs (licensed version): Env(license, processID, runID[, stochastic])
# processes with the same runID cooperate; processIDs count up from 0.
deterministic: 4.0
stochastic:   10.0

2.2 Basic decision variables

Three kinds of variables, each with an optional starting value. Categoricals can be limited to a list of allowed labels.

  • env.continuous(l, h[, v])
  • env.ordinal(l, h[, v])
  • env.categorical(l, h[, allowed][, v])
%pip install -q seekerdemo
import seekerdemo as skr

env = skr.Env('')
temp  = env.continuous(150.0, 250.0)          # any real number in [150, 250]
speed = env.continuous(1.0, 3.0, 2.0)         # ... starting the search at 2.0
batch = env.ordinal(1, 20)                    # integers; 7 is "close" to 8
crew  = env.ordinal(1, 6, 3)                  # ... starting at 3
line  = env.categorical(0, 4)                 # labels 0..4, no notion of closeness
oven  = env.categorical(0, 9, [2, 5, 7])      # only labels 2, 5 or 7 allowed
tool  = env.categorical(0, 9, [1, 4], 4)      # allowed labels, starting at 4

score = (env.sqr(temp - 180) + env.sqr(speed - 2.5) + env.sqr(batch - 12) + env.sqr(crew - 4)
         + env.if_(line == 3, 0, 5) + env.if_(oven == 7, 0, 5) + env.if_(tool == 1, 0, 5))
env.minimize(score, 1)
print([round(v.get_value(), 2) for v in (temp, speed, batch, crew, line, oven, tool)])
env.end()
[180.0, 2.5, 12.0, 4.0, 3.0, 7.0, 1.0]

2.3 Meta decision variables

Variables that carry structure by construction: groupings, packings, shares, split totals and permutations (2.3.1 to 2.3.3 cover the classes they return). Permutation precedences say which items must come before which in the order: precedences[i] lists the items that item i precedes, so get_permutation_inverse()[i] is smaller than that of each listed item.

  • env.partition(k, n[, init])
  • env.packing(k, n[, init])
  • env.bool(partition, item, k)
  • env.convex_combination(n | init)
  • env.split_total(n | init, total)
  • env.permutation(n | init[, precedences][, cyclic])
  • Partition.get_variables()
  • ConvexCombination.get_combination(), get_variables()
  • Permutation.get_permutation(), get_permutation_inverse(), get_variables()
%pip install -q seekerdemo
import seekerdemo as skr

# Partition: every item in exactly one group.  Packing: an extra set k = "left out".
w = [4.0, 7.0, 2.0, 5.0, 6.0, 3.0]
env = skr.Env('')
W = env.convert(w)
groups = env.partition(2, len(w))             # 2 groups, 6 items
picked = env.packing(2, len(w), [0, 1, 2, 2, 0, 1])   # 2 sets + set 2 = not packed, with start values
g = groups.get_variables()                    # group of each item
first_in_0 = env.bool(groups, 0, 0)           # 1 if item 0 is in group 0
balance = env.abs(env.sum_if(W, groups, 0), env.sum_if(W, groups, 1))
fill = env.abs(env.sum_if(W, picked, 0), 9) + env.abs(env.sum_if(W, picked, 1), 8)
env.minimize(balance + fill, 1)
print("groups:", [int(v.get_value()) for v in g], " item 0 in group 0:", int(first_in_0.get_value()))
print("packed:", [int(v.get_value()) for v in picked.get_variables()], "(2 = left out)")
env.end()
groups: [0, 1, 0, 0, 1, 0]  item 0 in group 0: 1
packed: [2, 0, 0, 1, 2, 1] (2 = left out)
%pip install -q seekerdemo
import seekerdemo as skr

env = skr.Env('')
cc = env.convex_combination(3)                # 3 shares >= 0 that sum to 1
st = env.split_total([4, 3, 3], 10)           # 3 integers >= 0 that sum to 10, start 4/3/3
# precedences[i] lists the items j that item i must precede in the order: 0 before 3
pm = env.permutation(5, [[3], [], [], [], []])
p, pinv = pm.get_permutation(), pm.get_permutation_inverse()
tour = env.permutation(4, 1.0)                # cyclic = 1: only successors matter (tours)

share, crew = cc.get_combination(), st.get_combination()
goal = (env.sum([env.sqr(share[i] - [.5, .3, .2][i]) for i in range(3)])
        + env.sum([env.sqr(crew[i] - [5, 3, 2][i]) for i in range(3)])
        + env.sum([env.abs(p[k], [3, 2, 1, 4, 0][k]) for k in range(5)]))
env.minimize(goal, 1)
show = lambda ts: [round(t.get_value(), 2) for t in ts]
print("shares", show(share), " crews", show(crew))
print("order p", show(p), " position of each item", show(pinv))
print("item 0 comes before item 3:", pinv[0].get_value() < pinv[3].get_value())
print("underlying variables:", len(cc.get_variables()), len(st.get_variables()), len(pm.get_variables()))
env.end()
shares [0.5, 0.3, 0.2]  crews [5.0, 3.0, 2.0]
order p [4.0, 2.0, 0.0, 3.0, 1.0]  position of each item [2.0, 4.0, 1.0, 3.0, 0.0]
item 0 comes before item 3: True
underlying variables: 3 3 5

2.4 Unary operators

Functions of one term.

  • abs, sqr, sqrt, exp, log
  • min_0, max_0
  • sin, cos
  • ceil, floor, round, trunc
  • norm_cdf, probit
%pip install -q seekerdemo
import seekerdemo as skr

env = skr.Env('')
a, p = env.convert(-2.7), env.convert(0.975)
res = {"abs": env.abs(a), "sqr": env.sqr(a), "sqrt(|a|)": env.sqrt(env.abs(a)),
       "exp": env.exp(a), "log(|a|)": env.log(env.abs(a)), "min_0": env.min_0(a), "max_0": env.max_0(a),
       "sin": env.sin(a), "cos": env.cos(a), "ceil": env.ceil(a), "floor": env.floor(a),
       "round": env.round(a), "trunc": env.trunc(a),
       "norm_cdf(1.96)": env.norm_cdf(env.convert(1.96)), "probit(0.975)": env.probit(p)}
env.evaluate()                                # compute terms without optimizing
for k, t in res.items():
    print(f"{k:15} {t.get_value():8.4f}")
env.end()
abs               2.7000
sqr               7.2900
sqrt(|a|)         1.6432
exp               0.0672
log(|a|)          0.9933
min_0            -2.7000
max_0             0.0000
sin              -0.4274
cos              -0.9041
ceil             -2.0000
floor            -3.0000
round            -3.0000
trunc            -2.0000
norm_cdf(1.96)    0.9750
probit(0.975)     1.9600

2.5 Binary operators

Functions of two terms. eucl is the squared difference.

  • abs(a, b) = |a − b|
  • eucl(a, b) = (a − b)²
  • power, power_round_exp
  • div, round_div, round_mod
  • a % b
%pip install -q seekerdemo
import seekerdemo as skr

env = skr.Env('')
a, b = env.convert(7.6), env.convert(2.4)
res = {"abs(a, b) = |a-b|": env.abs(a, b), "eucl(a, b) = (a-b)^2": env.eucl(a, b),
       "power(a, b)": env.power(a, b), "power_round_exp(a, b)": env.power_round_exp(a, b),
       "div(a, b)": env.div(a, b), "round_div(a, b)": env.round_div(a, b),
       "round_mod(a, b)": env.round_mod(a, b), "a % b": a % b}
env.evaluate()
for k, t in res.items():
    print(f"{k:24} {t.get_value():9.4f}")
env.end()
abs(a, b) = |a-b|           5.2000
eucl(a, b) = (a-b)^2       27.0400
power(a, b)               130.0028
power_round_exp(a, b)      57.7600
div(a, b)                   3.0000
round_div(a, b)             4.0000
round_mod(a, b)             0.0000
a % b                       0.4000

2.6 Right indexing

Read from a table at positions chosen by variables. A tensor is indexed with T[i] or T[i, j] (2.6.1); env.index([i, j], T) is the long form.

  • env.tensor(data)
  • T[i], T[i, j, …], T[[i, j, …]]
  • env.index([i, …], T)
%pip install -q seekerdemo
import seekerdemo as skr

# Which warehouse serves which store? Read costs from a table with variable indices.
cost = [[4.0, 9.0, 7.0],               # cost[warehouse][store]
        [8.0, 3.0, 6.0]]
env = skr.Env('')
C = env.tensor(cost)                   # read-only table
FEE = env.tensor([20.0, 15.0])         # 1-D tensor
wh = [env.categorical(0, 1) for _ in range(3)]
ship = env.sum([C[wh[s], env.convert(s)] for s in range(3)])   # C[i, j]: 2-D lookup
fee = env.sum([FEE[w] for w in wh]) / 3                        # FEE[i]: 1-D lookup
same = C[[wh[0], env.convert(0)]]       # list form, equivalent to env.index([...], C)
env.minimize(ship + fee, 1)
print("warehouse per store:", [int(w.get_value()) for w in wh], " shipping:", ship.get_value())
env.end()
warehouse per store: [0, 1, 1]  shipping: 13.0

2.7 Left indexing

Write values into a table at positions chosen by variables, then read the result. Positions written in one call must differ, which a permutation guarantees. The canvas class (2.7.1) converts back to lists and slices.

  • env.canvas(data)
  • env.assignment(canvas, indices, values)
  • C[i, j], env.index([i, j], C)
  • canvas.to_1d(), to_2d(), to_3d(), slice(dims, values)
%pip install -q seekerdemo
import seekerdemo as skr

# Place 3 parcels on a 2 x 3 shelf. A permutation of the 6 slots picks a distinct
# slot per parcel, so two parcels never land on the same position.
env = skr.Env('')
shelf = env.canvas([[0.0, 0.0, 0.0], [0.0, 0.0, 0.0]])    # writable 2-D table
slot = env.permutation(6).get_permutation()[:3]           # slot of parcels 0, 1, 2
pos = [[env.floor(s / 3), s % 3] for s in slot]           # (row, column) of each parcel
weight = env.convert([5.0, 3.0, 4.0])
filled = env.assignment(shelf, pos, weight)               # write at variable positions
top_row = filled.slice([0], [1]).to_1d()                  # fix dimension 0 to 1: the top row
weight_on_top = env.sum(top_row)
env.minimize(weight_on_top, 1)
print("shelf:", [[c.get_value() for c in r] for r in filled.to_2d()])
print("weight in the top row:", weight_on_top.get_value())
print("read one entry with []:", filled[[env.convert(0), env.convert(0)]].get_value())
env.end()
shelf: [[5.0, 3.0, 4.0], [0.0, 0.0, 0.0]]
weight in the top row: 0.0
read one entry with []: 5.0

2.8 Conditioning

Choose between two values depending on a condition. Either branch may be a term or a plain number.

  • env.if_(cond, then, otherwise)
%pip install -q seekerdemo
import seekerdemo as skr

# Shipping fee by weight tier: if_(condition, then, otherwise), terms or plain numbers.
env = skr.Env('')
for kg in [3.0, 12.0, 30.0]:
    w = env.convert(kg)
    fee = env.if_(w <= 5, 6.0,
                  env.if_(w <= 20, 6.0 + 0.8 * (w - 5), 18.0 + 0.5 * (w - 20)))
    env.evaluate()
    print(f"{kg:4.0f} kg -> fee {fee.get_value():.2f}")
env.end()
   3 kg -> fee 6.00
  12 kg -> fee 11.60
  30 kg -> fee 23.00

2.9 Aggregation

Summaries of a list of terms (2.9.1 to 2.9.19). Every aggregation also comes as _if, restricted to the items selected by a partition, switch conditions or interval conditions.

  • sum, prod, max, argmax, maximum, min, argmin, minimum
  • and_, or_
  • mean, variance, sample_variance, standard_deviation
  • norm1, norm2, geometric_mean, rmsv, average_absolute_value
  • sum_if, max_if, mean_if, … (all aggregations)
%pip install -q seekerdemo
import seekerdemo as skr

env = skr.Env('')
v = env.convert([5.0, -1.0, 9.0, 3.0, 2.0, 6.0])
f = {"sum": env.sum, "prod": env.prod, "max": env.max, "argmax": env.argmax, "min": env.min,
     "argmin": env.argmin, "mean": env.mean, "variance": env.variance,
     "sample_variance": env.sample_variance, "standard_deviation": env.standard_deviation,
     "norm1": env.norm1, "norm2": env.norm2, "rmsv": env.rmsv,
     "average_absolute_value": env.average_absolute_value}
res = {k: fn(v) for k, fn in f.items()}
res["geometric_mean(|v|)"] = env.geometric_mean([env.abs(t) for t in v])
res["maximum -> [pos, value]"] = env.maximum(v)
res["minimum -> [pos, value]"] = env.minimum(v)
res["and_ / or_ of (v > 0)"] = [env.and_([t > 0 for t in v]), env.or_([t > 0 for t in v])]
# _if variants: the same functions, restricted by a partition, switch or interval condition
part = env.partition(2, 6, [0, 1, 0, 1, 0, 1])
res["sum_if / max_if, group 0"] = [env.sum_if(v, part, 0), env.max_if(v, part, 0)]
ic = [env.interval_condition(t) for t in v]
res["mean_if v in [2, 6]"] = [env.mean_if(v, ic, 2.0, 6.0)]
env.evaluate()
for k, t in res.items():
    vals = [round(x.get_value(), 3) for x in (t if isinstance(t, list) else [t])]
    print(f"{k:26}", vals if len(vals) > 1 else vals[0])
env.end()
sum                        24.0
prod                       -1620.0
max                        9.0
argmax                     2.0
min                        -1.0
argmin                     1.0
mean                       4.0
variance                   10.0
sample_variance            12.0
standard_deviation         3.162
norm1                      26.0
norm2                      12.49
rmsv                       5.099
average_absolute_value     4.333
geometric_mean(|v|)        3.427
maximum -> [pos, value]    [2.0, 9.0]
minimum -> [pos, value]    [1.0, -1.0]
and_ / or_ of (v > 0)      [0.0, 1.0]
sum_if / max_if, group 0   [16.0, 9.0]
mean_if v in [2, 6]        4.0

2.10 Quantiles and sorting

The i-th smallest or largest value (i may be a variable), and sorted views of a list (2.10.1). Ties keep the original order.

  • env.ith_smallest_value(i, terms), ith_largest_value
  • env.float_sorting, int_sorting, partial_sorting(terms, k, maximize)
  • Sorting.get(), get_permutation(), get_permutation_inverse()
%pip install -q seekerdemo
import seekerdemo as skr

env = skr.Env('')
bids = env.convert([12.0, 7.5, 19.0, 7.5, 15.0])
k = env.ordinal(0, 4, 1)                         # the index may itself be a variable
second = env.ith_largest_value(1, bids)          # -> [value, position]
kth = env.ith_smallest_value(k, bids)
s = env.float_sorting(bids)                      # full sort, smallest first
top2 = env.partial_sorting(bids, 2, True)        # only the 2 largest, largest first
r = env.int_sorting(env.convert([2.6, 1.2, 3.9]))   # rounds first, then sorts
env.evaluate()
show = lambda ts: [round(t.get_value(), 2) for t in ts]
print("2nd largest [value, pos]:", show(second), "  k-th smallest (k=1):", show(kth))
print("sorted:", show(s.get()), " came from positions:", show(s.get_permutation()))
print("rank of each original bid:", show(s.get_permutation_inverse()))
print("top 2:", show(top2.get()[:2]), " int_sorting:", show(r.get()))
env.end()
2nd largest [value, pos]: [15.0, 4.0]   k-th smallest (k=1): [7.5, 3.0]
sorted: [7.5, 7.5, 12.0, 15.0, 19.0]  came from positions: [1.0, 3.0, 0.0, 4.0, 2.0]
rank of each original bid: [2.0, 0.0, 4.0, 1.0, 3.0]
top 2: [19.0, 15.0]  int_sorting: [1.0, 3.0, 4.0]

2.11 Conversion

Turn numbers, lists and matrices into terms.

  • env.convert(number | list | matrix)
%pip install -q seekerdemo
import seekerdemo as skr

env = skr.Env('')
one = env.convert(3)                              # number -> term
row = env.convert([1.5, 2.5])                     # list -> list of terms
mat = env.convert([[1, 2], [3, 4]])               # matrix -> matrix of terms
flag = env.convert(True)
x = env.ordinal(0, 10)
total = one * x + env.sum(row) + mat[1][0] + flag
env.minimize(env.abs(total, 20), 1)
print("x =", x.get_value(), " total =", total.get_value())
env.end()
x = 4.0  total = 20.0

2.12 Efficient Boolean indicators

Fast 0/1 conditions for many items at once: switch conditions compare a term with a list of values (2.12.1), interval conditions test whether it lies in a range (2.12.2). Both drive the _if aggregations.

  • env.switch_condition(term, matches | n)
  • env.interval_condition(term)
  • env.bool(condition, …) (boolean in C++)
%pip install -q seekerdemo
import seekerdemo as skr

# Six jobs on three machines (switch conditions) and staff present per hour (interval conditions).
hours = [2.0, 1.0, 3.0, 2.0, 1.0, 2.0]
env = skr.Env('')
machine = [env.categorical(0, 2) for _ in hours]
start = [env.ordinal(0, 6) for _ in hours]
H = env.convert(hours)
on = [env.switch_condition(m, 3) for m in machine]          # matches the values 0, 1, 2
load = [env.sum_if(H, on, k) for k in range(3)]             # hours per machine
at = [env.interval_condition(s) for s in start]
busy = [env.sum_if(env.convert([1.0] * 6), at, float(t - 1), float(t)) for t in range(8)]  # jobs started in [t-1, t]
is_m0 = env.bool(on[0], 0)                                  # 1 if job 0 is on machine 0
early = env.bool(at[0], 0.0, 2.0)                           # 1 if job 0 starts in [0, 2]
env.minimize(env.max(load) + env.max(busy) / 10, 1)
print("load per machine:", [l.get_value() for l in load], " max starts in 2 h:", max(b.get_value() for b in busy))
print("job 0 on machine 0:", is_m0.get_value(), " job 0 starts early:", early.get_value())
env.end()
load per machine: [4.0, 4.0, 3.0]  max starts in 2 h: 2.0
job 0 on machine 0: 1.0  job 0 starts early: 0.0

2.13 User-defined terms

Plug any Python function into the model. With incremental() returning True, Seeker only reports the inputs that changed (2.13.1, 2.13.2). Vectors return several values. For speed, compiled functions (e.g. with Numba) can be attached through the function pointer types (2.13.3, 2.13.4).

  • env.user_defined_term(inputs, obj)
  • env.user_defined_vector(inputs, obj, n)
  • UserEvalTerm: evaluate, inc_evaluate, incremental
  • UserEvalVector: recompute, inc_recompute, incremental
  • evaluate_fn_ptr, incremental_fn_ptr (Python only)
%pip install -q seekerdemo
import seekerdemo as skr

class Revenue(skr.UserEvalTerm):                 # a user-defined term with fast updates
    def __init__(self, price):
        super().__init__(); self.price = price; self.total = 0.0
    def evaluate(self, v):                       # full computation
        self.total = sum(p * min(x, 40) for p, x in zip(self.price, v)); return self.total
    def inc_evaluate(self, idx, old, new):       # only the changed inputs
        for i, o, n in zip(idx, old, new):
            self.total += self.price[i] * (min(n, 40) - min(o, 40))
        return self.total
    def incremental(self):
        return True

class Stats(skr.UserEvalVector):                 # a user-defined vector: several outputs
    def __init__(self):
        super().__init__()
    def recompute(self, v):
        return [max(v), sum(v) / len(v)]
    def incremental(self):
        return False

env = skr.Env('')
qty = [env.ordinal(0, 60) for _ in range(4)]
rev_fn, stats_fn = Revenue([3.0, 5.0, 2.0, 4.0]), Stats()   # keep references while solving
revenue = env.user_defined_term(qty, rev_fn)
peak, avg = env.user_defined_vector(qty, stats_fn, 2)
env.maximize(revenue - 2 * peak - avg, 1)
print("qty", [q.get_value() for q in qty], " revenue", revenue.get_value(), " peak", peak.get_value())
env.end()
qty [40.0, 40.0, 40.0, 40.0]  revenue 560.0  peak 40.0

2.14 Nested linear optimization

A linear program inside the model whose data are terms: Seeker re-solves it whenever its inputs change. The LP class gives status, objective, solution and duals (2.15.1).

  • env.lp(obj, varBounds, rowBounds, matrix, maximize[, condition])
  • sparse form: columnIndices, coefficients
  • LP.get_solution_status(), get_objective(), get_solution(), get_row_sums()
  • LP.get_dual_status(), get_row_duals(), get_column_duals()
%pip install -q seekerdemo
import seekerdemo as skr

# Outer decision: how much capacity to rent. Inner LP: best production plan for that capacity.
#   max 30 a + 20 b   s.t.  2a + 1b <= capacity,  1a + 1b <= 40,  0 <= a, b <= 30
env = skr.Env('')
capacity = env.ordinal(20, 100)
c = env.convert
plan = env.lp([c(30.0), c(20.0)],                       # objective coefficients
              [c(0.0), c(30.0), c(0.0), c(30.0)],      # bounds: a in [0,30], b in [0,30]
              [c(-1e9), capacity, c(-1e9), c(40.0)],   # row bounds: [low, high] per row
              [[c(2.0), c(1.0)], [c(1.0), c(1.0)]],    # constraint matrix
              True)                                     # maximize
ok = plan.get_solution_status()                         # 1 = optimal
margin = env.if_(ok == 1, plan.get_objective(), -1e6)   # guard against non-optimal LPs
profit = margin - 9.0 * capacity                        # rent costs 9 per unit
env.maximize(profit, 1)
a, b = plan.get_solution()
print(f"rent {capacity.get_value():.0f}: make a={a.get_value():.0f}, b={b.get_value():.0f}, profit {profit.get_value():.0f}")
if plan.get_dual_status().get_value():
    print("shadow prices of the rows:", [round(d.get_value(), 2) for d in plan.get_row_duals()])
env.end()
rent 70: make a=30, b=10, profit 470
shadow prices of the rows: [10.0, 10.0]

2.15 Nested mixed integer optimization

The same with integer variables, either from a parameterized .lp file, where ioskr_i stands for the i-th term, or from data with integrality flags. A node limit bounds the branch and bound.

  • env.lp(fileName, terms[, condition], nodeLimit)
  • env.lp(…, columnIndices, coefficients, isInteger, nodeLimit, maximize)
  • LP.get_mip_dual(), write_to_file(name)
%pip install -q seekerdemo
import seekerdemo as skr

# Nested MIP from a parameterized .lp file: "ioskr_i" is replaced by the i-th term.
open("trucks.lp", "w").write("""Maximize
 obj: 7 x + 5 y
Subject To
 weight: 3 x + 2 y <= ioskr_0
 slots:  x + y <= 6
General
 x y
End
""")
env = skr.Env('')
payload = env.ordinal(6, 30)                         # outer decision: truck payload
mip = env.lp("trucks.lp", [payload], 100)            # node limit 100
ok = mip.get_solution_status()
value = env.if_(ok == 1, mip.get_objective(), 0) - 1.2 * payload
env.maximize(value, 1)
x, y = mip.get_solution()
print(f"payload {payload.get_value():.0f}: x={round(x.get_value())}, y={round(y.get_value())}, net {value.get_value():.1f}")
print("MIP dual bound:", mip.get_mip_dual().get_value())

# The same MIP in sparse form with integrality flags:
c = env.convert
mip2 = env.lp([c(7.0), c(5.0)], [c(0.0), c(1e9), c(0.0), c(1e9)], [c(-1e9), c(18.0), c(-1e9), c(6.0)],
              [[0, 1], [0, 1]], [[c(3.0), c(2.0)], [c(1.0), c(1.0)]], [True, True], 100, True)
env.evaluate()
print("sparse MIP with payload 18:", mip2.get_objective().get_value())
env.end()
payload 18: x=6, y=0, net 20.4
MIP dual bound: 42.0
sparse MIP with payload 18: 42.0

2.16 Nested Seeker optimization

A second Seeker model optimized inside the first, for every outer solution: for example, the best packing for a shelf height chosen outside. The inner input is a placeholder term (inner.convert(0.0)) that the outer model sets.

  • env.nested_seeker(outerInputs, innerInputs, innerObjective, innerOutputs, innerEnv, stepLimit, maximize[, condition])
  • SubSolver.get_solution_status(), get_objective(), get_outputs()

Example follows with the next seekerdemo release.

2.17 Matrix and vector manipulation

Linear algebra on terms.

  • env.scalar_product(a, b)
  • env.matrix_multiplication(A, B)
  • env.matrix_transpose(A)
%pip install -q seekerdemo
import seekerdemo as skr

env = skr.Env('')
A = env.convert([[1.0, 2.0], [3.0, 4.0]])
w = [env.continuous(0, 1) for _ in range(2)]
B = [[w[0]], [w[1]]]                              # a 2 x 1 matrix of variables
AB = env.matrix_multiplication(A, B)             # 2 x 1
At = env.matrix_transpose(A)
dot = env.scalar_product(w, env.convert([3.0, 1.0]))
env.minimize(env.abs(AB[0][0], 2.0) + env.abs(AB[1][0], 4.4) + dot / 100, 1)
print("w", [round(x.get_value(), 3) for x in w], " A*w", [round(r[0].get_value(), 3) for r in AB])
print("transpose", [[t.get_value() for t in r] for r in At])
env.end()
w [0.4, 0.8]  A*w [2.0, 4.4]
transpose [[1.0, 3.0], [2.0, 4.0]]

2.18 Nested equation systems

Solve A x = b inside the model, where A and b may depend on decisions. Several right-hand sides can be solved at once.

  • env.equation_system(matrix, rhs[, condition])
  • sparse form: nonzeros, rows, cols, rhs
  • EQSystem.get_solution_status(), get_solutions(), get_solution(i)

Example follows with the next seekerdemo release.

2.19 Canonical distribution terms

Random terms from standard distributions, in a stochastic environment. The last, optional argument controls how finely Seeker samples the distribution.

  • continuous_uniform, discrete_uniform, triangular
  • continuous_exponential, discrete_exponential, bernoulli
  • categorical_distribution, binomial(p, n), poisson
  • normal, multivariate_normal, lognormal, chi2, gamma, beta, weibull
%pip install -q seekerdemo
import seekerdemo as skr

env = skr.Env('', True)                                   # stochastic environment
dists = {
    "continuous_uniform(2, 6)":        env.continuous_uniform(2, 6),
    "discrete_uniform(1, 6)":          env.discrete_uniform(1, 6),
    "triangular(0, 10, 2)":            env.triangular(0, 10, 2),
    "continuous_exponential(0.5)":     env.continuous_exponential(0.5),
    "discrete_exponential(0.5)":       env.discrete_exponential(0.5),
    "bernoulli(0.3)":                  env.bernoulli(0.3),
    "categorical_distribution":        env.categorical_distribution([1.0, 3.0], [10.0, 20.0]),
    "binomial(p=0.4, n=10)":           env.binomial(0.4, 10),
    "poisson(4)":                      env.poisson(4),
    "normal(100, 15)":                 env.normal(100, 15),
    "lognormal(1, 0.5)":               env.lognormal(1, 0.5),
    "chi2(3)":                         env.chi2(3),
    "gamma(2, 1.5)":                   env.gamma(2, 1.5),
    "beta(2, 5)":                      env.beta(2, 5),
    "weibull(1.5, 2)":                 env.weibull(1.5, 2),
}
means = {k: env.aggregate_mean(t) for k, t in dists.items()}
env.evaluate()                                             # compute aggregates without optimizing
for k, m in means.items():
    print(f"{k:30} mean {m.get_value():8.3f}")
env.end()
continuous_uniform(2, 6)       mean    4.045
discrete_uniform(1, 6)         mean    3.462
triangular(0, 10, 2)           mean    4.008
continuous_exponential(0.5)    mean    1.993
discrete_exponential(0.5)      mean    1.430
bernoulli(0.3)                 mean    0.298
categorical_distribution       mean   17.400
binomial(p=0.4, n=10)          mean    4.022
poisson(4)                     mean    3.978
normal(100, 15)                mean   99.219
lognormal(1, 0.5)              mean    3.171
chi2(3)                        mean    3.129
gamma(2, 1.5)                  mean    3.098
beta(2, 5)                     mean    0.278
weibull(1.5, 2)                mean    1.757

2.20 User-defined stochastic distributions

Your own sampler, for example historical data or an ML model's posterior. The vector version keeps values that belong together in the same scenario (2.20.1, 2.20.2).

  • env.user_distribution(obj, maxSample)
  • env.user_vector_distribution(obj, maxSample)
  • UserDistribution.sample_n(n)
  • UserVectorDistribution.sample_n(n)
%pip install -q seekerdemo
import random, seekerdemo as skr

class Footfall(skr.UserDistribution):            # your own sampler, e.g. from history or an ML model
    def __init__(self):
        super().__init__()
    def sample_n(self, n):
        rnd = random.Random(1)
        return [rnd.choice([80, 95, 110, 140, 210]) * rnd.uniform(0.9, 1.1) for _ in range(n)]

class Weather(skr.UserVectorDistribution):       # several random values that move together
    def __init__(self):
        super().__init__()
    def sample_n(self, n):
        rnd = random.Random(2); out = []
        for _ in range(n):
            t = rnd.gauss(20, 6)
            out.append([t, max(0.0, 30 - t + rnd.gauss(0, 3))])   # [temperature, umbrellas sold]
        return out

env = skr.Env('', True)
fd, wd = Footfall(), Weather()                   # keep references while solving
visitors = env.user_distribution(fd, 200)
temp, umbrellas = env.user_vector_distribution(wd, 200)
staff = env.ordinal(1, 10)
stock = env.ordinal(0, 40)
cost = env.aggregate_mean(25 * staff + 2 * env.max_0(visitors - 25 * staff)
                          + 1.5 * stock + 6 * env.max_0(umbrellas - stock))
env.minimize(cost, 2)
print(f"staff {staff.get_value():.0f}, umbrellas {stock.get_value():.0f}, expected cost {cost.get_value():.1f}")
pairs = env.correlated_scenarios([temp, umbrellas], 100)          # 100 joint scenarios
warm = [u for t, u in pairs if t > 25]; cold = [u for t, u in pairs if t < 15]
print("warm days sell fewer umbrellas than cold days:", sum(warm) / len(warm) < sum(cold) / len(cold))
env.end()
staff 5, umbrellas 14, expected cost 198.5
warm days sell fewer umbrellas than cold days: True

2.21 Decision-dependent distributions

Randomness that depends on decisions, such as demand that responds to price. The sampler receives the current values of the feature terms (2.21.1, 2.21.2).

  • env.user_defined_stochastic_term(features, obj)
  • env.user_defined_stochastic_vector(features, obj, n)
  • UserStochTerm / UserStochVector.sample_n(features, n)
%pip install -q seekerdemo
import random, seekerdemo as skr

class Sales(skr.UserStochVector):                 # random outcomes that depend on decisions
    def __init__(self):
        super().__init__()
    def sample_n(self, features, n):              # features = current [price, promo]
        price, promo = features; rnd = random.Random(4); out = []
        for _ in range(n):
            units = max(0.0, rnd.gauss(500 - 18 * price + 60 * promo, 40))
            out.append([units, units * rnd.uniform(0.1, 0.2)])        # [units sold, returns]
        return out

env = skr.Env('', True)
price, promo = env.continuous(5, 25), env.ordinal(0, 1)
fn = Sales()                                      # keep a reference while solving
units, returns = env.user_defined_stochastic_vector([price, promo], fn, 2)
profit = env.aggregate_mean((price - 4) * (units - returns) - 300 * promo)
env.maximize(profit, 2)
print(f"price {price.get_value():.2f}, promo {promo.get_value():.0f}, expected profit {profit.get_value():.0f}")
# A single random term works the same way: env.user_defined_stochastic_term([price], obj)
# with a class derived from skr.UserStochTerm.
env.end()
price 17.68, promo 1, expected profit 2509

2.22 Stochastic aggregation

Turn a random term into a number to optimize or constrain: expectations, spreads, quantiles, CVaR and probabilities. Call env.evaluate() before reading aggregates outside a solve (2.22.1).

  • env.evaluate()
  • aggregate_mean, geometric_mean, variance, aav, rmsv, stdev
  • aggregate_quantile, aggregate_cvar, aggregate_min, aggregate_max
  • aggregate_relative_frequency_geq, _leq, _eq
%pip install -q seekerdemo
import seekerdemo as skr

env = skr.Env('', True)
order = env.ordinal(0, 300)
demand = env.normal(150, 40, 0, 1e9, 500)
profit = 6 * env.min([order, demand]) - 2.5 * order
stats = {
    "aggregate_mean": env.aggregate_mean(profit),
    "aggregate_stdev": env.aggregate_stdev(profit),
    "aggregate_variance": env.aggregate_variance(profit),
    "aggregate_geometric_mean": env.aggregate_geometric_mean(env.max_0(profit) + 1),
    "aggregate_aav": env.aggregate_aav(profit),
    "aggregate_rmsv": env.aggregate_rmsv(profit),
    "aggregate_min / _max": [env.aggregate_min(profit), env.aggregate_max(profit)],
    "aggregate_quantile 10%": env.aggregate_quantile(profit, 0.1, False),
    "aggregate_cvar worst 10%": env.aggregate_cvar(profit, 0.1, False),
    "P(profit >= 300)": env.aggregate_relative_frequency_geq(profit, 300.0),
    "P(profit <= 0)": env.aggregate_relative_frequency_leq(profit, 0.0),
    "P(demand == 0)": env.aggregate_relative_frequency_eq(env.round(demand), 0.0),
}
order.set_value(150)
env.evaluate()                                 # aggregates hold dummy values until evaluated
print("order 150:", round(stats["aggregate_mean"].get_value(), 1))
env.maximize(stats["aggregate_mean"] + 0.5 * stats["aggregate_cvar worst 10%"], 2)
print(f"best order {order.get_value():.0f}")
for k, t in stats.items():
    vals = [round(x.get_value(), 2) for x in (t if isinstance(t, list) else [t])]
    print(f"  {k:26}", vals if len(vals) > 1 else vals[0])
env.end()
order 150: 428.2
best order 137
  aggregate_mean             416.81
  aggregate_stdev            112.66
  aggregate_variance         12691.94
  aggregate_geometric_mean   382.63
  aggregate_aav              418.12
  aggregate_rmsv             431.77
  aggregate_min / _max       [-236.88, 479.5]
  aggregate_quantile 10%     239.11
  aggregate_cvar worst 10%   144.23
  P(profit >= 300)           0.84
  P(profit <= 0)             0.01
  P(demand == 0)             0.0

2.23 Constraints

Create constraints (2.23.1), combine them (2.23.2), post them, or create and post in one call (2.23.3). Nothing is enforced until posted. A named constraint is reported if it cannot be met. 2.23.4 gives a term that tells whether all are met.

  • env.leq, lt, geq, gt, eq, neq
  • env.and_([constraints]), env.or_([constraints])
  • env.enforce(constraint, name)
  • env.enforce_leq, _lt, _geq, _gt, _eq, _neq(a, b, name)
  • env.fulfills_all_constraints_posted_so_far()
%pip install -q seekerdemo
import seekerdemo as skr

env = skr.Env('')
h = [env.ordinal(0, 10) for _ in range(3)]           # hours per machine
out = 30 * h[0] + 45 * h[1] + 20 * h[2]
cost = 25 * h[0] + 50 * h[1] + 18 * h[2]
# constraint objects: leq, lt, geq, gt, eq, neq ... nothing happens until enforced
night = env.leq(h[0] + h[1] + h[2], 20)
for x in h:                                          # composition: off, or at least 4 hours
    env.enforce(env.or_([env.eq(x, 0), env.geq(x, 4)]), "minimum run")
env.enforce(env.and_([night, env.neq(h[0], h[1])]), "shift rules")
env.enforce_geq(out, 400, "demand")                  # create and post in one call
env.enforce_lt(h[2], 8); env.enforce_gt(h[0] + h[1], 0)
env.enforce_leq(h[1], 9); env.enforce_eq(h[2] % 2, 0); env.enforce_neq(h[0], 7)
ok = env.fulfills_all_constraints_posted_so_far()
env.minimize(cost, 1)
print("hours", [x.get_value() for x in h], " output", out.get_value(), " cost", cost.get_value(),
      " all constraints met:", ok.get_value(), " constraints:", env.get_number_constraints())
env.end()
hours [10.0, 0.0, 6.0]  output 420.0  cost 358.0  all constraints met: 1.0  constraints: 10

2.24 Optimization functions

Minimize or maximize for a time limit, or for a step limit with reproducible results (_det). An optional bound stops the search early. Several objectives use fair and excellent values and optional weights.

  • env.minimize(obj, seconds[, bound]), maximize
  • env.minimize_det(obj, steps[, bound]), maximize_det
  • env.multi_objective(objs, fair, excellent, maximize[, weights], seconds)
  • env.multi_objective_det(…, steps)
%pip install -q seekerdemo
import seekerdemo as skr

def model():
    env = skr.Env('')
    x = [env.continuous(-5, 5) for _ in range(3)]
    f = env.sum([env.sqr(v - 1.5) for v in x])
    g = env.sum([env.abs(v + 1) for v in x])
    return env, x, f, g

env, x, f, g = model()
best = env.minimize(f, 1, 0.001)               # stop early once f <= 0.001
print("minimize with bound:", round(best, 4), env.get_status()); env.end()

env, x, f, g = model()
env.set_report(1e9)                             # no progress lines
print("maximize_det (5000 steps, reproducible):", round(env.maximize_det(-f, 5000), 4)); env.end()

env, x, f, g = model()
env.multi_objective([f, g], [10.0, 10.0], [1.0, 1.0], [False, False], 1)
print("multi_objective: f", round(f.get_value(), 2), " g", round(g.get_value(), 2)); env.end()

env, x, f, g = model()                          # weights: after both are fair, g matters 4x more
env.set_report(1e9)
env.multi_objective_det([f, g], [10.0, 10.0], [1.0, 1.0], [False, False], [1.0, 4.0], 5000)
print("multi_objective_det, weighted: f", round(f.get_value(), 2), " g", round(g.get_value(), 2)); env.end()
minimize with bound: 0.0 StatusType.bounded
maximize_det (5000 steps, reproducible): -0.0
multi_objective: f 4.03  g 4.03
multi_objective_det, weighted: f 2.24  g 4.91

2.25 Model updates between optimizations

Change the model between solves: remove constraints, freeze terms at chosen values, release them again.

  • env.unenforce(constraint)
  • env.freeze_term(t), freeze_terms(list)
  • env.unfreeze_term(t), unfreeze_terms(list)
  • term.set_value(v)
%pip install -q seekerdemo
import seekerdemo as skr

env = skr.Env('')
x = [env.ordinal(0, 10) for _ in range(3)]
price = env.convert(4.0)
profit = price * env.sum(x) - env.sum([env.sqr(v) for v in x])
cap = env.leq(env.sum(x), 4)
env.enforce(cap)
env.maximize(profit, 1)
print("with cap:     ", [v.get_value() for v in x], profit.get_value())
env.unenforce(cap)                               # remove the constraint
env.freeze_term(x[0]); x[0].set_value(1)         # fix x0 at 1 for the next run
env.maximize(profit, 1)
print("x0 frozen at 1:", [v.get_value() for v in x], profit.get_value())
env.unfreeze_terms([x[0]])                       # x0 is free again (freeze_terms / unfreeze_term exist too)
env.maximize(profit, 1)
print("all free:     ", [v.get_value() for v in x], profit.get_value())
env.end()
with cap:      [2.0, 1.0, 1.0] 10.0
x0 frozen at 1: [1.0, 2.0, 2.0] 11.0
all free:      [2.0, 2.0, 2.0] 12.0

2.26 Dynamic variable blocks and priorities

Guide the search: block variables while a condition holds, and give important variables more attention.

  • env.variable_block(var, condition)
  • env.variable_priority(var, priority)
%pip install -q seekerdemo
import seekerdemo as skr

env = skr.Env('')
open_ = env.ordinal(0, 1)                         # is the extra warehouse open?
size = env.ordinal(0, 50)
routes = [env.ordinal(0, 9) for _ in range(4)]
env.variable_block(size, open_ == 0)              # no point searching the size while closed
for i, r in enumerate(routes):
    env.variable_priority(r, env.convert(float(4 - i)))   # earlier routes are changed more often
gain = 4 * size * open_ - env.if_(open_ == 1, 60, 0) - 0.04 * env.sqr(size)        - env.sum([env.sqr(r - 5) for r in routes])
env.maximize(gain, 1)
print("open", open_.get_value(), " size", size.get_value(), " routes", [r.get_value() for r in routes])
env.end()
open 1.0  size 50.0  routes [5.0, 5.0, 5.0, 5.0]

2.27 Version, status, progress reports, and search statistics

Progress lines during the search, the final status (2.27.1) and counters.

  • env.version()
  • env.set_report(seconds[, terms, names])
  • env.get_status() → skr.StatusType
  • env.get_number_evaluations(), get_number_variables(), get_number_constraints()
%pip install -q seekerdemo
import seekerdemo as skr

env = skr.Env('')
print("version:", env.version(), " status before solving:", env.get_status())
x = [env.continuous(0, 10) for _ in range(5)]
f = env.sum([env.sqr(v - i) for i, v in enumerate(x)])
env.enforce_leq(env.sum(x), 8)
env.set_report(0.5, [f, x[0]], ["f", "x0"])      # progress line every 0.5 s with these terms
env.minimize(f, 1)
print("status:", env.get_status(), "| is timeout:", env.get_status() == skr.StatusType.timeout)
print("variables:", env.get_number_variables(), " constraints:", env.get_number_constraints(),
      " evaluations:", env.get_number_evaluations() > 0)
env.end()
version: InsideOpt Seeker Version 3.0.0  status before solving: StatusType.unoptimized
At time 0.500045: Objective = 1.000001; Status = Feasible; Evaluations = 319567
	f = 1.000001; x0 = 0.000000; 
status: StatusType.timeout | is timeout: True
variables: 5  constraints: 1  evaluations: True

2.28 Assurance suite

Tools to test, monitor and understand a model: test data (2.28.1), callbacks with search information (2.28.2 to 2.28.4), trade-off exploration (2.28.5, 2.28.6), and saving and restoring solutions (2.28.7).

  • env.correlated_scenarios(terms, n), env.test_set(terms)
  • env.callback(obj), skr.Callback.callback(info), skr.Info
  • env.get_best_progression()
  • env.strategic_exploration(objs, maximize, seconds[, bounds, fair, excellent, names]), skr.KPIType
  • env.get_solution(), env.set_solution(solution)
%pip install -q seekerdemo
import seekerdemo as skr

class Monitor(skr.Callback):                      # called during the search
    def __init__(self):
        super().__init__(); self.calls = 0; self.best = None
    def callback(self, info):
        self.calls += 1
        self.best = (info.get_current_best_value(), info.get_current_best_infeasibility(),
                     info.get_number_steps() > 0)

env = skr.Env('')
x = [env.continuous(0, 10) for _ in range(4)]
f = env.sum([env.sqr(v - 2 * i) for i, v in enumerate(x)]) + env.abs(x[0], x[3])
mon = Monitor(); env.callback(mon)                 # keep a reference while solving
env.minimize(f, 1)
times, infeas, values = env.get_best_progression()
print("callback calls > 0:", mon.calls > 0, " last best (value, infeasibility, steps>0):",
      tuple(round(v, 3) if isinstance(v, float) else v for v in mon.best))
print("improvements found:", len(values), " first/last objective:", round(values[0], 2), round(values[-1], 2))
saved = env.get_solution()                         # save the best solution ...
for v in x: v.set_value(0)
env.evaluate(); print("after resetting:", round(f.get_value(), 2))
env.set_solution(saved); env.evaluate()            # ... and restore it
print("restored:", round(f.get_value(), 2))
# test_set: meaningful samples of terms, to unit-test the model logic
print("test samples (x0, x3, f):", [[round(v, 2) for v in s] for s in env.test_set([x[0], x[3], f])][:3])
env.end()
callback calls > 0: True  last best (value, infeasibility, steps>0): (5.5, 0.0, True)
improvements found: 16  first/last objective: 56.0 5.5
after resetting: 56.0
restored: 5.5
test samples (x0, x3, f): [[0.0, 0.0, 52.36], [0.0, 0.0, 38.02], [0.0, 2.76, 13.28]]
%pip install -q seekerdemo
import seekerdemo as skr

# Strategic exploration: a set of good trade-offs between cost and delivery time.
env = skr.Env('')
speed = [env.continuous(1, 5) for _ in range(3)]
cost = env.sum([env.sqr(s) * 10 for s in speed])
time = env.sum([12 / s for s in speed])
env.set_report(1e9)                            # no progress lines
sols = env.strategic_exploration([cost, time], [False, False], 3,
                                 [skr.KPIType.fully_ranged, skr.KPIType.not_ranged],
                                 [300.0, 0.0], [60.0, 0.0], ["cost", "time"])
print("solutions found:", len(sols) > 0)
seen = set()
for s in sols:
    env.set_solution(s); env.evaluate()
    seen.add((round(cost.get_value()), round(time.get_value(), 1)))
for c, t in sorted(seen)[:6]:
    print(f"  cost {c:4d}  time {t:5.1f}")
env.end()

******************************************************************

Strategic Exploration Started

Starting Optimization of Individual KPIs

Optimizing cost
Final Value: 30

Optimizing time
At time 0.250091: Objective = 11.384320; Status = Feasible; Evaluations = 94667
	time = 11.384320; 
At time 0.500243: Objective = 11.384232; Status = Feasible; Evaluations = 172247
	time = 11.384232; 
Final Value: 11.3842

------------------------------

Starting Min-Max Regret Optimization
At time 0.250219: Objective = 0.739076; Status = Feasible; Evaluations = 354818
	cost = 122.621809; time = 17.807069; 
At time 0.500403: Objective = 0.739087; Status = Feasible; Evaluations = 408799
	cost = 122.619002; time = 17.806792; 

Min Max Regret KPIs
cost -> min: Final Value: 122.619
time -> min: Final Value: 17.8068

------------------------------

Starting Multi-Objective Optimization
At time 0.250272: Objective = 799.394796; Status = Feasible; Evaluations = 498957
	cost = 122.618980; time = 17.806793; 
At time 0.501242: Objective = 799.394796; Status = Feasible; Evaluations = 519162
	cost = 122.618980; time = 17.806793; 

Multi-Objective KPIs
cost -> min: Final Value: 122.619
time -> min: Final Value: 17.8068

******************************************************************

solutions found: True
  cost   30  time  36.0
  cost  123  time  17.8
  cost  300  time  11.4

2.29 Parameters

Fine-tune the search. Change these with care, and best with measurements.

  • env.set_local_improvement_size(s)
  • env.set_global_improvement_size(s)
  • env.set_exploration_size(s, t)
  • env.set_restart_likelihood(p)
  • env.set_parallel_coordination_parameters(interval, initialWait)
%pip install -q seekerdemo
import seekerdemo as skr

env = skr.Env('')
x = [env.continuous(-10, 10) for _ in range(6)]
rastrigin = env.sum([env.sqr(v) - 10 * env.cos(6.2832 * v) + 10 for v in x])   # many local optima
env.set_local_improvement_size(0.3)            # neighborhood for local improvement, in [0, 1]
env.set_global_improvement_size(0.6)           # neighborhood for recombination, in [0, 1]
env.set_exploration_size(0.2, 0.7)             # exploration neighborhoods s < t in [0, 1]
env.set_restart_likelihood(0.005)              # 0.01 is already high
env.set_parallel_coordination_parameters(2.0, 1.0)   # for parallel runs: interval, initial wait (s)
env.minimize(rastrigin, 2, 0.0)
print("best value:", round(rastrigin.get_value(), 4), " x:", [round(v.get_value(), 3) for v in x])
env.end()
best value: 0.0  x: [-0.0, 0.0, 0.0, 0.0, -0.0, 0.0]

3 The Term Class

3.1 Construction

Terms come from the environment: variables, converted data, and functions of other terms. Ordinary operators build new terms.

  • y + 1 ≡ env.sum([y, env.convert(1)])
%pip install -q seekerdemo
import seekerdemo as skr

env = skr.Env('')
y = env.ordinal(0, 10)
a = y + 1                                       # implicit notation ...
b = env.sum([y, env.convert(1)])                # ... means exactly this
c = 2 * y - 3 / (y + 1)                         # numbers mix freely with terms
env.minimize(env.abs(c, 7), 1)
print("y", y.get_value(), " a", a.get_value(), " b", b.get_value(), " c", round(c.get_value(), 3))
env.end()
y 4.0  a 5.0  b 5.0  c 7.4

3.2 Unary operators

Negation and logical not. In Python use t.not_(); the keyword not does not work on terms.

  • -t
  • t.not_()
%pip install -q seekerdemo
import seekerdemo as skr

env = skr.Env('')
a = env.categorical(0, 1, 1)
x = env.convert(2.5)
neg = -x                                       # negation
not_a = a.not_()                               # logical not (Python's "not a" is not allowed)
env.evaluate()
print("-x =", neg.get_value(), "  a =", a.get_value(), "  a.not_() =", not_a.get_value())
env.end()
-x = -2.5   a = 1.0   a.not_() = 0.0

3.3 Binary operators

Arithmetic (3.3.1) and comparisons (3.3.2) give terms. Comparisons are 0/1 terms, not constraints. Use env.and_ and env.or_ instead of Python's and/or. Comparisons with decimals use a small slack, so guard functions like sqrt accordingly.

  • + - * / % ^
  • == != <= >= < >
  • env.and_([...]), env.or_([...])
%pip install -q seekerdemo
import seekerdemo as skr

env = skr.Env('')
a, b = env.convert(7.0), env.convert(2.0)
arith = {"a + b": a + b, "a - b": a - b, "a * b": a * b, "a / b": a / b, "a % b": a % b, "a ^ b": a ^ b}
logic = {"a == b": a == b, "a != b": a != b, "a <= b": a <= b, "a >= b": a >= b, "a < b": a < b, "a > b": a > b}
both = {"or_": env.or_([a > b, a < b]), "and_": env.and_([a > b, a < b])}   # instead of Python's or / and
z = env.convert(-0.0000000001)
safe = env.if_(z < 0, z, env.sqrt(env.max_0(z)))   # comparisons use slack: guard sqrt with max_0
env.evaluate()
for d in (arith, logic, both):
    print({k: round(t.get_value(), 6) for k, t in d.items()})
print("guarded sqrt:", safe.get_value())
env.end()
{'a + b': 9.0, 'a - b': 5.0, 'a * b': 14.0, 'a / b': 3.5, 'a % b': 1.0, 'a ^ b': 49.0}
{'a == b': 0.0, 'a != b': 1.0, 'a <= b': 0.0, 'a >= b': 1.0, 'a < b': 0.0, 'a > b': 1.0}
{'or_': 1.0, 'and_': 0.0}
guarded sqrt: 0.0

3.4 Term values and status check

Read values after solving, one per scenario for random terms, check whether a term could be evaluated, and set values directly.

  • t.get_value()
  • t.get_values()
  • t.status_ok()
  • t.set_value(v[, evaluate])
%pip install -q seekerdemo
import seekerdemo as skr

env = skr.Env('', True)
q = env.ordinal(0, 20)
d = env.discrete_uniform(5, 15, 50)
short = env.max_0(d - q)                        # stochastic: one value per scenario
cost = env.aggregate_mean(2 * q + 5 * short)
bad = env.sqrt(q - 30)                          # undefined: square root of a negative number
env.minimize(cost, 1)
print("q =", q.get_value(), " expected cost =", round(cost.get_value(), 2))
print("shortage in the first 5 scenarios:", short.get_values()[:5])
print("status_ok: cost", cost.status_ok(), " bad", bad.status_ok())
q.set_value(10)                                 # set and update everything that depends on q
print("cost with q = 10:", round(cost.get_value(), 2))
q.set_value(12, False)                          # set without updating dependents
print("q =", q.get_value(), " cost still", round(cost.get_value(), 2))
env.end()
q = 11.0  expected cost = 26.64
shortage in the first 5 scenarios: [0.0, 0.0, 1.0, 2.0, 0.0]
status_ok: cost True  bad False
cost with q = 10: 26.92
q = 12.0  cost still 26.92