gigafibre-fsm/services/roster-solver/test_solver.py
louispaulb f4138cdd75 Roster AI (planification) + prise de rendez-vous client
Solveur OR-Tools (services/roster-solver) : couverture, compétences,
équité, coût chargé, cadence/efficacité, capacité-par-job ; contraintes
dures/souples façon Timefold.

Hub (lib/roster.js) : génération via solveur, publication par réécriture
de semaine (anti-doublons), demande (effectif ou nb de jobs), cadence/coût/
compétences par tech, pause, congés (Tech Availability + approbation),
booking (slots roster-aware / fit 3-dispos / confirm) + portail public /book.
Réessai sur serialization failures frappe_pg ; appels ERP séquentiels.

Ops : page Planification (grille compacte « J8 », multi-shift, drag-select
+ undo/redo, modèles de semaine, éditeur cadence&coût, congés, SMS opt-in),
page Rendez-vous (répartiteur), jobColor tech en pause → tickets rouges.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 16:42:44 -04:00

85 lines
3.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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