From 576c1a061fcba449108eb5f929fe722360934ea5 Mon Sep 17 00:00:00 2001 From: "Hermes (Red Froggy)" Date: Sun, 9 Aug 2026 20:10:52 +0000 Subject: [PATCH 1/3] fix: doublage not cumulated with ITE/ITI isolation According to 3CL method and LICIEL behavior, when a wall has a doublage and an ITE or ITI isolation, the doublage must NOT be included in the Umur0 calculation. The doublage is only counted in the following cases: - non-insulated wall - ITR isolation only - unknown isolation type Isolation types with ITE or ITI affected: ITI (3), ITE (4), ITI+ITE (6), ITI+ITR (7), ITE+ITR (8). Fix applied in both implementations: - src/3.2.1_mur.js (legacy) - src/features/engine/domain/enveloppe/mur/deperdition-mur.service.js Closes #146 --- src/3.2.1_mur.js | 13 ++- src/3.2.1_mur.spec.js | 98 +++++++++++++++++++ .../enveloppe/mur/deperdition-mur.service.js | 35 ++++--- 3 files changed, 131 insertions(+), 15 deletions(-) diff --git a/src/3.2.1_mur.js b/src/3.2.1_mur.js index 525bf58ec..a490a5a28 100644 --- a/src/3.2.1_mur.js +++ b/src/3.2.1_mur.js @@ -166,8 +166,17 @@ function calc_umur0(di, de, du) { } } - // Si présence d'un doublage - if (type_doublage > 2) { + /** + * Le doublage NE doit PAS être cumulé à une isolation ITE ou ITI. + * Il doit uniquement être pris en compte en l'absence d'isolation ITE/ITI (ex: ITR seule, non isolé, inconnu). + * Types avec ITE ou ITI : 3 (iti), 4 (ite), 6 (iti+ite), 7 (iti+itr), 8 (ite+itr) + * @see https://github.com/Open3CL/engine/issues/146 + */ + const type_isolation = parseInt(de.enum_type_isolation_id) || 1; + const hasIteOrIti = [3, 4, 6, 7, 8].includes(type_isolation); + + // Si présence d'un doublage et pas d'isolation ITE/ITI + if (type_doublage > 2 && !hasIteOrIti) { let umur0Doublage; // 3 - doublage indéterminé ou lame d'air inf 15 mm diff --git a/src/3.2.1_mur.spec.js b/src/3.2.1_mur.spec.js index 56ce1f39a..b098fc695 100644 --- a/src/3.2.1_mur.spec.js +++ b/src/3.2.1_mur.spec.js @@ -818,4 +818,102 @@ describe('calc_mur - méthode de saisie de U inconnue', () => { expect(warn).toHaveBeenCalledWith('methode_saisie_u inconnue:', 'valeur inexistante'); warn.mockRestore(); }); + + /** + * @see https://github.com/Open3CL/engine/issues/146 + * Le doublage NE doit PAS être cumulé à une isolation ITE ou ITI. + */ + describe('[MURS] Doublage non cumulé à une isolation ITE/ITI (#146)', () => { + const baseDE = { + enum_type_adjacence_id: '1', // Paroi sur l'extérieur (b=1) + enum_materiaux_structure_mur_id: '11', // Béton ≤20 cm + epaisseur_structure: 20, + enum_methode_saisie_u0_id: '2', + paroi_ancienne: 0 + }; + + test('doublage avec ITI : le doublage ne doit pas être pris en compte dans Umur0', () => { + const zc = 3; // H2a + const pc_id = 6; + const ej = 0; + // Mur béton 20 cm (umur0 ~ 2.5), avec doublage connu (type 5) ET isolation ITI (type 3) + const mur = { + donnee_entree: { + ...baseDE, + description: 'Mur béton avec doublage et ITI', + enum_methode_saisie_u_id: '3', // épaisseur isolation saisie + epaisseur_isolation: 10, // 10 cm + enum_type_doublage_id: '5', // doublage connu (plâtre brique bois) + enum_type_isolation_id: '3' // ITI + }, + donnee_intermediaire: {} + }; + calc_mur(mur, zc, pc_id, ej); + + // Sans doublage cumulé, umur0 = 2.5 (valeur brute du mur béton ≤20cm) + // Avec doublage cumulé à tort : umur0 = 1 / (1/2.5 + 0.21) ≈ 1.724 + expect(mur.donnee_intermediaire.umur0).toBeCloseTo(2.5, 2); + }); + + test('doublage avec ITE : le doublage ne doit pas être pris en compte dans Umur0', () => { + const zc = 3; + const pc_id = 6; + const ej = 0; + const mur = { + donnee_entree: { + ...baseDE, + description: 'Mur béton avec doublage et ITE', + enum_methode_saisie_u_id: '3', + epaisseur_isolation: 8, + enum_type_doublage_id: '4', // doublage indéterminé lame d'air sup 15mm + enum_type_isolation_id: '4' // ITE + }, + donnee_intermediaire: {} + }; + calc_mur(mur, zc, pc_id, ej); + + // umur0 = 2.5 (béton ≤20cm, doublage ignoré car ITE présent) + expect(mur.donnee_intermediaire.umur0).toBeCloseTo(2.5, 2); + }); + + test('doublage avec ITR seule : le doublage DOIT être pris en compte dans Umur0', () => { + const zc = 3; + const pc_id = 6; + const ej = 0; + const mur = { + donnee_entree: { + ...baseDE, + description: 'Mur béton avec doublage et ITR', + enum_methode_saisie_u_id: '1', // non isolé + enum_type_doublage_id: '5', // doublage connu + enum_type_isolation_id: '5' // ITR + }, + donnee_intermediaire: {} + }; + calc_mur(mur, zc, pc_id, ej); + + // ITR seule → le doublage est pris en compte : umur0 < 2.5 + expect(mur.donnee_intermediaire.umur0).toBeLessThan(2.5); + }); + + test('doublage sans isolation : le doublage DOIT être pris en compte dans Umur0', () => { + const zc = 3; + const pc_id = 6; + const ej = 0; + const mur = { + donnee_entree: { + ...baseDE, + description: 'Mur béton avec doublage sans isolation', + enum_methode_saisie_u_id: '1', // non isolé + enum_type_doublage_id: '5', + enum_type_isolation_id: '2' // non isolé + }, + donnee_intermediaire: {} + }; + calc_mur(mur, zc, pc_id, ej); + + // Non isolé → doublage pris en compte : umur0 < umur0_nu_brut (le doublage réduit bien la valeur) + expect(mur.donnee_intermediaire.umur0).toBeLessThan(2.5); + }); + }); }); diff --git a/src/features/engine/domain/enveloppe/mur/deperdition-mur.service.js b/src/features/engine/domain/enveloppe/mur/deperdition-mur.service.js index a03a1d3e2..61c171643 100755 --- a/src/features/engine/domain/enveloppe/mur/deperdition-mur.service.js +++ b/src/features/engine/domain/enveloppe/mur/deperdition-mur.service.js @@ -129,20 +129,29 @@ export class DeperditionMurService extends DeperditionService { } /** - * Pour l’ensemble des parois, la présence d’un doublage apporte une résistance thermique supplémentaire + * Pour l’ensemble des parois, la présence d’un doublage apporte une résistance thermique supplémentaire. + * Cependant, lorsqu'une isolation ITE ou ITI est présente, le doublage NE doit PAS être cumulé. + * Le doublage n'est pris en compte que si aucune isolation ITE/ITI n'est appliquée. + * Types d'isolation avec ITE ou ITI : 3 (iti), 4 (ite), 6 (iti+ite), 7 (iti+itr), 8 (ite+itr) + * @see https://github.com/Open3CL/engine/issues/146 */ - switch (murDE.enum_type_doublage_id) { - case '3': - // doublage indéterminé ou lame d'air inf 15 mm - umur0 = 1 / (1 / umur0 + 0.1); - break; - case '4': // doublage indéterminé ou lame d'air sup 15 mm - case '5': // doublage connu (plâtre brique bois) - umur0 = 1 / (1 / umur0 + 0.21); - break; - default: - // absence de doublage ou inconnu - break; + const typeIsolation = parseInt(murDE.enum_type_isolation_id) || 1; + const hasIteOrIti = [3, 4, 6, 7, 8].includes(typeIsolation); + + if (!hasIteOrIti) { + switch (murDE.enum_type_doublage_id) { + case '3': + // doublage indéterminé ou lame d'air inf 15 mm + umur0 = 1 / (1 / umur0 + 0.1); + break; + case '4': // doublage indéterminé ou lame d'air sup 15 mm + case '5': // doublage connu (plâtre brique bois) + umur0 = 1 / (1 / umur0 + 0.21); + break; + default: + // absence de doublage ou inconnu + break; + } } /** From 37724f0f154dd1f8b19ff40c5870b14d1749374e Mon Sep 17 00:00:00 2001 From: Michael Desigaud Date: Sun, 20 Sep 2026 18:08:18 +0200 Subject: [PATCH 2/3] fix: doublage for ite/iti --- README.md | 46 ++++----- dist/reports/corpus/corpus_list_main.json | 2 +- docs/CORPUS-HISTORY.md | 116 ++++++++++++++++++++++ src/3.2.1_mur.js | 18 ++-- src/3_deperdition.js | 3 +- 5 files changed, 151 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 4da0b546c..8586fe7a6 100644 --- a/README.md +++ b/README.md @@ -385,28 +385,28 @@ Ces résultats sont **générés automatiquement** à la fin de `npm run test:co -> **Version `1.6.2`** · branche `main` · généré le 2026-09-18 +> **Version `1.6.2`** · branche `pr-174` · généré le 2026-09-20 > Seuil de tolérance **5%** - - + +
9
corpus
89 996
DPE analysés
59 497
DPE conformes
66,11 %
réussite globale
59 522
DPE conformes
66,14 %
réussite globale
| | Corpus | Réussite | | DPE conformes | | :-: | :---------------------------------------------------------------------------------------------------------------------- | ----------: | :--------------------- | -------------: | | 🔴 | **Généraliste**
`corpus_dpe.csv` | **45,97 %** | `█████████░░░░░░░░░░░` | 4 597 / 10 000 | -| 🟢 | **Appartement · chauffage individuel (2025)**
`dpe_appartement_individuel_chauffage_individuel_2025.csv` | **91,55 %** | `██████████████████░░` | 9 155 / 10 000 | -| 🟢 | **Logement individuel (2025)**
`dpe_logement_individuel_2025.csv` | **88,14 %** | `██████████████████░░` | 8 812 / 9 998 | -| 🟢 | **Maison individuelle (2025)**
`dpe_maison_individuelle_2025.csv` | **87,93 %** | `██████████████████░░` | 8 793 / 10 000 | -| 🟡 | **Immeuble · chauffage individuel**
`dpe_immeuble_chauffage_individuel.csv` | **73,76 %** | `███████████████░░░░░` | 7 375 / 9 999 | -| 🟡 | **Appartement · chauffage collectif (2025)**
`dpe_appartement_individuel_chauffage_collectif_2025.csv` | **69,47 %** | `██████████████░░░░░░` | 6 947 / 10 000 | -| 🟡 | **Immeuble · chauffage collectif**
`dpe_immeuble_chauffage_collectif.csv` | **62,27 %** | `████████████░░░░░░░░` | 6 226 / 9 999 | -| 🔴 | **Immeuble · chauffage mixte**
`dpe_immeuble_chauffage_mixte.csv` | **48,37 %** | `██████████░░░░░░░░░░` | 4 837 / 10 000 | +| 🟢 | **Appartement · chauffage individuel (2025)**
`dpe_appartement_individuel_chauffage_individuel_2025.csv` | **91,62 %** | `██████████████████░░` | 9 162 / 10 000 | +| 🟢 | **Logement individuel (2025)**
`dpe_logement_individuel_2025.csv` | **88,16 %** | `██████████████████░░` | 8 814 / 9 998 | +| 🟢 | **Maison individuelle (2025)**
`dpe_maison_individuelle_2025.csv` | **87,99 %** | `██████████████████░░` | 8 799 / 10 000 | +| 🟡 | **Immeuble · chauffage individuel**
`dpe_immeuble_chauffage_individuel.csv` | **73,78 %** | `███████████████░░░░░` | 7 377 / 9 999 | +| 🟡 | **Appartement · chauffage collectif (2025)**
`dpe_appartement_individuel_chauffage_collectif_2025.csv` | **69,51 %** | `██████████████░░░░░░` | 6 951 / 10 000 | +| 🟡 | **Immeuble · chauffage collectif**
`dpe_immeuble_chauffage_collectif.csv` | **62,30 %** | `████████████░░░░░░░░` | 6 229 / 9 999 | +| 🔴 | **Immeuble · chauffage mixte**
`dpe_immeuble_chauffage_mixte.csv` | **48,38 %** | `██████████░░░░░░░░░░` | 4 838 / 10 000 | | 🔴 | **Individuel généré depuis l'immeuble (2026)**
`dpe_individuel_a_partir_dpe_immeuble_2026.csv` | **27,55 %** | `██████░░░░░░░░░░░░░░` | 2 755 / 10 000 | 🟢 ≥ 85 % · 🟡 ≥ 60 % · 🔴 < 60 % @@ -418,24 +418,24 @@ Ces résultats sont **générés automatiquement** à la fin de `npm run test:co - - - - + + + +
5,61 ms
moyenne
5,03 ms
médiane
0,11 ms
min
705,8 ms
max
5,23 ms
moyenne
4,61 ms
médiane
0,10 ms
min
577,0 ms
max
| Corpus | Moyenne | Médiane | Min | Max | p95 | p99 | | :--------------------------------------------- | ------: | ------: | ------: | -------: | ------: | ------: | -| **Généraliste** | 4,50 ms | 4,09 ms | 0,20 ms | 34,8 ms | 8,12 ms | 11,3 ms | -| **Appartement · chauffage individuel (2025)** | 3,71 ms | 3,46 ms | 1,32 ms | 26,1 ms | 6,22 ms | 7,99 ms | -| **Logement individuel (2025)** | 5,30 ms | 4,84 ms | 1,52 ms | 42,5 ms | 9,73 ms | 13,2 ms | -| **Maison individuelle (2025)** | 6,54 ms | 6,02 ms | 1,71 ms | 54,5 ms | 11,6 ms | 16,4 ms | -| **Immeuble · chauffage individuel** | 7,17 ms | 6,22 ms | 0,11 ms | 137,7 ms | 14,3 ms | 22,3 ms | -| **Appartement · chauffage collectif (2025)** | 3,98 ms | 3,76 ms | 1,22 ms | 37,6 ms | 6,35 ms | 8,00 ms | -| **Immeuble · chauffage collectif** | 6,40 ms | 5,58 ms | 0,15 ms | 101,0 ms | 12,1 ms | 21,2 ms | -| **Immeuble · chauffage mixte** | 6,01 ms | 5,01 ms | 0,22 ms | 705,8 ms | 12,4 ms | 19,8 ms | -| **Individuel généré depuis l'immeuble (2026)** | 6,89 ms | 6,27 ms | 1,52 ms | 65,1 ms | 13,0 ms | 19,7 ms | +| **Généraliste** | 4,37 ms | 3,96 ms | 0,22 ms | 35,5 ms | 7,93 ms | 10,8 ms | +| **Appartement · chauffage individuel (2025)** | 3,49 ms | 3,20 ms | 1,13 ms | 30,2 ms | 6,09 ms | 7,79 ms | +| **Logement individuel (2025)** | 4,96 ms | 4,38 ms | 1,34 ms | 43,1 ms | 9,48 ms | 12,9 ms | +| **Maison individuelle (2025)** | 5,88 ms | 5,34 ms | 1,68 ms | 47,0 ms | 10,5 ms | 14,2 ms | +| **Immeuble · chauffage individuel** | 6,66 ms | 5,73 ms | 0,10 ms | 83,7 ms | 13,6 ms | 21,0 ms | +| **Appartement · chauffage collectif (2025)** | 4,09 ms | 3,75 ms | 1,26 ms | 37,7 ms | 6,88 ms | 9,51 ms | +| **Immeuble · chauffage collectif** | 6,04 ms | 5,23 ms | 0,15 ms | 148,9 ms | 11,5 ms | 21,3 ms | +| **Immeuble · chauffage mixte** | 5,79 ms | 4,77 ms | 0,18 ms | 577,0 ms | 12,4 ms | 20,2 ms | +| **Individuel généré depuis l'immeuble (2026)** | 5,82 ms | 5,08 ms | 1,65 ms | 71,8 ms | 11,1 ms | 16,9 ms | La moyenne est tirée vers le haut par les DPE collectifs, dont le coût atteint plusieurs dizaines de fois la médiane : c’est la médiane qui décrit le cas courant, et p95/p99 le cas diff --git a/dist/reports/corpus/corpus_list_main.json b/dist/reports/corpus/corpus_list_main.json index 3e15dce40..290ee3339 100644 --- a/dist/reports/corpus/corpus_list_main.json +++ b/dist/reports/corpus/corpus_list_main.json @@ -10,5 +10,5 @@ "dpe_appartement_individuel_chauffage_collectif_2025.csv", "dpe_individuel_a_partir_dpe_immeuble_2026.csv" ], - "branches": ["main"] + "branches": ["main", "pr-174"] } diff --git a/docs/CORPUS-HISTORY.md b/docs/CORPUS-HISTORY.md index 1322d9e8c..266dd71de 100644 --- a/docs/CORPUS-HISTORY.md +++ b/docs/CORPUS-HISTORY.md @@ -3,6 +3,122 @@ Une section par génération, la plus récente en premier. Ce fichier est alimenté automatiquement par `npm run reports:readme`. +## 1.6.2 — 2026-09-20 (`pr-174`) + +> **Version `1.6.2`** · branche `pr-174` · généré le 2026-09-20 +> Seuil de tolérance **5%** + + + + + + + + +
9
corpus
89 996
DPE analysés
59 522
DPE conformes
66,14 %
réussite globale
+ +| | Corpus | Réussite | | DPE conformes | +| :-: | :---------------------------------------------------------------------------------------------------------------------- | ----------: | :--------------------- | -------------: | +| 🔴 | **Généraliste**
`corpus_dpe.csv` | **45,97 %** | `█████████░░░░░░░░░░░` | 4 597 / 10 000 | +| 🟢 | **Appartement · chauffage individuel (2025)**
`dpe_appartement_individuel_chauffage_individuel_2025.csv` | **91,62 %** | `██████████████████░░` | 9 162 / 10 000 | +| 🟢 | **Logement individuel (2025)**
`dpe_logement_individuel_2025.csv` | **88,16 %** | `██████████████████░░` | 8 814 / 9 998 | +| 🟢 | **Maison individuelle (2025)**
`dpe_maison_individuelle_2025.csv` | **87,99 %** | `██████████████████░░` | 8 799 / 10 000 | +| 🟡 | **Immeuble · chauffage individuel**
`dpe_immeuble_chauffage_individuel.csv` | **73,78 %** | `███████████████░░░░░` | 7 377 / 9 999 | +| 🟡 | **Appartement · chauffage collectif (2025)**
`dpe_appartement_individuel_chauffage_collectif_2025.csv` | **69,51 %** | `██████████████░░░░░░` | 6 951 / 10 000 | +| 🟡 | **Immeuble · chauffage collectif**
`dpe_immeuble_chauffage_collectif.csv` | **62,30 %** | `████████████░░░░░░░░` | 6 229 / 9 999 | +| 🔴 | **Immeuble · chauffage mixte**
`dpe_immeuble_chauffage_mixte.csv` | **48,38 %** | `██████████░░░░░░░░░░` | 4 838 / 10 000 | +| 🔴 | **Individuel généré depuis l'immeuble (2026)**
`dpe_individuel_a_partir_dpe_immeuble_2026.csv` | **27,55 %** | `██████░░░░░░░░░░░░░░` | 2 755 / 10 000 | + +🟢 ≥ 85 % · 🟡 ≥ 60 % · 🔴 < 60 % + +#### Temps d’exécution + +> Durée de l’appel à `calcul_3cl` par DPE, sur 89 956 calculs. +> La copie défensive de l’entrée et la lecture du fichier sont exclues de la mesure. + + + + + + + + +
5,23 ms
moyenne
4,61 ms
médiane
0,10 ms
min
577,0 ms
max
+ +| Corpus | Moyenne | Médiane | Min | Max | p95 | p99 | +| :--------------------------------------------- | ------: | ------: | ------: | -------: | ------: | ------: | +| **Généraliste** | 4,37 ms | 3,96 ms | 0,22 ms | 35,5 ms | 7,93 ms | 10,8 ms | +| **Appartement · chauffage individuel (2025)** | 3,49 ms | 3,20 ms | 1,13 ms | 30,2 ms | 6,09 ms | 7,79 ms | +| **Logement individuel (2025)** | 4,96 ms | 4,38 ms | 1,34 ms | 43,1 ms | 9,48 ms | 12,9 ms | +| **Maison individuelle (2025)** | 5,88 ms | 5,34 ms | 1,68 ms | 47,0 ms | 10,5 ms | 14,2 ms | +| **Immeuble · chauffage individuel** | 6,66 ms | 5,73 ms | 0,10 ms | 83,7 ms | 13,6 ms | 21,0 ms | +| **Appartement · chauffage collectif (2025)** | 4,09 ms | 3,75 ms | 1,26 ms | 37,7 ms | 6,88 ms | 9,51 ms | +| **Immeuble · chauffage collectif** | 6,04 ms | 5,23 ms | 0,15 ms | 148,9 ms | 11,5 ms | 21,3 ms | +| **Immeuble · chauffage mixte** | 5,79 ms | 4,77 ms | 0,18 ms | 577,0 ms | 12,4 ms | 20,2 ms | +| **Individuel généré depuis l'immeuble (2026)** | 5,82 ms | 5,08 ms | 1,65 ms | 71,8 ms | 11,1 ms | 16,9 ms | + +La moyenne est tirée vers le haut par les DPE collectifs, dont le coût atteint plusieurs +dizaines de fois la médiane : c’est la médiane qui décrit le cas courant, et p95/p99 le cas +défavorable réel. + +## 1.6.2 — 2026-09-18 (`pr-174`) + +> **Version `1.6.2`** · branche `pr-174` · généré le 2026-09-18 +> Seuil de tolérance **5%** + + + + + + + + +
9
corpus
89 996
DPE analysés
59 492
DPE conformes
66,11 %
réussite globale
+ +| | Corpus | Réussite | | DPE conformes | +| :-: | :---------------------------------------------------------------------------------------------------------------------- | ----------: | :--------------------- | -------------: | +| 🔴 | **Généraliste**
`corpus_dpe.csv` | **45,87 %** | `█████████░░░░░░░░░░░` | 4 587 / 10 000 | +| 🟢 | **Appartement · chauffage individuel (2025)**
`dpe_appartement_individuel_chauffage_individuel_2025.csv` | **91,62 %** | `██████████████████░░` | 9 162 / 10 000 | +| 🟢 | **Logement individuel (2025)**
`dpe_logement_individuel_2025.csv` | **88,16 %** | `██████████████████░░` | 8 814 / 9 998 | +| 🟢 | **Maison individuelle (2025)**
`dpe_maison_individuelle_2025.csv` | **87,99 %** | `██████████████████░░` | 8 799 / 10 000 | +| 🟡 | **Immeuble · chauffage individuel**
`dpe_immeuble_chauffage_individuel.csv` | **73,68 %** | `███████████████░░░░░` | 7 367 / 9 999 | +| 🟡 | **Appartement · chauffage collectif (2025)**
`dpe_appartement_individuel_chauffage_collectif_2025.csv` | **69,51 %** | `██████████████░░░░░░` | 6 951 / 10 000 | +| 🟡 | **Immeuble · chauffage collectif**
`dpe_immeuble_chauffage_collectif.csv` | **62,03 %** | `████████████░░░░░░░░` | 6 202 / 9 999 | +| 🔴 | **Immeuble · chauffage mixte**
`dpe_immeuble_chauffage_mixte.csv` | **48,55 %** | `██████████░░░░░░░░░░` | 4 855 / 10 000 | +| 🔴 | **Individuel généré depuis l'immeuble (2026)**
`dpe_individuel_a_partir_dpe_immeuble_2026.csv` | **27,55 %** | `██████░░░░░░░░░░░░░░` | 2 755 / 10 000 | + +🟢 ≥ 85 % · 🟡 ≥ 60 % · 🔴 < 60 % + +#### Temps d’exécution + +> Durée de l’appel à `calcul_3cl` par DPE, sur 89 956 calculs. +> La copie défensive de l’entrée et la lecture du fichier sont exclues de la mesure. + + + + + + + + +
5,31 ms
moyenne
4,73 ms
médiane
0,10 ms
min
626,6 ms
max
+ +| Corpus | Moyenne | Médiane | Min | Max | p95 | p99 | +| :--------------------------------------------- | ------: | ------: | ------: | -------: | ------: | ------: | +| **Généraliste** | 4,41 ms | 4,02 ms | 0,20 ms | 32,9 ms | 7,96 ms | 11,0 ms | +| **Appartement · chauffage individuel (2025)** | 3,49 ms | 3,21 ms | 1,16 ms | 30,2 ms | 5,94 ms | 7,46 ms | +| **Logement individuel (2025)** | 5,09 ms | 4,63 ms | 1,34 ms | 41,9 ms | 9,41 ms | 12,6 ms | +| **Maison individuelle (2025)** | 6,14 ms | 5,68 ms | 1,72 ms | 70,0 ms | 10,8 ms | 14,1 ms | +| **Immeuble · chauffage individuel** | 6,97 ms | 6,06 ms | 0,10 ms | 107,5 ms | 13,9 ms | 21,5 ms | +| **Appartement · chauffage collectif (2025)** | 4,01 ms | 3,65 ms | 1,31 ms | 87,4 ms | 6,49 ms | 10,9 ms | +| **Immeuble · chauffage collectif** | 5,68 ms | 5,01 ms | 0,20 ms | 114,5 ms | 10,4 ms | 18,6 ms | +| **Immeuble · chauffage mixte** | 5,95 ms | 4,90 ms | 0,18 ms | 626,6 ms | 12,5 ms | 20,3 ms | +| **Individuel généré depuis l'immeuble (2026)** | 6,01 ms | 5,38 ms | 1,54 ms | 64,5 ms | 11,2 ms | 17,4 ms | + +La moyenne est tirée vers le haut par les DPE collectifs, dont le coût atteint plusieurs +dizaines de fois la médiane : c’est la médiane qui décrit le cas courant, et p95/p99 le cas +défavorable réel. + ## 1.6.2 — 2026-09-18 (`main`) > **Version `1.6.2`** · branche `main` · généré le 2026-09-18 diff --git a/src/3.2.1_mur.js b/src/3.2.1_mur.js index a490a5a28..51bb16220 100644 --- a/src/3.2.1_mur.js +++ b/src/3.2.1_mur.js @@ -122,7 +122,7 @@ function tv_umur(di, de, du, pc_id, zc, effetJoule) { } } -function calc_umur0(di, de, du) { +function calc_umur0(di, de, du, versionDpe) { const umur0_avant = du.umur0_avant; const umur_avant = du.umur_avant; @@ -173,7 +173,7 @@ function calc_umur0(di, de, du) { * @see https://github.com/Open3CL/engine/issues/146 */ const type_isolation = parseInt(de.enum_type_isolation_id) || 1; - const hasIteOrIti = [3, 4, 6, 7, 8].includes(type_isolation); + const hasIteOrIti = versionDpe >= 2.4 && [3, 4, 6, 7, 8].includes(type_isolation); // Si présence d'un doublage et pas d'isolation ITE/ITI if (type_doublage > 2 && !hasIteOrIti) { @@ -221,7 +221,7 @@ function calc_umur0(di, de, du) { di.umur0 = Math.min(2.5, di.umur0); } -export default function calc_mur(mur, zc, pc_id, effetJoule) { +export default function calc_mur(mur, zc, pc_id, effetJoule, versionDpe) { const de = mur.donnee_entree; const du = {}; const di = {}; @@ -237,12 +237,12 @@ export default function calc_mur(mur, zc, pc_id, effetJoule) { const methode_saisie_u = requestInput(de, du, 'methode_saisie_u'); switch (methode_saisie_u) { case 'non isolé': - calc_umur0(di, de, du); + calc_umur0(di, de, du, versionDpe); di.umur = Math.min(di.umur0, 2.5); break; case 'epaisseur isolation saisie justifiée par mesure ou observation': case 'epaisseur isolation saisie justifiée à partir des documents justificatifs autorisés': { - calc_umur0(di, de, du); + calc_umur0(di, de, du, versionDpe); const epaisseurIsolation = requestInput(de, du, 'epaisseur_isolation', 'int') * 0.01; if (epaisseurIsolation) { @@ -260,7 +260,7 @@ export default function calc_mur(mur, zc, pc_id, effetJoule) { } case "resistance isolation saisie justifiée observation de l'isolant installé et mesure de son épaisseur": case 'resistance isolation saisie justifiée à partir des documents justificatifs autorisés': { - calc_umur0(di, de, du); + calc_umur0(di, de, du, versionDpe); const resistanceIsolation = requestInput(de, du, 'resistance_isolation', 'float'); if (resistanceIsolation) { @@ -277,19 +277,19 @@ export default function calc_mur(mur, zc, pc_id, effetJoule) { break; } case 'isolation inconnue (table forfaitaire)': - calc_umur0(di, de, du); + calc_umur0(di, de, du, versionDpe); tv_umur(di, de, du, pc_id, zc, effetJoule); di.umur = Math.min(di.umur, di.umur0); break; case "année d'isolation différente de l'année de construction saisie justifiée (table forfaitaire)": { - calc_umur0(di, de, du); + calc_umur0(di, de, du, versionDpe); const pi_id = requestInputID(de, du, 'periode_isolation') || pc_id; tv_umur(di, de, du, pi_id, zc, effetJoule); di.umur = Math.min(di.umur, di.umur0); break; } case 'année de construction saisie (table forfaitaire)': { - calc_umur0(di, de, du); + calc_umur0(di, de, du, versionDpe); // Si l'année d'isolation est connue, il faut l'utiliser et pas l'année de construction let pi_id = de.enum_periode_isolation_id || pc_id; if (!de.enum_periode_isolation_id) { diff --git a/src/3_deperdition.js b/src/3_deperdition.js index 95308c4b9..b558029ca 100644 --- a/src/3_deperdition.js +++ b/src/3_deperdition.js @@ -224,7 +224,8 @@ export default function calc_deperdition(cg, zc, th, effetJoule, dpe, Sh) { const declaredPontsThermiques = structuredClone(pt_list); const vt_list = logement.ventilation_collection.ventilation || []; - mur_list.forEach((mur) => calc_mur(mur, zc, pc, effetJoule)); + const versionDpe = parseFloat(dpe.administratif.enum_version_id); + mur_list.forEach((mur) => calc_mur(mur, zc, pc, effetJoule, versionDpe)); pb_list.forEach((pb) => calc_pb(pb, zc, pc, effetJoule, pb_list)); ph_list.forEach((ph) => calc_ph(ph, zc, pc, effetJoule)); bv_list.forEach((bv) => calc_bv(bv, zc)); From 160a40ab338efe2e1f006d027e0838fad4c4d6c6 Mon Sep 17 00:00:00 2001 From: Michael Desigaud Date: Sun, 20 Sep 2026 18:30:43 +0200 Subject: [PATCH 3/3] test: fix unit tests --- src/3.2.1_mur.spec.js | 171 ++++++++++++++++++++------------------ src/3_deperdition.js | 6 +- src/3_deperdition.spec.js | 47 +++++++++++ 3 files changed, 141 insertions(+), 83 deletions(-) diff --git a/src/3.2.1_mur.spec.js b/src/3.2.1_mur.spec.js index b098fc695..aafb6a96d 100644 --- a/src/3.2.1_mur.spec.js +++ b/src/3.2.1_mur.spec.js @@ -818,102 +818,109 @@ describe('calc_mur - méthode de saisie de U inconnue', () => { expect(warn).toHaveBeenCalledWith('methode_saisie_u inconnue:', 'valeur inexistante'); warn.mockRestore(); }); +}); + +/** + * Le doublage n'est plus cumulé à une isolation ITE ou ITI à partir de la version 2.4 du DPE. + * Les DPE antérieurs (2 à 2.3) ont été établis avec le cumul : on conserve ce comportement. + * @see https://github.com/Open3CL/engine/issues/146 + */ +describe('calc_mur - doublage et isolation ITE/ITI (#146)', () => { + /** Umur0 issu de la table forfaitaire mockée. */ + const UMUR0_TABLE = 2; + /** Umur0 avec doublage type 4 ou 5 cumulé : 1 / (1/2 + 0,21). */ + const UMUR0_AVEC_DOUBLAGE = 1.4084507042253522; /** - * @see https://github.com/Open3CL/engine/issues/146 - * Le doublage NE doit PAS être cumulé à une isolation ITE ou ITI. + * @param enum_type_isolation_id {string} + * @param enum_type_doublage_id {string} + * @return {object} */ - describe('[MURS] Doublage non cumulé à une isolation ITE/ITI (#146)', () => { - const baseDE = { - enum_type_adjacence_id: '1', // Paroi sur l'extérieur (b=1) - enum_materiaux_structure_mur_id: '11', // Béton ≤20 cm - epaisseur_structure: 20, - enum_methode_saisie_u0_id: '2', - paroi_ancienne: 0 - }; + const murIsoleAvecDoublage = (enum_type_isolation_id, enum_type_doublage_id = '5') => ({ + donnee_entree: { + methode_saisie_u: 'epaisseur isolation saisie justifiée par mesure ou observation', + methode_saisie_u0: + 'déterminé selon le matériau et épaisseur à partir de la table de valeur forfaitaire', + enum_materiaux_structure_mur_id: '5', + epaisseur_structure: 30, + epaisseur_isolation: 10, + enum_type_doublage_id, + enum_type_isolation_id + } + }); - test('doublage avec ITI : le doublage ne doit pas être pris en compte dans Umur0', () => { - const zc = 3; // H2a - const pc_id = 6; - const ej = 0; - // Mur béton 20 cm (umur0 ~ 2.5), avec doublage connu (type 5) ET isolation ITI (type 3) - const mur = { - donnee_entree: { - ...baseDE, - description: 'Mur béton avec doublage et ITI', - enum_methode_saisie_u_id: '3', // épaisseur isolation saisie - epaisseur_isolation: 10, // 10 cm - enum_type_doublage_id: '5', // doublage connu (plâtre brique bois) - enum_type_isolation_id: '3' // ITI - }, - donnee_intermediaire: {} - }; - calc_mur(mur, zc, pc_id, ej); + describe('DPE en version 2.4 et supérieure', () => { + test.each([ + ['iti', '3'], + ['ite', '4'], + ['iti+ite', '6'], + ['iti+itr', '7'], + ['ite+itr', '8'] + ])("isolation %s : le doublage n'est pas cumulé", (_label, typeIsolation) => { + const mur = murIsoleAvecDoublage(typeIsolation); - // Sans doublage cumulé, umur0 = 2.5 (valeur brute du mur béton ≤20cm) - // Avec doublage cumulé à tort : umur0 = 1 / (1/2.5 + 0.21) ≈ 1.724 - expect(mur.donnee_intermediaire.umur0).toBeCloseTo(2.5, 2); + calc_mur(mur, 'h1a', '1', '0', 2.4); + + expect(mur.donnee_intermediaire.umur0).toBeCloseTo(UMUR0_TABLE, 9); }); - test('doublage avec ITE : le doublage ne doit pas être pris en compte dans Umur0', () => { - const zc = 3; - const pc_id = 6; - const ej = 0; - const mur = { - donnee_entree: { - ...baseDE, - description: 'Mur béton avec doublage et ITE', - enum_methode_saisie_u_id: '3', - epaisseur_isolation: 8, - enum_type_doublage_id: '4', // doublage indéterminé lame d'air sup 15mm - enum_type_isolation_id: '4' // ITE - }, - donnee_intermediaire: {} - }; - calc_mur(mur, zc, pc_id, ej); + test.each([ + ['inconnu', '1'], + ['non isolé', '2'], + ['itr', '5'], + ["isolé mais type d'isolation inconnu", '9'] + ])('isolation %s : le doublage est cumulé', (_label, typeIsolation) => { + const mur = murIsoleAvecDoublage(typeIsolation); - // umur0 = 2.5 (béton ≤20cm, doublage ignoré car ITE présent) - expect(mur.donnee_intermediaire.umur0).toBeCloseTo(2.5, 2); + calc_mur(mur, 'h1a', '1', '0', 2.4); + + expect(mur.donnee_intermediaire.umur0).toBeCloseTo(UMUR0_AVEC_DOUBLAGE, 9); }); - test('doublage avec ITR seule : le doublage DOIT être pris en compte dans Umur0', () => { - const zc = 3; - const pc_id = 6; - const ej = 0; - const mur = { - donnee_entree: { - ...baseDE, - description: 'Mur béton avec doublage et ITR', - enum_methode_saisie_u_id: '1', // non isolé - enum_type_doublage_id: '5', // doublage connu - enum_type_isolation_id: '5' // ITR - }, - donnee_intermediaire: {} - }; - calc_mur(mur, zc, pc_id, ej); + test('type_isolation absent : le doublage est cumulé', () => { + const mur = murIsoleAvecDoublage(undefined); + + calc_mur(mur, 'h1a', '1', '0', 2.5); - // ITR seule → le doublage est pris en compte : umur0 < 2.5 - expect(mur.donnee_intermediaire.umur0).toBeLessThan(2.5); + expect(mur.donnee_intermediaire.umur0).toBeCloseTo(UMUR0_AVEC_DOUBLAGE, 9); }); + }); - test('doublage sans isolation : le doublage DOIT être pris en compte dans Umur0', () => { - const zc = 3; - const pc_id = 6; - const ej = 0; - const mur = { - donnee_entree: { - ...baseDE, - description: 'Mur béton avec doublage sans isolation', - enum_methode_saisie_u_id: '1', // non isolé - enum_type_doublage_id: '5', - enum_type_isolation_id: '2' // non isolé - }, - donnee_intermediaire: {} - }; - calc_mur(mur, zc, pc_id, ej); + describe('DPE antérieurs à la version 2.4', () => { + test.each([[2], [2.1], [2.2], [2.3]])( + 'version %s : le doublage reste cumulé à une isolation ITI', + (versionDpe) => { + const mur = murIsoleAvecDoublage('3'); - // Non isolé → doublage pris en compte : umur0 < umur0_nu_brut (le doublage réduit bien la valeur) - expect(mur.donnee_intermediaire.umur0).toBeLessThan(2.5); + calc_mur(mur, 'h1a', '1', '0', versionDpe); + + expect(mur.donnee_intermediaire.umur0).toBeCloseTo(UMUR0_AVEC_DOUBLAGE, 9); + } + ); + + test('version absente : le doublage reste cumulé à une isolation ITE', () => { + const mur = murIsoleAvecDoublage('4'); + + calc_mur(mur, 'h1a', '1', '0'); + + expect(mur.donnee_intermediaire.umur0).toBeCloseTo(UMUR0_AVEC_DOUBLAGE, 9); }); }); + + test.each([ + ['isolation inconnue (table forfaitaire)'], + [ + "année d'isolation différente de l'année de construction saisie justifiée (table forfaitaire)" + ], + ['année de construction saisie (table forfaitaire)'], + ['resistance isolation saisie justifiée à partir des documents justificatifs autorisés'] + ])('methode_saisie_u "%s" : la version du DPE est prise en compte', (methode_saisie_u) => { + const mur = murIsoleAvecDoublage('3'); + mur.donnee_entree.methode_saisie_u = methode_saisie_u; + mur.donnee_entree.resistance_isolation = 2.5; + + calc_mur(mur, 'h1a', '1', '0', 2.4); + + expect(mur.donnee_intermediaire.umur0).toBeCloseTo(UMUR0_TABLE, 9); + }); }); diff --git a/src/3_deperdition.js b/src/3_deperdition.js index b558029ca..3ac94b76b 100644 --- a/src/3_deperdition.js +++ b/src/3_deperdition.js @@ -224,7 +224,11 @@ export default function calc_deperdition(cg, zc, th, effetJoule, dpe, Sh) { const declaredPontsThermiques = structuredClone(pt_list); const vt_list = logement.ventilation_collection.ventilation || []; - const versionDpe = parseFloat(dpe.administratif.enum_version_id); + /** + * Version de la méthode appliquée au DPE. Absente des DPE partiels : les calculs qui en + * dépendent retombent alors sur le comportement historique. + */ + const versionDpe = parseFloat(dpe.administratif?.enum_version_id); mur_list.forEach((mur) => calc_mur(mur, zc, pc, effetJoule, versionDpe)); pb_list.forEach((pb) => calc_pb(pb, zc, pc, effetJoule, pb_list)); ph_list.forEach((ph) => calc_ph(ph, zc, pc, effetJoule)); diff --git a/src/3_deperdition.spec.js b/src/3_deperdition.spec.js index 349ecf75c..5752fc705 100644 --- a/src/3_deperdition.spec.js +++ b/src/3_deperdition.spec.js @@ -125,6 +125,7 @@ describe('Upt - déperdition linéique des ponts thermiques', () => { /** Fabrique un DPE minimal avec une enveloppe pré-calculée. */ function makeDpe({ + enum_version_id = '2.4', mur = [], pb = [], ph = [], @@ -141,6 +142,7 @@ function makeDpe({ } return { numero_dpe: 'TEST', + administratif: enum_version_id === null ? undefined : { enum_version_id }, logement: { sortie: { deperdition }, enveloppe: { @@ -239,6 +241,51 @@ describe("calc_deperdition - agrégation des déperditions de l'enveloppe", () = expect(calc_ventilation).toHaveBeenCalledTimes(1); }); + /** + * Certains calculs de Umur dépendent de la version de la méthode appliquée au DPE + * (cumul du doublage avec une isolation ITE/ITI avant la version 2.4). + * @see https://github.com/Open3CL/engine/issues/146 + */ + describe('version du DPE transmise au calcul des murs', () => { + /** @return {object[]} */ + const murCollection = () => [ + { + donnee_entree: { surface_paroi_opaque: 10, enum_type_adjacence_id: '1' }, + donnee_intermediaire: { umur: 0.5, b: 1 } + } + ]; + + test.each([ + ['2.3', 2.3], + ['2.4', 2.4], + ['2.5', 2.5] + ])('enum_version_id "%s" transmis à calc_mur', (enum_version_id, expected) => { + const dpe = makeDpe({ + enum_version_id, + mur: murCollection(), + ventilation: [{ donnee_intermediaire: { hvent: 0, hperm: 0 } }], + deperdition_mur: 5 + }); + + calc_deperdition({ enum_periode_construction_id: '1' }, 'h1a', 'th', '0', dpe, 100); + + expect(calc_mur).toHaveBeenCalledWith(expect.anything(), 'h1a', '1', '0', expected); + }); + + test('bloc administratif absent : calc_mur reçoit NaN et conserve le comportement historique', () => { + const dpe = makeDpe({ + enum_version_id: null, + mur: murCollection(), + ventilation: [{ donnee_intermediaire: { hvent: 0, hperm: 0 } }], + deperdition_mur: 5 + }); + + calc_deperdition({ enum_periode_construction_id: '1' }, 'h1a', 'th', '0', dpe, 100); + + expect(calc_mur).toHaveBeenCalledWith(expect.anything(), 'h1a', '1', '0', NaN); + }); + }); + /** * calc_Sdep (surface déperditive transmise à la ventilation) exclut les parois à coefficient * b nul et les parois adjacentes à un local non déperditif. Les parois à déperdition nulle