@open3cl/engine 1.8.33 → 1.8.34
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/11_ecs.js +49 -1
- package/11_ecs.spec.js +133 -0
- package/README.md +19 -19
- package/engine.js +1 -1
- package/package.json +1 -1
package/11_ecs.js
CHANGED
|
@@ -136,6 +136,49 @@ function calc_auxiliaire_distribution_ecs_immeuble(de, surfaceImmeuble, becsParM
|
|
|
136
136
|
return 0;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
/** Écart relatif toléré entre la somme des surfaces ECS exportées et SH. */
|
|
140
|
+
const TOLERANCE_SURFACE_ECS = 0.05;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Ratio de besoin ECS d'une installation individuelle d'un immeuble (par unité dimensionnée).
|
|
144
|
+
*
|
|
145
|
+
* Tribu (Moteur_DPE.dll, Calcul_batiment.Calcul_Cecs) : Ratio = Sh_ecs × Rdim / SH puis
|
|
146
|
+
* Becs = Becs_immeuble × Ratio / Rdim, la consommation étant remultipliée par rdim à l'agrégation
|
|
147
|
+
* (conso.js, cle_repartition_ecs). Les logiciels n'exportent pas tous Sh_ecs de la même façon :
|
|
148
|
+
* - surface d'un logement (Σ Sh × rdim ≈ SH) → ratio = Sh / SH ;
|
|
149
|
+
* - surface totale desservie par l'installation (Σ Sh ≈ SH) → ratio = Sh / SH / rdim.
|
|
150
|
+
* La convention est détectée sur l'ensemble des installations ECS du DPE. Repli sur
|
|
151
|
+
* 1 / nombre d'appartements si la surface est absente, si aucune des deux sommes ne retrouve SH
|
|
152
|
+
* (à TOLERANCE_SURFACE_ECS près) ou si le ratio est ≥ 1.
|
|
153
|
+
* @see Methode_de_calcul_3CL_DPE_2021-338.pdf - §11.4
|
|
154
|
+
*/
|
|
155
|
+
export function ratio_besoin_ecs_individuel(dpe, de, surfaceImmeuble, nombreAppartements) {
|
|
156
|
+
const sh = Number(de.surface_habitable);
|
|
157
|
+
const SH = Number(surfaceImmeuble);
|
|
158
|
+
if (!(sh > 0 && SH > 0)) return 1 / nombreAppartements;
|
|
159
|
+
|
|
160
|
+
const installations = dpe?.logement?.installation_ecs_collection?.installation_ecs || [
|
|
161
|
+
{ donnee_entree: de }
|
|
162
|
+
];
|
|
163
|
+
const des = installations.map((inst) => inst.donnee_entree);
|
|
164
|
+
const sommeSh = des.reduce((acc, d) => acc + (Number(d.surface_habitable) || 0), 0);
|
|
165
|
+
const sommeShRdim = des.reduce(
|
|
166
|
+
(acc, d) => acc + (Number(d.surface_habitable) || 0) * (Number(d.rdim) || 1),
|
|
167
|
+
0
|
|
168
|
+
);
|
|
169
|
+
const ecartParLogement = Math.abs(sommeShRdim - SH);
|
|
170
|
+
const ecartParInstallation = Math.abs(sommeSh - SH);
|
|
171
|
+
const surfaceParLogement = ecartParLogement < ecartParInstallation;
|
|
172
|
+
|
|
173
|
+
// Surfaces exportées incohérentes avec SH (installations partielles, surface moyenne…) : repli
|
|
174
|
+
if (Math.min(ecartParLogement, ecartParInstallation) > TOLERANCE_SURFACE_ECS * SH) {
|
|
175
|
+
return 1 / nombreAppartements;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const ratio = surfaceParLogement ? sh / SH : sh / SH / (Number(de.rdim) || 1);
|
|
179
|
+
return ratio < 1 ? ratio : 1 / nombreAppartements;
|
|
180
|
+
}
|
|
181
|
+
|
|
139
182
|
export default function calc_ecs(
|
|
140
183
|
dpe,
|
|
141
184
|
ecs,
|
|
@@ -173,7 +216,12 @@ export default function calc_ecs(
|
|
|
173
216
|
di.ratio_besoin_ecs = de.cle_repartition_ecs || 1;
|
|
174
217
|
} else if (isImmeubleSystemEcsIndividuels) {
|
|
175
218
|
if (nombreAppartements) {
|
|
176
|
-
di.ratio_besoin_ecs =
|
|
219
|
+
di.ratio_besoin_ecs = ratio_besoin_ecs_individuel(
|
|
220
|
+
dpe,
|
|
221
|
+
de,
|
|
222
|
+
surfaceImmeuble,
|
|
223
|
+
nombreAppartements
|
|
224
|
+
);
|
|
177
225
|
} else {
|
|
178
226
|
di.ratio_besoin_ecs =
|
|
179
227
|
((de.surface_habitable / (surfaceImmeuble || 1)) * (de.rdim || 1)) /
|
package/11_ecs.spec.js
CHANGED
|
@@ -82,6 +82,139 @@ describe('calc_ecs - ratio de besoin ECS', () => {
|
|
|
82
82
|
expect(ecs.donnee_intermediaire.besoin_ecs).toBeCloseTo(25, 9);
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Immeuble à ECS individuelles de surfaces différentes (cas CSTB IC1-0-3 / IC1-0-5 : 4
|
|
87
|
+
* appartements de 20, 30, 45 et 80 m² sur 175 m², rdim 1) : besoin au prorata de la surface
|
|
88
|
+
* desservie, comme Tribu (Becs appartement 1 = Becs immeuble × 20 / 175).
|
|
89
|
+
* @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §11.4
|
|
90
|
+
*/
|
|
91
|
+
test('immeuble à ECS individuelles : ratio = surface desservie / surface de l’immeuble', () => {
|
|
92
|
+
const des = [20, 30, 45, 80].map((surface) => ({
|
|
93
|
+
tv_rendement_distribution_ecs_id: '5',
|
|
94
|
+
surface_habitable: surface,
|
|
95
|
+
rdim: 1
|
|
96
|
+
}));
|
|
97
|
+
const dpe = {
|
|
98
|
+
logement: { installation_ecs_collection: { installation_ecs: des.map((de) => makeEcs(de)) } }
|
|
99
|
+
};
|
|
100
|
+
const ratios = dpe.logement.installation_ecs_collection.installation_ecs.map((ecs) => {
|
|
101
|
+
calc_ecs(dpe, ecs, 100, 200, 1, 'ca1', 'h1a', 'immeuble', false, 175, 4, true);
|
|
102
|
+
return ecs.donnee_intermediaire;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
expect(ratios[0].ratio_besoin_ecs).toBeCloseTo(20 / 175, 9);
|
|
106
|
+
expect(ratios[0].besoin_ecs).toBeCloseTo((100 * 20) / 175, 9);
|
|
107
|
+
expect(ratios[0].besoin_ecs_depensier).toBeCloseTo((200 * 20) / 175, 9);
|
|
108
|
+
expect(ratios[3].ratio_besoin_ecs).toBeCloseTo(80 / 175, 9);
|
|
109
|
+
expect(ratios.reduce((acc, di) => acc + di.ratio_besoin_ecs, 0)).toBeCloseTo(1, 9);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('immeuble à ECS individuelles, surface d’un logement exportée (Σ Sh × rdim = SH) : ratio = Sh / SH', () => {
|
|
113
|
+
// 3 installations de 100 m² par logement, rdim 2, 2 et 1 → 500 m²
|
|
114
|
+
const dpe = {
|
|
115
|
+
logement: {
|
|
116
|
+
installation_ecs_collection: {
|
|
117
|
+
installation_ecs: [2, 2, 1].map((rdim) =>
|
|
118
|
+
makeEcs({ tv_rendement_distribution_ecs_id: '5', surface_habitable: 100, rdim })
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
const [ecs] = dpe.logement.installation_ecs_collection.installation_ecs;
|
|
124
|
+
calc_ecs(dpe, ecs, 100, 200, 1, 'ca1', 'h1a', 'immeuble', false, 500, 5, true);
|
|
125
|
+
|
|
126
|
+
// besoin par logement ; la conso est remultipliée par rdim à l'agrégation
|
|
127
|
+
expect(ecs.donnee_intermediaire.ratio_besoin_ecs).toBeCloseTo(100 / 500, 9);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('immeuble à ECS individuelles, surface desservie totale exportée (Σ Sh = SH) : ratio = Sh / SH / rdim', () => {
|
|
131
|
+
// 2 installations desservant 300 m² (rdim 3) et 200 m² (rdim 2) → 500 m²
|
|
132
|
+
const dpe = {
|
|
133
|
+
logement: {
|
|
134
|
+
installation_ecs_collection: {
|
|
135
|
+
installation_ecs: [
|
|
136
|
+
makeEcs({ tv_rendement_distribution_ecs_id: '5', surface_habitable: 300, rdim: 3 }),
|
|
137
|
+
makeEcs({ tv_rendement_distribution_ecs_id: '5', surface_habitable: 200, rdim: 2 })
|
|
138
|
+
]
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const [ecs] = dpe.logement.installation_ecs_collection.installation_ecs;
|
|
143
|
+
calc_ecs(dpe, ecs, 100, 200, 1, 'ca1', 'h1a', 'immeuble', false, 500, 5, true);
|
|
144
|
+
|
|
145
|
+
expect(ecs.donnee_intermediaire.ratio_besoin_ecs).toBeCloseTo(300 / 500 / 3, 9);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('immeuble à ECS individuelles, rdim absent : rdim pris à 1', () => {
|
|
149
|
+
// 4 installations de 50 m² sans rdim : Σ Sh = 200 m² = SH
|
|
150
|
+
const dpe = {
|
|
151
|
+
logement: {
|
|
152
|
+
installation_ecs_collection: {
|
|
153
|
+
installation_ecs: [1, 2, 3, 4].map(() =>
|
|
154
|
+
makeEcs({ tv_rendement_distribution_ecs_id: '5', surface_habitable: 50 })
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
const [ecs] = dpe.logement.installation_ecs_collection.installation_ecs;
|
|
160
|
+
calc_ecs(dpe, ecs, 100, 200, 1, 'ca1', 'h1a', 'immeuble', false, 200, 5, true);
|
|
161
|
+
|
|
162
|
+
expect(ecs.donnee_intermediaire.ratio_besoin_ecs).toBeCloseTo(50 / 200, 9);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('immeuble à ECS individuelles, installation voisine sans surface ni rdim : ignorée dans la détection', () => {
|
|
166
|
+
const dpe = {
|
|
167
|
+
logement: {
|
|
168
|
+
installation_ecs_collection: {
|
|
169
|
+
installation_ecs: [
|
|
170
|
+
makeEcs({ tv_rendement_distribution_ecs_id: '5', surface_habitable: 100, rdim: 2 }),
|
|
171
|
+
makeEcs({ tv_rendement_distribution_ecs_id: '5' })
|
|
172
|
+
]
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
const [ecs] = dpe.logement.installation_ecs_collection.installation_ecs;
|
|
177
|
+
calc_ecs(dpe, ecs, 100, 200, 1, 'ca1', 'h1a', 'immeuble', false, 200, 2, true);
|
|
178
|
+
|
|
179
|
+
// Σ Sh × rdim = 200 = SH → surface par logement
|
|
180
|
+
expect(ecs.donnee_intermediaire.ratio_besoin_ecs).toBeCloseTo(100 / 200, 9);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('immeuble à ECS individuelles, surfaces exportées incohérentes avec SH → repli 1 / nombre d’appartements', () => {
|
|
184
|
+
// 2 installations de 88,67 m², rdim 1,33 (LICIEL) : Σ Sh = 177, Σ Sh × rdim = 236 ≠ 266 m²
|
|
185
|
+
const dpe = {
|
|
186
|
+
logement: {
|
|
187
|
+
installation_ecs_collection: {
|
|
188
|
+
installation_ecs: [1, 2].map(() =>
|
|
189
|
+
makeEcs({
|
|
190
|
+
tv_rendement_distribution_ecs_id: '5',
|
|
191
|
+
surface_habitable: 88.67,
|
|
192
|
+
rdim: 1.333
|
|
193
|
+
})
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
const [ecs] = dpe.logement.installation_ecs_collection.installation_ecs;
|
|
199
|
+
calc_ecs(dpe, ecs, 100, 200, 1, 'ca1', 'h1a', 'immeuble', false, 266, 4, true);
|
|
200
|
+
|
|
201
|
+
expect(ecs.donnee_intermediaire.ratio_besoin_ecs).toBe(0.25);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('immeuble à ECS individuelles, surface couvrant tout l’immeuble → repli 1 / nombre d’appartements', () => {
|
|
205
|
+
const ecs = makeEcs({ tv_rendement_distribution_ecs_id: '5', surface_habitable: 175 });
|
|
206
|
+
calc_ecs({}, ecs, 100, 200, 1, 'ca1', 'h1a', 'immeuble', false, 175, 4, true);
|
|
207
|
+
|
|
208
|
+
expect(ecs.donnee_intermediaire.ratio_besoin_ecs).toBe(0.25);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('immeuble à ECS individuelles, surface de l’immeuble absente → repli 1 / nombre d’appartements', () => {
|
|
212
|
+
const ecs = makeEcs({ tv_rendement_distribution_ecs_id: '5', surface_habitable: 20 });
|
|
213
|
+
calc_ecs({}, ecs, 100, 200, 1, 'ca1', 'h1a', 'immeuble', false, null, 4, true);
|
|
214
|
+
|
|
215
|
+
expect(ecs.donnee_intermediaire.ratio_besoin_ecs).toBe(0.25);
|
|
216
|
+
});
|
|
217
|
+
|
|
85
218
|
test('présence de rdim : ratio = 1 / rdim', () => {
|
|
86
219
|
const de = { tv_rendement_distribution_ecs_id: '5', rdim: 2 };
|
|
87
220
|
const ecs = makeEcs(de);
|
package/README.md
CHANGED
|
@@ -370,21 +370,21 @@ Ces résultats sont **générés automatiquement** à la fin de `npm run test:co
|
|
|
370
370
|
|
|
371
371
|
<!-- CORPUS:START -->
|
|
372
372
|
|
|
373
|
-
> **Version `1.8.
|
|
373
|
+
> **Version `1.8.33`** · branche `main` · généré le 2026-10-09
|
|
374
374
|
> Seuil de tolérance **5%**
|
|
375
375
|
|
|
376
376
|
<table>
|
|
377
377
|
<tr>
|
|
378
378
|
<td align="center"><strong>9</strong><br/><sub>corpus</sub></td>
|
|
379
|
-
<td align="center"><strong>89
|
|
380
|
-
<td align="center"><strong>60
|
|
381
|
-
<td align="center"><strong>67,
|
|
379
|
+
<td align="center"><strong>89 986</strong><br/><sub>DPE analysés</sub></td>
|
|
380
|
+
<td align="center"><strong>60 961</strong><br/><sub>DPE conformes</sub></td>
|
|
381
|
+
<td align="center"><strong>67,74 %</strong><br/><sub>réussite globale</sub></td>
|
|
382
382
|
</tr>
|
|
383
383
|
</table>
|
|
384
384
|
|
|
385
385
|
| | Corpus | Réussite | | DPE conformes |
|
|
386
386
|
| :-: | :---------------------------------------------------------------------------------------------------------------------- | ----------: | :--------------------- | -------------: |
|
|
387
|
-
| 🔴 | **Généraliste**<br/><sub>`corpus_dpe.csv`</sub> | **46,
|
|
387
|
+
| 🔴 | **Généraliste**<br/><sub>`corpus_dpe.csv`</sub> | **46,13 %** | `█████████░░░░░░░░░░░` | 4 613 / 9 999 |
|
|
388
388
|
| 🟢 | **Appartement · chauffage individuel (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_individuel_2025.csv`</sub> | **92,53 %** | `███████████████████░` | 9 253 / 10 000 |
|
|
389
389
|
| 🟢 | **Logement individuel (2025)**<br/><sub>`dpe_logement_individuel_2025.csv`</sub> | **89,54 %** | `██████████████████░░` | 8 947 / 9 992 |
|
|
390
390
|
| 🟢 | **Maison individuelle (2025)**<br/><sub>`dpe_maison_individuelle_2025.csv`</sub> | **88,60 %** | `██████████████████░░` | 8 859 / 9 999 |
|
|
@@ -398,29 +398,29 @@ Ces résultats sont **générés automatiquement** à la fin de `npm run test:co
|
|
|
398
398
|
|
|
399
399
|
#### Temps d’exécution
|
|
400
400
|
|
|
401
|
-
> Durée de l’appel à `calcul_3cl` par DPE, sur 89
|
|
401
|
+
> Durée de l’appel à `calcul_3cl` par DPE, sur 89 952 calculs.
|
|
402
402
|
> La copie défensive de l’entrée et la lecture du fichier sont exclues de la mesure.
|
|
403
403
|
|
|
404
404
|
<table>
|
|
405
405
|
<tr>
|
|
406
|
-
<td align="center"><strong>5,
|
|
407
|
-
<td align="center"><strong>
|
|
408
|
-
<td align="center"><strong>0,
|
|
409
|
-
<td align="center"><strong>
|
|
406
|
+
<td align="center"><strong>5,69 ms</strong><br/><sub>moyenne</sub></td>
|
|
407
|
+
<td align="center"><strong>5,00 ms</strong><br/><sub>médiane</sub></td>
|
|
408
|
+
<td align="center"><strong>0,11 ms</strong><br/><sub>min</sub></td>
|
|
409
|
+
<td align="center"><strong>479,4 ms</strong><br/><sub>max</sub></td>
|
|
410
410
|
</tr>
|
|
411
411
|
</table>
|
|
412
412
|
|
|
413
413
|
| Corpus | Moyenne | Médiane | Min | Max | p95 | p99 |
|
|
414
414
|
| :--------------------------------------------- | ------: | ------: | ------: | -------: | ------: | ------: |
|
|
415
|
-
| **Généraliste** |
|
|
416
|
-
| **Appartement · chauffage individuel (2025)** |
|
|
417
|
-
| **Logement individuel (2025)** |
|
|
418
|
-
| **Maison individuelle (2025)** |
|
|
419
|
-
| **Immeuble · chauffage individuel** |
|
|
420
|
-
| **Appartement · chauffage collectif (2025)** | 4,
|
|
421
|
-
| **Immeuble · chauffage collectif** |
|
|
422
|
-
| **Immeuble · chauffage mixte** | 5,
|
|
423
|
-
| **Individuel généré depuis l'immeuble (2026)** |
|
|
415
|
+
| **Généraliste** | 5,39 ms | 4,95 ms | 0,25 ms | 38,8 ms | 9,87 ms | 13,9 ms |
|
|
416
|
+
| **Appartement · chauffage individuel (2025)** | 4,01 ms | 3,64 ms | 1,41 ms | 28,6 ms | 6,92 ms | 9,06 ms |
|
|
417
|
+
| **Logement individuel (2025)** | 5,43 ms | 4,82 ms | 1,69 ms | 37,7 ms | 10,1 ms | 14,1 ms |
|
|
418
|
+
| **Maison individuelle (2025)** | 7,12 ms | 6,44 ms | 2,19 ms | 48,5 ms | 12,6 ms | 16,7 ms |
|
|
419
|
+
| **Immeuble · chauffage individuel** | 7,03 ms | 5,96 ms | 0,11 ms | 81,7 ms | 14,9 ms | 22,9 ms |
|
|
420
|
+
| **Appartement · chauffage collectif (2025)** | 4,34 ms | 3,91 ms | 1,41 ms | 49,2 ms | 7,18 ms | 9,95 ms |
|
|
421
|
+
| **Immeuble · chauffage collectif** | 6,26 ms | 5,48 ms | 0,16 ms | 139,4 ms | 11,5 ms | 20,2 ms |
|
|
422
|
+
| **Immeuble · chauffage mixte** | 5,33 ms | 4,36 ms | 0,18 ms | 479,4 ms | 11,0 ms | 17,8 ms |
|
|
423
|
+
| **Individuel généré depuis l'immeuble (2026)** | 6,30 ms | 5,41 ms | 1,57 ms | 43,6 ms | 12,2 ms | 18,1 ms |
|
|
424
424
|
|
|
425
425
|
<sub>La moyenne est tirée vers le haut par les DPE collectifs, dont le coût atteint plusieurs
|
|
426
426
|
dizaines de fois la médiane : c’est la médiane qui décrit le cas courant, et p95/p99 le cas
|
package/engine.js
CHANGED
|
@@ -33,7 +33,7 @@ import getFicheTechnique from './ficheTechnique.js';
|
|
|
33
33
|
import { ProductionENR } from './16.2_production_enr.js';
|
|
34
34
|
import DpeSanitizerService from './dpe-sanitizer.service.js';
|
|
35
35
|
|
|
36
|
-
const LIB_VERSION = '1.8.
|
|
36
|
+
const LIB_VERSION = '1.8.34';
|
|
37
37
|
|
|
38
38
|
const dpeSanitizerService = new DpeSanitizerService();
|
|
39
39
|
|