@open3cl/engine 1.9.1 → 1.10.1
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/15_conso_aux.js +18 -18
- package/15_conso_aux.spec.js +1 -1
- package/4_ventilation.js +19 -3
- package/4_ventilation.spec.js +83 -6
- package/README.md +34 -27
- package/engine.js +10 -3
- package/engine.spec.js +48 -0
- package/features/engine/domain/enveloppe/ventilation/deperdition-ventilation.service.js +4 -2
- package/features/engine/domain/enveloppe/ventilation/deperdition-ventilation.service.spec.js +25 -5
- package/package.json +1 -1
- package/utils.js +23 -0
- package/utils.spec.js +15 -0
package/15_conso_aux.js
CHANGED
|
@@ -368,6 +368,24 @@ export function conso_aux_distribution_ecs(
|
|
|
368
368
|
) {
|
|
369
369
|
const typeInstallation = parseInt(de.enum_type_installation_id);
|
|
370
370
|
|
|
371
|
+
const ratioVirtu = parseFloat(de.ratio_virtualisation);
|
|
372
|
+
let Sh_immeuble_ratio = Sh_immeuble;
|
|
373
|
+
let Sh_install = de.surface_habitable || Sh_logement;
|
|
374
|
+
let Niv_inst_ecs = de.nombre_niveau_installation_ecs || 1;
|
|
375
|
+
|
|
376
|
+
if (!(Sh_immeuble > Sh_logement) && ratioVirtu > 0 && ratioVirtu < 1) {
|
|
377
|
+
Sh_immeuble_ratio = Sh_logement / ratioVirtu;
|
|
378
|
+
Sh_install = Sh_logement;
|
|
379
|
+
Niv_inst_ecs = nombre_niveau_immeuble || Niv_inst_ecs;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Etape 3: Lb - longueur par défaut du bouclage ECS (m)
|
|
383
|
+
// Sh est la surface habitable des logements desservis par l'installation d'ECS
|
|
384
|
+
// Pour l'appartement, on utilise la surface à l'échelle de l'immeuble
|
|
385
|
+
const Sh = (Sh_install / Sh_logement) * Sh_immeuble_ratio;
|
|
386
|
+
const Lb = 4 * Math.pow(Sh / Niv_inst_ecs, 0.5) + 6 * (Niv_inst_ecs - 0.5);
|
|
387
|
+
di.lb = Lb;
|
|
388
|
+
|
|
371
389
|
if (typeInstallation === 1) {
|
|
372
390
|
di.conso_auxiliaire_distribution_ecs = 0;
|
|
373
391
|
return;
|
|
@@ -402,24 +420,6 @@ export function conso_aux_distribution_ecs(
|
|
|
402
420
|
const ca = enums.classe_altitude[caId];
|
|
403
421
|
const zc = enums.zone_climatique[zcId];
|
|
404
422
|
|
|
405
|
-
const ratioVirtu = parseFloat(de.ratio_virtualisation);
|
|
406
|
-
let Sh_immeuble_ratio = Sh_immeuble;
|
|
407
|
-
let Sh_install = de.surface_habitable || Sh_logement;
|
|
408
|
-
let Niv_inst_ecs = de.nombre_niveau_installation_ecs || 1;
|
|
409
|
-
|
|
410
|
-
if (!(Sh_immeuble > Sh_logement) && ratioVirtu > 0 && ratioVirtu < 1) {
|
|
411
|
-
Sh_immeuble_ratio = Sh_logement / ratioVirtu;
|
|
412
|
-
Sh_install = Sh_logement;
|
|
413
|
-
Niv_inst_ecs = nombre_niveau_immeuble || Niv_inst_ecs;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// Etape 3: Lb - longueur par défaut du bouclage ECS (m)
|
|
417
|
-
// Sh est la surface habitable des logements desservis par l'installation d'ECS
|
|
418
|
-
// Pour l'appartement, on utilise la surface à l'échelle de l'immeuble
|
|
419
|
-
const Sh = (Sh_install / Sh_logement) * Sh_immeuble_ratio;
|
|
420
|
-
const Lb = 4 * Math.pow(Sh / Niv_inst_ecs, 0.5) + 6 * (Niv_inst_ecs - 0.5);
|
|
421
|
-
di.lb = Lb;
|
|
422
|
-
|
|
423
423
|
// Etape 4: DeltaPb (kPa) - perte de charge dans le bouclage
|
|
424
424
|
const DeltaPb = 0.2 * Lb + 10;
|
|
425
425
|
|
package/15_conso_aux.spec.js
CHANGED
|
@@ -874,7 +874,7 @@ describe("conso_aux_distribution_ecs - auxiliaires de distribution d'ECS", () =>
|
|
|
874
874
|
])('%s : pas de longueur de bouclage lb', (_libelle, de) => {
|
|
875
875
|
const di = { besoin_ecs: 1000 };
|
|
876
876
|
conso_aux_distribution_ecs({}, de, di, 50, 2000, 1, 1, 30);
|
|
877
|
-
expect(di.lb).
|
|
877
|
+
expect(di.lb).toBeCloseTo(181.88543819998318);
|
|
878
878
|
});
|
|
879
879
|
|
|
880
880
|
test('plus de niveaux desservis : bouclage plus court, donc consommation plus faible', () => {
|
package/4_ventilation.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
tv,
|
|
3
|
+
requestInputID,
|
|
4
|
+
requestInput,
|
|
5
|
+
bug_for_bug_compat,
|
|
6
|
+
compat_for_corpus_analysis
|
|
7
|
+
} from './utils.js';
|
|
2
8
|
import calc_pvent from './5_conso_ventilation.js';
|
|
3
9
|
|
|
4
10
|
function tv_debits_ventilation(di, de, du) {
|
|
@@ -119,6 +125,11 @@ function tv_q4pa_conv(di, de, cg, mur_list, ph_list, porte_list, bv_list) {
|
|
|
119
125
|
* Si la ligne saisie correspond à la période de construction du DPE, on la reprend en entier
|
|
120
126
|
* (isolation et joints) pour reproduire le calcul du logiciel.
|
|
121
127
|
*
|
|
128
|
+
* Cette reprise n'est faite qu'avec l'option `compat_for_corpus_analysis` (analyse du corpus
|
|
129
|
+
* ADEME). Par défaut, la ligne contradictoire est seulement signalée et q4pa_conv est
|
|
130
|
+
* recalculé selon la méthode : un contrôle de cohérence du DPE doit détecter l'erreur et un
|
|
131
|
+
* scénario de travaux ne doit pas la propager.
|
|
132
|
+
*
|
|
122
133
|
* Sens unique : la reprise ne s'applique que si la ligne indique des surfaces isolées alors que
|
|
123
134
|
* le recalcul les donne non isolées. Dans le sens inverse (ligne « non isolées », recalcul
|
|
124
135
|
* « isolées »), le DPE a pu être modifié par l'appelant (scénario d'isolation des parois) :
|
|
@@ -127,6 +138,7 @@ function tv_q4pa_conv(di, de, cg, mur_list, ph_list, porte_list, bv_list) {
|
|
|
127
138
|
*/
|
|
128
139
|
if (rowQ4paConv.isolation_surfaces !== isolation_surfaces) {
|
|
129
140
|
if (
|
|
141
|
+
compat_for_corpus_analysis &&
|
|
130
142
|
isLigneQ4paSurfacesIsolees(rowQ4paConv, isolation_surfaces) &&
|
|
131
143
|
isLigneQ4paMemePeriode(rowQ4paConv, cg)
|
|
132
144
|
) {
|
|
@@ -147,10 +159,14 @@ function tv_q4pa_conv(di, de, cg, mur_list, ph_list, porte_list, bv_list) {
|
|
|
147
159
|
* Issue #294 : aucune menuiserie n'exporte `presence_joint`. Aucune donnée ne contredit alors
|
|
148
160
|
* la ligne q4pa_conv retenue par le logiciel (ex. ligne 12 « avant 1948 avec joints », 2,5) :
|
|
149
161
|
* on reprend sa valeur `presence_joints_menuiserie` au lieu de considérer les menuiseries
|
|
150
|
-
* sans joint.
|
|
162
|
+
* sans joint. Uniquement avec l'option `compat_for_corpus_analysis`.
|
|
151
163
|
* @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §4
|
|
152
164
|
*/
|
|
153
|
-
if (
|
|
165
|
+
if (
|
|
166
|
+
compat_for_corpus_analysis &&
|
|
167
|
+
!ligneDpeReprise &&
|
|
168
|
+
presenceJointNonRenseignee(porte_list, bv_list)
|
|
169
|
+
) {
|
|
154
170
|
pjt = rowQ4paConv.presence_joints_menuiserie === '1' ? '1' : '0';
|
|
155
171
|
} else if (!ligneDpeReprise && rowQ4paConv.presence_joints_menuiserie !== pjt) {
|
|
156
172
|
console.error(
|
package/4_ventilation.spec.js
CHANGED
|
@@ -4,7 +4,8 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
|
|
4
4
|
* Dépendances mockées pour isoler le module de ventilation :
|
|
5
5
|
* - `tv` : accès aux tables `debits_ventilation` / `q4pa_conv`, on contrôle les lignes ;
|
|
6
6
|
* - `requestInput` / `requestInputID` : passe-plats vers les données d'entrée ;
|
|
7
|
-
* - `bug_for_bug_compat` :
|
|
7
|
+
* - `bug_for_bug_compat` / `compat_for_corpus_analysis` : désactivés pour isoler le comportement
|
|
8
|
+
* nominal ;
|
|
8
9
|
* - `calc_pvent` (5_conso_ventilation.js) : espion, la conso de ventilation est testée ailleurs.
|
|
9
10
|
*/
|
|
10
11
|
vi.mock('./utils.js', () => ({
|
|
@@ -12,7 +13,8 @@ vi.mock('./utils.js', () => ({
|
|
|
12
13
|
tv: vi.fn(),
|
|
13
14
|
requestInput: (de, du, field) => de[field],
|
|
14
15
|
requestInputID: (de, du, field) => de[`enum_${field}_id`],
|
|
15
|
-
bug_for_bug_compat: false
|
|
16
|
+
bug_for_bug_compat: false,
|
|
17
|
+
compat_for_corpus_analysis: false
|
|
16
18
|
}));
|
|
17
19
|
|
|
18
20
|
vi.mock('./5_conso_ventilation.js', () => ({
|
|
@@ -301,10 +303,12 @@ describe('calc_ventilation - orchestration', () => {
|
|
|
301
303
|
*/
|
|
302
304
|
describe('calc_ventilation - compatibilité bug_for_bug_compat activée', () => {
|
|
303
305
|
/**
|
|
304
|
-
* Recharge le module avec `bug_for_bug_compat = true
|
|
305
|
-
*
|
|
306
|
+
* Recharge le module avec `bug_for_bug_compat = true` et `compat_for_corpus_analysis` (vrai par
|
|
307
|
+
* défaut : reprise de la ligne du DPE pour l'analyse de corpus). Le double de `tv` renvoie une
|
|
308
|
+
* ligne `q4pa_conv` distincte selon que le matcher cible l'identifiant déjà saisi ou le matcher
|
|
309
|
+
* complet.
|
|
306
310
|
*/
|
|
307
|
-
async function chargerAvecBug(rowParId) {
|
|
311
|
+
async function chargerAvecBug(rowParId, compat = true) {
|
|
308
312
|
vi.resetModules();
|
|
309
313
|
const tvBug = vi.fn((table, matcher) => {
|
|
310
314
|
if (table === 'debits_ventilation') return { ...ROW_DEBITS };
|
|
@@ -318,7 +322,8 @@ describe('calc_ventilation - compatibilité bug_for_bug_compat activée', () =>
|
|
|
318
322
|
tv: tvBug,
|
|
319
323
|
requestInput: (de, du, field) => de[field],
|
|
320
324
|
requestInputID: (de, du, field) => de[`enum_${field}_id`],
|
|
321
|
-
bug_for_bug_compat: true
|
|
325
|
+
bug_for_bug_compat: true,
|
|
326
|
+
compat_for_corpus_analysis: compat
|
|
322
327
|
}));
|
|
323
328
|
vi.doMock('./5_conso_ventilation.js', () => ({ default: vi.fn() }));
|
|
324
329
|
const { default: calc_ventilation_bug } = await import('./4_ventilation.js');
|
|
@@ -625,6 +630,78 @@ describe('calc_ventilation - compatibilité bug_for_bug_compat activée', () =>
|
|
|
625
630
|
errorSpy.mockRestore();
|
|
626
631
|
});
|
|
627
632
|
});
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Sans l'option `compat_for_corpus_analysis` (comportement par défaut) : la ligne q4pa_conv du DPE
|
|
636
|
+
* n'est jamais reprise, q4pa_conv est recalculé selon la méthode. Les écarts sont seulement
|
|
637
|
+
* signalés (contrôle de cohérence d'un DPE, scénarios de travaux).
|
|
638
|
+
*/
|
|
639
|
+
describe('sans compat_for_corpus_analysis : ligne du DPE jamais reprise', () => {
|
|
640
|
+
const murNonIsole = [
|
|
641
|
+
{
|
|
642
|
+
donnee_intermediaire: { b: 1 },
|
|
643
|
+
donnee_entree: { enum_type_isolation_id: '2', surface_paroi_opaque: 100 }
|
|
644
|
+
}
|
|
645
|
+
];
|
|
646
|
+
const vtLigne = (id) => ({
|
|
647
|
+
donnee_entree: { enum_type_ventilation_id: '1', tv_q4pa_conv_id: id, description: 'v' }
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
test('ligne « surfaces isolées » de la même période (#312) : isolation et joints recalculés', async () => {
|
|
651
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
652
|
+
const { calc_ventilation_bug, tvBug } = await chargerAvecBug(
|
|
653
|
+
{
|
|
654
|
+
q4pa_conv: '2',
|
|
655
|
+
tv_q4pa_conv_id: '11',
|
|
656
|
+
enum_periode_construction_id: '1',
|
|
657
|
+
isolation_surfaces: '1',
|
|
658
|
+
presence_joints_menuiserie: '1'
|
|
659
|
+
},
|
|
660
|
+
false
|
|
661
|
+
);
|
|
662
|
+
const baieSansJoint = [{ donnee_entree: { surface_totale_baie: 10, presence_joint: 0 } }];
|
|
663
|
+
|
|
664
|
+
calc_ventilation_bug(vtLigne(11), CG, 'th', 80, 100, murNonIsole, [], [], baieSansJoint);
|
|
665
|
+
|
|
666
|
+
expect(tvBug).toHaveBeenLastCalledWith(
|
|
667
|
+
'q4pa_conv',
|
|
668
|
+
expect.objectContaining({ isolation_surfaces: '0', presence_joints_menuiserie: '0' })
|
|
669
|
+
);
|
|
670
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
671
|
+
expect.stringContaining('La valeur recalculée isolation_surfaces = 0 est utilisée')
|
|
672
|
+
);
|
|
673
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
674
|
+
expect.stringContaining('La valeur recalculée presence_joints_menuiserie = 0 est utilisée')
|
|
675
|
+
);
|
|
676
|
+
expect(errorSpy).not.toHaveBeenCalledWith(expect.stringContaining('est conservée'));
|
|
677
|
+
errorSpy.mockRestore();
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
test('presence_joint non exporté (#294) : joints recalculés à « 0 » et écart signalé', async () => {
|
|
681
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
682
|
+
const { calc_ventilation_bug, tvBug } = await chargerAvecBug(
|
|
683
|
+
{
|
|
684
|
+
q4pa_conv: '2.5',
|
|
685
|
+
tv_q4pa_conv_id: '12',
|
|
686
|
+
isolation_surfaces: '0',
|
|
687
|
+
presence_joints_menuiserie: '1'
|
|
688
|
+
},
|
|
689
|
+
false
|
|
690
|
+
);
|
|
691
|
+
const baies = [{ donnee_entree: { surface_totale_baie: 10 } }];
|
|
692
|
+
|
|
693
|
+
calc_ventilation_bug(vtLigne(12), CG, 'th', 80, 100, murNonIsole, [], [], baies);
|
|
694
|
+
|
|
695
|
+
expect(tvBug).toHaveBeenLastCalledWith(
|
|
696
|
+
'q4pa_conv',
|
|
697
|
+
expect.objectContaining({ isolation_surfaces: '0', presence_joints_menuiserie: '0' })
|
|
698
|
+
);
|
|
699
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
700
|
+
expect.stringContaining('La valeur recalculée presence_joints_menuiserie = 0 est utilisée')
|
|
701
|
+
);
|
|
702
|
+
errorSpy.mockRestore();
|
|
703
|
+
});
|
|
704
|
+
});
|
|
628
705
|
});
|
|
629
706
|
|
|
630
707
|
describe('presenceJointNonRenseignee', () => {
|
package/README.md
CHANGED
|
@@ -251,11 +251,18 @@ const c = calcul_3cl(dpeData, { sanitize: false });
|
|
|
251
251
|
// Depuis un XML ADEME
|
|
252
252
|
const d = calcul_3cl_xml(xmlString);
|
|
253
253
|
const e = calcul_3cl_xml(xmlString, { sanitize: false });
|
|
254
|
+
|
|
255
|
+
// Analyse du corpus ADEME uniquement : reproduire les choix du logiciel de saisie
|
|
256
|
+
const f = calcul_3cl(dpeData, { sanitize: true, compat_for_corpus_analysis: true });
|
|
254
257
|
```
|
|
255
258
|
|
|
256
|
-
| Option
|
|
257
|
-
|
|
|
258
|
-
| `sanitize`
|
|
259
|
+
| Option | Défaut | Effet |
|
|
260
|
+
| :--------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
261
|
+
| `sanitize` | `true` | Normalise et corrige les incohérences du DPE d'entrée avant calcul. Désactivez-le pour un calcul brut. |
|
|
262
|
+
| `compat_for_corpus_analysis` | `false` | Reprend la ligne `q4pa_conv` choisie par le logiciel de saisie même si elle contredit l'isolation des parois ou les joints déclarés, pour reproduire les DPE du corpus ADEME. À n'utiliser que pour l'analyse de corpus. Désactivé, le moteur recalcule `q4pa_conv` selon la méthode (contrôle de cohérence d'un DPE, scénarios de travaux). |
|
|
263
|
+
|
|
264
|
+
> Dès qu'un objet `options` est passé, `sanitize` vaut `false` s'il n'est pas précisé : indiquez
|
|
265
|
+
> `sanitize: true` avec `compat_for_corpus_analysis`.
|
|
259
266
|
|
|
260
267
|
<details>
|
|
261
268
|
<summary><strong>Exemple d'objet JSON d'entrée (partiel)</strong></summary>
|
|
@@ -376,28 +383,28 @@ Ces résultats sont **générés automatiquement** à la fin de `npm run test:co
|
|
|
376
383
|
|
|
377
384
|
<!-- CORPUS:START -->
|
|
378
385
|
|
|
379
|
-
> **Version `1.
|
|
386
|
+
> **Version `1.10.0`** · branche `main` · généré le 2026-10-29
|
|
380
387
|
> Seuil de tolérance **5%**
|
|
381
388
|
|
|
382
389
|
<table>
|
|
383
390
|
<tr>
|
|
384
391
|
<td align="center"><strong>9</strong><br/><sub>corpus</sub></td>
|
|
385
392
|
<td align="center"><strong>88 475</strong><br/><sub>DPE analysés</sub></td>
|
|
386
|
-
<td align="center"><strong>75
|
|
387
|
-
<td align="center"><strong>85,
|
|
393
|
+
<td align="center"><strong>75 700</strong><br/><sub>DPE conformes</sub></td>
|
|
394
|
+
<td align="center"><strong>85,56 %</strong><br/><sub>réussite globale</sub></td>
|
|
388
395
|
</tr>
|
|
389
396
|
</table>
|
|
390
397
|
|
|
391
398
|
| | Corpus | Réussite | | DPE conformes |
|
|
392
399
|
| :-: | :---------------------------------------------------------------------------------------------------------------------- | ----------: | :--------------------- | -------------: |
|
|
393
|
-
| 🟢 | **Appartement · chauffage individuel (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_individuel_2025.csv`</sub> | **95,
|
|
394
|
-
| 🟢 | **Appartement · chauffage collectif (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_collectif_2025.csv`</sub> | **94,
|
|
395
|
-
| 🟢 | **Logement individuel (2025)**<br/><sub>`dpe_logement_individuel_2025.csv`</sub> | **93,
|
|
396
|
-
| 🟢 | **Maison individuelle (2025)**<br/><sub>`dpe_maison_individuelle_2025.csv`</sub> | **92,
|
|
397
|
-
| 🟢 | **Maison individuelle (2026)**<br/><sub>`dpe_maison_individuelle_2026.csv`</sub> | **90,
|
|
398
|
-
| 🟢 | **Immeuble · chauffage collectif**<br/><sub>`dpe_immeuble_chauffage_collectif.csv`</sub> | **89,
|
|
399
|
-
| 🟢 | **Immeuble · chauffage individuel**<br/><sub>`dpe_immeuble_chauffage_individuel.csv`</sub> | **86,
|
|
400
|
-
| 🟡 | **Immeuble · chauffage mixte**<br/><sub>`dpe_immeuble_chauffage_mixte.csv`</sub> | **70,
|
|
400
|
+
| 🟢 | **Appartement · chauffage individuel (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_individuel_2025.csv`</sub> | **95,43 %** | `███████████████████░` | 9 441 / 9 893 |
|
|
401
|
+
| 🟢 | **Appartement · chauffage collectif (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_collectif_2025.csv`</sub> | **94,55 %** | `███████████████████░` | 9 385 / 9 926 |
|
|
402
|
+
| 🟢 | **Logement individuel (2025)**<br/><sub>`dpe_logement_individuel_2025.csv`</sub> | **93,36 %** | `███████████████████░` | 9 180 / 9 833 |
|
|
403
|
+
| 🟢 | **Maison individuelle (2025)**<br/><sub>`dpe_maison_individuelle_2025.csv`</sub> | **92,14 %** | `██████████████████░░` | 9 003 / 9 771 |
|
|
404
|
+
| 🟢 | **Maison individuelle (2026)**<br/><sub>`dpe_maison_individuelle_2026.csv`</sub> | **90,46 %** | `██████████████████░░` | 9 046 / 10 000 |
|
|
405
|
+
| 🟢 | **Immeuble · chauffage collectif**<br/><sub>`dpe_immeuble_chauffage_collectif.csv`</sub> | **89,13 %** | `██████████████████░░` | 8 680 / 9 739 |
|
|
406
|
+
| 🟢 | **Immeuble · chauffage individuel**<br/><sub>`dpe_immeuble_chauffage_individuel.csv`</sub> | **86,04 %** | `█████████████████░░░` | 8 539 / 9 924 |
|
|
407
|
+
| 🟡 | **Immeuble · chauffage mixte**<br/><sub>`dpe_immeuble_chauffage_mixte.csv`</sub> | **70,50 %** | `██████████████░░░░░░` | 6 748 / 9 571 |
|
|
401
408
|
| 🔴 | **Individuel généré depuis l'immeuble (2026)**<br/><sub>`dpe_individuel_a_partir_dpe_immeuble_2026.csv`</sub> | **57,83 %** | `████████████░░░░░░░░` | 5 678 / 9 818 |
|
|
402
409
|
|
|
403
410
|
<sub>🟢 ≥ 85 % · 🟡 ≥ 60 % · 🔴 < 60 %</sub>
|
|
@@ -409,24 +416,24 @@ Ces résultats sont **générés automatiquement** à la fin de `npm run test:co
|
|
|
409
416
|
|
|
410
417
|
<table>
|
|
411
418
|
<tr>
|
|
412
|
-
<td align="center"><strong>
|
|
413
|
-
<td align="center"><strong>
|
|
414
|
-
<td align="center"><strong>0,
|
|
415
|
-
<td align="center"><strong>
|
|
419
|
+
<td align="center"><strong>5,64 ms</strong><br/><sub>moyenne</sub></td>
|
|
420
|
+
<td align="center"><strong>4,87 ms</strong><br/><sub>médiane</sub></td>
|
|
421
|
+
<td align="center"><strong>0,11 ms</strong><br/><sub>min</sub></td>
|
|
422
|
+
<td align="center"><strong>957,0 ms</strong><br/><sub>max</sub></td>
|
|
416
423
|
</tr>
|
|
417
424
|
</table>
|
|
418
425
|
|
|
419
426
|
| Corpus | Moyenne | Médiane | Min | Max | p95 | p99 |
|
|
420
427
|
| :--------------------------------------------- | ------: | ------: | ------: | -------: | ------: | ------: |
|
|
421
|
-
| **Appartement · chauffage individuel (2025)** |
|
|
422
|
-
| **Appartement · chauffage collectif (2025)** |
|
|
423
|
-
| **Logement individuel (2025)** |
|
|
424
|
-
| **Maison individuelle (2025)** |
|
|
425
|
-
| **Maison individuelle (2026)** |
|
|
426
|
-
| **Immeuble · chauffage collectif** |
|
|
427
|
-
| **Immeuble · chauffage individuel** |
|
|
428
|
-
| **Immeuble · chauffage mixte** |
|
|
429
|
-
| **Individuel généré depuis l'immeuble (2026)** |
|
|
428
|
+
| **Appartement · chauffage individuel (2025)** | 4,00 ms | 3,58 ms | 1,42 ms | 34,6 ms | 6,99 ms | 9,30 ms |
|
|
429
|
+
| **Appartement · chauffage collectif (2025)** | 4,46 ms | 4,04 ms | 1,62 ms | 42,4 ms | 7,33 ms | 9,33 ms |
|
|
430
|
+
| **Logement individuel (2025)** | 5,20 ms | 4,57 ms | 1,74 ms | 56,0 ms | 9,72 ms | 13,5 ms |
|
|
431
|
+
| **Maison individuelle (2025)** | 6,01 ms | 5,41 ms | 2,04 ms | 51,8 ms | 10,7 ms | 14,3 ms |
|
|
432
|
+
| **Maison individuelle (2026)** | 6,16 ms | 5,47 ms | 2,09 ms | 46,7 ms | 11,1 ms | 15,1 ms |
|
|
433
|
+
| **Immeuble · chauffage collectif** | 5,81 ms | 4,91 ms | 0,14 ms | 126,7 ms | 10,9 ms | 17,7 ms |
|
|
434
|
+
| **Immeuble · chauffage individuel** | 7,07 ms | 5,99 ms | 0,11 ms | 123,4 ms | 14,5 ms | 22,6 ms |
|
|
435
|
+
| **Immeuble · chauffage mixte** | 5,73 ms | 4,63 ms | 0,20 ms | 957,0 ms | 11,9 ms | 19,7 ms |
|
|
436
|
+
| **Individuel généré depuis l'immeuble (2026)** | 6,33 ms | 5,26 ms | 1,79 ms | 79,2 ms | 12,0 ms | 19,1 ms |
|
|
430
437
|
|
|
431
438
|
<sub>La moyenne est tirée vers le haut par les DPE collectifs, dont le coût atteint plusieurs
|
|
432
439
|
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
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
containsAnySubstring,
|
|
25
25
|
isEffetJoule,
|
|
26
26
|
set_bug_for_bug_compat,
|
|
27
|
+
set_compat_for_corpus_analysis,
|
|
27
28
|
use_enum_as_string,
|
|
28
29
|
useEnumAsString,
|
|
29
30
|
xmlParser
|
|
@@ -35,7 +36,7 @@ import DpeSanitizerService from './dpe-sanitizer.service.js';
|
|
|
35
36
|
import { reconstituerRdimDpeGenereImmeuble } from './17.2_rdim_dpe_genere_immeuble.js';
|
|
36
37
|
import { reconstituerRdimEcsIndividuelleUnique } from './11_rdim_ecs_individuelle_unique.js';
|
|
37
38
|
|
|
38
|
-
const LIB_VERSION = '1.
|
|
39
|
+
const LIB_VERSION = '1.10.1';
|
|
39
40
|
|
|
40
41
|
const dpeSanitizerService = new DpeSanitizerService();
|
|
41
42
|
|
|
@@ -58,7 +59,7 @@ export function getVersion() {
|
|
|
58
59
|
/**
|
|
59
60
|
* Run the engine with a full dpe xml content
|
|
60
61
|
* @param dpeXmlContent {string} A full dpe xml content
|
|
61
|
-
* @param options {{sanitize
|
|
62
|
+
* @param options {{sanitize?: boolean, compat_for_corpus_analysis?: boolean}?} voir `calcul_3cl`
|
|
62
63
|
* @return {FullDpe}
|
|
63
64
|
*/
|
|
64
65
|
export function calcul_3cl_xml(dpeXmlContent, options) {
|
|
@@ -70,12 +71,18 @@ export function calcul_3cl_xml(dpeXmlContent, options) {
|
|
|
70
71
|
/**
|
|
71
72
|
* Run the engine with a javascript plain object dpe
|
|
72
73
|
* @param inputDpe {FullDpe} A full plain object dpe
|
|
73
|
-
* @param options {{sanitize
|
|
74
|
+
* @param options {{sanitize?: boolean, compat_for_corpus_analysis?: boolean}?}
|
|
75
|
+
* - `sanitize` (défaut `true` si `options` est absent) : nettoyage du DPE d'entrée ;
|
|
76
|
+
* - `compat_for_corpus_analysis` (défaut `false`) : reprend les valeurs choisies par le logiciel de
|
|
77
|
+
* saisie qui contredisent les données d'entrée (ligne q4pa_conv, issues #312 et #294), pour
|
|
78
|
+
* reproduire les DPE du corpus ADEME. À réserver à l'analyse de corpus : jamais pour le contrôle
|
|
79
|
+
* d'un DPE ni pour les scénarios de travaux.
|
|
74
80
|
* @return {FullDpe}
|
|
75
81
|
*/
|
|
76
82
|
export function calcul_3cl(inputDpe, options) {
|
|
77
83
|
set_bug_for_bug_compat();
|
|
78
84
|
if (!options) options = { sanitize: true };
|
|
85
|
+
set_compat_for_corpus_analysis(options.compat_for_corpus_analysis === true);
|
|
79
86
|
let dpe;
|
|
80
87
|
if (options.sanitize) {
|
|
81
88
|
dpe = dpeSanitizerService.execute(inputDpe);
|
package/engine.spec.js
CHANGED
|
@@ -21,6 +21,7 @@ let useEnumAsStringFlag = false;
|
|
|
21
21
|
|
|
22
22
|
vi.mock('./utils.js', () => ({
|
|
23
23
|
set_bug_for_bug_compat: vi.fn(),
|
|
24
|
+
set_compat_for_corpus_analysis: vi.fn(),
|
|
24
25
|
add_references: vi.fn(),
|
|
25
26
|
get bug_for_bug_compat() {
|
|
26
27
|
return bugForBugCompat;
|
|
@@ -104,6 +105,7 @@ const {
|
|
|
104
105
|
collectionCanBeEmpty,
|
|
105
106
|
containsAnySubstring,
|
|
106
107
|
isEffetJoule,
|
|
108
|
+
set_compat_for_corpus_analysis,
|
|
107
109
|
useEnumAsString,
|
|
108
110
|
xmlParser
|
|
109
111
|
} = await import('./utils.js');
|
|
@@ -365,6 +367,52 @@ describe('calcul_3cl - garde-fous précoces', () => {
|
|
|
365
367
|
});
|
|
366
368
|
});
|
|
367
369
|
|
|
370
|
+
/**
|
|
371
|
+
* Option `compat_for_corpus_analysis` : reprise des valeurs du logiciel de saisie (ligne q4pa_conv)
|
|
372
|
+
* réservée à l'analyse du corpus ADEME. Désactivée par défaut et repositionnée à chaque appel.
|
|
373
|
+
*/
|
|
374
|
+
describe('calcul_3cl - option compat_for_corpus_analysis', () => {
|
|
375
|
+
const dpeNonSupporte = () => ({ administratif: { enum_modele_dpe_id: '99' } });
|
|
376
|
+
|
|
377
|
+
beforeEach(() => {
|
|
378
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test('sans options : désactivée', () => {
|
|
382
|
+
calcul_3cl(dpeNonSupporte());
|
|
383
|
+
expect(set_compat_for_corpus_analysis).toHaveBeenCalledExactlyOnceWith(false);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test('options sans le champ : désactivée', () => {
|
|
387
|
+
calcul_3cl(dpeNonSupporte(), { sanitize: false });
|
|
388
|
+
expect(set_compat_for_corpus_analysis).toHaveBeenCalledExactlyOnceWith(false);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test('valeur non booléenne « truthy » : désactivée (seul true active la reprise)', () => {
|
|
392
|
+
calcul_3cl(dpeNonSupporte(), { sanitize: false, compat_for_corpus_analysis: 'true' });
|
|
393
|
+
expect(set_compat_for_corpus_analysis).toHaveBeenCalledExactlyOnceWith(false);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test('true : activée, sans changer la sanitisation demandée', () => {
|
|
397
|
+
const dpe = dpeNonSupporte();
|
|
398
|
+
calcul_3cl(dpe, { sanitize: true, compat_for_corpus_analysis: true });
|
|
399
|
+
expect(set_compat_for_corpus_analysis).toHaveBeenCalledExactlyOnceWith(true);
|
|
400
|
+
expect(sanitizeExecute).toHaveBeenCalledWith(dpe);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test('repositionnée à chaque appel : un appel par défaut après un appel activé la désactive', () => {
|
|
404
|
+
calcul_3cl(dpeNonSupporte(), { sanitize: false, compat_for_corpus_analysis: true });
|
|
405
|
+
calcul_3cl(dpeNonSupporte());
|
|
406
|
+
expect(vi.mocked(set_compat_for_corpus_analysis).mock.calls).toEqual([[true], [false]]);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test('calcul_3cl_xml transmet l’option', () => {
|
|
410
|
+
vi.mocked(xmlParser.parse).mockReturnValue({ dpe: dpeNonSupporte() });
|
|
411
|
+
calcul_3cl_xml('<dpe/>', { sanitize: false, compat_for_corpus_analysis: true });
|
|
412
|
+
expect(set_compat_for_corpus_analysis).toHaveBeenCalledExactlyOnceWith(true);
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
|
|
368
416
|
describe('calcul_3cl_xml - garde sur le modèle de DPE', () => {
|
|
369
417
|
test('parse le XML puis retourne null pour un modèle non supporté', () => {
|
|
370
418
|
const dpe = { administratif: { enum_modele_dpe_id: '99' } };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DeperditionService } from '../deperdition.service.js';
|
|
2
2
|
|
|
3
3
|
import { logger } from '../../../../../core/util/logger/log-service.js';
|
|
4
|
-
import { bug_for_bug_compat } from '../../../../../utils.js';
|
|
4
|
+
import { bug_for_bug_compat, compat_for_corpus_analysis } from '../../../../../utils.js';
|
|
5
5
|
import { TypeHabitation } from '../../../../dpe/domain/models/type-habitation.model.js';
|
|
6
6
|
import { TypeVentilation } from '../../../../dpe/domain/models/type-ventilation.model.js';
|
|
7
7
|
|
|
@@ -100,12 +100,14 @@ export class DeperditionVentilationService extends DeperditionService {
|
|
|
100
100
|
presenceJointsMenuiserie = '1';
|
|
101
101
|
} else if (
|
|
102
102
|
bug_for_bug_compat &&
|
|
103
|
+
compat_for_corpus_analysis &&
|
|
103
104
|
presenceJointNonRenseignee &&
|
|
104
105
|
this.tvStore.getQ4paConvById(ventilationDE.tv_q4pa_conv_id)
|
|
105
106
|
?.presence_joints_menuiserie === '1'
|
|
106
107
|
) {
|
|
107
108
|
// Issue #294 : aucune menuiserie n'exporte presence_joint, rien ne contredit la ligne
|
|
108
|
-
// q4pa_conv « avant 1948 avec joints » retenue par le DPE => on la reprend
|
|
109
|
+
// q4pa_conv « avant 1948 avec joints » retenue par le DPE => on la reprend, uniquement
|
|
110
|
+
// pour l'analyse du corpus ADEME (option compat_for_corpus_analysis).
|
|
109
111
|
presenceJointsMenuiserie = '1';
|
|
110
112
|
}
|
|
111
113
|
}
|
package/features/engine/domain/enveloppe/ventilation/deperdition-ventilation.service.spec.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
|
2
2
|
|
|
3
|
-
/** `bug_for_bug_compat` (utils.js)
|
|
4
|
-
const state = vi.hoisted(() => ({ bug: false }));
|
|
3
|
+
/** `bug_for_bug_compat` et `compat_for_corpus_analysis` (utils.js) pilotés par le test. */
|
|
4
|
+
const state = vi.hoisted(() => ({ bug: false, compat: false }));
|
|
5
5
|
vi.mock('../../../../../utils.js', async (importOriginal) => ({
|
|
6
6
|
...(await importOriginal()),
|
|
7
7
|
get bug_for_bug_compat() {
|
|
8
8
|
return state.bug;
|
|
9
|
+
},
|
|
10
|
+
get compat_for_corpus_analysis() {
|
|
11
|
+
return state.compat;
|
|
9
12
|
}
|
|
10
13
|
}));
|
|
11
14
|
|
|
@@ -32,6 +35,7 @@ describe('DeperditionVentilationService.q4paConv', () => {
|
|
|
32
35
|
};
|
|
33
36
|
service = new DeperditionVentilationService(tvStore);
|
|
34
37
|
state.bug = false;
|
|
38
|
+
state.compat = false;
|
|
35
39
|
});
|
|
36
40
|
|
|
37
41
|
test('Q4Pa mesuré saisi : utilisé directement', () => {
|
|
@@ -52,6 +56,7 @@ describe('DeperditionVentilationService.q4paConv', () => {
|
|
|
52
56
|
describe('presence_joint non exporté (issue #294)', () => {
|
|
53
57
|
test('hors bug_for_bug_compat : la ligne du DPE est ignorée', () => {
|
|
54
58
|
state.bug = false;
|
|
59
|
+
state.compat = true;
|
|
55
60
|
tvStore.getQ4paConvById.mockReturnValue({ presence_joints_menuiserie: '1' });
|
|
56
61
|
|
|
57
62
|
service.q4paConv({ tv_q4pa_conv_id: 12 }, ctx, 0, 100, 0, 10, true);
|
|
@@ -59,8 +64,20 @@ describe('DeperditionVentilationService.q4paConv', () => {
|
|
|
59
64
|
expect(tvStore.getQ4paConv).toHaveBeenCalledWith('1', 'APPARTEMENT', '0', undefined);
|
|
60
65
|
});
|
|
61
66
|
|
|
62
|
-
test('
|
|
67
|
+
test('par défaut (sans compat_for_corpus_analysis) : la ligne du DPE est ignorée', () => {
|
|
63
68
|
state.bug = true;
|
|
69
|
+
state.compat = false;
|
|
70
|
+
tvStore.getQ4paConvById.mockReturnValue({ presence_joints_menuiserie: '1' });
|
|
71
|
+
|
|
72
|
+
service.q4paConv({ tv_q4pa_conv_id: 12 }, ctx, 0, 100, 0, 10, true);
|
|
73
|
+
|
|
74
|
+
expect(tvStore.getQ4paConvById).not.toHaveBeenCalled();
|
|
75
|
+
expect(tvStore.getQ4paConv).toHaveBeenCalledWith('1', 'APPARTEMENT', '0', undefined);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('compat_for_corpus_analysis et ligne DPE « avec joints » : joints repris', () => {
|
|
79
|
+
state.bug = true;
|
|
80
|
+
state.compat = true;
|
|
64
81
|
tvStore.getQ4paConvById.mockReturnValue({ presence_joints_menuiserie: '1' });
|
|
65
82
|
|
|
66
83
|
service.q4paConv({ tv_q4pa_conv_id: 12 }, ctx, 0, 100, 0, 10, true);
|
|
@@ -69,8 +86,9 @@ describe('DeperditionVentilationService.q4paConv', () => {
|
|
|
69
86
|
expect(tvStore.getQ4paConv).toHaveBeenCalledWith('1', 'APPARTEMENT', '0', '1');
|
|
70
87
|
});
|
|
71
88
|
|
|
72
|
-
test('
|
|
89
|
+
test('compat_for_corpus_analysis mais presence_joint renseigné : pas de reprise', () => {
|
|
73
90
|
state.bug = true;
|
|
91
|
+
state.compat = true;
|
|
74
92
|
tvStore.getQ4paConvById.mockReturnValue({ presence_joints_menuiserie: '1' });
|
|
75
93
|
|
|
76
94
|
service.q4paConv({ tv_q4pa_conv_id: 12 }, ctx, 0, 100, 0, 10, false);
|
|
@@ -78,8 +96,9 @@ describe('DeperditionVentilationService.q4paConv', () => {
|
|
|
78
96
|
expect(tvStore.getQ4paConv).toHaveBeenCalledWith('1', 'APPARTEMENT', '0', undefined);
|
|
79
97
|
});
|
|
80
98
|
|
|
81
|
-
test('
|
|
99
|
+
test('compat_for_corpus_analysis et ligne DPE sans joints : pas de reprise', () => {
|
|
82
100
|
state.bug = true;
|
|
101
|
+
state.compat = true;
|
|
83
102
|
tvStore.getQ4paConvById.mockReturnValue({ presence_joints_menuiserie: '' });
|
|
84
103
|
|
|
85
104
|
service.q4paConv({ tv_q4pa_conv_id: 1 }, ctx, 0, 100, 0, 10, true);
|
|
@@ -89,6 +108,7 @@ describe('DeperditionVentilationService.q4paConv', () => {
|
|
|
89
108
|
|
|
90
109
|
test('surfaces majoritairement isolées : le critère des joints ne s’applique pas', () => {
|
|
91
110
|
state.bug = true;
|
|
111
|
+
state.compat = true;
|
|
92
112
|
tvStore.getQ4paConvById.mockReturnValue({ presence_joints_menuiserie: '1' });
|
|
93
113
|
|
|
94
114
|
service.q4paConv({ tv_q4pa_conv_id: 12 }, ctx, 100, 0, 0, 10, true);
|
package/package.json
CHANGED
package/utils.js
CHANGED
|
@@ -40,6 +40,29 @@ export function set_bug_for_bug_compat() {
|
|
|
40
40
|
bug_for_bug_compat = true;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Compatibilité réservée à l'analyse du corpus ADEME.
|
|
45
|
+
*
|
|
46
|
+
* Activée, le moteur reprend certaines valeurs choisies par le logiciel de saisie même
|
|
47
|
+
* lorsqu'elles contredisent les données d'entrée (ex. ligne `tv_q4pa_conv_id` incohérente avec
|
|
48
|
+
* l'isolation des parois ou les joints des menuiseries, issues #312 et #294), dans le seul but de
|
|
49
|
+
* reproduire les résultats des DPE du corpus.
|
|
50
|
+
*
|
|
51
|
+
* Ne jamais l'activer pour contrôler la cohérence d'un DPE ni pour calculer des scénarios de
|
|
52
|
+
* travaux : l'erreur du logiciel de saisie serait alors masquée ou propagée.
|
|
53
|
+
*
|
|
54
|
+
* Positionnée à chaque appel de `calcul_3cl` à partir de l'option du même nom.
|
|
55
|
+
* @type {boolean}
|
|
56
|
+
*/
|
|
57
|
+
export let compat_for_corpus_analysis = false;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param value {boolean} `true` uniquement pour l'analyse du corpus ADEME
|
|
61
|
+
*/
|
|
62
|
+
export function set_compat_for_corpus_analysis(value = false) {
|
|
63
|
+
compat_for_corpus_analysis = value === true;
|
|
64
|
+
}
|
|
65
|
+
|
|
43
66
|
export function useEnumAsString(jsonObj) {
|
|
44
67
|
for (const key in jsonObj) {
|
|
45
68
|
if (jsonObj.hasOwnProperty(key)) {
|
package/utils.spec.js
CHANGED
|
@@ -35,6 +35,7 @@ vi.mock('./tv.js', () => ({
|
|
|
35
35
|
const {
|
|
36
36
|
xmlParser,
|
|
37
37
|
set_bug_for_bug_compat,
|
|
38
|
+
set_compat_for_corpus_analysis,
|
|
38
39
|
useEnumAsString,
|
|
39
40
|
set_use_enum_as_string,
|
|
40
41
|
set_tv_match_optimized_version,
|
|
@@ -171,6 +172,20 @@ describe('Utils unit tests', () => {
|
|
|
171
172
|
expect(utils.bug_for_bug_compat).toBe(true);
|
|
172
173
|
});
|
|
173
174
|
|
|
175
|
+
test('compat_for_corpus_analysis : désactivé par défaut, seul true l’active', () => {
|
|
176
|
+
expect(utils.compat_for_corpus_analysis).toBe(false);
|
|
177
|
+
set_compat_for_corpus_analysis(true);
|
|
178
|
+
expect(utils.compat_for_corpus_analysis).toBe(true);
|
|
179
|
+
set_compat_for_corpus_analysis();
|
|
180
|
+
expect(utils.compat_for_corpus_analysis).toBe(false);
|
|
181
|
+
set_compat_for_corpus_analysis(true);
|
|
182
|
+
set_compat_for_corpus_analysis('true');
|
|
183
|
+
expect(utils.compat_for_corpus_analysis).toBe(false);
|
|
184
|
+
set_compat_for_corpus_analysis(true);
|
|
185
|
+
set_compat_for_corpus_analysis(false);
|
|
186
|
+
expect(utils.compat_for_corpus_analysis).toBe(false);
|
|
187
|
+
});
|
|
188
|
+
|
|
174
189
|
test('set_use_enum_as_string active le drapeau', () => {
|
|
175
190
|
set_use_enum_as_string();
|
|
176
191
|
expect(utils.use_enum_as_string).toBe(true);
|