@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.
- package/17.2_rdim_dpe_genere_immeuble.js +73 -0
- package/17.2_rdim_dpe_genere_immeuble.spec.js +100 -0
- package/9_chauffage.js +3 -1
- package/9_chauffage.spec.js +9 -0
- package/README.md +50 -43
- package/conso.js +8 -3
- package/conso.spec.js +60 -0
- package/engine.js +21 -6
- package/engine.spec.js +100 -0
- package/package.json +2 -2
|
@@ -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 =
|
|
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
|
|
package/9_chauffage.spec.js
CHANGED
|
@@ -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
|
·
|
|
26
|
-
<a href="https://open3cl.github.io/engine/
|
|
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
|
|
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
|
|
143
|
-
|
|
|
144
|
-
| [
|
|
145
|
-
| [
|
|
146
|
-
| [@open3cl/engine](https://www.npmjs.com/package/@open3cl/engine)
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
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.
|
|
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,
|
|
407
|
-
<td align="center"><strong>4,
|
|
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>
|
|
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** |
|
|
416
|
-
| **Appartement · chauffage individuel (2025)** | 3,
|
|
417
|
-
| **Appartement · chauffage collectif (2025)** | 4,
|
|
418
|
-
| **Logement individuel (2025)** |
|
|
419
|
-
| **Maison individuelle (2025)** |
|
|
420
|
-
| **Immeuble · chauffage collectif** |
|
|
421
|
-
| **Immeuble · chauffage individuel** |
|
|
422
|
-
| **Immeuble · chauffage mixte** |
|
|
423
|
-
| **Individuel généré depuis l'immeuble (2026)** | 6,
|
|
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
|
-
|
|
|
457
|
-
|
|
|
458
|
-
|
|
|
459
|
-
|
|
|
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.
|
|
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
|
-
|
|
357
|
+
thCalc === 'immeuble' && ecs.every((e) => e.donnee_entree.enum_type_installation_id === '1');
|
|
343
358
|
|
|
344
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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": "
|
|
87
|
+
"license": "MIT",
|
|
88
88
|
"engines": {
|
|
89
89
|
"node": ">=24.14.1"
|
|
90
90
|
},
|