
Overview
Sports centres juggle courts, pitches, coaches and hundreds of requests every week, usually by hand. Sportslot models that week as a constraint problem and solves it.
The project was my bachelor's thesis, “Resource Scheduling Optimization for Sports Centres: A CP-SAT Based Approach”, developed during an internship at Kode S.r.l. in Pisa.
What I built
- A constraint model in Google OR-Tools CP-SAT balancing satisfaction against facility waste.
- A relational schema in PostgreSQL and a REST API around the solver.
- Containerised deployment with Docker.
Live demo
Open in a tabThe real scheduler, running. It loads inside the page, or opens in its own tab.
sportslot.it
The solver, on the XS instance
Read-only · outputs from a real run of run_solver.py
The smallest benchmark instance from the thesis: 37 training requests, 58 facilities, one week. Press Run to walk through loading it, building the CP-SAT model, solving it, and reading the schedule that comes out.
In [ ]:
import solver_utils as su
# XS: the smallest benchmark instance — one week of a sports centre
data = su.load_data(".", output_dir="output/instance_XS")
params = su.build_params(data)
# Which (request, resource, slot) triples are even legal?
feasible_starts = su.build_feasible_starts(params)
su.print_instance_stats(params, feasible_starts)In [ ]:
from ortools.sat.python import cp_model
model = cp_model.CpModel()
# x[q, r, t] = 1 → request q trains on resource r starting at slot t
for (q, r, t) in feasible_starts:
x_vars[(q, r, t)] = model.NewBoolVar(f"x_{q}_{r}_{t}")
# C5 — each request gets exactly its k weekly sessions
for q in requests_list:
model.Add(sum(x_by_req[q]) == effective_k[q])
# Objective: every scheduled session is worth its priority weight,
# with a smaller bonus for landing inside a preferred window
model.Maximize(sum(obj_terms))
proto = model.Proto()
print(f"variables : {len(proto.variables):,}")
print(f"constraints: {len(proto.constraints):,}")In [ ]:
solver = cp_model.CpSolver()
solver.parameters.max_time_in_seconds = 30
status = solver.Solve(model)
print(solver.StatusName(status))
print(f"sessions scheduled : {sessions_scheduled}/{sessions_required}")
print(f"coverage : {coverage_pct:.1f}%")
print(f"wall time : {solver.WallTime():.2f} s")In [ ]:
schedule = su.extract_schedule(solver, x_vars, params)
print(schedule.head(8).to_string(index=False))In [ ]:
import matplotlib.pyplot as plt
mon = schedule[schedule["day"] == "Mon"]
fig, ax = plt.subplots(figsize=(10, 6))
for _, row in mon.iterrows():
y = resources.index(row["resource_id"])
ax.barh(y, row["duration_slots"] / 2, left=row["start_slot"] % 48 / 2,
height=0.6, color=colors[y], edgecolor="white")
ax.set_title("Monday — scheduled sessions per resource (instance XS)")
plt.show()

