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
This commit is contained in:
Vendored
+170
@@ -0,0 +1,170 @@
|
||||
// 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];
|
||||
}
|
||||
Reference in New Issue
Block a user