@open3cl/engine 1.8.23 → 1.8.24

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/9_chauffage.js CHANGED
@@ -110,15 +110,18 @@ export default function calc_chauffage(
110
110
  * Cas particulier des PAC hybrides avec répartition forfaitaire du besoin
111
111
  * @type {number|number}
112
112
  */
113
- const Pnominal = gen_ch.reduce((acc, gen) => acc + (gen.donnee_intermediaire.pn || 0), 0);
114
- du.Pnominal = Pnominal;
115
-
116
113
  const nbCascadeAndCombustion = gen_ch.filter(
117
114
  (value) =>
118
115
  value.donnee_utilisateur.isCombustionGenerator &&
119
116
  Number.parseInt(de.enum_cfg_installation_ch_id) === 1
120
117
  ).length;
121
118
 
119
+ // Cascade avec priorité : seuls les générateurs principaux assurent le besoin (et dimensionnent
120
+ // le taux de charge) ; les générateurs secondaires (relève) ont un besoin nul.
121
+ const principaux = nbCascadeAndCombustion > 1 ? generateursPrincipauxCascade(gen_ch) : gen_ch;
122
+ const Pnominal = principaux.reduce((acc, gen) => acc + (gen.donnee_intermediaire.pn || 0), 0);
123
+ du.Pnominal = Pnominal;
124
+
122
125
  // Nombre de générateurs avec une consommation des auxiliaires de distribution
123
126
  const nbGenWithAuxConsoDistribution = gen_ch.reduce((acc, gen) => {
124
127
  if (hasConsoForAuxDistribution(gen.donnee_entree.enum_type_generateur_ch_id)) {
@@ -148,7 +151,8 @@ export default function calc_chauffage(
148
151
  nbCascadeAndCombustion,
149
152
  nbCascadeForSameEmetteur,
150
153
  Pnominal,
151
- zc
154
+ zc,
155
+ principaux.includes(gen)
152
156
  );
153
157
  /* c8 ignore next 2 -- repli défensif inatteignable : donnee_utilisateur est toujours renseignée
154
158
  par la première boucle sur gen_ch ci-dessus */
@@ -194,6 +198,29 @@ export default function calc_chauffage(
194
198
  ch.donnee_utilisateur = du;
195
199
  }
196
200
 
201
+ /**
202
+ * 13.2.1.3 Cascade de deux générateurs à combustion avec priorité
203
+ *
204
+ * `priorite_generateur_cascade` (modèle DPE ADEME) : 1 = générateur principal, 2 = secondaire…
205
+ * « Dans le cas de deux générateurs en cascade sans priorité, les deux générateurs sont déclarés
206
+ * comme principal. » Les générateurs principaux sont ceux de plus petite priorité ; si aucune
207
+ * priorité n'est renseignée, ou si toutes sont identiques, tous les générateurs sont principaux
208
+ * (cascade sans priorité : prorata des puissances nominales).
209
+ *
210
+ * Comme le moteur de référence CSTB (Tribu, autotests État APP5-0-4/41/43), le générateur
211
+ * prioritaire assure la totalité du besoin et son taux de charge est calculé avec sa seule Pn.
212
+ *
213
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §13.2.1.3
214
+ * @param gen_ch {GenerateurChauffageItem[]}
215
+ * @return {GenerateurChauffageItem[]} générateurs principaux
216
+ */
217
+ export function generateursPrincipauxCascade(gen_ch) {
218
+ const priorites = gen_ch.map((gen) => Number(gen.donnee_entree.priorite_generateur_cascade));
219
+ if (priorites.some((p) => !(p > 0))) return gen_ch;
220
+ const prioriteMin = Math.min(...priorites);
221
+ return gen_ch.filter((_gen, i) => priorites[i] === prioriteMin);
222
+ }
223
+
197
224
  /**
198
225
  * 13.2.1.3 Cascade de deux générateurs à combustion
199
226
  * Une puissance relative pour chaque générateur est calculée et appliquée à la conso globale de chauffage
@@ -203,7 +230,14 @@ export default function calc_chauffage(
203
230
  * Cas particulier des PAC hybrides avec répartition forfaitaire du besoin
204
231
  * @type {number|number}
205
232
  */
206
- function getProrataGenerateur(genCh, nbCascadeAndCombustion, nbGenerateurCascade, Pnominal, zc) {
233
+ function getProrataGenerateur(
234
+ genCh,
235
+ nbCascadeAndCombustion,
236
+ nbGenerateurCascade,
237
+ Pnominal,
238
+ zc,
239
+ estPrincipal
240
+ ) {
207
241
  // IDs des pompes à chaleur hybrides
208
242
  if (
209
243
  genCh.donnee_entree.enum_type_generateur_ch_id >= 145 &&
@@ -233,9 +267,11 @@ function getProrataGenerateur(genCh, nbCascadeAndCombustion, nbGenerateurCascade
233
267
  }
234
268
  }
235
269
 
236
- return nbCascadeAndCombustion > 1
237
- ? genCh.donnee_intermediaire.pn / Pnominal
238
- : 1 / (nbGenerateurCascade || 1);
270
+ if (nbCascadeAndCombustion > 1) {
271
+ // générateur secondaire d'une cascade avec priorité : aucun besoin assuré
272
+ return estPrincipal ? genCh.donnee_intermediaire.pn / Pnominal : 0;
273
+ }
274
+ return 1 / (nbGenerateurCascade || 1);
239
275
  }
240
276
 
241
277
  /**
@@ -58,7 +58,8 @@ vi.mock('./enums.js', () => ({
58
58
  const {
59
59
  default: calc_chauffage,
60
60
  tauxChargeForGenerator,
61
- isImmeuble
61
+ isImmeuble,
62
+ generateursPrincipauxCascade
62
63
  } = await import('./9_chauffage.js');
63
64
  const { requestInput, tv, tvColumnIDs } = await import('./utils.js');
64
65
  const { calc_emetteur_ch } = await import('./9_emetteur_ch.js');
@@ -457,6 +458,100 @@ describe('calc_chauffage - prorata entre générateurs', () => {
457
458
  });
458
459
  });
459
460
 
461
+ /**
462
+ * 13.2.1.3 Cascade de deux générateurs à combustion avec priorité
463
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §13.2.1.3
464
+ */
465
+ describe('calc_chauffage - cascade de générateurs à combustion avec priorité', () => {
466
+ /** Deux chaudières (combustion) en cascade sur un seul émetteur. */
467
+ function cascade(prio1, prio2) {
468
+ vi.mocked(checkForGeneratorType).mockImplementation((_dpe, _de, _di, du) => {
469
+ du.isCombustionGenerator = true;
470
+ });
471
+ vi.mocked(tvColumnIDs).mockReturnValue([]);
472
+ return installation(
473
+ [
474
+ generateur({ priorite_generateur_cascade: prio1 }, { pn: 17000 }),
475
+ generateur({ priorite_generateur_cascade: prio2 }, { pn: 13000 })
476
+ ],
477
+ [emetteur('1')]
478
+ );
479
+ }
480
+
481
+ test('sans priorité : prorata des puissances nominales (Pnominal = somme des Pn)', () => {
482
+ const ch = cascade(undefined, undefined);
483
+ appel(ch, { bch: 3000 });
484
+ expect(vi.mocked(calc_generateur_ch).mock.calls[0][5]).toBeCloseTo(1700, 9);
485
+ expect(vi.mocked(calc_generateur_ch).mock.calls[1][5]).toBeCloseTo(1300, 9);
486
+ expect(ch.donnee_utilisateur.Pnominal).toBe(30000);
487
+ });
488
+
489
+ test('deux générateurs déclarés principaux (même priorité) : prorata des puissances', () => {
490
+ const ch = cascade(1, 1);
491
+ appel(ch, { bch: 3000 });
492
+ expect(vi.mocked(calc_generateur_ch).mock.calls[0][5]).toBeCloseTo(1700, 9);
493
+ expect(vi.mocked(calc_generateur_ch).mock.calls[1][5]).toBeCloseTo(1300, 9);
494
+ });
495
+
496
+ test('priorité 1 / 2 : le principal assure tout le besoin, le secondaire aucun', () => {
497
+ const ch = cascade(1, 2);
498
+ appel(ch, { bch: 3000, bch_dep: 3600 });
499
+ const [appelPrincipal, appelSecondaire] = vi.mocked(calc_generateur_ch).mock.calls;
500
+ expect(appelPrincipal[5]).toBe(3000);
501
+ expect(appelPrincipal[6]).toBe(3600);
502
+ expect(appelSecondaire[5]).toBe(0);
503
+ expect(appelSecondaire[6]).toBe(0);
504
+ // le taux de charge (cdimref) est dimensionné sur la seule Pn du principal
505
+ expect(ch.donnee_utilisateur.Pnominal).toBe(17000);
506
+ });
507
+
508
+ test('priorité inversée (2 / 1) : le second générateur est le principal', () => {
509
+ const ch = cascade(2, 1);
510
+ appel(ch, { bch: 3000 });
511
+ expect(vi.mocked(calc_generateur_ch).mock.calls[0][5]).toBe(0);
512
+ expect(vi.mocked(calc_generateur_ch).mock.calls[1][5]).toBe(3000);
513
+ expect(ch.donnee_utilisateur.Pnominal).toBe(13000);
514
+ });
515
+
516
+ test('générateur unique avec priorité : besoin entier (pas de cascade)', () => {
517
+ vi.mocked(checkForGeneratorType).mockImplementation((_dpe, _de, _di, du) => {
518
+ du.isCombustionGenerator = true;
519
+ });
520
+ vi.mocked(tvColumnIDs).mockReturnValue([]);
521
+ const ch = installation(
522
+ [generateur({ priorite_generateur_cascade: 2 }, { pn: 17000 })],
523
+ [emetteur('1')]
524
+ );
525
+ appel(ch, { bch: 3000 });
526
+ expect(vi.mocked(calc_generateur_ch).mock.calls[0][5]).toBe(3000);
527
+ expect(ch.donnee_utilisateur.Pnominal).toBe(17000);
528
+ });
529
+ });
530
+
531
+ describe('generateursPrincipauxCascade', () => {
532
+ const gen = (priorite) => ({ donnee_entree: { priorite_generateur_cascade: priorite } });
533
+
534
+ test('priorité absente sur un générateur : tous principaux', () => {
535
+ const gens = [gen(1), gen(undefined)];
536
+ expect(generateursPrincipauxCascade(gens)).toEqual(gens);
537
+ });
538
+
539
+ test('priorité nulle ou invalide : tous principaux', () => {
540
+ const gens = [gen('0'), gen('2')];
541
+ expect(generateursPrincipauxCascade(gens)).toEqual(gens);
542
+ });
543
+
544
+ test('priorités saisies en chaîne : plus petite priorité retenue', () => {
545
+ const gens = [gen('2'), gen('1'), gen('3')];
546
+ expect(generateursPrincipauxCascade(gens)).toEqual([gens[1]]);
547
+ });
548
+
549
+ test('plusieurs générateurs de même priorité minimale : tous retenus', () => {
550
+ const gens = [gen(1), gen(1), gen(2)];
551
+ expect(generateursPrincipauxCascade(gens)).toEqual([gens[0], gens[1]]);
552
+ });
553
+ });
554
+
460
555
  describe('calc_chauffage - auxiliaires de distribution répartis', () => {
461
556
  test('répartition de la consommation d’auxiliaires sur les générateurs éligibles', () => {
462
557
  vi.mocked(hasConsoForAuxDistribution).mockReturnValue(true);
package/README.md CHANGED
@@ -370,7 +370,7 @@ Ces résultats sont **générés automatiquement** à la fin de `npm run test:co
370
370
 
371
371
  <!-- CORPUS:START -->
372
372
 
373
- > **Version `1.8.22`** · branche `main` · généré le 2026-10-04
373
+ > **Version `1.8.23`** · branche `main` · généré le 2026-10-04
374
374
  > Seuil de tolérance **5%**
375
375
 
376
376
  <table>
@@ -403,24 +403,24 @@ Ces résultats sont **générés automatiquement** à la fin de `npm run test:co
403
403
 
404
404
  <table>
405
405
  <tr>
406
- <td align="center"><strong>5,36 ms</strong><br/><sub>moyenne</sub></td>
407
- <td align="center"><strong>4,64 ms</strong><br/><sub>médiane</sub></td>
408
- <td align="center"><strong>0,10 ms</strong><br/><sub>min</sub></td>
409
- <td align="center"><strong>823,0 ms</strong><br/><sub>max</sub></td>
406
+ <td align="center"><strong>5,32 ms</strong><br/><sub>moyenne</sub></td>
407
+ <td align="center"><strong>4,66 ms</strong><br/><sub>médiane</sub></td>
408
+ <td align="center"><strong>0,12 ms</strong><br/><sub>min</sub></td>
409
+ <td align="center"><strong>933,6 ms</strong><br/><sub>max</sub></td>
410
410
  </tr>
411
411
  </table>
412
412
 
413
413
  | Corpus | Moyenne | Médiane | Min | Max | p95 | p99 |
414
414
  | :--------------------------------------------- | ------: | ------: | ------: | -------: | ------: | ------: |
415
- | **Généraliste** | 5,23 ms | 4,71 ms | 0,23 ms | 52,3 ms | 9,47 ms | 13,8 ms |
416
- | **Appartement · chauffage individuel (2025)** | 4,08 ms | 3,70 ms | 1,52 ms | 32,7 ms | 7,06 ms | 9,53 ms |
417
- | **Logement individuel (2025)** | 4,71 ms | 4,17 ms | 1,61 ms | 28,7 ms | 8,70 ms | 12,0 ms |
418
- | **Maison individuelle (2025)** | 6,26 ms | 5,66 ms | 1,96 ms | 47,2 ms | 11,3 ms | 15,1 ms |
419
- | **Immeuble · chauffage individuel** | 6,21 ms | 5,23 ms | 0,10 ms | 81,6 ms | 12,6 ms | 19,8 ms |
420
- | **Appartement · chauffage collectif (2025)** | 5,76 ms | 4,99 ms | 1,45 ms | 35,9 ms | 12,5 ms | 19,0 ms |
421
- | **Immeuble · chauffage collectif** | 5,22 ms | 4,34 ms | 0,12 ms | 119,3 ms | 9,61 ms | 17,5 ms |
422
- | **Immeuble · chauffage mixte** | 5,11 ms | 4,13 ms | 0,18 ms | 823,0 ms | 10,5 ms | 16,6 ms |
423
- | **Individuel généré depuis l'immeuble (2026)** | 5,63 ms | 4,86 ms | 1,65 ms | 44,3 ms | 10,6 ms | 15,8 ms |
415
+ | **Généraliste** | 4,90 ms | 4,46 ms | 0,22 ms | 38,9 ms | 8,82 ms | 12,0 ms |
416
+ | **Appartement · chauffage individuel (2025)** | 3,82 ms | 3,51 ms | 1,34 ms | 30,2 ms | 6,52 ms | 8,52 ms |
417
+ | **Logement individuel (2025)** | 5,25 ms | 4,67 ms | 1,66 ms | 38,9 ms | 9,78 ms | 13,2 ms |
418
+ | **Maison individuelle (2025)** | 6,36 ms | 5,77 ms | 2,05 ms | 50,5 ms | 11,3 ms | 14,5 ms |
419
+ | **Immeuble · chauffage individuel** | 6,23 ms | 5,25 ms | 0,12 ms | 122,1 ms | 12,5 ms | 20,5 ms |
420
+ | **Appartement · chauffage collectif (2025)** | 4,33 ms | 3,99 ms | 1,46 ms | 35,9 ms | 7,01 ms | 9,22 ms |
421
+ | **Immeuble · chauffage collectif** | 5,18 ms | 4,35 ms | 0,12 ms | 129,8 ms | 9,51 ms | 17,0 ms |
422
+ | **Immeuble · chauffage mixte** | 5,66 ms | 4,59 ms | 0,22 ms | 933,6 ms | 11,8 ms | 18,9 ms |
423
+ | **Individuel généré depuis l'immeuble (2026)** | 6,18 ms | 5,35 ms | 1,62 ms | 44,3 ms | 11,6 ms | 17,6 ms |
424
424
 
425
425
  <sub>La moyenne est tirée vers le haut par les DPE collectifs, dont le coût atteint plusieurs
426
426
  dizaines de fois la médiane : c’est la médiane qui décrit le cas courant, et p95/p99 le cas
package/engine.js CHANGED
@@ -33,7 +33,7 @@ import getFicheTechnique from './ficheTechnique.js';
33
33
  import { ProductionENR } from './16.2_production_enr.js';
34
34
  import DpeSanitizerService from './dpe-sanitizer.service.js';
35
35
 
36
- const LIB_VERSION = '1.8.23';
36
+ const LIB_VERSION = '1.8.24';
37
37
 
38
38
  const dpeSanitizerService = new DpeSanitizerService();
39
39
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open3cl/engine",
3
- "version": "1.8.23",
3
+ "version": "1.8.24",
4
4
  "description": "Open Source 3CL-DPE engine",
5
5
  "main": "index.js",
6
6
  "directories": {