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
171 lines
6.8 KiB
JavaScript
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];
|
|
}
|