Finding a meeting time

We have n people's availabilities for the meeting times 9a, 10a, 11a, 12, 1p, 2p, 3p, 4p. Each person's availability is reprsented as a Boolean vector $a^\top\in \{0,1\}^8$. We would like to schedule $J$ meetings between different groups of people, represented by $J$ index sets $\mathcal{I_j}\subseteq\{1,\dots,n\}$.

Rules:

  • Each meeting $\mathcal{I}_j$ must occur at one time $t$.
  • All people attending meeting $\mathcal{I}_j$ must be available at time $t$.
  • All people attending meeting $\mathcal{I}_j$ must not be attending another meeting at time $t$.
  • No attendee should have >2 hours of consecutive meetings.

Setup

We concatenate the availability row vectors into a 5 x 8 Boolean matrix $\bar A$.

using Satisfiability

n = 5 # number of people
T = 8 # number of times

# nxT matrix: each row is one attendee's meeting availability
A_bar = Bool[
    1 0 1 0 1 1 1 1
    0 1 1 0 0 0 0 1
    1 1 1 0 1 1 0 1
    1 1 0 1 1 0 0 0
    0 1 1 1 0 0 0 1
]

# A is a matrix-valued variable such that ``A_{it} = 1`` if attendee ``i`` is in a meeting at time ``t`` and 0 otherwise.
@satvariable(A[1:n, 1:T], Bool)
println(size(A))

The index_sets represent which meeting attendees are required at each meeting $\mathcal{I_j}$.

index_sets = [[1,2,3], [3,4,5], [1,3,5], [1,4]]
J = length(index_sets) # number of meetings

Logical constraints

If attendee $i$ is unavailable at time $t$ ($\bar A_{it} = 0$) then they cannot be in a meeting at time $t$.

unavailability = and(¬A_bar .⟹ ¬A)

For each meeting $j$, all attendees in index set $\mathcal{I_j}$ must be available at some time $t$ and not attending another meeting.

M = [and(A[index_sets[j], t]) for j=1:J, t=1:T]

# get a list of conflicts
conflicts = [filter((i) -> i != j && length(intersect(index_sets[j], index_sets[i])) > 0, 1:J) for j=1:J ]
no_double_booking = and(M[j,t] ⟹ ¬or(M[conflicts[j],t]) for j=1:J, t=1:T)
println("Constructed $(length(no_double_booking.children)) double-booking constraints.")

All meetings must be scheduled.

require_one_time = and(or(M[j,:]) for j=1:J)
println("Constructed $(length(require_one_time.children)) one-time constraints.")

No attendee should have more than 2 consecutive hours of meetings.

time_limit = and(¬and(A[i,t:t+2]) for i=1:n, t=1:T-2)
println("Constructed $(length(time_limit.children)) consecutivity constraints.")

Solving the problem

# solve
exprs = [no_double_booking, require_one_time, unavailability, time_limit]
status = sat!(exprs, solver=Z3())

println("status = $status") # for this example we know it's SAT
times = ["9a", "10a", "11a", "12p", "1p", "2p", "3p", "4p"]
for j=1:J
    Mj_value = value(M[j,:])
    println("Meeting with attendees $(index_sets[j]) can occur at $(times[filter((i) -> Mj_value[i], 1:length(Mj_value))]) .== true)])")
end

println("Value A: $(value(A))")
println("Value N: $(value(M))")