@open3cl/engine 1.8.57 → 1.8.59

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,62 @@
1
+ /**
2
+ * DPE immeuble avec une installation ECS individuelle unique décrite pour un logement type.
3
+ *
4
+ * Certains logiciels (3.0.x, 2.0.x, 1.1.x) exportent l'installation ECS individuelle unique d'un
5
+ * immeuble avec `surface_habitable` = SH / N et `nombre_logement` = N, sans `rdim`. Le calcul de
6
+ * référence (Tribu) a été fait avec Rdim = N : le besoin est ramené à un logement
7
+ * (Becs = Becs_immeuble × Sh × Rdim / SH / Rdim = Becs_immeuble / N), les pertes de stockage sont
8
+ * celles d'un seul ballon (Vs non multiplié), puis la consommation est multipliée par Rdim.
9
+ *
10
+ * Sans rdim, Open3CL impute le besoin de tout l'immeuble à un seul ballon : conso_ecs −25 à −30 %.
11
+ * On reconstitue donc rdim = N, ce qui réutilise la chaîne existante (11_ecs.js : besoin / rdim,
12
+ * conso.js : conso × rdim).
13
+ *
14
+ * LICIEL (nombre_logement = 1, Rdim non exporté) n'est pas concerné.
15
+ *
16
+ * @see Methode_de_calcul_3CL_DPE_2021-338.pdf - § 11.6, § 11.6.2 (p. 74-75) et § 17.2.2.3
17
+ * @see Tribu : Calcul_batiment.cs l. 583-588 (Ratio = Sh·Rdim/SH), l. 602-604 (Vs non multiplié),
18
+ * l. 1654-1664 et l. 1052 (Cecs / Rdim puis × Rdim)
19
+ * @see https://github.com/Open3CL/engine/issues/288
20
+ */
21
+
22
+ /** Tolérance relative sur Sh_ecs × N ≈ SH */
23
+ export const TOLERANCE_SURFACE_LOGEMENT_TYPE = 0.05;
24
+
25
+ /**
26
+ * Rdim implicite de l'installation ECS individuelle unique d'un immeuble, ou null si non concernée.
27
+ *
28
+ * @param th {string} type de bâtiment (maison, appartement, immeuble)
29
+ * @param instal_ecs {Array} installation_ecs
30
+ * @param surfaceHabitableImmeuble {number|string} surface habitable de l'immeuble (SH)
31
+ * @return {number|null}
32
+ */
33
+ export function rdimImpliciteEcsIndividuelleUnique(th, instal_ecs, surfaceHabitableImmeuble) {
34
+ if (th !== 'immeuble' || !Array.isArray(instal_ecs) || instal_ecs.length !== 1) return null;
35
+
36
+ const de = instal_ecs[0].donnee_entree;
37
+ if (!de || String(de.enum_type_installation_id) !== '1' || de.rdim) return null;
38
+
39
+ const N = Number(de.nombre_logement);
40
+ const SH = Number(surfaceHabitableImmeuble);
41
+ const sh = Number(de.surface_habitable);
42
+ if (!(N > 1) || !(SH > 0) || !(sh > 0)) return null;
43
+
44
+ return Math.abs(sh * N - SH) <= TOLERANCE_SURFACE_LOGEMENT_TYPE * SH ? N : null;
45
+ }
46
+
47
+ /**
48
+ * Reconstitue le rdim de l'installation ECS individuelle unique d'un immeuble décrite pour un
49
+ * logement type. L'installation reçoit `rdim` = nombre_logement et le flag `rdim_reconstitue`.
50
+ *
51
+ * @param th {string} type de bâtiment
52
+ * @param instal_ecs {Array} installation_ecs
53
+ * @param surfaceHabitableImmeuble {number|string} surface habitable de l'immeuble (SH)
54
+ * @return {boolean} true si le rdim a été reconstitué
55
+ */
56
+ export function reconstituerRdimEcsIndividuelleUnique(th, instal_ecs, surfaceHabitableImmeuble) {
57
+ const rdim = rdimImpliciteEcsIndividuelleUnique(th, instal_ecs, surfaceHabitableImmeuble);
58
+ if (rdim === null) return false;
59
+ instal_ecs[0].donnee_entree.rdim = rdim;
60
+ instal_ecs[0].donnee_entree.rdim_reconstitue = true;
61
+ return true;
62
+ }
@@ -0,0 +1,113 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import {
3
+ rdimImpliciteEcsIndividuelleUnique,
4
+ reconstituerRdimEcsIndividuelleUnique,
5
+ TOLERANCE_SURFACE_LOGEMENT_TYPE
6
+ } from './11_rdim_ecs_individuelle_unique.js';
7
+
8
+ /**
9
+ * @see Methode_de_calcul_3CL_DPE_2021-338.pdf - § 11.6, § 11.6.2 et § 17.2.2.3
10
+ * @see Tribu : Calcul_batiment.cs l. 583-588, l. 602-604, l. 1654-1664
11
+ * @see https://github.com/Open3CL/engine/issues/288
12
+ */
13
+ describe("Rdim de l'installation ECS individuelle unique d'un immeuble", () => {
14
+ /** Installation ECS décrite pour un logement type : Sh = SH / N, nombre_logement = N */
15
+ const ecs = (de) => [
16
+ {
17
+ donnee_entree: {
18
+ enum_type_installation_id: '1',
19
+ surface_habitable: 64,
20
+ nombre_logement: 10,
21
+ ...de
22
+ }
23
+ }
24
+ ];
25
+
26
+ describe('rdimImpliciteEcsIndividuelleUnique', () => {
27
+ test('installation individuelle unique, Sh × N = SH, sans rdim : rdim = nombre_logement', () => {
28
+ // Cas type 2244E3131012H : N = 10
29
+ expect(rdimImpliciteEcsIndividuelleUnique('immeuble', ecs(), 640)).toBe(10);
30
+ expect(
31
+ rdimImpliciteEcsIndividuelleUnique(
32
+ 'immeuble',
33
+ ecs({ enum_type_installation_id: 1, surface_habitable: '64', nombre_logement: '10' }),
34
+ '640'
35
+ )
36
+ ).toBe(10);
37
+ });
38
+
39
+ test('tolérance de 5 % sur Sh × N ≈ SH', () => {
40
+ expect(TOLERANCE_SURFACE_LOGEMENT_TYPE).toBe(0.05);
41
+ // |640 − SH| ≤ 0,05 × SH ⇔ 609,52 ≤ SH ≤ 673,68
42
+ expect(rdimImpliciteEcsIndividuelleUnique('immeuble', ecs(), 673)).toBe(10);
43
+ expect(rdimImpliciteEcsIndividuelleUnique('immeuble', ecs(), 610)).toBe(10);
44
+ expect(rdimImpliciteEcsIndividuelleUnique('immeuble', ecs(), 674)).toBeNull();
45
+ expect(rdimImpliciteEcsIndividuelleUnique('immeuble', ecs(), 609)).toBeNull();
46
+ });
47
+
48
+ test('installation décrite sur la surface de l’immeuble (Sh = SH) : non concernée', () => {
49
+ expect(
50
+ rdimImpliciteEcsIndividuelleUnique('immeuble', ecs({ surface_habitable: 640 }), 640)
51
+ ).toBeNull();
52
+ });
53
+
54
+ test('LICIEL : nombre_logement = 1 → non concerné', () => {
55
+ expect(
56
+ rdimImpliciteEcsIndividuelleUnique(
57
+ 'immeuble',
58
+ ecs({ nombre_logement: 1, surface_habitable: 640 }),
59
+ 640
60
+ )
61
+ ).toBeNull();
62
+ });
63
+
64
+ test('rdim exporté : on conserve la saisie', () => {
65
+ expect(rdimImpliciteEcsIndividuelleUnique('immeuble', ecs({ rdim: 4 }), 640)).toBeNull();
66
+ expect(rdimImpliciteEcsIndividuelleUnique('immeuble', ecs({ rdim: 1 }), 640)).toBeNull();
67
+ });
68
+
69
+ test('installation collective : non concernée', () => {
70
+ expect(
71
+ rdimImpliciteEcsIndividuelleUnique('immeuble', ecs({ enum_type_installation_id: '2' }), 640)
72
+ ).toBeNull();
73
+ });
74
+
75
+ test('maison ou appartement : non concerné', () => {
76
+ expect(rdimImpliciteEcsIndividuelleUnique('maison', ecs(), 640)).toBeNull();
77
+ expect(rdimImpliciteEcsIndividuelleUnique('appartement', ecs(), 640)).toBeNull();
78
+ });
79
+
80
+ test('plusieurs installations ECS : non concerné (#232)', () => {
81
+ expect(rdimImpliciteEcsIndividuelleUnique('immeuble', [...ecs(), ...ecs()], 640)).toBeNull();
82
+ });
83
+
84
+ test('données manquantes : non concerné', () => {
85
+ expect(rdimImpliciteEcsIndividuelleUnique('immeuble', undefined, 640)).toBeNull();
86
+ expect(rdimImpliciteEcsIndividuelleUnique('immeuble', [], 640)).toBeNull();
87
+ expect(rdimImpliciteEcsIndividuelleUnique('immeuble', [{}], 640)).toBeNull();
88
+ expect(rdimImpliciteEcsIndividuelleUnique('immeuble', ecs(), undefined)).toBeNull();
89
+ expect(rdimImpliciteEcsIndividuelleUnique('immeuble', ecs(), 0)).toBeNull();
90
+ expect(
91
+ rdimImpliciteEcsIndividuelleUnique('immeuble', ecs({ surface_habitable: undefined }), 640)
92
+ ).toBeNull();
93
+ expect(
94
+ rdimImpliciteEcsIndividuelleUnique('immeuble', ecs({ nombre_logement: undefined }), 640)
95
+ ).toBeNull();
96
+ });
97
+ });
98
+
99
+ describe('reconstituerRdimEcsIndividuelleUnique', () => {
100
+ test('installation concernée : rdim et flag positionnés', () => {
101
+ const inst = ecs();
102
+ expect(reconstituerRdimEcsIndividuelleUnique('immeuble', inst, 640)).toBe(true);
103
+ expect(inst[0].donnee_entree).toMatchObject({ rdim: 10, rdim_reconstitue: true });
104
+ });
105
+
106
+ test('installation non concernée : inchangée', () => {
107
+ const inst = ecs({ nombre_logement: 1 });
108
+ expect(reconstituerRdimEcsIndividuelleUnique('immeuble', inst, 640)).toBe(false);
109
+ expect(inst[0].donnee_entree.rdim).toBeUndefined();
110
+ expect(inst[0].donnee_entree.rdim_reconstitue).toBeUndefined();
111
+ });
112
+ });
113
+ });
@@ -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
  &middot;
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,21 +375,21 @@ 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.58`** · branche `main` · généré le 2026-10-14
374
379
  > Seuil de tolérance **5%**
375
380
 
376
381
  <table>
377
382
  <tr>
378
383
  <td align="center"><strong>9</strong><br/><sub>corpus</sub></td>
379
384
  <td align="center"><strong>89 418</strong><br/><sub>DPE analysés</sub></td>
380
- <td align="center"><strong>66 706</strong><br/><sub>DPE conformes</sub></td>
381
- <td align="center"><strong>74,60 %</strong><br/><sub>réussite globale</sub></td>
385
+ <td align="center"><strong>69 329</strong><br/><sub>DPE conformes</sub></td>
386
+ <td align="center"><strong>77,53 %</strong><br/><sub>réussite globale</sub></td>
382
387
  </tr>
383
388
  </table>
384
389
 
385
390
  | | Corpus | Réussite | | DPE conformes |
386
391
  | :-: | :---------------------------------------------------------------------------------------------------------------------- | ----------: | :--------------------- | -------------: |
387
- | 🔴 | **Généraliste**<br/><sub>`corpus_dpe.csv`</sub> | **47,08 %** | `█████████░░░░░░░░░░░` | 4 708 / 9 999 |
392
+ | 🔴 | **Généraliste**<br/><sub>`corpus_dpe.csv`</sub> | **47,67 %** | `██████████░░░░░░░░░░` | 4 767 / 9 999 |
388
393
  | 🟢 | **Appartement · chauffage individuel (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_individuel_2025.csv`</sub> | **92,90 %** | `███████████████████░` | 9 290 / 10 000 |
