@open3cl/engine 1.8.62 → 1.8.64

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.
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Le stockage ECS d'un générateur est-il en volume chauffé ?
3
+ *
4
+ * Tribu porte l'information au niveau de l'installation (`installation_ECS.Is_volume_chauffee`,
5
+ * Calcul_batiment.cs l.375-377). Dans l'export ADEME, elle se lit, par ordre de priorité :
6
+ * 1. `position_volume_chauffe_stockage` du générateur, s'il est renseigné ;
7
+ * 2. `tv_rendement_distribution_ecs_id` de l'installation : 1 ou 2 (production en volume
8
+ * habitable) → oui, 3 (production hors volume habitable) → non ;
9
+ * 3. `position_volume_chauffe` du générateur.
10
+ *
11
+ * @see Methode_de_calcul_3CL_DPE_2021-338.pdf - §9.1.1 et §11.3 (rendement de distribution)
12
+ * @param deGenerateur {object} donnee_entree du générateur ECS
13
+ * @param deInstallation {object} donnee_entree de l'installation ECS
14
+ * @returns {boolean}
15
+ */
16
+ export function isStockageEcsVolumeChauffe(deGenerateur, deInstallation) {
17
+ const positionStockage = Number.parseInt(deGenerateur.position_volume_chauffe_stockage);
18
+ if (positionStockage === 0 || positionStockage === 1) {
19
+ return positionStockage === 1;
20
+ }
21
+
22
+ const rdId = Number.parseInt(deInstallation?.tv_rendement_distribution_ecs_id);
23
+ if (rdId === 1 || rdId === 2) {
24
+ return true;
25
+ }
26
+ if (rdId === 3) {
27
+ return false;
28
+ }
29
+
30
+ return Number.parseInt(deGenerateur.position_volume_chauffe) !== 0;
31
+ }
@@ -0,0 +1,35 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { isStockageEcsVolumeChauffe } from './11_stockage_ecs_volume_chauffe.js';
3
+
4
+ describe('isStockageEcsVolumeChauffe - stockage ECS en volume chauffé (#293)', () => {
5
+ /**
6
+ * Tribu : installation_ECS.Is_volume_chauffee (Calcul_batiment.cs l.375-377), lue dans l'export
7
+ * par position_volume_chauffe_stockage, puis tv_rendement_distribution_ecs_id, puis
8
+ * position_volume_chauffe.
9
+ */
10
+ test.each([
11
+ // [position stockage, rendement distribution, position générateur, attendu]
12
+ [1, '3', 0, true], // stockage renseigné prioritaire (oui)
13
+ [0, '1', 1, false], // stockage renseigné prioritaire (non)
14
+ ['1', undefined, 0, true], // valeur texte acceptée
15
+ [undefined, '1', 0, true], // production en volume habitable
16
+ [undefined, 2, 0, true], // production en volume habitable, pièces non contiguës
17
+ [undefined, '3', 1, false], // production hors volume habitable
18
+ [undefined, '4', 0, false], // ligne collective : repli sur la position du générateur
19
+ [undefined, undefined, 1, true], // repli : générateur en volume chauffé
20
+ [undefined, undefined, 0, false], // repli : générateur hors volume chauffé
21
+ [undefined, undefined, undefined, true], // aucune information : récupération conservée
22
+ [null, null, 0, false] // champs nuls traités comme absents
23
+ ])('stockage %s, rd %s, générateur %s => %s', (pvcs, rd, pvc, attendu) => {
24
+ expect(
25
+ isStockageEcsVolumeChauffe(
26
+ { position_volume_chauffe_stockage: pvcs, position_volume_chauffe: pvc },
27
+ { tv_rendement_distribution_ecs_id: rd }
28
+ )
29
+ ).toBe(attendu);
30
+ });
31
+
32
+ test('installation absente : repli sur la position du générateur', () => {
33
+ expect(isStockageEcsVolumeChauffe({ position_volume_chauffe: 0 }, undefined)).toBe(false);
34
+ });
35
+ });
package/9_besoin_ch.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  import { calc_ai_j, calc_as_j } from './6.1_apport_gratuit.js';
10
10
  import { calc_sse_j } from './6.2_surface_sud_equivalente.js';
11
11
  import { bug_for_bug_compat, mois_liste } from './utils.js';
