Seeker Templates

Complete models for common kinds of problems, written the way Seeker works best. Every template has the same layout: a data block at the top that you replace, then the search variables, the derived terms, the objective and constraints, and the results. Pick the template closest to your problem, run it once, then swap in your data.

All templates run on the free demo in a few seconds. New to Seeker? Do the Quick start first; look up any function in the Modeling toolkit.

Overview

  • Assigning and grouping
    • Assign tasks to resources: Jobs to technicians, orders to plants, shifts to people, patients to wards, workloads to servers.
    • Split into groups of equal size: Project teams, classes, tasting panels, production batches with fixed size, balanced shipments.
    • Choose what to do, and when: Project portfolios, marketing campaigns, maintenance jobs per period, orders to accept, features per release.
  • Sequencing and scheduling
    • Sequence jobs on one resource: A machine, oven, paint line, operating room, test rig or crew that does one job at a time.
    • Schedule on parallel resources: Several machines, crews, docks, lanes or servers doing jobs in parallel, possibly at different speeds.
    • Place tasks in time under a shared limit: Maintenance windows, charging sessions, deliveries at docks, batch runs sharing power, staff-heavy tasks.
  • Routing
    • Routes for several vehicles: Deliveries, pickups, service visits, field sales, school buses, anything leaving and returning to a depot.
  • Amounts, shares and prices
    • Mix shares that add up to 100%: Feed, fuel, alloy and food blends; investment portfolios; splitting demand across suppliers or channels.
    • Distribute a fixed number of units: Staff across stores, ambulances across districts, sales reps across regions, budget blocks across channels.
    • Set prices for competing products: Product lines, subscription tiers, menus, ticket classes, anything where customers choose between your offers.
  • Uncertainty and simulation
    • Decide before demand is known: Fresh production, stock for an event, staffing, capacity reservations.
    • Tune a process you can simulate: Staffing levels, reorder policies, buffer sizes, control parameters: anywhere a simulation already exists.

Assigning and grouping

Assign tasks to resources

Use it for: Jobs to technicians, orders to plants, shifts to people, patients to wards, workloads to servers.

  • Search variables: One categorical per task: which resource. Eligibility (who can do what) goes into the allowed values.
  • Derived terms: Workload per resource with switch conditions; cost per task by looking up a cost table.
  • Objective: Two goals with multi_objective: total cost (overtime included) and the busiest resource.
%pip install -q seekerdemo "setuptools<81"
# TEMPLATE: Assign tasks to resources (people, machines, sites)
# Search variables: one categorical per task = which resource does it.
# Derived terms:    workload per resource (switch conditions), cost per task (tensor lookup).
# Objective:        two goals at once with multi_objective: cost and the busiest resource.
import random, seekerdemo as skr

# ---- your data -------------------------------------------------------------
random.seed(1)
resources = ["Ana", "Ben", "Chen", "Dita"]
tasks     = [f"job {j}" for j in range(14)]
hours     = [random.choice([1, 1.5, 2, 3]) for _ in tasks]            # per task
cost      = [[random.randint(20, 90) for _ in resources] for _ in tasks]  # task x resource
can_do    = [[r for r in range(4) if random.random() < .7] or [0] for _ in tasks]
DAY = 8.0                                                               # hours per resource

# ---- model ------------------------------------------------------------------
env = skr.Env('')
who = [env.categorical(0, len(resources) - 1, can_do[j]) for j in range(len(tasks))]

COST = env.tensor(cost)
total_cost = env.sum([env.index([env.convert(j), who[j]], COST) for j in range(len(tasks))])
is_on = [env.switch_condition(w, len(resources)) for w in who]           # "who == r?"
load = [env.sum_if(env.convert(hours), is_on, r) for r in range(len(resources))]
overtime = env.sum([env.max_0(l - DAY) for l in load])
busiest = env.max(load)

#   objectives,      fair (acceptable), excellent,  maximize?
env.multi_objective([total_cost + 100 * overtime, busiest],
                    [900, 8.0], [500, 6.0], [False, False], 3)

# ---- results ----------------------------------------------------------------
for r, name in enumerate(resources):
    mine = [tasks[j] for j in range(len(tasks)) if round(who[j].get_value()) == r]
    print(f"{name:5} {load[r].get_value():4.1f} h  {', '.join(mine)}")
print(f"cost {total_cost.get_value():.0f}, overtime {overtime.get_value():.1f} h, "
      f"busiest {busiest.get_value():.1f} h")
