Files
moteurs/dist/autonomie/taux.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

136 lines
6.3 KiB
JavaScript

// taux.ts — taux de couverture par axe, depuis une sélection de postes.
// Origine calculs/src/scripts/autonomie/taux.js (formules 99-brief §7), sorti
// du view-model : les entrées sont des grandeurs, plus l'état de l'interface.
// Coefficients lus dans la table injectée ; chaque lecture est tracée.
//
// Changement d'harmonisation (LOT P1.0, point 5) : la couverture ÉNERGIE
// applique le taux d'autoconsommation (0,30 sans batterie, 0,70 avec 10 kWh,
// interpolation linéaire entre les deux) — l'origine comparait la production
// brute au besoin, ce qui surestimait la couverture.
import { Trace } from '../commun/contrat.js';
function lit(trace, coefficients, cle, valeur) {
const c = coefficients[cle];
trace.source(cle, valeur, c.source, c.date);
}
/** kWc installés dans la sélection (dimensions en kWc). */
export function kwcSelectionnes(postes) {
let kwc = 0;
for (const p of postes)
if (p.dimension && p.dimension.unite === 'kWc')
kwc += p.dimension.valeur || 0;
return kwc;
}
/** kWh de batterie dans la sélection (dimensions en kWh). */
export function batterieSelectionnee(postes) {
let kwh = 0;
for (const p of postes)
if (p.dimension && p.dimension.unite === 'kWh')
kwh += p.dimension.valeur || 0;
return kwh;
}
/** Mètres linéaires de haie dans la sélection. */
export function mlHaiesSelectionnes(postes) {
let ml = 0;
for (const p of postes)
if (p.dimension && p.dimension.unite === 'ml')
ml += p.dimension.valeur || 0;
return ml;
}
/** Vrai si la sélection porte un équipement de captage. */
export function aCaptage(postes) {
return postes.some((p) => String(p.typologie_choisie).startsWith('captage') || String(p.poste_id).includes('captage'));
}
/**
* EAU : volume captable vs besoin substituable. null si aucun captage.
*/
export function tauxEau(entrees, coefficients, trace) {
if (!entrees.a_captage)
return { taux: null, detail: null };
const nb = entrees.nb_personnes || 1;
const rendement = coefficients.captage_rendement.valeur;
const besoin_l_an = coefficients.besoin_eau_l_j_pers.valeur * 365;
const part = coefficients.part_eau_substituable.valeur;
lit(trace, coefficients, 'captage_rendement', rendement);
lit(trace, coefficients, 'besoin_eau_l_j_pers', coefficients.besoin_eau_l_j_pers.valeur);
lit(trace, coefficients, 'part_eau_substituable', part);
const captableL = (entrees.surface_toit_m2 || 0) * (entrees.pluviometrie_mm / 1000) * rendement * 1000;
const besoinSubstL = nb * besoin_l_an * part;
if (!(besoinSubstL > 0 && captableL > 0))
return { taux: null, detail: null };
return {
taux: Math.min(100, Math.round((captableL / besoinSubstL) * 100)),
detail: {
captable_m3: Math.round(captableL / 1000),
besoin_m3: Math.round((nb * besoin_l_an) / 1000),
besoin_substituable_m3: Math.round(besoinSubstL / 1000),
},
};
}
/** Taux d'autoconsommation selon la batterie : 0 kWh → sans, ≥ 10 kWh → avec, linéaire entre. */
export function tauxAutoconso(batterie_kwh, coefficients) {
const sans = coefficients.autoconso_sans_batterie.valeur;
const avec = coefficients.autoconso_avec_batterie_10kwh.valeur;
const t = Math.max(0, Math.min(1, batterie_kwh / 10));
return sans + (avec - sans) * t;
}
/** Productible PV (kWh/kWc/an) pour un contexte, depuis la table de coefficients. */
export function productiblePv(contexte, coefficients, trace) {
const c = coefficients.productible_pv_kwh_kwc.valeur;
const cle = c.par === 'zone_rt' ? contexte.zone_climatique_rt : String(contexte.zone_biogeo_code);
const v = c.table[cle];
if (v == null) {
trace.avertit(`productible PV : aucune valeur pour ${c.par} « ${cle} », défaut ${c.defaut} kWh/kWc`);
return c.defaut;
}
if (c.par === 'zone_rt' && cle === 'H2d')
trace.avertit('productible PV H2d interpolé entre H2c et H3 (non sourcé)');
return v;
}
/**
* ÉNERGIE : production PV autoconsommée vs besoin d'électricité spécifique.
* null si aucun kWc dans la sélection.
*/
export function tauxEnergie(entrees, coefficients, trace) {
if (!(entrees.kwc > 0))
return { taux: null, detail: null };
const nb = entrees.nb_personnes || 1;
const kwh_par_kwc = productiblePv(entrees.contexte, coefficients, trace);
lit(trace, coefficients, 'productible_pv_kwh_kwc', kwh_par_kwc);
const table = coefficients.orientation_pv.valeur;
const coef = (entrees.orientation_toit ? table[entrees.orientation_toit] : undefined) ?? 1;
if (entrees.orientation_toit && table[entrees.orientation_toit] == null)
trace.avertit(`orientation « ${entrees.orientation_toit} » inconnue : facteur 1 appliqué`);
lit(trace, coefficients, 'orientation_pv', coef);
const production = entrees.kwc * kwh_par_kwc * coef;
const autoconso = tauxAutoconso(entrees.batterie_kwh, coefficients);
lit(trace, coefficients, 'autoconso_sans_batterie', coefficients.autoconso_sans_batterie.valeur);
lit(trace, coefficients, 'autoconso_avec_batterie_10kwh', coefficients.autoconso_avec_batterie_10kwh.valeur);
const besoin = nb * coefficients.besoin_elec_kwh_pers_an.valeur;
lit(trace, coefficients, 'besoin_elec_kwh_pers_an', coefficients.besoin_elec_kwh_pers_an.valeur);
const autoconsomme = production * autoconso;
return {
taux: Math.min(100, Math.round((autoconsomme / besoin) * 100)),
detail: {
kwc: entrees.kwc,
kwh_par_kwc,
facteur_orientation: coef,
production_kwh: Math.round(production),
batterie_kwh: entrees.batterie_kwh,
taux_autoconso: +autoconso.toFixed(3),
autoconsomme_kwh: Math.round(autoconsomme),
besoin_kwh: besoin,
},
};
}
/**
* ALIMENTATION : mètres de haie vs recommandation. null si aucune haie.
*/
export function tauxAlim(entrees, coefficients, trace) {
const nb = entrees.nb_personnes || 1;
const reco = nb * coefficients.ml_haie_par_pers.valeur;
lit(trace, coefficients, 'ml_haie_par_pers', coefficients.ml_haie_par_pers.valeur);
if (!(entrees.ml > 0 && reco > 0))
return { taux: null, detail: null };
return { taux: Math.min(100, Math.round((entrees.ml / reco) * 100)), detail: { ml: entrees.ml, reco_ml: reco } };
}