solveur : le RANG (compétence principale) domine la polyvalence — répare la mauvaise répartition

Bug (Louis) : Stephane (réparation rang 1, installation secondaire) recevait 3 installations, William
(installation rang 1/principale) une seule. Cause : versatility_weight=6 > rank_weight=2 → un tech moins
polyvalent mais NON spécialiste rafflait les jobs hors de sa spécialité. Fix : rank_weight=20 (domine),
versatility_weight=3 (léger départage entre techs de même rang → réserve les experts). Testé : William obtient
les installations. eff/AM-PM/distance inchangés.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
louispaulb 2026-07-07 12:10:37 -04:00
parent 8ee4c834f8
commit 4ae2907172
3 changed files with 4 additions and 4 deletions

View File

@ -4858,7 +4858,7 @@ async function optimizeSuggestion () {
dates: suggestWindow.value, tech_ids: selTechs.map(t => t.id),
// job_names = pool filtré (+ jobs DÉJÀ assignés de la fenêtre si « ré-optimiser l'existant ») le solveur les replace.
job_names: [...new Set([...suggestJobs().map(j => j.name), ...(suggestDlg.includeAssigned ? assignedNamesForWindow() : [])])],
opts: { rank_weight: solverOpts.rankWeight, speed_kmh: solverOpts.speedKmh, max_seconds: solverOpts.maxSeconds, overtime_coef: solverOpts.overtimeCoef },
opts: { rank_weight: 20, versatility_weight: 3, speed_kmh: solverOpts.speedKmh, max_seconds: solverOpts.maxSeconds, overtime_coef: solverOpts.overtimeCoef }, // rang (compétence principale) DOMINE ; polyvalence = léger départage
dur_overrides: { ...durOverride }, job_windows,
})
if (!r || !r.ok || !Array.isArray(r.plan)) throw new Error((r && r.error) || 'réponse invalide')
@ -4919,7 +4919,7 @@ async function optimizeSuggestionLocal () {
}
if (!vehicles.length) { unassigned.push(...entries); continue } // aucun tech éligible ce jour non assignés
let res
try { res = await roster.optimizeRoutes({ jobs, vehicles, max_seconds: solverOpts.maxSeconds, rank_weight: solverOpts.rankWeight, speed_kmh: solverOpts.speedKmh, overtime_coef: solverOpts.overtimeCoef }) } catch (e) { unassigned.push(...entries); continue }
try { res = await roster.optimizeRoutes({ jobs, vehicles, max_seconds: solverOpts.maxSeconds, rank_weight: 20, versatility_weight: 3, speed_kmh: solverOpts.speedKmh, overtime_coef: solverOpts.overtimeCoef }) } catch (e) { unassigned.push(...entries); continue }
if (!res || res.status !== 'OK') { unassigned.push(...entries); continue } // solveur absent/erreur non assignés (ne jamais perdre de jobs)
const placed = new Set()
for (const rt of (res.routes || [])) {

View File

@ -57,7 +57,7 @@ def solve_route(req: dict) -> dict:
eff_bias = float(req.get("eff_bias") if req.get("eff_bias") is not None else 8.0)
# RÉSERVE DES EXPERTS : pénalise les techs POLYVALENTS (beaucoup de compétences) → les cas simples vont d'abord
# aux moins polyvalents (qui ne pourraient rien faire d'autre), gardant les experts pour les jobs exigeants.
vers_w = float(req.get("versatility_weight") if req.get("versatility_weight") is not None else 6.0)
vers_w = float(req.get("versatility_weight") if req.get("versatility_weight") is not None else 3.0) # TIEBREAKER léger seulement — le RANG (compétence principale) doit dominer
def eff_of(veh, sk):
se = veh.get("skill_eff") or {}

View File

@ -263,7 +263,7 @@ async function optimizePlan (body) {
...(w ? { tw_start_min: w.tw_start_min, tw_end_min: w.tw_end_min } : {}),
}
})
const res = await solveVrp({ jobs: sJobs, vehicles, max_seconds: Number(opts.max_seconds) || 8, rank_weight: opts.rank_weight != null ? Number(opts.rank_weight) : 2, speed_kmh: Number(opts.speed_kmh) || 45, overtime_coef: opts.overtime_coef != null ? Number(opts.overtime_coef) : 20 })
const res = await solveVrp({ jobs: sJobs, vehicles, max_seconds: Number(opts.max_seconds) || 8, rank_weight: opts.rank_weight != null ? Number(opts.rank_weight) : 20, versatility_weight: opts.versatility_weight != null ? Number(opts.versatility_weight) : 3, speed_kmh: Number(opts.speed_kmh) || 45, overtime_coef: opts.overtime_coef != null ? Number(opts.overtime_coef) : 20 })
if (!res || res.status !== 'OK') { for (const j of rest) unassigned.push(entryOf(j, iso, durOf(j))); continue }
const placed = new Set()
for (const rt of (res.routes || [])) {