// scoreurs.ts — 10 scoreurs réutilisables [0..1] // Spec: V2-algo-spec-FONCTIONS.md §1.2 — origine calculs/src/lib/autonomie/scoreurs.js. // Correction LOT P1.0 : les coefficients (rendement de captage, facteurs // d'orientation) arrivent par `reglages` au lieu d'une constante de module. // // Note (bug latent d'origine, conservé pour la non-régression, à corriger en // patch) : score_pluviometrie, score_climat_chauffage et score_metres_haie // lisent `typo.parametre_dim` sans le parser ; quand il arrive en JSON // sérialisé (cas des données publiées), ils retombent sur leurs défauts. import { clamp01 } from './calibrage.js'; import { parseJsonArr } from './types.js'; function paramBrut(typo) { // Reproduit `typo.parametre_dim?.default` de l'origine : un objet est lu, // une chaîne JSON ne l'est PAS (ses propriétés sont undefined). const p = typo.parametre_dim; return p && typeof p === 'object' ? p : null; } /** * score_pluviometrie — pertinent pour stockage eau, récupération pluie * Volume récupéré annuel vs capacité de la cuve (cible : 2 mois de réserve) */ export function score_pluviometrie(typo, inputs, contexte_derived, _poste, _sel, reglages) { const surface = inputs.surface_toit_m2 ?? 0; const pluvio = contexte_derived.pluviometrie_mm ?? 800; const volume_recup_annuel = surface * (pluvio / 1000) * reglages.coefficients.captage_rendement.valeur; const cible_2mois = volume_recup_annuel / 6; const dim_default = paramBrut(typo)?.default ?? 8; if (cible_2mois <= 0) return 0.3; const ratio = dim_default / cible_2mois; return clamp01(1 - 0.5 * Math.abs(1 - ratio)); } /** * score_climat_chauffage — pertinent pour postes chauffage, ECS * Plus le DJU est élevé, plus une typologie puissante est pertinente */ export function score_climat_chauffage(typo, _inputs, contexte_derived) { const dju = contexte_derived.dju ?? 2200; const puissance = paramBrut(typo)?.default ?? 5; const puissance_cible = dju / 500; // heuristique ~5 kW pour H1 if (puissance_cible <= 0) return 0.5; return clamp01(1 - (0.4 * Math.abs(puissance - puissance_cible)) / puissance_cible); } /** * score_climat_meridional — bonus solutions sécheresse/canicule zone MED */ export function score_climat_meridional(typo, _inputs, contexte_derived) { const is_med = contexte_derived.zone_biogeo === 'MED'; if (!is_med) return 0.5; // neutre hors MED const tags = typo.tags ?? []; const med_tags = ['frais_passif', 'sec', 'mediterraneen']; return med_tags.some((t) => tags.includes(t)) ? 1.0 : 0.5; } /** * score_taille_foyer — pertinent pour postes dimensionnables au nb personnes */ export function score_taille_foyer(typo, inputs) { const param = paramBrut(typo); if (!param || param.strategie !== 'par_personne') return 0.5; const base = param.base ?? 0; const coef = param.coef ?? 1; const min = param.min ?? 0; const max = param.max ?? 100; const dim_calc = base + (inputs.nb_personnes ?? 4) * coef; if (max === min) return 0.5; const ratio_in_range = clamp01((dim_calc - min) / (max - min)); return 0.4 + 0.6 * ratio_in_range; } /** * score_surface_toit — pertinent pour PV, récupération eau pluie */ export function score_surface_toit(typo, inputs, _contexte, _poste, _sel, reglages) { const param = paramBrut(typo); if (!param || param.strategie !== 'par_surface_toit') return 0.5; const orient_factor = facteurOrientation(reglages, inputs.orientation_toit) ?? 0.8; const surface_utilisable = (inputs.surface_toit_m2 ?? 0) * orient_factor; const coef = param.coef ?? 0.1; const min = param.min ?? 0; const max = param.max ?? 100; const taille_optimale = surface_utilisable * coef; if (max === min) return 0.5; const ratio_in_range = clamp01((taille_optimale - min) / (max - min)); return 0.3 + 0.7 * ratio_in_range; } /** * score_orientation_toit — PV uniquement * Sud = 1.0, dégradation linéaire, nord = forte pénalité */ export function score_orientation_toit(_typo, inputs, _contexte, poste, _sel, reglages) { // On applique ce score dès qu'on est sur un poste PV (energie_pv) if (!poste || !poste.code?.includes('pv')) return 0.5; return facteurOrientation(reglages, inputs.orientation_toit) ?? 0.5; } /** * score_surface_parcelle — pertinent pour alim, phyto-épuration, irrigation */ export function score_surface_parcelle(typo, inputs) { const surface_utile = Math.max(0, (inputs.surface_parcelle_m2 ?? 0) - 100); const surface_min = typo.surface_min_m2 ?? 0; if (surface_utile < surface_min) return 0.0; // hard exclude const surface_optimale = typo.surface_optimale_m2 ?? 500; const ratio = surface_utile / surface_optimale; return clamp01(0.4 + 0.6 * Math.min(ratio, 1)); } /** * score_temps_dispo — modulation par disponibilité utilisateur */ export function score_temps_dispo(typo, inputs) { const td = inputs.temps_dispo ?? 2; const pose = typo.complexite_pose ?? 2; const diy = typo.flag_diy ?? false; if (td === 1) { if (pose === 1 && diy) return 0.8; if (pose === 3) return 0.4; return 0.6; } if (td === 3) { if (diy) return 1.0; if (pose === 3) return 0.7; return 0.7; } return 0.7; // td === 2 } /** * score_metres_haie — pertinent pour haies */ export function score_metres_haie(typo, inputs) { const metres = inputs.metres_haie_possibles ?? 0; // Origine : `if (!param) …` sur le champ brut — une chaîne JSON (truthy) // passe donc dans la branche « max ?? 100 ». Conservé tel quel. const brut = typo.parametre_dim; if (!brut) return metres > 10 ? 0.6 : 0.3; const max = paramBrut(typo)?.max ?? 100; return clamp01(0.3 + 0.7 * Math.min(metres / max, 1)); } /** * score_couplage_disponible — bonus synergies, calculé post-sélection initiale (étape 4) * @param typologies_selectionnees — codes déjà sélectionnés */ export function score_couplage_disponible(typo, _inputs, _contexte, _poste, typologies_selectionnees = []) { const requis = parseJsonArr(typo.couplages_requis); const exclus = parseJsonArr(typo.couplages_exclus); const has_requis = requis.some((c) => typologies_selectionnees.includes(c)); const has_exclus = exclus.some((c) => typologies_selectionnees.includes(c)); if (has_exclus) return 0.0; if (has_requis) return 1.0; return 0.5; } /** Facteur d'orientation PV depuis la table de coefficients ; undefined si orientation inconnue. */ export function facteurOrientation(reglages, orientation) { if (!orientation) return undefined; const table = reglages.coefficients.orientation_pv.valeur; return table[orientation]; }