All projects

Bachelor's thesis · Kode S.r.l.

Sportslot.it

A resource-scheduling optimiser for sports centres. It generates weekly schedules that maximise user satisfaction and minimise wasted facility time.

Mar 2026 – Jun 2026

B.Sc. thesis — deployed as sportslot.it

Sportslot.it

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.

The real scheduler, running. It loads inside the page, or opens in its own tab.

sportslot.it

The thesis, in 25 slides

PDF
Slide 1

1 / 25

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()