1 Commits
scip ... master

Author SHA1 Message Date
Margot Sauter
d329748b89 Weekend 19 2024-11-10 19:28:37 +01:00
6 changed files with 291 additions and 251 deletions

View File

@@ -8,38 +8,33 @@ config:
task_re: "[ ,/]+" task_re: "[ ,/]+"
tasks: tasks:
hotemetoten: hotemetoten:
personen: [lynn, Sjors, Peter] personen: [Lyra, mrngm, Pepper]
workload: 4 workload: 4
req: [1, 1, 1] req: [1, 1, 1]
hardcode: hardcode:
- lynn - Lyra
- Sjors - mrngm
- Peter - Pepper
lookup: [hotemetoten] lookup: [hotemetoten]
superkok: superkok:
req: [1, 1, 1] req: [1, 1, 1]
personen: [Loki, Bas, PP] personen: [MacGyver, Annelies, Mieksies]
workload: 4 workload: 4
lookup: [superkok] lookup: [superkok]
hardcode: hardcode:
- Loki - MacGyver
- Bas - Annelies
- PP - Mieksies
snijden: snijden:
req: [4, 2, 3] req: [2, 3, 3]
workload: 2 workload: 2
personen: iedereen personen: iedereen
lookup: [snijden] lookup: [snijden]
koken: koken:
req: [2, 0, 3] req: [3, 3, 3]
workload: 3 workload: 3
personen: liefhebbers personen: liefhebbers
lookup: [koken, kookhulp, hulpkoken] lookup: [koken, kookhulp, hulpkoken]
baskoken:
req: [0, 2, 0]
workload: 3
personen: liefhebbers
lookup: [baskoken]
afwassen: afwassen:
req: [3, 6, 6] req: [3, 6, 6]
workload: 2 workload: 2
@@ -66,45 +61,42 @@ tasks:
personen: liefhebbers personen: liefhebbers
lookup: [pendelen] lookup: [pendelen]
people: people:
# dusk en syntaxterror ontbreken ivm. overspannen - Abel
- Peter - Anita
- Sjors - Annelies
- Blondie
- carrot
- Dusk
- Eliza
- Harm
- John
- Joost
- Leonie
- lynn - lynn
- Dennis - Loki
- yorick
- Nova
- lucus - lucus
- Lyra - Lyra
- carrot - Maaike
- Annelies - Mabl
- Minnozz
- Quis
- Loki
- Anita
- MacGyver - MacGyver
- PP - mapzie
- Mieksies
- Harm
- PaxSum
- Hanne
- Abel
- Tanja
- Wassasin
- Blu
- Marlon
- Joost
- mrngm
- Weasel
- Pepper
- Rian
- avel
- Bas
- Blondie
- Margot - Margot
- Mike
- Yana
- Merel
- Sigi
- Carrie
- Marion - Marion
- John - Marlon
- Mieksies
- Minnozz
- MrNGm
- Nova
- PaxSum
- Pepper
- petervdv
- Quis
- Rian
- Roflincopter
- Sjors
- SyntaxTerror
- Tanja
- Thom
- Wassasin
- Weasel
- Yorick

22
glpm.py Normal file
View File

@@ -0,0 +1,22 @@
def col_all_zeros(ls, i, al=0):
return all(x[i] == al for x in ls)
def row_all_zeros(ls, i, al=0):
return all(x == al for x in ls[i])
def matrix(name, ls, default=0):
nonzero_cols = [i+1 for i in range(len(ls[0])) if not col_all_zeros(ls, i, default)]
nonzero_rows = [i+1 for i in range(len(ls)) if not row_all_zeros(ls, i, default)]
res = ""
for r in nonzero_rows:
res += "\n{:2d}".format(r)
for c in nonzero_cols:
res += " {:2d}".format(ls[r-1][c-1])
return param(name, res, " : " + " ".join("{:2d}".format(x) for x in nonzero_cols))
def dict(name, thing, default=None):
fmt_key = lambda k: " ".join((str(x+1) for x in k)) if type(k) == tuple else k+1
return param(name, ", ".join(["{} {}".format(fmt_key(k), v) for k,v in thing.items() if v != default]))
def param(name, val, middle=""):
val = str(val)
if "\n" in val:
val = val.replace("\n", "\n" + " " * (len(name) + 6))
return "param {}{} := {};".format(name, middle, val)

