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, logmin_0, max_0sin, cosceil, floor, round, truncnorm_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_expdiv, round_div, round_moda % 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, minimumand_, or_mean, variance, sample_variance, standard_deviationnorm1, norm2, geometric_mean, rmsv, average_absolute_valuesum_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_valueenv.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, incrementalUserEvalVector: recompute, inc_recompute, incrementalevaluate_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, coefficientsLP.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, rhsEQSystem.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, triangularcontinuous_exponential, discrete_exponential, bernoullicategorical_distribution, binomial(p, n), poissonnormal, 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, stdevaggregate_quantile, aggregate_cvar, aggregate_min, aggregate_maxaggregate_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, neqenv.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]), maximizeenv.minimize_det(obj, steps[, bound]), maximize_detenv.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.StatusTypeenv.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.Infoenv.get_best_progression()env.strategic_exploration(objs, maximize, seconds[, bounds, fair, excellent, names]), skr.KPITypeenv.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.
-tt.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