12
+ import { isStockageEcsVolumeChauffe } from './11_stockage_ecs_volume_chauffe.js';
12
13
 
13
14
  export default function calc_besoin_ch(
14
15
  ilpa,
@@ -67,10 +68,7 @@ export default function calc_besoin_ch(
67
68
  } else {
68
69
  Qgw = gen_ecs.reduce((acc, gen_ecs) => {
69
70
  // Pas de récupération de stockage si le ballon est hors volume chauffé
70
- if (
71
- gen_ecs.donnee_entree.position_volume_chauffe_stockage === 0 ||
72
- gen_ecs.donnee_entree.position_volume_chauffe === 0
73
- ) {
71
+ if (!isStockageEcsVolumeChauffe(gen_ecs.donnee_entree, instal_ecs.donnee_entree)) {
74
72
  return acc;
75
73
  }
76
74
 
@@ -313,7 +313,7 @@ describe('calc_besoin_ch - branches complémentaires', () => {
313
313
  donnee_intermediaire: { Qgw: 100 }
314
314
  },
315
315
  {
316
- // générateur hors volume chauffé => ignoré
316
+ // générateur hors volume chauffé mais stockage en volume chauffé => compté (#293)
317
317
  donnee_entree: { position_volume_chauffe_stockage: 1, position_volume_chauffe: 0 },
318
318
  donnee_intermediaire: { Qgw: 100 }
319
319
  },
@@ -660,3 +660,60 @@ describe('calc_besoin_ch - immeuble à installations ECS individuelles et collec
660
660
  expect(ret.pertes_distribution_ecs_recup).toBeCloseTo((0.48 * 100 * total) / 8760, 9);
661
661
  });
662
662
  });
663
+
664
+ describe('calc_besoin_ch - stockage ECS en volume chauffé (#293)', () => {
665
+ /**
666
+ * Cas des exemples de l'issue (2301E2484046J, 2322E2765016S) : ballon électrique LICIEL
667
+ * exporté avec position_volume_chauffe = 0 mais production en volume habitable (rd 1).
668
+ */
669
+ test('ballon en volume habitable exporté avec position_volume_chauffe = 0 : pertes récupérées', () => {
670
+ utilState.bug = false;
671
+ vi.mocked(calc_ai_j).mockReset().mockReturnValue(5000);
672
+ vi.mocked(calc_as_j).mockReset().mockReturnValue(2000);
673
+ vi.mocked(calc_sse_j).mockReset().mockReturnValue(10);
674
+ vi.mocked(calc_besoin_ecs_j).mockReset().mockReturnValue(10);
675
+ vi.mocked(calc_Qrec_gen_j).mockReset().mockReturnValue(0);
676
+ const instal = (rd) => [
677
+ {
678
+ donnee_entree: { enum_type_installation_id: '1', tv_rendement_distribution_ecs_id: rd },
679
+ generateur_ecs_collection: {
680
+ generateur_ecs: [
681
+ { donnee_entree: { position_volume_chauffe: 0 }, donnee_intermediaire: { Qgw: 8760 } }
682
+ ]
683
+ }
684
+ }
685
+ ];
686
+ const enVolume = calc_besoin_ch(
687
+ 0,
688
+ 0,
689
+ 0,
690
+ 0,
691
+ 100,
692
+ 100,
693
+ 3,
694
+ instal('1'),
695
+ [],
696
+ [],
697
+ null,
698
+ 'maison',
699
+ 1
700
+ );
701
+ const horsVolume = calc_besoin_ch(
702
+ 0,
703
+ 0,
704
+ 0,
705
+ 0,
706
+ 100,
707
+ 100,
708
+ 3,
709
+ instal('3'),
710
+ [],
711
+ [],
712
+ null,
713
+ 'maison',
714
+ 1
715
+ );
716
+ expect(enVolume.pertes_stockage_ecs_recup).toBeGreaterThan(0);
717
+ expect(horsVolume.pertes_stockage_ecs_recup).toBe(0);
718
+ });
719
+ });
package/9_chauffage.js CHANGED
@@ -22,6 +22,25 @@ export function isImmeuble(map_id) {
22
22
  return map.includes('immeuble') && !map.includes('appartement') && !map.includes('maison');
23
23
  }
24
24
 
25
+ /**
26
+ * Installation hybride collective-individuelle (enum_type_installation_id = 4) en configuration
27
+ * « chauffage collectif avec base + appoint » (enum_cfg_installation_ch_id = 10) : le premier
28
+ * émetteur est celui de la base collective, les suivants sont les émetteurs de l'appoint
29
+ * individuel. L'intermittence de ces derniers relève de la table « immeuble collectif, chauffage
30
+ * individuel » (Tribu Calcul_emetteur.cs : Is_individuel = oui pour l'émetteur d'appoint).
31
+ * @see Methode_de_calcul_3CL_DPE_2021-338.pdf - §9.8
32
+ * @param {object} instChDe donnee_entree de l'installation de chauffage
33
+ * @param {number} idx rang de l'émetteur dans l'installation
34
+ * @returns {boolean}
35
+ */
36
+ export function isEmetteurAppointIndividuel(instChDe, idx) {
37
+ return (
38
+ idx >= 1 &&
39
+ Number(instChDe.enum_type_installation_id) === 4 &&
40
+ Number(instChDe.enum_cfg_installation_ch_id) === 10
41
+ );
42
+ }
43
+
25
44
  export default function calc_chauffage(
26
45
  dpe,
27
46
  ch,
@@ -50,8 +69,8 @@ export default function calc_chauffage(
50
69
  di.besoin_ch_depensier = bch_dep;
51
70
 
52
71
  const em_ch = ch.emetteur_chauffage_collection.emetteur_chauffage;
53
- em_ch.forEach((em_ch) => {
54
- calc_emetteur_ch(em_ch, de, map_id, inertie_id);
72
+ em_ch.forEach((em_ch, idx) => {
73
+ calc_emetteur_ch(em_ch, de, map_id, inertie_id, isEmetteurAppointIndividuel(de, idx));
55
74
  });
56
75
 
57
76
  if (de.surface_chauffee !== Sh) {
@@ -61,7 +61,8 @@ const {
61
61
  isImmeuble,
62
62
  generateursPrincipauxCascade,
63
63
  ratioDimensionnement,
64
- reseauCirculateurUnique
64
+ reseauCirculateurUnique,
65
+ isEmetteurAppointIndividuel
65
66
  } = await import('./9_chauffage.js');
66
67
  const { requestInput, tv, tvColumnIDs } = await import('./utils.js');
67
68
  const { calc_emetteur_ch } = await import('./9_emetteur_ch.js');
@@ -289,6 +290,51 @@ describe('calc_chauffage - rdim de l’installation transmis au générateur', (
289
290
  });
290
291
  });
291
292
 
293
+ describe('isEmetteurAppointIndividuel (issue #292)', () => {
294
+ test.each([
295
+ ['4', '10', 0, false],
296
+ ['4', '10', 1, true],
297
+ ['4', '10', 3, true],
298
+ [4, 10, 1, true],
299
+ ['4', '11', 1, false],
300
+ ['4', '3', 1, false],
301
+ ['2', '10', 1, false],
302
+ ['1', '10', 1, false]
303
+ ])('type installation %s, cfg %s, rang %s → %s', (ti, cfg, idx, attendu) => {
304
+ expect(
305
+ isEmetteurAppointIndividuel(
306
+ { enum_type_installation_id: ti, enum_cfg_installation_ch_id: cfg },
307
+ idx
308
+ )
309
+ ).toBe(attendu);
310
+ });
311
+ });
312
+
313
+ describe('calc_chauffage - émetteurs d’une installation base + appoint (issue #292)', () => {
314
+ test('type 4 / cfg 10 : seul l’émetteur de rang ≥ 1 est traité comme appoint individuel', () => {
315
+ const ch = installation([generateur(), generateur()], [emetteur('1'), emetteur('1')], {
316
+ enum_type_installation_id: '4',
317
+ enum_cfg_installation_ch_id: '10'
318
+ });
319
+ appel(ch);
320
+
321
+ const appels = vi.mocked(calc_emetteur_ch).mock.calls;
322
+ expect(appels).toHaveLength(2);
323
+ expect(appels[0][4]).toBe(false);
324
+ expect(appels[1][4]).toBe(true);
325
+ });
326
+
327
+ test('autre configuration : aucun émetteur d’appoint individuel', () => {
328
+ const ch = installation([generateur()], [emetteur('1'), emetteur('1')], {
329
+ enum_type_installation_id: '4',
330
+ enum_cfg_installation_ch_id: '11'
331
+ });
332
+ appel(ch);
333
+
334
+ expect(vi.mocked(calc_emetteur_ch).mock.calls.map((c) => c[4])).toEqual([false, false]);
335
+ });
336
+ });
337
+
292
338
  describe('isImmeuble', () => {
293
339
  test('immeuble uniquement, pas appartement issu d’un immeuble ni méthode inconnue', () => {
294
340
  expect(isImmeuble(6)).toBe(true);
package/9_emetteur_ch.js CHANGED
@@ -65,7 +65,20 @@ function tv_rendement_regulation(di, de) {
65
65
  }
66
66
  }
67
67
 
68
- function tv_intermittence(di, de, inst_ch_de, map_id, inertie_id) {
68
+ /**
69
+ * En bug_for_bug_compat, indique si l'intermittence saisie par le logiciel (tv_intermittence_id)
70
+ * correspond à une ligne de la table réservée aux installations collectives.
71
+ * @param {object} de donnee_entree de l'émetteur
72
+ * @returns {boolean}
73
+ */
74
+ export function isIntermittenceCollectiveSaisie(de) {
75
+ if (!bug_for_bug_compat || !de.tv_intermittence_id) return false;
76
+ const row = tv('intermittence', { tv_intermittence_id: de.tv_intermittence_id }, de);
77
+ if (!row || !row.enum_type_installation_id) return false;
78
+ return !row.enum_type_installation_id.split('|').includes('1');
79
+ }
80
+
81
+ function tv_intermittence(di, de, inst_ch_de, map_id, inertie_id, isAppointIndividuel) {
69
82
  let hasComptage = false;
70
83
  let row;
71
84
 
@@ -106,6 +119,15 @@ function tv_intermittence(di, de, inst_ch_de, map_id, inertie_id) {
106
119
  matcher.enum_classe_inertie_id = inertie_id;
107
120
  }
108
121
 
122
+ // Émetteur d'appoint individuel d'une installation base collective + appoint individuel :
123
+ // table « chauffage individuel », sans notion de comptage individuel (§9.8).
124
+ // En bug_for_bug_compat, on conserve la table collective si le logiciel l'a retenue pour cet
125
+ // émetteur (tv_intermittence_id saisi sur une ligne sans installation individuelle).
126
+ if (isAppointIndividuel && !isIntermittenceCollectiveSaisie(de)) {
127
+ matcher.enum_type_installation_id = '1';
128
+ delete matcher.comptage_individuel;
129
+ }
130
+
109
131
  row = tv('intermittence', matcher, de);
110
132
  if (row) {
111
133
  di.i0 = Number(row.i0);
@@ -115,7 +137,13 @@ function tv_intermittence(di, de, inst_ch_de, map_id, inertie_id) {
115
137
  }
116
138
  }
117
139
 
118
- export function calc_emetteur_ch(em_ch, inst_ch_de, map_id, inertie_id) {
140
+ export function calc_emetteur_ch(
141
+ em_ch,
142
+ inst_ch_de,
143
+ map_id,
144
+ inertie_id,
145
+ isAppointIndividuel = false
146
+ ) {
119
147
  const de = em_ch.donnee_entree;
120
148
  const di = {};
121
149
  const du = {};
@@ -123,7 +151,7 @@ export function calc_emetteur_ch(em_ch, inst_ch_de, map_id, inertie_id) {
123
151
  tv_rendement_distribution_ch(di, de);
124
152
  tv_rendement_emission(di, de);
125
153
  tv_rendement_regulation(di, de);
126
- tv_intermittence(di, de, inst_ch_de, map_id, inertie_id);
154
+ tv_intermittence(di, de, inst_ch_de, map_id, inertie_id, isAppointIndividuel);
127
155
 
128
156
  em_ch.donnee_intermediaire = di;
129
157
  em_ch.donnee_utilisateur = du;
@@ -26,7 +26,8 @@ vi.mock('./core/tv/infrastructure/tvs.store.js', () => ({
26
26
  TvsStore: vi.fn(() => mockTvsStore)
27
27
  }));
28
28
 
29
- const { rendement_emission, calc_emetteur_ch } = await import('./9_emetteur_ch.js');
29
+ const { rendement_emission, calc_emetteur_ch, isIntermittenceCollectiveSaisie } =
30
+ await import('./9_emetteur_ch.js');
30
31
  const { tv } = await import('./utils.js');
31
32
 
32
33
  beforeEach(() => {
@@ -409,6 +410,126 @@ describe('tv_intermittence - intermittence', () => {
409
410
  });
410
411
  });
411
412
 
413
+ /**
414
+ * Issue #292 : émetteur d'appoint individuel d'une installation hybride (type 4) base collective +
415
+ * appoint individuel (cfg 10). Tribu (Calcul_emetteur.cs l.80-129) lit I0 dans la table
416
+ * « chauffage individuel » pour cet émetteur.
417
+ * @see Methode_de_calcul_3CL_DPE_2021-338.pdf - §9.8
418
+ */
419
+ describe('tv_intermittence - émetteur d’appoint individuel (issue #292)', () => {
420
+ function emetteurAppoint(tvIntermittenceId) {
421
+ const em = emetteur({ typeEmissionDistributionId: '11' });
422
+ Object.assign(em.donnee_entree, {
423
+ enum_type_chauffage_id: '2',
424
+ enum_equipement_intermittence_id: '1',
425
+ enum_type_regulation_id: '1',
426
+ tv_intermittence_id: tvIntermittenceId
427
+ });
428
+ return em;
429
+ }
430
+
431
+ const ligneIndividuelle = {
432
+ i0: '0.97',
433
+ tv_intermittence_id: '112',
434
+ enum_type_installation_id: '1|4'
435
+ };
436
+ const ligneCollective = {
437
+ i0: '1.07',
438
+ tv_intermittence_id: '158',
439
+ enum_type_installation_id: '2|3|4'
440
+ };
441
+
442
+ test('appoint individuel : matcher sur installation individuelle, sans comptage', () => {
443
+ const em = emetteurAppoint();
444
+ mockTv({ intermittence: ligneIndividuelle });
445
+
446
+ calc_emetteur_ch(em, { enum_type_installation_id: '4' }, '31', '3', true);
447
+
448
+ expect(tvMatchers('intermittence')).toEqual([
449
+ {
450
+ enum_methode_application_dpe_log_id: '31',
451
+ enum_type_installation_id: '1',
452
+ enum_type_chauffage_id: '2',
453
+ enum_equipement_intermittence_id: '1',
454
+ enum_type_regulation_id: '1',
455
+ enum_type_emission_distribution_id: '11'
456
+ }
457
+ ]);
458
+ expect(em.donnee_intermediaire.i0).toBeCloseTo(0.97, 10);
459
+ expect(em.donnee_entree.tv_intermittence_id).toBe(112);
460
+ });
461
+
462
+ test('émetteur de base (non appoint) : matcher inchangé (type 4, comptage)', () => {
463
+ const em = emetteurAppoint();
464
+ mockTv({ intermittence: ligneCollective });
465
+
466
+ calc_emetteur_ch(em, { enum_type_installation_id: '4' }, '31', '3');
467
+
468
+ const [matcher] = tvMatchers('intermittence');
469
+ expect(matcher.enum_type_installation_id).toBe('4');
470
+ expect(matcher.comptage_individuel).toBe('Absence');
471
+ expect(em.donnee_intermediaire.i0).toBeCloseTo(1.07, 10);
472
+ });
473
+
474
+ test('bug_for_bug_compat : ligne individuelle saisie par le logiciel → table individuelle', () => {
475
+ utilsState.bugForBugCompat = true;
476
+ const em = emetteurAppoint(112);
477
+ mockTv({ intermittence: ligneIndividuelle });
478
+
479
+ calc_emetteur_ch(em, { enum_type_installation_id: '4' }, '31', '3', true);
480
+
481
+ const matchers = tvMatchers('intermittence');
482
+ expect(matchers.at(-1).enum_type_installation_id).toBe('1');
483
+ expect(matchers.at(-1)).not.toHaveProperty('comptage_individuel');
484
+ });
485
+
486
+ test('bug_for_bug_compat : ligne collective saisie par le logiciel → table collective conservée', () => {
487
+ utilsState.bugForBugCompat = true;
488
+ const em = emetteurAppoint(158);
489
+ mockTv({ intermittence: ligneCollective });
490
+
491
+ calc_emetteur_ch(em, { enum_type_installation_id: '4' }, '27', '3', true);
492
+
493
+ const matchers = tvMatchers('intermittence');
494
+ expect(matchers.at(-1).enum_type_installation_id).toBe('4');
495
+ expect(matchers.at(-1).comptage_individuel).toBe('Absence');
496
+ expect(em.donnee_intermediaire.i0).toBeCloseTo(1.07, 10);
497
+ });
498
+ });
499
+
500
+ describe('isIntermittenceCollectiveSaisie', () => {
501
+ test('hors bug_for_bug_compat : toujours faux, sans lecture de table', () => {
502
+ expect(isIntermittenceCollectiveSaisie({ tv_intermittence_id: 158 })).toBe(false);
503
+ expect(tv).not.toHaveBeenCalled();
504
+ });
505
+
506
+ test('bug_for_bug_compat sans tv_intermittence_id : faux', () => {
507
+ utilsState.bugForBugCompat = true;
508
+ expect(isIntermittenceCollectiveSaisie({})).toBe(false);
509
+ expect(tv).not.toHaveBeenCalled();
510
+ });
511
+
512
+ test.each([
513
+ ['2|3|4', true],
514
+ ['2', true],
515
+ ['1|4', false],
516
+ ['1', false],
517
+ [undefined, false]
518
+ ])('type d’installation de la ligne saisie = %s → %s', (types, attendu) => {
519
+ utilsState.bugForBugCompat = true;
520
+ mockTv({ intermittence: { tv_intermittence_id: '7', enum_type_installation_id: types } });
521
+
522
+ expect(isIntermittenceCollectiveSaisie({ tv_intermittence_id: 7 })).toBe(attendu);
523
+ expect(tvMatchers('intermittence')).toEqual([{ tv_intermittence_id: 7 }]);
524
+ });
525
+
526
+ test('ligne saisie introuvable : faux', () => {
527
+ utilsState.bugForBugCompat = true;
528
+ mockTv({});
529
+ expect(isIntermittenceCollectiveSaisie({ tv_intermittence_id: 7 })).toBe(false);
530
+ });
531
+ });
532
+
412
533
  describe('calc_emetteur_ch - résultat', () => {
413
534
  test('initialise donnee_utilisateur et remplace donnee_intermediaire', () => {
414
535
  const em = emetteur({ typeEmissionDistributionId: '12' });
package/README.md CHANGED
@@ -375,29 +375,29 @@ 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.61`** · branche `main` · généré le 2026-10-15
378
+ > **Version `1.8.63`** · branche `main` · généré le 2026-10-15
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
384
  <td align="center"><strong>89 418</strong><br/><sub>DPE analysés</sub></td>
385
- <td align="center"><strong>70 789</strong><br/><sub>DPE conformes</sub></td>
386
- <td align="center"><strong>79,17 %</strong><br/><sub>réussite globale</sub></td>
385
+ <td align="center"><strong>71 098</strong><br/><sub>DPE conformes</sub></td>
386
+ <td align="center"><strong>79,51 %</strong><br/><sub>réussite globale</sub></td>
387
387
  </tr>
388
388
  </table>
389
389
 
390
390
  | | Corpus | Réussite | | DPE conformes |
391
391
  | :-: | :---------------------------------------------------------------------------------------------------------------------- | ----------: | :--------------------- | -------------: |
392
- | 🔴 | **Généraliste**<br/><sub>`corpus_dpe.csv`</sub> | **47,71 %** | `██████████░░░░░░░░░░` | 4 771 / 9 999 |
393
- | 🟢 | **Appartement · chauffage collectif (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_collectif_2025.csv`</sub> | **93,28 %** | `███████████████████░` | 9 328 / 10 000 |
394
- | 🟢 | **Appartement · chauffage individuel (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_individuel_2025.csv`</sub> | **92,96 %** | `███████████████████░` | 9 296 / 10 000 |
395
- | 🟢 | **Logement individuel (2025)**<br/><sub>`dpe_logement_individuel_2025.csv`</sub> | **90,69 %** | `██████████████████░░` | 9 062 / 9 992 |
396
- | 🟢 | **Maison individuelle (2025)**<br/><sub>`dpe_maison_individuelle_2025.csv`</sub> | **88,92 %** | `██████████████████░░` | 8 891 / 9 999 |
397
- | 🟢 | **Immeuble · chauffage collectif**<br/><sub>`dpe_immeuble_chauffage_collectif.csv`</sub> | **88,19 %** | `██████████████████░░` | 8 655 / 9 814 |
398
- | 🟢 | **Immeuble · chauffage individuel**<br/><sub>`dpe_immeuble_chauffage_individuel.csv`</sub> | **85,05 %** | `█████████████████░░░` | 8 503 / 9 998 |
399
- | 🟡 | **Immeuble · chauffage mixte**<br/><sub>`dpe_immeuble_chauffage_mixte.csv`</sub> | **69,36 %** | `██████████████░░░░░░` | 6 670 / 9 616 |
400
- | 🔴 | **Individuel généré depuis l'immeuble (2026)**<br/><sub>`dpe_individuel_a_partir_dpe_immeuble_2026.csv`</sub> | **56,13 %** | `███████████░░░░░░░░░` | 5 613 / 10 000 |
392
+ | 🔴 | **Généraliste**<br/><sub>`corpus_dpe.csv`</sub> | **47,82 %** | `██████████░░░░░░░░░░` | 4 782 / 9 999 |
393
+ | 🟢 | **Appartement · chauffage individuel (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_individuel_2025.csv`</sub> | **93,94 %** | `███████████████████░` | 9 394 / 10 000 |
394
+ | 🟢 | **Appartement · chauffage collectif (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_collectif_2025.csv`</sub> | **93,64 %** | `███████████████████░` | 9 364 / 10 000 |
395
+ | 🟢 | **Logement individuel (2025)**<br/><sub>`dpe_logement_individuel_2025.csv`</sub> | **91,57 %** | `██████████████████░░` | 9 150 / 9 992 |
396
+ | 🟢 | **Maison individuelle (2025)**<br/><sub>`dpe_maison_individuelle_2025.csv`</sub> | **89,44 %** | `██████████████████░░` | 8 943 / 9 999 |
397
+ | 🟢 | **Immeuble · chauffage collectif**<br/><sub>`dpe_immeuble_chauffage_collectif.csv`</sub> | **88,20 %** | `██████████████████░░` | 8 656 / 9 814 |
398
+ | 🟢 | **Immeuble · chauffage individuel**<br/><sub>`dpe_immeuble_chauffage_individuel.csv`</sub> | **85,14 %** | `█████████████████░░░` | 8 512 / 9 998 |
399
+ | 🟡 | **Immeuble · chauffage mixte**<br/><sub>`dpe_immeuble_chauffage_mixte.csv`</sub> | **69,53 %** | `██████████████░░░░░░` | 6 686 / 9 616 |
400
+ | 🔴 | **Individuel généré depuis l'immeuble (2026)**<br/><sub>`dpe_individuel_a_partir_dpe_immeuble_2026.csv`</sub> | **56,11 %** | `███████████░░░░░░░░░` | 5 611 / 10 000 |
401
401
 
402
402
  <sub>🟢 ≥ 85 % · 🟡 ≥ 60 % · 🔴 < 60 %</sub>
403
403
 
@@ -408,24 +408,24 @@ Ces résultats sont **générés automatiquement** à la fin de `npm run test:co
408
408
 
409
409
  <table>
410
410
  <tr>
411
- <td align="center"><strong>5,18 ms</strong><br/><sub>moyenne</sub></td>
412
- <td align="center"><strong>4,55 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>994,3 ms</strong><br/><sub>max</sub></td>
411
+ <td align="center"><strong>6,64 ms</strong><br/><sub>moyenne</sub></td>
412
+ <td align="center"><strong>5,83 ms</strong><br/><sub>médiane</sub></td>
413
+ <td align="center"><strong>0,13 ms</strong><br/><sub>min</sub></td>
414
+ <td align="center"><strong>759,2 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** | 4,55 ms | 4,10 ms | 0,21 ms | 40,1 ms | 8,24 ms | 11,1 ms |
421
- | **Appartement · chauffage collectif (2025)** | 4,51 ms | 4,14 ms | 1,59 ms | 31,4 ms | 7,33 ms | 9,53 ms |
422
- | **Appartement · chauffage individuel (2025)** | 4,03 ms | 3,71 ms | 1,49 ms | 34,3 ms | 6,83 ms | 8,45 ms |
423
- | **Logement individuel (2025)** | 4,85 ms | 4,34 ms | 1,66 ms | 30,8 ms | 8,92 ms | 11,8 ms |
424
- | **Maison individuelle (2025)** | 5,54 ms | 5,04 ms | 1,83 ms | 50,4 ms | 9,68 ms | 12,7 ms |
425
- | **Immeuble · chauffage collectif** | 5,54 ms | 4,80 ms | 0,12 ms | 92,2 ms | 10,1 ms | 15,7 ms |
426
- | **Immeuble · chauffage individuel** | 6,45 ms | 5,48 ms | 0,11 ms | 120,4 ms | 12,9 ms | 20,5 ms |
427
- | **Immeuble · chauffage mixte** | 5,36 ms | 4,35 ms | 0,18 ms | 994,3 ms | 11,0 ms | 18,0 ms |
428
- | **Individuel généré depuis l'immeuble (2026)** | 5,80 ms | 4,96 ms | 1,77 ms | 53,2 ms | 10,8 ms | 17,9 ms |
420
+ | **Généraliste** | 5,27 ms | 4,83 ms | 0,23 ms | 38,0 ms | 9,31 ms | 12,8 ms |
421
+ | **Appartement · chauffage individuel (2025)** | 5,05 ms | 4,61 ms | 1,84 ms | 39,8 ms | 8,69 ms | 11,1 ms |
422
+ | **Appartement · chauffage collectif (2025)** | 5,64 ms | 5,11 ms | 1,92 ms | 58,6 ms | 9,35 ms | 14,6 ms |
423
+ | **Logement individuel (2025)** | 6,17 ms | 5,54 ms | 2,13 ms | 48,5 ms | 11,3 ms | 15,3 ms |
424
+ | **Maison individuelle (2025)** | 7,18 ms | 6,49 ms | 2,29 ms | 52,4 ms | 12,7 ms | 16,4 ms |
425
+ | **Immeuble · chauffage collectif** | 7,20 ms | 6,19 ms | 0,24 ms | 140,8 ms | 13,4 ms | 22,0 ms |
426
+ | **Immeuble · chauffage individuel** | 8,44 ms | 7,21 ms | 0,13 ms | 115,0 ms | 17,0 ms | 27,1 ms |
427
+ | **Immeuble · chauffage mixte** | 7,18 ms | 5,88 ms | 0,23 ms | 759,2 ms | 15,3 ms | 24,7 ms |
428
+ | **Individuel généré depuis l'immeuble (2026)** | 7,70 ms | 6,63 ms | 2,02 ms | 81,6 ms | 14,5 ms | 24,0 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/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.62';
38
+ const LIB_VERSION = '1.8.64';
39
39
 
40
40
  const dpeSanitizerService = new DpeSanitizerService();
41
41
 
@@ -1,5 +1,6 @@
1
1
  import { GenerateurEcsService } from './generateur-ecs.service.js';
2
2
  import { inject } from 'dioma';
3
+ import { isStockageEcsVolumeChauffe } from '../../../../11_stockage_ecs_volume_chauffe.js';
3
4
 
4
5
  /**
5
6
  * Calcul des besoins et pertes des installations ECS
@@ -142,11 +143,8 @@ export class InstallationEcsService {
142
143
  if (installationIndividuelle) {
143
144
  installationEcs.donnee_utilisateur.QgwRecuperable =
144
145
  installationEcs.generateur_ecs_collection?.generateur_ecs
145
- .filter(
146
- (generateurEcs) =>
147
- parseInt(generateurEcs.donnee_entree.position_volume_chauffe_stockage) === 1 ||
148
- (generateurEcs.donnee_entree.position_volume_chauffe_stockage === undefined &&
149
- parseInt(generateurEcs.donnee_entree.position_volume_chauffe) === 1)
146
+ .filter((generateurEcs) =>
147
+ isStockageEcsVolumeChauffe(generateurEcs.donnee_entree, installationEcsDE)
150
148
  )
151
149
  .reduce((acc, generateurEcs) => {
152
150
  return acc + generateurEcs.donnee_utilisateur.Qgw;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open3cl/engine",
3
- "version": "1.8.62",
3
+ "version": "1.8.64",
4
4
  "description": "Open Source 3CL-DPE engine",
5
5
  "main": "index.js",
6
6
  "directories": {