From bc157588d1bbcd17e37b466b9432c4557c41c036 Mon Sep 17 00:00:00 2001 From: louispaulb Date: Thu, 2 Jul 2026 13:53:46 -0400 Subject: [PATCH] =?UTF-8?q?tune(dispatch):=20VRP=20prioritizes=20distance?= =?UTF-8?q?=20over=20specialty=20(rank=5Fweight=206=E2=86=922)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Skill capability stays a hard filter, so a multi-skill tech (e.g. an installer who also has the repair skill) can take those jobs. But the specialist bias (skill-order rank) was too strong (6 virtual min/rank), so a far repair-specialist beat a nearby installer-who-repairs. Lower rank_weight to 2 → distance dominates; specialty only breaks near-ties. Verified: repair job near a rank-3 multi-skill installer (4 min) vs a far rank-0 specialist (14 min) — old weight picked the far specialist (bug), new weight picks the nearby installer. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/ops/src/pages/PlanificationPage.vue | 2 +- services/roster-solver/route_solver.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/ops/src/pages/PlanificationPage.vue b/apps/ops/src/pages/PlanificationPage.vue index 6435515..f9bbf41 100644 --- a/apps/ops/src/pages/PlanificationPage.vue +++ b/apps/ops/src/pages/PlanificationPage.vue @@ -4103,7 +4103,7 @@ async function optimizeSuggestion () { } if (!vehicles.length) { out.push(...entries); continue } // aucun tech éligible (ex. week-end sans quart) → garder l'assignation greedy let res - try { res = await roster.optimizeRoutes({ jobs, vehicles, max_seconds: 8, rank_weight: 6 }) } catch (e) { out.push(...entries); continue } + try { res = await roster.optimizeRoutes({ jobs, vehicles, max_seconds: 8, rank_weight: 2 }) } catch (e) { out.push(...entries); continue } // rank_weight faible = DISTANCE prioritaire, spécialité = simple départage if (!res || res.status !== 'OK') { out.push(...entries); continue } // solveur absent/erreur/infaisable → on GARDE l'assignation greedy (ne jamais perdre de jobs) const placed = new Set() for (const rt of (res.routes || [])) { diff --git a/services/roster-solver/route_solver.py b/services/roster-solver/route_solver.py index 1e86953..60b84a6 100644 --- a/services/roster-solver/route_solver.py +++ b/services/roster-solver/route_solver.py @@ -46,7 +46,10 @@ def solve_route(req: dict) -> dict: speed = float(req.get("speed_kmh") or 45.0) default_service = int(req.get("default_service_min") or 60) - rank_w = float(req.get("rank_weight") or 5.0) # « minutes virtuelles » par cran d'ordre de compétence (spécialiste=0) + # DISTANCE PRIORITAIRE : la spécialité (ordre de compétence) ne fait que DÉPARTAGER à trajet ~égal. + # rank_w = « minutes virtuelles » par cran d'ordre (spécialiste=0). Petit → un tech multi-compétences PROCHE + # passe avant un spécialiste LOIN (ex. un installateur qui a aussi la réparation prend une réparation voisine). + rank_w = float(req.get("rank_weight") if req.get("rank_weight") is not None else 2.0) base_pen = int(req.get("drop_penalty") or 100000) max_seconds = float(req.get("max_seconds") or 10)