env.end()
Ana    7.5 h  job 0, job 6, job 12
Ben    7.0 h  job 3, job 5, job 7
Chen   7.5 h  job 2, job 8, job 9, job 10
Dita   8.0 h  job 1, job 4, job 11, job 13
cost 714, overtime 0.0 h, busiest 8.0 h

Make it yours

  • Replace the data block with your tasks, resources, hours, costs and eligibility.
  • Change DAY to each resource's capacity (or make it a list).
  • Prefer a single goal? Replace multi_objective by env.minimize(total_cost + 100 * overtime, 3).

Split into groups of equal size

Use it for: Project teams, classes, tasting panels, production batches with fixed size, balanced shipments.

  • Search variables: One permutation; consecutive blocks of positions form the groups, so sizes are equal by construction.
  • Derived terms: Totals of each attribute per group (tensor lookups) and each person's group (via the inverse permutation).
  • Objective: Groups as alike as possible; pairs that must not share a group are penalized.
%pip install -q seekerdemo "setuptools<81"
# TEMPLATE: Split people or items into groups of equal size
# Search variables: one permutation; positions 0..5 form group 0, 6..11 group 1, ...
#                   Equal sizes are guaranteed by construction, no constraints needed.
# Derived terms:    per-group totals of each attribute (tensor lookups).
# Objective:        groups as similar as possible; keep listed pairs apart.
import random, seekerdemo as skr

# ---- your data -------------------------------------------------------------
random.seed(4)
people = [f"P{i:02d}" for i in range(24)]
GROUPS, SIZE = 4, 6
attrs = [[random.randint(1, 9), random.randint(1, 9), random.randint(0, 1)]   # skill, years, senior
         for _ in people]
keep_apart = [(2, 9), (5, 17), (11, 12)]

# ---- model ------------------------------------------------------------------
env = skr.Env('')
p = env.permutation(len(people))
seat, where = p.get_permutation(), p.get_permutation_inverse()   # seat[k] = person at position k
A = env.tensor(attrs)
targets = [sum(a[d] for a in attrs) / GROUPS for d in range(3)]

spread = 0
for g in range(GROUPS):
    members = seat[g * SIZE:(g + 1) * SIZE]
    for d in range(3):
        total = env.sum([env.index([m, env.convert(d)], A) for m in members])
        spread = spread + env.sqr(total - targets[d])
group_of = [env.floor(where[i] / SIZE) for i in range(len(people))]
clashes = env.sum([group_of[a] == group_of[b] for a, b in keep_apart])

env.minimize(spread + 100 * clashes, 3)

# ---- results ----------------------------------------------------------------
for g in range(GROUPS):
    ids = [int(s.get_value()) for s in seat[g * SIZE:(g + 1) * SIZE]]
    sums = [sum(attrs[i][d] for i in ids) for d in range(3)]
    print(f"group {g}: {' '.join(people[i] for i in ids)}  skill {sums[0]}, years {sums[1]}, seniors {sums[2]}")
print(f"spread {spread.get_value():.1f}, clashes {clashes.get_value():.0f}")
env.end()
group 0: P05 P08 P19 P18 P01 P16  skill 30, years 29, seniors 3
group 1: P15 P12 P06 P10 P17 P04  skill 30, years 29, seniors 3
group 2: P02 P11 P14 P21 P03 P20  skill 29, years 29, seniors 3
group 3: P22 P23 P00 P13 P07 P09  skill 30, years 29, seniors 3
spread 0.8, clashes 0

Make it yours

  • Put your attributes in attrs (any number of columns) and set GROUPS and SIZE.
  • Unequal group sizes: cut the permutation at your own block boundaries.
  • Pairs that must stay together: penalize group_of[a] != group_of[b] instead.

Choose what to do, and when

Use it for: Project portfolios, marketing campaigns, maintenance jobs per period, orders to accept, features per release.

  • Search variables: One packing: each item goes into one of the periods or into the extra set "not chosen".
  • Derived terms: Spend per period with sum_if; value per item depending on its period.
  • Objective: Total value, reduced for every period of delay; a budget per period.
%pip install -q seekerdemo "setuptools<81"
# TEMPLATE: Choose which items to do, and when (projects, campaigns, orders)
# Search variables: a packing: each project goes into one of the quarters, or into
#                   the extra set "not funded". No yes/no variables needed.
# Derived terms:    spend per quarter (sum_if over the packing), value per project.
# Objective:        total value, with value lost for every quarter of delay.
import random, seekerdemo as skr

