@open3cl/engine 1.8.57 → 1.8.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,73 @@
1
+ /**
2
+ * DPE appartement généré à partir d'un DPE immeuble (méthodes 10 à 13, 33-34, 38-40).
3
+ *
4
+ * Certains logiciels (LICIEL / BBS_Slama) exportent une installation individuelle décrite sur la
5
+ * surface de l'immeuble entier (Sh_i ≈ SH immeuble) avec un `rdim` égal à 1, alors que le calcul
6
+ * (Tribu) a été fait avec un Rdim implicite égal au nombre de logements desservis. La consommation
7
+ * sortie ADEME vaut alors : clé × Σ C_i × (na × Sh_i / SH).
8
+ *
9
+ * En mode `bug_for_bug_compat`, on reconstitue ce rdim (`rdim = na × Sh_i / SH`), on calcule
10
+ * l'installation à l'échelle de l'immeuble, puis on applique la clé de répartition exportée :
11
+ * - chauffage individuel, gestion homogène : coef_IFC = 1 → Cch_ap = Clé_ap × Cch (et Caux_ch) ;
12
+ * - ECS : Cecs_ap = Cecs × Becs_ap / Becs (chaîne existante cle_ecs × rdim).
13
+ *
14
+ * @see Methode_de_calcul_3CL_DPE_2021-338.pdf - § 17.2.2.2.2 (p. 115) et § 17.2.2.3.1 (p. 116)
15
+ * @see Tribu : Calcul_installation.cs l. 62 (Rdim lu en entrée), Calcul_batiment.cs l. 836-958
16
+ * (conso = Cch1 × Rdim) et l. 1560 (Cdimref = Pn × Rdim)
17
+ * @see https://github.com/Open3CL/engine/issues/287
18
+ */
19
+
20
+ /** Ratio na × Sh_i / SH au-delà duquel l'installation est considérée comme « immeuble » */
21
+ export const SEUIL_RDIM_IMPLICITE = 1.5;
22
+
23
+ /**
24
+ * Rdim implicite d'une installation individuelle exportée avec rdim = 1, ou null si l'installation
25
+ * n'est pas concernée.
26
+ *
27
+ * @param de {object} donnee_entree de l'installation (chauffage ou ECS)
28
+ * @param surface {number|string} surface desservie par l'installation (Sh_i)
29
+ * @param nombreAppartement {number|string} nombre de logements de l'immeuble (na)
30
+ * @param surfaceHabitableImmeuble {number|string} surface habitable de l'immeuble (SH)
31
+ * @return {number|null}
32
+ */
33
+ export function rdimImplicite(de, surface, nombreAppartement, surfaceHabitableImmeuble) {
34
+ if (!de || de.enum_type_installation_id !== '1') return null;
35
+ if ((Number(de.rdim) || 1) !== 1) return null;
36
+
37
+ const na = Number(nombreAppartement);
38
+ const SH = Number(surfaceHabitableImmeuble);
39
+ const Shi = Number(surface);
40
+ if (!(na > 1) || !(SH > 0) || !(Shi > 0)) return null;
41
+
42
+ const rdim = (na * Shi) / SH;
43
+ return rdim > SEUIL_RDIM_IMPLICITE ? rdim : null;
44
+ }
45
+
46
+ /**
47
+ * Reconstitue le rdim des installations individuelles d'un DPE appartement généré depuis un DPE
48
+ * immeuble. Les installations concernées reçoivent `rdim` reconstitué et le flag `rdim_reconstitue`.
49
+ * Chauffage et ECS sont traités indépendamment.
50
+ *
51
+ * @param cg {object} caracteristique_generale
52
+ * @param instal_ch {Array} installation_chauffage
53
+ * @param instal_ecs {Array} installation_ecs
54
+ * @return {{ch: boolean, ecs: boolean}} installations reconstituées
55
+ */
56
+ export function reconstituerRdimDpeGenereImmeuble(cg, instal_ch, instal_ecs) {
57
+ const na = cg?.nombre_appartement;
58
+ const SH = cg?.surface_habitable_immeuble;
59
+
60
+ const appliquer = (installations, champSurface) =>
61
+ (installations || []).reduce((found, inst) => {
62
+ const de = inst.donnee_entree;
63
+ const rdim = rdimImplicite(de, de?.[champSurface], na, SH);
64
+ if (rdim === null) return found;
65
+ de.rdim = rdim;
66
+ de.rdim_reconstitue = true;
67
+ return true;
68
+ }, false);
69
+
70
+ const ch = appliquer(instal_ch, 'surface_chauffee');
71
+ const ecs = appliquer(instal_ecs, 'surface_habitable');
72
+ return { ch, ecs };
73
+ }
@@ -0,0 +1,100 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import {
3
+ rdimImplicite,
4
+ reconstituerRdimDpeGenereImmeuble,
5
+ SEUIL_RDIM_IMPLICITE
6
+ } from './17.2_rdim_dpe_genere_immeuble.js';
7
+
8
+ /**
9
+ * @see Methode_de_calcul_3CL_DPE_2021-338.pdf - § 17.2.2.2.2 et § 17.2.2.3.1
10
+ * @see https://github.com/Open3CL/engine/issues/287
11
+ */
12
+ describe('Reconstitution du rdim des DPE appartement générés depuis un DPE immeuble', () => {
13
+ describe('rdimImplicite', () => {
14
+ test('installation individuelle décrite sur la surface de l’immeuble : rdim = na × Sh_i / SH', () => {
15
+ // Cas 2694E0022738J : na = 62, Sh_i = SH = 4 094 m² → rdim = 62
16
+ expect(rdimImplicite({ enum_type_installation_id: '1', rdim: 1 }, 4094, 62, 4094)).toBe(62);
17
+ expect(rdimImplicite({ enum_type_installation_id: '1' }, 2047, '62', '4094')).toBe(31);
18
+ });
19
+
20
+ test('installation collective : non concernée', () => {
21
+ expect(rdimImplicite({ enum_type_installation_id: '2', rdim: 1 }, 4094, 62, 4094)).toBeNull();
22
+ });
23
+
24
+ test('rdim exporté différent de 1 : on conserve la saisie', () => {
25
+ expect(rdimImplicite({ enum_type_installation_id: '1', rdim: 4 }, 4094, 62, 4094)).toBeNull();
26
+ });
27
+
28
+ test('ratio na × Sh_i / SH inférieur ou égal au seuil : installation d’un seul logement', () => {
29
+ expect(SEUIL_RDIM_IMPLICITE).toBe(1.5);
30
+ // 10 × 60 / 600 = 1 → logement seul
31
+ expect(rdimImplicite({ enum_type_installation_id: '1' }, 60, 10, 600)).toBeNull();
32
+ // 10 × 90 / 600 = 1,5 → seuil non dépassé
33
+ expect(rdimImplicite({ enum_type_installation_id: '1' }, 90, 10, 600)).toBeNull();
34
+ // 10 × 91 / 600 > 1,5
35
+ expect(rdimImplicite({ enum_type_installation_id: '1' }, 91, 10, 600)).toBeCloseTo(
36
+ 1.516666667,
37
+ 9
38
+ );
39
+ });
40
+
41
+ test('données manquantes ou un seul logement : non concerné', () => {
42
+ const de = { enum_type_installation_id: '1' };
43
+ expect(rdimImplicite(undefined, 100, 10, 100)).toBeNull();
44
+ expect(rdimImplicite(de, 100, 1, 100)).toBeNull();
45
+ expect(rdimImplicite(de, 100, undefined, 100)).toBeNull();
46
+ expect(rdimImplicite(de, 100, 10, 0)).toBeNull();
47
+ expect(rdimImplicite(de, undefined, 10, 100)).toBeNull();
48
+ });
49
+ });
50
+
51
+ describe('reconstituerRdimDpeGenereImmeuble', () => {
52
+ const cg = { nombre_appartement: 62, surface_habitable_immeuble: 4094 };
53
+ const ch = (surface, type = '1') => ({
54
+ donnee_entree: { enum_type_installation_id: type, rdim: 1, surface_chauffee: surface }
55
+ });
56
+ const ecs = (surface, type = '1') => ({
57
+ donnee_entree: { enum_type_installation_id: type, rdim: 1, surface_habitable: surface }
58
+ });
59
+
60
+ test('chauffage et ECS décrits à l’échelle de l’immeuble : rdim reconstitué sur les deux', () => {
61
+ const instCh = [ch(4094)];
62
+ const instEcs = [ecs(4094)];
63
+ expect(reconstituerRdimDpeGenereImmeuble(cg, instCh, instEcs)).toEqual({
64
+ ch: true,
65
+ ecs: true
66
+ });
67
+ expect(instCh[0].donnee_entree).toMatchObject({ rdim: 62, rdim_reconstitue: true });
68
+ expect(instEcs[0].donnee_entree).toMatchObject({ rdim: 62, rdim_reconstitue: true });
69
+ });
70
+
71
+ test('ECS seule à l’échelle de l’immeuble : rdim reconstitué sur l’ECS uniquement', () => {
72
+ const instCh = [ch(63.8)];
73
+ const instEcs = [ecs(4094)];
74
+ expect(reconstituerRdimDpeGenereImmeuble(cg, instCh, instEcs)).toEqual({
75
+ ch: false,
76
+ ecs: true
77
+ });
78
+ expect(instCh[0].donnee_entree.rdim_reconstitue).toBeUndefined();
79
+ expect(instEcs[0].donnee_entree).toMatchObject({ rdim: 62, rdim_reconstitue: true });
80
+ });
81
+
82
+ test('seules les installations concernées sont modifiées', () => {
83
+ const instCh = [ch(4094), ch(4094, '2'), ch(63.8)];
84
+ expect(reconstituerRdimDpeGenereImmeuble(cg, instCh, undefined)).toEqual({
85
+ ch: true,
86
+ ecs: false
87
+ });
88
+ expect(instCh[0].donnee_entree.rdim_reconstitue).toBe(true);
89
+ expect(instCh[1].donnee_entree.rdim_reconstitue).toBeUndefined();
90
+ expect(instCh[2].donnee_entree.rdim_reconstitue).toBeUndefined();
91
+ });
92
+
93
+ test('collections absentes', () => {
94
+ expect(reconstituerRdimDpeGenereImmeuble(undefined, undefined, undefined)).toEqual({
95
+ ch: false,
96
+ ecs: false
97
+ });
98
+ });
99
+ });
100
+ });
package/9_chauffage.js CHANGED
@@ -71,7 +71,9 @@ export default function calc_chauffage(
71
71
  genChDe.cle_repartition_ch = de.cle_repartition_ch || 1;
72
72
  genChDe.enum_type_installation_id = de.enum_type_installation_id;
73
73
  genChDe.surface_chauffee = de.surface_chauffee || Sh;
74
- genChDe.rdim_installation_ch = isImmeuble(map_id) ? Number(de.rdim) || 1 : 1;
74
+ genChDe.rdim_installation_ch =
75
+ isImmeuble(map_id) || de.rdim_reconstitue ? Number(de.rdim) || 1 : 1;
76
+ genChDe.rdim_reconstitue = de.rdim_reconstitue;
75
77
  genChDe.nombre_niveau_installation_ch = de.nombre_niveau_installation_ch || 1;
76
78
  genChDe.fch = Fch || 0.5;
77
79
 
@@ -278,6 +278,15 @@ describe('calc_chauffage - rdim de l’installation transmis au générateur', (
278
278
  appel(ch, { map_id: 2 });
279
279
  expect(rdimGen(ch)).toBe(1);
280
280
  });
281
+
282
+ test('DPE appartement issu d’un DPE immeuble avec rdim reconstitué (issue #287) : rdim repris', () => {
283
+ const ch = installation([generateur()], [emetteur('1')], { rdim: 62, rdim_reconstitue: true });
284
+ appel(ch, { map_id: 10 });
285
+ expect(rdimGen(ch)).toBe(62);
286
+ expect(
287
+ ch.generateur_chauffage_collection.generateur_chauffage[0].donnee_entree.rdim_reconstitue
288
+ ).toBe(true);
289
+ });
281
290
  });