View File

@@ -1,83 +1,61 @@
#! /usr/bin/env nix-shell #! /usr/bin/env nix-shell
#!nix-shell -i python3 -p python3 python3Packages.pyyaml -I nixpkgs=flake:nixpkgs #!nix-shell -i python3 -p python3 python3Packages.pyyaml glpk -I nixpkgs=flake:nixpkgs
# pip install pyyaml pyscipopt pyright
import sys import sys
import yaml import yaml
import re import re
import argparse import subprocess
from collections import OrderedDict, defaultdict from collections import OrderedDict
from pyscipopt import Model, quicksum import glpm
from typing import Any, Tuple, TypeVar
from dataclasses import dataclass, field
from tabulate import tabulate
conf = yaml.safe_load(open('config.yaml', 'r')) conf = yaml.safe_load(open('config.yaml', 'r'))
config = conf['config']
DEFAULT_CONFIG = {
"max_load_person": 6,
"day_names": [f"dag {str(i)}" for i in range(conf['config']['days'])],
}
config = DEFAULT_CONFIG | conf['config']
config['ignore'].append('') config['ignore'].append('')
assert(len(config['day_names']) == config['days']) tasks = OrderedDict(conf['tasks'])
index = lambda x: {v:k for k,v in enumerate(x)}
QUADRATIC = False
@dataclass
class TaskConfig:
personen: list[str]
workload: int
req: list[int]
name: str
hardcode: list[str] | None = None
lookup: list[str] = field(default_factory=list)
tasks: dict[str, TaskConfig] = OrderedDict({k: TaskConfig(**({"name": k} | t)) for k, t in conf['tasks'].items()})
X = TypeVar("X")
def index(x: dict[X, Any]) -> dict[X, int]:
return {v: k for k, v in enumerate(x)}
daily_workloads = \ daily_workloads = \
[sum(task.workload * task.req[d] for task in tasks.values()) for d in range(config['days'])] [sum(task['workload'] * task['req'][d] for task in tasks.values()) for d in range(config['days'])]
ALL_DAYS: set[int] = set(range(config['days'])) ALL_DAYS = set(range(config['days']))
class Person(object): class Person(object):
def __init__(self, name: str, conf={"dagen":ALL_DAYS}): def __init__(self, conf={"dagen":ALL_DAYS}):
self.name = name self.can = set()
self.can: set[str] = set() self.loves = set()
self.loves: set[str] = set() self.hates = set()
self.hates: set[str] = set() self.does = set() # hardcoded
self.does: set[Tuple[int, str]] = set() # hardcoded
self.has_prefs = False self.has_prefs = False
self.conf = conf self.conf = conf
self.conf['dagen'] = set(conf['dagen']) self.conf['dagen'] = set(conf['dagen'])
def vrolijkheid(self): def vrolijkheid(self):
res = config['days'] - len(self.does) res = config['days'] - len(self.does)
for (_,t) in self.does: for (d,t) in self.does:
if t in self.loves: if t in self.loves:
res += config['weights']['likes'] res += config['weights']['likes']
if t in self.hates: if t in self.hates:
res -= config['weights']['hates'] res -= config['weights']['hates']
return res return res
def workload(self, tasks): def workload(self, tasks):
return sum(tasks[t].workload for (_,t) in self.does) return sum(tasks[t]['workload'] for (d,t) in self.does)
def cost(self, num_people): def cost(self, num_people):
return round(sum((daily_workloads[d] for d in self.conf['dagen'])) / num_people) return round(sum((daily_workloads[d] for d in self.conf['dagen'])) / num_people)
# probabilistic round: int(math.floor(x + random.random())) # probabilistic round: int(math.floor(x + random.random()))
def read_people(conf_ppl) -> dict[str, Person]: def read_people(conf_ppl):
def isdict(x):
return type(x) == dict
people = OrderedDict() people = OrderedDict()
for x in conf_ppl: for x in conf_ppl:
val = {"dagen": ALL_DAYS} val = {"dagen": ALL_DAYS}
if isinstance(x, dict): if type(x) == dict:
x,val = x.popitem() x,val = x.popitem()
people[x.lower()] = Person(x, val) people[x.lower()] = Person(val)
return people return people
# deal with loves/hates # deal with loves/hates
def make_task_lut(tasks: dict[str, TaskConfig]): def make_task_lut(tasks):
task_lut = defaultdict(set) task_lut = {}
for t, taskconf in tasks.items(): for t, taskconf in tasks.items():
for lookup in taskconf.lookup: if 'lookup' in taskconf:
task_lut[lookup] |= {t.lower()} for lookup in taskconf['lookup']:
task_lut[lookup] = t
task_re = re.compile(config['task_re']) task_re = re.compile(config['task_re'])
def lookup_tasks(tasks): def lookup_tasks(tasks):
return set.union(set(), *(task_lut[x.strip()] for x in task_re.split(tasks) if x not in config['ignore'])) return (task_lut[x] for x in task_re.split(tasks) if not x in config['ignore'])
return lookup_tasks return lookup_tasks
def read_prefs(pref_file, tasks, people): def read_prefs(pref_file, tasks, people):
lookup_tasks = make_task_lut(tasks) lookup_tasks = make_task_lut(tasks)
@@ -91,137 +69,103 @@ def read_prefs(pref_file, tasks, people):
if not p.has_prefs: if not p.has_prefs:
print("warning: no preferences for", name, file=sys.stderr) print("warning: no preferences for", name, file=sys.stderr)
# deal with capability and hardcode # deal with capability and hardcode
def set_capabilities(tasks: dict[str, TaskConfig], people: dict[str, Person]): def set_capabilities(tasks, people):
for (task,conf) in tasks.items(): for ti,(task,conf) in enumerate(tasks.items()):
if conf.personen == 'iedereen': if conf['personen'] == 'iedereen':
for p in people.values(): for p in people.values():
p.can.add(task) p.can.add(task)
elif conf.personen == 'liefhebbers': elif conf['personen'] == 'liefhebbers':
for p in people.values(): for p in people.values():
if task in p.loves: if task in p.loves:
p.can.add(task) p.can.add(task)
else: else:
for p in conf.personen: for p in conf['personen']:
people[p.lower()].can.add(task) people[p.lower()].can.add(task)
if conf.hardcode is not None: if 'hardcode' in conf:
for day, pers in enumerate(conf.hardcode): for day, pers in enumerate(conf['hardcode']):
if pers:
people[pers.lower()].does.add((day, task)) people[pers.lower()].does.add((day, task))
# format as matrices
def write_tasks(people: dict[str, Person], tasks: dict[str, TaskConfig], file=sys.stdout): def matrices(people, tasks):
headers = ["wie"] + config['day_names'] mat = lambda a,b: [[0 for j in b] for i in a]
if not args.simple: loves = mat(people, tasks)
headers += ["workload", "vrolijkheid"] hates = mat(people, tasks)
tabl = [] capab = mat(people, tasks)
for p in people.values(): tsk_idx = index(tasks)
days = [[] for _ in range(config['days'])] hardcode = {}
for (d,t) in p.does: max_loads = {}
days[d].append((t, t in p.loves, t in p.hates)) costs = {}
if not args.simple: for i,(person, p) in enumerate(people.items()):
def q(w): for t in p.loves: loves[i][tsk_idx[t]] = 1
return ",".join([tasks[t].name + (" :)" if love else "") + (" :(" if hate else "") for (t,love,hate) in w]) for t in p.hates: hates[i][tsk_idx[t]] = 1
else: for t in p.can: capab[i][tsk_idx[t]] = 1
def q(w): for (d,t) in p.does: hardcode[(i,tsk_idx[t],d)] = 1
return ",".join([tasks[t].name for (t,_,_) in w])
row = [p.name, *map(q, days)]
if not args.simple:
row += [p.workload(tasks), p.vrolijkheid()]
tabl.append(row)
print(tabulate(tabl, headers=headers, tablefmt=args.output_format), file=file)
def scipsol(people: dict[str, Person], tasks: dict[str, TaskConfig]):
max_loads: dict[Tuple[str, int], int] = {}
for i,p in people.items():
if 'max_load' in p.conf: # max_load override for Pol if 'max_load' in p.conf: # max_load override for Pol
for d,load in enumerate(p.conf['max_load']): for d,l in enumerate(p.conf['max_load']):
max_loads[(i,d)] = load max_loads[(i,d)] = l
# filter days that the person does not exist # filter days that the person does not exist
for d in ALL_DAYS - p.conf['dagen']: for d in ALL_DAYS - p.conf['dagen']:
max_loads[(i,d)] = 0 max_loads[(i,d)] = 0
costs[i] = p.cost(len(people))
req = mat(range(config['days']), tasks)
for di in range(config['days']):
for ti,t in enumerate(tasks.values()):
req[di][ti] = t['req'][di]
workload = {tsk_idx[t]: tasks[t]['workload'] for t in tasks}
return [loves, hates, capab, hardcode, max_loads, req, workload, costs]
def read_assignment(file, people, tasks):
def between_the_lines(f, a=">>>>\n", b="<<<<\n"):
for l in f:
if l == a: break
for l in f:
if l == b: break
yield map(int, l.strip().split())
m = Model() for p in people.values():
does = {} p.does = set()
happiness = [] person_vl = list(people.values())
stdevs = [] task_nm = list(tasks.keys())
errors = [] for [p,d,j,W,l] in between_the_lines(file):
for pname, person in people.items(): person_vl[p-1].does.add((d-1, task_nm[j-1]))
workloads = [] def write_data(people, tasks, file=sys.stdout):
p_error = m.addVar(vtype="I", name=f"{pname}_error", lb=0, ub=None) [loves, hates, capab, hardcode, max_loads, req, workload, costs] = matrices(people, tasks)
for d in ALL_DAYS: print(glpm.matrix("L", loves), file=file)
pdt = [] print(glpm.matrix("H", hates), file=file)
for task in tasks: print(glpm.matrix("C", capab, 1), file=file)
var = m.addVar(vtype="B", name=f"{pname}_does_{task}@{d}") print(glpm.matrix("R", req, None), file=file)
pdt.append(var) print(glpm.dict("Q", hardcode), file=file)
does[(pname, d, task)] = var print(glpm.dict("Wl", workload), file=file)
# a person only does what (s)he is capable of print(glpm.dict("max_load", max_loads), file=file)
if task not in person.can: print(glpm.dict("Costs", costs), file=file)
m.addCons(var == 0) print(glpm.param("D_count", config['days']), file=file)
workloads.append(var * tasks[task].workload) print(glpm.param("P_count", len(people)), file=file)
for task in person.loves: print(glpm.param("J_count", len(tasks)), file=file)
happiness.append(does[(pname, d, task)] * config['weights']['likes']) print(glpm.param("ML", 6), file=file) # CHANGE THIS
for task in person.hates: print(glpm.param("WL", config['weights']['likes']), file=file)
happiness.append(does[(pname, d, task)] * (config['weights']['hates'] * -1)) print(glpm.param("WH", config['weights']['hates']), file=file)
def write_tasks(people, tasks, file=sys.stdout):
# max_load_person: a person only has one task per day at most for name, p in people.items():
m.addCons(quicksum(pdt) <= max_loads.get((pname, d), 1)) days = [[] for i in range(config['days'])]
for (d,t) in p.does:
m.addCons(p_error >= person.cost(len(people)) - quicksum(workloads)) days[d].append((t, t in p.loves, t in p.hates))
m.addCons(p_error >= quicksum(workloads) - person.cost(len(people))) q = lambda w: ",".join([t + (" <3" if l else "") + (" :(" if h else "") for (t,l,h) in w])
errors.append(p_error) days_fmt = " {} ||" * len(days)
days_filled = days_fmt.format(*map(q, days))
stdevs.append((person.cost(len(people)) - quicksum(workloads)) ** 2) print("| {} ||{} {} || {}".format(name, days_filled, p.vrolijkheid(), p.workload(tasks)), file=file)
print("|-")
# min_load_person: person has at least 1 task
m.addCons(quicksum([does[(pname, d, task)] for d in ALL_DAYS for task in tasks]) >= 1)
# duplicate_jobs: a person does not perform the same job on all days
for task in tasks:
m.addCons(quicksum(does[(pname, d, task)] for d in ALL_DAYS) <= len(ALL_DAYS) - 1)
# max_load_person_total
m.addCons(quicksum([does[(pname, d, task)] * tasks[task].workload for d in ALL_DAYS for task in tasks]) <= config['max_load_person'])
# hardcode constraint
for d, task in person.does:
m.addCons(does[(pname, d, task)] == 1)
# all_allocated: each task in allocated
for j in tasks:
for d in ALL_DAYS:
m.addCons(quicksum(does[(p, d, j)] for p in people) == tasks[j].req[d])
objective = m.addVar(name="objvar", vtype="C", lb=None, ub=None)
m.setObjective(objective, "maximize")
if QUADRATIC:
m.addCons(objective <= quicksum(happiness) - quicksum(stdevs) / 2)
else:
if args.max_total_error is not None:
m.addCons(quicksum(errors) <= args.max_total_error)
m.addCons(objective <= quicksum(happiness) - quicksum(errors))
m.setObjective(objective, "maximize")
m.solveConcurrent()
for pname, person in people.items():
for d in ALL_DAYS:
for task in tasks:
if m.getVal(does[(pname, d, task)]):
person.does.add((d, task))
write_tasks(people, tasks)
print("Totale vrolijkheid:", sum(p.vrolijkheid() for p in people.values()))
print("workload deviation:", m.getVal(quicksum(stdevs)))
parser = argparse.ArgumentParser()
parser.add_argument("-q", "--quadratic", action="store_true")
parser.add_argument("--simple", action="store_true", help="hide workload and happiness")
parser.add_argument("--max_total_error", type=int, default=None)
parser.add_argument("--output-format", default="mediawiki", help="`tabulate` output format")
args = parser.parse_args()
QUADRATIC = args.quadratic
people = read_people(conf['people']) people = read_people(conf['people'])
with open('prefs_table', 'r') as pref_file: with open('prefs_table', 'r') as pref_file:
read_prefs(pref_file, tasks, people) read_prefs(pref_file, tasks, people)
set_capabilities(tasks, people) set_capabilities(tasks, people)
scipsol(people, tasks) if len(sys.argv)>1 and sys.argv[1] == 'in':
write_data(people, tasks)
elif len(sys.argv)>1 and sys.argv[1] == 'out':
with open('output', 'r') as out_file:
read_assignment(out_file, people, tasks)
write_tasks(people, tasks)
else:
with open('data', 'w') as out:
write_data(people, tasks, file=out)
subprocess.call(["glpsol", "--model", "model.glpm", "-d", "data", "--tmlim", "15", "--log", "output"], stdout=sys.stderr, stderr=sys.stdout)
with open('output', 'r') as file:
read_assignment(file, people, tasks)
write_tasks(people, tasks)