# ---- your data -------------------------------------------------------------
random.seed(6)
projects = [f"proj {j}" for j in range(14)]
cost     = [random.randint(20, 90) for _ in projects]        # EUR k
value    = [round(c * random.uniform(1.2, 2.5)) for c in cost]
Q, BUDGET, DELAY_LOSS = 4, 150, 0.12                          # quarters, EUR k per quarter, per quarter

# ---- model ------------------------------------------------------------------
env = skr.Env('')
plan = env.packing(Q, len(projects))                          # value Q means "not funded"
when = plan.get_variables()
spend = [env.sum_if(env.convert([float(c) for c in cost]), plan, q) for q in range(Q)]
for q in range(Q):
    env.enforce_leq(spend[q], BUDGET, f"budget Q{q + 1}")
gain = env.sum([env.if_(when[j] < Q, value[j] * (1 - DELAY_LOSS * when[j]), 0)
                for j in range(len(projects))])
env.maximize(gain, 3)

# ---- results ----------------------------------------------------------------
for q in range(Q):
    chosen = [projects[j] for j in range(len(projects)) if round(when[j].get_value()) == q]
    print(f"Q{q + 1}: spend {spend[q].get_value():3.0f}  {', '.join(chosen)}")
print("not funded:", [projects[j] for j in range(len(projects)) if round(when[j].get_value()) == Q])
print(f"total value {gain.get_value():.0f}")
env.end()
Q1: spend 146  proj 0, proj 3, proj 5, proj 10
Q2: spend 147  proj 6, proj 7
Q3: spend 147  proj 1, proj 4, proj 12
Q4: spend 147  proj 2, proj 9, proj 13
not funded: ['proj 8', 'proj 11']
total value 947

Make it yours

  • Replace costs, values, the number of periods, budget and delay loss.
  • No timing, only selection: use env.packing(1, n); set 1 means not chosen.
  • Several resources per period (money and people): add one sum_if and one limit per resource.

Sequencing and scheduling

Sequence jobs on one resource

Use it for: A machine, oven, paint line, operating room, test rig or crew that does one job at a time.

  • Search variables: One permutation: the order. Start times are not variables.
  • Derived terms: Earliest start and end times, setup between consecutive jobs (sequence-dependent), lateness.
  • Objective: Weighted lateness plus setup time.
%pip install -q seekerdemo "setuptools<81"
# TEMPLATE: Sequence jobs on one resource (machine, oven, crew, operating room)
# Search variables: one permutation = the order of the jobs.
# Derived terms:    start and end times (earliest possible), setups between
#                   consecutive jobs, lateness. These are the times users want.
# Objective:        weighted lateness + setup time.
import random, seekerdemo as skr

# ---- your data -------------------------------------------------------------
random.seed(2)
N = 15
ready  = [random.randint(0, 120) for _ in range(N)]          # earliest start (min)
dur    = [random.randint(15, 60) for _ in range(N)]
due    = [r + d + random.randint(30, 300) for r, d in zip(ready, dur)]
weight = [random.choice([1, 2, 5]) for _ in range(N)]
family = [random.randrange(3) for _ in range(N)]
setup  = [[0 if family[a] == family[b] else 20 for b in range(N)] for a in range(N)]

# ---- model ------------------------------------------------------------------
env = skr.Env('')
order = env.permutation(N).get_permutation()                 # order[k] = job in position k
R, D, U, W = (env.tensor(x) for x in (ready, dur, due, weight))
S = env.tensor(setup)

t, late, setups, start = env.convert(0), [], [], []
for k in range(N):
    job = order[k]
    if k > 0:
        s = env.index([order[k - 1], job], S)
        setups.append(s)
        t = t + s
    t = env.max([t, env.index([job], R)])                    # wait until the job is ready
    start.append(t)
    t = t + env.index([job], D)                              # end time
    late.append(env.index([job], W) * env.max_0(t - env.index([job], U)))
lateness, setup_time = env.sum(late), env.sum(setups)
env.minimize(lateness + setup_time, 3)

# ---- results ----------------------------------------------------------------
for k in range(N):
    j = int(order[k].get_value())
    print(f"{start[k].get_value():5.0f}  job {j:2d}  (family {family[j]}, due {due[j]})")