282
291
 
283
292
  describe('isImmeuble', () => {
package/README.md CHANGED
@@ -23,7 +23,7 @@ Implémentation open source du moteur Open3CL de l'ADEME.
23
23
  ·
24
24
  <a href="https://github.com/Open3CL/engine/issues/new?labels=enhancement&template=feature-request---.md">Créer une feature</a>
25
25
  &middot;
26
- <a href="https://open3cl.github.io/engine/reports/corpus">Rapports de corpus</a>
26
+ <a href="https://open3cl.github.io/engine/">🏠 Démo 3D en ligne</a>
27
27
  </p>
28
28
  </div>
29
29
 
@@ -33,6 +33,21 @@ Implémentation open source du moteur Open3CL de l'ADEME.
33
33
 
34
34
  </div>
35
35
 
36
+ <br />
37
+
38
+ <div align="center">
39
+
40
+ ### 🏠 Voir un DPE en 3D
41
+
42
+ **[➡️ Ouvrir la démo 3D en ligne](https://open3cl.github.io/engine/)**
43
+
44
+ Chargez le DPE d'un logement : Open3CL le recalcule dans le navigateur et en reconstruit la maquette 3D.
45
+ Chaque paroi, fenêtre et équipement s'affiche avec ses déperditions — consultables et modifiables en direct.
46
+
47
+ [open3cl_3D_demo_resized.webm](https://github.com/user-attachments/assets/534a7f78-a493-41b7-9f73-128076349d84)
48
+
49
+ </div>
50
+
36
51
  <details>
37
52
  <summary>📚 Sommaire</summary>
38
53
  <ol>
@@ -92,7 +107,7 @@ consommations, émissions de gaz à effet de serre, coûts et étiquettes.
92
107
  | 🔍 **Vérifiable** | ~90 000 DPE réels rejoués à chaque version, résultats publiés (voir [Résultats corpus](#résultats-corpus)) |
93
108
  | 🧩 **Sans dépendance** | Pure JavaScript (ESM), aucun service externe, fonctionne en Node.js comme dans un navigateur |
94
109
  | 📦 **Prête à intégrer** | Entrée XML ADEME ou objet JSON, sortie JSON complète |
95
- | 🆓 **Libre** | Licence GPL-3.0, développée au grand jour |
110
+ | 🆓 **Libre** | Licence MIT, développée au grand jour |
96
111
 
97
112
  ### Ce que fait le moteur
98
113
 
@@ -139,11 +154,11 @@ flowchart LR
139
154
 
140
155
  ### L'écosystème Open3CL
141
156
 
142
- | Ressource | Description |
143
- | :-------------------------------------------------------------------- | :-------------------------------------------------------- |
144
- | [open3cl.fr](https://open3cl.fr) | Le site du projet |
145
- | [Rapports de corpus](https://open3cl.github.io/engine/reports/corpus) | Le tableau de bord interactif des résultats sur DPE réels |
146
- | [@open3cl/engine](https://www.npmjs.com/package/@open3cl/engine) | Le paquet npm |
157
+ | Ressource | Description |
158
+ | :--------------------------------------------------------------- | :------------------------------------------------------------------------- |
159
+ | [Démo 3D](https://open3cl.github.io/engine/) | Visualisation 3D des déperditions d'un DPE, recalculées dans le navigateur |
160
+ | [open3cl.fr](https://open3cl.fr) | Le site du projet |
161
+ | [@open3cl/engine](https://www.npmjs.com/package/@open3cl/engine) | Le paquet npm |
147
162
 
148
163
  <p align="right">(<a href="#readme-top">Retour sommaire</a>)</p>
149
164
 
@@ -318,24 +333,14 @@ d'environnement, quotas de l'API ADEME, structure des rapports.
318
333
  ### Le rapport interactif
319
334
 
320
335
  Chaque exécution produit un tableau de bord HTML : jauge de réussite, ratio par contrôle, liste des DPE au-dessus du
321
- seuil avec le **détail des propriétés en écart**, et comparaison entre deux branches.
336
+ seuil avec le **détail des propriétés en écart**, et comparaison entre deux branches. Il est écrit dans
337
+ `dist/reports/corpus` et se consulte en local :
322
338
 
323
- <div align="center">
324
- <a href="https://open3cl.github.io/engine/reports/corpus">
325
- <img src="images/corpus-report.png" alt="Rapport de corpus Open3CL" width="900">
326
- </a>
327
- <br/>
328
- <sub>👉 <a href="https://open3cl.github.io/engine/reports/corpus"><strong>Ouvrir le rapport interactif</strong></a></sub>
329
- </div>
339
+ ```sh
340
+ npm run reports:preview # sert dist/reports/corpus et ouvre le navigateur
341
+ ```
330
342
 
331
- > [!NOTE]
332
- > GitHub neutralise les scripts et les `iframe` dans les fichiers markdown : le rapport ne peut donc pas être
333
- > **intégré**
334
- > tel quel dans ce README. Il est affiché ici sous forme d'aperçu cliquable, et reste consultable en ligne ou en local :
335
- >
336
- > ```sh
337
- > npm run reports:preview # sert dist/reports/corpus et ouvre le navigateur
338
- > ```
343
+ Les chiffres de la dernière exécution sont repris ci-dessous, dans [Résultats corpus](#résultats-corpus).
339
344
 
340
345
  ### Lancer un corpus
341
346
 
@@ -370,7 +375,7 @@ Ces résultats sont **générés automatiquement** à la fin de `npm run test:co
370
375
 
371
376
  <!-- CORPUS:START -->
372
377
 
373
- > **Version `1.8.56`** · branche `main` · généré le 2026-10-13
378
+ > **Version `1.8.57`** · branche `main` · généré le 2026-10-14
374
379
  > Seuil de tolérance **5%**
375
380
 
376
381
  <table>
@@ -403,24 +408,24 @@ Ces résultats sont **générés automatiquement** à la fin de `npm run test:co
403
408
 
404
409
  <table>
405
410
  <tr>
406
- <td align="center"><strong>5,09 ms</strong><br/><sub>moyenne</sub></td>
407
- <td align="center"><strong>4,44 ms</strong><br/><sub>médiane</sub></td>
411
+ <td align="center"><strong>5,53 ms</strong><br/><sub>moyenne</sub></td>
412
+ <td align="center"><strong>4,81 ms</strong><br/><sub>médiane</sub></td>
408
413
  <td align="center"><strong>0,12 ms</strong><br/><sub>min</sub></td>
409
- <td align="center"><strong>854,0 ms</strong><br/><sub>max</sub></td>
414
+ <td align="center"><strong>742,8 ms</strong><br/><sub>max</sub></td>
410
415
  </tr>
411
416
  </table>
412
417
 
413
418
  | Corpus | Moyenne | Médiane | Min | Max | p95 | p99 |
414
419
  | :--------------------------------------------- | ------: | ------: | ------: | -------: | ------: | ------: |
415
- | **Généraliste** | 5,11 ms | 4,71 ms | 0,20 ms | 34,6 ms | 9,00 ms | 12,2 ms |
416
- | **Appartement · chauffage individuel (2025)** | 3,79 ms | 3,44 ms | 1,48 ms | 33,9 ms | 6,57 ms | 8,40 ms |
417
- | **Appartement · chauffage collectif (2025)** | 4,45 ms | 4,04 ms | 1,63 ms | 39,4 ms | 7,29 ms | 9,55 ms |
418
- | **Logement individuel (2025)** | 4,68 ms | 4,12 ms | 1,66 ms | 46,7 ms | 8,62 ms | 12,6 ms |
419
- | **Maison individuelle (2025)** | 5,24 ms | 4,81 ms | 1,94 ms | 50,8 ms | 8,85 ms | 11,8 ms |
420
- | **Immeuble · chauffage collectif** | 4,99 ms | 4,28 ms | 0,12 ms | 99,4 ms | 8,96 ms | 14,0 ms |
421
- | **Immeuble · chauffage individuel** | 5,73 ms | 4,88 ms | 0,12 ms | 98,8 ms | 11,1 ms | 18,4 ms |
422
- | **Immeuble · chauffage mixte** | 4,87 ms | 3,97 ms | 0,18 ms | 854,0 ms | 9,80 ms | 16,0 ms |
423
- | **Individuel généré depuis l'immeuble (2026)** | 6,94 ms | 5,65 ms | 1,82 ms | 88,8 ms | 15,2 ms | 25,7 ms |
420
+ | **Généraliste** | 4,78 ms | 4,29 ms | 0,23 ms | 40,0 ms | 8,73 ms | 12,9 ms |
421
+ | **Appartement · chauffage individuel (2025)** | 3,72 ms | 3,39 ms | 1,41 ms | 33,6 ms | 6,34 ms | 8,20 ms |
422
+ | **Appartement · chauffage collectif (2025)** | 4,22 ms | 3,81 ms | 1,66 ms | 30,8 ms | 6,96 ms | 8,81 ms |
423
+ | **Logement individuel (2025)** | 5,29 ms | 4,69 ms | 1,77 ms | 43,8 ms | 9,75 ms | 13,8 ms |
424
+ | **Maison individuelle (2025)** | 6,53 ms | 5,76 ms | 1,90 ms | 55,5 ms | 11,9 ms | 17,5 ms |
425
+ | **Immeuble · chauffage collectif** | 5,71 ms | 4,92 ms | 0,13 ms | 134,1 ms | 10,5 ms | 17,7 ms |
426
+ | **Immeuble · chauffage individuel** | 7,32 ms | 6,25 ms | 0,12 ms | 97,9 ms | 14,8 ms | 23,2 ms |
427
+ | **Immeuble · chauffage mixte** | 5,87 ms | 4,75 ms | 0,21 ms | 742,8 ms | 12,5 ms | 20,4 ms |
428
+ | **Individuel généré depuis l'immeuble (2026)** | 6,34 ms | 5,40 ms | 1,62 ms | 55,7 ms | 12,2 ms | 20,3 ms |
424
429
 
425
430
  <sub>La moyenne est tirée vers le haut par les DPE collectifs, dont le coût atteint plusieurs
426
431
  dizaines de fois la médiane : c’est la médiane qui décrit le cas courant, et p95/p99 le cas
@@ -441,6 +446,7 @@ flowchart TD
441
446
  subgraph fait ["✅ Fait"]
442
447
  A1["Site Open3CL"]
443
448
  A2["Rapports de corpus interactifs"]
449
+ A3["Démo 3D en ligne"]
444
450
  end
445
451
  subgraph cours ["🚧 En cours"]
446
452
  B1["Refonte technique"]
@@ -450,13 +456,14 @@ flowchart TD
450
456
  fait --> cours
451
457
  ```
452
458
 
453
- | Étape | Statut | Détail |
454
- | :------------------ | :---------- | :--------------------------------------------------------------- |
455
- | Site Open3CL | ✅ Terminé | [open3cl.fr](https://open3cl.fr) |
456
- | Rapports de tests | ✅ Terminé | Tableau de bord interactif publié à chaque exécution |
457
- | Refonte technique | 🚧 En cours | Découpage par article de la méthode, couverture de tests à 100 % |
458
- | DPE à l'immeuble | 🚧 En cours | Fiabilisation des appartements générés depuis un DPE immeuble |
459
- | Certification ADEME | 🚧 En cours | Objectif de long terme |
459
+ | Étape | Statut | Détail |
460
+ | :------------------ | :---------- | :----------------------------------------------------------------------------------------------- |
461
+ | Site Open3CL | ✅ Terminé | [open3cl.fr](https://open3cl.fr) |
462
+ | Démo 3D en ligne | ✅ Terminé | [Visualisation 3D des déperditions](https://open3cl.github.io/engine/), publiée sur GitHub Pages |
463
+ | Rapports de tests | ✅ Terminé | Tableau de bord interactif généré à chaque exécution |
464
+ | Refonte technique | 🚧 En cours | Découpage par article de la méthode, couverture de tests à 100 % |
465
+ | DPE à l'immeuble | 🚧 En cours | Fiabilisation des appartements générés depuis un DPE immeuble |
466
+ | Certification ADEME | 🚧 En cours | Objectif de long terme |
460
467
 
461
468
  Le détail complet des bugs et fonctionnalités en cours est dans les
462
469
  [issues](https://github.com/Open3CL/engine/issues).
package/conso.js CHANGED
@@ -962,9 +962,11 @@ function getChauffageConso(gen_ch, field, coef, prorataChauffage, prefix, facteu
962
962
  const conso =
963
963
  gen_ch.donnee_intermediaire[field] * (typeEnergie === 'électricité ch' ? facteurAc : 1);
964
964
 
965
- // La clé de répartition n'est utilisée que dans le cadre des chauffages collectifs
965
+ // La clé de répartition n'est utilisée que dans le cadre des chauffages collectifs, ou d'un
966
+ // chauffage individuel calculé à l'échelle de l'immeuble (rdim reconstitué, § 17.2.2.2.2)
966
967
  const repartition =
967
- gen_ch.donnee_entree.enum_type_installation_id !== '1'
968
+ gen_ch.donnee_entree.enum_type_installation_id !== '1' ||
969
+ gen_ch.donnee_entree.rdim_reconstitue
968
970
  ? gen_ch.donnee_entree.cle_repartition_ch || prorataChauffage
969
971
  : prorataChauffage;
970
972
 
@@ -1014,7 +1016,10 @@ function calc_conso_pond(
1014
1016
  ret.auxiliaire_generation_ch = gen_ch.reduce((acc, gen_ch) => {
1015
1017
  const conso =
1016
1018
  (gen_ch.donnee_intermediaire.conso_auxiliaire_generation_ch || 0) *
1017
- fAc('auxiliaire_generation_ch');
1019
+ fAc('auxiliaire_generation_ch') *
1020
+ (gen_ch.donnee_entree.rdim_reconstitue
1021
+ ? gen_ch.donnee_entree.cle_repartition_ch || prorataChauffage
1022
+ : 1);
1018
1023
  return acc + getConso(coef, 'électricité auxiliaire', conso);
1019
1024
  }, 0);
1020
1025
 
package/conso.spec.js CHANGED
@@ -393,6 +393,66 @@ describe('calc_conso - agrégation des consommations', () => {
393
393
  expect(res.ef_conso.conso_auxiliaire_distribution_ch).toBe(200);
394
394
  });
395
395
 
396
+ test('chauffage individuel avec rdim reconstitué (issue #287) : clé appliquée à conso et aux. génération, pas à la distribution', () => {
397
+ // § 17.2.2.2.2 : chauffage individuel, coef_IFC = 1 → Cch_ap = Clé_ap × Cch_immeuble
398
+ const ch = [
399
+ installCh({ cle_repartition_ch: 0.25, enum_methode_calcul_conso_id: '3' }, [
400
+ genCh(
401
+ '1',
402
+ {
403
+ conso_ch: 1000,
404
+ conso_ch_depensier: 1200,
405
+ conso_auxiliaire_generation_ch: 40,
406
+ conso_auxiliaire_distribution_ch: 80
407
+ },
408
+ { rdim_reconstitue: true }
409
+ )
410
+ ])
411
+ ];
412
+
413
+ const res = calc_conso(100, 1, 1, [], ch, [], [], 1, 1, DATE_DPE, coef_ep);
414
+
415
+ expect(res.ef_conso.conso_ch).toBe(250);
416
+ expect(res.ef_conso.conso_auxiliaire_generation_ch).toBe(10);
417
+ // Distribution non proratisée, comme dans les sorties ADEME
418
+ expect(res.ef_conso.conso_auxiliaire_distribution_ch).toBe(80);
419
+ });
420
+
421
+ test('chauffage individuel sans rdim reconstitué : clé ignorée (prorata chauffage)', () => {
422
+ const ch = [
423
+ installCh({ cle_repartition_ch: 0.25, enum_methode_calcul_conso_id: '3' }, [
424
+ genCh('1', {
425
+ conso_ch: 1000,
426
+ conso_ch_depensier: 1200,
427
+ conso_auxiliaire_generation_ch: 40
428
+ })
429
+ ])
430
+ ];
431
+
432
+ const res = calc_conso(100, 1, 1, [], ch, [], [], 1, 1, DATE_DPE, coef_ep);
433
+
434
+ expect(res.ef_conso.conso_ch).toBe(1000);
435
+ expect(res.ef_conso.conso_auxiliaire_generation_ch).toBe(40);
436
+ });
437
+
438
+ test('chauffage individuel avec rdim reconstitué et clé nulle : repli sur le prorata chauffage, comme la conso', () => {
439
+ // clé installation 0 et prorata 0 → clé générateur 0 → repli sur prorataChauffage (0)
440
+ const ch = [
441
+ installCh({ cle_repartition_ch: 0, enum_methode_calcul_conso_id: '3' }, [
442
+ genCh(
443
+ '1',
444
+ { conso_ch: 1000, conso_ch_depensier: 1200, conso_auxiliaire_generation_ch: 40 },
445
+ { rdim_reconstitue: true }
446
+ )
447
+ ])
448
+ ];
449
+
450
+ const res = calc_conso(100, 1, 1, [], ch, [], [], 1, 0, DATE_DPE, coef_ep);
451
+
452
+ expect(res.ef_conso.conso_ch).toBe(0);
453
+ expect(res.ef_conso.conso_auxiliaire_generation_ch).toBe(0);
454
+ });
455
+
396
456
  test('installation avec clé absente (non collective) : répartition = prorata chauffage', () => {
397
457
  const ch = [
398
458
  installCh(
package/engine.js CHANGED
@@ -32,8 +32,9 @@ import { Inertie } from './7_inertie.js';
32
32
  import getFicheTechnique from './ficheTechnique.js';
33
33
  import { ProductionENR, ratioProrataPvLogement } from './16.2_production_enr.js';
34
34
  import DpeSanitizerService from './dpe-sanitizer.service.js';
35
+ import { reconstituerRdimDpeGenereImmeuble } from './17.2_rdim_dpe_genere_immeuble.js';
35
36
 
36
- const LIB_VERSION = '1.8.57';
37
+ const LIB_VERSION = '1.8.58';
37
38
 
38
39
  const dpeSanitizerService = new DpeSanitizerService();
39
40
 
@@ -291,6 +292,20 @@ export function calcul_3cl(inputDpe, options) {
291
292
 
292
293
  const ecs = logement.installation_ecs_collection.installation_ecs || [];
293
294
  const Nb_lgt = cg.nombre_appartement || 1;
295
+
296
+ /**
297
+ * DPE appartement généré depuis un DPE immeuble : installation individuelle décrite sur la surface
298
+ * de l'immeuble avec un rdim exporté à 1 → reconstitution du rdim implicite, calcul à l'échelle de
299
+ * l'immeuble puis application de la clé de répartition (§ 17.2.2.2.2).
300
+ * @see https://github.com/Open3CL/engine/issues/287
301
+ */
302
+ const rdimReconstitue =
303
+ bug_for_bug_compat && dpeGenereImmeuble
304
+ ? reconstituerRdimDpeGenereImmeuble(cg, instal_ch, ecs)
305
+ : { ch: false, ecs: false };
306
+ // Type de bâtiment pour le besoin / l'ECS (thCalc) et pour le taux de charge (thCh)
307
+ const thCalc = rdimReconstitue.ecs ? 'immeuble' : th;
308
+ const thCh = rdimReconstitue.ch ? 'immeuble' : th;
294
309
  const hsp = cg.hsp;
295
310
  const clim = logement.climatisation_collection.climatisation || [];
296
311
  let apport_et_besoin = calc_apport_et_besoin(
@@ -339,9 +354,9 @@ export function calcul_3cl(inputDpe, options) {
339
354
  if (ecs.length > 1) {
340
355
  // Immeuble avec différents systèmes individuels
341
356
  isImmeubleSystemEcsIndividuels =
342
- th === 'immeuble' && ecs.every((e) => e.donnee_entree.enum_type_installation_id === '1');
357
+ thCalc === 'immeuble' && ecs.every((e) => e.donnee_entree.enum_type_installation_id === '1');
343
358
 
344
- if (th === 'immeuble' && !isImmeubleSystemEcsIndividuels) {
359
+ if (thCalc === 'immeuble' && !isImmeubleSystemEcsIndividuels) {
345
360
  isImmeubleMultiEcs =
346
361
  Number(cg.surface_habitable_immeuble) > 0 &&
347
362
  ecs.every((e) => Number(e.donnee_entree.surface_habitable) > 0);
@@ -454,7 +469,7 @@ export function calcul_3cl(inputDpe, options) {
454
469
  GV,
455
470
  ca_id,
456
471
  zc_id,
457
- th,
472
+ thCalc,
458
473
  virtualisationECS,
459
474
  dpe.logement.caracteristique_generale.surface_habitable_immeuble,
460
475
  dpe.logement.caracteristique_generale.nombre_appartement,
@@ -521,7 +536,7 @@ export function calcul_3cl(inputDpe, options) {
521
536
  instal_ch,
522
537
  bv_list,
523
538
  ets,
524
- th,
539
+ thCalc,
525
540
  dpe.logement.caracteristique_generale.nombre_appartement
526
541
  );
527
542
  apport_et_besoin = { ...apport_et_besoin, ...besoin_ch };
@@ -535,7 +550,7 @@ export function calcul_3cl(inputDpe, options) {
535
550
  * 13.2.1.2 Présence d’un ou plusieurs générateurs à combustion indépendants
536
551
  * Calcul des taux de charge pour chacun des générateurs de chauffage
537
552
  */
538
- tauxChargeForGenerator(instal_ch, GV, ca_id, zc_id, th);
553
+ tauxChargeForGenerator(instal_ch, GV, ca_id, zc_id, thCh);
539
554
 
540
555
  instal_ch.forEach((ch) => {
541
556
  calc_chauffage(
package/engine.spec.js CHANGED
@@ -70,6 +70,9 @@ vi.mock('./15_conso_aux.js', () => ({ conso_aux_distribution_ecs: vi.fn() }));
70
70
  vi.mock('./2021_04_13_confort_ete.js', () => ({ default: vi.fn() }));
71
71
  vi.mock('./2021_04_13_qualite_isolation.js', () => ({ default: vi.fn() }));
72
72
  vi.mock('./ficheTechnique.js', () => ({ default: vi.fn() }));
73
+ vi.mock('./17.2_rdim_dpe_genere_immeuble.js', () => ({
74
+ reconstituerRdimDpeGenereImmeuble: vi.fn()
75
+ }));
73
76
 
74
77
  // Les instances Inertie / ProductionENR sont créées au chargement du module.
75
78
  // On expose des méthodes mockées récupérables dans les tests.
@@ -112,6 +115,7 @@ const { conso_aux_distribution_ecs } = await import('./15_conso_aux.js');
112
115
  const calc_confort_ete = (await import('./2021_04_13_confort_ete.js')).default;
113
116
  const calc_qualite_isolation = (await import('./2021_04_13_qualite_isolation.js')).default;
114
117
  const getFicheTechnique = (await import('./ficheTechnique.js')).default;
118
+ const { reconstituerRdimDpeGenereImmeuble } = await import('./17.2_rdim_dpe_genere_immeuble.js');
115
119
 
116
120
  /** Objet conso renvoyé par le mock de `calc_conso` (mêmes réf pour les 3 appels). */
117
121
  let consoResult;
@@ -1098,6 +1102,102 @@ describe('calcul_3cl - réalignements ECS (bug_for_bug_compat)', () => {
1098
1102
  });
1099
1103
  });
1100
1104
 
1105
+ /**
1106
+ * DPE appartement généré depuis un DPE immeuble : reconstitution du rdim (issue #287).
1107
+ * @see Methode_de_calcul_3CL_DPE_2021-338.pdf - § 17.2.2.2.2
1108
+ */
1109
+ describe('calcul_3cl - rdim reconstitué des DPE générés depuis un DPE immeuble (#287)', () => {
1110
+ const METHODE_GENEREE = '10';
1111
+
1112
+ function makeDpeGenere() {
1113
+ enums.methode_application_dpe_log = {
1114
+ 1: 'dpe maison individuelle',
1115
+ [METHODE_GENEREE]:
1116
+ 'dpe appartement généré à partir des données dpe immeuble chauffage individuel ecs individuel'
1117
+ };
1118
+ const dpe = makeDpe();
1119
+ dpe.logement.caracteristique_generale.enum_methode_application_dpe_log_id = METHODE_GENEREE;
1120
+ return dpe;
1121
+ }
1122
+
1123
+ /** Type de bâtiment transmis à calc_ecs, calc_besoin_ch et au taux de charge. */
1124
+ const thTransmis = () => ({
1125
+ ecs: calc_ecs.mock.calls[0][7],
1126
+ besoinCh: calc_besoin_ch.mock.calls[0][11],
1127
+ tauxCharge: tauxChargeForGenerator.mock.calls[0][4]
1128
+ });
1129
+
1130
+ test('bug_for_bug_compat : reconstitution appelée avec les caractéristiques et les installations', () => {
1131
+ bugForBugCompat = true;
1132
+ vi.mocked(reconstituerRdimDpeGenereImmeuble).mockReturnValue({ ch: false, ecs: false });
1133
+ const dpe = makeDpeGenere();
1134
+
1135
+ calcul_3cl(dpe, { sanitize: false });
1136
+
1137
+ expect(reconstituerRdimDpeGenereImmeuble).toHaveBeenCalledWith(
1138
+ dpe.logement.caracteristique_generale,
1139
+ dpe.logement.installation_chauffage_collection.installation_chauffage,
1140
+ dpe.logement.installation_ecs_collection.installation_ecs
1141
+ );
1142
+ expect(thTransmis()).toEqual({
1143
+ ecs: 'appartement',
1144
+ besoinCh: 'appartement',
1145
+ tauxCharge: 'appartement'
1146
+ });
1147
+ });
1148
+
1149
+ test('chauffage et ECS reconstitués : calcul à l’échelle de l’immeuble partout', () => {
1150
+ bugForBugCompat = true;
1151
+ vi.mocked(reconstituerRdimDpeGenereImmeuble).mockReturnValue({ ch: true, ecs: true });
1152
+
1153
+ calcul_3cl(makeDpeGenere(), { sanitize: false });
1154
+
1155
+ expect(thTransmis()).toEqual({ ecs: 'immeuble', besoinCh: 'immeuble', tauxCharge: 'immeuble' });
1156
+ });
1157
+
1158
+ test('chauffage seul reconstitué : seul le taux de charge passe à l’échelle de l’immeuble', () => {
1159
+ bugForBugCompat = true;
1160
+ vi.mocked(reconstituerRdimDpeGenereImmeuble).mockReturnValue({ ch: true, ecs: false });
1161
+
1162
+ calcul_3cl(makeDpeGenere(), { sanitize: false });
1163
+
1164
+ expect(thTransmis()).toEqual({
1165
+ ecs: 'appartement',
1166
+ besoinCh: 'appartement',
1167
+ tauxCharge: 'immeuble'
1168
+ });
1169
+ });
1170
+
1171
+ test('ECS seule reconstituée : besoin et ECS à l’échelle de l’immeuble, pas le taux de charge', () => {
1172
+ bugForBugCompat = true;
1173
+ vi.mocked(reconstituerRdimDpeGenereImmeuble).mockReturnValue({ ch: false, ecs: true });
1174
+
1175
+ calcul_3cl(makeDpeGenere(), { sanitize: false });
1176
+
1177
+ expect(thTransmis()).toEqual({
1178
+ ecs: 'immeuble',
1179
+ besoinCh: 'immeuble',
1180
+ tauxCharge: 'appartement'
1181
+ });
1182
+ });
1183
+
1184
+ test('sans bug_for_bug_compat : aucune reconstitution', () => {
1185
+ calcul_3cl(makeDpeGenere(), { sanitize: false });
1186
+
1187
+ expect(reconstituerRdimDpeGenereImmeuble).not.toHaveBeenCalled();
1188
+ expect(thTransmis().tauxCharge).toBe('appartement');
1189
+ });
1190
+
1191
+ test('DPE non généré depuis un immeuble : aucune reconstitution, même en bug_for_bug_compat', () => {
1192
+ bugForBugCompat = true;
1193
+
1194
+ calcul_3cl(makeDpe(), { sanitize: false });
1195
+
1196
+ expect(reconstituerRdimDpeGenereImmeuble).not.toHaveBeenCalled();
1197
+ expect(thTransmis().tauxCharge).toBe('maison');
1198
+ });
1199
+ });
1200
+
1101
1201
  describe('get_conso_coeff_1_7_2027', () => {
1102
1202
  test('projette la conso EP 2027 pour une maison', () => {
1103
1203
  vi.mocked(classe_bilan_dpe).mockReturnValue('C');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open3cl/engine",
3
- "version": "1.8.57",
3
+ "version": "1.8.58",
4
4
  "description": "Open Source 3CL-DPE engine",
5
5
  "main": "index.js",
6
6
  "directories": {
@@ -84,7 +84,7 @@
84
84
  "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
85
85
  },
86
86
  "author": "Open3Cl",
87
- "license": "GPL-3.0",
87
+ "license": "MIT",
88
88
  "engines": {
89
89
  "node": ">=24.14.1"
90
90
  },