Solveur : le malus de chaînage n'est pas limité au même poste
Correction : le malus ajouté précédemment ne se déclenchait que si le bénévole avait tenu le créneau précédent du *même* poste. L'intention était plus générale — limiter les enchaînements de shifts d'un bénévole, quel que soit le poste, pas seulement faire tourner un poste en particulier. - `SolverShift.predecessor_shift_id: str | None` → `predecessor_shift_ids: frozenset[str]` : l'ensemble des créneaux (n'importe quel poste) du même jour dont la fin coïncide avec le début de celui-ci. Peut contenir plusieurs ids (plusieurs postes se terminant à la même heure). - Loader : le calcul du prédécesseur se fait maintenant sur tous les créneaux de l'édition regroupés par jour (comparaison des heures de fin), plus par poste. - Miroir côté mock (`buildPredecessorMap`, `runSolver`). - Nouveau test d'intégration (`test_api_solver.py`) : deux postes différents, confirme que le chaînage s'applique bien d'un poste à l'autre. - CLAUDE.md à jour. Vérifié : 185 tests backend (dont le nouveau), ruff, alembic check ; typecheck/lint/build/smoke frontend. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
64e2f86647
commit
b97b95c606
@@ -40,21 +40,24 @@ def build_solver_input(*, session: Session, edition: Edition) -> SolverInput:
|
||||
)
|
||||
shifts_by_id = {s.id: s for s in shifts}
|
||||
|
||||
# Prédécesseur (même poste, même jour, créneau juste avant) : sert au
|
||||
# malus de chaînage (voir `engine._score`). Calculé sur tous les créneaux
|
||||
# du poste, pas seulement ceux à compléter.
|
||||
predecessor_id: dict = {}
|
||||
by_position: dict = defaultdict(list)
|
||||
# Prédécesseur(s) : créneaux (n'importe quel poste) du même jour dont la
|
||||
# fin coïncide avec le début de celui-ci — sert au malus de chaînage
|
||||
# (voir `engine._score`), pour limiter les enchaînements de shifts plutôt
|
||||
# que cibler la rotation d'un poste en particulier. Calculé sur tous les
|
||||
# créneaux de l'édition, pas seulement ceux à compléter.
|
||||
predecessor_ids: dict = {}
|
||||
by_day: dict = defaultdict(list)
|
||||
for s in shifts:
|
||||
by_position[s.position_id].append(s)
|
||||
for position_shifts in by_position.values():
|
||||
by_day: dict = defaultdict(list)
|
||||
for s in position_shifts:
|
||||
by_day[s.starts_at.date()].append(s)
|
||||
for day_shifts in by_day.values():
|
||||
day_shifts.sort(key=lambda s: s.starts_at)
|
||||
for previous, current in zip(day_shifts, day_shifts[1:], strict=False):
|
||||
predecessor_id[current.id] = str(previous.id)
|
||||
by_day[s.starts_at.date()].append(s)
|
||||
for day_shifts in by_day.values():
|
||||
ids_by_end: dict = defaultdict(list)
|
||||
for s in day_shifts:
|
||||
ids_by_end[s.ends_at].append(str(s.id))
|
||||
distinct_ends = sorted(ids_by_end)
|
||||
for s in day_shifts:
|
||||
earlier_ends = [e for e in distinct_ends if e <= s.starts_at]
|
||||
if earlier_ends:
|
||||
predecessor_ids[s.id] = frozenset(ids_by_end[earlier_ends[-1]])
|
||||
|
||||
assigned_count: dict = defaultdict(int)
|
||||
shifts_by_volunteer: dict = defaultdict(list)
|
||||
@@ -73,7 +76,7 @@ def build_solver_input(*, session: Session, edition: Edition) -> SolverInput:
|
||||
min_staff=s.min_staff,
|
||||
ideal_staff=s.ideal_staff,
|
||||
already_assigned=assigned_count.get(s.id, 0),
|
||||
predecessor_shift_id=predecessor_id.get(s.id),
|
||||
predecessor_shift_ids=predecessor_ids.get(s.id, frozenset()),
|
||||
)
|
||||
for s in shifts
|
||||
if assigned_count.get(s.id, 0) < s.ideal_staff
|
||||
|
||||
Reference in New Issue
Block a user