print(f"weighted lateness {lateness.get_value():.0f}, setup {setup_time.get_value():.0f} min")
env.end()
    7  job  2  (family 1, due 240)
   24  job  7  (family 1, due 375)
   79  job 12  (family 1, due 124)
  122  job 13  (family 1, due 199)
  169  job 11  (family 1, due 306)
  238  job  5  (family 2, due 295)
  263  job 14  (family 2, due 305)
  295  job  9  (family 2, due 270)
  342  job  6  (family 2, due 394)
  404  job  3  (family 0, due 331)
  456  job 10  (family 0, due 273)
  514  job  0  (family 1, due 186)
  542  job  4  (family 1, due 261)
  600  job  1  (family 1, due 205)
  673  job  8  (family 2, due 248)
weighted lateness 3158, setup 80 min

Make it yours

  • Replace ready times, durations, due dates, weights and the setup matrix.
  • Other goals reuse the same times: makespan (t after the loop), total completion time, energy by time of day.
  • Jobs that must come before others: create the permutation with precedences.

Schedule on parallel resources

Use it for: Several machines, crews, docks, lanes or servers doing jobs in parallel, possibly at different speeds.

  • Search variables: One permutation: the priority order. The search never has to choose machines directly.
  • Derived terms: A list-scheduling decoder: each job goes to the resource that is free first. A canvas holds the free times and is updated at the chosen resource (left-indexing).
  • Objective: Finish early (makespan), with weighted completion times as a tie-breaker.
%pip install -q seekerdemo "setuptools<81"
# TEMPLATE: Schedule jobs on several parallel resources (machines, crews, lanes)
# Search variables: one permutation = the priority order of the jobs.
# Derived terms:    a list-scheduling decoder: each job goes to the resource that
#                   becomes free first. A canvas holds the free times; the job's end
#                   time is written back at the chosen resource (left-indexing).
# Objective:        finish everything early, and favor important jobs.
import random, seekerdemo as skr

# ---- your data -------------------------------------------------------------
random.seed(3)
N, M = 16, 3
dur    = [random.randint(10, 60) for _ in range(N)]     # minutes on a standard resource
weight = [random.choice([1, 3]) for _ in range(N)]
speed  = [1.0, 1.0, 1.5]                                # resource 2 is 50% faster

# ---- model ------------------------------------------------------------------
env = skr.Env('')
order = env.permutation(N).get_permutation()
DUR, WGT, SPD = env.tensor(dur), env.tensor(weight), env.tensor(speed)

free = env.canvas([0.0] * M)                            # when each resource is free
ends, where = [], []
for k in range(N):
    job = order[k]
    m = env.argmin(free.to_1d())                        # resource that is free first
    start = env.index([m], free)
    end = start + env.index([job], DUR) / env.index([m], SPD)
    free = env.assignment(free, [[m]], [end])           # write the new free time at m
    ends.append(env.index([job], WGT) * end)
    where.append(m)
makespan = env.max(free.to_1d())
weighted_end = env.sum(ends)
env.minimize(makespan + weighted_end / 1000, 3)         # small term breaks ties

# ---- results ----------------------------------------------------------------
for m in range(M):
    jobs = [int(order[k].get_value()) for k in range(N) if round(where[k].get_value()) == m]
    print(f"resource {m}: jobs {jobs}")
print(f"all done after {makespan.get_value():.1f} min")
env.end()
resource 0: jobs [11, 14, 0, 4, 8]
resource 1: jobs [15, 9, 1, 13, 10]
resource 2: jobs [3, 12, 2, 7, 6, 5]
all done after 160.0 min

Make it yours

  • Set dur, weight and speed; any number of resources.
  • Resource-dependent durations: replace the division by a 2-D tensor lookup [job, m].
  • Prefer explicit control? Add a categorical machine choice per job as in the canvas toolkit example.

Place tasks in time under a shared limit

Use it for: Maintenance windows, charging sessions, deliveries at docks, batch runs sharing power, staff-heavy tasks.

  • Search variables: A start slot per task.
  • Derived terms: Use of the shared resource in every slot with interval conditions.
  • Objective: Low peak use, close to each task's preferred time.
%pip install -q seekerdemo "setuptools<81"
# TEMPLATE: Place tasks in time with a shared resource limit (crews, power, docks)
# Search variables: a start slot per task.
# Derived terms:    resource use per slot via interval conditions: task i uses the
#                   resource in slot t if its start lies in [t - duration + 1, t].
# Objective:        peak use + distance from each task's preferred start.
import random, seekerdemo as skr

# ---- your data -------------------------------------------------------------
random.seed(9)
SLOTS = 24                                                   # hours of the day
tasks  = [f"task {i}" for i in range(14)]
dur    = [random.randint(1, 4) for _ in tasks]               # hours
crew   = [random.randint(1, 3) for _ in tasks]               # people needed
prefer = [random.randint(6, 16) for _ in tasks]              # preferred start hour

