Files
moteurs/dist/autonomie/noyau/scoreurs.js
T
JulesandClaude Fable 5.1 69809df5d1 LOT P1.0 : paquet @transformer/moteurs v0.1.0
Extraction du cœur autonomie de calculs.trans-former.fr (12 modules, sans
singleton, couplages sans mutation, console derrière debug), découpage en trois
moteurs d'axe + intégrateur (mode global = calculs, mode par_axe = plateforme),
moteurs commun.contexte (Corse 2A/2B, DOM 97x corrigés) et commun.geometrie,
table des coefficients harmonisés (docs/CONSTANTES.md pour Jules, R-7),
validation structurelle des données (schemas/), tests hors ligne : 28 assertions
d'origine + épingle générée depuis le code d'astro-pro + écarts d'harmonisation
figés + contrat des moteurs. dist/ committé.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019UBHYPdeQYy2m1GVJp6b12
2026-09-28 17:06:49 +02:00

171 lines
6.8 KiB
JavaScript

// 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];
}