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