84
model.glpm Normal file
View File

@@ -0,0 +1,84 @@
/* Number of people */
param P_count, integer, > 0;
/* Number of jobs */
param J_count, integer, > 0;
/* Number of days */
param D_count, integer, > 0;
param WL, integer, > 0;
param WH, integer, > 0;
param ML, integer, > 0;
set P := 1..P_count;
set J := 1..J_count;
set D := 1..D_count;
/* aanwezigheid x workload for that day */
param Costs{p in P}, integer, >= 0;
/* Person p likes to solve jobs j */
param L{p in P, j in J} default 0, binary;
/* Person p hates to solve jobs j */
param H{p in P, j in J} default 0, binary;
/* Person p is capable to perform job j */
param C{p in P, j in J} default 1, binary;
/* How many jobs need to be done on what day */
param R{d in D, j in J}, integer, >= 0;
/* hardcoded */
param Q{p in P, j in J, d in D}, default 0, binary;
/* workload */
param Wl{j in J}, integer, >= 0;
param max_load{p in P, d in D}, default 1, integer;
/* Person p is allocated to do job j on day d */
var A{p in P, j in J, d in D}, binary;
var error{p in P}, integer, >= 0;
s.t. hardcode{p in P, j in J, d in D}: A[p,j,d] >= Q[p,j,d];
/* A person only has one task per day, at most */
s.t. max_load_person{p in P, d in D}: sum{j in J} A[p,j,d] <= max_load[p,d];
/* A person has at least 1 task */
s.t. min_load_person{p in P}: sum{j in J, d in D} A[p,j,d] >= 1;
/* A person does not perform the same job on all days */
/* s.t. duplicate_jobs{p in P, j in J}: sum{d in D} A[p,j,d] <= D_count-1; */
s.t. max_load_person_total{p in P}: (sum{d in D, j in J} A[p,j,d] * Wl[j]) <= ML;
/* Each task is allocated */
s.t. all_allocated{j in J, d in D}: sum{p in P} A[p,j,d] == R[d, j];
/* A person only performs what (s)he is capable of */
s.t. capability_person{p in P, j in J, d in D}: A[p,j,d] <= C[p,j];
s.t. error_lt{p in P}: error[p] >= ((sum{j in J, d in D} A[p,j,d] * Wl[j]) - Costs[p]);
s.t. error_gt{p in P}: error[p] >= Costs[p] - (sum{j in J, d in D} A[p,j,d] * Wl[j]);
/* Maximize enjoyment */
# minimize error_diff: sum{p in P} error[p];
maximize enjoyment: (sum{p in P, d in D, j in J} A[p,j,d] * (L[p, j] * WL - H[p, j] * WH)) - sum{p in P} error[p];
solve;
printf "Sum %d\n", (sum{p in P, d in D, j in J} A[p,j,d] * (L[p, j] * WL - H[p, j] * WH));
printf "p d j W l\n";
printf ">>>>\n";
printf{p in P, d in D, j in J : A[p,j,d] > 0} "%d %d %d %d %d\n", p, d, j, A[p,j,d] * (L[p, j] * WL - H[p, j] * WH), Wl[j];
printf "<<<<\n";
printf "workloads\n";
printf "p l\n";
printf{p in P} "%d %d\n", p, abs((sum{j in J, d in D : A[p,j,d] > 0} Wl[j]) - Costs[p]);
printf "workload_dev: %d\n", sum{p in P} abs((sum{j in J, d in D : A[p,j,d] > 0} Wl[j]) - Costs[p])^2;
end;