# ---- model ------------------------------------------------------------------
env = skr.Env('')
start = [env.ordinal(0, SLOTS - d) for d in dur]             # task ends by midnight
cond = [env.interval_condition(s) for s in start]
people = env.convert([float(c) for c in crew])
use = [env.sum_if(people, cond, [t - d + 1 for d in dur], [t] * len(tasks)) for t in range(SLOTS)]
peak = env.max(use)
shift = env.sum([env.abs(start[i], prefer[i]) for i in range(len(tasks))])
env.minimize(10 * peak + shift, 3)

# ---- results ----------------------------------------------------------------
print("start hours:", [int(s.get_value()) for s in start])
print("people per hour:", [int(u.get_value()) for u in use])
print(f"peak {peak.get_value():.0f} people, total shift from preferred {shift.get_value():.0f} h")
env.end()
start hours: [2, 10, 9, 16, 9, 12, 7, 10, 9, 13, 10, 14, 6, 5]
people per hour: [0, 0, 3, 3, 3, 6, 5, 6, 4, 6, 6, 4, 4, 4, 6, 6, 4, 4, 0, 0, 0, 0, 0, 0]
peak 6 people, total shift from preferred 13 h

Make it yours

  • Replace durations, resource needs and preferred starts; change SLOTS to your horizon.
  • Hard limit instead of a low peak: env.enforce_leq(u, LIMIT) for each slot, or penalize the excess.
  • Time-dependent prices (energy): add sum_if terms weighted by the slot price.

Routing

Routes for several vehicles

Use it for: Deliveries, pickups, service visits, field sales, school buses, anything leaving and returning to a depot.

  • Search variables: One permutation over customers plus separator tokens that split it into one route per vehicle.
  • Derived terms: Distance by looking up a distance table; the load along each route, reset at every separator.
  • Objective: Distance plus a penalty for overloading a vehicle.
%pip install -q seekerdemo "setuptools<81"
# TEMPLATE: Routes for several vehicles from one depot
# Search variables: one permutation over customers plus (vehicles - 1) separator
#                   tokens. The tokens cut the sequence into one route per vehicle
#                   and stand for "back to the depot". No assignment variables needed.
# Derived terms:    travel distance (tensor lookups), load along each route.
# Objective:        distance + penalty for overloading a vehicle.
import math, random, seekerdemo as skr

# ---- your data -------------------------------------------------------------
random.seed(5)
N, V, CAP = 15, 3, 60                                   # customers, vehicles, capacity
depot = (50, 50)
xy = [(random.randint(0, 100), random.randint(0, 100)) for _ in range(N)]
demand = [random.randint(5, 20) for _ in range(N)]

# ---- model ------------------------------------------------------------------
nodes = xy + [depot] * (V - 1)                          # separators sit at the depot
dem = demand + [0] * (V - 1)
sep = [0] * N + [1] * (V - 1)
K = len(nodes)
dist = [[math.dist(a, b) for b in nodes] for a in nodes]
from_depot = [math.dist(depot, a) for a in nodes]

env = skr.Env('')
tour = env.permutation(K).get_permutation()
DIST, DEP, DEM, SEP = env.tensor(dist), env.tensor(from_depot), env.tensor(dem), env.tensor(sep)

km = env.index([tour[0]], DEP) + env.index([tour[K - 1]], DEP)   # leave and return to the depot
load, overload = env.convert(0), 0
for k in range(K):
    if k > 0:
        km = km + env.index([tour[k - 1], tour[k]], DIST)
    is_sep = env.index([tour[k]], SEP)
    load = env.if_(is_sep, 0, load + env.index([tour[k]], DEM))   # reset at each separator
    overload = overload + env.max_0(load - CAP)
env.minimize(km + 50 * overload, 4)

# ---- results ----------------------------------------------------------------
routes, cur = [], []
for k in range(K):
    i = int(tour[k].get_value())
    if i >= N: routes.append(cur); cur = []
    else: cur.append(i)
routes.append(cur)
for v, r in enumerate(routes):
    print(f"vehicle {v}: depot -> {r} -> depot, load {sum(demand[i] for i in r)}")
print(f"distance {km.get_value():.1f}, overload {overload.get_value():.0f}")
env.end()
vehicle 0: depot -> [14, 7, 10, 6, 11] -> depot, load 50
vehicle 1: depot -> [8, 12, 4, 13, 9] -> depot, load 59
vehicle 2: depot -> [0, 5, 1, 3, 2] -> depot, load 52
distance 499.9, overload 0