389
394
  | 🟢 | **Appartement · chauffage collectif (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_collectif_2025.csv`</sub> | **91,72 %** | `██████████████████░░` | 9 172 / 10 000 |
390
395
  | 🟢 | **Logement individuel (2025)**<br/><sub>`dpe_logement_individuel_2025.csv`</sub> | **90,47 %** | `██████████████████░░` | 9 040 / 9 992 |
@@ -392,7 +397,7 @@ Ces résultats sont **générés automatiquement** à la fin de `npm run test:co
392
397
  | 🟢 | **Immeuble · chauffage collectif**<br/><sub>`dpe_immeuble_chauffage_collectif.csv`</sub> | **85,04 %** | `█████████████████░░░` | 8 346 / 9 814 |
393
398
  | 🟡 | **Immeuble · chauffage individuel**<br/><sub>`dpe_immeuble_chauffage_individuel.csv`</sub> | **84,58 %** | `█████████████████░░░` | 8 456 / 9 998 |
394
399
  | 🔴 | **Immeuble · chauffage mixte**<br/><sub>`dpe_immeuble_chauffage_mixte.csv`</sub> | **59,86 %** | `████████████░░░░░░░░` | 5 756 / 9 616 |
395
- | 🔴 | **Individuel généré depuis l'immeuble (2026)**<br/><sub>`dpe_individuel_a_partir_dpe_immeuble_2026.csv`</sub> | **30,49 %** | `██████░░░░░░░░░░░░░░` | 3 049 / 10 000 |
400
+ | 🔴 | **Individuel généré depuis l'immeuble (2026)**<br/><sub>`dpe_individuel_a_partir_dpe_immeuble_2026.csv`</sub> | **56,13 %** | `███████████░░░░░░░░░` | 5 613 / 10 000 |
396
401
 
397
402
  <sub>🟢 ≥ 85 % · 🟡 ≥ 60 % · 🔴 < 60 %</sub>
398
403
 
@@ -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>
408
- <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>
411
+ <td align="center"><strong>5,46 ms</strong><br/><sub>moyenne</sub></td>
412
+ <td align="center"><strong>4,80 ms</strong><br/><sub>médiane</sub></td>
413
+ <td align="center"><strong>0,11 ms</strong><br/><sub>min</sub></td>
414
+ <td align="center"><strong>655,4 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,56 ms | 4,11 ms | 0,22 ms | 28,4 ms | 8,39 ms | 11,5 ms |
421
+ | **Appartement · chauffage individuel (2025)** | 4,13 ms | 3,83 ms | 1,53 ms | 33,1 ms | 6,90 ms | 8,83 ms |
422
+ | **Appartement · chauffage collectif (2025)** | 4,69 ms | 4,36 ms | 1,65 ms | 38,2 ms | 7,69 ms | 10,3 ms |
423
+ | **Logement individuel (2025)** | 5,25 ms | 4,70 ms | 1,64 ms | 43,6 ms | 9,63 ms | 12,9 ms |
424
+ | **Maison individuelle (2025)** | 6,17 ms | 5,56 ms | 1,99 ms | 53,3 ms | 11,0 ms | 14,5 ms |
425
+ | **Immeuble · chauffage collectif** | 5,61 ms | 4,85 ms | 0,21 ms | 104,4 ms | 10,3 ms | 16,3 ms |
426
+ | **Immeuble · chauffage individuel** | 6,70 ms | 5,71 ms | 0,11 ms | 164,2 ms | 13,6 ms | 21,3 ms |
427
+ | **Immeuble · chauffage mixte** | 5,70 ms | 4,65 ms | 0,20 ms | 655,4 ms | 12,0 ms | 19,4 ms |
428
+ | **Individuel généré depuis l'immeuble (2026)** | 6,33 ms | 5,45 ms | 1,65 ms | 53,8 ms | 11,9 ms | 19,5 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,10 @@ 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';
36
+ import { reconstituerRdimEcsIndividuelleUnique } from './11_rdim_ecs_individuelle_unique.js';
35
37
 
36
- const LIB_VERSION = '1.8.57';
38
+ const LIB_VERSION = '1.8.59';
37
39
 
38
40
  const dpeSanitizerService = new DpeSanitizerService();
39
41
 
@@ -290,7 +292,27 @@ export function calcul_3cl(inputDpe, options) {
290
292
  const ilpa = logement.meteo.batiment_materiaux_anciens === 1 ? '1' : '0';
291
293
 
292
294
  const ecs = logement.installation_ecs_collection.installation_ecs || [];
295
+ /**
296
+ * Immeuble avec une installation ECS individuelle unique décrite pour un logement type
297
+ * (Sh_ecs × nombre_logement ≈ SH, sans rdim) : rdim = nombre_logement.
298
+ * @see https://github.com/Open3CL/engine/issues/288
299
+ */
300
+ reconstituerRdimEcsIndividuelleUnique(th, ecs, cg.surface_habitable_immeuble);
293
301
  const Nb_lgt = cg.nombre_appartement || 1;
302
+
303
+ /**
304
+ * DPE appartement généré depuis un DPE immeuble : installation individuelle décrite sur la surface
305
+ * de l'immeuble avec un rdim exporté à 1 → reconstitution du rdim implicite, calcul à l'échelle de
306
+ * l'immeuble puis application de la clé de répartition (§ 17.2.2.2.2).
307
+ * @see https://github.com/Open3CL/engine/issues/287
308
+ */
309
+ const rdimReconstitue =
310
+ bug_for_bug_compat && dpeGenereImmeuble
311
+ ? reconstituerRdimDpeGenereImmeuble(cg, instal_ch, ecs)
312
+ : { ch: false, ecs: false };
313
+ // Type de bâtiment pour le besoin / l'ECS (thCalc) et pour le taux de charge (thCh)
314
+ const thCalc = rdimReconstitue.ecs ? 'immeuble' : th;
315
+ const thCh = rdimReconstitue.ch ? 'immeuble' : th;
294
316
  const hsp = cg.hsp;
295
317
  const clim = logement.climatisation_collection.climatisation || [];
296
318
  let apport_et_besoin = calc_apport_et_besoin(
@@ -339,9 +361,9 @@ export function calcul_3cl(inputDpe, options) {
339
361
  if (ecs.length > 1) {
340
362
  // Immeuble avec différents systèmes individuels
341
363
  isImmeubleSystemEcsIndividuels =
342
- th === 'immeuble' && ecs.every((e) => e.donnee_entree.enum_type_installation_id === '1');
364
+ thCalc === 'immeuble' && ecs.every((e) => e.donnee_entree.enum_type_installation_id === '1');
343
365
 
344
- if (th === 'immeuble' && !isImmeubleSystemEcsIndividuels) {
366
+ if (thCalc === 'immeuble' && !isImmeubleSystemEcsIndividuels) {
345
367
  isImmeubleMultiEcs =
346
368
  Number(cg.surface_habitable_immeuble) > 0 &&
347
369
  ecs.every((e) => Number(e.donnee_entree.surface_habitable) > 0);
@@ -454,7 +476,7 @@ export function calcul_3cl(inputDpe, options) {
454
476
  GV,
455
477
  ca_id,
456
478
  zc_id,
457
- th,
479
+ thCalc,
458
480
  virtualisationECS,
459
481
  dpe.logement.caracteristique_generale.surface_habitable_immeuble,
460
482
  dpe.logement.caracteristique_generale.nombre_appartement,
@@ -521,7 +543,7 @@ export function calcul_3cl(inputDpe, options) {
521
543
  instal_ch,
522
544
  bv_list,
523
545
  ets,
524
- th,
546
+ thCalc,
525
547
  dpe.logement.caracteristique_generale.nombre_appartement
526
548
  );
527
549
  apport_et_besoin = { ...apport_et_besoin, ...besoin_ch };
@@ -535,7 +557,7 @@ export function calcul_3cl(inputDpe, options) {
535
557
  * 13.2.1.2 Présence d’un ou plusieurs générateurs à combustion indépendants
536
558
  * Calcul des taux de charge pour chacun des générateurs de chauffage
537
559
  */
538
- tauxChargeForGenerator(instal_ch, GV, ca_id, zc_id, th);
560
+ tauxChargeForGenerator(instal_ch, GV, ca_id, zc_id, thCh);
539
561
 
540
562
  instal_ch.forEach((ch) => {
541
563
  calc_chauffage(
package/engine.spec.js CHANGED
@@ -70,6 +70,12 @@ 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
+ }));
76
+ vi.mock('./11_rdim_ecs_individuelle_unique.js', () => ({
77
+ reconstituerRdimEcsIndividuelleUnique: vi.fn()
78
+ }));
73
79
 
74
80
  // Les instances Inertie / ProductionENR sont créées au chargement du module.
75
81
  // On expose des méthodes mockées récupérables dans les tests.
@@ -112,6 +118,9 @@ const { conso_aux_distribution_ecs } = await import('./15_conso_aux.js');
112
118
  const calc_confort_ete = (await import('./2021_04_13_confort_ete.js')).default;
113
119
  const calc_qualite_isolation = (await import('./2021_04_13_qualite_isolation.js')).default;
114
120
  const getFicheTechnique = (await import('./ficheTechnique.js')).default;
121
+ const { reconstituerRdimDpeGenereImmeuble } = await import('./17.2_rdim_dpe_genere_immeuble.js');
122
+ const { reconstituerRdimEcsIndividuelleUnique } =
123
+ await import('./11_rdim_ecs_individuelle_unique.js');
115
124
 
116
125
  /** Objet conso renvoyé par le mock de `calc_conso` (mêmes réf pour les 3 appels). */
117
126
  let consoResult;
@@ -1098,6 +1107,123 @@ describe('calcul_3cl - réalignements ECS (bug_for_bug_compat)', () => {
1098
1107
  });
1099
1108
  });
1100
1109
 
1110
+ /**
1111
+ * DPE appartement généré depuis un DPE immeuble : reconstitution du rdim (issue #287).
1112
+ * @see Methode_de_calcul_3CL_DPE_2021-338.pdf - § 17.2.2.2.2
1113
+ */
1114
+ describe('calcul_3cl - rdim reconstitué des DPE générés depuis un DPE immeuble (#287)', () => {
1115
+ const METHODE_GENEREE = '10';
1116
+
1117
+ function makeDpeGenere() {
1118
+ enums.methode_application_dpe_log = {
1119
+ 1: 'dpe maison individuelle',
1120
+ [METHODE_GENEREE]:
1121
+ 'dpe appartement généré à partir des données dpe immeuble chauffage individuel ecs individuel'
1122
+ };
1123
+ const dpe = makeDpe();
1124
+ dpe.logement.caracteristique_generale.enum_methode_application_dpe_log_id = METHODE_GENEREE;
1125
+ return dpe;
1126
+ }
1127
+
1128
+ /** Type de bâtiment transmis à calc_ecs, calc_besoin_ch et au taux de charge. */
1129
+ const thTransmis = () => ({
1130
+ ecs: calc_ecs.mock.calls[0][7],
1131
+ besoinCh: calc_besoin_ch.mock.calls[0][11],
1132
+ tauxCharge: tauxChargeForGenerator.mock.calls[0][4]
1133
+ });
1134
+
1135
+ test('bug_for_bug_compat : reconstitution appelée avec les caractéristiques et les installations', () => {
1136
+ bugForBugCompat = true;
1137
+ vi.mocked(reconstituerRdimDpeGenereImmeuble).mockReturnValue({ ch: false, ecs: false });
1138
+ const dpe = makeDpeGenere();
1139
+
1140
+ calcul_3cl(dpe, { sanitize: false });
1141
+
1142
+ expect(reconstituerRdimDpeGenereImmeuble).toHaveBeenCalledWith(
1143
+ dpe.logement.caracteristique_generale,
1144
+ dpe.logement.installation_chauffage_collection.installation_chauffage,
1145
+ dpe.logement.installation_ecs_collection.installation_ecs
1146
+ );
1147
+ expect(thTransmis()).toEqual({
1148
+ ecs: 'appartement',
1149
+ besoinCh: 'appartement',
1150
+ tauxCharge: 'appartement'
1151
+ });
1152
+ });
1153
+
1154
+ test('chauffage et ECS reconstitués : calcul à l’échelle de l’immeuble partout', () => {
1155
+ bugForBugCompat = true;
1156
+ vi.mocked(reconstituerRdimDpeGenereImmeuble).mockReturnValue({ ch: true, ecs: true });
1157
+
1158
+ calcul_3cl(makeDpeGenere(), { sanitize: false });
1159
+
1160
+ expect(thTransmis()).toEqual({ ecs: 'immeuble', besoinCh: 'immeuble', tauxCharge: 'immeuble' });
1161
+ });
1162
+
1163
+ test('chauffage seul reconstitué : seul le taux de charge passe à l’échelle de l’immeuble', () => {
1164
+ bugForBugCompat = true;
1165
+ vi.mocked(reconstituerRdimDpeGenereImmeuble).mockReturnValue({ ch: true, ecs: false });
1166
+
1167
+ calcul_3cl(makeDpeGenere(), { sanitize: false });
1168
+
1169
+ expect(thTransmis()).toEqual({
1170
+ ecs: 'appartement',
1171
+ besoinCh: 'appartement',
1172
+ tauxCharge: 'immeuble'
1173
+ });
1174
+ });
1175
+
1176
+ test('ECS seule reconstituée : besoin et ECS à l’échelle de l’immeuble, pas le taux de charge', () => {
1177
+ bugForBugCompat = true;
1178
+ vi.mocked(reconstituerRdimDpeGenereImmeuble).mockReturnValue({ ch: false, ecs: true });
1179
+
1180
+ calcul_3cl(makeDpeGenere(), { sanitize: false });
1181
+
1182
+ expect(thTransmis()).toEqual({
1183
+ ecs: 'immeuble',
1184
+ besoinCh: 'immeuble',
1185
+ tauxCharge: 'appartement'
1186
+ });
1187
+ });
1188
+
1189
+ test('sans bug_for_bug_compat : aucune reconstitution', () => {
1190
+ calcul_3cl(makeDpeGenere(), { sanitize: false });
1191
+
1192
+ expect(reconstituerRdimDpeGenereImmeuble).not.toHaveBeenCalled();
1193
+ expect(thTransmis().tauxCharge).toBe('appartement');
1194
+ });
1195
+
1196
+ test('DPE non généré depuis un immeuble : aucune reconstitution, même en bug_for_bug_compat', () => {
1197
+ bugForBugCompat = true;
1198
+
1199
+ calcul_3cl(makeDpe(), { sanitize: false });
1200
+
1201
+ expect(reconstituerRdimDpeGenereImmeuble).not.toHaveBeenCalled();
1202
+ expect(thTransmis().tauxCharge).toBe('maison');
1203
+ });
1204
+ });
1205
+
1206
+ describe('calcul_3cl - rdim de l’installation ECS individuelle unique d’un immeuble (#288)', () => {
1207
+ test('reconstitution appelée avec le type de bâtiment, les installations ECS et SH, avant le calcul ECS', () => {
1208
+ const dpe = makeDpe();
1209
+ dpe.logement.caracteristique_generale.surface_habitable_immeuble = 640;
1210
+ // la reconstitution doit précéder le calcul de l'ECS (besoin / rdim)
1211
+ vi.mocked(reconstituerRdimEcsIndividuelleUnique).mockImplementationOnce(() => {
1212
+ expect(calc_ecs).not.toHaveBeenCalled();
1213
+ return false;
1214
+ });
1215
+
1216
+ calcul_3cl(dpe, { sanitize: false });
1217
+
1218
+ expect(reconstituerRdimEcsIndividuelleUnique).toHaveBeenCalledWith(
1219
+ 'maison',
1220
+ dpe.logement.installation_ecs_collection.installation_ecs,
1221
+ 640
1222
+ );
1223
+ expect(calc_ecs).toHaveBeenCalled();
1224
+ });
1225
+ });
1226
+
1101
1227
  describe('get_conso_coeff_1_7_2027', () => {
1102
1228
  test('projette la conso EP 2027 pour une maison', () => {
1103
1229
  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.59",
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
  },