Deux postes en même temps possibles, malus dans le solveur
Le chevauchement de créneaux (SHIFT_OVERLAP) n'est plus bloquant : il remonte en avertissement et l'affectation est enregistrée. Le solveur ne l'exclut plus mais applique un fort malus au bénévole déjà affecté ailleurs au même moment (proposé seulement sous l'effectif minimal, faute de mieux) ; il n'est jamais reproposé sur un créneau où il est déjà. Même malus dans le classement par défaut du panneau et dans le mock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d0fa1e109c
commit
0f904f5e3d
@@ -5,8 +5,10 @@ effectif idéal, une liste ordonnée de bénévoles candidats. C'est la
|
||||
coordination qui affecte (dans l'app, via l'écran habituel — le solveur se
|
||||
contente de faire remonter ces candidats en tête de liste).
|
||||
|
||||
Contrainte dure : aucun chevauchement de créneaux pour un même bénévole (règle
|
||||
`SHIFT_OVERLAP`, cf. `app.validation`). Tout le reste (chaînage entre
|
||||
Contrainte dure : jamais deux suggestions qui se chevauchent pour un même
|
||||
bénévole dans un même calcul. Un bénévole déjà affecté à un autre poste au
|
||||
même moment reste proposable (la validation ne fait qu'avertir,
|
||||
`SHIFT_OVERLAP`) mais avec un fort malus. Tout le reste (chaînage entre
|
||||
créneaux consécutifs, vœu de poste, vœu de compagnon, vœu d'artiste, équité de
|
||||
charge) est un terme de l'objectif, pas une contrainte. Aucune limite d'heures
|
||||
par journée ni de temps de repos : ni le solveur ni la validation n'en
|
||||
@@ -42,6 +44,11 @@ _CHAIN_PENALTY = 40
|
||||
# Par compagnon souhaité déjà affecté au créneau (0 à N vœux par bénévole,
|
||||
# rendus réciproques par le loader — voir `SolverVolunteer.wished_companion_ids`).
|
||||
_COMPANION_BONUS_PER_MATCH = 25
|
||||
# Le bénévole est déjà affecté (affectation décidée) à un créneau qui
|
||||
# chevauche celui-ci. Supérieur à tous les bonus cumulables et à la base
|
||||
# normale : il n'est proposé que pour un créneau sous son minimum, et
|
||||
# seulement faute de mieux.
|
||||
_ALREADY_BUSY_PENALTY = 300
|
||||
|
||||
|
||||
def _overlaps(a: ShiftInfo, b: ShiftInfo) -> bool:
|
||||
@@ -78,6 +85,8 @@ def _score(volunteer: SolverVolunteer, shift: SolverShift) -> int:
|
||||
_overlaps(shift.shift_info, passage) for passage in volunteer.wished_artist_windows
|
||||
):
|
||||
score -= _ARTIST_CONFLICT_PENALTY
|
||||
if any(_overlaps(shift.shift_info, other) for other in volunteer.existing_shifts):
|
||||
score -= _ALREADY_BUSY_PENALTY
|
||||
score -= (_FAIRNESS_PENALTY_PER_HOUR * volunteer.existing_minutes) // 60
|
||||
return score
|
||||
|
||||
@@ -98,7 +107,8 @@ def solve(
|
||||
if gap <= 0:
|
||||
continue
|
||||
for vi, volunteer in enumerate(data.volunteers):
|
||||
if any(_overlaps(shift.shift_info, other) for other in volunteer.existing_shifts):
|
||||
# Déjà affecté à ce créneau : rien à suggérer.
|
||||
if any(other.id == shift.id for other in volunteer.existing_shifts):
|
||||
continue
|
||||
key = (vi, si)
|
||||
variables[key] = model.NewBoolVar(f"x_{vi}_{si}")
|
||||
@@ -115,8 +125,8 @@ def solve(
|
||||
if shift_vars and gap > 0:
|
||||
model.Add(sum(shift_vars) <= gap)
|
||||
|
||||
# Aucun chevauchement entre deux créneaux nouvellement suggérés au même
|
||||
# bénévole (règle `SHIFT_OVERLAP`, bloquante).
|
||||
# Jamais deux créneaux qui se chevauchent suggérés au même bénévole dans
|
||||
# un même calcul (contre les affectations décidées : malus, cf. `_score`).
|
||||
for vi in range(len(data.volunteers)):
|
||||
my_shifts = [si for si in range(len(data.shifts)) if (vi, si) in variables]
|
||||
for a in range(len(my_shifts)):
|
||||
|
||||
Reference in New Issue
Block a user