Make it yours

  • Replace coordinates and demands, or fill dist with real road distances or travel times.
  • Different vehicle capacities: look up the capacity of the current route by counting separators passed.
  • Time windows: carry an arrival time along the route like the load, and penalize late arrivals.

Amounts, shares and prices

Mix shares that add up to 100%

Use it for: Feed, fuel, alloy and food blends; investment portfolios; splitting demand across suppliers or channels.

  • Search variables: One convex combination: the share of each ingredient.
  • Derived terms: Properties of the mix, including one that blends nonlinearly (a power mean).
  • Objective: Cost per tonne plus penalties for leaving the specification.
%pip install -q seekerdemo "setuptools<81"
# TEMPLATE: Mix shares that add up to 100% (blends, portfolios, sourcing splits)
# Search variables: one convex combination = the share of each ingredient.
#                   Shares are >= 0 and sum to 1 by construction.
# Derived terms:    properties of the mix; here one property mixes nonlinearly.
# Objective:        cost per tonne + penalties for missing the specification.
import seekerdemo as skr

# ---- your data -------------------------------------------------------------
names   = ["maize", "wheat", "soy meal", "barley", "sunflower", "fish meal"]
price   = [210, 240, 420, 190, 300, 1100]        # EUR per tonne
protein = [8.5, 11.5, 46.0, 10.5, 33.0, 64.0]    # %
fiber   = [2.2, 2.6, 6.0, 5.0, 23.0, 0.5]        # %
hardness= [6.0, 7.5, 3.0, 5.0, 2.5, 1.0]         # pellet quality index
spec = {"protein": (18.0, 21.0), "fiber": (0.0, 6.0), "hardness": (5.5, 99)}

# ---- model ------------------------------------------------------------------
env = skr.Env('')
share = env.convex_combination(len(names)).get_combination()
mix = lambda col: env.scalar_product(share, env.convert([float(v) for v in col]))
props = {"protein": mix(protein), "fiber": mix(fiber),
         # pellet hardness blends nonlinearly (a power mean), not as a weighted average
         "hardness": env.power(mix([h ** 3 for h in hardness]), 1 / 3)}
miss = env.sum([env.max_0(lo - props[k]) + env.max_0(props[k] - hi)
                for k, (lo, hi) in spec.items()])
cost = mix(price)
env.minimize(cost + 1000 * miss, 3)

# ---- results ----------------------------------------------------------------
for n, s in zip(names, share):
    print(f"{n:10} {100 * s.get_value():5.1f} %")
print({k: round(v.get_value(), 2) for k, v in props.items()}, f"cost EUR {cost.get_value():.1f}/t")
env.end()
maize        0.0 %
wheat       21.9 %
soy meal    15.7 %
barley      54.8 %
sunflower    7.6 %
fish meal    0.0 %
{'protein': 18.0, 'fiber': 6.0, 'hardness': 5.5} cost EUR 245.4/t

Make it yours

  • Replace ingredients, prices, properties and the specification ranges.
  • Any property formula works: averages, power means, interaction terms, look-up curves.
  • Minimum or maximum share of an ingredient: add it to the penalty, or enforce it.

Distribute a fixed number of units

Use it for: Staff across stores, ambulances across districts, sales reps across regions, budget blocks across channels.

  • Search variables: One split total: whole numbers per area that add up to the total.
  • Derived terms: Service per area with diminishing returns (each extra unit helps less).
  • Objective: Fewest expected missed requests.
%pip install -q seekerdemo "setuptools<81"
# TEMPLATE: Distribute a fixed number of units (staff, vehicles, budget blocks)
# Search variables: one split total = how many units each area gets; the
#                   numbers are whole and add up to the total by construction.
# Derived terms:    service in each area, with diminishing returns per extra unit.
# Objective:        fewest expected missed requests across all areas.
import math, seekerdemo as skr

# ---- your data -------------------------------------------------------------
areas    = ["North", "East", "Center", "South", "West", "Harbor"]
requests = [120, 80, 210, 95, 60, 140]     # expected requests per day
per_unit = [14, 12, 18, 13, 10, 15]        # requests one unit can handle
UNITS = 55

# ---- model ------------------------------------------------------------------
env = skr.Env('')
units = env.split_total(len(areas), UNITS).get_combination()
# handled requests saturate: the 10th unit in an area adds less than the 1st
handled = [requests[a] * (1 - env.exp(-1 * units[a] * per_unit[a] / requests[a]))
           for a in range(len(areas))]
