@open3cl/engine 1.8.66 → 1.8.67

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/10_clim.js CHANGED
@@ -14,10 +14,22 @@ function tv_seer(di, de, du, zc_id) {
14
14
  }
15
15
  }
16
16
 
17
- export default function calc_clim(clim, bfr, bfr_dep, zc_id, Sh) {
17
+ /**
18
+ * @param clim {object} climatisation
19
+ * @param bfr {number} besoin de froid du bâtiment
20
+ * @param bfr_dep {number} besoin de froid dépensier du bâtiment
21
+ * @param zc_id {string} zone climatique
22
+ * @param Sh {number} surface de référence du calcul (SH immeuble pour un DPE généré depuis l'immeuble)
23
+ * @param prorataLogement {number} part du logement dans la consommation de froid de l'installation
24
+ * (Sh_logement / SH_immeuble pour un DPE appartement généré à partir de l'immeuble, 1 sinon). Les
25
+ * données intermédiaires restent à l'échelle de l'installation, comme dans le DPE de référence ;
26
+ * le prorata n'est appliqué qu'à l'agrégation des consommations du logement (conso.js).
27
+ * @see https://github.com/Open3CL/engine/issues/303
28
+ */
29
+ export default function calc_clim(clim, bfr, bfr_dep, zc_id, Sh, prorataLogement = 1) {
18
30
  const de = clim.donnee_entree;
19
31
  const di = clim.donnee_intermediaire || {};
20
- const du = {};
32
+ const du = { prorata_logement: prorataLogement };
21
33
 
22
34
  const rs = de.surface_clim / Sh;
23
35
  di.besoin_fr = bfr * rs;
package/10_clim.spec.js CHANGED
@@ -105,6 +105,32 @@ describe('calc_clim - consommation de climatisation', () => {
105
105
  expect(c.donnee_intermediaire.eer).toBe(3);
106
106
  });
107
107
 
108
+ /**
109
+ * DPE appartement généré à partir de l'immeuble : la part du logement est transmise via
110
+ * donnee_utilisateur, les données intermédiaires restant à l'échelle de l'installation.
111
+ * @see https://github.com/Open3CL/engine/issues/303
112
+ */
113
+ test('prorata logement : mémorisé, données intermédiaires à l’échelle de l’installation', () => {
114
+ tv.mockReturnValue({ tv_seer_id: '5', eer: 3 });
115
+ const c = clim({ surface_clim: 100, enum_methode_saisie_carac_sys_id: '1' });
116
+
117
+ calc_clim(c, 1000, 1500, '1', 100, 0.25);
118
+
119
+ expect(c.donnee_utilisateur.prorata_logement).toBe(0.25);
120
+ // inchangé par le prorata : 0.9 * 1000 / 3
121
+ expect(c.donnee_intermediaire.besoin_fr).toBeCloseTo(1000, 9);
122
+ expect(c.donnee_intermediaire.conso_fr).toBeCloseTo(300, 9);
123
+ });
124
+
125
+ test('prorata logement absent : 1 par défaut', () => {
126
+ tv.mockReturnValue({ tv_seer_id: '5', eer: 3 });
127
+ const c = clim({ surface_clim: 100, enum_methode_saisie_carac_sys_id: '1' });
128
+
129
+ calc_clim(c, 1000, 1500, '1', 100);
130
+
131
+ expect(c.donnee_utilisateur.prorata_logement).toBe(1);
132
+ });
133
+
108
134
  test('aucune ligne forfaitaire trouvée : EER indéfini, conso non finie', () => {
109
135
  tv.mockReturnValue(null);
110
136
  const c = clim({ surface_clim: 50, enum_methode_saisie_carac_sys_id: '1' });
package/README.md CHANGED
@@ -375,57 +375,57 @@ Ces résultats sont **générés automatiquement** à la fin de `npm run test:co
375
375
 
376
376
  <!-- CORPUS:START -->
377
377
 
378
- > **Version `1.8.65`** · branche `main` · généré le 2026-10-15
378
+ > **Version `1.8.66`** · branche `main` · généré le 2026-10-16
379
379
  > Seuil de tolérance **5%**
380
380
 
381
381
  <table>
382
382
  <tr>
383
383
  <td align="center"><strong>9</strong><br/><sub>corpus</sub></td>
384
- <td align="center"><strong>89 418</strong><br/><sub>DPE analysés</sub></td>
385
- <td align="center"><strong>71 395</strong><br/><sub>DPE conformes</sub></td>
386
- <td align="center"><strong>79,84 %</strong><br/><sub>réussite globale</sub></td>
384
+ <td align="center"><strong>88 332</strong><br/><sub>DPE analysés</sub></td>
385
+ <td align="center"><strong>71 434</strong><br/><sub>DPE conformes</sub></td>
386
+ <td align="center"><strong>80,87 %</strong><br/><sub>réussite globale</sub></td>
387
387
  </tr>
388
388
  </table>
389
389
 
390
- | | Corpus | Réussite | | DPE conformes |
391
- | :-: | :---------------------------------------------------------------------------------------------------------------------- | ----------: | :--------------------- | -------------: |
392
- | 🔴 | **Généraliste**<br/><sub>`corpus_dpe.csv`</sub> | **48,32 %** | `██████████░░░░░░░░░░` | 4 832 / 9 999 |
393
- | 🟢 | **Appartement · chauffage individuel (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_individuel_2025.csv`</sub> | **94,37 %** | `███████████████████░` | 9 437 / 10 000 |
394
- | 🟢 | **Appartement · chauffage collectif (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_collectif_2025.csv`</sub> | **93,84 %** | `███████████████████░` | 9 384 / 10 000 |
395
- | 🟢 | **Logement individuel (2025)**<br/><sub>`dpe_logement_individuel_2025.csv`</sub> | **91,80 %** | `██████████████████░░` | 9 173 / 9 992 |
396
- | 🟢 | **Maison individuelle (2025)**<br/><sub>`dpe_maison_individuelle_2025.csv`</sub> | **89,93 %** | `██████████████████░░` | 8 992 / 9 999 |
397
- | 🟢 | **Immeuble · chauffage collectif**<br/><sub>`dpe_immeuble_chauffage_collectif.csv`</sub> | **88,40 %** | `██████████████████░░` | 8 676 / 9 814 |
398
- | 🟢 | **Immeuble · chauffage individuel**<br/><sub>`dpe_immeuble_chauffage_individuel.csv`</sub> | **85,37 %** | `█████████████████░░░` | 8 535 / 9 998 |
399
- | 🟡 | **Immeuble · chauffage mixte**<br/><sub>`dpe_immeuble_chauffage_mixte.csv`</sub> | **70,01 %** | `██████████████░░░░░░` | 6 732 / 9 616 |
400
- | 🔴 | **Individuel généré depuis l'immeuble (2026)**<br/><sub>`dpe_individuel_a_partir_dpe_immeuble_2026.csv`</sub> | **56,34 %** | `███████████░░░░░░░░░` | 5 634 / 10 000 |
390
+ | | Corpus | Réussite | | DPE conformes |
391
+ | :-: | :---------------------------------------------------------------------------------------------------------------------- | ----------: | :--------------------- | ------------: |
392
+ | 🔴 | **Généraliste**<br/><sub>`corpus_dpe.csv`</sub> | **49,26 %** | `██████████░░░░░░░░░░` | 4 856 / 9 857 |
393
+ | 🟢 | **Appartement · chauffage individuel (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_individuel_2025.csv`</sub> | **95,39 %** | `███████████████████░` | 9 437 / 9 893 |
394
+ | 🟢 | **Appartement · chauffage collectif (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_collectif_2025.csv`</sub> | **94,54 %** | `███████████████████░` | 9 384 / 9 926 |
395
+ | 🟢 | **Logement individuel (2025)**<br/><sub>`dpe_logement_individuel_2025.csv`</sub> | **93,29 %** | `███████████████████░` | 9 173 / 9 833 |
396
+ | 🟢 | **Maison individuelle (2025)**<br/><sub>`dpe_maison_individuelle_2025.csv`</sub> | **92,03 %** | `██████████████████░░` | 8 992 / 9 771 |
397
+ | 🟢 | **Immeuble · chauffage collectif**<br/><sub>`dpe_immeuble_chauffage_collectif.csv`</sub> | **89,10 %** | `██████████████████░░` | 8 677 / 9 739 |
398
+ | 🟢 | **Immeuble · chauffage individuel**<br/><sub>`dpe_immeuble_chauffage_individuel.csv`</sub> | **86,00 %** | `█████████████████░░░` | 8 535 / 9 924 |
399
+ | 🟡 | **Immeuble · chauffage mixte**<br/><sub>`dpe_immeuble_chauffage_mixte.csv`</sub> | **70,48 %** | `██████████████░░░░░░` | 6 746 / 9 571 |
400
+ | 🔴 | **Individuel généré depuis l'immeuble (2026)**<br/><sub>`dpe_individuel_a_partir_dpe_immeuble_2026.csv`</sub> | **57,38 %** | `███████████░░░░░░░░░` | 5 634 / 9 818 |
401
401
 
402
402
  <sub>🟢 ≥ 85 % · 🟡 ≥ 60 % · 🔴 < 60 %</sub>
403
403
 
404
404
  #### Temps d’exécution
405
405
 
406
- > Durée de l’appel à `calcul_3cl` par DPE, sur 89 384 calculs.
406
+ > Durée de l’appel à `calcul_3cl` par DPE, sur 88 298 calculs.
407
407
  > La copie défensive de l’entrée et la lecture du fichier sont exclues de la mesure.
408
408
 
409
409
  <table>
410
410
  <tr>
411
- <td align="center"><strong>6,57 ms</strong><br/><sub>moyenne</sub></td>
412
- <td align="center"><strong>5,93 ms</strong><br/><sub>médiane</sub></td>
413
- <td align="center"><strong>0,14 ms</strong><br/><sub>min</sub></td>
414
- <td align="center"><strong>1,18 s</strong><br/><sub>max</sub></td>
411
+ <td align="center"><strong>5,90 ms</strong><br/><sub>moyenne</sub></td>
412
+ <td align="center"><strong>5,27 ms</strong><br/><sub>médiane</sub></td>
413
+ <td align="center"><strong>0,11 ms</strong><br/><sub>min</sub></td>
414
+ <td align="center"><strong>874,0 ms</strong><br/><sub>max</sub></td>
415
415
  </tr>
416
416
  </table>
417
417
 
418
418
  | Corpus | Moyenne | Médiane | Min | Max | p95 | p99 |
419
419
  | :--------------------------------------------- | ------: | ------: | ------: | -------: | ------: | ------: |
420
- | **Généraliste** | 5,86 ms | 5,38 ms | 0,25 ms | 67,5 ms | 10,3 ms | 14,9 ms |
421
- | **Appartement · chauffage individuel (2025)** | 4,75 ms | 4,45 ms | 1,70 ms | 31,4 ms | 7,78 ms | 9,98 ms |
422
- | **Appartement · chauffage collectif (2025)** | 5,57 ms | 5,35 ms | 1,77 ms | 41,9 ms | 8,90 ms | 12,9 ms |
423
- | **Logement individuel (2025)** | 6,78 ms | 6,33 ms | 1,78 ms | 40,9 ms | 11,9 ms | 15,7 ms |
424
- | **Maison individuelle (2025)** | 6,44 ms | 5,79 ms | 2,00 ms | 44,3 ms | 11,6 ms | 16,0 ms |
425
- | **Immeuble · chauffage collectif** | 6,89 ms | 6,20 ms | 0,14 ms | 125,0 ms | 12,5 ms | 18,9 ms |
426
- | **Immeuble · chauffage individuel** | 8,10 ms | 7,01 ms | 0,17 ms | 155,3 ms | 16,0 ms | 26,6 ms |
427
- | **Immeuble · chauffage mixte** | 6,87 ms | 5,71 ms | 0,21 ms | 1,18 s | 14,1 ms | 22,6 ms |
428
- | **Individuel généré depuis l'immeuble (2026)** | 7,87 ms | 7,17 ms | 1,90 ms | 59,4 ms | 14,3 ms | 23,0 ms |
420
+ | **Généraliste** | 4,71 ms | 4,25 ms | 0,21 ms | 35,5 ms | 8,47 ms | 11,9 ms |
421
+ | **Appartement · chauffage individuel (2025)** | 4,35 ms | 4,04 ms | 1,48 ms | 41,0 ms | 7,22 ms | 9,56 ms |
422
+ | **Appartement · chauffage collectif (2025)** | 4,56 ms | 4,20 ms | 1,42 ms | 38,5 ms | 7,37 ms | 9,29 ms |
423
+ | **Logement individuel (2025)** | 6,03 ms | 5,55 ms | 1,79 ms | 39,2 ms | 10,8 ms | 14,1 ms |
424
+ | **Maison individuelle (2025)** | 7,11 ms | 6,66 ms | 2,05 ms | 40,4 ms | 12,1 ms | 16,0 ms |
425
+ | **Immeuble · chauffage collectif** | 6,10 ms | 5,34 ms | 0,22 ms | 102,6 ms | 11,1 ms | 19,0 ms |
426
+ | **Immeuble · chauffage individuel** | 7,30 ms | 6,25 ms | 0,11 ms | 146,4 ms | 14,7 ms | 23,0 ms |
427
+ | **Immeuble · chauffage mixte** | 6,13 ms | 5,11 ms | 0,18 ms | 874,0 ms | 12,7 ms | 20,3 ms |
428
+ | **Individuel généré depuis l'immeuble (2026)** | 6,82 ms | 6,06 ms | 1,88 ms | 56,5 ms | 12,8 ms | 19,6 ms |
429
429
 
430
430
  <sub>La moyenne est tirée vers le haut par les DPE collectifs, dont le coût atteint plusieurs
431
431
  dizaines de fois la médiane : c’est la médiane qui décrit le cas courant, et p95/p99 le cas
package/conso.js CHANGED
@@ -983,6 +983,16 @@ function getChauffageConso(gen_ch, field, coef, prorataChauffage, prefix, facteu
983
983
  }, 0);
984
984
  }
985
985
 
986
+ /**
987
+ * Part du logement dans la consommation de froid d'une installation (1 par défaut), positionnée par
988
+ * calc_clim pour les DPE appartement générés à partir de l'immeuble (issue #303).
989
+ * @param fr {object} climatisation
990
+ * @return {number}
991
+ */
992
+ export function prorataFroidLogement(fr) {
993
+ return fr.donnee_utilisateur?.prorata_logement ?? 1;
994
+ }
995
+
986
996
  function calc_conso_pond(
987
997
  Sh,
988
998
  zc_id,
@@ -1086,13 +1096,15 @@ function calc_conso_pond(
1086
1096
  ret.fr = fr_list.reduce((acc, fr) => {
1087
1097
  const typeEnergie = getTypeEnergie(fr.donnee_entree, 'fr');
1088
1098
  const conso =
1089
- fr.donnee_intermediaire.conso_fr * (typeEnergie === 'électricité fr' ? fAc('fr') : 1);
1099
+ fr.donnee_intermediaire.conso_fr *
1100
+ prorataFroidLogement(fr) *
1101
+ (typeEnergie === 'électricité fr' ? fAc('fr') : 1);
1090
1102
 
1091
1103
  return acc + getConso(coef, typeEnergie, conso);
1092
1104
  }, 0);
1093
1105
 
1094
1106
  ret.fr_depensier = fr_list.reduce((acc, fr) => {
1095
- const conso = fr.donnee_intermediaire.conso_fr_depensier;
1107
+ const conso = fr.donnee_intermediaire.conso_fr_depensier * prorataFroidLogement(fr);
1096
1108
  const typeEnergie = getTypeEnergie(fr.donnee_entree, 'fr');
1097
1109
 
1098
1110
  return acc + getConso(coef, typeEnergie, conso);
package/conso.spec.js CHANGED
@@ -71,7 +71,8 @@ const {
71
71
  ARRETES_RESEAU_CHALEUR,
72
72
  COEF_GES_AUTRES_RESEAUX_CHALEUR,
73
73
  indexArreteReseauChaleur,
74
- getContenuCo2ReseauChaleur
74
+ getContenuCo2ReseauChaleur,
75
+ prorataFroidLogement
75
76
  } = await import('./conso.js');
76
77
  const { tv } = await import('./utils.js');
77
78
 
@@ -298,6 +299,49 @@ describe('calc_conso - agrégation des consommations', () => {
298
299
  expect(net.cout.cout_eclairage).toBeCloseTo(brut.cout.cout_eclairage, 9);
299
300
  });
300
301
 
302
+ /**
303
+ * DPE appartement généré à partir de l'immeuble : conso de froid ramenée au logement par le
304
+ * prorata mémorisé dans donnee_utilisateur.prorata_logement.
305
+ * @see https://github.com/Open3CL/engine/issues/303
306
+ */
307
+ test('froid : conso et conso dépensière proratisées au logement', () => {
308
+ const fr = (du) => [
309
+ {
310
+ donnee_entree: { enum_type_energie_id: '1' },
311
+ donnee_intermediaire: { conso_fr: 400, conso_fr_depensier: 1000 },
312
+ donnee_utilisateur: du
313
+ }
314
+ ];
315
+ const plein = calc_conso(100, 1, 1, [], [], [], fr(undefined), 1, 1, DATE_DPE, coef_ep);
316
+ const lot = calc_conso(
317
+ 100,
318
+ 1,
319
+ 1,
320
+ [],
321
+ [],
322
+ [],
323
+ fr({ prorata_logement: 0.25 }),
324
+ 1,
325
+ 1,
326
+ DATE_DPE,
327
+ coef_ep
328
+ );
329
+
330
+ expect(plein.ef_conso.conso_fr).toBeCloseTo(400, 9);
331
+ expect(lot.ef_conso.conso_fr).toBeCloseTo(100, 9);
332
+ expect(lot.ef_conso.conso_fr_depensier).toBeCloseTo(250, 9);
333
+ expect(lot.emission_ges.emission_ges_fr).toBeCloseTo(
334
+ plein.emission_ges.emission_ges_fr * 0.25,
335
+ 9
336
+ );
337
+ });
338
+
339
+ test('prorataFroidLogement : valeur mémorisée, 1 par défaut', () => {
340
+ expect(prorataFroidLogement({ donnee_utilisateur: { prorata_logement: 0.3 } })).toBe(0.3);
341
+ expect(prorataFroidLogement({ donnee_utilisateur: {} })).toBe(1);
342
+ expect(prorataFroidLogement({})).toBe(1);
343
+ });
344
+
301
345
  test('autoconsommation PV : froid électrique minoré, froid non électrique non minoré', () => {
302
346
  const frElec = [
303
347
  {
package/engine.js CHANGED
@@ -35,7 +35,7 @@ import DpeSanitizerService from './dpe-sanitizer.service.js';
35
35
  import { reconstituerRdimDpeGenereImmeuble } from './17.2_rdim_dpe_genere_immeuble.js';
36
36
  import { reconstituerRdimEcsIndividuelleUnique } from './11_rdim_ecs_individuelle_unique.js';
37
37
 
38
- const LIB_VERSION = '1.8.66';
38
+ const LIB_VERSION = '1.8.67';
39
39
 
40
40
  const dpeSanitizerService = new DpeSanitizerService();
41
41
 
@@ -330,7 +330,18 @@ export function calcul_3cl(inputDpe, options) {
330
330
 
331
331
  const bfr = apport_et_besoin.besoin_fr;
332
332
  const bfr_dep = apport_et_besoin.besoin_fr_depensier;
333
- clim.forEach((clim) => calc_clim(clim, bfr, bfr_dep, zc_id, ShChauffageAndEcs));
333
+ /**
334
+ * DPE appartement généré à partir de l'immeuble : besoin et conso de froid calculés à l'échelle de
335
+ * l'installation (immeuble), puis ramenés au logement au prorata Sh_logement / SH_immeuble, comme le
336
+ * chauffage et l'ECS (Tribu `Calcul_installation_froid.cs` l.62-64, `Calcul_batiment.cs`
337
+ * l.1114-1138).
338
+ * @see https://github.com/Open3CL/engine/issues/303
339
+ */
340
+ const prorataFroid =
341
+ dpeGenereImmeuble && Number(ShChauffageAndEcs) > Number(Sh) && Number(Sh) > 0
342
+ ? Number(Sh) / Number(ShChauffageAndEcs)
343
+ : 1;
344
+ clim.forEach((clim) => calc_clim(clim, bfr, bfr_dep, zc_id, ShChauffageAndEcs, prorataFroid));
334
345
 
335
346
  /**
336
347
  * La consommation ECS est obtenu pour certains types de DPE par virtualisation des générateurs collectifs en générateurs individuels virtuels
package/engine.spec.js CHANGED
@@ -776,6 +776,38 @@ describe('calcul_3cl - surfaces immeuble / appartement et prorata', () => {
776
776
  // Auxiliaires ECS : Sh = surface logement, ramenée à l'immeuble dans le calcul.
777
777
  expect(conso_aux_distribution_ecs.mock.calls[0][3]).toBe(80);
778
778
  expect(conso_aux_distribution_ecs.mock.calls[0][4]).toBe(200);
779
+ // Froid : pas un DPE généré depuis l'immeuble, conso de froid non proratisée (#303).
780
+ expect(calc_clim.mock.calls[0][4]).toBe(200);
781
+ expect(calc_clim.mock.calls[0][5]).toBe(1);
782
+ });
783
+
784
+ /**
785
+ * @see https://github.com/Open3CL/engine/issues/303
786
+ */
787
+ test.each(['10', '11', '12', '13', '33', '34', '38', '39', '40'])(
788
+ "DPE appartement généré à partir de l'immeuble (map %s) : conso de froid proratisée Sh / SH",
789
+ (mapId) => {
790
+ enums.methode_application_dpe_log = { [mapId]: 'dpe appartement généré immeuble' };
791
+ const dpe = makeDpe();
792
+ dpe.logement.caracteristique_generale.enum_methode_application_dpe_log_id = mapId;
793
+
794
+ calcul_3cl(dpe, { sanitize: false });
795
+
796
+ // Besoin calculé sur SH immeuble (200), part du logement = 80 / 200.
797
+ expect(calc_clim.mock.calls[0][4]).toBe(200);
798
+ expect(calc_clim.mock.calls[0][5]).toBeCloseTo(0.4, 9);
799
+ }
800
+ );
801
+
802
+ test("DPE généré depuis l'immeuble avec SH immeuble ≤ Sh logement : prorata de froid neutre", () => {
803
+ enums.methode_application_dpe_log = { 10: 'dpe appartement généré immeuble' };
804
+ const dpe = makeDpe();
805
+ dpe.logement.caracteristique_generale.enum_methode_application_dpe_log_id = '10';
806
+ dpe.logement.caracteristique_generale.surface_habitable_immeuble = 80;
807
+
808
+ calcul_3cl(dpe, { sanitize: false });
809
+
810
+ expect(calc_clim.mock.calls[0][5]).toBe(1);
779
811
  });
780
812
 
781
813
  test('appartement : rapport SH immeuble / SH logement transmis au calcul du coût', () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open3cl/engine",
3
- "version": "1.8.66",
3
+ "version": "1.8.67",
4
4
  "description": "Open Source 3CL-DPE engine",
5
5
  "main": "index.js",
6
6
  "directories": {