""" Test/démo du solveur : charge sample_request.json, résout, et imprime un horaire lisible + le rapport de couverture (dispo vs requis). Sert aussi de vérification (assert : statut faisable + chaque tech respecte ses contraintes). python test_solver.py """ import json import os import sys from collections import defaultdict from solver import solve_roster HERE = os.path.dirname(os.path.abspath(__file__)) def main(): with open(os.path.join(HERE, "sample_request.json")) as f: req = json.load(f) res = solve_roster(req) print(f"\n=== STATUT : {res['status']} ({res.get('solve_ms')} ms) ===") assert res["status"] in ("OPTIMAL", "FEASIBLE"), "solveur infaisable" # Grille employé × jour days = [req["horizon"]["start"]] from datetime import date, timedelta y, m, d = (int(x) for x in req["horizon"]["start"].split("-")) start = date(y, m, d) days = [(start + timedelta(days=i)).isoformat() for i in range(req["horizon"]["days"])] by_tech = defaultdict(dict) for a in res["assignments"]: by_tech[a["tech_name"]][a["date"]] = f"{a['shift']}@{a['zone']}" print("\n=== HORAIRE (Roster AI) ===") hdr = "Technicien".ljust(16) + "".join(dd[5:].ljust(12) for dd in days) print(hdr) for t in req["technicians"]: row = t["name"].ljust(16) for dd in days: row += (by_tech.get(t["name"], {}).get(dd, "·")).ljust(12) print(row) print("\n=== HEURES / TECH (équité, écart =", res.get("spread_hours"), "h) ===") for tid, h in sorted(res["tech_hours"].items(), key=lambda kv: -kv[1]): name = next((t["name"] for t in req["technicians"] if t["id"] == tid), tid) print(f" {name.ljust(16)} {h} h") print("\n=== COUVERTURE (dispo vs requis) ===") for c in res["coverage_report"]: flag = " ✅" if c["shortfall"] == 0 else f" ❌ MANQUE {c['shortfall']}" print(f" {c['date']} {c['shift']:5} {c['zone']:10} requis={c['required']} assigné={c['assigned']}{flag}") print("\n=== EXPLICATIONS ===") for e in res["explanations"]: print(" " + e) # Vérifs dures # 1) ≤ 1 shift/jour/tech seen = set() for a in res["assignments"]: key = (a["tech"], a["date"]) assert key not in seen, f"double-booking {key}" seen.add(key) # 2) compétences respectées skill_of = {t["id"]: set(t["skills"]) for t in req["technicians"]} cov_skills = {(c["date"], c["shift"], c.get("zone", "—")): set(c.get("required_skills", [])) for c in req["coverage"]} for a in res["assignments"]: req_sk = cov_skills.get((a["date"], a["shift"], a["zone"]), set()) assert req_sk.issubset(skill_of[a["tech"]]), f"compétence manquante: {a}" # 3) indispo respectée unavail = {t["id"]: set(t.get("unavailable", [])) for t in req["technicians"]} for a in res["assignments"]: assert a["date"] not in unavail[a["tech"]], f"assigné un jour indispo: {a}" print("\n✅ Toutes les contraintes dures respectées (1 shift/jour, compétences, indispos).") return 0 if __name__ == "__main__": sys.exit(main())