missed = env.sum([requests[a] - handled[a] for a in range(len(areas))])
env.minimize(missed, 3)

# ---- results ----------------------------------------------------------------
for a, name in enumerate(areas):
    print(f"{name:7} {units[a].get_value():3.0f} units, handles {handled[a].get_value():5.1f} of {requests[a]}")
print(f"missed requests per day: {missed.get_value():.1f}")
env.end()
North     9 units, handles  78.0 of 120
East      6 units, handles  47.5 of 80
Center   16 units, handles 156.7 of 210
South     8 units, handles  63.2 of 95
West      5 units, handles  33.9 of 60
Harbor   11 units, handles  96.9 of 140
missed requests per day: 228.8

Make it yours

  • Replace areas, request volumes, unit capacity and the total.
  • Use your own response curve, from history or a queueing formula.
  • Allow keeping units in reserve: add one extra area with no effect.

Set prices for competing products

Use it for: Product lines, subscription tiers, menus, ticket classes, anything where customers choose between your offers.

  • Search variables: One ordinal per product. Price endings and price order rules are built into the variables.
  • Derived terms: Sales from a logit choice model with a no-purchase option.
  • Objective: Profit.
%pip install -q seekerdemo "setuptools<81"
# TEMPLATE: Set prices for products that compete with each other
# Search variables: one ordinal per product = price in whole euros; the shown price
#                   ends in .99 by construction. Price order rules are built in too.
# Derived terms:    demand from a logit choice model (customers pick one product or none).
# Objective:        profit.
import seekerdemo as skr

# ---- your data -------------------------------------------------------------
products = ["basic", "plus", "premium", "family pack"]
appeal   = [2.0, 2.6, 3.0, 2.8]          # utility of each product at price 0
cost     = [4.0, 6.0, 9.0, 11.0]          # unit cost
SENS, MARKET = 0.22, 10000                # price sensitivity, customers per month

# ---- model ------------------------------------------------------------------
env = skr.Env('')
basic   = env.ordinal(5, 40)                               # whole euros
plus    = basic + env.ordinal(1, 15)                       # plus costs more than basic
premium = plus + env.ordinal(1, 20)                        # premium more than plus
family  = env.ordinal(10, 60)
price = [p - 0.01 for p in (basic, plus, premium, family)] # ends in .99

utility = [env.exp(appeal[i] - SENS * price[i]) for i in range(4)]
denom = 1 + env.sum(utility)                               # 1 = buy nothing
sales = [MARKET * u / denom for u in utility]
profit = env.sum([sales[i] * (price[i] - cost[i]) for i in range(4)])
env.maximize(profit, 3)

# ---- results ----------------------------------------------------------------
for i, n in enumerate(products):
    print(f"{n:12} EUR {price[i].get_value():6.2f}   sells {sales[i].get_value():5.0f}")
print(f"profit EUR {profit.get_value():,.0f} per month")
env.end()
basic        EUR  13.99   sells  1540
plus         EUR  15.99   sells  1807
premium      EUR  18.99   sells  1393
family pack  EUR  20.99   sells   735
profit EUR 54,698 per month

Make it yours

  • Replace appeal, costs, price sensitivity and market size, ideally estimated from your sales data.
  • Segments: compute the choice model per segment and add up the sales.
  • Any demand model works: linear, constant elasticity, or an ML model as a user-defined term.

Uncertainty and simulation

Decide before demand is known

Use it for: Fresh production, stock for an event, staffing, capacity reservations.

  • Search variables: How much to make of each product.
  • Derived terms: Random demand (sampled), sales, leftovers and profit per sample.
  • Objective: Expected profit, with each product meeting all demand on at least 80% of days (chance constraints).
%pip install -q seekerdemo "setuptools<81"
# TEMPLATE: Decide before demand is known (production, stock, staffing)
# Search variables: how much to make of each product (decided today).
# Derived terms:    random demand per product (sampled), sales, leftovers, profit.
# Objective:        expected profit, with a minimum chance of serving all demand.
import seekerdemo as skr

# ---- your data -------------------------------------------------------------
products = ["sandwiches", "salads", "wraps"]
mean     = [180, 90, 120]              # expected demand tomorrow
stdev    = [45, 30, 35]
price    = [5.5, 7.0, 6.0]
cost     = [2.0, 2.8, 2.2]             # leftovers are thrown away
KITCHEN_MIN = [3, 5, 4]                # minutes per unit
KITCHEN_CAP = 2100                     # minutes available
SERVICE = 0.8                          # each product: serve everyone on 80% of days

