magisch-corvee-script/magisch_corvee_script.py

169 lines
6.7 KiB
Python
Raw Normal View History

2018-02-14 16:43:28 +01:00
#! /usr/bin/env nix-shell
#!nix-shell -i python3 -p python python3Packages.pyyaml glpk
import sys
import yaml
import re
import subprocess
from collections import OrderedDict
import glpm
2019-02-18 11:36:35 +01:00
conf = yaml.load(open('config.yaml', 'r'))
config = conf['config']
config['ignore'].append('')
tasks = OrderedDict(conf['tasks'])
index = lambda x: {v:k for k,v in enumerate(x)}
daily_workloads = \
[sum(task['workload'] * task['req'][d] for task in tasks.values()) for d in range(config['days'])]
ALL_DAYS = set(range(config['days']))
2018-02-14 16:43:28 +01:00
class Person(object):
2019-02-18 11:36:35 +01:00
def __init__(self, conf={"dagen":ALL_DAYS}):
2018-02-14 16:43:28 +01:00
self.can = set()
self.loves = set()
self.hates = set()
self.does = set() # hardcoded
self.has_prefs = False
self.conf = conf
2019-02-18 11:36:35 +01:00
self.conf['dagen'] = set(conf['dagen'])
2018-02-14 16:43:28 +01:00
def vrolijkheid(self):
res = config['days'] - len(self.does)
for (d,t) in self.does:
if t in self.loves:
res += config['weights']['likes']
if t in self.hates:
res -= config['weights']['hates']
return res
def workload(self, tasks):
return sum(tasks[t]['workload'] for (d,t) in self.does)
2019-02-18 11:36:35 +01:00
def cost(self, num_people):
return round(sum((daily_workloads[d] for d in self.conf['dagen'])) / num_people)
# probabilistic round: int(math.floor(x + random.random()))
2018-02-14 16:43:28 +01:00
def read_people(conf_ppl):
def isdict(x):
return type(x) == dict
people = OrderedDict()
for x in conf_ppl:
2019-02-18 11:36:35 +01:00
val = {"dagen": ALL_DAYS}
2018-02-14 16:43:28 +01:00
if type(x) == dict:
x,val = x.popitem()
people[x.lower()] = Person(val)
return people
# deal with loves/hates
def make_task_lut(tasks):
task_lut = {}
for t, taskconf in tasks.items():
if 'lookup' in taskconf:
for lookup in taskconf['lookup']:
task_lut[lookup] = t
task_re = re.compile(config['task_re'])
def lookup_tasks(tasks):
return (task_lut[x] for x in task_re.split(tasks) if not x in config['ignore'])
return lookup_tasks
def read_prefs(pref_file, tasks, people):
lookup_tasks = make_task_lut(tasks)
# read the wiki corvee table
for [name, loves, hates] in ((q.strip().lower() for q in x.split('\t')) for x in pref_file):
p = people[name]
p.has_prefs = True
p.loves |= set(lookup_tasks(loves))
p.hates |= set(lookup_tasks(hates))
for name, p in people.items():
if not p.has_prefs:
print("warning: no preferences for", name, file=sys.stderr)
# deal with capability and hardcode
def set_capabilities(tasks, people):
for ti,(task,conf) in enumerate(tasks.items()):
if conf['personen'] == 'iedereen':
for p in people.values():
p.can.add(task)
elif conf['personen'] == 'liefhebbers':
for p in people.values():
if task in p.loves:
p.can.add(task)
else:
for p in conf['personen']:
people[p.lower()].can.add(task)
if 'hardcode' in conf:
for day, pers in enumerate(conf['hardcode']):
people[pers.lower()].does.add((day, task))
# format as matrices
def matrices(people, tasks):
mat = lambda a,b: [[0 for j in b] for i in a]
loves = mat(people, tasks)
hates = mat(people, tasks)
capab = mat(people, tasks)
tsk_idx = index(tasks)
hardcode = {}
max_loads = {}
2019-02-18 11:36:35 +01:00
costs = {}
2018-02-14 16:43:28 +01:00
for i,(person, p) in enumerate(people.items()):
for t in p.loves: loves[i][tsk_idx[t]] = 1
for t in p.hates: hates[i][tsk_idx[t]] = 1
for t in p.can: capab[i][tsk_idx[t]] = 1
for (d,t) in p.does: hardcode[(i,tsk_idx[t],d)] = 1
if 'max_load' in p.conf: # max_load override for Pol
for d,l in enumerate(p.conf['max_load']):
max_loads[(i,d)] = l
2019-02-18 11:36:35 +01:00
# filter days that the person does not exist
for d in ALL_DAYS - p.conf['dagen']:
max_loads[(i,d)] = 0
costs[i] = p.cost(len(people))
2018-02-14 16:43:28 +01:00
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}
2019-02-18 11:36:35 +01:00
return [loves, hates, capab, hardcode, max_loads, req, workload, costs]
2018-02-14 16:43:28 +01:00
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())
for p in people.values():
p.does = set()
person_vl = list(people.values())
task_nm = list(tasks.keys())
for [p,d,j,W,l] in between_the_lines(file):
person_vl[p-1].does.add((d-1, task_nm[j-1]))
def write_data(people, tasks, file=sys.stdout):
2019-02-18 11:36:35 +01:00
[loves, hates, capab, hardcode, max_loads, req, workload, costs] = matrices(people, tasks)
2018-02-14 16:43:28 +01:00
print(glpm.matrix("L", loves), file=file)
print(glpm.matrix("H", hates), file=file)
print(glpm.matrix("C", capab, 1), file=file)
print(glpm.matrix("R", req, None), file=file)
print(glpm.dict("Q", hardcode), file=file)
print(glpm.dict("Wl", workload), file=file)
print(glpm.dict("max_load", max_loads), file=file)
2019-02-18 11:36:35 +01:00
print(glpm.dict("Costs", costs), file=file)
2018-02-14 16:43:28 +01:00
print(glpm.param("D_count", config['days']), file=file)
print(glpm.param("P_count", len(people)), file=file)
print(glpm.param("J_count", len(tasks)), file=file)
print(glpm.param("WL", config['weights']['likes']), file=file)
print(glpm.param("WH", config['weights']['hates']), file=file)
def write_tasks(people, tasks, file=sys.stdout):
for name, p in people.items():
2019-02-18 11:36:35 +01:00
days = [[] for i in range(config['days'])]
2018-02-14 16:43:28 +01:00
for (d,t) in p.does:
2019-02-18 11:36:35 +01:00
days[d].append((t, t in p.loves, t in p.hates))
q = lambda w: ",".join([t + (" <3" if l else "") + (" :(" if h else "") for (t,l,h) in w])
print("| {} || {} || {} || {} || {} || {} || {}".format(name, *map(q, days), p.vrolijkheid(), p.workload(tasks)), file=file)
print("|-")
2018-02-14 16:43:28 +01:00
people = read_people(conf['people'])
with open('prefs_table', 'r') as pref_file:
read_prefs(pref_file, tasks, people)
set_capabilities(tasks, people)
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)
2019-02-18 11:36:35 +01:00
subprocess.call(["glpsol", "--model", "model.glpm", "-d", "data", "--tmlim", "15", "--log", "output"], stdout=sys.stderr, stderr=sys.stdout)
2018-02-14 16:43:28 +01:00
with open('output', 'r') as file:
read_assignment(file, people, tasks)
write_tasks(people, tasks)