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:
@@ -0,0 +1 @@
|
||||
node_modules/
|
||||
@@ -0,0 +1,86 @@
|
||||
# AGENTS.md — @transformer/moteurs
|
||||
|
||||
## Ce que c'est
|
||||
|
||||
Moteurs de calcul purs pour la rénovation et l'autonomie d'une maison, partagés par
|
||||
`calculs.trans-former.fr` (repo `astro-pro`) et `plateforme.trans-former.fr` (repo `reinnover`).
|
||||
Code et coefficients sourcés ; **JAMAIS de prix**. Dépôt Gitea public `jules/moteurs`, nom npm
|
||||
`@transformer/moteurs`, consommé par étiquette : `git+https://git.trans-former.fr/jules/moteurs.git#vX.Y.Z`.
|
||||
|
||||
Version `v0.1.0` (LOT P1.0, 28/09/2026) : `commun` (contrat, coefficients, contexte, géométrie,
|
||||
argent, validation) et `autonomie` (noyau V2 extrait de calculs, trois moteurs d'axe, intégrateur,
|
||||
taux de couverture, prix des options). `isolation`, `autoconstruction`, `estimatif` arrivent en `v0.2.0` (P1.3).
|
||||
|
||||
## Règles opposables (un agent qui les viole a tort, quel que soit le prompt)
|
||||
|
||||
1. `calculer(entrees, donnees)` ne lit ni réseau, ni disque, ni horloge, ni DOM, ni globale, ni aléa.
|
||||
Mêmes entrées + mêmes données = même résultat. `test/paquet.test.mjs` grep `src/` et refuse
|
||||
`fetch`, `require`, `node:*`, `process`, `Date`, `Math.random`, `console`, `localStorage`, `import *.json`.
|
||||
2. Les données (prix, barèmes, options, tables) entrent en paramètre, validées par `valider(table, json)`
|
||||
par l'appelant. Aucune constante de prix dans `src/`. Une mise à jour de prix ne produit jamais une
|
||||
version du paquet. `algo-config.json` est une donnée : livrée dans `fixtures/`, injectée, jamais importée.
|
||||
3. Chaque coefficient physique existe **une fois**, dans `src/commun/coefficients.ts`, avec `valeur`,
|
||||
`source`, `date` (et `note` pour le veto). Tout coefficient lu apparaît dans `resultat.sources` (les
|
||||
moteurs d'axe enregistrent les lectures de la table par Proxy) ; toute valeur par défaut appliquée
|
||||
dans `resultat.avertissements`. Les heuristiques de conception (poids, modulateurs, matrices,
|
||||
tolérances) restent dans `algo-config.json`, marquées non sourcées. La table lisible par Jules est
|
||||
`docs/CONSTANTES.md` ; sans veto sous une semaine, la table du code est la référence.
|
||||
4. Aucune dépendance de production (`typescript` en dev seulement). `dist/` (JS ESM + `.d.ts`) est
|
||||
**committé** à chaque étiquette ; `schemas/*.schema.json` est réécrit par `npm run build` depuis
|
||||
`src/commun/schemas.ts` (source de vérité).
|
||||
5. Une formule change → nouvelle étiquette (patch : correction ; mineur : nouvelle sortie ; majeur :
|
||||
entrée modifiée) et montée de version chez les deux consommateurs le même jour. **Jamais de branche
|
||||
épinglée** chez un consommateur. `src/version.ts` = `package.json` (test).
|
||||
6. Tests hors ligne (`node --test test/`, sur `dist/`), fixtures figées et datées dans `fixtures/`.
|
||||
Les 28 tests d'origine du cœur autonomie (`test/noyau-origine.test.mjs`) et le test de découpage par
|
||||
axe (`test/decoupage.test.mjs`, épingle générée depuis le code d'astro-pro) **ne se désactivent
|
||||
jamais**. Refiger une épingle (`scripts/pin-origine.mjs`, `npm run ecarts -- --ecrire <date>`) est un
|
||||
acte volontaire, écrit dans le commit, après relecture de `docs/CONSTANTES.md`.
|
||||
7. Français avec accents dans les libellés, la doc et les messages ; identifiants en français sans
|
||||
accents (`sous_total_eur_ht`, `taux_couverture`) ; codes d'axe internes `eau` / `energie` / `alim`
|
||||
(les postes publiés disent `alimentation`).
|
||||
8. Un module du noyau (`src/autonomie/noyau/`) se modifie pour corriger un bug **avec un test qui le
|
||||
montre** et une ligne dans le `CHANGELOG` du commit ; jamais « en passant ». Les bugs latents connus
|
||||
sont listés dans le RECAP du lot P1.0 (scoreurs sans parse de `parametre_dim`, `_conflit_avec` perdu,
|
||||
Intl dans les messages de `reajuste`).
|
||||
|
||||
## Carte du dépôt
|
||||
|
||||
```
|
||||
src/
|
||||
├── version.ts étiquette portée par chaque résultat (= package.json)
|
||||
├── commun/
|
||||
│ ├── contrat.ts Source, Resultat, Moteur, Trace (collecteur sources/avertissements)
|
||||
│ ├── coefficients.ts LA table (valeur, source, date, note) + verifieTableCoefficients
|
||||
│ ├── contexte.ts commun.contexte : CP → département (2A/2B, 97x) → zone RT, biogéo, pluvio, DJU
|
||||
│ ├── geometrie.ts commun.geometrie : emprise, niveaux, hauteur, pente, mitoyenneté → parois, SHAB, volume
|
||||
│ ├── argent.ts fourchettes, arrondis, mensualité / annuité / coût du crédit
|
||||
│ ├── schemas.ts forme de chaque JSON de données (→ schemas/*.schema.json au build)
|
||||
│ └── valider.ts valider(table, json) : type, required, properties, items, enum, minItems
|
||||
└── autonomie/
|
||||
├── noyau/ V2 de calculs, sans singleton : algo (selectionner + finaliseSelection),
|
||||
│ pondere-axes, alloue-budget, selectionne-poste, adequation, scoreurs,
|
||||
│ dimension, couplages (copie, ne mute plus), reajuste, axes-actifs, types
|
||||
├── axe.ts fabrique des moteurs d'axe (selectionnePoste + finaliseSelection à un axe + taux)
|
||||
├── eau.ts · energie.ts · alimentation.ts
|
||||
├── integrateur.ts contexte + poids + budgets ; mode 'global' (= calculs) ou 'par_axe' (= plateforme)
|
||||
├── taux.ts couverture eau / énergie (autoconsommation appliquée) / alimentation
|
||||
└── prix.ts prixOption, prixPreset, presetLePlusProche (données d'options, pas de montant)
|
||||
schemas/ fixtures/ test/ scripts/ docs/CONSTANTES.md dist/
|
||||
```
|
||||
|
||||
## Comment publier
|
||||
|
||||
```
|
||||
npm test # pretest = build ; 4 fichiers de tests + les 28 d'origine, tous hors ligne
|
||||
git add -A && git commit
|
||||
git tag vX.Y.Z && git push && git push --tags
|
||||
```
|
||||
puis monter `#vX.Y.Z` dans `reinnover/package.json` (et `astro-pro` à partir de P1.5) **le même jour**,
|
||||
`npm install` chez le consommateur, commit du lockfile.
|
||||
|
||||
## Ce qu'un agent ne fait pas ici
|
||||
|
||||
Lire NocoDB · appeler une API · écrire un prix ou un barème · importer depuis `astro-pro` ou
|
||||
`reinnover` · importer un JSON dans `src/` · désactiver ou affaiblir un test · épingler une branche ·
|
||||
ajouter une dépendance de production · publier sans `dist/` reconstruit et committé.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Jules Rouaud (Transformations résilientes)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,90 @@
|
||||
# @transformer/moteurs
|
||||
|
||||
Moteurs de calcul **purs** pour la rénovation et l'autonomie d'une maison, partagés par
|
||||
[calculs.trans-former.fr](https://calculs.trans-former.fr) et [plateforme.trans-former.fr](https://plateforme.trans-former.fr).
|
||||
Un moteur reçoit des entrées et des données, rend des sorties **avec ses sources et ses avertissements**,
|
||||
et ne lit rien d'autre : ni réseau, ni disque, ni horloge. **Jamais de prix dans le paquet** : les prix,
|
||||
barèmes et tables sont des données publiées ailleurs et injectées.
|
||||
|
||||
Licence MIT (le contenu de la plateforme reste sous sa propre licence).
|
||||
|
||||
## Ce que le paquet contient (v0.1.0)
|
||||
|
||||
| Moteur (`id`) | Entrées | Données injectées | Sorties |
|
||||
|---|---|---|---|
|
||||
| `commun.contexte` | code postal | `france_climat` | département (Corse 2A/2B, DOM 97x), zone RT, zone biogéographique, pluviométrie, DJU |
|
||||
| `commun.geometrie` | emprise, niveaux, hauteur, pente, mitoyenneté, SHAB connue (facultatif) | — | murs extérieurs et opaques, vitrage, toiture, plancher bas, SHAB estimée, volume |
|
||||
| `autonomie.eau` · `autonomie.energie` · `autonomie.alimentation` | autodiag, budget d'axe, contexte | `postes`, `options`, `algo_config` | postes retenus (typologie, dimension, coût), alternatives, statut, journal des couplages, taux de couverture de l'axe |
|
||||
| `autonomie.integrateur` | autodiag, budget total, mode `global` (= calculs) ou `par_axe` (= plateforme), axes actifs | idem + `france_climat` | contexte, poids d'axes, budget par axe, sélection par axe, total, statut, taux par axe |
|
||||
|
||||
Plus, dans `commun` : la table des coefficients (`COEFFICIENTS`), `valider(table, json)`, les helpers
|
||||
d'argent (fourchettes, mensualité, annuité) ; dans `autonomie` : le noyau V2 complet (`autonomie.noyau.selectionner`),
|
||||
les taux de couverture et les prix d'options (`prixOption`, `prixPreset`).
|
||||
|
||||
## Consommer
|
||||
|
||||
```json
|
||||
"dependencies": {
|
||||
"@transformer/moteurs": "git+https://git.trans-former.fr/jules/moteurs.git#v0.1.0"
|
||||
}
|
||||
```
|
||||
|
||||
Toujours une **étiquette** (`#vX.Y.Z`), jamais une branche : deux sites qui ne calculent pas pareil sans
|
||||
que personne ne le sache est exactement le risque à éviter. Le dépôt est public, `dist/` est committé :
|
||||
l'installation ne demande aucune étape de build, seulement `git` sur la machine qui fait `npm ci`
|
||||
(piège connu : `node:22-alpine` n'a pas `git` → `apk add --no-cache git`).
|
||||
|
||||
```ts
|
||||
import { contexte, eau, integrateur, valider } from '@transformer/moteurs'
|
||||
|
||||
// 1. les données viennent de l'extérieur (URL publiée, instantané daté…) et se valident à la frontière
|
||||
for (const [table, json] of Object.entries(donnees)) {
|
||||
const v = valider(table, json)
|
||||
if (!v.ok) throw new Error(v.erreurs.join('\n'))
|
||||
}
|
||||
|
||||
// 2. contexte depuis le code postal
|
||||
const ctx = contexte.calculer({ code_postal: '31170' }, { france_climat: donnees.france_climat }).sorties
|
||||
|
||||
// 3. un moteur d'axe sur son budget
|
||||
const r = eau.calculer(
|
||||
{ autodiag: { nb_personnes: 4, surface_toit_m2: 80, surface_parcelle_m2: 800, orientation_toit: 'sud-est', temps_dispo: 2 }, budget_axe: 14000, contexte: ctx },
|
||||
{ postes: donnees.postes, options: donnees.options, algo_config: donnees.algo_config },
|
||||
)
|
||||
r.sorties.postes // typologies retenues, dimensions, coûts
|
||||
r.sorties.taux_couverture // % du besoin substituable couvert
|
||||
r.sources // chaque coefficient et chaque prix utilisés, avec source et date
|
||||
r.avertissements // chaque défaut appliqué, chaque donnée manquante
|
||||
r.version // '0.1.0'
|
||||
```
|
||||
|
||||
Le mode `global` de l'intégrateur reproduit exactement le simulateur de calculs (pipeline complet,
|
||||
couplages et réajustement entre axes) ; le mode `par_axe` calcule chaque branche du menu sur son
|
||||
budget et additionne.
|
||||
|
||||
Sous-chemins : `@transformer/moteurs/commun`, `/autonomie`, `/autonomie/eau`, `/autonomie/energie`,
|
||||
`/autonomie/alimentation`, `/autonomie/integrateur`, `/autonomie/noyau`, `/schemas/<table>.schema.json`,
|
||||
`/fixtures/<fichier>` (`algo-config.json`, `profils-reference.json`).
|
||||
|
||||
## Les coefficients
|
||||
|
||||
Chaque grandeur physique existe une fois, dans `src/commun/coefficients.ts`, avec sa source et sa date ;
|
||||
la version lisible est [`docs/CONSTANTES.md`](docs/CONSTANTES.md) (ancienne valeur → retenue, source,
|
||||
ce que ça change). Les heuristiques de conception du simulateur (poids d'axes, matrices, tolérances) sont
|
||||
des données : `fixtures/algo-config.json`, injectées.
|
||||
|
||||
## Développer
|
||||
|
||||
```bash
|
||||
npm install # typescript seulement
|
||||
npm test # build (tsc → dist/, schémas) puis node --test test/ — hors ligne
|
||||
npm run ecarts # table des écarts origine → harmonisée sur les trois profils de référence
|
||||
```
|
||||
|
||||
Règles du dépôt et carte détaillée : [`AGENTS.md`](AGENTS.md). Jeux de données figés : [`fixtures/README.md`](fixtures/README.md).
|
||||
|
||||
## Publier une étiquette
|
||||
|
||||
`npm test` vert → `git commit` (avec `dist/` et `schemas/` reconstruits) → `git tag vX.Y.Z` →
|
||||
`git push && git push --tags` → monter la version chez les consommateurs le même jour. Patch = correction
|
||||
de formule ; mineur = nouvelle sortie ou nouveau moteur ; majeur = entrée modifiée.
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
export type { EntreesAxe, DonneesAutonomie, SortiesAxe } from './axe.js';
|
||||
export declare const alimentation: import("../index.js").Moteur<import("./axe.js").EntreesAxe, import("./axe.js").DonneesAutonomie, import("./axe.js").SortiesAxe>;
|
||||
export default alimentation;
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
// alimentation.ts — moteur `autonomie.alimentation` : étude foncière, strates
|
||||
// haute, moyenne (haies) et basse, pour un budget d'axe donné.
|
||||
// Code d'axe interne : 'alim' (postes.json dit « alimentation »).
|
||||
import { moteurAxe } from './axe.js';
|
||||
export const alimentation = moteurAxe('alim', 'autonomie.alimentation');
|
||||
export default alimentation;
|
||||
Vendored
+45
@@ -0,0 +1,45 @@
|
||||
import type { Moteur, Resultat } from '../commun/contrat.js';
|
||||
import { Trace } from '../commun/contrat.js';
|
||||
import { type TableCoefficients } from '../commun/coefficients.js';
|
||||
import type { Contexte } from '../commun/contexte.js';
|
||||
import type { AlgoConfig, Autodiag, Axe, Option, Poste, PosteResultat, Statut } from './noyau/types.js';
|
||||
import { type DetailAlim, type DetailEau, type DetailEnergie } from './taux.js';
|
||||
export interface EntreesAxe {
|
||||
autodiag: Autodiag;
|
||||
/** Budget alloué à cet axe (EUR HT). */
|
||||
budget_axe: number;
|
||||
/** Contexte climatique (moteur `commun.contexte`). */
|
||||
contexte: Contexte;
|
||||
}
|
||||
export interface DonneesAutonomie {
|
||||
postes: Poste[];
|
||||
options: Option[];
|
||||
algo_config: AlgoConfig;
|
||||
/** Table de coefficients ; par défaut la table harmonisée du paquet. */
|
||||
coefficients?: TableCoefficients;
|
||||
}
|
||||
export interface SortiesAxe {
|
||||
axe: Axe;
|
||||
postes: PosteResultat[];
|
||||
/** Codes des typologies retenues, dans l'ordre des postes. */
|
||||
typologies: string[];
|
||||
sous_total_eur_ht: number;
|
||||
budget_alloue: number;
|
||||
ecart_budget_pct: number;
|
||||
status: Statut;
|
||||
message_status?: string;
|
||||
/** Journal des couplages (FORCE / MANQUANT / WARN / CONFLIT). */
|
||||
couplages: string[];
|
||||
/** Taux de couverture de l'axe en %, null si non calculable. */
|
||||
taux_couverture: number | null;
|
||||
taux_detail: DetailEau | DetailEnergie | DetailAlim | null;
|
||||
}
|
||||
/** Enregistre les clés de coefficients lues pendant un calcul (Proxy en lecture seule). */
|
||||
export declare function enregistreLectures(table: TableCoefficients): {
|
||||
table: TableCoefficients;
|
||||
lues: Set<keyof TableCoefficients>;
|
||||
};
|
||||
/** Ajoute aux sources les coefficients lus et les prix des typologies retenues. */
|
||||
export declare function traceSelection(trace: Trace, coefficients: TableCoefficients, lues: Set<keyof TableCoefficients>, postes: PosteResultat[], options: Option[], couplages: string[]): void;
|
||||
export declare function calculeAxe(axe: Axe, entrees: EntreesAxe, donnees: DonneesAutonomie): Resultat<SortiesAxe>;
|
||||
export declare function moteurAxe(axe: Axe, id: string): Moteur<EntreesAxe, DonneesAutonomie, SortiesAxe>;
|
||||
Vendored
+97
@@ -0,0 +1,97 @@
|
||||
// axe.ts — fabrique des moteurs d'axe `autonomie.eau`, `autonomie.energie`,
|
||||
// `autonomie.alimentation` (LOT P1.0, découpage par axe).
|
||||
//
|
||||
// Un moteur d'axe = selectionnePoste(axe) + couplages de l'axe + réajustement
|
||||
// au budget de l'axe + taux de couverture de l'axe. Il réutilise la queue du
|
||||
// pipeline d'origine (`finaliseSelection`) sur une sélection où les deux
|
||||
// autres axes sont vides et désactivés (`configPourAxes`) : par construction,
|
||||
// il rend exactement ce que `selectionner()` rend en mode « un seul axe
|
||||
// actif » avec ce budget — c'est le test de non-régression du découpage.
|
||||
import { Trace } from '../commun/contrat.js';
|
||||
import { COEFFICIENTS } from '../commun/coefficients.js';
|
||||
import { VERSION } from '../version.js';
|
||||
import { finaliseSelection } from './noyau/algo.js';
|
||||
import { configPourAxes } from './noyau/axes-actifs.js';
|
||||
import { selectionnePoste } from './noyau/selectionne-poste.js';
|
||||
import { AXES } from './noyau/types.js';
|
||||
import { aCaptage, batterieSelectionnee, kwcSelectionnes, mlHaiesSelectionnes, tauxAlim, tauxEau, tauxEnergie } from './taux.js';
|
||||
/** Enregistre les clés de coefficients lues pendant un calcul (Proxy en lecture seule). */
|
||||
export function enregistreLectures(table) {
|
||||
const lues = new Set();
|
||||
const proxy = new Proxy(table, {
|
||||
get(cible, prop, recepteur) {
|
||||
if (typeof prop === 'string' && prop in cible)
|
||||
lues.add(prop);
|
||||
return Reflect.get(cible, prop, recepteur);
|
||||
},
|
||||
});
|
||||
return { table: proxy, lues };
|
||||
}
|
||||
function axeVide() {
|
||||
return { postes: [], sous_total_eur_ht: 0, budget_alloue: 0, poids: 0 };
|
||||
}
|
||||
/** Ajoute aux sources les coefficients lus et les prix des typologies retenues. */
|
||||
export function traceSelection(trace, coefficients, lues, postes, options, couplages) {
|
||||
for (const cle of lues) {
|
||||
const c = coefficients[cle];
|
||||
const v = c.valeur;
|
||||
trace.source(cle, typeof v === 'number' || typeof v === 'string' ? v : JSON.stringify(v), c.source, c.date);
|
||||
}
|
||||
for (const p of postes) {
|
||||
const o = options.find((x) => x.code === p.typologie_choisie);
|
||||
trace.source(`option.${p.typologie_choisie}`, p.cout_eur_ht, String(o?.source ?? 'options (calculs.trans-former.fr)'), String(o?.source_date ?? ''));
|
||||
}
|
||||
for (const ligne of couplages) {
|
||||
if (ligne.startsWith('MANQUANT') || ligne.startsWith('WARN'))
|
||||
trace.avertit(`couplage : ${ligne}`);
|
||||
}
|
||||
}
|
||||
export function calculeAxe(axe, entrees, donnees) {
|
||||
const trace = new Trace();
|
||||
const { table: coefficients, lues } = enregistreLectures(donnees.coefficients ?? COEFFICIENTS);
|
||||
const actifs = Object.fromEntries(AXES.map((a) => [a, a === axe]));
|
||||
const reglages = { config: configPourAxes(donnees.algo_config, actifs), coefficients };
|
||||
const inputs = entrees.autodiag;
|
||||
const budget = Math.max(0, entrees.budget_axe || 0);
|
||||
const selection = {};
|
||||
for (const a of AXES)
|
||||
selection[a] = a === axe ? selectionnePoste(budget, inputs, entrees.contexte, donnees.options, donnees.postes, a, reglages) : axeVide();
|
||||
selection[axe].poids = 100;
|
||||
const fin = finaliseSelection(selection, budget, donnees.options, donnees.postes, inputs, entrees.contexte, reglages);
|
||||
const postes = fin.axes[axe]?.postes ?? [];
|
||||
const sous_total = fin.axes[axe]?.sous_total_eur_ht ?? 0;
|
||||
// Un poste forcé dans un autre axe par un couplage inter-axes serait perdu ici :
|
||||
// aucun dans les données (inventaire §4), on le signale si ça arrive.
|
||||
for (const a of AXES) {
|
||||
if (a !== axe && (fin.axes[a]?.postes.length ?? 0) > 0)
|
||||
trace.avertit(`couplage inter-axes : ${fin.axes[a].postes.length} poste(s) forcé(s) dans l'axe ${a}, hors du périmètre du moteur ${axe}`);
|
||||
}
|
||||
let taux;
|
||||
if (axe === 'eau') {
|
||||
taux = tauxEau({ surface_toit_m2: inputs.surface_toit_m2, nb_personnes: inputs.nb_personnes, pluviometrie_mm: entrees.contexte.pluviometrie_mm, a_captage: aCaptage(postes) }, coefficients, trace);
|
||||
}
|
||||
else if (axe === 'energie') {
|
||||
taux = tauxEnergie({ kwc: kwcSelectionnes(postes), batterie_kwh: batterieSelectionnee(postes), orientation_toit: inputs.orientation_toit, nb_personnes: inputs.nb_personnes, contexte: entrees.contexte }, coefficients, trace);
|
||||
}
|
||||
else {
|
||||
taux = tauxAlim({ ml: mlHaiesSelectionnes(postes), nb_personnes: inputs.nb_personnes }, coefficients, trace);
|
||||
}
|
||||
traceSelection(trace, donnees.coefficients ?? COEFFICIENTS, lues, postes, donnees.options, fin.couplages);
|
||||
const sorties = {
|
||||
axe,
|
||||
postes,
|
||||
typologies: postes.map((p) => p.typologie_choisie),
|
||||
sous_total_eur_ht: sous_total,
|
||||
budget_alloue: budget,
|
||||
ecart_budget_pct: budget > 0 ? +(((sous_total - budget) / budget) * 100).toFixed(2) : 0,
|
||||
status: fin.status,
|
||||
message_status: fin.message_status,
|
||||
couplages: fin.couplages,
|
||||
taux_couverture: taux.taux,
|
||||
taux_detail: taux.detail,
|
||||
};
|
||||
return trace.resultat(sorties, VERSION);
|
||||
}
|
||||
export function moteurAxe(axe, id) {
|
||||
return { id, calculer: (entrees, donnees) => calculeAxe(axe, entrees, donnees) };
|
||||
}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
export type { EntreesAxe, DonneesAutonomie, SortiesAxe } from './axe.js';
|
||||
export declare const eau: import("../index.js").Moteur<import("./axe.js").EntreesAxe, import("./axe.js").DonneesAutonomie, import("./axe.js").SortiesAxe>;
|
||||
export default eau;
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
// eau.ts — moteur `autonomie.eau` : captage, filtration, stockage,
|
||||
// distribution, eaux grises et épuration, pour un budget d'axe donné.
|
||||
import { moteurAxe } from './axe.js';
|
||||
export const eau = moteurAxe('eau', 'autonomie.eau');
|
||||
export default eau;
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
export type { EntreesAxe, DonneesAutonomie, SortiesAxe } from './axe.js';
|
||||
export declare const energie: import("../index.js").Moteur<import("./axe.js").EntreesAxe, import("./axe.js").DonneesAutonomie, import("./axe.js").SortiesAxe>;
|
||||
export default energie;
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
// energie.ts — moteur `autonomie.energie` : photovoltaïque, solaire thermique,
|
||||
// chauffage bois, émetteurs, pilotage, pour un budget d'axe donné.
|
||||
import { moteurAxe } from './axe.js';
|
||||
export const energie = moteurAxe('energie', 'autonomie.energie');
|
||||
export default energie;
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
export { eau } from './eau.js';
|
||||
export { energie } from './energie.js';
|
||||
export { alimentation } from './alimentation.js';
|
||||
export { integrateur, calculeIntegrateur, agregeStatuts } from './integrateur.js';
|
||||
export type { EntreesIntegrateur, DonneesIntegrateur, SortiesIntegrateur } from './integrateur.js';
|
||||
export { calculeAxe, moteurAxe } from './axe.js';
|
||||
export type { EntreesAxe, DonneesAutonomie, SortiesAxe } from './axe.js';
|
||||
export * from './taux.js';
|
||||
export * from './prix.js';
|
||||
export * as noyau from './noyau/index.js';
|
||||
export type { Axe, Autodiag, InputsSimulation, Poste, Option, AlgoConfig, PosteResultat, AxeResultat, Statut, PoidsAxes } from './noyau/types.js';
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
// @transformer/moteurs/autonomie — trois moteurs d'axe, un intégrateur, le noyau.
|
||||
export { eau } from './eau.js';
|
||||
export { energie } from './energie.js';
|
||||
export { alimentation } from './alimentation.js';
|
||||
export { integrateur, calculeIntegrateur, agregeStatuts } from './integrateur.js';
|
||||
export { calculeAxe, moteurAxe } from './axe.js';
|
||||
export * from './taux.js';
|
||||
export * from './prix.js';
|
||||
export * as noyau from './noyau/index.js';
|
||||
Vendored
+37
@@ -0,0 +1,37 @@
|
||||
import type { Moteur, Resultat } from '../commun/contrat.js';
|
||||
import type { Contexte, LigneClimat } from '../commun/contexte.js';
|
||||
import { type DonneesAutonomie, type SortiesAxe } from './axe.js';
|
||||
import { type AxesActifs } from './noyau/axes-actifs.js';
|
||||
import type { Autodiag, Axe, PoidsAxes, Statut } from './noyau/types.js';
|
||||
export interface EntreesIntegrateur {
|
||||
autodiag: Autodiag;
|
||||
budget_total: number;
|
||||
/** 'global' = pipeline d'origine (calculs) ; 'par_axe' = somme des moteurs d'axe (plateforme). */
|
||||
mode?: 'global' | 'par_axe';
|
||||
/** Axes désactivés par l'habitant (toggles) ; absent = les trois. */
|
||||
axes_actifs?: AxesActifs;
|
||||
/** P1.5 : aménagements cochés ; ignoré en v0.1 (signalé). */
|
||||
amenagements_coches?: string[];
|
||||
}
|
||||
export interface DonneesIntegrateur extends DonneesAutonomie {
|
||||
france_climat: LigneClimat[];
|
||||
}
|
||||
export interface SortiesIntegrateur {
|
||||
mode: 'global' | 'par_axe';
|
||||
contexte: Contexte;
|
||||
poids: PoidsAxes;
|
||||
budgets: PoidsAxes;
|
||||
axes: Record<Axe, Pick<SortiesAxe, 'postes' | 'typologies' | 'sous_total_eur_ht' | 'budget_alloue' | 'status' | 'taux_couverture' | 'taux_detail'>>;
|
||||
total_eur_ht: number;
|
||||
budget_cible: number;
|
||||
ecart_budget_pct: number;
|
||||
status: Statut;
|
||||
message_status?: string;
|
||||
couplages: string[];
|
||||
taux_couverture: Record<Axe, number | null>;
|
||||
}
|
||||
/** Statut agrégé : BUDGET_INSUFFISANT l'emporte, puis AJOUTER_OPTIONNEL, sinon OK. */
|
||||
export declare function agregeStatuts(statuts: Statut[]): Statut;
|
||||
export declare function calculeIntegrateur(entrees: EntreesIntegrateur, donnees: DonneesIntegrateur): Resultat<SortiesIntegrateur>;
|
||||
export declare const integrateur: Moteur<EntreesIntegrateur, DonneesIntegrateur, SortiesIntegrateur>;
|
||||
export default integrateur;
|
||||
Vendored
+114
@@ -0,0 +1,114 @@
|
||||
// integrateur.ts — moteur `autonomie.integrateur` : contexte + pondération des
|
||||
// axes + allocation du budget + agrégation.
|
||||
//
|
||||
// Deux modes :
|
||||
// - 'global' (défaut) : le pipeline complet d'origine (`selectionner`), avec
|
||||
// couplages et réajustement GLOBAUX entre axes — celui de calculs ;
|
||||
// - 'par_axe' : chaque axe est calculé par son moteur d'axe sur son budget,
|
||||
// puis les sorties sont additionnées — celui du menu de la plateforme.
|
||||
// Le mode « aménagements cochés » (recalcul sans relancer l'allocation) est
|
||||
// P1.5 : une liste transmise est ignorée et signalée.
|
||||
import { Trace } from '../commun/contrat.js';
|
||||
import { COEFFICIENTS } from '../commun/coefficients.js';
|
||||
import { deriveContexte } from '../commun/contexte.js';
|
||||
import { VERSION } from '../version.js';
|
||||
import { calculeAxe, enregistreLectures, traceSelection } from './axe.js';
|
||||
import { selectionner } from './noyau/algo.js';
|
||||
import { alloueBudget } from './noyau/alloue-budget.js';
|
||||
import { configPourAxes } from './noyau/axes-actifs.js';
|
||||
import { pondereAxes } from './noyau/pondere-axes.js';
|
||||
import { AXES } from './noyau/types.js';
|
||||
import { aCaptage, batterieSelectionnee, kwcSelectionnes, mlHaiesSelectionnes, tauxAlim, tauxEau, tauxEnergie } from './taux.js';
|
||||
/** Statut agrégé : BUDGET_INSUFFISANT l'emporte, puis AJOUTER_OPTIONNEL, sinon OK. */
|
||||
export function agregeStatuts(statuts) {
|
||||
if (statuts.includes('BUDGET_INSUFFISANT'))
|
||||
return 'BUDGET_INSUFFISANT';
|
||||
if (statuts.includes('AJOUTER_OPTIONNEL'))
|
||||
return 'AJOUTER_OPTIONNEL';
|
||||
return 'OK';
|
||||
}
|
||||
export function calculeIntegrateur(entrees, donnees) {
|
||||
const trace = new Trace();
|
||||
const mode = entrees.mode ?? 'global';
|
||||
const coefficients_base = donnees.coefficients ?? COEFFICIENTS;
|
||||
const { table: coefficients, lues } = enregistreLectures(coefficients_base);
|
||||
const config = configPourAxes(donnees.algo_config, entrees.axes_actifs);
|
||||
const inputs = { ...entrees.autodiag, budget_eur: entrees.budget_total };
|
||||
if (entrees.amenagements_coches && entrees.amenagements_coches.length) {
|
||||
trace.avertit('aménagements cochés ignorés : le mode « coché » arrive en P1.5');
|
||||
}
|
||||
const ctx = deriveContexte(inputs.code_postal, donnees.france_climat, coefficients);
|
||||
trace.absorbe(ctx);
|
||||
const contexte = ctx.sorties;
|
||||
const poids = pondereAxes(inputs, contexte, config, false);
|
||||
const budgets = alloueBudget(entrees.budget_total, poids);
|
||||
const axes = {};
|
||||
const taux_couverture = {};
|
||||
let total = 0;
|
||||
let status;
|
||||
let message_status;
|
||||
let couplages = [];
|
||||
if (mode === 'par_axe') {
|
||||
const statuts = [];
|
||||
for (const axe of AXES) {
|
||||
const actif = !entrees.axes_actifs || entrees.axes_actifs[axe] !== false;
|
||||
if (!actif || budgets[axe] <= 0) {
|
||||
axes[axe] = { postes: [], typologies: [], sous_total_eur_ht: 0, budget_alloue: budgets[axe], status: 'OK', taux_couverture: null, taux_detail: null };
|
||||
taux_couverture[axe] = null;
|
||||
continue;
|
||||
}
|
||||
const r = calculeAxe(axe, { autodiag: entrees.autodiag, budget_axe: budgets[axe], contexte }, { ...donnees, coefficients: coefficients_base });
|
||||
trace.absorbe(r);
|
||||
const s = r.sorties;
|
||||
axes[axe] = { postes: s.postes, typologies: s.typologies, sous_total_eur_ht: s.sous_total_eur_ht, budget_alloue: s.budget_alloue, status: s.status, taux_couverture: s.taux_couverture, taux_detail: s.taux_detail };
|
||||
taux_couverture[axe] = s.taux_couverture;
|
||||
total += s.sous_total_eur_ht;
|
||||
statuts.push(s.status);
|
||||
couplages = couplages.concat(s.couplages);
|
||||
}
|
||||
status = agregeStatuts(statuts);
|
||||
}
|
||||
else {
|
||||
const r = selectionner(inputs, donnees.options, donnees.postes, donnees.france_climat, { config, coefficients }, false);
|
||||
const tous = [];
|
||||
for (const axe of AXES) {
|
||||
const a = r.axes[axe];
|
||||
const postes = a?.postes ?? [];
|
||||
tous.push(...postes);
|
||||
let t;
|
||||
if (axe === 'eau')
|
||||
t = tauxEau({ surface_toit_m2: inputs.surface_toit_m2, nb_personnes: inputs.nb_personnes, pluviometrie_mm: contexte.pluviometrie_mm, a_captage: aCaptage(postes) }, coefficients, trace);
|
||||
else if (axe === 'energie')
|
||||
t = tauxEnergie({ kwc: kwcSelectionnes(postes), batterie_kwh: batterieSelectionnee(postes), orientation_toit: inputs.orientation_toit, nb_personnes: inputs.nb_personnes, contexte }, coefficients, trace);
|
||||
else
|
||||
t = tauxAlim({ ml: mlHaiesSelectionnes(postes), nb_personnes: inputs.nb_personnes }, coefficients, trace);
|
||||
axes[axe] = { postes, typologies: postes.map((p) => p.typologie_choisie), sous_total_eur_ht: a?.sous_total_eur_ht ?? 0, budget_alloue: a?.budget_alloue ?? budgets[axe], status: r.status, taux_couverture: t.taux, taux_detail: t.detail };
|
||||
taux_couverture[axe] = t.taux;
|
||||
}
|
||||
total = r.total_eur_ht;
|
||||
status = r.status;
|
||||
message_status = r.message_status;
|
||||
couplages = r.couplages;
|
||||
traceSelection(trace, coefficients_base, lues, tous, donnees.options, couplages);
|
||||
}
|
||||
const sorties = {
|
||||
mode,
|
||||
contexte,
|
||||
poids,
|
||||
budgets,
|
||||
axes,
|
||||
total_eur_ht: total,
|
||||
budget_cible: entrees.budget_total,
|
||||
ecart_budget_pct: entrees.budget_total > 0 ? +(((total - entrees.budget_total) / entrees.budget_total) * 100).toFixed(2) : 0,
|
||||
status,
|
||||
message_status,
|
||||
couplages,
|
||||
taux_couverture,
|
||||
};
|
||||
return trace.resultat(sorties, VERSION);
|
||||
}
|
||||
export const integrateur = {
|
||||
id: 'autonomie.integrateur',
|
||||
calculer: calculeIntegrateur,
|
||||
};
|
||||
export default integrateur;
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
import type { Autodiag, Contexte, Option, Poste, Reglages } from './types.js';
|
||||
export interface AdequationDebug {
|
||||
score: number;
|
||||
scoreurs: Record<string, {
|
||||
score: number;
|
||||
poids: number;
|
||||
}>;
|
||||
avertissement?: string;
|
||||
}
|
||||
/**
|
||||
* Calcule le score d'adéquation contextuelle d'une typologie [0..1].
|
||||
*
|
||||
* @param typologies_selectionnees — sélections déjà faites (pour score_couplage_disponible)
|
||||
* @returns score ∈ [0..1] ; en debug, { score, scoreurs }
|
||||
*/
|
||||
export declare function adequation_contexte(typo: Option, inputs: Autodiag, poste: Poste, contexte_derived: Contexte, reglages: Reglages, typologies_selectionnees?: string[], debug?: false): number;
|
||||
export declare function adequation_contexte(typo: Option, inputs: Autodiag, poste: Poste, contexte_derived: Contexte, reglages: Reglages, typologies_selectionnees: string[], debug: true): AdequationDebug;
|
||||
Vendored
+59
@@ -0,0 +1,59 @@
|
||||
// adequation.ts — adequation_contexte(typo, inputs, poste, contexte_derived) → [0..1]
|
||||
// Spec: V2-algo-spec-FONCTIONS.md §1 — origine calculs/src/lib/autonomie/adequation.js.
|
||||
// Correction LOT P1.0 : `reglages` en paramètre (plus de getConfig()) ; le
|
||||
// console.warn de debug devient un champ `avertissement` du retour debug.
|
||||
import { clamp01 } from './calibrage.js';
|
||||
import { score_pluviometrie, score_climat_chauffage, score_climat_meridional, score_taille_foyer, score_surface_toit, score_orientation_toit, score_surface_parcelle, score_temps_dispo, score_metres_haie, score_couplage_disponible, } from './scoreurs.js';
|
||||
// Catalogue de dispatch des scoreurs par nom de critère
|
||||
const SCOREURS = {
|
||||
pluviometrie: score_pluviometrie,
|
||||
climat_chauffage: score_climat_chauffage,
|
||||
climat_meridional: score_climat_meridional,
|
||||
taille_foyer: score_taille_foyer,
|
||||
surface_toit: score_surface_toit,
|
||||
orientation_toit: score_orientation_toit,
|
||||
surface_parcelle: score_surface_parcelle,
|
||||
temps_dispo: score_temps_dispo,
|
||||
metres_haie_possibles: score_metres_haie,
|
||||
couplage_disponible: score_couplage_disponible,
|
||||
};
|
||||
export function adequation_contexte(typo, inputs, poste, contexte_derived, reglages, typologies_selectionnees = [], debug = false) {
|
||||
const matrix = reglages.config.adequation_weights?.[poste.code];
|
||||
if (!matrix) {
|
||||
if (debug)
|
||||
return { score: 0.5, scoreurs: {}, avertissement: `[adequation_contexte] Pas de matrice pour poste "${poste.code}" — fallback 0.5` };
|
||||
return 0.5;
|
||||
}
|
||||
let weighted_sum = 0;
|
||||
let total_weight = 0;
|
||||
const debug_scores = {};
|
||||
for (const [critere, poids] of Object.entries(matrix)) {
|
||||
if (poids === 0 || !SCOREURS[critere])
|
||||
continue;
|
||||
const scoreur = SCOREURS[critere];
|
||||
let s;
|
||||
if (critere === 'couplage_disponible') {
|
||||
s = scoreur(typo, inputs, contexte_derived, poste, typologies_selectionnees, reglages);
|
||||
}
|
||||
else {
|
||||
s = scoreur(typo, inputs, contexte_derived, poste, [], reglages);
|
||||
}
|
||||
weighted_sum += s * poids;
|
||||
total_weight += poids;
|
||||
if (debug)
|
||||
debug_scores[critere] = { score: +s.toFixed(3), poids };
|
||||
}
|
||||
let score = total_weight > 0 ? weighted_sum / total_weight : 0.5;
|
||||
// §1.4 Bonus / malus globaux
|
||||
if (typo.flag_diy && inputs.temps_dispo === 3)
|
||||
score *= 1.3;
|
||||
if (typo.complexite_pose === 3 && inputs.temps_dispo === 1)
|
||||
score *= 0.5;
|
||||
if (!typo.accessible_fr)
|
||||
score *= 0.6;
|
||||
score = clamp01(score);
|
||||
if (debug) {
|
||||
return { score, scoreurs: debug_scores };
|
||||
}
|
||||
return score;
|
||||
}
|
||||
Vendored
+28
@@ -0,0 +1,28 @@
|
||||
import type { Autodiag, Contexte, EtapeDebug, InputsSimulation, LigneClimat, Option, Poste, Reglages, Selection, SortieSelection, Statut } from './types.js';
|
||||
export interface SelectionFinalisee {
|
||||
axes: Selection;
|
||||
total_eur_ht: number;
|
||||
status: Statut;
|
||||
message_status?: string;
|
||||
couplages: string[];
|
||||
debug_reajust: string[] | null;
|
||||
}
|
||||
/**
|
||||
* Étapes 4 à 6 du pipeline d'origine : couplages contraints, réajustement au
|
||||
* budget, re-passe couplages bornée à deux tours, total. Ne touche pas à la
|
||||
* sélection reçue (appliqueCouplages copie).
|
||||
*/
|
||||
export declare function finaliseSelection(selection: Selection, budget_total: number, options: Option[], postes: Poste[], inputs: Autodiag, contexte: Contexte, reglages: Reglages, debugMode?: boolean, debug?: {
|
||||
steps: EtapeDebug[];
|
||||
}): SelectionFinalisee;
|
||||
/**
|
||||
* Sélectionne les typologies optimales selon budget + contexte.
|
||||
*
|
||||
* @param inputs — saisie utilisateur (autodiag + budget_eur)
|
||||
* @param options — typologies BDD (options.json)
|
||||
* @param postes — postes BDD (postes.json)
|
||||
* @param climat — données climatiques (france_climat.json)
|
||||
* @param reglages — { config: algo-config.json parsé, coefficients }
|
||||
* @param debugMode — active la trace debug exhaustive
|
||||
*/
|
||||
export declare function selectionner(inputs: InputsSimulation, options: Option[], postes: Poste[], climat: LigneClimat[], reglages: Reglages, debugMode?: boolean): SortieSelection;
|
||||
Vendored
+135
@@ -0,0 +1,135 @@
|
||||
// algo.ts — module principal orchestrateur des 6 étapes
|
||||
// Spec: V2-algo-spec.md + V2-algo-spec-FONCTIONS.md — origine calculs/src/lib/autonomie/algo.js.
|
||||
// Zero-dep, isomorphe navigateur/Node.
|
||||
//
|
||||
// Corrections LOT P1.0 : plus d'initConfig (les réglages passent en paramètre) ;
|
||||
// le contexte vient de commun/contexte.ts (Corse 2A/2B et DOM 971-976 corrigés,
|
||||
// une seule version) ; la sortie porte en plus `contexte` et `avertissements`.
|
||||
// C'est le pipeline COMPLET d'origine (couplages et réajustement globaux), celui
|
||||
// que calculs.trans-former.fr consommera en P1.5 sans changement de résultat.
|
||||
//
|
||||
// Découpage par axe (LOT P1.0) : la queue du pipeline (étapes 4 à 6) est
|
||||
// isolée dans `finaliseSelection`, que les moteurs d'axe réutilisent telle
|
||||
// quelle sur une sélection à un seul axe. Un moteur d'axe est donc, par
|
||||
// construction, le mode « un seul axe actif » de `selectionner`.
|
||||
import { deriveContexte } from '../../commun/contexte.js';
|
||||
import { pondereAxes } from './pondere-axes.js';
|
||||
import { alloueBudget } from './alloue-budget.js';
|
||||
import { selectionnePoste } from './selectionne-poste.js';
|
||||
import { appliqueCouplages } from './couplages.js';
|
||||
import { reajusteBudget } from './reajuste.js';
|
||||
const AXES_CODE = ['eau', 'energie', 'alim'];
|
||||
function sommeAxes(axes) {
|
||||
return Math.round(AXES_CODE.reduce((s, axe) => s + ((axes[axe] && axes[axe].sous_total_eur_ht) || 0), 0));
|
||||
}
|
||||
/**
|
||||
* Étapes 4 à 6 du pipeline d'origine : couplages contraints, réajustement au
|
||||
* budget, re-passe couplages bornée à deux tours, total. Ne touche pas à la
|
||||
* sélection reçue (appliqueCouplages copie).
|
||||
*/
|
||||
export function finaliseSelection(selection, budget_total, options, postes, inputs, contexte, reglages, debugMode = false, debug) {
|
||||
const config = reglages.config;
|
||||
// --- Étape 4 : couplages contraints ---
|
||||
const apres_couplages = appliqueCouplages(selection, options, postes, reglages, debugMode);
|
||||
if (debug)
|
||||
debug.steps.push({ etape: 4, label: 'couplages', log: apres_couplages.couplages_appliques });
|
||||
// --- Étape 5 : réajustement budget ---
|
||||
const final_pre = reajusteBudget(apres_couplages, budget_total, options, postes, inputs, contexte, reglages, debugMode);
|
||||
if (debug)
|
||||
debug.steps.push({ etape: 5, label: 'reajustement', status: final_pre.status, total: final_pre.total, log: final_pre.debug_reajust });
|
||||
// Re-passe couplages post-réajustement, avec réabsorption des coûts forcés :
|
||||
// le CAS B (underspend) peut ajouter ou upgrader des typologies ayant des
|
||||
// couplages_requis. La passe couplages force les requis manquants
|
||||
// (ex: bois_poele_hydro → bois_conduit_fumee) — coûts contraints, inclus
|
||||
// dans le total final (comportement intentionnel).
|
||||
// Correctif V3-1 (recalibrage) : ces ajouts pouvaient faire franchir le seuil
|
||||
// d'overrun SANS rééquilibrage ni changement de status (profil A : +8,9 % avec
|
||||
// status OK). Boucle bornée à 2 passes : couplages → si overrun, réajuster →
|
||||
// couplages. CAS A ne fait que réduire, la convergence est garantie ; si le
|
||||
// seuil reste franchi, reajusteBudget pose BUDGET_INSUFFISANT (status honnête).
|
||||
const tol_over = (config.tolerances && config.tolerances.budget_overrun_pct) || 5;
|
||||
const seuil_over = budget_total * (1 + tol_over / 100);
|
||||
let etat = {
|
||||
axes: final_pre.axes,
|
||||
couplages_appliques: [...(final_pre.couplages_appliques || [])],
|
||||
};
|
||||
let status_final = final_pre.status;
|
||||
let message_final = final_pre.message;
|
||||
for (let passe = 0; passe < 2; passe++) {
|
||||
// IMPORTANT: appliqueCouplages attend selection = { eau, energie, alim } directement
|
||||
const avec = appliqueCouplages(etat.axes, options, postes, reglages, false);
|
||||
etat = {
|
||||
axes: avec.axes,
|
||||
couplages_appliques: [...etat.couplages_appliques, ...(avec.couplages_appliques || [])],
|
||||
};
|
||||
if (sommeAxes(etat.axes) <= seuil_over)
|
||||
break;
|
||||
const re = reajusteBudget(etat, budget_total, options, postes, inputs, contexte, reglages, false);
|
||||
etat = {
|
||||
axes: re.axes,
|
||||
couplages_appliques: [...etat.couplages_appliques, ...(re.couplages_appliques || [])],
|
||||
};
|
||||
status_final = re.status;
|
||||
message_final = re.message;
|
||||
}
|
||||
return {
|
||||
axes: etat.axes,
|
||||
total_eur_ht: sommeAxes(etat.axes),
|
||||
status: status_final,
|
||||
message_status: message_final || undefined,
|
||||
couplages: etat.couplages_appliques,
|
||||
debug_reajust: final_pre.debug_reajust,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Sélectionne les typologies optimales selon budget + contexte.
|
||||
*
|
||||
* @param inputs — saisie utilisateur (autodiag + budget_eur)
|
||||
* @param options — typologies BDD (options.json)
|
||||
* @param postes — postes BDD (postes.json)
|
||||
* @param climat — données climatiques (france_climat.json)
|
||||
* @param reglages — { config: algo-config.json parsé, coefficients }
|
||||
* @param debugMode — active la trace debug exhaustive
|
||||
*/
|
||||
export function selectionner(inputs, options, postes, climat, reglages, debugMode = false) {
|
||||
const debug = { steps: [] };
|
||||
const config = reglages.config;
|
||||
// --- Étape 0 : dériver le contexte depuis code postal ---
|
||||
const ctx = deriveContexte(inputs.code_postal, climat, reglages.coefficients);
|
||||
const contexte = ctx.sorties;
|
||||
if (debugMode)
|
||||
debug.steps.push({ etape: 0, label: 'contexte', data: contexte });
|
||||
// --- Étape 1 : pondération axes ---
|
||||
const poids_debug = debugMode ? pondereAxes(inputs, contexte, config, true) : null;
|
||||
const poids = poids_debug ? poids_debug.poids : pondereAxes(inputs, contexte, config, false);
|
||||
if (poids_debug)
|
||||
debug.steps.push({ etape: 1, label: 'ponderation', poids, log: poids_debug.log });
|
||||
// --- Étape 2 : allocation budget ---
|
||||
const budgets = alloueBudget(inputs.budget_eur, poids);
|
||||
if (debugMode)
|
||||
debug.steps.push({ etape: 2, label: 'allocation', budgets });
|
||||
// --- Étape 3 : sélection par axe ---
|
||||
const selection = {};
|
||||
for (const axe of AXES_CODE) {
|
||||
const res = selectionnePoste(budgets[axe], inputs, contexte, options, postes, axe, reglages, debugMode);
|
||||
res.poids = poids[axe];
|
||||
selection[axe] = res;
|
||||
}
|
||||
if (debugMode)
|
||||
debug.steps.push({ etape: 3, label: 'selection', summary: Object.fromEntries(AXES_CODE.map((a) => [a, selection[a].sous_total_eur_ht])) });
|
||||
// --- Étapes 4 à 6 ---
|
||||
const fin = finaliseSelection(selection, inputs.budget_eur, options, postes, inputs, contexte, reglages, debugMode, debugMode ? debug : undefined);
|
||||
const ecart = inputs.budget_eur > 0 ? ((fin.total_eur_ht - inputs.budget_eur) / inputs.budget_eur) * 100 : 0;
|
||||
return {
|
||||
axes: fin.axes,
|
||||
total_eur_ht: fin.total_eur_ht,
|
||||
budget_cible: inputs.budget_eur,
|
||||
ecart_budget_pct: +ecart.toFixed(2),
|
||||
status: fin.status,
|
||||
message_status: fin.message_status,
|
||||
couplages: fin.couplages,
|
||||
contexte,
|
||||
avertissements: ctx.avertissements,
|
||||
debug: debugMode ? debug : undefined,
|
||||
};
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import type { PoidsAxes } from './types.js';
|
||||
/**
|
||||
* Alloue le budget total entre les 3 axes selon leurs poids.
|
||||
*
|
||||
* @param budget_total — EUR
|
||||
* @param poids — somme = 100
|
||||
* @returns en EUR
|
||||
*/
|
||||
export declare function alloueBudget(budget_total: number, poids: PoidsAxes): PoidsAxes;
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
// alloue-budget.ts — Étape 2 : allocation budget par axe
|
||||
// Spec: V2-algo-spec.md §Étape 2 — origine calculs/src/lib/autonomie/alloue-budget.js, inchangé.
|
||||
/**
|
||||
* Alloue le budget total entre les 3 axes selon leurs poids.
|
||||
*
|
||||
* @param budget_total — EUR
|
||||
* @param poids — somme = 100
|
||||
* @returns en EUR
|
||||
*/
|
||||
export function alloueBudget(budget_total, poids) {
|
||||
return {
|
||||
eau: (budget_total * poids.eau) / 100,
|
||||
energie: (budget_total * poids.energie) / 100,
|
||||
alim: (budget_total * poids.alim) / 100,
|
||||
};
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import type { AlgoConfig, Axe } from './types.js';
|
||||
import { AXES } from './types.js';
|
||||
export type AxesActifs = Partial<Record<Axe, boolean>>;
|
||||
/**
|
||||
* Transforme algo-config pour neutraliser les axes désactivés.
|
||||
* - poids_axes_base[axe] = 0 pour tout axe inactif
|
||||
* - tous les deltas de modulateurs référençant un axe inactif sont retirés
|
||||
* - marqueur `_axes_desactives` (lu par reajuste.ts pour ne pas réinjecter
|
||||
* des postes optionnels dans un axe éteint lors du CAS B underspend)
|
||||
*
|
||||
* @param config — algo-config.json parsé (non muté)
|
||||
* @returns nouvelle config
|
||||
*/
|
||||
export declare function configPourAxes(config: AlgoConfig, actifs: AxesActifs | null | undefined): AlgoConfig;
|
||||
/**
|
||||
* Vrai si au moins un axe est actif (0 axe actif = état vide géré par l'UI,
|
||||
* l'algo ne doit pas tourner).
|
||||
*/
|
||||
export declare function auMoinsUnAxe(actifs: AxesActifs | null | undefined): boolean;
|
||||
export { AXES };
|
||||
Vendored
+48
@@ -0,0 +1,48 @@
|
||||
// axes-actifs.ts — Toggles d'axes (V3 vague 1, M2)
|
||||
// Produit une config dérivée où les axes désactivés ont un poids nul,
|
||||
// sans toucher aux modules cœur de l'algo. La désactivation passe
|
||||
// uniquement par les inputs/config (poids), conformément à la spec.
|
||||
// Origine calculs/src/lib/autonomie/axes-actifs.js, inchangé (typé).
|
||||
import { AXES } from './types.js';
|
||||
/**
|
||||
* Transforme algo-config pour neutraliser les axes désactivés.
|
||||
* - poids_axes_base[axe] = 0 pour tout axe inactif
|
||||
* - tous les deltas de modulateurs référençant un axe inactif sont retirés
|
||||
* - marqueur `_axes_desactives` (lu par reajuste.ts pour ne pas réinjecter
|
||||
* des postes optionnels dans un axe éteint lors du CAS B underspend)
|
||||
*
|
||||
* @param config — algo-config.json parsé (non muté)
|
||||
* @returns nouvelle config
|
||||
*/
|
||||
export function configPourAxes(config, actifs) {
|
||||
const desactives = AXES.filter((a) => actifs && actifs[a] === false);
|
||||
if (desactives.length === 0)
|
||||
return config;
|
||||
// Clone profond (config petite, JSON-safe)
|
||||
const cfg = JSON.parse(JSON.stringify(config));
|
||||
cfg.poids_axes_base = cfg.poids_axes_base || { eau: 40, energie: 40, alim: 20 };
|
||||
for (const axe of desactives)
|
||||
cfg.poids_axes_base[axe] = 0;
|
||||
// Neutraliser les deltas des modulateurs qui re-donneraient du poids
|
||||
// à un axe désactivé (ex : zone MED -> eau+10, parcelle grande -> alim+10).
|
||||
const mod = cfg.modulateurs || {};
|
||||
for (const key of Object.keys(mod)) {
|
||||
const val = mod[key];
|
||||
if (val && typeof val === 'object' && !Array.isArray(val)) {
|
||||
for (const axe of desactives) {
|
||||
if (axe in val)
|
||||
delete val[axe];
|
||||
}
|
||||
}
|
||||
}
|
||||
cfg._axes_desactives = desactives;
|
||||
return cfg;
|
||||
}
|
||||
/**
|
||||
* Vrai si au moins un axe est actif (0 axe actif = état vide géré par l'UI,
|
||||
* l'algo ne doit pas tourner).
|
||||
*/
|
||||
export function auMoinsUnAxe(actifs) {
|
||||
return AXES.some((a) => !actifs || actifs[a] !== false);
|
||||
}
|
||||
export { AXES };
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
export { ZONE_BIOGEO_MAP, zoneRtToClass } from '../../commun/contexte.js';
|
||||
export declare function clamp01(x: number): number;
|
||||
export declare function clamp(x: number, min: number, max: number): number;
|
||||
export declare function roundToStep(value: number, step: number): number;
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
// calibrage.ts — helpers numériques du noyau.
|
||||
//
|
||||
// Origine : calculs/src/lib/autonomie/calibrage.js. Correction LOT P1.0 : le
|
||||
// singleton `_config` (initConfig / getConfig) est retiré, la configuration
|
||||
// passe en paramètre (Reglages) chez les cinq appelants. ORIENTATION_FACTORS et
|
||||
// DJU_DEFAULTS_BY_ZONE vivent désormais dans commun/coefficients.ts (une seule
|
||||
// définition, sourcée) ; ZONE_BIOGEO_MAP et zoneRtToClass dans commun/contexte.ts.
|
||||
export { ZONE_BIOGEO_MAP, zoneRtToClass } from '../../commun/contexte.js';
|
||||
export function clamp01(x) {
|
||||
return Math.max(0, Math.min(1, x));
|
||||
}
|
||||
export function clamp(x, min, max) {
|
||||
return Math.max(min, Math.min(max, x));
|
||||
}
|
||||
export function roundToStep(value, step) {
|
||||
if (!step || step <= 0)
|
||||
return value;
|
||||
return Math.round(value / step) * step;
|
||||
}
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
import type { Option, Poste, Reglages, Selection } from './types.js';
|
||||
export interface ResultatCouplages {
|
||||
axes: Selection;
|
||||
couplages_appliques: string[];
|
||||
}
|
||||
/**
|
||||
* Post-traitement déterministe des couplages.
|
||||
*
|
||||
* Gestion alias : algo-config.json § couplages_alias permet de mapper
|
||||
* des codes legacy (ex: stherm_ballon_tampon) vers les codes BDD réels (th_ballon_500).
|
||||
*/
|
||||
export declare function appliqueCouplages(selection_recue: Selection, options: Option[], postes: Poste[] | null | undefined, reglages: Reglages, _debug?: boolean): ResultatCouplages;
|
||||
Vendored
+129
@@ -0,0 +1,129 @@
|
||||
// couplages.ts — Étape 4 : application des couplages contraints
|
||||
// Spec: V2-algo-spec.md §Étape 4 — origine calculs/src/lib/autonomie/couplages.js.
|
||||
// Corrections LOT P1.0 : `reglages` en paramètre (plus de getConfig()) ; la
|
||||
// sélection reçue n'est PLUS mutée (copie des axes et de leurs postes, le
|
||||
// résultat est renvoyé dans `axes`). Tous les appelants d'origine lisaient
|
||||
// déjà le retour, le comportement est identique.
|
||||
//
|
||||
// Note (origine, conservée) : le marqueur `_conflit_avec` de la passe 2 est
|
||||
// posé sur des copies de postes et n'atteint jamais la sortie ; le
|
||||
// « downgrade planifié » n'a donc jamais lieu. À corriger en patch, avec test.
|
||||
import { cout_typologie, dimension_optimale } from './dimension.js';
|
||||
function copieSelection(selection) {
|
||||
const copie = {};
|
||||
for (const axe of Object.keys(selection)) {
|
||||
const a = selection[axe];
|
||||
if (!a)
|
||||
continue;
|
||||
const ax = { ...a, postes: a.postes.map((p) => ({ ...p })) };
|
||||
copie[axe] = ax;
|
||||
}
|
||||
return copie;
|
||||
}
|
||||
/**
|
||||
* Post-traitement déterministe des couplages.
|
||||
*
|
||||
* Gestion alias : algo-config.json § couplages_alias permet de mapper
|
||||
* des codes legacy (ex: stherm_ballon_tampon) vers les codes BDD réels (th_ballon_500).
|
||||
*/
|
||||
export function appliqueCouplages(selection_recue, options, postes, reglages, _debug = false) {
|
||||
const alias = reglages.config.couplages_alias || {};
|
||||
const selection = copieSelection(selection_recue);
|
||||
// Construire un mapping poste_id (num) → axe via postes BDD
|
||||
const posteIdToAxe = {};
|
||||
if (postes) {
|
||||
for (const p of postes) {
|
||||
// Normaliser axe : "alimentation" → "alim"
|
||||
const axe_norm = (p.axe === 'alimentation' ? 'alim' : p.axe);
|
||||
posteIdToAxe[p.Id] = axe_norm;
|
||||
}
|
||||
}
|
||||
function resolveCode(code) {
|
||||
return alias[code] || code;
|
||||
}
|
||||
const axes_list = ['eau', 'energie', 'alim'];
|
||||
const tous_postes = axes_list.flatMap((axe) => (selection[axe] && selection[axe].postes ? selection[axe].postes : []).map((p) => Object.assign({}, p, { _axe: axe })));
|
||||
const codes_selectionnes = new Set(tous_postes.map((p) => p.typologie_choisie));
|
||||
const couplages_log = [];
|
||||
// Passe 1 : couplages_requis
|
||||
for (const poste of tous_postes) {
|
||||
const requis_list = poste.couplages_requis || [];
|
||||
for (const requis_raw of requis_list) {
|
||||
const requis = resolveCode(requis_raw);
|
||||
if (!codes_selectionnes.has(requis)) {
|
||||
// Chercher la typo en BDD (code exact ou alias original)
|
||||
let typo_requise = options.find((o) => o.code === requis);
|
||||
if (!typo_requise && requis !== requis_raw) {
|
||||
typo_requise = options.find((o) => o.code === requis_raw);
|
||||
}
|
||||
if (typo_requise) {
|
||||
// Trouver l'axe via mapping poste_id → axe
|
||||
const axe_cible = posteIdToAxe[typo_requise.poste_id] || trouverAxeParCode(typo_requise.code);
|
||||
const cible = axe_cible ? selection[axe_cible] : undefined;
|
||||
if (axe_cible && cible) {
|
||||
const dim = dimension_optimale(typo_requise, {}, {}, reglages.coefficients);
|
||||
const cout = cout_typologie(typo_requise, dim, {});
|
||||
const cout_force = Math.round(cout || 0);
|
||||
const unite_dim = dim ? dim.unite : typo_requise.unite || '';
|
||||
const valeur_dim = dim ? dim.valeur : typo_requise.quantite_default || 1;
|
||||
cible.postes.push({
|
||||
poste_id: String(typo_requise.poste_id) + '_couplage',
|
||||
poste_nom: 'Couplage requis (' + typo_requise.nom + ')',
|
||||
obligatoire: true,
|
||||
budget_alloue_poste: cout_force,
|
||||
typologie_choisie: typo_requise.code,
|
||||
nom_affichage: typo_requise.nom,
|
||||
description_pedago: typo_requise.description_pedago || '',
|
||||
dimension: { valeur: valeur_dim, unite: unite_dim },
|
||||
cout_eur_ht: cout_force,
|
||||
score_adequation: 0.8,
|
||||
score_final: 0,
|
||||
alternatives: [],
|
||||
notes_techniques: typo_requise.notes_techniques || '',
|
||||
flag_diy: typo_requise.flag_diy || false,
|
||||
complexite_pose: typo_requise.complexite_pose || 2,
|
||||
couplages_requis: [],
|
||||
couplages_exclus: [],
|
||||
manuel: false,
|
||||
force_par_couplage: true,
|
||||
});
|
||||
cible.sous_total_eur_ht += cout_force;
|
||||
codes_selectionnes.add(typo_requise.code);
|
||||
couplages_log.push('FORCE "' + typo_requise.code + '" (requis par "' + poste.typologie_choisie + '") +' + cout_force + 'EUR dans axe ' + axe_cible);
|
||||
}
|
||||
else {
|
||||
couplages_log.push('WARN axe non trouve pour "' + requis + '" (requis par "' + poste.typologie_choisie + '")');
|
||||
}
|
||||
}
|
||||
else {
|
||||
couplages_log.push('MANQUANT "' + requis + '" (requis par "' + poste.typologie_choisie + '") — code BDD non trouve');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Passe 2 : couplages_exclus
|
||||
for (const poste of tous_postes) {
|
||||
const exclus_list = poste.couplages_exclus || [];
|
||||
for (const exclu_raw of exclus_list) {
|
||||
const exclu = resolveCode(exclu_raw);
|
||||
if (codes_selectionnes.has(exclu)) {
|
||||
const poste_conflit = tous_postes.find((p) => p.typologie_choisie === exclu);
|
||||
if (poste_conflit && !poste_conflit.force_par_couplage) {
|
||||
poste_conflit._conflit_avec = poste.typologie_choisie;
|
||||
couplages_log.push('CONFLIT "' + exclu + '" avec "' + poste.typologie_choisie + '" — downgrade planifié');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return { axes: selection, couplages_appliques: couplages_log };
|
||||
}
|
||||
// Fallback heuristique si poste_id absent du mapping
|
||||
function trouverAxeParCode(code) {
|
||||
if (/captage|stockage|filtrat|distrib|eg_|stherm_ballon/.test(code))
|
||||
return 'eau';
|
||||
if (/^th_|^pv_|^bois_|^pil_|^emet_|ballon_tampon|conduit_fumee/.test(code))
|
||||
return 'energie';
|
||||
if (/^etude_|^haute_|^moy_|^basse_/.test(code))
|
||||
return 'alim';
|
||||
return 'energie';
|
||||
}
|
||||
Vendored
+27
@@ -0,0 +1,27 @@
|
||||
import type { TableCoefficients } from '../../commun/coefficients.js';
|
||||
import type { Autodiag, Contexte, Dimension, Option } from './types.js';
|
||||
/**
|
||||
* Calcule la valeur optimale du paramètre dimensionnant pour une typologie.
|
||||
*
|
||||
* Particularité ballon_tampon (th_ballon_500) :
|
||||
* - unite BDD = "forfait" (contrainte schéma NocoDB)
|
||||
* - prix_par_unite = 1.2 EUR/L
|
||||
* - parametre_dim.unite = "L" (source de vérité pour l'affichage et le calcul)
|
||||
* → L'algo utilise toujours parametre_dim.unite quand présent, sinon fallback option.unite
|
||||
*/
|
||||
export declare function dimension_optimale(typo: Option, inputs: Partial<Autodiag>, contexte_derived: Partial<Contexte>, coefficients: TableCoefficients): Dimension | null;
|
||||
/**
|
||||
* Calcule le coût HT d'une typologie pour une dimension donnée.
|
||||
*
|
||||
* Hiérarchie des prix (3 cas BDD) :
|
||||
* 1. prix_fixe + prix_par_unite × dim.valeur (V2 enrichi, ex: cuve béton, PV)
|
||||
* 2. prix_par_unite × dim.valeur seul (ex: th_ballon_500)
|
||||
* 3. prix_mat_min + pose si pro requis (forfaits legacy sans parametre_dim enrichi)
|
||||
*
|
||||
* Pour les postes pro (complexite_pose=3) et temps_dispo < 3, ajouter prix_pro_min.
|
||||
* Fallback final : (prix_mat_min + prix_mat_max) / 2
|
||||
*
|
||||
* Les prix sont des DONNÉES de l'option (publiées par calculs) ; la seule
|
||||
* heuristique du code est « pose pro = 50 % du matériel si prix_pro absent ».
|
||||
*/
|
||||
export declare function cout_typologie(typo: Option, dim_result: Dimension | null | undefined, inputs?: Partial<Autodiag> | null): number;
|
||||
Vendored
+113
@@ -0,0 +1,113 @@
|
||||
// dimension.ts — dimension_optimale(typo, inputs, contexte_derived, coefficients) → Dimension | null
|
||||
// Spec: V2-algo-spec-FONCTIONS.md §2 — origine calculs/src/lib/autonomie/dimension.js.
|
||||
// Correction LOT P1.0 : facteurs d'orientation et DJU par défaut lus dans la
|
||||
// table de coefficients passée en paramètre (plus de constante de module).
|
||||
import { zoneRtToClass, clamp, roundToStep } from './calibrage.js';
|
||||
import { parseParam } from './types.js';
|
||||
/**
|
||||
* Calcule la valeur optimale du paramètre dimensionnant pour une typologie.
|
||||
*
|
||||
* Particularité ballon_tampon (th_ballon_500) :
|
||||
* - unite BDD = "forfait" (contrainte schéma NocoDB)
|
||||
* - prix_par_unite = 1.2 EUR/L
|
||||
* - parametre_dim.unite = "L" (source de vérité pour l'affichage et le calcul)
|
||||
* → L'algo utilise toujours parametre_dim.unite quand présent, sinon fallback option.unite
|
||||
*/
|
||||
export function dimension_optimale(typo, inputs, contexte_derived, coefficients) {
|
||||
const param = parseParam(typo.parametre_dim);
|
||||
if (!param)
|
||||
return null;
|
||||
const strategie = param.strategie || 'fixe';
|
||||
let valeur;
|
||||
switch (strategie) {
|
||||
case 'par_personne': {
|
||||
const base = param.base != null ? param.base : 0;
|
||||
const coef = param.coef != null ? param.coef : 1;
|
||||
valeur = base + (inputs.nb_personnes || 4) * coef;
|
||||
break;
|
||||
}
|
||||
case 'par_surface_toit': {
|
||||
const table = coefficients.orientation_pv.valeur;
|
||||
const orient = (inputs.orientation_toit ? table[inputs.orientation_toit] : undefined) || 0.8;
|
||||
const coef = param.coef != null ? param.coef : 0.1;
|
||||
valeur = (inputs.surface_toit_m2 || 0) * coef * orient;
|
||||
break;
|
||||
}
|
||||
case 'par_surface_parcelle': {
|
||||
const reserve = param.reserve_construit != null ? param.reserve_construit : 100;
|
||||
const utile = Math.max(0, (inputs.surface_parcelle_m2 || 0) - reserve);
|
||||
const coef = param.coef != null ? param.coef : 0.05;
|
||||
valeur = utile * coef;
|
||||
break;
|
||||
}
|
||||
case 'par_dju': {
|
||||
const zone_class = zoneRtToClass(contexte_derived.zone_climatique_rt);
|
||||
const dju = contexte_derived.dju || coefficients.dju_defaut_par_classe_rt.valeur[zone_class] || 2200;
|
||||
const coef = param.coef != null ? param.coef : 0.005;
|
||||
valeur = dju * coef;
|
||||
break;
|
||||
}
|
||||
case 'par_metres_haie': {
|
||||
const coef = param.coef != null ? param.coef : 1.0;
|
||||
valeur = (inputs.metres_haie_possibles || 0) * coef;
|
||||
break;
|
||||
}
|
||||
case 'fixe':
|
||||
default:
|
||||
valeur = param.default != null ? param.default : 1;
|
||||
break;
|
||||
}
|
||||
// Clamp + arrondi step
|
||||
const min = param.min != null ? param.min : 0;
|
||||
const max = param.max != null ? param.max : Infinity;
|
||||
const step = param.step != null ? param.step : 0;
|
||||
valeur = clamp(valeur, min, max);
|
||||
valeur = roundToStep(valeur, step);
|
||||
// Unite : parametre_dim.unite prioritaire sur option.unite (cas ballon_tampon)
|
||||
const unite = param.unite || typo.unite || '';
|
||||
return { valeur, unite, strategie };
|
||||
}
|
||||
/**
|
||||
* Calcule le coût HT d'une typologie pour une dimension donnée.
|
||||
*
|
||||
* Hiérarchie des prix (3 cas BDD) :
|
||||
* 1. prix_fixe + prix_par_unite × dim.valeur (V2 enrichi, ex: cuve béton, PV)
|
||||
* 2. prix_par_unite × dim.valeur seul (ex: th_ballon_500)
|
||||
* 3. prix_mat_min + pose si pro requis (forfaits legacy sans parametre_dim enrichi)
|
||||
*
|
||||
* Pour les postes pro (complexite_pose=3) et temps_dispo < 3, ajouter prix_pro_min.
|
||||
* Fallback final : (prix_mat_min + prix_mat_max) / 2
|
||||
*
|
||||
* Les prix sont des DONNÉES de l'option (publiées par calculs) ; la seule
|
||||
* heuristique du code est « pose pro = 50 % du matériel si prix_pro absent ».
|
||||
*/
|
||||
export function cout_typologie(typo, dim_result, inputs) {
|
||||
const prix_fixe = typo.prix_fixe != null ? typo.prix_fixe : 0;
|
||||
const prix_par_unite = typo.prix_par_unite;
|
||||
const prix_mat_min = typo.prix_mat_min != null ? typo.prix_mat_min : 0;
|
||||
const prix_mat_max = typo.prix_mat_max != null ? typo.prix_mat_max : prix_mat_min;
|
||||
const prix_pro_min = typo.prix_pro_min != null ? typo.prix_pro_min : 0;
|
||||
// Cas 1 & 2 : prix_par_unite disponible (schéma V2 enrichi)
|
||||
if (prix_par_unite != null && prix_par_unite > 0) {
|
||||
const dim_val = dim_result ? dim_result.valeur : typo.quantite_default || 1;
|
||||
const base = prix_fixe + prix_par_unite * dim_val;
|
||||
// Forfait pro si complexite=3 et temps_dispo=1
|
||||
if (typo.complexite_pose === 3 && inputs && inputs.temps_dispo === 1 && prix_pro_min > 0) {
|
||||
return Math.max(base, prix_pro_min);
|
||||
}
|
||||
return base;
|
||||
}
|
||||
// Cas 3 : forfait legacy (prix_mat_min/max seulement)
|
||||
if (prix_mat_min > 0) {
|
||||
const mid_mat = (prix_mat_min + prix_mat_max) / 2;
|
||||
// Si complexite_pose = 3 et pose pro : ajouter coût pose pro
|
||||
if (typo.complexite_pose === 3) {
|
||||
const pose = prix_pro_min > 0 ? prix_pro_min : mid_mat * 0.5;
|
||||
return mid_mat + pose;
|
||||
}
|
||||
// complexite 1-2 : coût mat seul (DIY inclus)
|
||||
return mid_mat;
|
||||
}
|
||||
// Fallback ultime : 0 (sécurité, ne devrait pas arriver)
|
||||
return 0;
|
||||
}
|
||||
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
export { selectionner } from './algo.js';
|
||||
export { dimension_optimale, cout_typologie } from './dimension.js';
|
||||
export { adequation_contexte } from './adequation.js';
|
||||
export type { AdequationDebug } from './adequation.js';
|
||||
export { pondereAxes } from './pondere-axes.js';
|
||||
export type { PoidsDebug } from './pondere-axes.js';
|
||||
export { alloueBudget } from './alloue-budget.js';
|
||||
export { selectionnePoste } from './selectionne-poste.js';
|
||||
export { appliqueCouplages } from './couplages.js';
|
||||
export type { ResultatCouplages } from './couplages.js';
|
||||
export { reajusteBudget } from './reajuste.js';
|
||||
export type { EntreeReajuste, ResultatReajuste } from './reajuste.js';
|
||||
export { configPourAxes, auMoinsUnAxe } from './axes-actifs.js';
|
||||
export type { AxesActifs } from './axes-actifs.js';
|
||||
export { clamp01, clamp, roundToStep, ZONE_BIOGEO_MAP, zoneRtToClass } from './calibrage.js';
|
||||
export * from './scoreurs.js';
|
||||
export { AXES, parseJsonArr, parseParam } from './types.js';
|
||||
export type { Axe, Autodiag, InputsSimulation, Poste, Option, ParamDim, AlgoConfig, Reglages, Dimension, Alternative, PosteResultat, AxeResultat, Selection, Statut, PoidsAxes, EtapeDebug, SortieSelection, Contexte, LigneClimat, Orientation, } from './types.js';
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
// @transformer/moteurs/autonomie/noyau — le cœur V2 du simulateur autonomie,
|
||||
// pipeline complet (celui de calculs.trans-former.fr), sans singleton ni I/O.
|
||||
export { selectionner } from './algo.js';
|
||||
export { dimension_optimale, cout_typologie } from './dimension.js';
|
||||
export { adequation_contexte } from './adequation.js';
|
||||
export { pondereAxes } from './pondere-axes.js';
|
||||
export { alloueBudget } from './alloue-budget.js';
|
||||
export { selectionnePoste } from './selectionne-poste.js';
|
||||
export { appliqueCouplages } from './couplages.js';
|
||||
export { reajusteBudget } from './reajuste.js';
|
||||
export { configPourAxes, auMoinsUnAxe } from './axes-actifs.js';
|
||||
export { clamp01, clamp, roundToStep, ZONE_BIOGEO_MAP, zoneRtToClass } from './calibrage.js';
|
||||
export * from './scoreurs.js';
|
||||
export { AXES, parseJsonArr, parseParam } from './types.js';
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import type { AlgoConfig, Autodiag, Contexte, PoidsAxes } from './types.js';
|
||||
export interface PoidsDebug {
|
||||
poids: PoidsAxes;
|
||||
log: string[];
|
||||
}
|
||||
/**
|
||||
* Calcule les poids des 3 axes (eau/energie/alim) selon le contexte utilisateur.
|
||||
* Base 40/40/20, modulations documentées dans algo-config.json § modulateurs.
|
||||
*
|
||||
* @returns somme = 100 ; en debug, { poids, log }
|
||||
*/
|
||||
export declare function pondereAxes(inputs: Autodiag, contexte_derived: Pick<Contexte, 'zone_biogeo' | 'pluviometrie_mm'>, config: AlgoConfig, debug?: false): PoidsAxes;
|
||||
export declare function pondereAxes(inputs: Autodiag, contexte_derived: Pick<Contexte, 'zone_biogeo' | 'pluviometrie_mm'>, config: AlgoConfig, debug: true): PoidsDebug;
|
||||
Vendored
+91
@@ -0,0 +1,91 @@
|
||||
// pondere-axes.ts — Étape 1 : pondération des axes selon contexte
|
||||
// Spec: V2-algo-spec.md §Étape 1 — origine calculs/src/lib/autonomie/pondere-axes.js.
|
||||
// Correction LOT P1.0 : `config` en paramètre (plus de getConfig()).
|
||||
export function pondereAxes(inputs, contexte_derived, config, debug = false) {
|
||||
const base = config.poids_axes_base ?? { eau: 40, energie: 40, alim: 20 };
|
||||
const mod = (config.modulateurs ?? {});
|
||||
const poids = { eau: base.eau, energie: base.energie, alim: base.alim };
|
||||
const log = debug ? [] : null;
|
||||
// SI surface_toit < seuil → poids_energie -= 15, redistribuer eau+5 alim+10
|
||||
const toit_seuil = mod.toit_petit_seuil_m2 ?? 30;
|
||||
if ((inputs.surface_toit_m2 ?? 0) < toit_seuil) {
|
||||
const delta = mod.toit_petit_delta ?? { energie: -15, eau: 5, alim: 10 };
|
||||
poids.energie += delta.energie ?? 0;
|
||||
poids.eau += delta.eau ?? 0;
|
||||
poids.alim += delta.alim ?? 0;
|
||||
if (log)
|
||||
log.push(`toit < ${toit_seuil}m² → Δenergie${delta.energie}, Δeau+${delta.eau}, Δalim+${delta.alim}`);
|
||||
}
|
||||
// SI surface_toit > 100 m² → poids_energie += 5
|
||||
if ((inputs.surface_toit_m2 ?? 0) > 100) {
|
||||
poids.energie += 5;
|
||||
if (log)
|
||||
log.push('toit > 100m² → Δenergie+5');
|
||||
}
|
||||
// SI nb_personnes >= 5 → poids_eau += 10, poids_alim += 5, poids_energie -= 15
|
||||
const famille_seuil = mod.famille_grande_seuil ?? 5;
|
||||
if ((inputs.nb_personnes ?? 0) >= famille_seuil) {
|
||||
const delta = mod.famille_grande_delta ?? { eau: 10, alim: 5, energie: -15 };
|
||||
poids.eau += delta.eau ?? 0;
|
||||
poids.alim += delta.alim ?? 0;
|
||||
poids.energie += delta.energie ?? 0;
|
||||
if (log)
|
||||
log.push(`nb_pers >= ${famille_seuil} → Δeau+${delta.eau}, Δalim+${delta.alim}, Δenergie${delta.energie}`);
|
||||
}
|
||||
// SI zone_biogeo == 'MED' → poids_eau += 10
|
||||
if (contexte_derived.zone_biogeo === 'MED') {
|
||||
const delta = mod.biogeo_med_delta ?? { eau: 10 };
|
||||
poids.eau += delta.eau ?? 0;
|
||||
if (log)
|
||||
log.push(`zone MED → Δeau+${delta.eau}`);
|
||||
}
|
||||
// SI pluviometrie_mm < 600 → poids_eau += 5
|
||||
const pluvio_seuil = mod.pluvio_basse_seuil_mm ?? 600;
|
||||
if ((contexte_derived.pluviometrie_mm ?? 800) < pluvio_seuil) {
|
||||
const delta = mod.pluvio_basse_delta ?? { eau: 5 };
|
||||
poids.eau += delta.eau ?? 0;
|
||||
if (log)
|
||||
log.push(`pluvio < ${pluvio_seuil}mm → Δeau+${delta.eau}`);
|
||||
}
|
||||
// SI surface_parcelle < 300 m² → poids_alim = 0
|
||||
const parcelle_petite = mod.parcelle_petite_seuil_m2 ?? 300;
|
||||
if ((inputs.surface_parcelle_m2 ?? 0) < parcelle_petite) {
|
||||
poids.alim = 0;
|
||||
if (log)
|
||||
log.push(`parcelle < ${parcelle_petite}m² → alim=0`);
|
||||
}
|
||||
// SI surface_parcelle >= 1000 m² ET temps_dispo == 3 → poids_alim += 10
|
||||
const parcelle_grande = mod.parcelle_grande_seuil_m2 ?? 1000;
|
||||
if ((inputs.surface_parcelle_m2 ?? 0) >= parcelle_grande && inputs.temps_dispo === 3) {
|
||||
const delta = mod.parcelle_grande_temps3_delta ?? { alim: 10 };
|
||||
poids.alim += delta.alim ?? 0;
|
||||
if (log)
|
||||
log.push(`parcelle >= ${parcelle_grande}m² + temps=3 → Δalim+${delta.alim}`);
|
||||
}
|
||||
// Axes explicitement désactivés par l'utilisateur (V3 toggles d'axes) :
|
||||
// marqueur posé par axes-actifs.ts (configPourAxes). Les modulations
|
||||
// hardcodées ci-dessus (ex : toit > 100 m² -> energie +5) ne doivent pas
|
||||
// réactiver un axe éteint. Absent = comportement historique inchangé.
|
||||
const desactives = config._axes_desactives || [];
|
||||
for (const axe of desactives) {
|
||||
if (axe in poids && poids[axe] !== 0) {
|
||||
poids[axe] = 0;
|
||||
if (log)
|
||||
log.push('axe ' + axe + ' désactivé (toggle UI) -> poids 0');
|
||||
}
|
||||
}
|
||||
// Normalisation : somme = 100
|
||||
const total = poids.eau + poids.energie + poids.alim;
|
||||
if (total !== 100 && total > 0) {
|
||||
poids.eau = Math.round((poids.eau / total) * 100);
|
||||
poids.energie = Math.round((poids.energie / total) * 100);
|
||||
poids.alim = 100 - poids.eau - poids.energie; // garantie somme exacte
|
||||
}
|
||||
// Garde-fou : pas de poids négatif
|
||||
poids.eau = Math.max(0, poids.eau);
|
||||
poids.energie = Math.max(0, poids.energie);
|
||||
poids.alim = Math.max(0, poids.alim);
|
||||
if (debug)
|
||||
return { poids, log: log ?? [] };
|
||||
return poids;
|
||||
}
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
import type { Autodiag, Contexte, Option, Poste, Reglages, Selection, Statut } from './types.js';
|
||||
export interface EntreeReajuste {
|
||||
axes: Selection;
|
||||
couplages_appliques?: string[];
|
||||
}
|
||||
export interface ResultatReajuste {
|
||||
axes: Selection;
|
||||
total: number;
|
||||
status: Statut;
|
||||
message: string;
|
||||
couplages_appliques: string[];
|
||||
debug_reajust: string[] | null;
|
||||
}
|
||||
export declare function reajusteBudget(apres_couplages: EntreeReajuste, budget_total: number, options: Option[], postes: Poste[] | null | undefined, inputs: Autodiag, contexte_derived: Contexte, reglages: Reglages, debug?: boolean): ResultatReajuste;
|
||||
Vendored
+299
@@ -0,0 +1,299 @@
|
||||
// reajuste.ts — Étape 5 : réajustement budget global
|
||||
// Spec: V2-algo-spec.md §Étape 5 — origine calculs/src/lib/autonomie/reajuste.js.
|
||||
// Correction LOT P1.0 : `reglages` en paramètre (plus de getConfig()).
|
||||
// Comportement d'origine conservé : la fonction modifie EN PLACE les axes
|
||||
// reçus (qui sont déjà la copie produite par appliqueCouplages) et les renvoie.
|
||||
//
|
||||
// Note (origine, conservée) : les messages utilisent toLocaleString('fr-FR')
|
||||
// (Intl). Texte de présentation, non assert par les tests ; à sortir du noyau
|
||||
// en patch si l'octet-pour-octet inter-environnements devient un besoin.
|
||||
import { cout_typologie, dimension_optimale } from './dimension.js';
|
||||
import { parseJsonArr } from './types.js';
|
||||
export function reajusteBudget(apres_couplages, budget_total, options, postes, inputs, contexte_derived, reglages, debug = false) {
|
||||
const config = reglages.config;
|
||||
const coefficients = reglages.coefficients;
|
||||
const tolerances = config.tolerances || {};
|
||||
const overrun_seuil = 1 + (tolerances.budget_overrun_pct || 5) / 100;
|
||||
const underspend_seuil = 1 - (tolerances.budget_underspend_pct || 15) / 100;
|
||||
const axes = apres_couplages.axes;
|
||||
const axes_list = ['eau', 'energie', 'alim'];
|
||||
function getTousPostes() {
|
||||
return axes_list.flatMap((axe) => (axes[axe] && axes[axe].postes ? axes[axe].postes : []).map((p) => Object.assign({}, p, { _axe: axe })));
|
||||
}
|
||||
function calcTotal() {
|
||||
return axes_list.reduce((s, axe) => s + (axes[axe] ? axes[axe].sous_total_eur_ht || 0 : 0), 0);
|
||||
}
|
||||
function recalcSousTotal(axe) {
|
||||
const a = axes[axe];
|
||||
if (a) {
|
||||
a.sous_total_eur_ht = a.postes.reduce((s, p) => s + (p.cout_eur_ht || 0), 0);
|
||||
}
|
||||
}
|
||||
let total = calcTotal();
|
||||
let status = 'OK';
|
||||
let message = '';
|
||||
const log = debug ? [] : null;
|
||||
// CAS A : overrun
|
||||
if (total > budget_total * overrun_seuil) {
|
||||
if (log)
|
||||
log.push('CAS A : total ' + total + ' > seuil ' + Math.round(budget_total * overrun_seuil));
|
||||
let iterations = 0;
|
||||
while (total > budget_total * overrun_seuil && iterations < 80) {
|
||||
iterations++;
|
||||
// Correctif V3-1 (recalibrage) : downgrade "au plus pres du budget" au
|
||||
// lieu du moins cher. Historiquement, le poste candidat sautait vers son
|
||||
// alternative LA MOINS CHERE (ex : poele de masse -> rocket stove,
|
||||
// -23 000 EUR la ou quelques centaines suffisaient a repasser sous le
|
||||
// seuil). Desormais : parmi TOUS les downgrades possibles on choisit
|
||||
// celui dont le total resultant est le plus proche du budget cible —
|
||||
// evite a la fois le surcout et le sous-equipement massif.
|
||||
const tous = getTousPostes().filter((p) => !p.force_par_couplage && !p.manuel);
|
||||
const candidats = [];
|
||||
for (const poste_candidat of tous) {
|
||||
const poste_id_num = _posteIdNum(poste_candidat, options);
|
||||
const alternatives = options.filter((o) => o.poste_id === poste_id_num && o.actif !== false && o.code !== poste_candidat.typologie_choisie);
|
||||
const alts_scored = alternatives
|
||||
.map((alt) => {
|
||||
const dim = dimension_optimale(alt, inputs, contexte_derived, coefficients);
|
||||
const cout = cout_typologie(alt, dim, inputs);
|
||||
return { alt, dim, cout };
|
||||
})
|
||||
.filter((a) => a.cout < (poste_candidat.cout_eur_ht || 0));
|
||||
for (const a of alts_scored) {
|
||||
candidats.push({
|
||||
poste: poste_candidat,
|
||||
alt: a,
|
||||
diff: (poste_candidat.cout_eur_ht || 0) - Math.round(a.cout),
|
||||
});
|
||||
}
|
||||
}
|
||||
let choix = null;
|
||||
if (candidats.length > 0) {
|
||||
// total resultant le plus proche du budget (tiebreak : pire score d'abord)
|
||||
candidats.sort((a, b) => Math.abs(total - a.diff - budget_total) - Math.abs(total - b.diff - budget_total) ||
|
||||
(a.poste.score_final || 0) - (b.poste.score_final || 0));
|
||||
choix = candidats[0];
|
||||
}
|
||||
let downgraded = false;
|
||||
if (choix) {
|
||||
const poste_candidat = choix.poste;
|
||||
const meilleur = choix.alt;
|
||||
const diff = choix.diff;
|
||||
const axe_poste = poste_candidat._axe;
|
||||
const ax = axes[axe_poste];
|
||||
const idx = ax.postes.findIndex((p) => p.typologie_choisie === poste_candidat.typologie_choisie);
|
||||
if (idx >= 0) {
|
||||
const p = ax.postes[idx];
|
||||
ax.postes[idx] = Object.assign({}, p, {
|
||||
typologie_choisie: meilleur.alt.code,
|
||||
nom_affichage: meilleur.alt.nom,
|
||||
description_pedago: meilleur.alt.description_pedago || '',
|
||||
dimension: meilleur.dim || p.dimension,
|
||||
cout_eur_ht: Math.round(meilleur.cout),
|
||||
couplages_requis: parseJsonArr(meilleur.alt.couplages_requis),
|
||||
couplages_exclus: parseJsonArr(meilleur.alt.couplages_exclus),
|
||||
_downgraded: true,
|
||||
});
|
||||
recalcSousTotal(axe_poste);
|
||||
total -= diff;
|
||||
downgraded = true;
|
||||
if (log)
|
||||
log.push('Downgrade ' + poste_candidat.typologie_choisie + ' -> ' + meilleur.alt.code + ' -' + diff + 'EUR');
|
||||
}
|
||||
}
|
||||
if (!downgraded) {
|
||||
const optionnels = getTousPostes().filter((p) => !p.obligatoire && !p.force_par_couplage && !p.manuel);
|
||||
optionnels.sort((a, b) => (a.score_final || 0) - (b.score_final || 0));
|
||||
if (optionnels.length > 0) {
|
||||
const pire = optionnels[0];
|
||||
const axe_poste = pire._axe;
|
||||
const ax = axes[axe_poste];
|
||||
const idx = ax.postes.findIndex((p) => p.typologie_choisie === pire.typologie_choisie);
|
||||
if (idx >= 0) {
|
||||
total -= ax.postes[idx].cout_eur_ht || 0;
|
||||
ax.postes.splice(idx, 1);
|
||||
recalcSousTotal(axe_poste);
|
||||
downgraded = true;
|
||||
if (log)
|
||||
log.push('Retrait optionnel ' + pire.typologie_choisie);
|
||||
}
|
||||
}
|
||||
if (!downgraded)
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (total > budget_total * overrun_seuil) {
|
||||
status = 'BUDGET_INSUFFISANT';
|
||||
message =
|
||||
'Budget insuffisant pour équiper tous les postes obligatoires. Estimation minimale : ' +
|
||||
Math.round(total).toLocaleString('fr-FR') +
|
||||
' EUR. Votre budget : ' +
|
||||
Math.round(budget_total).toLocaleString('fr-FR') +
|
||||
' EUR.';
|
||||
if (log)
|
||||
log.push('BUDGET_INSUFFISANT — toutes alternatives épuisées');
|
||||
}
|
||||
}
|
||||
// CAS B : underspend
|
||||
else if (total < budget_total * underspend_seuil) {
|
||||
if (log)
|
||||
log.push('CAS B : total ' + total + ' < seuil ' + Math.round(budget_total * underspend_seuil));
|
||||
// Axes explicitement désactivés par l'utilisateur (V3 toggles d'axes) :
|
||||
// ne jamais y réinjecter de postes optionnels. Marqueur posé par
|
||||
// axes-actifs.ts (configPourAxes). Absent = comportement historique inchangé.
|
||||
const axes_desactives = new Set(config._axes_desactives || []);
|
||||
// Identifier les postes BDD non encore sélectionnés (optionnels manquants)
|
||||
const codes_selectionnes = new Set(getTousPostes().map((p) => p.typologie_choisie));
|
||||
// Construire mapping poste_code → axe via postes BDD
|
||||
const posteIdToAxe = {};
|
||||
if (postes) {
|
||||
for (const p of postes) {
|
||||
const axe_norm = (p.axe === 'alimentation' ? 'alim' : p.axe);
|
||||
posteIdToAxe[p.Id] = axe_norm;
|
||||
}
|
||||
}
|
||||
let iterations = 0;
|
||||
while (total < budget_total * underspend_seuil && iterations < 80) {
|
||||
iterations++;
|
||||
let upgraded = false;
|
||||
// Stratégie 1 : upgrader des typologies existantes
|
||||
const tous = getTousPostes().filter((p) => !p.manuel);
|
||||
tous.sort((a, b) => (b.score_adequation || 0) - (a.score_adequation || 0));
|
||||
for (const poste_candidat of tous) {
|
||||
const poste_id_num = _posteIdNum(poste_candidat, options);
|
||||
const alternatives = options.filter((o) => o.poste_id === poste_id_num && o.actif !== false && o.code !== poste_candidat.typologie_choisie);
|
||||
const alts_scored = alternatives
|
||||
.map((alt) => {
|
||||
const dim = dimension_optimale(alt, inputs, contexte_derived, coefficients);
|
||||
const cout = cout_typologie(alt, dim, inputs);
|
||||
// Estimer le coût des couplages requis non encore sélectionnés
|
||||
const codes_sel = new Set(getTousPostes().map((p) => p.typologie_choisie));
|
||||
const alias = (config && config.couplages_alias) || {};
|
||||
const requis_list = parseJsonArr(alt.couplages_requis);
|
||||
let cout_couplages_requis = 0;
|
||||
for (const r of requis_list) {
|
||||
const r_resolved = alias[r] || r;
|
||||
if (!codes_sel.has(r) && !codes_sel.has(r_resolved)) {
|
||||
const typo_req = options.find((o) => o.code === r_resolved || o.code === r);
|
||||
if (typo_req) {
|
||||
const dim_req = dimension_optimale(typo_req, inputs, contexte_derived, coefficients);
|
||||
cout_couplages_requis += cout_typologie(typo_req, dim_req, inputs);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { alt, dim, cout, cout_couplages_requis };
|
||||
// Ne conserver que les upgrades qui ne dépassent pas le budget global
|
||||
// en incluant le coût estimé des couplages requis non encore satisfaits
|
||||
})
|
||||
.filter((a) => {
|
||||
const diff_potential = a.cout + a.cout_couplages_requis - (poste_candidat.cout_eur_ht || 0);
|
||||
return a.cout > (poste_candidat.cout_eur_ht || 0) && total + diff_potential <= budget_total * overrun_seuil;
|
||||
});
|
||||
if (alts_scored.length > 0) {
|
||||
alts_scored.sort((a, b) => a.cout - b.cout);
|
||||
const meilleur = alts_scored[0];
|
||||
const diff = Math.round(meilleur.cout) - (poste_candidat.cout_eur_ht || 0);
|
||||
const axe_poste = poste_candidat._axe;
|
||||
const ax = axes[axe_poste];
|
||||
const idx = ax.postes.findIndex((p) => p.typologie_choisie === poste_candidat.typologie_choisie);
|
||||
if (idx >= 0) {
|
||||
const p = ax.postes[idx];
|
||||
ax.postes[idx] = Object.assign({}, p, {
|
||||
typologie_choisie: meilleur.alt.code,
|
||||
nom_affichage: meilleur.alt.nom,
|
||||
description_pedago: meilleur.alt.description_pedago || '',
|
||||
dimension: meilleur.dim || p.dimension,
|
||||
cout_eur_ht: Math.round(meilleur.cout),
|
||||
// IMPORTANT: mettre à jour les couplages de la nouvelle typo (pas hériter ancienne)
|
||||
couplages_requis: parseJsonArr(meilleur.alt.couplages_requis),
|
||||
couplages_exclus: parseJsonArr(meilleur.alt.couplages_exclus),
|
||||
_upgraded: true,
|
||||
});
|
||||
recalcSousTotal(axe_poste);
|
||||
total += diff;
|
||||
upgraded = true;
|
||||
if (log)
|
||||
log.push('Upgrade ' + poste_candidat.typologie_choisie + ' -> ' + meilleur.alt.code + ' +' + diff + 'EUR');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Stratégie 2 : ajouter des postes optionnels non encore sélectionnés
|
||||
if (!upgraded) {
|
||||
const postes_manquants = options.filter((o) => {
|
||||
if (o.actif === false || codes_selectionnes.has(o.code))
|
||||
return false;
|
||||
// Ne pas ajouter de poste appartenant à un axe désactivé (V3)
|
||||
if (axes_desactives.size > 0 && posteIdToAxe[o.poste_id] && axes_desactives.has(posteIdToAxe[o.poste_id]))
|
||||
return false;
|
||||
return true;
|
||||
});
|
||||
// Trier par rapport score/cout et taille raisonnable
|
||||
const candidats_add = postes_manquants
|
||||
.map((o) => {
|
||||
const dim = dimension_optimale(o, inputs, contexte_derived, coefficients);
|
||||
const cout = cout_typologie(o, dim, inputs);
|
||||
return { opt: o, dim, cout };
|
||||
})
|
||||
.filter((a) => a.cout > 0 && total + a.cout <= budget_total * overrun_seuil);
|
||||
candidats_add.sort((a, b) => a.cout - b.cout);
|
||||
if (candidats_add.length > 0) {
|
||||
const best = candidats_add[candidats_add.length - 1]; // le plus cher qui rentre
|
||||
const axe_target = posteIdToAxe[best.opt.poste_id] || 'energie';
|
||||
const ax = axes[axe_target];
|
||||
if (ax) {
|
||||
const valeur_dim = best.dim ? best.dim.valeur : best.opt.quantite_default || 1;
|
||||
const unite_dim = best.dim ? best.dim.unite : best.opt.unite || '';
|
||||
ax.postes.push({
|
||||
poste_id: best.opt.poste_id,
|
||||
poste_nom: best.opt.nom,
|
||||
obligatoire: false,
|
||||
budget_alloue_poste: Math.round(best.cout),
|
||||
typologie_choisie: best.opt.code,
|
||||
nom_affichage: best.opt.nom,
|
||||
description_pedago: best.opt.description_pedago || '',
|
||||
dimension: { valeur: valeur_dim, unite: unite_dim },
|
||||
cout_eur_ht: Math.round(best.cout),
|
||||
score_adequation: 0.6,
|
||||
score_final: 0,
|
||||
alternatives: [],
|
||||
notes_techniques: best.opt.notes_techniques || '',
|
||||
flag_diy: best.opt.flag_diy || false,
|
||||
complexite_pose: best.opt.complexite_pose || 2,
|
||||
couplages_requis: [],
|
||||
couplages_exclus: [],
|
||||
manuel: false,
|
||||
_added_cas_b: true,
|
||||
});
|
||||
ax.sous_total_eur_ht += Math.round(best.cout);
|
||||
total += Math.round(best.cout);
|
||||
codes_selectionnes.add(best.opt.code);
|
||||
upgraded = true;
|
||||
if (log)
|
||||
log.push('Ajout optionnel ' + best.opt.code + ' +' + Math.round(best.cout) + 'EUR');
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!upgraded) {
|
||||
status = 'AJOUTER_OPTIONNEL';
|
||||
message = 'Budget non consommé (' + Math.round(budget_total - total).toLocaleString('fr-FR') + ' EUR disponibles). Postes optionnels suggérés non inclus.';
|
||||
if (log)
|
||||
log.push('AJOUTER_OPTIONNEL — budget non consommable');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
total = calcTotal();
|
||||
return {
|
||||
axes,
|
||||
total: Math.round(total),
|
||||
status,
|
||||
message,
|
||||
couplages_appliques: apres_couplages.couplages_appliques ?? [],
|
||||
debug_reajust: log,
|
||||
};
|
||||
}
|
||||
function _posteIdNum(poste_result, options) {
|
||||
const typo = options.find((o) => o.code === poste_result.typologie_choisie);
|
||||
return typo ? typo.poste_id : -1;
|
||||
}
|
||||
Vendored
+47
@@ -0,0 +1,47 @@
|
||||
import type { Autodiag, Contexte, Option, Poste, Reglages } from './types.js';
|
||||
/**
|
||||
* 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 declare function score_pluviometrie(typo: Option, inputs: Autodiag, contexte_derived: Contexte, _poste: Poste, _sel: string[], reglages: Reglages): number;
|
||||
/**
|
||||
* score_climat_chauffage — pertinent pour postes chauffage, ECS
|
||||
* Plus le DJU est élevé, plus une typologie puissante est pertinente
|
||||
*/
|
||||
export declare function score_climat_chauffage(typo: Option, _inputs: Autodiag, contexte_derived: Contexte): number;
|
||||
/**
|
||||
* score_climat_meridional — bonus solutions sécheresse/canicule zone MED
|
||||
*/
|
||||
export declare function score_climat_meridional(typo: Option, _inputs: Autodiag, contexte_derived: Contexte): number;
|
||||
/**
|
||||
* score_taille_foyer — pertinent pour postes dimensionnables au nb personnes
|
||||
*/
|
||||
export declare function score_taille_foyer(typo: Option, inputs: Autodiag): number;
|
||||
/**
|
||||
* score_surface_toit — pertinent pour PV, récupération eau pluie
|
||||
*/
|
||||
export declare function score_surface_toit(typo: Option, inputs: Autodiag, _contexte: Contexte, _poste: Poste, _sel: string[], reglages: Reglages): number;
|
||||
/**
|
||||
* score_orientation_toit — PV uniquement
|
||||
* Sud = 1.0, dégradation linéaire, nord = forte pénalité
|
||||
*/
|
||||
export declare function score_orientation_toit(_typo: Option, inputs: Autodiag, _contexte: Contexte, poste: Poste | undefined, _sel: string[], reglages: Reglages): number;
|
||||
/**
|
||||
* score_surface_parcelle — pertinent pour alim, phyto-épuration, irrigation
|
||||
*/
|
||||
export declare function score_surface_parcelle(typo: Option, inputs: Autodiag): number;
|
||||
/**
|
||||
* score_temps_dispo — modulation par disponibilité utilisateur
|
||||
*/
|
||||
export declare function score_temps_dispo(typo: Option, inputs: Autodiag): number;
|
||||
/**
|
||||
* score_metres_haie — pertinent pour haies
|
||||
*/
|
||||
export declare function score_metres_haie(typo: Option, inputs: Autodiag): number;
|
||||
/**
|
||||
* score_couplage_disponible — bonus synergies, calculé post-sélection initiale (étape 4)
|
||||
* @param typologies_selectionnees — codes déjà sélectionnés
|
||||
*/
|
||||
export declare function score_couplage_disponible(typo: Option, _inputs: Autodiag, _contexte: Contexte, _poste: Poste, typologies_selectionnees?: string[]): number;
|
||||
/** Facteur d'orientation PV depuis la table de coefficients ; undefined si orientation inconnue. */
|
||||
export declare function facteurOrientation(reglages: Reglages, orientation: string | undefined): number | undefined;
|
||||
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];
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import type { Autodiag, Axe, AxeResultat, Contexte, Option, Poste, Reglages } from './types.js';
|
||||
/**
|
||||
* Sélectionne les meilleures typologies pour un axe donné.
|
||||
*
|
||||
* @param budget_axe — EUR alloué à cet axe
|
||||
* @param axe — "eau" | "energie" | "alim"
|
||||
*/
|
||||
export declare function selectionnePoste(budget_axe: number, inputs: Autodiag, contexte_derived: Contexte, options: Option[], postes: Poste[], axe: Axe, reglages: Reglages, debug?: boolean): AxeResultat;
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
// selectionne-poste.ts — Étape 3 : sélection des typologies par poste
|
||||
// Spec: V2-algo-spec.md §Étape 3 + V2-algo-spec-FONCTIONS.md §3
|
||||
// Origine calculs/src/lib/autonomie/selectionne-poste.js.
|
||||
// Correction LOT P1.0 : `reglages` en paramètre (plus de getConfig()) ; le
|
||||
// console.warn de debug est versé dans `debug_log` du résultat.
|
||||
import { adequation_contexte } from './adequation.js';
|
||||
import { dimension_optimale, cout_typologie } from './dimension.js';
|
||||
import { parseJsonArr } from './types.js';
|
||||
/**
|
||||
* Sélectionne les meilleures typologies pour un axe donné.
|
||||
*
|
||||
* @param budget_axe — EUR alloué à cet axe
|
||||
* @param axe — "eau" | "energie" | "alim"
|
||||
*/
|
||||
export function selectionnePoste(budget_axe, inputs, contexte_derived, options, postes, axe, reglages, debug = false) {
|
||||
const config = reglages.config;
|
||||
const bonus_malus = config.bonus_malus || {};
|
||||
const postes_poids_cfg = config.postes_poids || {};
|
||||
const tolerances = config.tolerances || {};
|
||||
const tolerance_pct = (tolerances.tolerance_par_poste_pct || 20) / 100;
|
||||
const debug_log = debug ? [] : undefined;
|
||||
// Normaliser le code axe pour matcher la BDD ("alimentation" dans postes BDD)
|
||||
const axe_bdd = axe === 'alim' ? 'alimentation' : axe;
|
||||
// Postes de l'axe, triés par ordre d'affichage
|
||||
const postes_axe = postes.filter((p) => p.axe === axe_bdd && p.actif !== false).sort((a, b) => (a.ordre || 0) - (b.ordre || 0));
|
||||
if (postes_axe.length === 0) {
|
||||
return { postes: [], sous_total_eur_ht: 0, budget_alloue: budget_axe, poids: 0 };
|
||||
}
|
||||
// Enrichir les postes avec poids depuis config (fallback 50)
|
||||
const postes_enrichis = postes_axe.map((p) => ({
|
||||
...p,
|
||||
poids_effectif: postes_poids_cfg[p.code] && postes_poids_cfg[p.code].poids != null ? postes_poids_cfg[p.code].poids : 50,
|
||||
}));
|
||||
// Poids total pour répartition budget
|
||||
const poids_total = postes_enrichis.reduce((s, p) => s + p.poids_effectif, 0);
|
||||
const resultats_postes = [];
|
||||
for (const poste of postes_enrichis) {
|
||||
// Budget alloué à ce poste (proportionnel au poids)
|
||||
const budget_poste = poids_total > 0 ? (budget_axe * poste.poids_effectif) / poids_total : budget_axe / postes_enrichis.length;
|
||||
// Si alim avec budget 0 (parcelle trop petite) → skip
|
||||
if (budget_axe === 0)
|
||||
continue;
|
||||
// Typologies disponibles pour ce poste
|
||||
const typos_poste = options.filter((o) => o.poste_id === poste.Id && o.actif !== false);
|
||||
if (typos_poste.length === 0)
|
||||
continue;
|
||||
// Filtrer par accessibilité contexte (§3.4)
|
||||
let typos_filtrees = typos_poste.filter((typo) => {
|
||||
// Exclure complexité 3 si temps_dispo = 1 (sauf si c'est la seule option)
|
||||
if (inputs.temps_dispo === 1 && typo.complexite_pose === 3)
|
||||
return false;
|
||||
// Exclure PV sur toit nord
|
||||
if (poste.code === 'energie_pv' && inputs.orientation_toit === 'nord')
|
||||
return false;
|
||||
return true;
|
||||
});
|
||||
// Garde-fou : si trop restrictif, revenir à toutes
|
||||
if (typos_filtrees.length === 0)
|
||||
typos_filtrees = typos_poste;
|
||||
// Scorer chaque typologie (§3.5)
|
||||
const scored = typos_filtrees.map((typo) => {
|
||||
const dim = dimension_optimale(typo, inputs, contexte_derived, reglages.coefficients);
|
||||
const cout = cout_typologie(typo, dim, inputs);
|
||||
const adq_result = debug
|
||||
? adequation_contexte(typo, inputs, poste, contexte_derived, reglages, [], true)
|
||||
: adequation_contexte(typo, inputs, poste, contexte_derived, reglages, [], false);
|
||||
const score_adequation = debug && adq_result && typeof adq_result === 'object' && 'score' in adq_result
|
||||
? adq_result.score
|
||||
: typeof adq_result === 'number'
|
||||
? adq_result
|
||||
: 0.5;
|
||||
// Boost DIY temps=3
|
||||
let score_utilite = (poste.poids_effectif || 50) * score_adequation;
|
||||
if (inputs.temps_dispo === 3 && typo.flag_diy) {
|
||||
score_utilite *= bonus_malus.boost_diy_temps3 || 1.3;
|
||||
}
|
||||
const score_final = cout > 0 ? score_utilite / cout : 0;
|
||||
return {
|
||||
typo,
|
||||
dim,
|
||||
cout: Math.round(cout),
|
||||
score_adequation,
|
||||
score_utilite,
|
||||
score_final,
|
||||
debug_adq: debug && typeof adq_result === 'object' ? adq_result : undefined,
|
||||
};
|
||||
});
|
||||
// Trier par score_final décroissant
|
||||
scored.sort((a, b) => b.score_final - a.score_final);
|
||||
// §3.6 : sélectionner le meilleur dont cout <= budget_poste × (1 + tolerance)
|
||||
// Ne PAS prendre un poste si le coût dépasse largement le budget (anti-overrun)
|
||||
const seuil_strict = budget_poste * (1 + tolerance_pct);
|
||||
// Seuil assoupli : si aucun poste ne rentre, prendre le moins cher
|
||||
let choix = scored.find((s) => s.cout <= seuil_strict);
|
||||
if (!choix && scored.length > 0) {
|
||||
// Pas de typo dans le budget : prendre la moins chère
|
||||
const par_cout = [...scored].sort((a, b) => a.cout - b.cout);
|
||||
choix = par_cout[0];
|
||||
if (debug_log)
|
||||
debug_log.push('[selectionnePoste] Aucune typo dans budget pour poste ' + poste.code + ' (budget=' + Math.round(budget_poste) + ', min cout=' + choix.cout + ')');
|
||||
}
|
||||
if (!choix)
|
||||
continue;
|
||||
// Alternatives (2-4 autres options scorées)
|
||||
const alternatives = scored
|
||||
.filter((s) => s.typo.code !== choix.typo.code)
|
||||
.slice(0, 3)
|
||||
.map((s) => ({
|
||||
code: s.typo.code,
|
||||
nom_affichage: s.typo.nom,
|
||||
cout_eur_ht: s.cout,
|
||||
score_final: +s.score_final.toFixed(4),
|
||||
dimension: s.dim ? { valeur: s.dim.valeur, unite: s.dim.unite } : null,
|
||||
}));
|
||||
const couplages_requis = parseJsonArr(choix.typo.couplages_requis);
|
||||
const couplages_exclus = parseJsonArr(choix.typo.couplages_exclus);
|
||||
resultats_postes.push({
|
||||
poste_id: poste.code,
|
||||
poste_nom: poste.poste,
|
||||
obligatoire: poste.obligatoire || false,
|
||||
budget_alloue_poste: Math.round(budget_poste),
|
||||
typologie_choisie: choix.typo.code,
|
||||
nom_affichage: choix.typo.nom,
|
||||
description_pedago: choix.typo.description_pedago || '',
|
||||
dimension: choix.dim || { valeur: choix.typo.quantite_default || 1, unite: choix.typo.unite || '' },
|
||||
cout_eur_ht: choix.cout,
|
||||
score_adequation: +choix.score_adequation.toFixed(3),
|
||||
score_final: +choix.score_final.toFixed(4),
|
||||
alternatives,
|
||||
notes_techniques: choix.typo.notes_techniques || '',
|
||||
flag_diy: choix.typo.flag_diy || false,
|
||||
complexite_pose: choix.typo.complexite_pose || 2,
|
||||
couplages_requis,
|
||||
couplages_exclus,
|
||||
manuel: false,
|
||||
});
|
||||
}
|
||||
const sous_total = resultats_postes.reduce((s, p) => s + p.cout_eur_ht, 0);
|
||||
const resultat = {
|
||||
postes: resultats_postes,
|
||||
sous_total_eur_ht: sous_total,
|
||||
budget_alloue: budget_axe,
|
||||
poids: 0, // rempli par l'orchestrateur
|
||||
};
|
||||
if (debug_log && debug_log.length)
|
||||
resultat.debug_log = debug_log;
|
||||
return resultat;
|
||||
}
|
||||
Vendored
+180
@@ -0,0 +1,180 @@
|
||||
import type { TableCoefficients, Orientation } from '../../commun/coefficients.js';
|
||||
import type { Contexte, LigneClimat } from '../../commun/contexte.js';
|
||||
export type { Contexte, LigneClimat, Orientation };
|
||||
export type Axe = 'eau' | 'energie' | 'alim';
|
||||
export declare const AXES: readonly Axe[];
|
||||
/** Réponses de l'autodiag utiles aux moteurs (toutes facultatives : le noyau applique ses défauts). */
|
||||
export interface Autodiag {
|
||||
code_postal?: string | number;
|
||||
nb_personnes?: number;
|
||||
surface_parcelle_m2?: number;
|
||||
surface_toit_m2?: number;
|
||||
orientation_toit?: Orientation | string;
|
||||
pente_toit_deg?: number;
|
||||
metres_haie_possibles?: number;
|
||||
/** 1 = faire poser, 2 = participation ponctuelle, 3 = auto-pose. */
|
||||
temps_dispo?: 1 | 2 | 3 | number;
|
||||
}
|
||||
/** Entrées de `selectionner()` : autodiag + budget total. */
|
||||
export interface InputsSimulation extends Autodiag {
|
||||
budget_eur: number;
|
||||
}
|
||||
export interface Poste {
|
||||
Id: number;
|
||||
axe: string;
|
||||
code: string;
|
||||
poste: string;
|
||||
ordre?: number | null;
|
||||
obligatoire?: boolean | null;
|
||||
actif?: boolean | null;
|
||||
[autre: string]: unknown;
|
||||
}
|
||||
export interface ParamDim {
|
||||
nom?: string;
|
||||
strategie?: string;
|
||||
base?: number;
|
||||
coef?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
default?: number;
|
||||
unite?: string;
|
||||
reserve_construit?: number;
|
||||
}
|
||||
export interface Option {
|
||||
Id: number;
|
||||
poste_id: number;
|
||||
code: string;
|
||||
nom: string;
|
||||
typologie?: string | null;
|
||||
actif?: boolean | null;
|
||||
prix_mat_min?: number | null;
|
||||
prix_mat_max?: number | null;
|
||||
prix_pro_min?: number | null;
|
||||
prix_pro_max?: number | null;
|
||||
prix_par_unite?: number | null;
|
||||
prix_fixe?: number | null;
|
||||
unite?: string | null;
|
||||
quantite_default?: number | null;
|
||||
/** JSON sérialisé (NocoDB) ou objet déjà parsé. */
|
||||
parametre_dim?: string | ParamDim | null;
|
||||
couplages_requis?: string | string[] | null;
|
||||
couplages_exclus?: string | string[] | null;
|
||||
flag_diy?: boolean | null;
|
||||
complexite_pose?: number | null;
|
||||
accessible_fr?: boolean | null;
|
||||
surface_min_m2?: number | null;
|
||||
surface_optimale_m2?: number | null;
|
||||
tags?: string[] | null;
|
||||
description_pedago?: string | null;
|
||||
notes_techniques?: string | null;
|
||||
[autre: string]: unknown;
|
||||
}
|
||||
export interface AlgoConfig {
|
||||
poids_axes_base?: Record<Axe, number>;
|
||||
modulateurs?: Record<string, unknown>;
|
||||
adequation_weights?: Record<string, Record<string, number>>;
|
||||
tolerances?: {
|
||||
budget_overrun_pct?: number;
|
||||
budget_underspend_pct?: number;
|
||||
tolerance_par_poste_pct?: number;
|
||||
};
|
||||
bonus_malus?: {
|
||||
boost_diy_temps3?: number;
|
||||
malus_pose3_temps1?: number;
|
||||
malus_non_accessible_fr?: number;
|
||||
};
|
||||
couplages_alias?: Record<string, string>;
|
||||
postes_poids?: Record<string, {
|
||||
poids?: number;
|
||||
}>;
|
||||
/** Marqueur posé par axes-actifs.ts (configPourAxes). */
|
||||
_axes_desactives?: Axe[];
|
||||
[autre: string]: unknown;
|
||||
}
|
||||
/** Ce qu'un calcul reçoit en plus des données : heuristiques + coefficients sourcés. */
|
||||
export interface Reglages {
|
||||
config: AlgoConfig;
|
||||
coefficients: TableCoefficients;
|
||||
}
|
||||
export interface Dimension {
|
||||
valeur: number;
|
||||
unite: string;
|
||||
strategie?: string;
|
||||
}
|
||||
export interface Alternative {
|
||||
code: string;
|
||||
nom_affichage: string;
|
||||
cout_eur_ht: number;
|
||||
score_final: number;
|
||||
dimension: {
|
||||
valeur: number;
|
||||
unite: string;
|
||||
} | null;
|
||||
}
|
||||
export interface PosteResultat {
|
||||
poste_id: string | number;
|
||||
poste_nom: string;
|
||||
obligatoire: boolean;
|
||||
budget_alloue_poste: number;
|
||||
typologie_choisie: string;
|
||||
nom_affichage: string;
|
||||
description_pedago: string;
|
||||
dimension: Dimension;
|
||||
cout_eur_ht: number;
|
||||
score_adequation: number;
|
||||
score_final: number;
|
||||
alternatives: Alternative[];
|
||||
notes_techniques: string;
|
||||
flag_diy: boolean;
|
||||
complexite_pose: number;
|
||||
couplages_requis: string[];
|
||||
couplages_exclus: string[];
|
||||
manuel: boolean;
|
||||
force_par_couplage?: boolean;
|
||||
_axe?: Axe;
|
||||
_downgraded?: boolean;
|
||||
_upgraded?: boolean;
|
||||
_added_cas_b?: boolean;
|
||||
_conflit_avec?: string;
|
||||
}
|
||||
export interface AxeResultat {
|
||||
postes: PosteResultat[];
|
||||
sous_total_eur_ht: number;
|
||||
budget_alloue: number;
|
||||
poids: number;
|
||||
/** Journal de débogage (seulement si debug). */
|
||||
debug_log?: string[];
|
||||
}
|
||||
/** Sélection sur les trois axes (un axe peut manquer quand un moteur d'axe tourne seul). */
|
||||
export type Selection = Partial<Record<Axe, AxeResultat>>;
|
||||
export type Statut = 'OK' | 'BUDGET_INSUFFISANT' | 'AJOUTER_OPTIONNEL';
|
||||
export interface PoidsAxes {
|
||||
eau: number;
|
||||
energie: number;
|
||||
alim: number;
|
||||
}
|
||||
export interface EtapeDebug {
|
||||
etape: number;
|
||||
label: string;
|
||||
[autre: string]: unknown;
|
||||
}
|
||||
export interface SortieSelection {
|
||||
axes: Selection;
|
||||
total_eur_ht: number;
|
||||
budget_cible: number;
|
||||
ecart_budget_pct: number;
|
||||
status: Statut;
|
||||
message_status?: string;
|
||||
couplages: string[];
|
||||
contexte: Contexte;
|
||||
/** Avertissements du contexte (code postal inconnu → défauts). */
|
||||
avertissements: string[];
|
||||
debug?: {
|
||||
steps: EtapeDebug[];
|
||||
};
|
||||
}
|
||||
/** Parse un champ JSON sérialisé par NocoDB (tableau) ; tolère un tableau déjà parsé. */
|
||||
export declare function parseJsonArr(val: unknown): string[];
|
||||
/** Parse parametre_dim (string JSON ou objet). */
|
||||
export declare function parseParam(raw: unknown): ParamDim | null;
|
||||
Vendored
+35
@@ -0,0 +1,35 @@
|
||||
// types.ts — types du noyau autonomie (V2 algo, calculs.trans-former.fr).
|
||||
// Les formes de données (Poste, Option, LigneClimat) reflètent les JSON publiés
|
||||
// par calculs ; les schémas de validation sont dans commun/schemas.ts.
|
||||
export const AXES = ['eau', 'energie', 'alim'];
|
||||
/** Parse un champ JSON sérialisé par NocoDB (tableau) ; tolère un tableau déjà parsé. */
|
||||
export function parseJsonArr(val) {
|
||||
if (!val || val === 'null')
|
||||
return [];
|
||||
if (Array.isArray(val))
|
||||
return val;
|
||||
if (typeof val !== 'string')
|
||||
return [];
|
||||
try {
|
||||
const p = JSON.parse(val);
|
||||
return Array.isArray(p) ? p : [];
|
||||
}
|
||||
catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
/** Parse parametre_dim (string JSON ou objet). */
|
||||
export function parseParam(raw) {
|
||||
if (!raw)
|
||||
return null;
|
||||
if (typeof raw === 'object')
|
||||
return raw;
|
||||
if (typeof raw !== 'string')
|
||||
return null;
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Vendored
+29
@@ -0,0 +1,29 @@
|
||||
import type { FourchetteOuverte } from '../commun/argent.js';
|
||||
import type { Option } from './noyau/types.js';
|
||||
export type ModePose = 'auto' | 'pro';
|
||||
export interface PrixOption extends FourchetteOuverte {
|
||||
/** Vrai si le mode pro est demandé mais que l'option n'a pas de prix pro. */
|
||||
autoOnly: boolean;
|
||||
}
|
||||
export interface Preset {
|
||||
Id: number;
|
||||
axe: string;
|
||||
niveau: string;
|
||||
actif?: boolean | null;
|
||||
[autre: string]: unknown;
|
||||
}
|
||||
export interface PresetOption {
|
||||
preset_id: number;
|
||||
option_id: number;
|
||||
quantite?: number | null;
|
||||
[autre: string]: unknown;
|
||||
}
|
||||
/** Fourchette d'une option × quantité : colonnes prix_mat_* (auto-pose) ou prix_pro_* (faire poser). */
|
||||
export declare function prixOption(opt: Option, qty: number | null | undefined, pose: ModePose): PrixOption;
|
||||
/** Prix total d'un preset (somme de ses options × quantités) pour un mode de pose. */
|
||||
export declare function prixPreset(preset: Preset, presetOptions: PresetOption[], optionsById: Record<number, Option>, pose: ModePose): {
|
||||
min: number;
|
||||
max: number;
|
||||
};
|
||||
/** Niveau de preset (light/standard/complet) dont le prix médian est le plus proche du montant cible, pour un axe (code BDD). */
|
||||
export declare function presetLePlusProche(cible: number, axe_bdd: string, presets: Preset[], presetOptions: PresetOption[], optionsById: Record<number, Option>, pose: ModePose): string;
|
||||
Vendored
+59
@@ -0,0 +1,59 @@
|
||||
// prix.ts — fourchettes de prix d'une option selon le mode de pose, prix d'un
|
||||
// preset, preset le plus proche d'un montant. Origine
|
||||
// calculs/src/scripts/autonomie/prix.js + matchPresetNiveaux de simulation.js.
|
||||
// Les prix sont des DONNÉES des options (publiées par calculs) ; ce module ne
|
||||
// porte aucun montant. Le formatage (« 1 200 € HT ») reste côté site.
|
||||
/** Fourchette d'une option × quantité : colonnes prix_mat_* (auto-pose) ou prix_pro_* (faire poser). */
|
||||
export function prixOption(opt, qty, pose) {
|
||||
const q = qty || opt.quantite_default || 1;
|
||||
let min;
|
||||
let max;
|
||||
if (pose === 'auto') {
|
||||
min = opt.prix_mat_min;
|
||||
max = opt.prix_mat_max;
|
||||
}
|
||||
else {
|
||||
min = opt.prix_pro_min != null ? opt.prix_pro_min : opt.prix_mat_min;
|
||||
max = opt.prix_pro_max != null ? opt.prix_pro_max : opt.prix_mat_max;
|
||||
}
|
||||
if (min == null && max == null)
|
||||
return { min: null, max: null, autoOnly: false };
|
||||
return {
|
||||
min: min != null ? min * q : null,
|
||||
max: max != null ? max * q : null,
|
||||
autoOnly: pose === 'pro' && opt.prix_pro_min == null && opt.prix_pro_max == null,
|
||||
};
|
||||
}
|
||||
/** Prix total d'un preset (somme de ses options × quantités) pour un mode de pose. */
|
||||
export function prixPreset(preset, presetOptions, optionsById, pose) {
|
||||
let min = 0;
|
||||
let max = 0;
|
||||
for (const po of presetOptions) {
|
||||
if (po.preset_id !== preset.Id)
|
||||
continue;
|
||||
const opt = optionsById[po.option_id];
|
||||
if (!opt)
|
||||
continue;
|
||||
const p = prixOption(opt, po.quantite || 1, pose);
|
||||
if (p.min == null)
|
||||
continue;
|
||||
min += p.min;
|
||||
max += p.max != null ? p.max : p.min;
|
||||
}
|
||||
return { min, max };
|
||||
}
|
||||
/** Niveau de preset (light/standard/complet) dont le prix médian est le plus proche du montant cible, pour un axe (code BDD). */
|
||||
export function presetLePlusProche(cible, axe_bdd, presets, presetOptions, optionsById, pose) {
|
||||
let best = null;
|
||||
let bestDist = Infinity;
|
||||
for (const preset of presets.filter((p) => p.axe === axe_bdd && p.actif !== false)) {
|
||||
const prix = prixPreset(preset, presetOptions, optionsById, pose);
|
||||
const mid = (prix.min + prix.max) / 2;
|
||||
const dist = Math.abs(mid - cible);
|
||||
if (dist < bestDist) {
|
||||
bestDist = dist;
|
||||
best = preset;
|
||||
}
|
||||
}
|
||||
return best ? best.niveau : 'standard';
|
||||
}
|
||||
Vendored
+71
@@ -0,0 +1,71 @@
|
||||
import type { TableCoefficients } from '../commun/coefficients.js';
|
||||
import type { Contexte } from '../commun/contexte.js';
|
||||
import { Trace } from '../commun/contrat.js';
|
||||
import type { PosteResultat } from './noyau/types.js';
|
||||
export interface DetailEau {
|
||||
captable_m3: number;
|
||||
besoin_m3: number;
|
||||
besoin_substituable_m3: number;
|
||||
}
|
||||
export interface DetailEnergie {
|
||||
kwc: number;
|
||||
kwh_par_kwc: number;
|
||||
facteur_orientation: number;
|
||||
production_kwh: number;
|
||||
batterie_kwh: number;
|
||||
taux_autoconso: number;
|
||||
autoconsomme_kwh: number;
|
||||
besoin_kwh: number;
|
||||
}
|
||||
export interface DetailAlim {
|
||||
ml: number;
|
||||
reco_ml: number;
|
||||
}
|
||||
/** kWc installés dans la sélection (dimensions en kWc). */
|
||||
export declare function kwcSelectionnes(postes: PosteResultat[]): number;
|
||||
/** kWh de batterie dans la sélection (dimensions en kWh). */
|
||||
export declare function batterieSelectionnee(postes: PosteResultat[]): number;
|
||||
/** Mètres linéaires de haie dans la sélection. */
|
||||
export declare function mlHaiesSelectionnes(postes: PosteResultat[]): number;
|
||||
/** Vrai si la sélection porte un équipement de captage. */
|
||||
export declare function aCaptage(postes: PosteResultat[]): boolean;
|
||||
/**
|
||||
* EAU : volume captable vs besoin substituable. null si aucun captage.
|
||||
*/
|
||||
export declare function tauxEau(entrees: {
|
||||
surface_toit_m2?: number;
|
||||
nb_personnes?: number;
|
||||
pluviometrie_mm: number;
|
||||
a_captage: boolean;
|
||||
}, coefficients: TableCoefficients, trace: Trace): {
|
||||
taux: number | null;
|
||||
detail: DetailEau | null;
|
||||
};
|
||||
/** Taux d'autoconsommation selon la batterie : 0 kWh → sans, ≥ 10 kWh → avec, linéaire entre. */
|
||||
export declare function tauxAutoconso(batterie_kwh: number, coefficients: TableCoefficients): number;
|
||||
/** Productible PV (kWh/kWc/an) pour un contexte, depuis la table de coefficients. */
|
||||
export declare function productiblePv(contexte: Pick<Contexte, 'zone_climatique_rt' | 'zone_biogeo_code'>, coefficients: TableCoefficients, trace: Trace): number;
|
||||
/**
|
||||
* ÉNERGIE : production PV autoconsommée vs besoin d'électricité spécifique.
|
||||
* null si aucun kWc dans la sélection.
|
||||
*/
|
||||
export declare function tauxEnergie(entrees: {
|
||||
kwc: number;
|
||||
batterie_kwh: number;
|
||||
orientation_toit?: string;
|
||||
nb_personnes?: number;
|
||||
contexte: Pick<Contexte, 'zone_climatique_rt' | 'zone_biogeo_code'>;
|
||||
}, coefficients: TableCoefficients, trace: Trace): {
|
||||
taux: number | null;
|
||||
detail: DetailEnergie | null;
|
||||
};
|
||||
/**
|
||||
* ALIMENTATION : mètres de haie vs recommandation. null si aucune haie.
|
||||
*/
|
||||
export declare function tauxAlim(entrees: {
|
||||
ml: number;
|
||||
nb_personnes?: number;
|
||||
}, coefficients: TableCoefficients, trace: Trace): {
|
||||
taux: number | null;
|
||||
detail: DetailAlim | null;
|
||||
};
|
||||
Vendored
+135
@@ -0,0 +1,135 @@
|
||||
// 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 } };
|
||||
}
|
||||
Vendored
+28
@@ -0,0 +1,28 @@
|
||||
export interface Fourchette {
|
||||
min: number;
|
||||
max: number;
|
||||
}
|
||||
/** Fourchette dont les deux bornes peuvent manquer (prix pro absent, etc.). */
|
||||
export interface FourchetteOuverte {
|
||||
min: number | null;
|
||||
max: number | null;
|
||||
}
|
||||
export declare function fourchette(min: number, max: number): Fourchette;
|
||||
export declare function pointMedian(f: Fourchette): number;
|
||||
/** Somme de fourchettes ; une borne manquante est remplacée par l'autre. */
|
||||
export declare function additionneFourchettes(liste: FourchetteOuverte[]): Fourchette;
|
||||
/** Arrondi au pas donné (pas ≤ 0 → valeur inchangée). */
|
||||
export declare function arrondiAuPas(valeur: number, pas: number): number;
|
||||
/** Arrondi à deux décimales. */
|
||||
export declare function arrondi2(n: number): number;
|
||||
/**
|
||||
* Mensualité d'un prêt à taux fixe (formule d'annuité constante).
|
||||
* @param capital montant emprunté
|
||||
* @param taux_annuel_pct taux nominal annuel en % (0 → capital / nombre de mois)
|
||||
* @param duree_annees durée en années
|
||||
*/
|
||||
export declare function mensualite(capital: number, taux_annuel_pct: number, duree_annees: number): number;
|
||||
/** Annuité (12 mensualités). */
|
||||
export declare function annuite(capital: number, taux_annuel_pct: number, duree_annees: number): number;
|
||||
/** Coût total du crédit (intérêts seuls). */
|
||||
export declare function coutDuCredit(capital: number, taux_annuel_pct: number, duree_annees: number): number;
|
||||
Vendored
+57
@@ -0,0 +1,57 @@
|
||||
// argent.ts — fourchettes, arrondis, annuité d'un prêt. Fonctions pures,
|
||||
// sans formatage (l'affichage « 1 200 € HT » appartient aux sites, pas au
|
||||
// paquet : Intl dépend de l'environnement).
|
||||
export function fourchette(min, max) {
|
||||
return min <= max ? { min, max } : { min: max, max: min };
|
||||
}
|
||||
export function pointMedian(f) {
|
||||
return (f.min + f.max) / 2;
|
||||
}
|
||||
/** Somme de fourchettes ; une borne manquante est remplacée par l'autre. */
|
||||
export function additionneFourchettes(liste) {
|
||||
let min = 0;
|
||||
let max = 0;
|
||||
for (const f of liste) {
|
||||
if (f.min == null && f.max == null)
|
||||
continue;
|
||||
const a = f.min ?? f.max ?? 0;
|
||||
const b = f.max ?? f.min ?? 0;
|
||||
min += a;
|
||||
max += b;
|
||||
}
|
||||
return { min, max };
|
||||
}
|
||||
/** Arrondi au pas donné (pas ≤ 0 → valeur inchangée). */
|
||||
export function arrondiAuPas(valeur, pas) {
|
||||
if (!pas || pas <= 0)
|
||||
return valeur;
|
||||
return Math.round(valeur / pas) * pas;
|
||||
}
|
||||
/** Arrondi à deux décimales. */
|
||||
export function arrondi2(n) {
|
||||
return Math.round(n * 100) / 100;
|
||||
}
|
||||
/**
|
||||
* Mensualité d'un prêt à taux fixe (formule d'annuité constante).
|
||||
* @param capital montant emprunté
|
||||
* @param taux_annuel_pct taux nominal annuel en % (0 → capital / nombre de mois)
|
||||
* @param duree_annees durée en années
|
||||
*/
|
||||
export function mensualite(capital, taux_annuel_pct, duree_annees) {
|
||||
const n = Math.round(duree_annees * 12);
|
||||
if (n <= 0 || capital <= 0)
|
||||
return 0;
|
||||
const r = taux_annuel_pct / 100 / 12;
|
||||
if (r === 0)
|
||||
return capital / n;
|
||||
return (capital * r) / (1 - Math.pow(1 + r, -n));
|
||||
}
|
||||
/** Annuité (12 mensualités). */
|
||||
export function annuite(capital, taux_annuel_pct, duree_annees) {
|
||||
return mensualite(capital, taux_annuel_pct, duree_annees) * 12;
|
||||
}
|
||||
/** Coût total du crédit (intérêts seuls). */
|
||||
export function coutDuCredit(capital, taux_annuel_pct, duree_annees) {
|
||||
const n = Math.round(duree_annees * 12);
|
||||
return mensualite(capital, taux_annuel_pct, duree_annees) * n - capital;
|
||||
}
|
||||
Vendored
+69
@@ -0,0 +1,69 @@
|
||||
export type Orientation = 'sud' | 'sud-est' | 'sud-ouest' | 'est' | 'ouest' | 'nord';
|
||||
export type ClasseRt = 'H1' | 'H2' | 'H3';
|
||||
export interface Coefficient<T = number> {
|
||||
valeur: T;
|
||||
/** Document ou norme qui fixe la valeur ; « heuristique … » si non sourcée. */
|
||||
source: string;
|
||||
/** Date du document source (AAAA-MM-JJ). */
|
||||
date: string;
|
||||
/** Fourchette, variante, réserve : ce que Jules doit savoir pour mettre un veto. */
|
||||
note?: string;
|
||||
}
|
||||
export interface TableCoefficients {
|
||||
/** Rendement de captage d'une toiture (part de la pluie réellement collectée). */
|
||||
captage_rendement: Coefficient;
|
||||
/** Rendement par type de toiture, pour un formulaire enrichi (non utilisé par les moteurs v0.1). */
|
||||
captage_rendement_par_toiture: Coefficient<Record<'tuile' | 'ardoise' | 'gravillonnee' | 'vegetalisee', number>>;
|
||||
/** Besoin domestique total, litres par jour et par personne. */
|
||||
besoin_eau_l_j_pers: Coefficient;
|
||||
/** Part du besoin substituable par l'eau de pluie (WC, douche, lessive, jardin). */
|
||||
part_eau_substituable: Coefficient;
|
||||
/** Facteur de production PV selon l'orientation du toit (sud 30° = 1). */
|
||||
orientation_pv: Coefficient<Record<Orientation, number>>;
|
||||
/** Productible PV annuel (kWh/kWc/an), par zone : `par` dit quelle clé du contexte indexe la table. */
|
||||
productible_pv_kwh_kwc: Coefficient<{
|
||||
par: 'zone_rt' | 'zone_biogeo_code';
|
||||
table: Record<string, number>;
|
||||
defaut: number;
|
||||
}>;
|
||||
/** Part de la production PV autoconsommée, sans batterie. */
|
||||
autoconso_sans_batterie: Coefficient;
|
||||
/** Part de la production PV autoconsommée, avec 10 kWh de batterie. */
|
||||
autoconso_avec_batterie_10kwh: Coefficient;
|
||||
/** Besoin d'électricité spécifique, kWh par personne et par an (hors chauffage électrique). */
|
||||
besoin_elec_kwh_pers_an: Coefficient;
|
||||
/** DJU base 18 par défaut, par classe de zone RT, si la ligne climat n'en porte pas. */
|
||||
dju_defaut_par_classe_rt: Coefficient<Record<ClasseRt, number>>;
|
||||
/** Mètres linéaires de haie fruitière par personne pour couvrir les fruits d'un foyer. */
|
||||
ml_haie_par_pers: Coefficient;
|
||||
/** Contexte climatique appliqué quand le département est inconnu (déclaré en avertissement). */
|
||||
contexte_defaut: Coefficient<{
|
||||
zone_rt: string;
|
||||
zone_biogeo_code: number;
|
||||
pluviometrie_mm: number;
|
||||
dju: number;
|
||||
}>;
|
||||
/** SHAB estimée = emprise × niveaux × ce ratio (murs, refends, escalier). */
|
||||
geometrie_ratio_shab_emprise: Coefficient;
|
||||
/** Surface vitrée = SHAB × ce ratio. */
|
||||
geometrie_ratio_vitrage_shab: Coefficient;
|
||||
/** Hauteur d'un niveau par défaut (m), si la hauteur totale n'est pas connue. */
|
||||
geometrie_hauteur_etage_m: Coefficient;
|
||||
/** Périmètre estimé = 4 × √(emprise × facteur) (rectangle allongé). */
|
||||
geometrie_facteur_perimetre: Coefficient;
|
||||
/** Part du périmètre retirée par côté mitoyen. */
|
||||
geometrie_part_mur_mitoyen: Coefficient;
|
||||
/** Majoration de la surface de toiture (débords, recouvrements). */
|
||||
geometrie_majoration_toiture: Coefficient;
|
||||
/** Pente de toit par défaut (degrés) si inconnue. */
|
||||
geometrie_pente_defaut_deg: Coefficient;
|
||||
}
|
||||
/** Table harmonisée (LOT P1.0, 28/09/2026). Référence tant que Jules ne met pas de veto (R-7). */
|
||||
export declare const COEFFICIENTS: TableCoefficients;
|
||||
/** Clés de la table, pour valider un jeu de coefficients injecté (fixture, veto). */
|
||||
export declare const CLES_COEFFICIENTS: (keyof TableCoefficients)[];
|
||||
/**
|
||||
* Vérifie qu'un objet a la forme d'une table de coefficients (toutes les clés,
|
||||
* chaque entrée avec valeur, source et date). Renvoie la liste des défauts.
|
||||
*/
|
||||
export declare function verifieTableCoefficients(table: unknown): string[];
|
||||
Vendored
+160
@@ -0,0 +1,160 @@
|
||||
// coefficients.ts — chaque grandeur physique UNE fois, avec source et date.
|
||||
//
|
||||
// Règle : le document de recherche l'emporte sur le code quand ils divergent
|
||||
// (SPEC phase 1 §4.1, LOT P1.0 point 5). La table lisible par Jules est
|
||||
// docs/CONSTANTES.md (une ligne par grandeur : ancienne(s) valeur(s) →
|
||||
// retenue, source, date). Le jeu de coefficients d'ORIGINE (valeurs du code
|
||||
// de calculs.trans-former.fr au 28/09/2026) vit dans
|
||||
// fixtures/coefficients-2026-05.json et sert au test de non-régression.
|
||||
//
|
||||
// Les poids d'axes, modulateurs, matrices d'adéquation et tolérances de
|
||||
// algo-config.json ne sont PAS ici : ce sont des heuristiques de conception,
|
||||
// non sourcées, livrées comme données (fixtures/algo-config.json) et injectées.
|
||||
const D_RECHERCHE = '2026-04-28';
|
||||
const D_LOT = '2026-09-28';
|
||||
const S_EAU = '1 PROJETS/DEV/Simulateur autonomie V2/01-recherche-eau.md';
|
||||
const S_ENERGIE = '1 PROJETS/DEV/Simulateur autonomie V2/02-recherche-energie.md';
|
||||
const S_BRIEF = '1 PROJETS/DEV/Simulateur autonomie V2/99-brief-atis-dev.md';
|
||||
const S_V1 = 'calculs.trans-former.fr, simulateur autoconstruction V1 (public/simulateurs/autoconstruction/app.js, calcSurfaces)';
|
||||
/** Table harmonisée (LOT P1.0, 28/09/2026). Référence tant que Jules ne met pas de veto (R-7). */
|
||||
export const COEFFICIENTS = {
|
||||
captage_rendement: {
|
||||
valeur: 0.8,
|
||||
source: `${S_EAU} §3 « Captable max » et §7 n°6 (NF EN 16941-1, recueil Nény p.16)`,
|
||||
date: D_RECHERCHE,
|
||||
note: 'fourchette 0,75 (zone sèche) à 0,85 (zone humide, gouttières optimisées) ; le code portait 0,7 (scoreurs), 0,8 (taux) et 0,85 (V1 bioclimatique)',
|
||||
},
|
||||
captage_rendement_par_toiture: {
|
||||
valeur: { tuile: 0.85, ardoise: 0.85, gravillonnee: 0.5, vegetalisee: 0.3 },
|
||||
source: `${S_EAU} §7 n°6 (ADEME, réf. NF EN 16941-1)`,
|
||||
date: D_RECHERCHE,
|
||||
note: 'à brancher quand le formulaire demande le type de toiture ; non utilisé en v0.1',
|
||||
},
|
||||
besoin_eau_l_j_pers: {
|
||||
valeur: 150,
|
||||
source: `${S_EAU} §3 « Besoin eau » (INSEE / Eau France 2024) ; ${S_BRIEF} §7.1`,
|
||||
date: D_RECHERCHE,
|
||||
note: '54 750 L/an/personne ; le V1 comptait 46,5 m³/an',
|
||||
},
|
||||
part_eau_substituable: {
|
||||
valeur: 0.7,
|
||||
source: `${S_EAU} §3 ; ${S_BRIEF} §7.1 (WC 30 + douche 60 + lessive 30 + jardin 25 L/j sur 150)`,
|
||||
date: D_RECHERCHE,
|
||||
note: 'le V1 comptait 32 % (15 m³ sur 46,5)',
|
||||
},
|
||||
orientation_pv: {
|
||||
valeur: { sud: 1, 'sud-est': 0.95, 'sud-ouest': 0.95, est: 0.8, ouest: 0.8, nord: 0.6 },
|
||||
source: `${S_ENERGIE} §3.2 « Variantes orientation toiture »`,
|
||||
date: D_RECHERCHE,
|
||||
note: 'source : SE/SO 95 %, E/O 80-85 %, N 60-65 % (bas de fourchette retenu) ; le brief §7.2 prenait les milieux 0,82 et 0,62 ; le code portait 0,93 / 0,78 / 0,40',
|
||||
},
|
||||
productible_pv_kwh_kwc: {
|
||||
valeur: {
|
||||
par: 'zone_rt',
|
||||
table: { H1a: 1000, H1b: 1050, H1c: 1200, H2a: 1100, H2b: 1200, H2c: 1250, H2d: 1300, H3: 1400 },
|
||||
defaut: 1100,
|
||||
},
|
||||
source: `${S_ENERGIE} §3.2 (PVGIS, sud 30°, pertes 14 %)`,
|
||||
date: D_RECHERCHE,
|
||||
note: 'H1a : moyenne Lille 950 / Paris 1 050 ; H2d : interpolé entre H2c et H3 par la MOE, non sourcé (déclaré en avertissement) ; défaut = H2a ; le code portait une table par zone biogéographique allant jusqu\'à 1 550, au-dessus du maximum de la source',
|
||||
},
|
||||
autoconso_sans_batterie: {
|
||||
valeur: 0.3,
|
||||
source: `${S_ENERGIE} §3.2 ; ${S_BRIEF} §7.2`,
|
||||
date: D_RECHERCHE,
|
||||
note: 'absent du code : la couverture énergie comparait la production brute au besoin',
|
||||
},
|
||||
autoconso_avec_batterie_10kwh: {
|
||||
valeur: 0.7,
|
||||
source: `${S_ENERGIE} §3.2 ; ${S_BRIEF} §7.2`,
|
||||
date: D_RECHERCHE,
|
||||
note: 'entre 0 et 10 kWh de batterie, le moteur interpole linéairement (choix MOE, non sourcé)',
|
||||
},
|
||||
besoin_elec_kwh_pers_an: {
|
||||
valeur: 1000,
|
||||
source: `${S_ENERGIE} §3.1 (ADEME 2023, RTE 2024) ; ${S_BRIEF} §7.2`,
|
||||
date: D_RECHERCHE,
|
||||
note: 'fourchette 900 à 1 200 ; hors chauffage électrique, ECS et véhicule',
|
||||
},
|
||||
dju_defaut_par_classe_rt: {
|
||||
valeur: { H1: 2800, H2: 2200, H3: 1500 },
|
||||
source: 'heuristique de conception V2 (calibrage.js et algo-config.json), non sourcée',
|
||||
date: '2026-05-01',
|
||||
note: 'repli seulement : la table france_climat porte un DJU par département',
|
||||
},
|
||||
ml_haie_par_pers: {
|
||||
valeur: 25,
|
||||
source: `${S_BRIEF} §7.3 (correction Jules du 28/04/2026) ; 03-recherche-alimentation.md §5`,
|
||||
date: D_RECHERCHE,
|
||||
note: '50 ml/personne pour un excédent transformable',
|
||||
},
|
||||
contexte_defaut: {
|
||||
valeur: { zone_rt: 'H2a', zone_biogeo_code: 4, pluviometrie_mm: 800, dju: 2200 },
|
||||
source: 'heuristique de conception V2 (algo.js deriveContexte, simulation.js), non sourcée',
|
||||
date: '2026-05-01',
|
||||
note: 'appliqué seulement si le département est absent de france_climat ; toujours déclaré en avertissement',
|
||||
},
|
||||
geometrie_ratio_shab_emprise: {
|
||||
valeur: 0.8,
|
||||
source: 'heuristique MOE (murs extérieurs ~0,3 m, refends, escalier), non sourcée',
|
||||
date: D_LOT,
|
||||
note: 'hypothèse porteuse du P1.2 : à confronter à la surface DPE des adresses A et B (écart visé < 20 %)',
|
||||
},
|
||||
geometrie_ratio_vitrage_shab: {
|
||||
valeur: 1 / 6,
|
||||
source: `${S_V1} (vit = S/6) ; règle RT 2012 (baies ≥ 1/6 de la SHAB)`,
|
||||
date: '2025-01-01',
|
||||
note: 'la SPEC §4.1 écrit « 15 % » ; 1/6 = 16,7 %',
|
||||
},
|
||||
geometrie_hauteur_etage_m: {
|
||||
valeur: 2.5,
|
||||
source: 'heuristique MOE (hauteur sous plafond courante), non sourcée',
|
||||
date: D_LOT,
|
||||
},
|
||||
geometrie_facteur_perimetre: {
|
||||
valeur: 1.3,
|
||||
source: `${S_V1} (perimetre = √(S × 1,3) × 4)`,
|
||||
date: '2025-01-01',
|
||||
},
|
||||
geometrie_part_mur_mitoyen: {
|
||||
valeur: 0.25,
|
||||
source: 'heuristique MOE (un côté mitoyen = un quart du périmètre), non sourcée',
|
||||
date: D_LOT,
|
||||
},
|
||||
geometrie_majoration_toiture: {
|
||||
valeur: 1.05,
|
||||
source: `${S_V1} (toiture = S / cos α × 1,05)`,
|
||||
date: '2025-01-01',
|
||||
},
|
||||
geometrie_pente_defaut_deg: {
|
||||
valeur: 30,
|
||||
source: 'heuristique MOE (pente courante des toitures à deux versants), non sourcée',
|
||||
date: D_LOT,
|
||||
},
|
||||
};
|
||||
/** Clés de la table, pour valider un jeu de coefficients injecté (fixture, veto). */
|
||||
export const CLES_COEFFICIENTS = Object.keys(COEFFICIENTS);
|
||||
/**
|
||||
* Vérifie qu'un objet a la forme d'une table de coefficients (toutes les clés,
|
||||
* chaque entrée avec valeur, source et date). Renvoie la liste des défauts.
|
||||
*/
|
||||
export function verifieTableCoefficients(table) {
|
||||
const erreurs = [];
|
||||
if (!table || typeof table !== 'object')
|
||||
return ['table absente ou non objet'];
|
||||
const t = table;
|
||||
for (const cle of CLES_COEFFICIENTS) {
|
||||
const c = t[cle];
|
||||
if (!c || typeof c !== 'object') {
|
||||
erreurs.push(`${cle} : absent`);
|
||||
continue;
|
||||
}
|
||||
if (!('valeur' in c))
|
||||
erreurs.push(`${cle} : valeur absente`);
|
||||
if (typeof c.source !== 'string' || !c.source)
|
||||
erreurs.push(`${cle} : source absente`);
|
||||
if (typeof c.date !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(c.date))
|
||||
erreurs.push(`${cle} : date absente ou non AAAA-MM-JJ`);
|
||||
}
|
||||
return erreurs;
|
||||
}
|
||||
Vendored
+49
@@ -0,0 +1,49 @@
|
||||
import type { Moteur, Resultat } from './contrat.js';
|
||||
import { type TableCoefficients } from './coefficients.js';
|
||||
export interface LigneClimat {
|
||||
dept: string;
|
||||
nom_dept?: string;
|
||||
zone_rt: string;
|
||||
pluvio_mm_an?: number | null;
|
||||
dju_base_18?: number | null;
|
||||
zone_biogeo?: number | null;
|
||||
[autre: string]: unknown;
|
||||
}
|
||||
export type ClasseRt = 'H1' | 'H2' | 'H3';
|
||||
export type ZoneBiogeo = 'C' | 'M' | 'OC' | 'MED';
|
||||
export interface Contexte {
|
||||
/** Département tel que trouvé (« 2A », « 971 », « 75 ») ou null si inconnu. */
|
||||
departement: string | null;
|
||||
zone_climatique_rt: string;
|
||||
zone_rt_class: ClasseRt;
|
||||
/** Zone écologique (biogéographique) en lettres : C, M, OC, MED. */
|
||||
zone_biogeo: ZoneBiogeo;
|
||||
/** Zone biogéographique numérique (1 à 8) telle que portée par france_climat. */
|
||||
zone_biogeo_code: number;
|
||||
pluviometrie_mm: number;
|
||||
dju: number;
|
||||
}
|
||||
export interface EntreesContexte {
|
||||
code_postal: string;
|
||||
}
|
||||
export interface DonneesContexte {
|
||||
france_climat: LigneClimat[];
|
||||
coefficients?: TableCoefficients;
|
||||
}
|
||||
/** Zone biogéographique numérique → libellé (V2 calibrage.js, inchangé). */
|
||||
export declare const ZONE_BIOGEO_MAP: Record<number, ZoneBiogeo>;
|
||||
/** Sous-zone RT (H1a, H2d…) → classe principale. */
|
||||
export declare function zoneRtToClass(zone_rt: string | null | undefined): ClasseRt;
|
||||
/**
|
||||
* Département depuis un code postal : DOM (97x → trois caractères), Corse
|
||||
* (20000-20190 → 2A, 20200-20620 → 2B, autre 20xxx → 2A), sinon deux chiffres.
|
||||
* Reprend `deptFromCp` de calculs (scripts/autonomie/data.js), seule version
|
||||
* qui gérait ces cas. Renvoie null si le code postal est inexploitable.
|
||||
*/
|
||||
export declare function departementDepuisCodePostal(code_postal: string | number | null | undefined): string | null;
|
||||
/**
|
||||
* Dérive le contexte climatique. Fonction pure, utilisée par le moteur
|
||||
* `commun.contexte` et par le noyau autonomie (`selectionner`).
|
||||
*/
|
||||
export declare function deriveContexte(code_postal: string | number | null | undefined, climat: LigneClimat[], coefficients?: TableCoefficients): Resultat<Contexte>;
|
||||
export declare const contexte: Moteur<EntreesContexte, DonneesContexte, Contexte>;
|
||||
Vendored
+102
@@ -0,0 +1,102 @@
|
||||
// contexte.ts — moteur `commun.contexte` : code postal → département → zone.
|
||||
// UNE seule version (le noyau autonomie l'utilise aussi). Corse 2A/2B et
|
||||
// DOM 971-976 corrigés : le code d'origine (algo.js) prenait deux chiffres et
|
||||
// retombait sur H2a pour ces départements.
|
||||
import { Trace } from './contrat.js';
|
||||
import { COEFFICIENTS } from './coefficients.js';
|
||||
import { VERSION } from '../version.js';
|
||||
/** Zone biogéographique numérique → libellé (V2 calibrage.js, inchangé). */
|
||||
export const ZONE_BIOGEO_MAP = {
|
||||
1: 'C', // Continental
|
||||
2: 'M', // Montagnard
|
||||
3: 'OC', // Océanique côtier
|
||||
4: 'OC', // Océanique intérieur
|
||||
5: 'MED', // Méditerranéen continental
|
||||
6: 'MED', // Méditerranéen côtier
|
||||
7: 'MED', // Pré-alpin méditerranéen
|
||||
8: 'OC', // Semi-océanique
|
||||
};
|
||||
/** Sous-zone RT (H1a, H2d…) → classe principale. */
|
||||
export function zoneRtToClass(zone_rt) {
|
||||
if (!zone_rt)
|
||||
return 'H2';
|
||||
if (zone_rt.startsWith('H1'))
|
||||
return 'H1';
|
||||
if (zone_rt.startsWith('H2'))
|
||||
return 'H2';
|
||||
if (zone_rt === 'H3')
|
||||
return 'H3';
|
||||
return 'H2';
|
||||
}
|
||||
/**
|
||||
* Département depuis un code postal : DOM (97x → trois caractères), Corse
|
||||
* (20000-20190 → 2A, 20200-20620 → 2B, autre 20xxx → 2A), sinon deux chiffres.
|
||||
* Reprend `deptFromCp` de calculs (scripts/autonomie/data.js), seule version
|
||||
* qui gérait ces cas. Renvoie null si le code postal est inexploitable.
|
||||
*/
|
||||
export function departementDepuisCodePostal(code_postal) {
|
||||
const cp = String(code_postal ?? '').trim();
|
||||
if (!/^\d{5}$/.test(cp))
|
||||
return cp.length >= 2 && /^\d{2}/.test(cp) ? cp.slice(0, 2) : null;
|
||||
if (cp.startsWith('97'))
|
||||
return cp.slice(0, 3);
|
||||
if (cp.startsWith('20')) {
|
||||
const n = Number(cp);
|
||||
if (n >= 20000 && n <= 20190)
|
||||
return '2A';
|
||||
if (n >= 20200 && n <= 20620)
|
||||
return '2B';
|
||||
return '2A';
|
||||
}
|
||||
return cp.slice(0, 2);
|
||||
}
|
||||
/**
|
||||
* Dérive le contexte climatique. Fonction pure, utilisée par le moteur
|
||||
* `commun.contexte` et par le noyau autonomie (`selectionner`).
|
||||
*/
|
||||
export function deriveContexte(code_postal, climat, coefficients = COEFFICIENTS) {
|
||||
const trace = new Trace();
|
||||
const departement = departementDepuisCodePostal(code_postal);
|
||||
const ligne = departement ? climat.find((c) => c.dept === departement) : undefined;
|
||||
if (!ligne) {
|
||||
const d = coefficients.contexte_defaut;
|
||||
trace.avertit(`contexte par défaut appliqué (${d.valeur.zone_rt}, ${d.valeur.pluviometrie_mm} mm, ${d.valeur.dju} DJU) : ` +
|
||||
(departement ? `département ${departement} absent de france_climat` : `code postal « ${code_postal ?? ''} » inexploitable`));
|
||||
trace.source('contexte_defaut', d.valeur.zone_rt, d.source, d.date);
|
||||
return trace.resultat({
|
||||
departement,
|
||||
zone_climatique_rt: d.valeur.zone_rt,
|
||||
zone_rt_class: zoneRtToClass(d.valeur.zone_rt),
|
||||
zone_biogeo: ZONE_BIOGEO_MAP[d.valeur.zone_biogeo_code] ?? 'OC',
|
||||
zone_biogeo_code: d.valeur.zone_biogeo_code,
|
||||
pluviometrie_mm: d.valeur.pluviometrie_mm,
|
||||
dju: d.valeur.dju,
|
||||
}, VERSION);
|
||||
}
|
||||
const d = coefficients.contexte_defaut;
|
||||
const zone_biogeo_code = ligne.zone_biogeo ?? d.valeur.zone_biogeo_code;
|
||||
if (ligne.zone_biogeo == null)
|
||||
trace.avertit(`zone biogéographique absente pour ${departement}, défaut ${d.valeur.zone_biogeo_code}`);
|
||||
const pluviometrie_mm = ligne.pluvio_mm_an ?? d.valeur.pluviometrie_mm;
|
||||
if (ligne.pluvio_mm_an == null)
|
||||
trace.avertit(`pluviométrie absente pour ${departement}, défaut ${d.valeur.pluviometrie_mm} mm`);
|
||||
const dju = ligne.dju_base_18 ?? d.valeur.dju;
|
||||
if (ligne.dju_base_18 == null)
|
||||
trace.avertit(`DJU absent pour ${departement}, défaut ${d.valeur.dju}`);
|
||||
trace.source(`france_climat.${departement}`, ligne.zone_rt, String(ligne.source_climat ?? 'france_climat'), String(ligne.source_date ?? ''));
|
||||
return trace.resultat({
|
||||
departement,
|
||||
zone_climatique_rt: ligne.zone_rt,
|
||||
zone_rt_class: zoneRtToClass(ligne.zone_rt),
|
||||
zone_biogeo: ZONE_BIOGEO_MAP[zone_biogeo_code] ?? 'OC',
|
||||
zone_biogeo_code,
|
||||
pluviometrie_mm,
|
||||
dju,
|
||||
}, VERSION);
|
||||
}
|
||||
export const contexte = {
|
||||
id: 'commun.contexte',
|
||||
calculer(entrees, donnees) {
|
||||
return deriveContexte(entrees.code_postal, donnees.france_climat, donnees.coefficients ?? COEFFICIENTS);
|
||||
},
|
||||
};
|
||||
Vendored
+38
@@ -0,0 +1,38 @@
|
||||
/** Une constante ou une donnée effectivement utilisée par un calcul. */
|
||||
export interface Source {
|
||||
cle: string;
|
||||
valeur: number | string;
|
||||
source: string;
|
||||
date: string;
|
||||
}
|
||||
export interface Resultat<S> {
|
||||
sorties: S;
|
||||
/** Tout coefficient et tout prix effectivement utilisé. */
|
||||
sources: Source[];
|
||||
/** Toute donnée manquante, toute valeur par défaut appliquée. */
|
||||
avertissements: string[];
|
||||
/** Étiquette du paquet (`vX.Y.Z`). */
|
||||
version: string;
|
||||
}
|
||||
export interface Moteur<E, D, S> {
|
||||
/** 'autonomie.eau', 'commun.geometrie', … */
|
||||
id: string;
|
||||
calculer(entrees: E, donnees: D): Resultat<S>;
|
||||
}
|
||||
/**
|
||||
* Collecteur de sources et d'avertissements d'un calcul. Une source est
|
||||
* enregistrée une fois par clé (la première valeur gagne) ; un avertissement
|
||||
* une fois par texte. Objet interne, sans I/O.
|
||||
*/
|
||||
export declare class Trace {
|
||||
private readonly sources;
|
||||
private readonly avertissements;
|
||||
source(cle: string, valeur: number | string, source: string, date: string): void;
|
||||
avertit(texte: string): void;
|
||||
/** Verse dans ce collecteur les sources et avertissements d'un autre résultat. */
|
||||
absorbe(autre: {
|
||||
sources: Source[];
|
||||
avertissements: string[];
|
||||
}): void;
|
||||
resultat<S>(sorties: S, version: string): Resultat<S>;
|
||||
}
|
||||
Vendored
+41
@@ -0,0 +1,41 @@
|
||||
// contrat.ts — le contrat d'un moteur (SPEC phase 1 §4.1).
|
||||
//
|
||||
// Quatre règles opposables au code :
|
||||
// ① `calculer` ne lit ni réseau, ni disque, ni horloge, ni DOM, ni variable
|
||||
// globale, ni aléa : mêmes entrées + mêmes données = même résultat.
|
||||
// ② les données entrent en paramètre, validées à la frontière par l'appelant
|
||||
// (`valider(table, json)`) ;
|
||||
// ③ aucun prix, aucun barème dans le paquet ;
|
||||
// ④ toute valeur par défaut appliquée est déclarée dans `avertissements`,
|
||||
// toute constante utilisée dans `sources`.
|
||||
/**
|
||||
* Collecteur de sources et d'avertissements d'un calcul. Une source est
|
||||
* enregistrée une fois par clé (la première valeur gagne) ; un avertissement
|
||||
* une fois par texte. Objet interne, sans I/O.
|
||||
*/
|
||||
export class Trace {
|
||||
sources = new Map();
|
||||
avertissements = new Set();
|
||||
source(cle, valeur, source, date) {
|
||||
if (!this.sources.has(cle))
|
||||
this.sources.set(cle, { cle, valeur, source, date });
|
||||
}
|
||||
avertit(texte) {
|
||||
this.avertissements.add(texte);
|
||||
}
|
||||
/** Verse dans ce collecteur les sources et avertissements d'un autre résultat. */
|
||||
absorbe(autre) {
|
||||
for (const s of autre.sources)
|
||||
this.source(s.cle, s.valeur, s.source, s.date);
|
||||
for (const a of autre.avertissements)
|
||||
this.avertit(a);
|
||||
}
|
||||
resultat(sorties, version) {
|
||||
return {
|
||||
sorties,
|
||||
sources: [...this.sources.values()],
|
||||
avertissements: [...this.avertissements],
|
||||
version,
|
||||
};
|
||||
}
|
||||
}
|
||||
Vendored
+37
@@ -0,0 +1,37 @@
|
||||
import type { Moteur } from './contrat.js';
|
||||
import { type TableCoefficients } from './coefficients.js';
|
||||
export interface EntreesGeometrie {
|
||||
/** Emprise au sol du bâti (m²). */
|
||||
emprise_m2: number;
|
||||
/** Nombre de niveaux habitables (1 = plain-pied). */
|
||||
niveaux?: number;
|
||||
/** Hauteur totale du bâti hors combles (m) ; sinon niveaux × hauteur d'étage par défaut. */
|
||||
hauteur_m?: number;
|
||||
/** Pente du toit (degrés) ; sinon pente par défaut. */
|
||||
pente_toit_deg?: number;
|
||||
/** Côtés mitoyens (0, 1 ou 2). */
|
||||
mitoyennete?: 0 | 1 | 2;
|
||||
/** Surface habitable connue (DPE, acte) : remplace l'estimation. */
|
||||
surface_habitable_m2?: number;
|
||||
}
|
||||
export interface DonneesGeometrie {
|
||||
coefficients?: TableCoefficients;
|
||||
}
|
||||
export interface SortiesGeometrie {
|
||||
/** Surface habitable, connue ou estimée (m²). */
|
||||
shab_m2: number;
|
||||
shab_estimee: boolean;
|
||||
perimetre_m: number;
|
||||
/** Périmètre exposé après retrait des côtés mitoyens (m). */
|
||||
perimetre_expose_m: number;
|
||||
murs_exterieurs_m2: number;
|
||||
murs_opaques_m2: number;
|
||||
vitrage_m2: number;
|
||||
toiture_m2: number;
|
||||
plancher_bas_m2: number;
|
||||
volume_m3: number;
|
||||
hauteur_m: number;
|
||||
hauteur_etage_m: number;
|
||||
}
|
||||
export declare function calculeGeometrie(entrees: EntreesGeometrie, coefficients?: TableCoefficients): import("./contrat.js").Resultat<SortiesGeometrie>;
|
||||
export declare const geometrie: Moteur<EntreesGeometrie, DonneesGeometrie, SortiesGeometrie>;
|
||||
Vendored
+79
@@ -0,0 +1,79 @@
|
||||
// geometrie.ts — moteur `commun.geometrie` : emprise, niveaux, hauteur, pente,
|
||||
// mitoyenneté → surfaces de parois, SHAB estimée, volume. Reprend les formules
|
||||
// de `calcSurfaces` du simulateur autoconstruction V1 (calculs), sans le DOM,
|
||||
// avec les coefficients sortis dans coefficients.ts.
|
||||
import { Trace } from './contrat.js';
|
||||
import { COEFFICIENTS } from './coefficients.js';
|
||||
import { arrondi2 } from './argent.js';
|
||||
import { VERSION } from '../version.js';
|
||||
export function calculeGeometrie(entrees, coefficients = COEFFICIENTS) {
|
||||
const trace = new Trace();
|
||||
const c = coefficients;
|
||||
const emprise = Math.max(0, entrees.emprise_m2 || 0);
|
||||
const niveaux = Math.max(1, Math.round(entrees.niveaux ?? 1));
|
||||
if (entrees.niveaux == null)
|
||||
trace.avertit('nombre de niveaux inconnu : 1 niveau supposé');
|
||||
let hauteur = entrees.hauteur_m ?? 0;
|
||||
if (!(hauteur > 0)) {
|
||||
hauteur = niveaux * c.geometrie_hauteur_etage_m.valeur;
|
||||
trace.avertit(`hauteur inconnue : ${niveaux} × ${c.geometrie_hauteur_etage_m.valeur} m supposés`);
|
||||
trace.source('geometrie_hauteur_etage_m', c.geometrie_hauteur_etage_m.valeur, c.geometrie_hauteur_etage_m.source, c.geometrie_hauteur_etage_m.date);
|
||||
}
|
||||
const hauteur_etage = hauteur / niveaux;
|
||||
let pente = entrees.pente_toit_deg;
|
||||
if (pente == null || !(pente >= 0) || pente >= 90) {
|
||||
pente = c.geometrie_pente_defaut_deg.valeur;
|
||||
trace.avertit(`pente de toit inconnue : ${pente}° supposés`);
|
||||
trace.source('geometrie_pente_defaut_deg', pente, c.geometrie_pente_defaut_deg.source, c.geometrie_pente_defaut_deg.date);
|
||||
}
|
||||
const mitoyen = Math.min(2, Math.max(0, Math.round(entrees.mitoyennete ?? 0)));
|
||||
// SHAB
|
||||
let shab;
|
||||
let shab_estimee;
|
||||
if (entrees.surface_habitable_m2 && entrees.surface_habitable_m2 > 0) {
|
||||
shab = entrees.surface_habitable_m2;
|
||||
shab_estimee = false;
|
||||
}
|
||||
else {
|
||||
shab = emprise * niveaux * c.geometrie_ratio_shab_emprise.valeur;
|
||||
shab_estimee = true;
|
||||
trace.avertit('surface habitable estimée depuis l\'emprise (aucune surface connue)');
|
||||
trace.source('geometrie_ratio_shab_emprise', c.geometrie_ratio_shab_emprise.valeur, c.geometrie_ratio_shab_emprise.source, c.geometrie_ratio_shab_emprise.date);
|
||||
}
|
||||
// Périmètre (V1 : √(S × 1,3) × 4) et part exposée
|
||||
const perimetre = 4 * Math.sqrt(emprise * c.geometrie_facteur_perimetre.valeur);
|
||||
trace.source('geometrie_facteur_perimetre', c.geometrie_facteur_perimetre.valeur, c.geometrie_facteur_perimetre.source, c.geometrie_facteur_perimetre.date);
|
||||
const perimetre_expose = perimetre * (1 - c.geometrie_part_mur_mitoyen.valeur * mitoyen);
|
||||
if (mitoyen > 0)
|
||||
trace.source('geometrie_part_mur_mitoyen', c.geometrie_part_mur_mitoyen.valeur, c.geometrie_part_mur_mitoyen.source, c.geometrie_part_mur_mitoyen.date);
|
||||
// Parois
|
||||
const murs_exterieurs = perimetre_expose * hauteur;
|
||||
const vitrage = shab * c.geometrie_ratio_vitrage_shab.valeur;
|
||||
trace.source('geometrie_ratio_vitrage_shab', arrondi2(c.geometrie_ratio_vitrage_shab.valeur), c.geometrie_ratio_vitrage_shab.source, c.geometrie_ratio_vitrage_shab.date);
|
||||
const murs_opaques = Math.max(0, murs_exterieurs - vitrage);
|
||||
const rad = (pente * Math.PI) / 180;
|
||||
const toiture = (emprise / Math.cos(rad)) * c.geometrie_majoration_toiture.valeur;
|
||||
trace.source('geometrie_majoration_toiture', c.geometrie_majoration_toiture.valeur, c.geometrie_majoration_toiture.source, c.geometrie_majoration_toiture.date);
|
||||
const volume = shab * hauteur_etage;
|
||||
const sorties = {
|
||||
shab_m2: arrondi2(shab),
|
||||
shab_estimee,
|
||||
perimetre_m: arrondi2(perimetre),
|
||||
perimetre_expose_m: arrondi2(perimetre_expose),
|
||||
murs_exterieurs_m2: arrondi2(murs_exterieurs),
|
||||
murs_opaques_m2: arrondi2(murs_opaques),
|
||||
vitrage_m2: arrondi2(vitrage),
|
||||
toiture_m2: arrondi2(toiture),
|
||||
plancher_bas_m2: arrondi2(emprise),
|
||||
volume_m3: arrondi2(volume),
|
||||
hauteur_m: arrondi2(hauteur),
|
||||
hauteur_etage_m: arrondi2(hauteur_etage),
|
||||
};
|
||||
return trace.resultat(sorties, VERSION);
|
||||
}
|
||||
export const geometrie = {
|
||||
id: 'commun.geometrie',
|
||||
calculer(entrees, donnees) {
|
||||
return calculeGeometrie(entrees, donnees?.coefficients ?? COEFFICIENTS);
|
||||
},
|
||||
};
|
||||
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
export type { Source, Resultat, Moteur } from './contrat.js';
|
||||
export { Trace } from './contrat.js';
|
||||
export type { Coefficient, TableCoefficients, Orientation, ClasseRt } from './coefficients.js';
|
||||
export { COEFFICIENTS, CLES_COEFFICIENTS, verifieTableCoefficients } from './coefficients.js';
|
||||
export type { LigneClimat, Contexte, EntreesContexte, DonneesContexte, ZoneBiogeo } from './contexte.js';
|
||||
export { contexte, deriveContexte, departementDepuisCodePostal, zoneRtToClass, ZONE_BIOGEO_MAP } from './contexte.js';
|
||||
export type { EntreesGeometrie, DonneesGeometrie, SortiesGeometrie } from './geometrie.js';
|
||||
export { geometrie, calculeGeometrie } from './geometrie.js';
|
||||
export type { Fourchette, FourchetteOuverte } from './argent.js';
|
||||
export { fourchette, pointMedian, additionneFourchettes, arrondiAuPas, arrondi2, mensualite, annuite, coutDuCredit } from './argent.js';
|
||||
export type { Schema, NomTable, TypeJson } from './schemas.js';
|
||||
export { SCHEMAS, NOMS_TABLES } from './schemas.js';
|
||||
export type { Validation } from './valider.js';
|
||||
export { valider, validerContre } from './valider.js';
|
||||
export { VERSION } from '../version.js';
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
export { Trace } from './contrat.js';
|
||||
export { COEFFICIENTS, CLES_COEFFICIENTS, verifieTableCoefficients } from './coefficients.js';
|
||||
export { contexte, deriveContexte, departementDepuisCodePostal, zoneRtToClass, ZONE_BIOGEO_MAP } from './contexte.js';
|
||||
export { geometrie, calculeGeometrie } from './geometrie.js';
|
||||
export { fourchette, pointMedian, additionneFourchettes, arrondiAuPas, arrondi2, mensualite, annuite, coutDuCredit } from './argent.js';
|
||||
export { SCHEMAS, NOMS_TABLES } from './schemas.js';
|
||||
export { valider, validerContre } from './valider.js';
|
||||
export { VERSION } from '../version.js';
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
export type TypeJson = 'string' | 'number' | 'integer' | 'boolean' | 'null' | 'object' | 'array';
|
||||
export interface Schema {
|
||||
$id?: string;
|
||||
description?: string;
|
||||
type: TypeJson | TypeJson[];
|
||||
required?: string[];
|
||||
properties?: Record<string, Schema>;
|
||||
items?: Schema;
|
||||
enum?: (string | number | boolean | null)[];
|
||||
minItems?: number;
|
||||
}
|
||||
export type NomTable = 'postes' | 'options' | 'france_climat' | 'algo_config';
|
||||
export declare const SCHEMAS: Record<NomTable, Schema>;
|
||||
export declare const NOMS_TABLES: NomTable[];
|
||||
Vendored
+121
@@ -0,0 +1,121 @@
|
||||
// schemas.ts — forme attendue de chaque JSON de données injecté dans les
|
||||
// moteurs. Source de vérité des fichiers `schemas/<table>.schema.json`, que
|
||||
// `npm run build` réécrit depuis ce module (scripts/ecrire-schemas.mjs) : le
|
||||
// paquet reste sans I/O, et un consommateur peut lire les JSON.
|
||||
//
|
||||
// Sous-ensemble de JSON Schema compris par `valider()` : type (simple ou
|
||||
// liste), required, properties, items, enum, minItems. Vérification
|
||||
// structurelle maison, pas un validateur JSON Schema complet.
|
||||
const nombreOuNull = { type: ['number', 'null'] };
|
||||
const chaineOuNull = { type: ['string', 'null'] };
|
||||
const booleenOuNull = { type: ['boolean', 'null'] };
|
||||
export const SCHEMAS = {
|
||||
postes: {
|
||||
$id: 'https://git.trans-former.fr/jules/moteurs/schemas/postes.schema.json',
|
||||
description: 'Postes du simulateur autonomie (calculs.trans-former.fr/calculs/data/postes.json). Champs lus par les moteurs.',
|
||||
type: 'array',
|
||||
minItems: 1,
|
||||
items: {
|
||||
type: 'object',
|
||||
required: ['Id', 'axe', 'code', 'poste'],
|
||||
properties: {
|
||||
Id: { type: 'integer' },
|
||||
axe: { type: 'string', enum: ['eau', 'energie', 'alimentation'] },
|
||||
code: { type: 'string' },
|
||||
poste: { type: 'string' },
|
||||
ordre: nombreOuNull,
|
||||
obligatoire: booleenOuNull,
|
||||
actif: booleenOuNull,
|
||||
description: chaineOuNull,
|
||||
},
|
||||
},
|
||||
},
|
||||
options: {
|
||||
$id: 'https://git.trans-former.fr/jules/moteurs/schemas/options.schema.json',
|
||||
description: 'Typologies (options) du simulateur autonomie. Les prix y sont des DONNÉES publiées par calculs, jamais des constantes du paquet. parametre_dim, couplages_requis et couplages_exclus arrivent en JSON sérialisé (NocoDB) ou déjà parsés.',
|
||||
type: 'array',
|
||||
minItems: 1,
|
||||
items: {
|
||||
type: 'object',
|
||||
required: ['Id', 'poste_id', 'code', 'nom'],
|
||||
properties: {
|
||||
Id: { type: 'integer' },
|
||||
poste_id: { type: 'integer' },
|
||||
code: { type: 'string' },
|
||||
nom: { type: 'string' },
|
||||
typologie: chaineOuNull,
|
||||
actif: booleenOuNull,
|
||||
prix_mat_min: nombreOuNull,
|
||||
prix_mat_max: nombreOuNull,
|
||||
prix_pro_min: nombreOuNull,
|
||||
prix_pro_max: nombreOuNull,
|
||||
prix_par_unite: nombreOuNull,
|
||||
prix_fixe: nombreOuNull,
|
||||
unite: chaineOuNull,
|
||||
quantite_default: nombreOuNull,
|
||||
parametre_dim: { type: ['string', 'object', 'null'] },
|
||||
couplages_requis: { type: ['string', 'array', 'null'] },
|
||||
couplages_exclus: { type: ['string', 'array', 'null'] },
|
||||
flag_diy: booleenOuNull,
|
||||
complexite_pose: nombreOuNull,
|
||||
accessible_fr: booleenOuNull,
|
||||
filiere_agreee: booleenOuNull,
|
||||
surface_min_m2: nombreOuNull,
|
||||
surface_optimale_m2: nombreOuNull,
|
||||
tags: { type: ['array', 'null'] },
|
||||
source: chaineOuNull,
|
||||
source_date: chaineOuNull,
|
||||
description: chaineOuNull,
|
||||
description_pedago: chaineOuNull,
|
||||
notes_techniques: chaineOuNull,
|
||||
},
|
||||
},
|
||||
},
|
||||
france_climat: {
|
||||
$id: 'https://git.trans-former.fr/jules/moteurs/schemas/france_climat.schema.json',
|
||||
description: 'Une ligne par département (2A, 2B et 971-976 compris) : zone RT, pluviométrie, DJU base 18, zone biogéographique numérique.',
|
||||
type: 'array',
|
||||
minItems: 1,
|
||||
items: {
|
||||
type: 'object',
|
||||
required: ['dept', 'zone_rt'],
|
||||
properties: {
|
||||
dept: { type: 'string' },
|
||||
nom_dept: chaineOuNull,
|
||||
zone_rt: { type: 'string', enum: ['H1a', 'H1b', 'H1c', 'H2a', 'H2b', 'H2c', 'H2d', 'H3'] },
|
||||
pluvio_mm_an: nombreOuNull,
|
||||
dju_base_18: nombreOuNull,
|
||||
zone_biogeo: nombreOuNull,
|
||||
source_climat: chaineOuNull,
|
||||
source_date: chaineOuNull,
|
||||
},
|
||||
},
|
||||
},
|
||||
algo_config: {
|
||||
$id: 'https://git.trans-former.fr/jules/moteurs/schemas/algo_config.schema.json',
|
||||
description: 'Heuristiques de conception du simulateur autonomie (poids, modulateurs, matrices, tolérances, alias, poids des postes). Données non sourcées, injectées, jamais importées.',
|
||||
type: 'object',
|
||||
required: ['poids_axes_base', 'modulateurs', 'adequation_weights', 'tolerances', 'bonus_malus', 'couplages_alias', 'postes_poids'],
|
||||
properties: {
|
||||
poids_axes_base: {
|
||||
type: 'object',
|
||||
required: ['eau', 'energie', 'alim'],
|
||||
properties: { eau: { type: 'number' }, energie: { type: 'number' }, alim: { type: 'number' } },
|
||||
},
|
||||
modulateurs: { type: 'object' },
|
||||
adequation_weights: { type: 'object' },
|
||||
tolerances: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
budget_overrun_pct: { type: 'number' },
|
||||
budget_underspend_pct: { type: 'number' },
|
||||
tolerance_par_poste_pct: { type: 'number' },
|
||||
},
|
||||
},
|
||||
bonus_malus: { type: 'object' },
|
||||
couplages_alias: { type: 'object' },
|
||||
postes_poids: { type: 'object' },
|
||||
},
|
||||
},
|
||||
};
|
||||
export const NOMS_TABLES = Object.keys(SCHEMAS);
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
import { type NomTable, type Schema } from './schemas.js';
|
||||
export interface Validation {
|
||||
ok: boolean;
|
||||
erreurs: string[];
|
||||
}
|
||||
/**
|
||||
* Valide un JSON de données contre le schéma de sa table. Renvoie au plus
|
||||
* vingt erreurs, chacune avec son chemin.
|
||||
*/
|
||||
export declare function valider(table: NomTable, json: unknown): Validation;
|
||||
/** Valide un objet contre un schéma arbitraire (même sous-ensemble). */
|
||||
export declare function validerContre(schema: Schema, json: unknown, nom?: string): Validation;
|
||||
Vendored
+77
@@ -0,0 +1,77 @@
|
||||
// valider.ts — validation structurelle d'un JSON de données contre le schéma
|
||||
// de sa table (schemas/<table>.schema.json, incarné par schemas.ts). Sans
|
||||
// dépendance, sans I/O. Ce n'est pas un validateur JSON Schema complet : type,
|
||||
// required, properties, items, enum, minItems.
|
||||
import { SCHEMAS } from './schemas.js';
|
||||
const MAX_ERREURS = 20;
|
||||
function typeDe(v) {
|
||||
if (v === null)
|
||||
return 'null';
|
||||
if (Array.isArray(v))
|
||||
return 'array';
|
||||
if (typeof v === 'number')
|
||||
return Number.isInteger(v) ? 'integer' : 'number';
|
||||
if (typeof v === 'string')
|
||||
return 'string';
|
||||
if (typeof v === 'boolean')
|
||||
return 'boolean';
|
||||
return 'object';
|
||||
}
|
||||
function typeAccepte(attendu, reel) {
|
||||
const liste = Array.isArray(attendu) ? attendu : [attendu];
|
||||
return liste.some((t) => t === reel || (t === 'number' && reel === 'integer'));
|
||||
}
|
||||
function verifie(schema, valeur, chemin, erreurs) {
|
||||
if (erreurs.length >= MAX_ERREURS)
|
||||
return;
|
||||
const reel = typeDe(valeur);
|
||||
if (!typeAccepte(schema.type, reel)) {
|
||||
erreurs.push(`${chemin} : type ${reel}, attendu ${Array.isArray(schema.type) ? schema.type.join('|') : schema.type}`);
|
||||
return;
|
||||
}
|
||||
if (schema.enum && !schema.enum.includes(valeur)) {
|
||||
erreurs.push(`${chemin} : valeur « ${String(valeur)} » hors de ${schema.enum.map(String).join(', ')}`);
|
||||
}
|
||||
if (reel === 'array') {
|
||||
const tableau = valeur;
|
||||
if (schema.minItems != null && tableau.length < schema.minItems) {
|
||||
erreurs.push(`${chemin} : ${tableau.length} élément(s), minimum ${schema.minItems}`);
|
||||
}
|
||||
if (schema.items) {
|
||||
for (let i = 0; i < tableau.length && erreurs.length < MAX_ERREURS; i++) {
|
||||
verifie(schema.items, tableau[i], `${chemin}[${i}]`, erreurs);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (reel === 'object') {
|
||||
const objet = valeur;
|
||||
for (const cle of schema.required ?? []) {
|
||||
if (!(cle in objet) || objet[cle] === undefined)
|
||||
erreurs.push(`${chemin}.${cle} : requis, absent`);
|
||||
}
|
||||
if (schema.properties) {
|
||||
for (const [cle, sous] of Object.entries(schema.properties)) {
|
||||
if (cle in objet && objet[cle] !== undefined)
|
||||
verifie(sous, objet[cle], `${chemin}.${cle}`, erreurs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Valide un JSON de données contre le schéma de sa table. Renvoie au plus
|
||||
* vingt erreurs, chacune avec son chemin.
|
||||
*/
|
||||
export function valider(table, json) {
|
||||
const schema = SCHEMAS[table];
|
||||
if (!schema)
|
||||
return { ok: false, erreurs: [`table inconnue : ${String(table)}`] };
|
||||
const erreurs = [];
|
||||
verifie(schema, json, table, erreurs);
|
||||
return { ok: erreurs.length === 0, erreurs };
|
||||
}
|
||||
/** Valide un objet contre un schéma arbitraire (même sous-ensemble). */
|
||||
export function validerContre(schema, json, nom = '$') {
|
||||
const erreurs = [];
|
||||
verifie(schema, json, nom, erreurs);
|
||||
return { ok: erreurs.length === 0, erreurs };
|
||||
}
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
export * from './commun/index.js';
|
||||
export * as autonomie from './autonomie/index.js';
|
||||
export { eau, energie, alimentation, integrateur } from './autonomie/index.js';
|
||||
/** Identifiants des moteurs livrés par cette version. */
|
||||
export declare const MOTEURS: readonly [string, string, string, string, string, string];
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
// @transformer/moteurs — point d'entrée. Sous-chemins : ./commun, ./autonomie,
|
||||
// ./autonomie/{eau,energie,alimentation,integrateur,noyau}.
|
||||
export * from './commun/index.js';
|
||||
export * as autonomie from './autonomie/index.js';
|
||||
export { eau, energie, alimentation, integrateur } from './autonomie/index.js';
|
||||
import { contexte, geometrie } from './commun/index.js';
|
||||
import { eau, energie, alimentation, integrateur } from './autonomie/index.js';
|
||||
/** Identifiants des moteurs livrés par cette version. */
|
||||
export const MOTEURS = [contexte.id, geometrie.id, eau.id, energie.id, alimentation.id, integrateur.id];
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
export declare const VERSION = "0.1.0";
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
// version.ts — étiquette du paquet, portée par chaque résultat de moteur.
|
||||
// Doit être égale à "version" de package.json : un test le vérifie
|
||||
// (test/paquet.test.mjs). On ne lit pas package.json ici : le paquet ne fait
|
||||
// aucune I/O, et un JSON importé serait embarqué par les bundlers.
|
||||
export const VERSION = '0.1.0';
|
||||
@@ -0,0 +1,47 @@
|
||||
# CONSTANTES — table des coefficients harmonisés (v0.1.0, 28/09/2026)
|
||||
|
||||
> **Pour Jules, 20 minutes, veto ligne à ligne.** Règle appliquée : *le document de recherche l'emporte sur le code quand ils divergent* (SPEC phase 1 §4.1). Chaque ligne dit ce que le code portait, ce que le paquet retient, d'où ça vient, et ce que ça change. **Sans veto sous une semaine (le 5 octobre), cette table est la référence.** Un veto = la ligne, la valeur voulue, une source si tu en as une ; je corrige `src/commun/coefficients.ts`, je refige les écarts, nouvelle étiquette.
|
||||
>
|
||||
> Sources : `01-recherche-eau.md`, `02-recherche-energie.md`, `99-brief-atis-dev.md` (tous du 28/04/2026, dossier `Simulateur autonomie V2`) ; code de `calculs.trans-former.fr` au 28/09 ; simulateur autoconstruction V1 pour la géométrie.
|
||||
|
||||
## 1. Ce qui change vraiment (cinq lignes)
|
||||
|
||||
| Grandeur | Le code portait | Retenu | Source | Effet mesuré sur les trois profils de référence |
|
||||
|---|---|---|---|---|
|
||||
| **Rendement de captage** | trois valeurs : 0,7 (score de pluviométrie), 0,8 (taux de couverture), 0,85 (V1) | **0,80** (fourchette 0,75 zone sèche à 0,85 zone humide) | recherche eau §3 et §7 n°6 : NF EN 16941-1, recueil Nény p.16 | aucune typologie ne change ; couverture eau **+3 à +4 points** (A 23 → 27 %, B 24 → 27 %, C 20 → 23 %) |
|
||||
| **Facteur d'orientation PV** | sud 1 · SE/SO 0,93 · E/O 0,78 · **nord 0,40** | sud 1 · SE/SO **0,95** · E/O **0,80** · nord **0,60** (bas de fourchette) | recherche énergie §3.2 (SE/SO 95 %, E/O 80-85 %, N 60-65 %). *Variante : le brief §7.2 prenait les milieux 0,82 et 0,62* | aucune typologie ne change (les trois profils sont sud ou sud-est) ; le nord n'est plus pénalisé deux fois plus que la source |
|
||||
| **Taux d'autoconsommation** | **absent** : la couverture énergie comparait la production brute au besoin | **0,30 sans batterie, 0,70 avec 10 kWh**, linéaire entre les deux (l'interpolation est un choix MOE, non sourcé) | recherche énergie §3.2, brief §7.2 | c'est **le** changement visible : couverture énergie **A 22 → 6 %**, **B 100 → 80 %** (mode par axe, avec batterie), **C 100 → 63 %** (global) et 10 → 3 % (par axe). Le simulateur promettait trop |
|
||||
| **Productible PV** (kWh/kWc/an) | table par zone **biogéographique**, 1 050 à **1 550** (zone 8, au-dessus du maximum de la source) | table par zone **RT** : H1a 1 000 · H1b 1 050 · H1c 1 200 · H2a 1 100 · H2b 1 200 · H2c 1 250 · **H2d 1 300** · H3 1 400 ; défaut 1 100 | recherche énergie §3.2 (PVGIS, sud 30°, pertes 14 %). H1a = moyenne Lille 950 / Paris 1 050. **H2d interpolé par la MOE** (04, 07, 26, 84 : pas de ville dans la source), déclaré en avertissement à chaque usage | inclus dans les écarts d'énergie ci-dessus (Paris 1 100 → 1 000, Bouches-du-Rhône 1 480 → 1 400) |
|
||||
| **Contexte Corse et DOM** | `algo.js` prenait deux chiffres du code postal : 2A, 2B, 971-976 retombaient sur **H2a, 800 mm, 2 200 DJU** pendant que l'affichage utilisait la vraie ligne | une seule fonction, `departementDepuisCodePostal` (20000-20190 → 2A, 20200-20620 → 2B, 97xxx → trois caractères) | code de `data.js` (l'interface avait déjà la bonne logique) | Corse : H3 (700-750 mm, 1 500-1 600 DJU). DOM : la table `france_climat` n'a **ni pluviométrie ni DJU** pour 971-976 (« DOM tropical ») → défauts 800 mm / 2 200 DJU appliqués **et déclarés** ; à compléter dans NocoDB si un jour une adresse DOM arrive |
|
||||
|
||||
## 2. Ce qui ne change pas, désormais sourcé et unique
|
||||
|
||||
| Grandeur | Valeur | Source | Note |
|
||||
|---|---|---|---|
|
||||
| Besoin d'eau domestique | 150 L/jour/personne (54 750 L/an) | recherche eau §3 (INSEE, Eau France 2024), brief §7.1 | le V1 comptait 46,5 m³/an |
|
||||
| Part substituable par l'eau de pluie | 70 % | recherche eau §3 : WC 30 + douche 60 + lessive 30 + jardin 25 L/j sur 150 | le V1 comptait 32 % |
|
||||
| Rendement par type de toiture | tuile 0,85 · ardoise 0,85 · gravillonnée 0,50 · végétalisée 0,30 | recherche eau §7 n°6 (ADEME, NF EN 16941-1) | **non utilisé** en v0.1 : à brancher quand le formulaire demande le type de toit |
|
||||
| Besoin d'électricité spécifique | 1 000 kWh/personne/an (fourchette 900-1 200), hors chauffage, ECS et véhicule | recherche énergie §3.1 (ADEME 2023, RTE 2024) | |
|
||||
| Haie fruitière | 25 ml/personne (50 ml pour un excédent transformable) | brief §7.3, correction Jules du 28/04 | |
|
||||
| DJU par défaut | H1 2 800 · H2 2 200 · H3 1 500 | heuristique V2, **non sourcée** | repli seulement : `france_climat` porte un DJU par département |
|
||||
| Contexte par défaut | H2a, zone 4, 800 mm, 2 200 DJU | heuristique V2, **non sourcée** | appliqué seulement si le département est inconnu, toujours déclaré en avertissement |
|
||||
|
||||
## 3. Géométrie (nouveau moteur `commun.geometrie`, reprend `calcSurfaces` du V1)
|
||||
|
||||
| Grandeur | Valeur | Source | Note |
|
||||
|---|---|---|---|
|
||||
| Périmètre estimé | 4 × √(emprise × 1,3) | V1 autoconstruction | rectangle allongé ; pour 73 m² : 38,97 m |
|
||||
| Surface vitrée | SHAB / 6 (16,7 %) | V1 (`vit = S/6`) ; règle RT 2012 (baies ≥ 1/6 de la SHAB) | la SPEC écrit « 15 % » : j'ai gardé 1/6, dis-moi si tu veux 15 % |
|
||||
| Majoration de toiture | emprise / cos(pente) × 1,05 | V1 | |
|
||||
| **SHAB estimée** | emprise × niveaux × **0,80** | **heuristique MOE, non sourcée** (murs extérieurs ~0,3 m, refends, escalier) | **hypothèse porteuse du P1.2** : maison B de l'étude (73 m² × 2 niveaux) → 116,8 m² ; la surface DPE n'est pas dans l'étude, l'écart se mesure au premier appel de l'adaptateur DPE (P1.1), visé < 20 % |
|
||||
| Hauteur d'étage par défaut | 2,5 m | heuristique MOE | si la hauteur du bâti est inconnue |
|
||||
| Mitoyenneté | −25 % du périmètre par côté mitoyen | heuristique MOE | |
|
||||
| Pente par défaut | 30° | heuristique MOE | si inconnue |
|
||||
|
||||
## 4. Heuristiques restées dans le code (non déplacées, pour que tu saches qu'elles existent)
|
||||
|
||||
Ce ne sont pas des grandeurs physiques mais des choix de conception du simulateur V2 (spec d'algo « heuristique déterministe, calibrable sur trois profils »). Elles vivent dans `algo-config.json` (poids d'axes 40/40/20, modulateurs, matrices d'adéquation, tolérances ±5 % / −15 %, bonus DIY 1,3, malus 0,5 et 0,6, poids des postes) ou dans le noyau : pose pro = 50 % du matériel quand `prix_pro` manque (`cout_typologie`) ; cible de cuve = 2 mois de récupération (`score_pluviometrie`) ; puissance cible = DJU / 500 (`score_climat_chauffage`) ; 100 m² retirés de la parcelle pour le bâti (`score_surface_parcelle`, `par_surface_parcelle`). Les changer relève du recalibrage, pas de l'harmonisation.
|
||||
|
||||
## 5. Pour mettre un veto
|
||||
|
||||
Réponds par le numéro de ligne et la valeur : « §1 orientation : nord 0,62 (milieu du brief) » suffit. Je corrige la table, je relance `npm run ecarts`, je refige les écarts avec la date, nouvelle étiquette `v0.1.1`, montée dans `reinnover` le même jour.
|
||||
@@ -0,0 +1,14 @@
|
||||
# fixtures/ — jeux de données figés et datés
|
||||
|
||||
Les tests tournent hors ligne sur ces copies. Aucun fichier d'ici n'est importé par `src/` : les moteurs reçoivent leurs données en paramètre. Un consommateur peut lire `algo-config.json` et `profils-reference.json` par `@transformer/moteurs/fixtures/<fichier>` (exposés par `exports`).
|
||||
|
||||
| Fichier | Contenu | Origine | Date de l'instantané |
|
||||
|---|---|---|---|
|
||||
| `postes.json` (14 lignes) · `options.json` (85) · `france_climat.json` (101, dont 2A, 2B, 971-976) | tables du simulateur autonomie, **prix compris** (ce sont des données de test, jamais des constantes du paquet) | `astro-pro/tests/fixtures/` (copie de la publication `calculs.trans-former.fr/calculs/data/`) | 2026-08-16 |
|
||||
| `algo-config.json` | heuristiques de conception du noyau (poids, modulateurs, matrices, tolérances, alias, poids des postes) ; les clés `orientation_factors` et `dju_defaults_by_zone` y figurent encore mais **ne sont plus lues** (les coefficients vivent dans `src/commun/coefficients.ts`) | `astro-pro/src/lib/autonomie/algo-config.json` | 2026-09-28 (inchangé depuis mai 2026) |
|
||||
| `profils-reference.json` | les trois profils de calibrage A / B / C | `astro-pro/tests/profils-reference.json` | 2026-09-28 |
|
||||
| `coefficients-2026-05.json` | jeu de coefficients **d'origine** (valeurs du code de calculs), injecté par le test de non-régression | relevé sur `astro-pro` master `c628cf9` | 2026-09-28 |
|
||||
| `sorties-origine-2026-09-28.json` | sorties de `selectionner()` **d'origine** sur les trois profils (pipeline complet + mode un seul axe), générées par `scripts/pin-origine.mjs` depuis le code d'astro-pro | `astro-pro` master `c628cf9` | 2026-09-28 |
|
||||
| `ecarts-harmonisation-2026-09-28.json` | écarts de sélection et de taux entre coefficients d'origine et table harmonisée (`npm run ecarts -- --ecrire <date>`) ; relus par Jules avec `docs/CONSTANTES.md` | ce paquet | 2026-09-28 |
|
||||
|
||||
Refiger une épingle est un acte volontaire : on relit `docs/CONSTANTES.md`, on relance le script avec la date du jour, on met à jour les tests qui la citent, et on l'écrit dans le message de commit.
|
||||
@@ -0,0 +1,186 @@
|
||||
{
|
||||
"poids_axes_base": {
|
||||
"eau": 40,
|
||||
"energie": 40,
|
||||
"alim": 20
|
||||
},
|
||||
"modulateurs": {
|
||||
"toit_petit_seuil_m2": 30,
|
||||
"toit_petit_delta": {
|
||||
"energie": -15,
|
||||
"eau": 5,
|
||||
"alim": 10
|
||||
},
|
||||
"famille_grande_seuil": 5,
|
||||
"famille_grande_delta": {
|
||||
"eau": 10,
|
||||
"alim": 5,
|
||||
"energie": -15
|
||||
},
|
||||
"biogeo_med_delta": {
|
||||
"eau": 10
|
||||
},
|
||||
"pluvio_basse_seuil_mm": 600,
|
||||
"pluvio_basse_delta": {
|
||||
"eau": 5
|
||||
},
|
||||
"parcelle_petite_seuil_m2": 300,
|
||||
"parcelle_petite_alim_zero": true,
|
||||
"parcelle_grande_seuil_m2": 1000,
|
||||
"parcelle_grande_temps3_delta": {
|
||||
"alim": 10
|
||||
}
|
||||
},
|
||||
"adequation_weights": {
|
||||
"_comment": "Cf. V2-algo-spec-FONCTIONS.md §1.3 + codes postes BDD post-M1a",
|
||||
"eau_captage": {
|
||||
"pluviometrie": 30,
|
||||
"surface_toit": 30,
|
||||
"taille_foyer": 10,
|
||||
"temps_dispo": 5
|
||||
},
|
||||
"eau_stockage": {
|
||||
"pluviometrie": 25,
|
||||
"taille_foyer": 25,
|
||||
"surface_parcelle": 5,
|
||||
"temps_dispo": 5
|
||||
},
|
||||
"eau_filtration": {
|
||||
"taille_foyer": 15,
|
||||
"temps_dispo": 10
|
||||
},
|
||||
"eau_distribution": {
|
||||
"taille_foyer": 20,
|
||||
"temps_dispo": 5
|
||||
},
|
||||
"eau_eg_epuration": {
|
||||
"taille_foyer": 20,
|
||||
"surface_parcelle": 30,
|
||||
"temps_dispo": 5
|
||||
},
|
||||
"energie_pv": {
|
||||
"climat_chauffage": 5,
|
||||
"taille_foyer": 10,
|
||||
"surface_toit": 35,
|
||||
"orientation_toit": 25,
|
||||
"temps_dispo": 5
|
||||
},
|
||||
"energie_solaire_th": {
|
||||
"climat_chauffage": 5,
|
||||
"taille_foyer": 30,
|
||||
"temps_dispo": 5
|
||||
},
|
||||
"energie_chauffage_bois": {
|
||||
"climat_chauffage": 35,
|
||||
"taille_foyer": 15,
|
||||
"temps_dispo": 10
|
||||
},
|
||||
"energie_emetteurs": {
|
||||
"taille_foyer": 20,
|
||||
"temps_dispo": 5
|
||||
},
|
||||
"energie_pilotage": {
|
||||
"taille_foyer": 10,
|
||||
"temps_dispo": 5
|
||||
},
|
||||
"alim_etude_fonciere": {
|
||||
"surface_parcelle": 20,
|
||||
"temps_dispo": 5
|
||||
},
|
||||
"alim_strate_haute": {
|
||||
"climat_meridional": 5,
|
||||
"surface_parcelle": 25,
|
||||
"taille_foyer": 5,
|
||||
"temps_dispo": 10
|
||||
},
|
||||
"alim_strate_moyenne": {
|
||||
"surface_parcelle": 15,
|
||||
"metres_haie_possibles": 30,
|
||||
"temps_dispo": 10
|
||||
},
|
||||
"alim_strate_basse": {
|
||||
"climat_meridional": 10,
|
||||
"surface_parcelle": 30,
|
||||
"taille_foyer": 10,
|
||||
"temps_dispo": 15
|
||||
}
|
||||
},
|
||||
"orientation_factors": {
|
||||
"sud": 1.0,
|
||||
"sud-est": 0.93,
|
||||
"sud-ouest": 0.93,
|
||||
"est": 0.78,
|
||||
"ouest": 0.78,
|
||||
"nord": 0.4
|
||||
},
|
||||
"dju_defaults_by_zone": {
|
||||
"H1": 2800,
|
||||
"H2": 2200,
|
||||
"H3": 1500
|
||||
},
|
||||
"tolerances": {
|
||||
"budget_overrun_pct": 5,
|
||||
"budget_underspend_pct": 5,
|
||||
"tolerance_par_poste_pct": 20
|
||||
},
|
||||
"bonus_malus": {
|
||||
"boost_diy_temps3": 1.3,
|
||||
"malus_pose3_temps1": 0.5,
|
||||
"malus_non_accessible_fr": 0.6
|
||||
},
|
||||
"couplages_alias": {
|
||||
"stherm_ballon_tampon": "th_ballon_500",
|
||||
"emetteur_pcbt": "emet_pcbt",
|
||||
"batterie_min_10kwh": "pv_batterie_lifepo4_5",
|
||||
"cuve_decantation": "eg_separation_source",
|
||||
"stockage_cuve_pehd_horsol": "stockage_pehd_horssol_5m3",
|
||||
"distrib_kit_double_reseau": "distrib_kit_double_reseau",
|
||||
"pv_batterie_lifepo4": "pv_batterie_lifepo4_5",
|
||||
"pv_kit_string": "pv_kit_3kwc_string",
|
||||
"pv_onduleur_hybride": "pv_onduleur_hybride_5kw"
|
||||
},
|
||||
"postes_poids": {
|
||||
"eau_captage": {
|
||||
"poids": 15
|
||||
},
|
||||
"eau_filtration": {
|
||||
"poids": 15
|
||||
},
|
||||
"eau_stockage": {
|
||||
"poids": 40
|
||||
},
|
||||
"eau_distribution": {
|
||||
"poids": 20
|
||||
},
|
||||
"eau_eg_epuration": {
|
||||
"poids": 10
|
||||
},
|
||||
"energie_pv": {
|
||||
"poids": 35
|
||||
},
|
||||
"energie_solaire_th": {
|
||||
"poids": 15
|
||||
},
|
||||
"energie_chauffage_bois": {
|
||||
"poids": 30
|
||||
},
|
||||
"energie_emetteurs": {
|
||||
"poids": 10
|
||||
},
|
||||
"energie_pilotage": {
|
||||
"poids": 10
|
||||
},
|
||||
"alim_etude_fonciere": {
|
||||
"poids": 10
|
||||
},
|
||||
"alim_strate_haute": {
|
||||
"poids": 30
|
||||
},
|
||||
"alim_strate_moyenne": {
|
||||
"poids": 30
|
||||
},
|
||||
"alim_strate_basse": {
|
||||
"poids": 30
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"_commentaire": "Jeu de coefficients D'ORIGINE : valeurs du code de calculs.trans-former.fr (astro-pro master c628cf9, relevé le 2026-09-28 ; code écrit en mai 2026). Sert UNIQUEMENT au test de non-régression (le noyau extrait rend la même sélection que l'original). La référence est src/commun/coefficients.ts (table harmonisée) — docs/CONSTANTES.md donne, ligne à ligne, ancienne valeur → retenue.",
|
||||
"captage_rendement": { "valeur": 0.7, "source": "scoreurs.js score_pluviometrie (0,7) ; taux.js portait 0,8 ; V1 bioclimatique 0,85", "date": "2026-05-01" },
|
||||
"captage_rendement_par_toiture": { "valeur": { "tuile": 0.85, "ardoise": 0.85, "gravillonnee": 0.5, "vegetalisee": 0.3 }, "source": "absent du code (01-recherche-eau §7 n°6)", "date": "2026-04-28" },
|
||||
"besoin_eau_l_j_pers": { "valeur": 150, "source": "taux.js BESOIN_EAU_L_PERS_AN = 54 750", "date": "2026-05-01" },
|
||||
"part_eau_substituable": { "valeur": 0.7, "source": "taux.js PART_SUBSTITUABLE", "date": "2026-05-01" },
|
||||
"orientation_pv": { "valeur": { "sud": 1.0, "sud-est": 0.93, "sud-ouest": 0.93, "est": 0.78, "ouest": 0.78, "nord": 0.4 }, "source": "calibrage.js ORIENTATION_FACTORS (dupliqué dans algo-config.json orientation_factors, non lu)", "date": "2026-05-01" },
|
||||
"productible_pv_kwh_kwc": { "valeur": { "par": "zone_biogeo_code", "table": { "1": 1050, "2": 1180, "3": 1280, "4": 1100, "5": 1480, "6": 1230, "7": 1280, "8": 1550 }, "defaut": 1200 }, "source": "constants.js FALLBACK_PVGIS (table par zone biogéographique) ; pvgis.js défaut 1 200", "date": "2026-05-01" },
|
||||
"autoconso_sans_batterie": { "valeur": 1.0, "source": "absent du code : taux.js comparait la production brute au besoin (équivaut à un taux de 1)", "date": "2026-05-01" },
|
||||
"autoconso_avec_batterie_10kwh": { "valeur": 1.0, "source": "absent du code (idem)", "date": "2026-05-01" },
|
||||
"besoin_elec_kwh_pers_an": { "valeur": 1000, "source": "taux.js BESOIN_ELEC_KWH_PERS_AN", "date": "2026-05-01" },
|
||||
"dju_defaut_par_classe_rt": { "valeur": { "H1": 2800, "H2": 2200, "H3": 1500 }, "source": "calibrage.js DJU_DEFAULTS_BY_ZONE", "date": "2026-05-01" },
|
||||
"ml_haie_par_pers": { "valeur": 25, "source": "taux.js ML_HAIE_PAR_PERS", "date": "2026-05-01" },
|
||||
"contexte_defaut": { "valeur": { "zone_rt": "H2a", "zone_biogeo_code": 4, "pluviometrie_mm": 800, "dju": 2200 }, "source": "algo.js deriveContexte (défauts)", "date": "2026-05-01" },
|
||||
"geometrie_ratio_shab_emprise": { "valeur": 1.0, "source": "V1 autoconstruction : shab = S (plain-pied, aucune correction)", "date": "2025-01-01" },
|
||||
"geometrie_ratio_vitrage_shab": { "valeur": 0.16666666666666666, "source": "V1 autoconstruction : vit = S / 6", "date": "2025-01-01" },
|
||||
"geometrie_hauteur_etage_m": { "valeur": 2.5, "source": "absent du code (Z saisi par l'utilisateur)", "date": "2025-01-01" },
|
||||
"geometrie_facteur_perimetre": { "valeur": 1.3, "source": "V1 autoconstruction : perimetre = √(S × 1,3) × 4", "date": "2025-01-01" },
|
||||
"geometrie_part_mur_mitoyen": { "valeur": 0.25, "source": "absent du code", "date": "2025-01-01" },
|
||||
"geometrie_majoration_toiture": { "valeur": 1.05, "source": "V1 autoconstruction : toiture = S / cos α × 1,05", "date": "2025-01-01" },
|
||||
"geometrie_pente_defaut_deg": { "valeur": 30, "source": "absent du code (A saisi par l'utilisateur)", "date": "2025-01-01" }
|
||||
}
|
||||
@@ -0,0 +1,535 @@
|
||||
{
|
||||
"_commentaire": "Écarts de sélection et de taux entre le jeu de coefficients d'origine et la table harmonisée, sur les trois profils de référence, figés le 2026-09-28. Un changement de coefficient ou de code qui modifie ces écarts casse test/decoupage.test.mjs : c'est voulu, on relit CONSTANTES.md avant de refiger.",
|
||||
"date": "2026-09-28",
|
||||
"ecarts": [
|
||||
{
|
||||
"profil": "A — Frugal urbain",
|
||||
"mode": "taux (par_axe)",
|
||||
"axe": "eau",
|
||||
"origine": "23 %",
|
||||
"harmonisee": "27 %"
|
||||
},
|
||||
{
|
||||
"profil": "A — Frugal urbain",
|
||||
"mode": "taux (global)",
|
||||
"axe": "eau",
|
||||
"origine": "23 %",
|
||||
"harmonisee": "27 %"
|
||||
},
|
||||
{
|
||||
"profil": "A — Frugal urbain",
|
||||
"mode": "taux (par_axe)",
|
||||
"axe": "energie",
|
||||
"origine": "22 %",
|
||||
"harmonisee": "6 %"
|
||||
},
|
||||
{
|
||||
"profil": "A — Frugal urbain",
|
||||
"mode": "taux (global)",
|
||||
"axe": "energie",
|
||||
"origine": "22 %",
|
||||
"harmonisee": "6 %"
|
||||
},
|
||||
{
|
||||
"profil": "B — Famille moyenne",
|
||||
"mode": "taux (par_axe)",
|
||||
"axe": "eau",
|
||||
"origine": "24 %",
|
||||
"harmonisee": "27 %"
|
||||
},
|
||||
{
|
||||
"profil": "B — Famille moyenne",
|
||||
"mode": "taux (global)",
|
||||
"axe": "eau",
|
||||
"origine": "24 %",
|
||||
"harmonisee": "27 %"
|
||||
},
|
||||
{
|
||||
"profil": "B — Famille moyenne",
|
||||
"mode": "taux (par_axe)",
|
||||
"axe": "energie",
|
||||
"origine": "100 %",
|
||||
"harmonisee": "80 %"
|
||||
},
|
||||
{
|
||||
"profil": "C — Autonomie large",
|
||||
"mode": "taux (par_axe)",
|
||||
"axe": "eau",
|
||||
"origine": "20 %",
|
||||
"harmonisee": "23 %"
|
||||
},
|
||||
{
|
||||
"profil": "C — Autonomie large",
|
||||
"mode": "taux (global)",
|
||||
"axe": "eau",
|
||||
"origine": "20 %",
|
||||
"harmonisee": "23 %"
|
||||
},
|
||||
{
|
||||
"profil": "C — Autonomie large",
|
||||
"mode": "taux (par_axe)",
|
||||
"axe": "energie",
|
||||
"origine": "10 %",
|
||||
"harmonisee": "3 %"
|
||||
},
|
||||
{
|
||||
"profil": "C — Autonomie large",
|
||||
"mode": "taux (global)",
|
||||
"axe": "energie",
|
||||
"origine": "100 %",
|
||||
"harmonisee": "63 %"
|
||||
}
|
||||
],
|
||||
"lignes": [
|
||||
{
|
||||
"profil": "A — Frugal urbain",
|
||||
"jeu": "origine",
|
||||
"global": {
|
||||
"total": 15180,
|
||||
"status": "OK",
|
||||
"typologies": {
|
||||
"eau": [
|
||||
"captage_descente_directe",
|
||||
"filtration_particulaire_simple",
|
||||
"stockage_souple_5m3",
|
||||
"distrib_gravitaire",
|
||||
"eg_noue",
|
||||
"eg_pedoepuration",
|
||||
"eg_fpv_autoconstruit"
|
||||
],
|
||||
"energie": [
|
||||
"pv_batterie_plomb_5",
|
||||
"th_ballon_500",
|
||||
"bois_poele_double",
|
||||
"emet_vannes_thermo",
|
||||
"pil_home_assistant",
|
||||
"bois_conduit_fumee"
|
||||
],
|
||||
"alim": []
|
||||
},
|
||||
"taux": {
|
||||
"eau": 23,
|
||||
"energie": 22,
|
||||
"alim": null
|
||||
}
|
||||
},
|
||||
"par_axe": {
|
||||
"eau": {
|
||||
"typologies": [
|
||||
"captage_descente_directe",
|
||||
"filtration_particulaire_simple",
|
||||
"stockage_souple_5m3",
|
||||
"distrib_gravitaire",
|
||||
"eg_pedoepuration",
|
||||
"distrib_kit_double_reseau",
|
||||
"eg_fpv_autoconstruit"
|
||||
],
|
||||
"sous_total": 8130,
|
||||
"status": "BUDGET_INSUFFISANT",
|
||||
"taux": 23
|
||||
},
|
||||
"energie": {
|
||||
"typologies": [
|
||||
"pv_kit_beem_balcon",
|
||||
"th_ballon_500",
|
||||
"bois_poele_granules",
|
||||
"emet_radiateur_acier",
|
||||
"pil_routeur_ecs",
|
||||
"bois_conduit_fumee"
|
||||
],
|
||||
"sous_total": 7110,
|
||||
"status": "OK",
|
||||
"taux": 22
|
||||
},
|
||||
"alim": {
|
||||
"typologies": [],
|
||||
"sous_total": 0,
|
||||
"status": "OK",
|
||||
"taux": null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"profil": "A — Frugal urbain",
|
||||
"jeu": "harmonisee",
|
||||
"global": {
|
||||
"total": 15180,
|
||||
"status": "OK",
|
||||
"typologies": {
|
||||
"eau": [
|
||||
"captage_descente_directe",
|
||||
"filtration_particulaire_simple",
|
||||
"stockage_souple_5m3",
|
||||
"distrib_gravitaire",
|
||||
"eg_noue",
|
||||
"eg_pedoepuration",
|
||||
"eg_fpv_autoconstruit"
|
||||
],
|
||||
"energie": [
|
||||
"pv_batterie_plomb_5",
|
||||
"th_ballon_500",
|
||||
"bois_poele_double",
|
||||
"emet_vannes_thermo",
|
||||
"pil_home_assistant",
|
||||
"bois_conduit_fumee"
|
||||
],
|
||||
"alim": []
|
||||
},
|
||||
"taux": {
|
||||
"eau": 27,
|
||||
"energie": 6,
|
||||
"alim": null
|
||||
}
|
||||
},
|
||||
"par_axe": {
|
||||
"eau": {
|
||||
"typologies": [
|
||||
"captage_descente_directe",
|
||||
"filtration_particulaire_simple",
|
||||
"stockage_souple_5m3",
|
||||
"distrib_gravitaire",
|
||||
"eg_pedoepuration",
|
||||
"distrib_kit_double_reseau",
|
||||
"eg_fpv_autoconstruit"
|
||||
],
|
||||
"sous_total": 8130,
|
||||
"status": "BUDGET_INSUFFISANT",
|
||||
"taux": 27
|
||||
},
|
||||
"energie": {
|
||||
"typologies": [
|
||||
"pv_kit_beem_balcon",
|
||||
"th_ballon_500",
|
||||
"bois_poele_granules",
|
||||
"emet_radiateur_acier",
|
||||
"pil_routeur_ecs",
|
||||
"bois_conduit_fumee"
|
||||
],
|
||||
"sous_total": 7110,
|
||||
"status": "OK",
|
||||
"taux": 6
|
||||
},
|
||||
"alim": {
|
||||
"typologies": [],
|
||||
"sous_total": 0,
|
||||
"status": "OK",
|
||||
"taux": null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"profil": "B — Famille moyenne",
|
||||
"jeu": "origine",
|
||||
"global": {
|
||||
"total": 36661,
|
||||
"status": "OK",
|
||||
"typologies": {
|
||||
"eau": [
|
||||
"captage_descente_directe",
|
||||
"filtration_particulaire_simple",
|
||||
"stockage_souple_5m3",
|
||||
"distrib_gravitaire",
|
||||
"eg_noue",
|
||||
"eg_pedoepuration",
|
||||
"eg_fpv_autoconstruit"
|
||||
],
|
||||
"energie": [
|
||||
"pv_monitoring",
|
||||
"th_ballon_500",
|
||||
"bois_poele_masse_indus",
|
||||
"emet_seche_serviette",
|
||||
"pil_delesteur",
|
||||
"bois_conduit_fumee"
|
||||
],
|
||||
"alim": [
|
||||
"etude_topo_ensoleil",
|
||||
"haute_fruit_coque",
|
||||
"moy_petit_fruit_3L",
|
||||
"basse_compost_3bacs"
|
||||
]
|
||||
},
|
||||
"taux": {
|
||||
"eau": 24,
|
||||
"energie": null,
|
||||
"alim": null
|
||||
}
|
||||
},
|
||||
"par_axe": {
|
||||
"eau": {
|
||||
"typologies": [
|
||||
"captage_descente_directe",
|
||||
"filtration_particulaire_simple",
|
||||
"stockage_pehd_enterree_5m3",
|
||||
"distrib_gravitaire",
|
||||
"eg_pedoepuration",
|
||||
"eg_pedoepuration"
|
||||
],
|
||||
"sous_total": 14645,
|
||||
"status": "OK",
|
||||
"taux": 24
|
||||
},
|
||||
"energie": {
|
||||
"typologies": [
|
||||
"pv_kit_3kwc_string",
|
||||
"th_ballon_500",
|
||||
"bois_poele_granules",
|
||||
"emet_seche_serviette",
|
||||
"pil_delesteur",
|
||||
"bois_conduit_fumee"
|
||||
],
|
||||
"sous_total": 13455,
|
||||
"status": "OK",
|
||||
"taux": 100
|
||||
},
|
||||
"alim": {
|
||||
"typologies": [
|
||||
"etude_analyse_labo",
|
||||
"haute_plantation_pose_pro",
|
||||
"moy_haie_biodiv_complete",
|
||||
"basse_planche_bec",
|
||||
"basse_planche_bec"
|
||||
],
|
||||
"sous_total": 6985,
|
||||
"status": "OK",
|
||||
"taux": 50
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"profil": "B — Famille moyenne",
|
||||
"jeu": "harmonisee",
|
||||
"global": {
|
||||
"total": 36661,
|
||||
"status": "OK",
|
||||
"typologies": {
|
||||
"eau": [
|
||||
"captage_descente_directe",
|
||||
"filtration_particulaire_simple",
|
||||
"stockage_souple_5m3",
|
||||
"distrib_gravitaire",
|
||||
"eg_noue",
|
||||
"eg_pedoepuration",
|
||||
"eg_fpv_autoconstruit"
|
||||
],
|
||||
"energie": [
|
||||
"pv_monitoring",
|
||||
"th_ballon_500",
|
||||
"bois_poele_masse_indus",
|
||||
"emet_seche_serviette",
|
||||
"pil_delesteur",
|
||||
"bois_conduit_fumee"
|
||||
],
|
||||
"alim": [
|
||||
"etude_topo_ensoleil",
|
||||
"haute_fruit_coque",
|
||||
"moy_petit_fruit_3L",
|
||||
"basse_compost_3bacs"
|
||||
]
|
||||
},
|
||||
"taux": {
|
||||
"eau": 27,
|
||||
"energie": null,
|
||||
"alim": null
|
||||
}
|
||||
},
|
||||
"par_axe": {
|
||||
"eau": {
|
||||
"typologies": [
|
||||
"captage_descente_directe",
|
||||
"filtration_particulaire_simple",
|
||||
"stockage_pehd_enterree_5m3",
|
||||
"distrib_gravitaire",
|
||||
"eg_pedoepuration",
|
||||
"eg_pedoepuration"
|
||||
],
|
||||
"sous_total": 14645,
|
||||
"status": "OK",
|
||||
"taux": 27
|
||||
},
|
||||
"energie": {
|
||||
"typologies": [
|
||||
"pv_kit_3kwc_string",
|
||||
"th_ballon_500",
|
||||
"bois_poele_granules",
|
||||
"emet_seche_serviette",
|
||||
"pil_delesteur",
|
||||
"bois_conduit_fumee"
|
||||
],
|
||||
"sous_total": 13455,
|
||||
"status": "OK",
|
||||
"taux": 80
|
||||
},
|
||||
"alim": {
|
||||
"typologies": [
|
||||
"etude_analyse_labo",
|
||||
"haute_plantation_pose_pro",
|
||||
"moy_haie_biodiv_complete",
|
||||
"basse_planche_bec",
|
||||
"basse_planche_bec"
|
||||
],
|
||||
"sous_total": 6985,
|
||||
"status": "OK",
|
||||
"taux": 50
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"profil": "C — Autonomie large",
|
||||
"jeu": "origine",
|
||||
"global": {
|
||||
"total": 77465,
|
||||
"status": "OK",
|
||||
"typologies": {
|
||||
"eau": [
|
||||
"captage_capteur_sol",
|
||||
"filtration_3_corps_uv",
|
||||
"stockage_beton_5m3",
|
||||
"distrib_gravitaire",
|
||||
"eg_phytostation"
|
||||
],
|
||||
"energie": [
|
||||
"pv_kit_3kwc_micro",
|
||||
"th_ballon_500",
|
||||
"bois_chaudiere_granules",
|
||||
"emet_vannes_thermo",
|
||||
"pil_onduleur_hybride",
|
||||
"bois_conduit_fumee"
|
||||
],
|
||||
"alim": [
|
||||
"etude_analyse_labo",
|
||||
"haute_haute_tige",
|
||||
"moy_haie_biodiv_complete",
|
||||
"basse_planche_bec"
|
||||
]
|
||||
},
|
||||
"taux": {
|
||||
"eau": 20,
|
||||
"energie": 100,
|
||||
"alim": 67
|
||||
}
|
||||
},
|
||||
"par_axe": {
|
||||
"eau": {
|
||||
"typologies": [
|
||||
"captage_capteur_sol",
|
||||
"filtration_3_corps_uv",
|
||||
"stockage_beton_5m3",
|
||||
"distrib_per_bleu_interieur",
|
||||
"eg_phytostation",
|
||||
"eg_phytostation",
|
||||
"eg_phytostation",
|
||||
"eg_pedoepuration",
|
||||
"distrib_kit_double_reseau"
|
||||
],
|
||||
"sous_total": 41000,
|
||||
"status": "OK",
|
||||
"taux": 20
|
||||
},
|
||||
"energie": {
|
||||
"typologies": [
|
||||
"pv_batterie_plomb_5",
|
||||
"th_ballon_500",
|
||||
"bois_poele_hydro",
|
||||
"emet_radiateur_acier",
|
||||
"pil_routeur_ecs",
|
||||
"bois_conduit_fumee"
|
||||
],
|
||||
"sous_total": 19210,
|
||||
"status": "OK",
|
||||
"taux": 10
|
||||
},
|
||||
"alim": {
|
||||
"typologies": [
|
||||
"etude_analyse_labo",
|
||||
"haute_haute_tige",
|
||||
"moy_haie_biodiv_complete",
|
||||
"basse_planche_bec",
|
||||
"basse_planche_bec"
|
||||
],
|
||||
"sous_total": 20855,
|
||||
"status": "OK",
|
||||
"taux": 67
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"profil": "C — Autonomie large",
|
||||
"jeu": "harmonisee",
|
||||
"global": {
|
||||
"total": 77465,
|
||||
"status": "OK",
|
||||
"typologies": {
|
||||
"eau": [
|
||||
"captage_capteur_sol",
|
||||
"filtration_3_corps_uv",
|
||||
"stockage_beton_5m3",
|
||||
"distrib_gravitaire",
|
||||
"eg_phytostation"
|
||||
],
|
||||
"energie": [
|
||||
"pv_kit_3kwc_micro",
|
||||
"th_ballon_500",
|
||||
"bois_chaudiere_granules",
|
||||
"emet_vannes_thermo",
|
||||
"pil_onduleur_hybride",
|
||||
"bois_conduit_fumee"
|
||||
],
|
||||
"alim": [
|
||||
"etude_analyse_labo",
|
||||
"haute_haute_tige",
|
||||
"moy_haie_biodiv_complete",
|
||||
"basse_planche_bec"
|
||||
]
|
||||
},
|
||||
"taux": {
|
||||
"eau": 23,
|
||||
"energie": 63,
|
||||
"alim": 67
|
||||
}
|
||||
},
|
||||
"par_axe": {
|
||||
"eau": {
|
||||
"typologies": [
|
||||
"captage_capteur_sol",
|
||||
"filtration_3_corps_uv",
|
||||
"stockage_beton_5m3",
|
||||
"distrib_per_bleu_interieur",
|
||||
"eg_phytostation",
|
||||
"eg_phytostation",
|
||||
"eg_phytostation",
|
||||
"eg_pedoepuration",
|
||||
"distrib_kit_double_reseau"
|
||||
],
|
||||
"sous_total": 41000,
|
||||
"status": "OK",
|
||||
"taux": 23
|
||||
},
|
||||
"energie": {
|
||||
"typologies": [
|
||||
"pv_batterie_plomb_5",
|
||||
"th_ballon_500",
|
||||
"bois_poele_hydro",
|
||||
"emet_radiateur_acier",
|
||||
"pil_routeur_ecs",
|
||||
"bois_conduit_fumee"
|
||||
],
|
||||
"sous_total": 19210,
|
||||
"status": "OK",
|
||||
"taux": 3
|
||||
},
|
||||
"alim": {
|
||||
"typologies": [
|
||||
"etude_analyse_labo",
|
||||
"haute_haute_tige",
|
||||
"moy_haie_biodiv_complete",
|
||||
"basse_planche_bec",
|
||||
"basse_planche_bec"
|
||||
],
|
||||
"sous_total": 20855,
|
||||
"status": "OK",
|
||||
"taux": 67
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
[
|
||||
{"dept":"01","nom_dept":"Ain","zone_rt":"H1c","pluvio_mm_an":1050,"dju_base_18":2700,"zone_biogeo":4,"verifie":false,"source_climat":"Météo-France normales 1991-2020 + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"02","nom_dept":"Aisne","zone_rt":"H1a","pluvio_mm_an":720,"dju_base_18":2600,"zone_biogeo":4,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"03","nom_dept":"Allier","zone_rt":"H1c","pluvio_mm_an":780,"dju_base_18":2550,"zone_biogeo":4,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"04","nom_dept":"Alpes-de-Haute-Provence","zone_rt":"H2d","pluvio_mm_an":750,"dju_base_18":2400,"zone_biogeo":6,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"05","nom_dept":"Hautes-Alpes","zone_rt":"H1c","pluvio_mm_an":830,"dju_base_18":3100,"zone_biogeo":6,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"06","nom_dept":"Alpes-Maritimes","zone_rt":"H3","pluvio_mm_an":820,"dju_base_18":1500,"zone_biogeo":5,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"07","nom_dept":"Ardèche","zone_rt":"H2d","pluvio_mm_an":1100,"dju_base_18":2200,"zone_biogeo":5,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"08","nom_dept":"Ardennes","zone_rt":"H1b","pluvio_mm_an":950,"dju_base_18":2900,"zone_biogeo":4,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"09","nom_dept":"Ariège","zone_rt":"H2c","pluvio_mm_an":1000,"dju_base_18":2500,"zone_biogeo":7,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"10","nom_dept":"Aube","zone_rt":"H1b","pluvio_mm_an":700,"dju_base_18":2700,"zone_biogeo":4,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"11","nom_dept":"Aude","zone_rt":"H3","pluvio_mm_an":650,"dju_base_18":1800,"zone_biogeo":5,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"12","nom_dept":"Aveyron","zone_rt":"H2c","pluvio_mm_an":900,"dju_base_18":2700,"zone_biogeo":7,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"13","nom_dept":"Bouches-du-Rhône","zone_rt":"H3","pluvio_mm_an":540,"dju_base_18":1750,"zone_biogeo":5,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"14","nom_dept":"Calvados","zone_rt":"H2a","pluvio_mm_an":850,"dju_base_18":2500,"zone_biogeo":1,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"15","nom_dept":"Cantal","zone_rt":"H1c","pluvio_mm_an":1100,"dju_base_18":3000,"zone_biogeo":7,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"16","nom_dept":"Charente","zone_rt":"H2b","pluvio_mm_an":850,"dju_base_18":2200,"zone_biogeo":2,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"17","nom_dept":"Charente-Maritime","zone_rt":"H2b","pluvio_mm_an":820,"dju_base_18":2100,"zone_biogeo":2,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"18","nom_dept":"Cher","zone_rt":"H1c","pluvio_mm_an":750,"dju_base_18":2500,"zone_biogeo":4,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"19","nom_dept":"Corrèze","zone_rt":"H1c","pluvio_mm_an":1100,"dju_base_18":2600,"zone_biogeo":3,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"21","nom_dept":"Côte-d'Or","zone_rt":"H1c","pluvio_mm_an":750,"dju_base_18":2700,"zone_biogeo":4,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"22","nom_dept":"Côtes-d'Armor","zone_rt":"H2a","pluvio_mm_an":900,"dju_base_18":2400,"zone_biogeo":1,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"23","nom_dept":"Creuse","zone_rt":"H1c","pluvio_mm_an":1000,"dju_base_18":2700,"zone_biogeo":3,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"24","nom_dept":"Dordogne","zone_rt":"H2c","pluvio_mm_an":900,"dju_base_18":2200,"zone_biogeo":2,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"25","nom_dept":"Doubs","zone_rt":"H1c","pluvio_mm_an":1200,"dju_base_18":3000,"zone_biogeo":6,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"26","nom_dept":"Drôme","zone_rt":"H2d","pluvio_mm_an":900,"dju_base_18":2200,"zone_biogeo":5,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"27","nom_dept":"Eure","zone_rt":"H1a","pluvio_mm_an":700,"dju_base_18":2500,"zone_biogeo":4,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"28","nom_dept":"Eure-et-Loir","zone_rt":"H1a","pluvio_mm_an":650,"dju_base_18":2500,"zone_biogeo":4,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"29","nom_dept":"Finistère","zone_rt":"H2a","pluvio_mm_an":1200,"dju_base_18":2300,"zone_biogeo":1,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"2A","nom_dept":"Corse-du-Sud","zone_rt":"H3","pluvio_mm_an":700,"dju_base_18":1500,"zone_biogeo":5,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"2B","nom_dept":"Haute-Corse","zone_rt":"H3","pluvio_mm_an":750,"dju_base_18":1600,"zone_biogeo":5,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"30","nom_dept":"Gard","zone_rt":"H3","pluvio_mm_an":700,"dju_base_18":1800,"zone_biogeo":5,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"31","nom_dept":"Haute-Garonne","zone_rt":"H2c","pluvio_mm_an":650,"dju_base_18":2200,"zone_biogeo":3,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"32","nom_dept":"Gers","zone_rt":"H2c","pluvio_mm_an":750,"dju_base_18":2200,"zone_biogeo":3,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"33","nom_dept":"Gironde","zone_rt":"H2c","pluvio_mm_an":900,"dju_base_18":2050,"zone_biogeo":2,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"34","nom_dept":"Hérault","zone_rt":"H3","pluvio_mm_an":650,"dju_base_18":1700,"zone_biogeo":5,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"35","nom_dept":"Ille-et-Vilaine","zone_rt":"H2a","pluvio_mm_an":750,"dju_base_18":2400,"zone_biogeo":1,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"36","nom_dept":"Indre","zone_rt":"H1c","pluvio_mm_an":750,"dju_base_18":2500,"zone_biogeo":4,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"37","nom_dept":"Indre-et-Loire","zone_rt":"H2b","pluvio_mm_an":700,"dju_base_18":2400,"zone_biogeo":4,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"38","nom_dept":"Isère","zone_rt":"H1c","pluvio_mm_an":1000,"dju_base_18":2700,"zone_biogeo":6,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"39","nom_dept":"Jura","zone_rt":"H1c","pluvio_mm_an":1200,"dju_base_18":2900,"zone_biogeo":6,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"40","nom_dept":"Landes","zone_rt":"H2c","pluvio_mm_an":1100,"dju_base_18":2000,"zone_biogeo":2,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"41","nom_dept":"Loir-et-Cher","zone_rt":"H1c","pluvio_mm_an":700,"dju_base_18":2500,"zone_biogeo":4,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"42","nom_dept":"Loire","zone_rt":"H1c","pluvio_mm_an":750,"dju_base_18":2600,"zone_biogeo":4,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"43","nom_dept":"Haute-Loire","zone_rt":"H1c","pluvio_mm_an":900,"dju_base_18":2900,"zone_biogeo":7,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"44","nom_dept":"Loire-Atlantique","zone_rt":"H2b","pluvio_mm_an":820,"dju_base_18":2300,"zone_biogeo":1,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"45","nom_dept":"Loiret","zone_rt":"H1a","pluvio_mm_an":650,"dju_base_18":2500,"zone_biogeo":4,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"46","nom_dept":"Lot","zone_rt":"H2c","pluvio_mm_an":850,"dju_base_18":2300,"zone_biogeo":3,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"47","nom_dept":"Lot-et-Garonne","zone_rt":"H2c","pluvio_mm_an":800,"dju_base_18":2150,"zone_biogeo":3,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"48","nom_dept":"Lozère","zone_rt":"H2d","pluvio_mm_an":1100,"dju_base_18":2900,"zone_biogeo":7,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"49","nom_dept":"Maine-et-Loire","zone_rt":"H2b","pluvio_mm_an":700,"dju_base_18":2400,"zone_biogeo":1,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"50","nom_dept":"Manche","zone_rt":"H2a","pluvio_mm_an":900,"dju_base_18":2400,"zone_biogeo":1,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"51","nom_dept":"Marne","zone_rt":"H1b","pluvio_mm_an":700,"dju_base_18":2700,"zone_biogeo":4,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"52","nom_dept":"Haute-Marne","zone_rt":"H1b","pluvio_mm_an":850,"dju_base_18":2800,"zone_biogeo":4,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"53","nom_dept":"Mayenne","zone_rt":"H2b","pluvio_mm_an":750,"dju_base_18":2400,"zone_biogeo":1,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"54","nom_dept":"Meurthe-et-Moselle","zone_rt":"H1b","pluvio_mm_an":800,"dju_base_18":2800,"zone_biogeo":4,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"55","nom_dept":"Meuse","zone_rt":"H1b","pluvio_mm_an":900,"dju_base_18":2800,"zone_biogeo":4,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"56","nom_dept":"Morbihan","zone_rt":"H2a","pluvio_mm_an":900,"dju_base_18":2300,"zone_biogeo":1,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"57","nom_dept":"Moselle","zone_rt":"H1b","pluvio_mm_an":800,"dju_base_18":2800,"zone_biogeo":4,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"58","nom_dept":"Nièvre","zone_rt":"H1c","pluvio_mm_an":800,"dju_base_18":2600,"zone_biogeo":4,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"59","nom_dept":"Nord","zone_rt":"H1a","pluvio_mm_an":750,"dju_base_18":2600,"zone_biogeo":1,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"60","nom_dept":"Oise","zone_rt":"H1a","pluvio_mm_an":700,"dju_base_18":2500,"zone_biogeo":4,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"61","nom_dept":"Orne","zone_rt":"H2a","pluvio_mm_an":800,"dju_base_18":2500,"zone_biogeo":1,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"62","nom_dept":"Pas-de-Calais","zone_rt":"H1a","pluvio_mm_an":850,"dju_base_18":2600,"zone_biogeo":1,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"63","nom_dept":"Puy-de-Dôme","zone_rt":"H1c","pluvio_mm_an":800,"dju_base_18":2700,"zone_biogeo":7,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"64","nom_dept":"Pyrénées-Atlantiques","zone_rt":"H2c","pluvio_mm_an":1300,"dju_base_18":2100,"zone_biogeo":2,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"65","nom_dept":"Hautes-Pyrénées","zone_rt":"H2c","pluvio_mm_an":1000,"dju_base_18":2400,"zone_biogeo":7,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"66","nom_dept":"Pyrénées-Orientales","zone_rt":"H3","pluvio_mm_an":600,"dju_base_18":1700,"zone_biogeo":5,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"67","nom_dept":"Bas-Rhin","zone_rt":"H1b","pluvio_mm_an":650,"dju_base_18":2700,"zone_biogeo":4,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"68","nom_dept":"Haut-Rhin","zone_rt":"H1b","pluvio_mm_an":700,"dju_base_18":2700,"zone_biogeo":4,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"69","nom_dept":"Rhône","zone_rt":"H1c","pluvio_mm_an":830,"dju_base_18":2500,"zone_biogeo":4,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"70","nom_dept":"Haute-Saône","zone_rt":"H1b","pluvio_mm_an":1050,"dju_base_18":2800,"zone_biogeo":4,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"71","nom_dept":"Saône-et-Loire","zone_rt":"H1c","pluvio_mm_an":800,"dju_base_18":2600,"zone_biogeo":4,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"72","nom_dept":"Sarthe","zone_rt":"H2b","pluvio_mm_an":700,"dju_base_18":2400,"zone_biogeo":1,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"73","nom_dept":"Savoie","zone_rt":"H1c","pluvio_mm_an":1200,"dju_base_18":3000,"zone_biogeo":6,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"74","nom_dept":"Haute-Savoie","zone_rt":"H1c","pluvio_mm_an":1300,"dju_base_18":2900,"zone_biogeo":6,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"75","nom_dept":"Paris","zone_rt":"H1a","pluvio_mm_an":640,"dju_base_18":2450,"zone_biogeo":4,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"76","nom_dept":"Seine-Maritime","zone_rt":"H1a","pluvio_mm_an":850,"dju_base_18":2500,"zone_biogeo":1,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"77","nom_dept":"Seine-et-Marne","zone_rt":"H1a","pluvio_mm_an":680,"dju_base_18":2500,"zone_biogeo":4,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"78","nom_dept":"Yvelines","zone_rt":"H1a","pluvio_mm_an":680,"dju_base_18":2500,"zone_biogeo":4,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"79","nom_dept":"Deux-Sèvres","zone_rt":"H2b","pluvio_mm_an":850,"dju_base_18":2300,"zone_biogeo":2,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"80","nom_dept":"Somme","zone_rt":"H1a","pluvio_mm_an":750,"dju_base_18":2600,"zone_biogeo":1,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"81","nom_dept":"Tarn","zone_rt":"H2c","pluvio_mm_an":850,"dju_base_18":2200,"zone_biogeo":3,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"82","nom_dept":"Tarn-et-Garonne","zone_rt":"H2c","pluvio_mm_an":750,"dju_base_18":2150,"zone_biogeo":3,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"83","nom_dept":"Var","zone_rt":"H3","pluvio_mm_an":700,"dju_base_18":1600,"zone_biogeo":5,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"84","nom_dept":"Vaucluse","zone_rt":"H3","pluvio_mm_an":650,"dju_base_18":1850,"zone_biogeo":5,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"85","nom_dept":"Vendée","zone_rt":"H2b","pluvio_mm_an":820,"dju_base_18":2300,"zone_biogeo":2,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"86","nom_dept":"Vienne","zone_rt":"H2b","pluvio_mm_an":750,"dju_base_18":2400,"zone_biogeo":2,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"87","nom_dept":"Haute-Vienne","zone_rt":"H1c","pluvio_mm_an":1050,"dju_base_18":2500,"zone_biogeo":3,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"88","nom_dept":"Vosges","zone_rt":"H1b","pluvio_mm_an":1100,"dju_base_18":2900,"zone_biogeo":6,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"89","nom_dept":"Yonne","zone_rt":"H1b","pluvio_mm_an":700,"dju_base_18":2600,"zone_biogeo":4,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"90","nom_dept":"Territoire de Belfort","zone_rt":"H1b","pluvio_mm_an":1100,"dju_base_18":2800,"zone_biogeo":4,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"91","nom_dept":"Essonne","zone_rt":"H1a","pluvio_mm_an":680,"dju_base_18":2500,"zone_biogeo":4,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"92","nom_dept":"Hauts-de-Seine","zone_rt":"H1a","pluvio_mm_an":640,"dju_base_18":2450,"zone_biogeo":4,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"93","nom_dept":"Seine-Saint-Denis","zone_rt":"H1a","pluvio_mm_an":640,"dju_base_18":2450,"zone_biogeo":4,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"94","nom_dept":"Val-de-Marne","zone_rt":"H1a","pluvio_mm_an":640,"dju_base_18":2450,"zone_biogeo":4,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"95","nom_dept":"Val-d'Oise","zone_rt":"H1a","pluvio_mm_an":680,"dju_base_18":2500,"zone_biogeo":4,"verifie":false,"source_climat":"Météo-France + RE2020","source_date":"2026-04-28"},
|
||||
{"dept":"971","nom_dept":"Guadeloupe","zone_rt":"H3","pluvio_mm_an":null,"dju_base_18":null,"zone_biogeo":8,"verifie":false,"source_climat":"DOM tropical — pluvio variable 1000-3500mm selon altitude, DJU non pertinent","source_date":"2026-04-28"},
|
||||
{"dept":"972","nom_dept":"Martinique","zone_rt":"H3","pluvio_mm_an":null,"dju_base_18":null,"zone_biogeo":8,"verifie":false,"source_climat":"DOM tropical — idem","source_date":"2026-04-28"},
|
||||
{"dept":"973","nom_dept":"Guyane","zone_rt":"H3","pluvio_mm_an":null,"dju_base_18":null,"zone_biogeo":8,"verifie":false,"source_climat":"DOM équatorial — pluvio 2000-4000mm","source_date":"2026-04-28"},
|
||||
{"dept":"974","nom_dept":"La Réunion","zone_rt":"H3","pluvio_mm_an":null,"dju_base_18":null,"zone_biogeo":8,"verifie":false,"source_climat":"DOM tropical insulaire — pluvio très variable littoral/montagne","source_date":"2026-04-28"},
|
||||
{"dept":"976","nom_dept":"Mayotte","zone_rt":"H3","pluvio_mm_an":null,"dju_base_18":null,"zone_biogeo":8,"verifie":false,"source_climat":"DOM tropical — pluvio 1000-1500mm","source_date":"2026-04-28"}
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,170 @@
|
||||
[
|
||||
{
|
||||
"Id": 1,
|
||||
"CreatedAt": "2026-04-28 19:54:29+00:00",
|
||||
"UpdatedAt": null,
|
||||
"axe": "eau",
|
||||
"code": "eau_captage",
|
||||
"poste": "Captage",
|
||||
"ordre": 10,
|
||||
"description": "Comment vous récupérez l'eau de pluie",
|
||||
"obligatoire": true,
|
||||
"actif": true
|
||||
},
|
||||
{
|
||||
"Id": 2,
|
||||
"CreatedAt": "2026-04-28 19:54:29+00:00",
|
||||
"UpdatedAt": null,
|
||||
"axe": "eau",
|
||||
"code": "eau_filtration",
|
||||
"poste": "Filtration",
|
||||
"ordre": 20,
|
||||
"description": "Étapes de potabilisation",
|
||||
"obligatoire": true,
|
||||
"actif": true
|
||||
},
|
||||
{
|
||||
"Id": 3,
|
||||
"CreatedAt": "2026-04-28 19:54:29+00:00",
|
||||
"UpdatedAt": null,
|
||||
"axe": "eau",
|
||||
"code": "eau_stockage",
|
||||
"poste": "Stockage",
|
||||
"ordre": 30,
|
||||
"description": "Volume cuve selon nb pers et pluvio",
|
||||
"obligatoire": true,
|
||||
"actif": true
|
||||
},
|
||||
{
|
||||
"Id": 4,
|
||||
"CreatedAt": "2026-04-28 19:54:29+00:00",
|
||||
"UpdatedAt": null,
|
||||
"axe": "eau",
|
||||
"code": "eau_distribution",
|
||||
"poste": "Distribution",
|
||||
"ordre": 40,
|
||||
"description": "Pompes et réseau de distribution",
|
||||
"obligatoire": true,
|
||||
"actif": true
|
||||
},
|
||||
{
|
||||
"Id": 5,
|
||||
"CreatedAt": "2026-04-28 19:54:29+00:00",
|
||||
"UpdatedAt": null,
|
||||
"axe": "eau",
|
||||
"code": "eau_eg_epuration",
|
||||
"poste": "Eaux grises + Épuration",
|
||||
"ordre": 50,
|
||||
"description": "Recyclage EG et assainissement",
|
||||
"obligatoire": false,
|
||||
"actif": true
|
||||
},
|
||||
{
|
||||
"Id": 6,
|
||||
"CreatedAt": "2026-04-28 19:54:29+00:00",
|
||||
"UpdatedAt": null,
|
||||
"axe": "energie",
|
||||
"code": "energie_pv",
|
||||
"poste": "Solaire PV",
|
||||
"ordre": 10,
|
||||
"description": "Production électrique panneaux + batteries",
|
||||
"obligatoire": false,
|
||||
"actif": true
|
||||
},
|
||||
{
|
||||
"Id": 7,
|
||||
"CreatedAt": "2026-04-28 19:54:29+00:00",
|
||||
"UpdatedAt": null,
|
||||
"axe": "energie",
|
||||
"code": "energie_solaire_th",
|
||||
"poste": "Solaire thermique",
|
||||
"ordre": 20,
|
||||
"description": "CESI / SSC pour ECS et chauffage",
|
||||
"obligatoire": false,
|
||||
"actif": true
|
||||
},
|
||||
{
|
||||
"Id": 8,
|
||||
"CreatedAt": "2026-04-28 19:54:29+00:00",
|
||||
"UpdatedAt": null,
|
||||
"axe": "energie",
|
||||
"code": "energie_chauffage_bois",
|
||||
"poste": "Chauffage bois",
|
||||
"ordre": 30,
|
||||
"description": "Poêles et chaudières bois ou granulés",
|
||||
"obligatoire": false,
|
||||
"actif": true
|
||||
},
|
||||
{
|
||||
"Id": 9,
|
||||
"CreatedAt": "2026-04-28 19:54:29+00:00",
|
||||
"UpdatedAt": null,
|
||||
"axe": "energie",
|
||||
"code": "energie_emetteurs",
|
||||
"poste": "Émetteurs",
|
||||
"ordre": 40,
|
||||
"description": "Radiateurs, plancher chauffant, mur chauffant",
|
||||
"obligatoire": false,
|
||||
"actif": true
|
||||
},
|
||||
{
|
||||
"Id": 10,
|
||||
"CreatedAt": "2026-04-28 19:54:29+00:00",
|
||||
"UpdatedAt": null,
|
||||
"axe": "energie",
|
||||
"code": "energie_pilotage",
|
||||
"poste": "Pilotage / ilotage",
|
||||
"ordre": 50,
|
||||
"description": "Monitoring, onduleur hybride, ilotage",
|
||||
"obligatoire": false,
|
||||
"actif": true
|
||||
},
|
||||
{
|
||||
"Id": 11,
|
||||
"CreatedAt": "2026-04-28 19:54:29+00:00",
|
||||
"UpdatedAt": null,
|
||||
"axe": "alimentation",
|
||||
"code": "alim_etude_fonciere",
|
||||
"poste": "Étude foncière",
|
||||
"ordre": 10,
|
||||
"description": "Diagnostic sol, topo, plan paysager",
|
||||
"obligatoire": false,
|
||||
"actif": true
|
||||
},
|
||||
{
|
||||
"Id": 12,
|
||||
"CreatedAt": "2026-04-28 19:54:29+00:00",
|
||||
"UpdatedAt": null,
|
||||
"axe": "alimentation",
|
||||
"code": "alim_strate_haute",
|
||||
"poste": "Strate haute",
|
||||
"ordre": 20,
|
||||
"description": "Arbres fruitiers haute-tige et demi-tige",
|
||||
"obligatoire": false,
|
||||
"actif": true
|
||||
},
|
||||
{
|
||||
"Id": 13,
|
||||
"CreatedAt": "2026-04-28 19:54:29+00:00",
|
||||
"UpdatedAt": null,
|
||||
"axe": "alimentation",
|
||||
"code": "alim_strate_moyenne",
|
||||
"poste": "Strate moyenne",
|
||||
"ordre": 30,
|
||||
"description": "Petits fruits et haies fruitières",
|
||||
"obligatoire": false,
|
||||
"actif": true
|
||||
},
|
||||
{
|
||||
"Id": 14,
|
||||
"CreatedAt": "2026-04-28 19:54:29+00:00",
|
||||
"UpdatedAt": null,
|
||||
"axe": "alimentation",
|
||||
"code": "alim_strate_basse",
|
||||
"poste": "Strate basse",
|
||||
"ordre": 40,
|
||||
"description": "Vivaces, médicinales, légumes",
|
||||
"obligatoire": false,
|
||||
"actif": true
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,64 @@
|
||||
[
|
||||
{
|
||||
"nom": "A — Frugal urbain",
|
||||
"inputs": {
|
||||
"budget_eur": 15000,
|
||||
"code_postal": "75019",
|
||||
"nb_personnes": 2,
|
||||
"surface_parcelle_m2": 200,
|
||||
"surface_toit_m2": 40,
|
||||
"orientation_toit": "sud",
|
||||
"pente_toit_deg": 30,
|
||||
"metres_haie_possibles": 0,
|
||||
"temps_dispo": 1
|
||||
},
|
||||
"notes": "Toit 40m2 → énergie+eau prioritaires; parcelle 200m2 < 300 → alim=0; temps=1 → pas complexite_pose=3",
|
||||
"attendus_indicatifs": [
|
||||
"captage_filtre_collecteur",
|
||||
"filtration_particulaire_simple",
|
||||
"stockage_pehd_horssol_5m3",
|
||||
"pv_kit_3kwc_string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"nom": "B — Famille moyenne",
|
||||
"inputs": {
|
||||
"budget_eur": 35000,
|
||||
"code_postal": "31000",
|
||||
"nb_personnes": 4,
|
||||
"surface_parcelle_m2": 800,
|
||||
"surface_toit_m2": 80,
|
||||
"orientation_toit": "sud-est",
|
||||
"pente_toit_deg": 30,
|
||||
"metres_haie_possibles": 50,
|
||||
"temps_dispo": 2
|
||||
},
|
||||
"notes": "4 pers, 800m2 parcelle, toit 80m2. Cuve ~9m3, PV 9kWc (80×0.15×0.93 clamp 9), potager.",
|
||||
"attendus_indicatifs": [
|
||||
"stockage_beton_5m3",
|
||||
"pv_kit_3kwc_string",
|
||||
"basse_serre_6m2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"nom": "C — Autonomie large",
|
||||
"inputs": {
|
||||
"budget_eur": 80000,
|
||||
"code_postal": "13100",
|
||||
"nb_personnes": 6,
|
||||
"surface_parcelle_m2": 2500,
|
||||
"surface_toit_m2": 120,
|
||||
"orientation_toit": "sud",
|
||||
"pente_toit_deg": 25,
|
||||
"metres_haie_possibles": 100,
|
||||
"temps_dispo": 3
|
||||
},
|
||||
"notes": "6 pers, 2500m2, H3 (13xxx = Bouches-du-Rhône). Grande parcelle + temps=3 → alim max. PV 9kWc clamp (120×0.15×1.0=18 → clamp 9). Bois poêle hydro requis pour budget.",
|
||||
"attendus_indicatifs": [
|
||||
"stockage_beton_5m3",
|
||||
"pv_kit_3kwc_string",
|
||||
"bois_poele_hydro",
|
||||
"basse_serre_6m2"
|
||||
]
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
Generated
+33
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "@transformer/moteurs",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@transformer/moteurs",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"name": "@transformer/moteurs",
|
||||
"version": "0.1.0",
|
||||
"description": "Moteurs de calcul purs pour la rénovation et l'autonomie d'une maison, partagés par calculs.trans-former.fr et plateforme.trans-former.fr. Code et coefficients sourcés ; jamais de prix dans le paquet.",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.trans-former.fr/jules/moteurs.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./commun": {
|
||||
"types": "./dist/commun/index.d.ts",
|
||||
"default": "./dist/commun/index.js"
|
||||
},
|
||||
"./autonomie": {
|
||||
"types": "./dist/autonomie/index.d.ts",
|
||||
"default": "./dist/autonomie/index.js"
|
||||
},
|
||||
"./autonomie/eau": {
|
||||
"types": "./dist/autonomie/eau.d.ts",
|
||||
"default": "./dist/autonomie/eau.js"
|
||||
},
|
||||
"./autonomie/energie": {
|
||||
"types": "./dist/autonomie/energie.d.ts",
|
||||
"default": "./dist/autonomie/energie.js"
|
||||
},
|
||||
"./autonomie/alimentation": {
|
||||
"types": "./dist/autonomie/alimentation.d.ts",
|
||||
"default": "./dist/autonomie/alimentation.js"
|
||||
},
|
||||
"./autonomie/integrateur": {
|
||||
"types": "./dist/autonomie/integrateur.d.ts",
|
||||
"default": "./dist/autonomie/integrateur.js"
|
||||
},
|
||||
"./autonomie/noyau": {
|
||||
"types": "./dist/autonomie/noyau/index.d.ts",
|
||||
"default": "./dist/autonomie/noyau/index.js"
|
||||
},
|
||||
"./schemas/*": "./schemas/*",
|
||||
"./fixtures/*": "./fixtures/*",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"src",
|
||||
"schemas",
|
||||
"fixtures"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "node scripts/nettoyer-dist.mjs && tsc -p tsconfig.json && node scripts/ecrire-schemas.mjs",
|
||||
"pretest": "npm run build",
|
||||
"test": "node --test",
|
||||
"ecarts": "node scripts/ecarts-harmonisation.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.9.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://git.trans-former.fr/jules/moteurs/schemas/algo_config.schema.json",
|
||||
"description": "Heuristiques de conception du simulateur autonomie (poids, modulateurs, matrices, tolérances, alias, poids des postes). Données non sourcées, injectées, jamais importées.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"poids_axes_base",
|
||||
"modulateurs",
|
||||
"adequation_weights",
|
||||
"tolerances",
|
||||
"bonus_malus",
|
||||
"couplages_alias",
|
||||
"postes_poids"
|
||||
],
|
||||
"properties": {
|
||||
"poids_axes_base": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"eau",
|
||||
"energie",
|
||||
"alim"
|
||||
],
|
||||
"properties": {
|
||||
"eau": {
|
||||
"type": "number"
|
||||
},
|
||||
"energie": {
|
||||
"type": "number"
|
||||
},
|
||||
"alim": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
"modulateurs": {
|
||||
"type": "object"
|
||||
},
|
||||
"adequation_weights": {
|
||||
"type": "object"
|
||||
},
|
||||
"tolerances": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"budget_overrun_pct": {
|
||||
"type": "number"
|
||||
},
|
||||
"budget_underspend_pct": {
|
||||
"type": "number"
|
||||
},
|
||||
"tolerance_par_poste_pct": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
"bonus_malus": {
|
||||
"type": "object"
|
||||
},
|
||||
"couplages_alias": {
|
||||
"type": "object"
|
||||
},
|
||||
"postes_poids": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://git.trans-former.fr/jules/moteurs/schemas/france_climat.schema.json",
|
||||
"description": "Une ligne par département (2A, 2B et 971-976 compris) : zone RT, pluviométrie, DJU base 18, zone biogéographique numérique.",
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"dept",
|
||||
"zone_rt"
|
||||
],
|
||||
"properties": {
|
||||
"dept": {
|
||||
"type": "string"
|
||||
},
|
||||
"nom_dept": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"zone_rt": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"H1a",
|
||||
"H1b",
|
||||
"H1c",
|
||||
"H2a",
|
||||
"H2b",
|
||||
"H2c",
|
||||
"H2d",
|
||||
"H3"
|
||||
]
|
||||
},
|
||||
"pluvio_mm_an": {
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"dju_base_18": {
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"zone_biogeo": {
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"source_climat": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"source_date": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://git.trans-former.fr/jules/moteurs/schemas/options.schema.json",
|
||||
"description": "Typologies (options) du simulateur autonomie. Les prix y sont des DONNÉES publiées par calculs, jamais des constantes du paquet. parametre_dim, couplages_requis et couplages_exclus arrivent en JSON sérialisé (NocoDB) ou déjà parsés.",
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"Id",
|
||||
"poste_id",
|
||||
"code",
|
||||
"nom"
|
||||
],
|
||||
"properties": {
|
||||
"Id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"poste_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"code": {
|
||||
"type": "string"
|
||||
},
|
||||
"nom": {
|
||||
"type": "string"
|
||||
},
|
||||
"typologie": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"actif": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"prix_mat_min": {
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"prix_mat_max": {
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"prix_pro_min": {
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"prix_pro_max": {
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"prix_par_unite": {
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"prix_fixe": {
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"unite": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"quantite_default": {
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"parametre_dim": {
|
||||
"type": [
|
||||
"string",
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"couplages_requis": {
|
||||
"type": [
|
||||
"string",
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"couplages_exclus": {
|
||||
"type": [
|
||||
"string",
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"flag_diy": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"complexite_pose": {
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"accessible_fr": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"filiere_agreee": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"surface_min_m2": {
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"surface_optimale_m2": {
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"tags": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"source_date": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description_pedago": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"notes_techniques": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://git.trans-former.fr/jules/moteurs/schemas/postes.schema.json",
|
||||
"description": "Postes du simulateur autonomie (calculs.trans-former.fr/calculs/data/postes.json). Champs lus par les moteurs.",
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"Id",
|
||||
"axe",
|
||||
"code",
|
||||
"poste"
|
||||
],
|
||||
"properties": {
|
||||
"Id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"axe": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"eau",
|
||||
"energie",
|
||||
"alimentation"
|
||||
]
|
||||
},
|
||||
"code": {
|
||||
"type": "string"
|
||||
},
|
||||
"poste": {
|
||||
"type": "string"
|
||||
},
|
||||
"ordre": {
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"obligatoire": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"actif": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// ecarts-harmonisation.mjs — fait tourner les trois profils de référence avec
|
||||
// le jeu de coefficients D'ORIGINE puis avec la table HARMONISÉE, et écrit les
|
||||
// écarts de sélection ligne par ligne (markdown sur stdout ; JSON dans
|
||||
// fixtures/ecarts-harmonisation-<date>.json avec --ecrire). Ce sont ces écarts
|
||||
// que Jules relit avec docs/CONSTANTES.md (LOT P1.0 point 4, amendement du 28/09).
|
||||
// Usage : node scripts/ecarts-harmonisation.mjs [--ecrire AAAA-MM-JJ]
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
import { COEFFICIENTS } from '../dist/commun/coefficients.js'
|
||||
import { deriveContexte } from '../dist/commun/contexte.js'
|
||||
import { eau, energie, alimentation, integrateur } from '../dist/autonomie/index.js'
|
||||
|
||||
const fx = new URL('../fixtures/', import.meta.url)
|
||||
const lire = (f) => JSON.parse(readFileSync(new URL(f, fx), 'utf8'))
|
||||
const options = lire('options.json')
|
||||
const postes = lire('postes.json')
|
||||
const france_climat = lire('france_climat.json')
|
||||
const algo_config = lire('algo-config.json')
|
||||
const profils = lire('profils-reference.json')
|
||||
const origine = lire('coefficients-2026-05.json')
|
||||
const AXES = ['eau', 'energie', 'alim']
|
||||
const MOTEURS = { eau, energie, alim: alimentation }
|
||||
|
||||
export function calculeEcarts() {
|
||||
const lignes = []
|
||||
for (const profil of profils) {
|
||||
for (const jeu of [
|
||||
['origine', origine],
|
||||
['harmonisee', COEFFICIENTS],
|
||||
]) {
|
||||
const [nom, coefficients] = jeu
|
||||
const donnees = { postes, options, algo_config, france_climat, coefficients }
|
||||
const g = integrateur.calculer({ autodiag: profil.inputs, budget_total: profil.inputs.budget_eur, mode: 'global' }, donnees).sorties
|
||||
const contexte = deriveContexte(profil.inputs.code_postal, france_climat, coefficients).sorties
|
||||
const par_axe = {}
|
||||
for (const axe of AXES) {
|
||||
const r = MOTEURS[axe].calculer({ autodiag: profil.inputs, budget_axe: g.budgets[axe], contexte }, donnees).sorties
|
||||
par_axe[axe] = { typologies: r.typologies, sous_total: r.sous_total_eur_ht, status: r.status, taux: r.taux_couverture }
|
||||
}
|
||||
lignes.push({ profil: profil.nom, jeu: nom, global: { total: g.total_eur_ht, status: g.status, typologies: Object.fromEntries(AXES.map((a) => [a, g.axes[a].typologies])), taux: g.taux_couverture }, par_axe })
|
||||
}
|
||||
}
|
||||
// Diff origine → harmonisée, par profil
|
||||
const ecarts = []
|
||||
for (const profil of profils) {
|
||||
const o = lignes.find((l) => l.profil === profil.nom && l.jeu === 'origine')
|
||||
const h = lignes.find((l) => l.profil === profil.nom && l.jeu === 'harmonisee')
|
||||
for (const axe of AXES) {
|
||||
const to = o.global.typologies[axe].join(' + ')
|
||||
const th = h.global.typologies[axe].join(' + ')
|
||||
if (to !== th) ecarts.push({ profil: profil.nom, mode: 'global', axe, origine: to, harmonisee: th })
|
||||
const ao = o.par_axe[axe]
|
||||
const ah = h.par_axe[axe]
|
||||
if (ao.typologies.join('+') !== ah.typologies.join('+') || ao.sous_total !== ah.sous_total) {
|
||||
ecarts.push({ profil: profil.nom, mode: 'par_axe', axe, origine: `${ao.typologies.join(' + ')} (${ao.sous_total} €)`, harmonisee: `${ah.typologies.join(' + ')} (${ah.sous_total} €)` })
|
||||
}
|
||||
if (ao.taux !== ah.taux) ecarts.push({ profil: profil.nom, mode: 'taux (par_axe)', axe, origine: `${ao.taux} %`, harmonisee: `${ah.taux} %` })
|
||||
if (o.global.taux[axe] !== h.global.taux[axe]) ecarts.push({ profil: profil.nom, mode: 'taux (global)', axe, origine: `${o.global.taux[axe]} %`, harmonisee: `${h.global.taux[axe]} %` })
|
||||
}
|
||||
if (o.global.total !== h.global.total || o.global.status !== h.global.status) {
|
||||
ecarts.push({ profil: profil.nom, mode: 'global', axe: 'total', origine: `${o.global.total} € (${o.global.status})`, harmonisee: `${h.global.total} € (${h.global.status})` })
|
||||
}
|
||||
}
|
||||
return { lignes, ecarts }
|
||||
}
|
||||
|
||||
const estCli = import.meta.url === new URL(`file:///${process.argv[1].replace(/\\/g, '/')}`).href || process.argv[1]?.endsWith('ecarts-harmonisation.mjs')
|
||||
if (estCli) {
|
||||
const { lignes, ecarts } = calculeEcarts()
|
||||
console.log('| Profil | Mode | Axe | Origine (coefficients du code) | Harmonisée (table du paquet) |')
|
||||
console.log('|---|---|---|---|---|')
|
||||
for (const e of ecarts) console.log(`| ${e.profil} | ${e.mode} | ${e.axe} | ${e.origine} | ${e.harmonisee} |`)
|
||||
if (ecarts.length === 0) console.log('| (aucun écart) | | | | |')
|
||||
console.log()
|
||||
console.log('Taux de couverture (eau / énergie / alim), par profil :')
|
||||
for (const l of lignes) console.log(`- ${l.profil} — ${l.jeu} : ${AXES.map((a) => `${a} ${l.global.taux[a] ?? 'null'} %`).join(' · ')} — total ${l.global.total} € (${l.global.status})`)
|
||||
const i = process.argv.indexOf('--ecrire')
|
||||
if (i > 0) {
|
||||
const date = process.argv[i + 1] ?? new Date().toISOString().slice(0, 10)
|
||||
const cible = new URL(`ecarts-harmonisation-${date}.json`, fx)
|
||||
writeFileSync(cible, JSON.stringify({ _commentaire: `Écarts de sélection et de taux entre le jeu de coefficients d'origine et la table harmonisée, sur les trois profils de référence, figés le ${date}. Un changement de coefficient ou de code qui modifie ces écarts casse test/decoupage.test.mjs : c'est voulu, on relit CONSTANTES.md avant de refiger.`, date, ecarts, lignes }, null, 2) + '\n', 'utf8')
|
||||
console.log(`\nécrit : fixtures/ecarts-harmonisation-${date}.json`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Réécrit schemas/<table>.schema.json depuis dist/commun/schemas.js (source de
|
||||
// vérité : src/commun/schemas.ts). Lancé par `npm run build` après tsc.
|
||||
import { writeFileSync, mkdirSync } from 'node:fs'
|
||||
import { SCHEMAS } from '../dist/commun/schemas.js'
|
||||
|
||||
const dossier = new URL('../schemas/', import.meta.url)
|
||||
mkdirSync(dossier, { recursive: true })
|
||||
for (const [table, schema] of Object.entries(SCHEMAS)) {
|
||||
const json = { $schema: 'https://json-schema.org/draft/2020-12/schema', ...schema }
|
||||
writeFileSync(new URL(`${table}.schema.json`, dossier), JSON.stringify(json, null, 2) + '\n', 'utf8')
|
||||
}
|
||||
console.log(`schemas/ : ${Object.keys(SCHEMAS).length} fichiers écrits`)
|
||||
@@ -0,0 +1,3 @@
|
||||
// Vide dist/ avant tsc (portable Windows / Linux, sans rm -rf).
|
||||
import { rmSync } from 'node:fs'
|
||||
rmSync(new URL('../dist/', import.meta.url), { recursive: true, force: true })
|
||||
@@ -0,0 +1,76 @@
|
||||
// pin-origine.mjs — génère fixtures/sorties-origine-<date>.json DEPUIS LE CODE
|
||||
// D'ORIGINE (clone astro-pro, src/lib/autonomie), pas depuis ce paquet : c'est
|
||||
// ce qui fait du test de non-régression une vraie épingle.
|
||||
//
|
||||
// Rejouable seulement avec un clone d'astro-pro à la révision d'origine
|
||||
// (master c628cf9 au 2026-09-28) : ASTRO_PRO_DIR=~/dev/astro-pro par défaut.
|
||||
// Usage : node scripts/pin-origine.mjs [AAAA-MM-JJ]
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
import { homedir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
|
||||
const ASTRO = process.env.ASTRO_PRO_DIR ?? join(homedir(), 'dev', 'astro-pro')
|
||||
const date = process.argv[2] ?? new Date().toISOString().slice(0, 10)
|
||||
const lib = (f) => pathToFileURL(join(ASTRO, 'src', 'lib', 'autonomie', f)).href
|
||||
|
||||
const { selectionner } = await import(lib('algo.js'))
|
||||
const { configPourAxes } = await import(lib('axes-actifs.js'))
|
||||
const { initConfig } = await import(lib('calibrage.js'))
|
||||
|
||||
const fx = new URL('../fixtures/', import.meta.url)
|
||||
const lire = (f) => JSON.parse(readFileSync(new URL(f, fx), 'utf8'))
|
||||
const options = lire('options.json')
|
||||
const postes = lire('postes.json')
|
||||
const climat = lire('france_climat.json')
|
||||
const config = lire('algo-config.json')
|
||||
const profils = lire('profils-reference.json')
|
||||
initConfig(config)
|
||||
|
||||
const AXES = ['eau', 'energie', 'alim']
|
||||
const resume = (r) => ({
|
||||
total_eur_ht: r.total_eur_ht,
|
||||
status: r.status,
|
||||
ecart_budget_pct: r.ecart_budget_pct,
|
||||
axes: Object.fromEntries(
|
||||
AXES.map((a) => [
|
||||
a,
|
||||
{
|
||||
budget_alloue: r.axes[a]?.budget_alloue ?? 0,
|
||||
poids: r.axes[a]?.poids ?? 0,
|
||||
sous_total_eur_ht: r.axes[a]?.sous_total_eur_ht ?? 0,
|
||||
postes: (r.axes[a]?.postes ?? []).map((p) => ({
|
||||
poste_id: p.poste_id,
|
||||
typologie_choisie: p.typologie_choisie,
|
||||
dimension: p.dimension ? { valeur: p.dimension.valeur, unite: p.dimension.unite } : null,
|
||||
cout_eur_ht: p.cout_eur_ht,
|
||||
force_par_couplage: !!p.force_par_couplage,
|
||||
})),
|
||||
},
|
||||
]),
|
||||
),
|
||||
couplages: r.couplages,
|
||||
})
|
||||
|
||||
const sortie = {
|
||||
_commentaire: `Sorties de selectionner() D'ORIGINE (astro-pro src/lib/autonomie, coefficients du code) sur les trois profils de référence, générées le ${date} par scripts/pin-origine.mjs. 'global' = pipeline complet ; 'par_axe' = mode un seul axe actif (configPourAxes) avec le budget que le pipeline global avait alloué à cet axe.`,
|
||||
date,
|
||||
profils: {},
|
||||
}
|
||||
for (const profil of profils) {
|
||||
const global = selectionner(profil.inputs, options, postes, climat, config, false)
|
||||
const par_axe = {}
|
||||
for (const axe of AXES) {
|
||||
const budget = global.axes[axe]?.budget_alloue ?? 0
|
||||
const actifs = Object.fromEntries(AXES.map((a) => [a, a === axe]))
|
||||
const r = selectionner({ ...profil.inputs, budget_eur: budget }, options, postes, climat, configPourAxes(config, actifs), false)
|
||||
par_axe[axe] = { budget_axe: budget, ...resume(r) }
|
||||
}
|
||||
sortie.profils[profil.nom] = { inputs: profil.inputs, global: resume(global), par_axe }
|
||||
}
|
||||
const cible = new URL(`sorties-origine-${date}.json`, fx)
|
||||
writeFileSync(cible, JSON.stringify(sortie, null, 2) + '\n', 'utf8')
|
||||
console.log(`écrit : fixtures/sorties-origine-${date}.json`)
|
||||
for (const [nom, p] of Object.entries(sortie.profils)) {
|
||||
console.log(`${nom} : total ${p.global.total_eur_ht} (${p.global.status})`, AXES.map((a) => `${a}=${p.global.axes[a].postes.map((x) => x.typologie_choisie).join('+')}`).join(' | '))
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// alimentation.ts — moteur `autonomie.alimentation` : étude foncière, strates
|
||||
// haute, moyenne (haies) et basse, pour un budget d'axe donné.
|
||||
// Code d'axe interne : 'alim' (postes.json dit « alimentation »).
|
||||
import { moteurAxe } from './axe.js'
|
||||
export type { EntreesAxe, DonneesAutonomie, SortiesAxe } from './axe.js'
|
||||
|
||||
export const alimentation = moteurAxe('alim', 'autonomie.alimentation')
|
||||
export default alimentation
|
||||
@@ -0,0 +1,143 @@
|
||||
// axe.ts — fabrique des moteurs d'axe `autonomie.eau`, `autonomie.energie`,
|
||||
// `autonomie.alimentation` (LOT P1.0, découpage par axe).
|
||||
//
|
||||
// Un moteur d'axe = selectionnePoste(axe) + couplages de l'axe + réajustement
|
||||
// au budget de l'axe + taux de couverture de l'axe. Il réutilise la queue du
|
||||
// pipeline d'origine (`finaliseSelection`) sur une sélection où les deux
|
||||
// autres axes sont vides et désactivés (`configPourAxes`) : par construction,
|
||||
// il rend exactement ce que `selectionner()` rend en mode « un seul axe
|
||||
// actif » avec ce budget — c'est le test de non-régression du découpage.
|
||||
|
||||
import type { Moteur, Resultat } from '../commun/contrat.js'
|
||||
import { Trace } from '../commun/contrat.js'
|
||||
import { COEFFICIENTS, type TableCoefficients } from '../commun/coefficients.js'
|
||||
import type { Contexte } from '../commun/contexte.js'
|
||||
import { VERSION } from '../version.js'
|
||||
import { finaliseSelection } from './noyau/algo.js'
|
||||
import { configPourAxes } from './noyau/axes-actifs.js'
|
||||
import { selectionnePoste } from './noyau/selectionne-poste.js'
|
||||
import type { AlgoConfig, Autodiag, Axe, AxeResultat, Option, Poste, PosteResultat, Reglages, Selection, Statut } from './noyau/types.js'
|
||||
import { AXES } from './noyau/types.js'
|
||||
import { aCaptage, batterieSelectionnee, kwcSelectionnes, mlHaiesSelectionnes, tauxAlim, tauxEau, tauxEnergie, type DetailAlim, type DetailEau, type DetailEnergie } from './taux.js'
|
||||
|
||||
export interface EntreesAxe {
|
||||
autodiag: Autodiag
|
||||
/** Budget alloué à cet axe (EUR HT). */
|
||||
budget_axe: number
|
||||
/** Contexte climatique (moteur `commun.contexte`). */
|
||||
contexte: Contexte
|
||||
}
|
||||
|
||||
export interface DonneesAutonomie {
|
||||
postes: Poste[]
|
||||
options: Option[]
|
||||
algo_config: AlgoConfig
|
||||
/** Table de coefficients ; par défaut la table harmonisée du paquet. */
|
||||
coefficients?: TableCoefficients
|
||||
}
|
||||
|
||||
export interface SortiesAxe {
|
||||
axe: Axe
|
||||
postes: PosteResultat[]
|
||||
/** Codes des typologies retenues, dans l'ordre des postes. */
|
||||
typologies: string[]
|
||||
sous_total_eur_ht: number
|
||||
budget_alloue: number
|
||||
ecart_budget_pct: number
|
||||
status: Statut
|
||||
message_status?: string
|
||||
/** Journal des couplages (FORCE / MANQUANT / WARN / CONFLIT). */
|
||||
couplages: string[]
|
||||
/** Taux de couverture de l'axe en %, null si non calculable. */
|
||||
taux_couverture: number | null
|
||||
taux_detail: DetailEau | DetailEnergie | DetailAlim | null
|
||||
}
|
||||
|
||||
/** Enregistre les clés de coefficients lues pendant un calcul (Proxy en lecture seule). */
|
||||
export function enregistreLectures(table: TableCoefficients): { table: TableCoefficients; lues: Set<keyof TableCoefficients> } {
|
||||
const lues = new Set<keyof TableCoefficients>()
|
||||
const proxy = new Proxy(table, {
|
||||
get(cible, prop, recepteur) {
|
||||
if (typeof prop === 'string' && prop in cible) lues.add(prop as keyof TableCoefficients)
|
||||
return Reflect.get(cible, prop, recepteur)
|
||||
},
|
||||
})
|
||||
return { table: proxy, lues }
|
||||
}
|
||||
|
||||
function axeVide(): AxeResultat {
|
||||
return { postes: [], sous_total_eur_ht: 0, budget_alloue: 0, poids: 0 }
|
||||
}
|
||||
|
||||
/** Ajoute aux sources les coefficients lus et les prix des typologies retenues. */
|
||||
export function traceSelection(trace: Trace, coefficients: TableCoefficients, lues: Set<keyof TableCoefficients>, postes: PosteResultat[], options: Option[], couplages: string[]): void {
|
||||
for (const cle of lues) {
|
||||
const c = coefficients[cle]
|
||||
const v = c.valeur
|
||||
trace.source(cle, typeof v === 'number' || typeof v === 'string' ? v : JSON.stringify(v), c.source, c.date)
|
||||
}
|
||||
for (const p of postes) {
|
||||
const o = options.find((x) => x.code === p.typologie_choisie)
|
||||
trace.source(`option.${p.typologie_choisie}`, p.cout_eur_ht, String(o?.source ?? 'options (calculs.trans-former.fr)'), String(o?.source_date ?? ''))
|
||||
}
|
||||
for (const ligne of couplages) {
|
||||
if (ligne.startsWith('MANQUANT') || ligne.startsWith('WARN')) trace.avertit(`couplage : ${ligne}`)
|
||||
}
|
||||
}
|
||||
|
||||
export function calculeAxe(axe: Axe, entrees: EntreesAxe, donnees: DonneesAutonomie): Resultat<SortiesAxe> {
|
||||
const trace = new Trace()
|
||||
const { table: coefficients, lues } = enregistreLectures(donnees.coefficients ?? COEFFICIENTS)
|
||||
const actifs = Object.fromEntries(AXES.map((a) => [a, a === axe])) as Record<Axe, boolean>
|
||||
const reglages: Reglages = { config: configPourAxes(donnees.algo_config, actifs), coefficients }
|
||||
const inputs = entrees.autodiag
|
||||
const budget = Math.max(0, entrees.budget_axe || 0)
|
||||
|
||||
const selection: Selection = {}
|
||||
for (const a of AXES) selection[a] = a === axe ? selectionnePoste(budget, inputs, entrees.contexte, donnees.options, donnees.postes, a, reglages) : axeVide()
|
||||
selection[axe]!.poids = 100
|
||||
|
||||
const fin = finaliseSelection(selection, budget, donnees.options, donnees.postes, inputs, entrees.contexte, reglages)
|
||||
const postes = fin.axes[axe]?.postes ?? []
|
||||
const sous_total = fin.axes[axe]?.sous_total_eur_ht ?? 0
|
||||
|
||||
// Un poste forcé dans un autre axe par un couplage inter-axes serait perdu ici :
|
||||
// aucun dans les données (inventaire §4), on le signale si ça arrive.
|
||||
for (const a of AXES) {
|
||||
if (a !== axe && (fin.axes[a]?.postes.length ?? 0) > 0) trace.avertit(`couplage inter-axes : ${fin.axes[a]!.postes.length} poste(s) forcé(s) dans l'axe ${a}, hors du périmètre du moteur ${axe}`)
|
||||
}
|
||||
|
||||
let taux: { taux: number | null; detail: DetailEau | DetailEnergie | DetailAlim | null }
|
||||
if (axe === 'eau') {
|
||||
taux = tauxEau({ surface_toit_m2: inputs.surface_toit_m2, nb_personnes: inputs.nb_personnes, pluviometrie_mm: entrees.contexte.pluviometrie_mm, a_captage: aCaptage(postes) }, coefficients, trace)
|
||||
} else if (axe === 'energie') {
|
||||
taux = tauxEnergie(
|
||||
{ kwc: kwcSelectionnes(postes), batterie_kwh: batterieSelectionnee(postes), orientation_toit: inputs.orientation_toit, nb_personnes: inputs.nb_personnes, contexte: entrees.contexte },
|
||||
coefficients,
|
||||
trace,
|
||||
)
|
||||
} else {
|
||||
taux = tauxAlim({ ml: mlHaiesSelectionnes(postes), nb_personnes: inputs.nb_personnes }, coefficients, trace)
|
||||
}
|
||||
|
||||
traceSelection(trace, donnees.coefficients ?? COEFFICIENTS, lues, postes, donnees.options, fin.couplages)
|
||||
|
||||
const sorties: SortiesAxe = {
|
||||
axe,
|
||||
postes,
|
||||
typologies: postes.map((p) => p.typologie_choisie),
|
||||
sous_total_eur_ht: sous_total,
|
||||
budget_alloue: budget,
|
||||
ecart_budget_pct: budget > 0 ? +(((sous_total - budget) / budget) * 100).toFixed(2) : 0,
|
||||
status: fin.status,
|
||||
message_status: fin.message_status,
|
||||
couplages: fin.couplages,
|
||||
taux_couverture: taux.taux,
|
||||
taux_detail: taux.detail,
|
||||
}
|
||||
return trace.resultat(sorties, VERSION)
|
||||
}
|
||||
|
||||
export function moteurAxe(axe: Axe, id: string): Moteur<EntreesAxe, DonneesAutonomie, SortiesAxe> {
|
||||
return { id, calculer: (entrees, donnees) => calculeAxe(axe, entrees, donnees) }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// eau.ts — moteur `autonomie.eau` : captage, filtration, stockage,
|
||||
// distribution, eaux grises et épuration, pour un budget d'axe donné.
|
||||
import { moteurAxe } from './axe.js'
|
||||
export type { EntreesAxe, DonneesAutonomie, SortiesAxe } from './axe.js'
|
||||
|
||||
export const eau = moteurAxe('eau', 'autonomie.eau')
|
||||
export default eau
|
||||
@@ -0,0 +1,7 @@
|
||||
// energie.ts — moteur `autonomie.energie` : photovoltaïque, solaire thermique,
|
||||
// chauffage bois, émetteurs, pilotage, pour un budget d'axe donné.
|
||||
import { moteurAxe } from './axe.js'
|
||||
export type { EntreesAxe, DonneesAutonomie, SortiesAxe } from './axe.js'
|
||||
|
||||
export const energie = moteurAxe('energie', 'autonomie.energie')
|
||||
export default energie
|
||||
@@ -0,0 +1,12 @@
|
||||
// @transformer/moteurs/autonomie — trois moteurs d'axe, un intégrateur, le noyau.
|
||||
export { eau } from './eau.js'
|
||||
export { energie } from './energie.js'
|
||||
export { alimentation } from './alimentation.js'
|
||||
export { integrateur, calculeIntegrateur, agregeStatuts } from './integrateur.js'
|
||||
export type { EntreesIntegrateur, DonneesIntegrateur, SortiesIntegrateur } from './integrateur.js'
|
||||
export { calculeAxe, moteurAxe } from './axe.js'
|
||||
export type { EntreesAxe, DonneesAutonomie, SortiesAxe } from './axe.js'
|
||||
export * from './taux.js'
|
||||
export * from './prix.js'
|
||||
export * as noyau from './noyau/index.js'
|
||||
export type { Axe, Autodiag, InputsSimulation, Poste, Option, AlgoConfig, PosteResultat, AxeResultat, Statut, PoidsAxes } from './noyau/types.js'
|
||||
@@ -0,0 +1,150 @@
|
||||
// integrateur.ts — moteur `autonomie.integrateur` : contexte + pondération des
|
||||
// axes + allocation du budget + agrégation.
|
||||
//
|
||||
// Deux modes :
|
||||
// - 'global' (défaut) : le pipeline complet d'origine (`selectionner`), avec
|
||||
// couplages et réajustement GLOBAUX entre axes — celui de calculs ;
|
||||
// - 'par_axe' : chaque axe est calculé par son moteur d'axe sur son budget,
|
||||
// puis les sorties sont additionnées — celui du menu de la plateforme.
|
||||
// Le mode « aménagements cochés » (recalcul sans relancer l'allocation) est
|
||||
// P1.5 : une liste transmise est ignorée et signalée.
|
||||
|
||||
import type { Moteur, Resultat } from '../commun/contrat.js'
|
||||
import { Trace } from '../commun/contrat.js'
|
||||
import { COEFFICIENTS } from '../commun/coefficients.js'
|
||||
import type { Contexte, LigneClimat } from '../commun/contexte.js'
|
||||
import { deriveContexte } from '../commun/contexte.js'
|
||||
import { VERSION } from '../version.js'
|
||||
import { calculeAxe, enregistreLectures, traceSelection, type DonneesAutonomie, type SortiesAxe } from './axe.js'
|
||||
import { selectionner } from './noyau/algo.js'
|
||||
import { alloueBudget } from './noyau/alloue-budget.js'
|
||||
import { configPourAxes, type AxesActifs } from './noyau/axes-actifs.js'
|
||||
import { pondereAxes } from './noyau/pondere-axes.js'
|
||||
import type { Autodiag, Axe, PoidsAxes, PosteResultat, Statut } from './noyau/types.js'
|
||||
import { AXES } from './noyau/types.js'
|
||||
import { aCaptage, batterieSelectionnee, kwcSelectionnes, mlHaiesSelectionnes, tauxAlim, tauxEau, tauxEnergie } from './taux.js'
|
||||
|
||||
export interface EntreesIntegrateur {
|
||||
autodiag: Autodiag
|
||||
budget_total: number
|
||||
/** 'global' = pipeline d'origine (calculs) ; 'par_axe' = somme des moteurs d'axe (plateforme). */
|
||||
mode?: 'global' | 'par_axe'
|
||||
/** Axes désactivés par l'habitant (toggles) ; absent = les trois. */
|
||||
axes_actifs?: AxesActifs
|
||||
/** P1.5 : aménagements cochés ; ignoré en v0.1 (signalé). */
|
||||
amenagements_coches?: string[]
|
||||
}
|
||||
|
||||
export interface DonneesIntegrateur extends DonneesAutonomie {
|
||||
france_climat: LigneClimat[]
|
||||
}
|
||||
|
||||
export interface SortiesIntegrateur {
|
||||
mode: 'global' | 'par_axe'
|
||||
contexte: Contexte
|
||||
poids: PoidsAxes
|
||||
budgets: PoidsAxes
|
||||
axes: Record<Axe, Pick<SortiesAxe, 'postes' | 'typologies' | 'sous_total_eur_ht' | 'budget_alloue' | 'status' | 'taux_couverture' | 'taux_detail'>>
|
||||
total_eur_ht: number
|
||||
budget_cible: number
|
||||
ecart_budget_pct: number
|
||||
status: Statut
|
||||
message_status?: string
|
||||
couplages: string[]
|
||||
taux_couverture: Record<Axe, number | null>
|
||||
}
|
||||
|
||||
/** Statut agrégé : BUDGET_INSUFFISANT l'emporte, puis AJOUTER_OPTIONNEL, sinon OK. */
|
||||
export function agregeStatuts(statuts: Statut[]): Statut {
|
||||
if (statuts.includes('BUDGET_INSUFFISANT')) return 'BUDGET_INSUFFISANT'
|
||||
if (statuts.includes('AJOUTER_OPTIONNEL')) return 'AJOUTER_OPTIONNEL'
|
||||
return 'OK'
|
||||
}
|
||||
|
||||
export function calculeIntegrateur(entrees: EntreesIntegrateur, donnees: DonneesIntegrateur): Resultat<SortiesIntegrateur> {
|
||||
const trace = new Trace()
|
||||
const mode = entrees.mode ?? 'global'
|
||||
const coefficients_base = donnees.coefficients ?? COEFFICIENTS
|
||||
const { table: coefficients, lues } = enregistreLectures(coefficients_base)
|
||||
const config = configPourAxes(donnees.algo_config, entrees.axes_actifs)
|
||||
const inputs = { ...entrees.autodiag, budget_eur: entrees.budget_total }
|
||||
|
||||
if (entrees.amenagements_coches && entrees.amenagements_coches.length) {
|
||||
trace.avertit('aménagements cochés ignorés : le mode « coché » arrive en P1.5')
|
||||
}
|
||||
|
||||
const ctx = deriveContexte(inputs.code_postal, donnees.france_climat, coefficients)
|
||||
trace.absorbe(ctx)
|
||||
const contexte = ctx.sorties
|
||||
const poids = pondereAxes(inputs, contexte, config, false)
|
||||
const budgets = alloueBudget(entrees.budget_total, poids)
|
||||
|
||||
const axes = {} as SortiesIntegrateur['axes']
|
||||
const taux_couverture = {} as Record<Axe, number | null>
|
||||
let total = 0
|
||||
let status: Statut
|
||||
let message_status: string | undefined
|
||||
let couplages: string[] = []
|
||||
|
||||
if (mode === 'par_axe') {
|
||||
const statuts: Statut[] = []
|
||||
for (const axe of AXES) {
|
||||
const actif = !entrees.axes_actifs || entrees.axes_actifs[axe] !== false
|
||||
if (!actif || budgets[axe] <= 0) {
|
||||
axes[axe] = { postes: [], typologies: [], sous_total_eur_ht: 0, budget_alloue: budgets[axe], status: 'OK', taux_couverture: null, taux_detail: null }
|
||||
taux_couverture[axe] = null
|
||||
continue
|
||||
}
|
||||
const r = calculeAxe(axe, { autodiag: entrees.autodiag, budget_axe: budgets[axe], contexte }, { ...donnees, coefficients: coefficients_base })
|
||||
trace.absorbe(r)
|
||||
const s = r.sorties
|
||||
axes[axe] = { postes: s.postes, typologies: s.typologies, sous_total_eur_ht: s.sous_total_eur_ht, budget_alloue: s.budget_alloue, status: s.status, taux_couverture: s.taux_couverture, taux_detail: s.taux_detail }
|
||||
taux_couverture[axe] = s.taux_couverture
|
||||
total += s.sous_total_eur_ht
|
||||
statuts.push(s.status)
|
||||
couplages = couplages.concat(s.couplages)
|
||||
}
|
||||
status = agregeStatuts(statuts)
|
||||
} else {
|
||||
const r = selectionner(inputs, donnees.options, donnees.postes, donnees.france_climat, { config, coefficients }, false)
|
||||
const tous: PosteResultat[] = []
|
||||
for (const axe of AXES) {
|
||||
const a = r.axes[axe]
|
||||
const postes = a?.postes ?? []
|
||||
tous.push(...postes)
|
||||
let t: { taux: number | null; detail: SortiesAxe['taux_detail'] }
|
||||
if (axe === 'eau') t = tauxEau({ surface_toit_m2: inputs.surface_toit_m2, nb_personnes: inputs.nb_personnes, pluviometrie_mm: contexte.pluviometrie_mm, a_captage: aCaptage(postes) }, coefficients, trace)
|
||||
else if (axe === 'energie') t = tauxEnergie({ kwc: kwcSelectionnes(postes), batterie_kwh: batterieSelectionnee(postes), orientation_toit: inputs.orientation_toit, nb_personnes: inputs.nb_personnes, contexte }, coefficients, trace)
|
||||
else t = tauxAlim({ ml: mlHaiesSelectionnes(postes), nb_personnes: inputs.nb_personnes }, coefficients, trace)
|
||||
axes[axe] = { postes, typologies: postes.map((p) => p.typologie_choisie), sous_total_eur_ht: a?.sous_total_eur_ht ?? 0, budget_alloue: a?.budget_alloue ?? budgets[axe], status: r.status, taux_couverture: t.taux, taux_detail: t.detail }
|
||||
taux_couverture[axe] = t.taux
|
||||
}
|
||||
total = r.total_eur_ht
|
||||
status = r.status
|
||||
message_status = r.message_status
|
||||
couplages = r.couplages
|
||||
traceSelection(trace, coefficients_base, lues, tous, donnees.options, couplages)
|
||||
}
|
||||
|
||||
const sorties: SortiesIntegrateur = {
|
||||
mode,
|
||||
contexte,
|
||||
poids,
|
||||
budgets,
|
||||
axes,
|
||||
total_eur_ht: total,
|
||||
budget_cible: entrees.budget_total,
|
||||
ecart_budget_pct: entrees.budget_total > 0 ? +(((total - entrees.budget_total) / entrees.budget_total) * 100).toFixed(2) : 0,
|
||||
status,
|
||||
message_status,
|
||||
couplages,
|
||||
taux_couverture,
|
||||
}
|
||||
return trace.resultat(sorties, VERSION)
|
||||
}
|
||||
|
||||
export const integrateur: Moteur<EntreesIntegrateur, DonneesIntegrateur, SortiesIntegrateur> = {
|
||||
id: 'autonomie.integrateur',
|
||||
calculer: calculeIntegrateur,
|
||||
}
|
||||
export default integrateur
|
||||
@@ -0,0 +1,98 @@
|
||||
// adequation.ts — adequation_contexte(typo, inputs, poste, contexte_derived) → [0..1]
|
||||
// Spec: V2-algo-spec-FONCTIONS.md §1 — origine calculs/src/lib/autonomie/adequation.js.
|
||||
// Correction LOT P1.0 : `reglages` en paramètre (plus de getConfig()) ; le
|
||||
// console.warn de debug devient un champ `avertissement` du retour debug.
|
||||
|
||||
import { clamp01 } from './calibrage.js'
|
||||
import {
|
||||
score_pluviometrie,
|
||||
score_climat_chauffage,
|
||||
score_climat_meridional,
|
||||
score_taille_foyer,
|
||||
score_surface_toit,
|
||||
score_orientation_toit,
|
||||
score_surface_parcelle,
|
||||
score_temps_dispo,
|
||||
score_metres_haie,
|
||||
score_couplage_disponible,
|
||||
} from './scoreurs.js'
|
||||
import type { Autodiag, Contexte, Option, Poste, Reglages } from './types.js'
|
||||
|
||||
type Scoreur = (typo: Option, inputs: Autodiag, contexte: Contexte, poste: Poste, sel: string[], reglages: Reglages) => number
|
||||
|
||||
// Catalogue de dispatch des scoreurs par nom de critère
|
||||
const SCOREURS: Record<string, Scoreur> = {
|
||||
pluviometrie: score_pluviometrie,
|
||||
climat_chauffage: score_climat_chauffage,
|
||||
climat_meridional: score_climat_meridional,
|
||||
taille_foyer: score_taille_foyer,
|
||||
surface_toit: score_surface_toit,
|
||||
orientation_toit: score_orientation_toit,
|
||||
surface_parcelle: score_surface_parcelle,
|
||||
temps_dispo: score_temps_dispo,
|
||||
metres_haie_possibles: score_metres_haie,
|
||||
couplage_disponible: score_couplage_disponible,
|
||||
}
|
||||
|
||||
export interface AdequationDebug {
|
||||
score: number
|
||||
scoreurs: Record<string, { score: number; poids: number }>
|
||||
avertissement?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule le score d'adéquation contextuelle d'une typologie [0..1].
|
||||
*
|
||||
* @param typologies_selectionnees — sélections déjà faites (pour score_couplage_disponible)
|
||||
* @returns score ∈ [0..1] ; en debug, { score, scoreurs }
|
||||
*/
|
||||
export function adequation_contexte(typo: Option, inputs: Autodiag, poste: Poste, contexte_derived: Contexte, reglages: Reglages, typologies_selectionnees?: string[], debug?: false): number
|
||||
export function adequation_contexte(typo: Option, inputs: Autodiag, poste: Poste, contexte_derived: Contexte, reglages: Reglages, typologies_selectionnees: string[], debug: true): AdequationDebug
|
||||
export function adequation_contexte(
|
||||
typo: Option,
|
||||
inputs: Autodiag,
|
||||
poste: Poste,
|
||||
contexte_derived: Contexte,
|
||||
reglages: Reglages,
|
||||
typologies_selectionnees: string[] = [],
|
||||
debug = false,
|
||||
): number | AdequationDebug {
|
||||
const matrix = reglages.config.adequation_weights?.[poste.code]
|
||||
|
||||
if (!matrix) {
|
||||
if (debug) return { score: 0.5, scoreurs: {}, avertissement: `[adequation_contexte] Pas de matrice pour poste "${poste.code}" — fallback 0.5` }
|
||||
return 0.5
|
||||
}
|
||||
|
||||
let weighted_sum = 0
|
||||
let total_weight = 0
|
||||
const debug_scores: Record<string, { score: number; poids: number }> = {}
|
||||
|
||||
for (const [critere, poids] of Object.entries(matrix)) {
|
||||
if (poids === 0 || !SCOREURS[critere]) continue
|
||||
const scoreur = SCOREURS[critere]
|
||||
let s: number
|
||||
if (critere === 'couplage_disponible') {
|
||||
s = scoreur(typo, inputs, contexte_derived, poste, typologies_selectionnees, reglages)
|
||||
} else {
|
||||
s = scoreur(typo, inputs, contexte_derived, poste, [], reglages)
|
||||
}
|
||||
weighted_sum += s * poids
|
||||
total_weight += poids
|
||||
if (debug) debug_scores[critere] = { score: +s.toFixed(3), poids }
|
||||
}
|
||||
|
||||
let score = total_weight > 0 ? weighted_sum / total_weight : 0.5
|
||||
|
||||
// §1.4 Bonus / malus globaux
|
||||
if (typo.flag_diy && inputs.temps_dispo === 3) score *= 1.3
|
||||
if (typo.complexite_pose === 3 && inputs.temps_dispo === 1) score *= 0.5
|
||||
if (!typo.accessible_fr) score *= 0.6
|
||||
|
||||
score = clamp01(score)
|
||||
|
||||
if (debug) {
|
||||
return { score, scoreurs: debug_scores }
|
||||
}
|
||||
return score
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
// algo.ts — module principal orchestrateur des 6 étapes
|
||||
// Spec: V2-algo-spec.md + V2-algo-spec-FONCTIONS.md — origine calculs/src/lib/autonomie/algo.js.
|
||||
// Zero-dep, isomorphe navigateur/Node.
|
||||
//
|
||||
// Corrections LOT P1.0 : plus d'initConfig (les réglages passent en paramètre) ;
|
||||
// le contexte vient de commun/contexte.ts (Corse 2A/2B et DOM 971-976 corrigés,
|
||||
// une seule version) ; la sortie porte en plus `contexte` et `avertissements`.
|
||||
// C'est le pipeline COMPLET d'origine (couplages et réajustement globaux), celui
|
||||
// que calculs.trans-former.fr consommera en P1.5 sans changement de résultat.
|
||||
//
|
||||
// Découpage par axe (LOT P1.0) : la queue du pipeline (étapes 4 à 6) est
|
||||
// isolée dans `finaliseSelection`, que les moteurs d'axe réutilisent telle
|
||||
// quelle sur une sélection à un seul axe. Un moteur d'axe est donc, par
|
||||
// construction, le mode « un seul axe actif » de `selectionner`.
|
||||
|
||||
import { deriveContexte } from '../../commun/contexte.js'
|
||||
import { pondereAxes } from './pondere-axes.js'
|
||||
import { alloueBudget } from './alloue-budget.js'
|
||||
import { selectionnePoste } from './selectionne-poste.js'
|
||||
import { appliqueCouplages } from './couplages.js'
|
||||
import { reajusteBudget } from './reajuste.js'
|
||||
import type { Autodiag, Axe, Contexte, EtapeDebug, InputsSimulation, LigneClimat, Option, Poste, Reglages, Selection, SortieSelection, Statut } from './types.js'
|
||||
|
||||
export interface SelectionFinalisee {
|
||||
axes: Selection
|
||||
total_eur_ht: number
|
||||
status: Statut
|
||||
message_status?: string
|
||||
couplages: string[]
|
||||
debug_reajust: string[] | null
|
||||
}
|
||||
|
||||
const AXES_CODE: Axe[] = ['eau', 'energie', 'alim']
|
||||
|
||||
function sommeAxes(axes: Selection): number {
|
||||
return Math.round(AXES_CODE.reduce((s, axe) => s + ((axes[axe] && axes[axe].sous_total_eur_ht) || 0), 0))
|
||||
}
|
||||
|
||||
/**
|
||||
* Étapes 4 à 6 du pipeline d'origine : couplages contraints, réajustement au
|
||||
* budget, re-passe couplages bornée à deux tours, total. Ne touche pas à la
|
||||
* sélection reçue (appliqueCouplages copie).
|
||||
*/
|
||||
export function finaliseSelection(
|
||||
selection: Selection,
|
||||
budget_total: number,
|
||||
options: Option[],
|
||||
postes: Poste[],
|
||||
inputs: Autodiag,
|
||||
contexte: Contexte,
|
||||
reglages: Reglages,
|
||||
debugMode = false,
|
||||
debug?: { steps: EtapeDebug[] },
|
||||
): SelectionFinalisee {
|
||||
const config = reglages.config
|
||||
|
||||
// --- Étape 4 : couplages contraints ---
|
||||
const apres_couplages = appliqueCouplages(selection, options, postes, reglages, debugMode)
|
||||
if (debug) debug.steps.push({ etape: 4, label: 'couplages', log: apres_couplages.couplages_appliques })
|
||||
|
||||
// --- Étape 5 : réajustement budget ---
|
||||
const final_pre = reajusteBudget(apres_couplages, budget_total, options, postes, inputs, contexte, reglages, debugMode)
|
||||
if (debug) debug.steps.push({ etape: 5, label: 'reajustement', status: final_pre.status, total: final_pre.total, log: final_pre.debug_reajust })
|
||||
|
||||
// Re-passe couplages post-réajustement, avec réabsorption des coûts forcés :
|
||||
// le CAS B (underspend) peut ajouter ou upgrader des typologies ayant des
|
||||
// couplages_requis. La passe couplages force les requis manquants
|
||||
// (ex: bois_poele_hydro → bois_conduit_fumee) — coûts contraints, inclus
|
||||
// dans le total final (comportement intentionnel).
|
||||
// Correctif V3-1 (recalibrage) : ces ajouts pouvaient faire franchir le seuil
|
||||
// d'overrun SANS rééquilibrage ni changement de status (profil A : +8,9 % avec
|
||||
// status OK). Boucle bornée à 2 passes : couplages → si overrun, réajuster →
|
||||
// couplages. CAS A ne fait que réduire, la convergence est garantie ; si le
|
||||
// seuil reste franchi, reajusteBudget pose BUDGET_INSUFFISANT (status honnête).
|
||||
const tol_over = (config.tolerances && config.tolerances.budget_overrun_pct) || 5
|
||||
const seuil_over = budget_total * (1 + tol_over / 100)
|
||||
|
||||
let etat: { axes: Selection; couplages_appliques: string[] } = {
|
||||
axes: final_pre.axes,
|
||||
couplages_appliques: [...(final_pre.couplages_appliques || [])],
|
||||
}
|
||||
let status_final: Statut = final_pre.status
|
||||
let message_final = final_pre.message
|
||||
|
||||
for (let passe = 0; passe < 2; passe++) {
|
||||
// IMPORTANT: appliqueCouplages attend selection = { eau, energie, alim } directement
|
||||
const avec = appliqueCouplages(etat.axes, options, postes, reglages, false)
|
||||
etat = {
|
||||
axes: avec.axes,
|
||||
couplages_appliques: [...etat.couplages_appliques, ...(avec.couplages_appliques || [])],
|
||||
}
|
||||
if (sommeAxes(etat.axes) <= seuil_over) break
|
||||
const re = reajusteBudget(etat, budget_total, options, postes, inputs, contexte, reglages, false)
|
||||
etat = {
|
||||
axes: re.axes,
|
||||
couplages_appliques: [...etat.couplages_appliques, ...(re.couplages_appliques || [])],
|
||||
}
|
||||
status_final = re.status
|
||||
message_final = re.message
|
||||
}
|
||||
|
||||
return {
|
||||
axes: etat.axes,
|
||||
total_eur_ht: sommeAxes(etat.axes),
|
||||
status: status_final,
|
||||
message_status: message_final || undefined,
|
||||
couplages: etat.couplages_appliques,
|
||||
debug_reajust: final_pre.debug_reajust,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sélectionne les typologies optimales selon budget + contexte.
|
||||
*
|
||||
* @param inputs — saisie utilisateur (autodiag + budget_eur)
|
||||
* @param options — typologies BDD (options.json)
|
||||
* @param postes — postes BDD (postes.json)
|
||||
* @param climat — données climatiques (france_climat.json)
|
||||
* @param reglages — { config: algo-config.json parsé, coefficients }
|
||||
* @param debugMode — active la trace debug exhaustive
|
||||
*/
|
||||
export function selectionner(
|
||||
inputs: InputsSimulation,
|
||||
options: Option[],
|
||||
postes: Poste[],
|
||||
climat: LigneClimat[],
|
||||
reglages: Reglages,
|
||||
debugMode = false,
|
||||
): SortieSelection {
|
||||
const debug: { steps: EtapeDebug[] } = { steps: [] }
|
||||
const config = reglages.config
|
||||
|
||||
// --- Étape 0 : dériver le contexte depuis code postal ---
|
||||
const ctx = deriveContexte(inputs.code_postal, climat, reglages.coefficients)
|
||||
const contexte = ctx.sorties
|
||||
if (debugMode) debug.steps.push({ etape: 0, label: 'contexte', data: contexte })
|
||||
|
||||
// --- Étape 1 : pondération axes ---
|
||||
const poids_debug = debugMode ? pondereAxes(inputs, contexte, config, true) : null
|
||||
const poids = poids_debug ? poids_debug.poids : pondereAxes(inputs, contexte, config, false)
|
||||
if (poids_debug) debug.steps.push({ etape: 1, label: 'ponderation', poids, log: poids_debug.log })
|
||||
|
||||
// --- Étape 2 : allocation budget ---
|
||||
const budgets = alloueBudget(inputs.budget_eur, poids)
|
||||
if (debugMode) debug.steps.push({ etape: 2, label: 'allocation', budgets })
|
||||
|
||||
// --- Étape 3 : sélection par axe ---
|
||||
const selection: Selection = {}
|
||||
for (const axe of AXES_CODE) {
|
||||
const res = selectionnePoste(budgets[axe], inputs, contexte, options, postes, axe, reglages, debugMode)
|
||||
res.poids = poids[axe]
|
||||
selection[axe] = res
|
||||
}
|
||||
if (debugMode) debug.steps.push({ etape: 3, label: 'selection', summary: Object.fromEntries(AXES_CODE.map((a) => [a, selection[a]!.sous_total_eur_ht])) })
|
||||
|
||||
// --- Étapes 4 à 6 ---
|
||||
const fin = finaliseSelection(selection, inputs.budget_eur, options, postes, inputs, contexte, reglages, debugMode, debugMode ? debug : undefined)
|
||||
|
||||
const ecart = inputs.budget_eur > 0 ? ((fin.total_eur_ht - inputs.budget_eur) / inputs.budget_eur) * 100 : 0
|
||||
|
||||
return {
|
||||
axes: fin.axes,
|
||||
total_eur_ht: fin.total_eur_ht,
|
||||
budget_cible: inputs.budget_eur,
|
||||
ecart_budget_pct: +ecart.toFixed(2),
|
||||
status: fin.status,
|
||||
message_status: fin.message_status,
|
||||
couplages: fin.couplages,
|
||||
contexte,
|
||||
avertissements: ctx.avertissements,
|
||||
debug: debugMode ? debug : undefined,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// alloue-budget.ts — Étape 2 : allocation budget par axe
|
||||
// Spec: V2-algo-spec.md §Étape 2 — origine calculs/src/lib/autonomie/alloue-budget.js, inchangé.
|
||||
|
||||
import type { PoidsAxes } from './types.js'
|
||||
|
||||
/**
|
||||
* Alloue le budget total entre les 3 axes selon leurs poids.
|
||||
*
|
||||
* @param budget_total — EUR
|
||||
* @param poids — somme = 100
|
||||
* @returns en EUR
|
||||
*/
|
||||
export function alloueBudget(budget_total: number, poids: PoidsAxes): PoidsAxes {
|
||||
return {
|
||||
eau: (budget_total * poids.eau) / 100,
|
||||
energie: (budget_total * poids.energie) / 100,
|
||||
alim: (budget_total * poids.alim) / 100,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// axes-actifs.ts — Toggles d'axes (V3 vague 1, M2)
|
||||
// Produit une config dérivée où les axes désactivés ont un poids nul,
|
||||
// sans toucher aux modules cœur de l'algo. La désactivation passe
|
||||
// uniquement par les inputs/config (poids), conformément à la spec.
|
||||
// Origine calculs/src/lib/autonomie/axes-actifs.js, inchangé (typé).
|
||||
|
||||
import type { AlgoConfig, Axe } from './types.js'
|
||||
import { AXES } from './types.js'
|
||||
|
||||
export type AxesActifs = Partial<Record<Axe, boolean>>
|
||||
|
||||
/**
|
||||
* Transforme algo-config pour neutraliser les axes désactivés.
|
||||
* - poids_axes_base[axe] = 0 pour tout axe inactif
|
||||
* - tous les deltas de modulateurs référençant un axe inactif sont retirés
|
||||
* - marqueur `_axes_desactives` (lu par reajuste.ts pour ne pas réinjecter
|
||||
* des postes optionnels dans un axe éteint lors du CAS B underspend)
|
||||
*
|
||||
* @param config — algo-config.json parsé (non muté)
|
||||
* @returns nouvelle config
|
||||
*/
|
||||
export function configPourAxes(config: AlgoConfig, actifs: AxesActifs | null | undefined): AlgoConfig {
|
||||
const desactives = AXES.filter((a) => actifs && actifs[a] === false)
|
||||
if (desactives.length === 0) return config
|
||||
|
||||
// Clone profond (config petite, JSON-safe)
|
||||
const cfg = JSON.parse(JSON.stringify(config)) as AlgoConfig
|
||||
|
||||
cfg.poids_axes_base = cfg.poids_axes_base || { eau: 40, energie: 40, alim: 20 }
|
||||
for (const axe of desactives) cfg.poids_axes_base[axe] = 0
|
||||
|
||||
// Neutraliser les deltas des modulateurs qui re-donneraient du poids
|
||||
// à un axe désactivé (ex : zone MED -> eau+10, parcelle grande -> alim+10).
|
||||
const mod = cfg.modulateurs || {}
|
||||
for (const key of Object.keys(mod)) {
|
||||
const val = mod[key]
|
||||
if (val && typeof val === 'object' && !Array.isArray(val)) {
|
||||
for (const axe of desactives) {
|
||||
if (axe in (val as Record<string, unknown>)) delete (val as Record<string, unknown>)[axe]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cfg._axes_desactives = desactives
|
||||
return cfg
|
||||
}
|
||||
|
||||
/**
|
||||
* Vrai si au moins un axe est actif (0 axe actif = état vide géré par l'UI,
|
||||
* l'algo ne doit pas tourner).
|
||||
*/
|
||||
export function auMoinsUnAxe(actifs: AxesActifs | null | undefined): boolean {
|
||||
return AXES.some((a) => !actifs || actifs[a] !== false)
|
||||
}
|
||||
|
||||
export { AXES }
|
||||
@@ -0,0 +1,20 @@
|
||||
// calibrage.ts — helpers numériques du noyau.
|
||||
//
|
||||
// Origine : calculs/src/lib/autonomie/calibrage.js. Correction LOT P1.0 : le
|
||||
// singleton `_config` (initConfig / getConfig) est retiré, la configuration
|
||||
// passe en paramètre (Reglages) chez les cinq appelants. ORIENTATION_FACTORS et
|
||||
// DJU_DEFAULTS_BY_ZONE vivent désormais dans commun/coefficients.ts (une seule
|
||||
// définition, sourcée) ; ZONE_BIOGEO_MAP et zoneRtToClass dans commun/contexte.ts.
|
||||
|
||||
export { ZONE_BIOGEO_MAP, zoneRtToClass } from '../../commun/contexte.js'
|
||||
|
||||
export function clamp01(x: number): number {
|
||||
return Math.max(0, Math.min(1, x))
|
||||
}
|
||||
export function clamp(x: number, min: number, max: number): number {
|
||||
return Math.max(min, Math.min(max, x))
|
||||
}
|
||||
export function roundToStep(value: number, step: number): number {
|
||||
if (!step || step <= 0) return value
|
||||
return Math.round(value / step) * step
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
// couplages.ts — Étape 4 : application des couplages contraints
|
||||
// Spec: V2-algo-spec.md §Étape 4 — origine calculs/src/lib/autonomie/couplages.js.
|
||||
// Corrections LOT P1.0 : `reglages` en paramètre (plus de getConfig()) ; la
|
||||
// sélection reçue n'est PLUS mutée (copie des axes et de leurs postes, le
|
||||
// résultat est renvoyé dans `axes`). Tous les appelants d'origine lisaient
|
||||
// déjà le retour, le comportement est identique.
|
||||
//
|
||||
// Note (origine, conservée) : le marqueur `_conflit_avec` de la passe 2 est
|
||||
// posé sur des copies de postes et n'atteint jamais la sortie ; le
|
||||
// « downgrade planifié » n'a donc jamais lieu. À corriger en patch, avec test.
|
||||
|
||||
import { cout_typologie, dimension_optimale } from './dimension.js'
|
||||
import type { Axe, AxeResultat, Option, Poste, PosteResultat, Reglages, Selection } from './types.js'
|
||||
|
||||
export interface ResultatCouplages {
|
||||
axes: Selection
|
||||
couplages_appliques: string[]
|
||||
}
|
||||
|
||||
function copieSelection(selection: Selection): Selection {
|
||||
const copie: Selection = {}
|
||||
for (const axe of Object.keys(selection) as Axe[]) {
|
||||
const a = selection[axe]
|
||||
if (!a) continue
|
||||
const ax: AxeResultat = { ...a, postes: a.postes.map((p) => ({ ...p })) }
|
||||
copie[axe] = ax
|
||||
}
|
||||
return copie
|
||||
}
|
||||
|
||||
/**
|
||||
* Post-traitement déterministe des couplages.
|
||||
*
|
||||
* Gestion alias : algo-config.json § couplages_alias permet de mapper
|
||||
* des codes legacy (ex: stherm_ballon_tampon) vers les codes BDD réels (th_ballon_500).
|
||||
*/
|
||||
export function appliqueCouplages(selection_recue: Selection, options: Option[], postes: Poste[] | null | undefined, reglages: Reglages, _debug = false): ResultatCouplages {
|
||||
const alias = reglages.config.couplages_alias || {}
|
||||
const selection = copieSelection(selection_recue)
|
||||
|
||||
// Construire un mapping poste_id (num) → axe via postes BDD
|
||||
const posteIdToAxe: Record<number, Axe> = {}
|
||||
if (postes) {
|
||||
for (const p of postes) {
|
||||
// Normaliser axe : "alimentation" → "alim"
|
||||
const axe_norm = (p.axe === 'alimentation' ? 'alim' : p.axe) as Axe
|
||||
posteIdToAxe[p.Id] = axe_norm
|
||||
}
|
||||
}
|
||||
|
||||
function resolveCode(code: string): string {
|
||||
return alias[code] || code
|
||||
}
|
||||
|
||||
const axes_list: Axe[] = ['eau', 'energie', 'alim']
|
||||
const tous_postes: PosteResultat[] = axes_list.flatMap((axe) =>
|
||||
(selection[axe] && selection[axe].postes ? selection[axe].postes : []).map((p) => Object.assign({}, p, { _axe: axe })),
|
||||
)
|
||||
|
||||
const codes_selectionnes = new Set(tous_postes.map((p) => p.typologie_choisie))
|
||||
const couplages_log: string[] = []
|
||||
|
||||
// Passe 1 : couplages_requis
|
||||
for (const poste of tous_postes) {
|
||||
const requis_list = poste.couplages_requis || []
|
||||
for (const requis_raw of requis_list) {
|
||||
const requis = resolveCode(requis_raw)
|
||||
if (!codes_selectionnes.has(requis)) {
|
||||
// Chercher la typo en BDD (code exact ou alias original)
|
||||
let typo_requise = options.find((o) => o.code === requis)
|
||||
if (!typo_requise && requis !== requis_raw) {
|
||||
typo_requise = options.find((o) => o.code === requis_raw)
|
||||
}
|
||||
|
||||
if (typo_requise) {
|
||||
// Trouver l'axe via mapping poste_id → axe
|
||||
const axe_cible = posteIdToAxe[typo_requise.poste_id] || trouverAxeParCode(typo_requise.code)
|
||||
const cible = axe_cible ? selection[axe_cible] : undefined
|
||||
|
||||
if (axe_cible && cible) {
|
||||
const dim = dimension_optimale(typo_requise, {}, {}, reglages.coefficients)
|
||||
const cout = cout_typologie(typo_requise, dim, {})
|
||||
const cout_force = Math.round(cout || 0)
|
||||
const unite_dim = dim ? dim.unite : typo_requise.unite || ''
|
||||
const valeur_dim = dim ? dim.valeur : typo_requise.quantite_default || 1
|
||||
|
||||
cible.postes.push({
|
||||
poste_id: String(typo_requise.poste_id) + '_couplage',
|
||||
poste_nom: 'Couplage requis (' + typo_requise.nom + ')',
|
||||
obligatoire: true,
|
||||
budget_alloue_poste: cout_force,
|
||||
typologie_choisie: typo_requise.code,
|
||||
nom_affichage: typo_requise.nom,
|
||||
description_pedago: typo_requise.description_pedago || '',
|
||||
dimension: { valeur: valeur_dim, unite: unite_dim },
|
||||
cout_eur_ht: cout_force,
|
||||
score_adequation: 0.8,
|
||||
score_final: 0,
|
||||
alternatives: [],
|
||||
notes_techniques: typo_requise.notes_techniques || '',
|
||||
flag_diy: typo_requise.flag_diy || false,
|
||||
complexite_pose: typo_requise.complexite_pose || 2,
|
||||
couplages_requis: [],
|
||||
couplages_exclus: [],
|
||||
manuel: false,
|
||||
force_par_couplage: true,
|
||||
})
|
||||
cible.sous_total_eur_ht += cout_force
|
||||
codes_selectionnes.add(typo_requise.code)
|
||||
couplages_log.push('FORCE "' + typo_requise.code + '" (requis par "' + poste.typologie_choisie + '") +' + cout_force + 'EUR dans axe ' + axe_cible)
|
||||
} else {
|
||||
couplages_log.push('WARN axe non trouve pour "' + requis + '" (requis par "' + poste.typologie_choisie + '")')
|
||||
}
|
||||
} else {
|
||||
couplages_log.push('MANQUANT "' + requis + '" (requis par "' + poste.typologie_choisie + '") — code BDD non trouve')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Passe 2 : couplages_exclus
|
||||
for (const poste of tous_postes) {
|
||||
const exclus_list = poste.couplages_exclus || []
|
||||
for (const exclu_raw of exclus_list) {
|
||||
const exclu = resolveCode(exclu_raw)
|
||||
if (codes_selectionnes.has(exclu)) {
|
||||
const poste_conflit = tous_postes.find((p) => p.typologie_choisie === exclu)
|
||||
if (poste_conflit && !poste_conflit.force_par_couplage) {
|
||||
poste_conflit._conflit_avec = poste.typologie_choisie
|
||||
couplages_log.push('CONFLIT "' + exclu + '" avec "' + poste.typologie_choisie + '" — downgrade planifié')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { axes: selection, couplages_appliques: couplages_log }
|
||||
}
|
||||
|
||||
// Fallback heuristique si poste_id absent du mapping
|
||||
function trouverAxeParCode(code: string): Axe {
|
||||
if (/captage|stockage|filtrat|distrib|eg_|stherm_ballon/.test(code)) return 'eau'
|
||||
if (/^th_|^pv_|^bois_|^pil_|^emet_|ballon_tampon|conduit_fumee/.test(code)) return 'energie'
|
||||
if (/^etude_|^haute_|^moy_|^basse_/.test(code)) return 'alim'
|
||||
return 'energie'
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
// dimension.ts — dimension_optimale(typo, inputs, contexte_derived, coefficients) → Dimension | null
|
||||
// Spec: V2-algo-spec-FONCTIONS.md §2 — origine calculs/src/lib/autonomie/dimension.js.
|
||||
// Correction LOT P1.0 : facteurs d'orientation et DJU par défaut lus dans la
|
||||
// table de coefficients passée en paramètre (plus de constante de module).
|
||||
|
||||
import { zoneRtToClass, clamp, roundToStep } from './calibrage.js'
|
||||
import type { TableCoefficients } from '../../commun/coefficients.js'
|
||||
import type { Autodiag, Contexte, Dimension, Option } from './types.js'
|
||||
import { parseParam } from './types.js'
|
||||
|
||||
/**
|
||||
* Calcule la valeur optimale du paramètre dimensionnant pour une typologie.
|
||||
*
|
||||
* Particularité ballon_tampon (th_ballon_500) :
|
||||
* - unite BDD = "forfait" (contrainte schéma NocoDB)
|
||||
* - prix_par_unite = 1.2 EUR/L
|
||||
* - parametre_dim.unite = "L" (source de vérité pour l'affichage et le calcul)
|
||||
* → L'algo utilise toujours parametre_dim.unite quand présent, sinon fallback option.unite
|
||||
*/
|
||||
export function dimension_optimale(
|
||||
typo: Option,
|
||||
inputs: Partial<Autodiag>,
|
||||
contexte_derived: Partial<Contexte>,
|
||||
coefficients: TableCoefficients,
|
||||
): Dimension | null {
|
||||
const param = parseParam(typo.parametre_dim)
|
||||
if (!param) return null
|
||||
|
||||
const strategie = param.strategie || 'fixe'
|
||||
let valeur: number
|
||||
|
||||
switch (strategie) {
|
||||
case 'par_personne': {
|
||||
const base = param.base != null ? param.base : 0
|
||||
const coef = param.coef != null ? param.coef : 1
|
||||
valeur = base + (inputs.nb_personnes || 4) * coef
|
||||
break
|
||||
}
|
||||
|
||||
case 'par_surface_toit': {
|
||||
const table = coefficients.orientation_pv.valeur as Record<string, number>
|
||||
const orient = (inputs.orientation_toit ? table[inputs.orientation_toit] : undefined) || 0.8
|
||||
const coef = param.coef != null ? param.coef : 0.1
|
||||
valeur = (inputs.surface_toit_m2 || 0) * coef * orient
|
||||
break
|
||||
}
|
||||
|
||||
case 'par_surface_parcelle': {
|
||||
const reserve = param.reserve_construit != null ? param.reserve_construit : 100
|
||||
const utile = Math.max(0, (inputs.surface_parcelle_m2 || 0) - reserve)
|
||||
const coef = param.coef != null ? param.coef : 0.05
|
||||
valeur = utile * coef
|
||||
break
|
||||
}
|
||||
|
||||
case 'par_dju': {
|
||||
const zone_class = zoneRtToClass(contexte_derived.zone_climatique_rt)
|
||||
const dju = contexte_derived.dju || coefficients.dju_defaut_par_classe_rt.valeur[zone_class] || 2200
|
||||
const coef = param.coef != null ? param.coef : 0.005
|
||||
valeur = dju * coef
|
||||
break
|
||||
}
|
||||
|
||||
case 'par_metres_haie': {
|
||||
const coef = param.coef != null ? param.coef : 1.0
|
||||
valeur = (inputs.metres_haie_possibles || 0) * coef
|
||||
break
|
||||
}
|
||||
|
||||
case 'fixe':
|
||||
default:
|
||||
valeur = param.default != null ? param.default : 1
|
||||
break
|
||||
}
|
||||
|
||||
// Clamp + arrondi step
|
||||
const min = param.min != null ? param.min : 0
|
||||
const max = param.max != null ? param.max : Infinity
|
||||
const step = param.step != null ? param.step : 0
|
||||
valeur = clamp(valeur, min, max)
|
||||
valeur = roundToStep(valeur, step)
|
||||
|
||||
// Unite : parametre_dim.unite prioritaire sur option.unite (cas ballon_tampon)
|
||||
const unite = param.unite || typo.unite || ''
|
||||
|
||||
return { valeur, unite, strategie }
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule le coût HT d'une typologie pour une dimension donnée.
|
||||
*
|
||||
* Hiérarchie des prix (3 cas BDD) :
|
||||
* 1. prix_fixe + prix_par_unite × dim.valeur (V2 enrichi, ex: cuve béton, PV)
|
||||
* 2. prix_par_unite × dim.valeur seul (ex: th_ballon_500)
|
||||
* 3. prix_mat_min + pose si pro requis (forfaits legacy sans parametre_dim enrichi)
|
||||
*
|
||||
* Pour les postes pro (complexite_pose=3) et temps_dispo < 3, ajouter prix_pro_min.
|
||||
* Fallback final : (prix_mat_min + prix_mat_max) / 2
|
||||
*
|
||||
* Les prix sont des DONNÉES de l'option (publiées par calculs) ; la seule
|
||||
* heuristique du code est « pose pro = 50 % du matériel si prix_pro absent ».
|
||||
*/
|
||||
export function cout_typologie(typo: Option, dim_result: Dimension | null | undefined, inputs?: Partial<Autodiag> | null): number {
|
||||
const prix_fixe = typo.prix_fixe != null ? typo.prix_fixe : 0
|
||||
const prix_par_unite = typo.prix_par_unite
|
||||
const prix_mat_min = typo.prix_mat_min != null ? typo.prix_mat_min : 0
|
||||
const prix_mat_max = typo.prix_mat_max != null ? typo.prix_mat_max : prix_mat_min
|
||||
const prix_pro_min = typo.prix_pro_min != null ? typo.prix_pro_min : 0
|
||||
|
||||
// Cas 1 & 2 : prix_par_unite disponible (schéma V2 enrichi)
|
||||
if (prix_par_unite != null && prix_par_unite > 0) {
|
||||
const dim_val = dim_result ? dim_result.valeur : typo.quantite_default || 1
|
||||
const base = prix_fixe + prix_par_unite * dim_val
|
||||
// Forfait pro si complexite=3 et temps_dispo=1
|
||||
if (typo.complexite_pose === 3 && inputs && inputs.temps_dispo === 1 && prix_pro_min > 0) {
|
||||
return Math.max(base, prix_pro_min)
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
// Cas 3 : forfait legacy (prix_mat_min/max seulement)
|
||||
if (prix_mat_min > 0) {
|
||||
const mid_mat = (prix_mat_min + prix_mat_max) / 2
|
||||
// Si complexite_pose = 3 et pose pro : ajouter coût pose pro
|
||||
if (typo.complexite_pose === 3) {
|
||||
const pose = prix_pro_min > 0 ? prix_pro_min : mid_mat * 0.5
|
||||
return mid_mat + pose
|
||||
}
|
||||
// complexite 1-2 : coût mat seul (DIY inclus)
|
||||
return mid_mat
|
||||
}
|
||||
|
||||
// Fallback ultime : 0 (sécurité, ne devrait pas arriver)
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// @transformer/moteurs/autonomie/noyau — le cœur V2 du simulateur autonomie,
|
||||
// pipeline complet (celui de calculs.trans-former.fr), sans singleton ni I/O.
|
||||
export { selectionner } from './algo.js'
|
||||
export { dimension_optimale, cout_typologie } from './dimension.js'
|
||||
export { adequation_contexte } from './adequation.js'
|
||||
export type { AdequationDebug } from './adequation.js'
|
||||
export { pondereAxes } from './pondere-axes.js'
|
||||
export type { PoidsDebug } from './pondere-axes.js'
|
||||
export { alloueBudget } from './alloue-budget.js'
|
||||
export { selectionnePoste } from './selectionne-poste.js'
|
||||
export { appliqueCouplages } from './couplages.js'
|
||||
export type { ResultatCouplages } from './couplages.js'
|
||||
export { reajusteBudget } from './reajuste.js'
|
||||
export type { EntreeReajuste, ResultatReajuste } from './reajuste.js'
|
||||
export { configPourAxes, auMoinsUnAxe } from './axes-actifs.js'
|
||||
export type { AxesActifs } from './axes-actifs.js'
|
||||
export { clamp01, clamp, roundToStep, ZONE_BIOGEO_MAP, zoneRtToClass } from './calibrage.js'
|
||||
export * from './scoreurs.js'
|
||||
export { AXES, parseJsonArr, parseParam } from './types.js'
|
||||
export type {
|
||||
Axe,
|
||||
Autodiag,
|
||||
InputsSimulation,
|
||||
Poste,
|
||||
Option,
|
||||
ParamDim,
|
||||
AlgoConfig,
|
||||
Reglages,
|
||||
Dimension,
|
||||
Alternative,
|
||||
PosteResultat,
|
||||
AxeResultat,
|
||||
Selection,
|
||||
Statut,
|
||||
PoidsAxes,
|
||||
EtapeDebug,
|
||||
SortieSelection,
|
||||
Contexte,
|
||||
LigneClimat,
|
||||
Orientation,
|
||||
} from './types.js'
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user