View File

@@ -1,30 +1,31 @@
Quis snijden, koken, snackdealen schoonmaken, baskoken, fotograferen Abel - pendelen
Annelies afwassen, fotograferen snackdealen Anita schoonmaken, afwassen fotograferen, pendelen
MacGyver koken, afwassen fotograferen, snijden, baskoken, schoonmaken carrot koken, snijden -
Joost snijden, koken schoonmaken, snackdealen, fotograferen, baskoken Dusk pendelen, snijden, koken afwassen, schoonmaken
Anita afwassen Eliza schoonmaken, koken, snijden snackdealen, pendelen, afwassen
Dennis schoonmaken, afwassen koken, baskoken, snackdealen Harm pendelen, fotograferen snackdealen, schoonmaken
bas koken, snijden, pendelen schoonmaken Joost snijden fotograferen, pendelen, schoonmaken, snackdealen
lucus snijden, koken, baskoken schoonmaken Leonie koken, snijden, pendelen snackdealen, fotograferen
Tanja snijden, koken, afwassen, schoonmaken fotograferen Loki snijden, koken, snackdealen afwassen, schoonmaken
Lyra schoonmaken, afwassen koken, baskoken lucus koken, snijden schoonmaken, snackdealen
PaxSum snijden, koken, baskoken schoonmaken, afwassen, snackdealen lynn koken, snackdealen, schoonmaken afwassen, snijden, koken
Merel snackdealen, schoonmaken Maaike snijden, koken, afwassen snackdealen, fotograferen, pendelen
Abel snijden, afwassen, schoonmaken baskoken Mabl snijden, schoonmaken fotograferen, pendelen, snackdealen
Weasel fotograferen, snijden baskoken, snackdealen, schoonmaken mapzie snackdealen snijden, koken, schoonmaken, afwassen
Minnozz fotograferen, snijden afwassen, schoonmaken Margot snijden, koken, afwassen snackdealen, schoonmaken
Yorick snijden, koken schoonmaken Marion snijden, koken, afwassen snackdealen, fotograferen, schoonmaken
Harm pendelen, fotograferen schoonmaken, snackdealen Marlon koken, snijden afwassen, schoonmaken
Mieksies schoonmaken, snackdealen koken, baskoken, fotograferen Minnozz fotograferen, snijden schoonmaken, afwassen
avel snackdealen fotograferen, koken, schoonmaken, afwassen, pendelen, baskoken Nova pendelen, koken, snijden snackdealen, fotograferen, schoonmaken
Carrie snackdealen, pendelen, snijden schoonmaken, afwassen, pendelen, koken, baskoken PaxSum snijden, koken, afwassen fotograferen, snackdealen
carrot snijden, koken, baskoken schoonmaken petervdv snackdealen, pendelen -
MrNGm pendelen, snijden, afwassen, schoonmaken snackdealen Quis snackdealen, koken, snijden fotograferen, schoonmaken
Nova pendelen, koken, snijden schoonmaken, afwassen, snackdealen Rian snijden, afwassen snackdealen, fotograferen, pendelen
Hanne schoonmaken, snijden snackdealen, afwassen, pendelen Roflincopter schoonmaken, afwassen koken
Mike snijden, snackdealen, koken schoonmaken Sjors pendelen, snackdealen koken, afwassen
Marion snijden, koken schoonmaken, fotograferen, snackdealen SyntaxTerror snijden, schoonmaken, afwassen snackdealen, pendelen
Blondie fotograferen, snijden, afwassen snackdealen, baskoken, schoonmaken, pendelen Tanja snijden, koken, afwassen, schoonmaken fotograferen, pendelen
Blu afwassen, snijden, koken, pendelen schoonmaken, baskoken, fotograferen Thom koken, afwassen, pendelen fotograferen, snackdealen
John afwassen, snijden pendelen, fotograferen Wassasin snackdealen, koken, snijden afwassen, schoonmaken, fotograferen
Margot afwassen, schoonmaken baskoken, koken, pendelen, fotograferen Weasel fotograferen, snijden schoonmaken, snackdealen
Yorick koken, snijden, afwassen snackdealen, fotograferen

View File

@@ -1,3 +0,0 @@
tabulate~=0.9.0
PySCIPOpt~=4.4.0
PyYAML~=6.0.1