Move /should/ to the optimizing part, and fill that part out

This commit is contained in:
Lars Tveito 2019-11-19 01:59:20 +01:00
parent b6ac1a131f
commit 312b396f3c

View File

@ -254,46 +254,29 @@ cry for help.
return [0 <= comms[c] for c in comms] return [0 <= comms[c] for c in comms]
#+END_SRC #+END_SRC
** Committee constraints
The $S$ relation is sort of odd. That one examiner /should/ form a committee The $S$ relation is sort of odd. That one examiner /should/ form a committee
with someone they relate to by $S$. Let's interpret it as $e_1$ should not with someone they relate to by $S$. This is not an absolute requirement,
form a committee to someone they are not related to by $S$. which is not ideal for a satisfiability problem, so we will ignore this
constraint for now. The $A$ relation is similar, but clearer. For any pair
#+BEGIN_SRC python :tangle committees.py $(i,j) \in A$, we don't form a committee consisting of those examiners.
def should_correct_with_constraint(comms, S, C):
examiners = set(range(len(C)))
return [comms[frozenset([i, j])] == 0
for i in S
for j in examiners - S[i]
if j != i]
#+END_SRC
The $A$ relation is similar, and easier.
#+BEGIN_SRC python :tangle committees.py #+BEGIN_SRC python :tangle committees.py
def avoid_correct_with_constraint(comms, A): def avoid_correct_with_constraint(comms, A):
return [comms[frozenset([i, j])] == 0 for i, j in A] return [comms[frozenset([i, j])] == 0 for i, j in A]
#+END_SRC #+END_SRC
Each committee will correct their exams to times, so if the sum of all the ** All exams are corrected constraint
committees is $N$, then all exams have been corrected twice. Let's encode
that as a constraint. Each committee correct their exams two times (once by each examiner), so if
the sum of all the committees is $N$, then all exams have been corrected
twice (presumably by two different examiners). Let's encode that as a
constraint.
#+BEGIN_SRC python :tangle committees.py #+BEGIN_SRC python :tangle committees.py
def all_corrected_constraint(comms, N): def all_corrected_constraint(comms, N):
return [sum(comms.values()) == N] return sum(comms.values()) == N
#+END_SRC
Let's collect all the constraints in a single list.
#+BEGIN_SRC python :tangle committees.py
def constraints(instance):
N, C, S, A = instance
comms = committees(C)
return (capacity_constraint(comms, C) +
non_negative_constraint(comms) +
all_corrected_constraint(comms, N) +
should_correct_with_constraint(comms, S, C) +
avoid_correct_with_constraint(comms, A))
#+END_SRC #+END_SRC
** Invoking Z3 ** Invoking Z3
@ -302,9 +285,15 @@ cry for help.
#+BEGIN_SRC python :tangle committees.py #+BEGIN_SRC python :tangle committees.py
def check_instance(instance): def check_instance(instance):
N, C, S, A = instance
comms = committees(C)
s = Solver() s = Solver()
s.add(constraints(instance)) s.add(capacity_constraint(comms, C))
s.add(non_negative_constraint(comms))
s.add(all_corrected_constraint(comms, N))
s.add(avoid_correct_with_constraint(comms, A))
s.check() s.check()
return s.model() return s.model()
@ -333,12 +322,17 @@ cry for help.
This is not especially readable, so let's write a quick prettyprinter. This is not especially readable, so let's write a quick prettyprinter.
#+BEGIN_SRC python :tangle committees.py #+BEGIN_SRC python :tangle committees.py
def prettyprint(m): def prettyprint(instance, m):
for k in m: N, C, S, A = instance
cname = k.name()[10:-1] comms = committees(C)
cval = m[k].as_long() exams = [sum(m[comms[c]].as_long() for c in comms if i in c)
if cval > 0: for i in range(len(C))]
print(cname + ':', cval) examiners = '\n'.join(['%d: %d/%d' % (i, exams[i], C[i])
for i in range(len(C))])
cs = [(c, m[comms[c]].as_long()) for c in sorted(comms, key=sorted)]
csstr = '\n'.join([', '.join(map(str, sorted(c))) + ': ' + str(cv)
for c, cv in cs if cv > 0])
print(examiners + '\n\n' + csstr)
#+END_SRC #+END_SRC
This outputs the something like: This outputs the something like:
@ -353,52 +347,145 @@ cry for help.
#+END_EXAMPLE #+END_EXAMPLE
Note the /something like/. There are multiple ways to satisfy this set of Note the /something like/. There are multiple ways to satisfy this set of
constraints, and Z3 only guarantees to provide /some/ solution (if one constraints, and Z3 only provide /some/ solution (if one exists).
exists).
* Optimization * Optimization
So far, we have found a way to model the problem and satisfy all the So far, we have found a way to model the problem and satisfy the constraints.
constraint. However, it is preferable to have fewer committees, because all However, it is preferable to have fewer committees, because all committees
committees have to discuss the exams, causing administrative overhead. Z3 have to discuss the exams, causing administrative overhead. Z3 also provides
also provides optimization, meaning that we can find a smallest or largest optimization, meaning that we can find a smallest or largest solution for
solution for numeric theories. numeric theories. The underlying theory for optimization is MaxSMT.
** Minimize committees ** Minimize committees
In our case, we want to minimize the number of committees. In our case, we want to minimize the number of committees. First we write a
function to find the number of committees which we will soon minimize.
#+BEGIN_SRC python :tangle committees.py #+BEGIN_SRC python :tangle committees.py
def minimize_committees(comms): def number_of_committees(comms):
return sum(If(0 < comms[c], 1, 0) for c in comms) return sum(If(0 < comms[c], 1, 0) for c in comms)
#+END_SRC #+END_SRC
Now we can invoke Z3, using an ~Optimize~ instance and adding our Now we can invoke Z3, using an ~Optimize~ instance and adding our
minimization constraint. minimization constraint.
#+BEGIN_SRC python :tangle committees.py #+BEGIN_SRC python :tangle committees.py
def optimize_instance(instance): def optimize_instance(instance):
o = Optimize() N, C, S, A = instance
comms = committees(C)
o.add(constraints(instance)) o = Optimize()
o.minimize(minimize_committees(committees(instance)))
o.check() o.add(capacity_constraint(comms, C))
return o.model() o.add(non_negative_constraint(comms))
#+END_SRC o.add(all_corrected_constraint(comms, N))
o.add(avoid_correct_with_constraint(comms, A))
There is still more than one way to satisfy this model, but we are o.minimize(number_of_committees(comms))
guaranteed to get a minimal number of committees (which is 6 in our
example).
#+BEGIN_EXAMPLE o.check()
{2, 3}: 56 return o.model()
{0, 1}: 137 #+END_SRC
{2, 5}: 7
{0, 5}: 23 There is still more than one way to satisfy this model, but we are
{2, 4}: 47 guaranteed to get a minimal number of committees (which is 6 in our
{1, 4}: 13 example).
#+END_EXAMPLE
#+BEGIN_EXAMPLE
{2, 3}: 56
{0, 1}: 137
{2, 5}: 7
{0, 5}: 23
{2, 4}: 47
{1, 4}: 13
#+END_EXAMPLE
** Dealing with /should/
Remember $S$, which maps examiners to other examiners they /should/ form a
committee with. With optimization, we now have a way of expressing that some
solution is more preferable than another. One way to model this is
maximizing the number of exams given to committees that consists of an
examiner $i$ that should be in a committee with examiner $j$. We want this
for all such pairs $i,j$, and can achieve this by summing all such
committees.
#+BEGIN_SRC python :tangle committees.py
def should_correct_with_weight(comms, S, C):
return sum(comms[frozenset([i, j])] for i in S for j in S[i])
#+END_SRC
When adding multiple optimization objectives (or goals), Z3 defaults to
order the objectives lexicographically, i.e. in the order they appear. If we
place the minimization of committees before the
~should_correct_with_weight~, then we still are guaranteed to get a minimal
number of committees.
#+BEGIN_SRC python :tangle committees.py
def optimize_instance(instance):
N, C, S, A = instance
comms = committees(C)
o = Optimize()
o.add(capacity_constraint(comms, C))
o.add(non_negative_constraint(comms))
o.add(all_corrected_constraint(comms, N))
o.add(avoid_correct_with_constraint(comms, A))
o.minimize(number_of_committees(comms))
o.maximize(should_correct_with_weight(comms, S, C))
o.check()
return o.model()
#+END_SRC
#+BEGIN_EXAMPLE
{0, 2}: 50
{1, 3}: 60
{1, 5}: 7
{2, 4}: 60
{0, 5}: 23
{0, 1}: 83
#+END_EXAMPLE
** Optimize for capacities
Maybe we can try to satisfy (🙃) all the examiners by trying to close the
gap between their capacity and the number of exams they end up correcting.
Usually at the Department, there is quite a lot of flex in these capacities;
if you are willing to correct $50$ exams, then you will most likely be okey
with correcting $40$ and /actually/ willing to correct $52$. Therefore, we
can try to add some slack to the capacity.
#+BEGIN_SRC python :tangle committees.py
def capacity_slack(comms, i, C):
a = sum(comms[c] for c in comms if i in c)
return If(a > C[i], a - C[i], C[i] - a)
def capacity_weight(comms, C):
return sum(capacity_slack(comms, i, C) for i in range(len(C)))
#+END_SRC
#+BEGIN_SRC python :tangle committees.py
def optimize_instance(instance):
N, C, S, A = instance
comms = committees(C)
o = Optimize()
o.add(non_negative_constraint(comms))
o.add(all_corrected_constraint(comms, N))
o.add(avoid_correct_with_constraint(comms, A))
o.minimize(capacity_weight(comms, C))
o.minimize(number_of_committees(comms))
o.maximize(should_correct_with_weight(comms, S, C))
o.check()
return o.model()
#+END_SRC
** COMMENT Implementation draft ** COMMENT Implementation draft
@ -412,7 +499,7 @@ cry for help.
s.add(capacity_constraint(comms, C)) s.add(capacity_constraint(comms, C))
s.add(non_negative_constraint(comms)) s.add(non_negative_constraint(comms))
s.add(all_corrected_constraint(comms, N)) s.add(all_corrected_constraint(comms, N))
s.add(should_correct_with_constraint(comms, S, C)) s.add(should_correct_with_weight(comms, S, C))
s.add(avoid_correct_with_constraint(comms, A)) s.add(avoid_correct_with_constraint(comms, A))
s.check() s.check()