# ---- model ------------------------------------------------------------------
env = skr.Env('', True)                                          # stochastic environment
make = [env.ordinal(0, 400) for _ in products]
demand = [env.normal(m, s, 0, 1e9, 300) for m, s in zip(mean, stdev)]
profit = env.sum([price[i] * env.min([make[i], demand[i]]) - cost[i] * make[i]
                  for i in range(len(products))])
env.enforce_leq(env.sum([KITCHEN_MIN[i] * make[i] for i in range(len(products))]), KITCHEN_CAP)
served = [env.aggregate_relative_frequency_geq(make[i] - demand[i], 0) for i in range(len(products))]
for s in served:
    env.enforce_geq(s, SERVICE)                                  # chance constraint
expected = env.aggregate_mean(profit)
env.maximize(expected, 3)

# ---- results ----------------------------------------------------------------
for i, n in enumerate(products):
    print(f"{n:11} make {make[i].get_value():4.0f}  (everyone served on {100 * served[i].get_value():.0f}% of days)")
print(f"expected profit EUR {expected.get_value():.0f}")
env.end()
sandwiches  make  219  (everyone served on 81% of days)
salads      make  115  (everyone served on 81% of days)
wraps       make  151  (everyone served on 82% of days)
expected profit EUR 1170

Make it yours

  • Replace demand distributions with yours, or sample from an ML forecast with user_distribution.
  • Risk-averse? Maximize aggregate_cvar(profit, 0.1, False) or mix it with the mean.
  • Remove the service rule, or make it one overall rule instead of one per product.

Tune a process you can simulate

Use it for: Staffing levels, reorder policies, buffer sizes, control parameters: anywhere a simulation already exists.

  • Search variables: The parameters of the policy, here agents per shift.
  • Derived terms: One user-defined term that runs the simulation in Python.
  • Objective: Staff cost plus the cost of customers waiting too long.
%pip install -q seekerdemo "setuptools<81"
# TEMPLATE: Tune the parameters of a process you can simulate
# Search variables: the policy parameters (here: per-shift staffing of a help desk).
# Derived terms:    one user-defined term that runs your simulation in Python.
# Objective:        staff cost + cost of customers waiting too long.
import random, seekerdemo as skr

# ---- your data -------------------------------------------------------------
random.seed(10)
SHIFTS = ["06-10", "10-14", "14-18", "18-22"]
calls = [[random.expovariate(1 / 7) for _ in range(n)]           # call lengths (min)
         for n in (40, 95, 80, 35)]                              # calls per shift
WAGE, WAIT_COST, PATIENCE = 90, 25.0, 3                          # per agent-shift, per late call, min

def simulate(agents):                     # simple day replay: first free agent takes the call
    late = 0
    for s, n_agents in enumerate(agents):
        free = [0.0] * max(1, n_agents)
        gap = 240 / len(calls[s])
        for c, length in enumerate(calls[s]):
            arrive = c * gap
            i = min(range(len(free)), key=free.__getitem__)
            begin = max(arrive, free[i])
            late += begin - arrive > PATIENCE
            free[i] = begin + length
    return WAGE * sum(agents) + WAIT_COST * late

class DayCost(skr.UserEvalTerm):
    def __init__(self):
        super().__init__()
    def evaluate(self, v):
        return simulate([round(x) for x in v])
    def incremental(self):
        return False

# ---- model ------------------------------------------------------------------
env = skr.Env('')
agents = [env.ordinal(1, 30) for _ in SHIFTS]
sim = DayCost()                                   # keep a reference while solving
cost = env.user_defined_term(agents, sim)
env.minimize(cost, 3)

# ---- results ----------------------------------------------------------------
print("agents per shift:", dict(zip(SHIFTS, [int(a.get_value()) for a in agents])))
print(f"daily cost EUR {cost.get_value():,.0f}  (same staff all day, 4 each: EUR {simulate([4] * 4):,.0f})")
env.end()
agents per shift: {'06-10': 3, '10-14': 4, '14-18': 4, '18-22': 3}
daily cost EUR 1,360  (same staff all day, 4 each: EUR 1,540)

Make it yours

  • Replace simulate by your own simulation; it receives the parameter values.
  • Keep the simulation deterministic (fixed random seed) so Seeker compares like with like.
  • Parameters with an order (S above s): build it in, for example S = s + env.ordinal(1, 100).