@open3cl/engine 1.8.54 → 1.8.56

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_conso_ch.js CHANGED
@@ -277,10 +277,15 @@ function calc_ch_base_appoint(
277
277
  const idx_ilpa = ilpa ? 1 : 0;
278
278
 
279
279
  const dh14Saison = mois_liste.reduce((acc, mois) => acc + dh14[idx_ilpa][ca][mois][zc], 0);
280
- const rdim = de.rdim || 1;
281
- const ratio_s = (s_chauffee_inst * rdim) / Sh;
282
- const bch1 = (bch * ratio_s) / rdim;
283
- const t = 14 - (pe * dh14Saison) / bch1;
280
+ /**
281
+ * Part de l'immeuble desservie par l'installation : S_ratio / Rdim = Sh_chauffée / Stotal
282
+ * (Tribu Calcul_installation.cs l.79-86 : bch et Bch1_mensuel sont proratisés tous les deux).
283
+ * `bch` est déjà proratisé par calc_chauffage ; le besoin mensuel, calculé pour tout le
284
+ * bâtiment, doit l'être avec le même ratio, sinon la part de base de chaque installation est
285
+ * calculée sur le besoin de l'immeuble entier.
286
+ */
287
+ const ratioSurface = s_chauffee_inst > 0 && Sh > 0 ? s_chauffee_inst / Sh : 1;
288
+ const t = 14 - (pe * dh14Saison) / bch;
284
289
 
285
290
  di.conso_ch = 0;
286
291
  for (const mois of mois_liste) {
@@ -310,10 +315,10 @@ function calc_ch_base_appoint(
310
315
  if (ratioDh <= 0) {
311
316
  bch_base_j = 0;
312
317
  } else {
313
- bch_base_j = besoin_ch_mois[mois] * ratioDh;
318
+ bch_base_j = besoin_ch_mois[mois] * ratioSurface * ratioDh;
314
319
  }
315
320
  } else {
316
- bch_base_j = besoin_ch_mois[mois] * 0.5;
321
+ bch_base_j = besoin_ch_mois[mois] * ratioSurface * 0.5;
317
322
  }
318
323
  bch_base += bch_base_j;
319
324
  }
@@ -462,6 +462,90 @@ describe('conso_ch - installation collective base + appoint', () => {
462
462
  expect(di.conso_ch).toBeCloseTo(0, 9);
463
463
  });
464
464
 
465
+ /**
466
+ * Immeuble à plusieurs installations base + appoint : le besoin mensuel (calculé pour tout le
467
+ * bâtiment) est proratisé à la surface chauffée par l'installation, comme `bch`.
468
+ * Tribu Calcul_installation.cs l.79-86 : bch = Bch1 × S_ratio / Rdim et
469
+ * Bch1_mensuel × S_ratio / Rdim, avec S_ratio = Sh_chauffée × Rdim / Stotal.
470
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §9.8
471
+ */
472
+ describe('installation desservant une partie de l’immeuble (Sh_installation < Sh)', () => {
473
+ function appelPartiel(pos, { pn = 0, Sh = 200, sInst = 100, bch = 25, rdim = 1 } = {}) {
474
+ const args = baseAppointArgs(pos, { pn });
475
+ const [di, de, _pos, cfg, em_list, GV] = args;
476
+ de.rdim = rdim;
477
+ const besoinCouvert = conso_ch(
478
+ di,
479
+ de,
480
+ {},
481
+ _pos,
482
+ cfg,
483
+ em_list,
484
+ GV,
485
+ Sh,
486
+ 2.5,
487
+ bch,
488
+ bch,
489
+ -9.5,
490
+ 0,
491
+ 1,
492
+ 1,
493
+ { Janvier: 3000, Février: 2000 },
494
+ sInst,
495
+ args[16]
496
+ );
497
+ return { di, besoinCouvert };
498
+ }
499
+
500
+ test('pe = 0 : base = moitié du besoin mensuel de la seule part de l’installation', () => {
501
+ // Σ besoin mensuel immeuble = 5 kWh ; part de l'installation = 100 / 200 => base = 1,25 kWh
502
+ const { di, besoinCouvert } = appelPartiel(0);
503
+ expect(besoinCouvert).toBeCloseTo(1.25, 9);
504
+ // conso = base × Int (1) / (Rg 0,9 × Rendement émission 0,9)
505
+ expect(di.conso_ch).toBeCloseTo(1.25 / 0.81, 9);
506
+ });
507
+
508
+ test('pe = 0 : appoint = besoin de l’installation moins la base proratisée', () => {
509
+ const { besoinCouvert } = appelPartiel(1);
510
+ expect(besoinCouvert).toBeCloseTo(25 - 1.25, 9);
511
+ });
512
+
513
+ test('base + appoint = besoin de l’installation, quelle que soit la part de surface', () => {
514
+ for (const sInst of [20, 100, 180]) {
515
+ const bch = (50 * sInst) / 200;
516
+ const base = appelPartiel(0, { sInst, bch }).besoinCouvert;
517
+ const appoint = appelPartiel(1, { sInst, bch }).besoinCouvert;
518
+ expect(base + appoint).toBeCloseTo(bch, 9);
519
+ expect(base).toBeLessThanOrEqual(bch);
520
+ }
521
+ });
522
+
523
+ test('le rdim de l’installation ne modifie pas la part de base (S_ratio / Rdim)', () => {
524
+ const sansRdim = appelPartiel(0, { pn: 5000 }).besoinCouvert;
525
+ const avecRdim = appelPartiel(0, { pn: 5000, rdim: 7 }).besoinCouvert;
526
+ expect(avecRdim).toBeCloseTo(sansRdim, 9);
527
+ });
528
+
529
+ test('installation couvrant tout l’immeuble : comportement inchangé', () => {
530
+ // Sh = surface de l'installation => même résultat que le cas nominal
531
+ const { di } = appelPartiel(0, { Sh: 100, sInst: 100, bch: 50 });
532
+ expect(di.conso_ch).toBeCloseTo(3.0864197530864197, 9);
533
+ });
534
+
535
+ test('surface d’installation absente ou nulle : pas de proratisation du besoin mensuel', () => {
536
+ // Repli sur le besoin mensuel entier (ratio 1) : base = moitié de Σ besoin mensuel = 2,5 kWh
537
+ for (const sInst of [0, -1]) {
538
+ expect(appelPartiel(0, { sInst }).besoinCouvert).toBeCloseTo(2.5, 9);
539
+ }
540
+ });
541
+
542
+ test('surface habitable de l’immeuble nulle : pas de proratisation (aucune division par 0)', () => {
543
+ const { besoinCouvert } = appelPartiel(0, { Sh: 0 });
544
+ expect(Number.isFinite(besoinCouvert)).toBe(true);
545
+ expect(besoinCouvert).toBeCloseTo(2.5, 9);
546
+ });
547
+ });
548
+
465
549
  test('logement collectif ILPA + valeurs par défaut (rdim, rendement de génération)', () => {
466
550
  const di = { rg: 1, rg_dep: 1 };
467
551
  // pas de rdim => rdim = 1 ; pas de rendement_generation => Ich = 1
package/README.md CHANGED
@@ -370,57 +370,57 @@ 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.53`** · branche `main` · généré le 2026-10-12
373
+ > **Version `1.8.55`** · branche `main` · généré le 2026-10-12
374
374
  > Seuil de tolérance **5%**
375
375
 
376
376
  <table>
377
377
  <tr>
378
378
  <td align="center"><strong>9</strong><br/><sub>corpus</sub></td>
379
- <td align="center"><strong>89 984</strong><br/><sub>DPE analysés</sub></td>
380
- <td align="center"><strong>65 424</strong><br/><sub>DPE conformes</sub></td>
381
- <td align="center"><strong>72,71 %</strong><br/><sub>réussite globale</sub></td>
379
+ <td align="center"><strong>89 853</strong><br/><sub>DPE analysés</sub></td>
380
+ <td align="center"><strong>66 634</strong><br/><sub>DPE conformes</sub></td>
381
+ <td align="center"><strong>74,16 %</strong><br/><sub>réussite globale</sub></td>
382
382
  </tr>
383
383
  </table>
384
384
 
385
385
  | | Corpus | Réussite | | DPE conformes |
386
386
  | :-: | :---------------------------------------------------------------------------------------------------------------------- | ----------: | :--------------------- | -------------: |
387
- | 🔴 | **Généraliste**<br/><sub>`corpus_dpe.csv`</sub> | **46,65 %** | `█████████░░░░░░░░░░░` | 4 665 / 9 999 |
387
+ | 🔴 | **Généraliste**<br/><sub>`corpus_dpe.csv`</sub> | **47,08 %** | `█████████░░░░░░░░░░░` | 4 708 / 9 999 |
388
388
  | 🟢 | **Appartement · chauffage individuel (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_individuel_2025.csv`</sub> | **92,90 %** | `███████████████████░` | 9 290 / 10 000 |
389
- | 🟢 | **Appartement · chauffage collectif (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_collectif_2025.csv`</sub> | **91,27 %** | `██████████████████░░` | 9 127 / 10 000 |
390
- | 🟢 | **Logement individuel (2025)**<br/><sub>`dpe_logement_individuel_2025.csv`</sub> | **89,93 %** | `██████████████████░░` | 8 986 / 9 992 |
391
- | 🟢 | **Maison individuelle (2025)**<br/><sub>`dpe_maison_individuelle_2025.csv`</sub> | **88,88 %** | `██████████████████░░` | 8 887 / 9 999 |
392
- | 🟡 | **Immeuble · chauffage individuel**<br/><sub>`dpe_immeuble_chauffage_individuel.csv`</sub> | **84,57 %** | `█████████████████░░░` | 8 455 / 9 998 |
393
- | 🟡 | **Immeuble · chauffage collectif**<br/><sub>`dpe_immeuble_chauffage_collectif.csv`</sub> | **74,67 %** | `███████████████░░░░░` | 7 466 / 9 999 |
394
- | 🔴 | **Immeuble · chauffage mixte**<br/><sub>`dpe_immeuble_chauffage_mixte.csv`</sub> | **56,65 %** | `███████████░░░░░░░░░` | 5 663 / 9 997 |
395
- | 🔴 | **Individuel généré depuis l'immeuble (2026)**<br/><sub>`dpe_individuel_a_partir_dpe_immeuble_2026.csv`</sub> | **28,85 %** | `██████░░░░░░░░░░░░░░` | 2 885 / 10 000 |
389
+ | 🟢 | **Appartement · chauffage collectif (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_collectif_2025.csv`</sub> | **91,72 %** | `██████████████████░░` | 9 172 / 10 000 |
390
+ | 🟢 | **Logement individuel (2025)**<br/><sub>`dpe_logement_individuel_2025.csv`</sub> | **90,47 %** | `██████████████████░░` | 9 040 / 9 992 |
391
+ | 🟢 | **Maison individuelle (2025)**<br/><sub>`dpe_maison_individuelle_2025.csv`</sub> | **88,90 %** | `██████████████████░░` | 8 889 / 9 999 |
392
+ | 🟡 | **Immeuble · chauffage individuel**<br/><sub>`dpe_immeuble_chauffage_individuel.csv`</sub> | **84,58 %** | `█████████████████░░░` | 8 456 / 9 998 |
393
+ | 🟡 | **Immeuble · chauffage collectif**<br/><sub>`dpe_immeuble_chauffage_collectif.csv`</sub> | **84,58 %** | `█████████████████░░░` | 8 346 / 9 868 |
394
+ | 🔴 | **Immeuble · chauffage mixte**<br/><sub>`dpe_immeuble_chauffage_mixte.csv`</sub> | **56,86 %** | `███████████░░░░░░░░░` | 5 684 / 9 997 |
395
+ | 🔴 | **Individuel généré depuis l'immeuble (2026)**<br/><sub>`dpe_individuel_a_partir_dpe_immeuble_2026.csv`</sub> | **30,49 %** | `██████░░░░░░░░░░░░░░` | 3 049 / 10 000 |
396
396
 
397
397
  <sub>🟢 ≥ 85 % · 🟡 ≥ 60 % · 🔴 < 60 %</sub>
398
398
 
399
399
  #### Temps d’exécution
400
400
 
401
- > Durée de l’appel à `calcul_3cl` par DPE, sur 89 950 calculs.
401
+ > Durée de l’appel à `calcul_3cl` par DPE, sur 89 819 calculs.
402
402
  > La copie défensive de l’entrée et la lecture du fichier sont exclues de la mesure.
403
403
 
404
404
  <table>
405
405
  <tr>
406
- <td align="center"><strong>5,49 ms</strong><br/><sub>moyenne</sub></td>
407
- <td align="center"><strong>4,84 ms</strong><br/><sub>médiane</sub></td>
406
+ <td align="center"><strong>5,46 ms</strong><br/><sub>moyenne</sub></td>
407
+ <td align="center"><strong>4,77 ms</strong><br/><sub>médiane</sub></td>
408
408
  <td align="center"><strong>0,13 ms</strong><br/><sub>min</sub></td>
409
- <td align="center"><strong>611,3 ms</strong><br/><sub>max</sub></td>
409
+ <td align="center"><strong>656,0 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** | 4,94 ms | 4,49 ms | 0,22 ms | 38,8 ms | 9,08 ms | 12,2 ms |
416
- | **Appartement · chauffage individuel (2025)** | 4,19 ms | 3,87 ms | 1,55 ms | 32,4 ms | 7,06 ms | 8,85 ms |
417
- | **Appartement · chauffage collectif (2025)** | 4,43 ms | 4,07 ms | 1,56 ms | 36,2 ms | 7,16 ms | 8,84 ms |
418
- | **Logement individuel (2025)** | 5,31 ms | 4,77 ms | 1,70 ms | 32,9 ms | 9,86 ms | 13,0 ms |
419
- | **Maison individuelle (2025)** | 6,18 ms | 5,61 ms | 2,02 ms | 52,6 ms | 11,0 ms | 14,0 ms |
420
- | **Immeuble · chauffage individuel** | 6,90 ms | 5,91 ms | 0,13 ms | 99,0 ms | 13,9 ms | 22,2 ms |
421
- | **Immeuble · chauffage collectif** | 6,10 ms | 5,31 ms | 0,15 ms | 135,3 ms | 11,3 ms | 20,3 ms |
422
- | **Immeuble · chauffage mixte** | 5,28 ms | 4,33 ms | 0,18 ms | 611,3 ms | 10,9 ms | 18,4 ms |
423
- | **Individuel généré depuis l'immeuble (2026)** | 6,06 ms | 5,16 ms | 1,81 ms | 47,3 ms | 11,3 ms | 18,4 ms |
415
+ | **Généraliste** | 4,85 ms | 4,39 ms | 0,21 ms | 35,8 ms | 8,80 ms | 12,2 ms |
416
+ | **Appartement · chauffage individuel (2025)** | 4,07 ms | 3,71 ms | 1,47 ms | 33,7 ms | 7,08 ms | 8,93 ms |
417
+ | **Appartement · chauffage collectif (2025)** | 4,41 ms | 4,02 ms | 1,53 ms | 35,1 ms | 7,23 ms | 9,09 ms |
418
+ | **Logement individuel (2025)** | 5,29 ms | 4,69 ms | 1,73 ms | 45,8 ms | 9,87 ms | 13,7 ms |
419
+ | **Maison individuelle (2025)** | 6,25 ms | 5,66 ms | 1,90 ms | 52,3 ms | 11,1 ms | 14,7 ms |
420
+ | **Immeuble · chauffage individuel** | 6,46 ms | 5,54 ms | 0,13 ms | 105,5 ms | 12,9 ms | 20,2 ms |
421
+ | **Immeuble · chauffage collectif** | 5,76 ms | 4,91 ms | 0,19 ms | 116,4 ms | 10,6 ms | 16,8 ms |
422
+ | **Immeuble · chauffage mixte** | 5,76 ms | 4,70 ms | 0,21 ms | 656,0 ms | 12,1 ms | 19,6 ms |
423
+ | **Individuel généré depuis l'immeuble (2026)** | 6,29 ms | 5,31 ms | 1,74 ms | 59,8 ms | 11,9 ms | 20,1 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/conso.js CHANGED
@@ -410,6 +410,25 @@ export function getContenuCo2ReseauChaleur(
410
410
  return COEF_GES_AUTRES_RESEAUX_CHALEUR;
411
411
  }
412
412
 
413
+ /**
414
+ * Générateurs « chaudière(s) <énergie> multi bâtiment modélisée(s) comme un réseau de chaleur »
415
+ * (enum_type_generateur_ch_id 109-111, enum_type_generateur_ecs_id 74-76) : énergie réellement
416
+ * consommée par les chaudières, dont le facteur d'émission s'applique à la place du facteur
417
+ * « autres réseaux de chaleur ».
418
+ *
419
+ * @see Methode_de_calcul_3CL_DPE_2021-338.pdf - §17.3 p.118 : « l'installation de chauffage est
420
+ * traitée comme un réseau de chaleur local. Les émissions de gaz à effet de serre seront calculées
421
+ * à partir des énergies consommées réellement par les générateurs. »
422
+ * Tribu (Calcul_batiment.cs l.1189) applique `contenu_CO2_RCU_ch`, transmis par le logiciel : LICIEL
423
+ * et Pleiades y placent le facteur de l'énergie des chaudières (« Taux CO2 fonction du système réel »).
424
+ * Le bois retient 0,03 (bûches / granulés), valeur exportée par la majorité des DPE LICIEL.
425
+ * Les PAC multi-bâtiment (112 / 77) ne sont pas couvertes : facteurs exportés non reproductibles.
426
+ */
427
+ const ENERGIE_MULTI_BATIMENT = {
428
+ ch: { 109: 'bois – bûches', 110: 'fioul domestique', 111: 'gaz naturel' },
429
+ ecs: { 74: 'bois – bûches', 75: 'fioul domestique', 76: 'gaz naturel' }
430
+ };
431
+
413
432
  /**
414
433
  * Récupération du coefficient d'émission GES pour le générateur
415
434
  * @param gen_ch {GenerateurChauffageItem||GenerateurEcsItem}
@@ -435,6 +454,10 @@ function getGesCoeffForGenerateur(gen_ch, suffix, dateDpe, versionLogiciel) {
435
454
  versionLogiciel
436
455
  );
437
456
  }
457
+ // Chaudière(s) multi bâtiment sans identifiant : facteur de l'énergie des chaudières (§17.3)
458
+ const typeGenerateur = gen_ch.donnee_entree[`enum_type_generateur_${suffix}_id`];
459
+ const energieMultiBatiment = ENERGIE_MULTI_BATIMENT[suffix][typeGenerateur];
460
+ if (energieMultiBatiment) return coef_ges[energieMultiBatiment];
438
461
  // Prendre la valeur pour "AUTRES RESEAUX DE CHALEUR"
439
462
  return COEF_GES_AUTRES_RESEAUX_CHALEUR;
440
463
  }
package/conso.spec.js CHANGED
@@ -1416,4 +1416,66 @@ describe('réseaux de chaleur - sélection de l’arrêté et du contenu CO2', (
1416
1416
  const autreRes = calc_conso(...args);
1417
1417
  expect(autreRes.emission_ges.emission_ges_ch).toBeCloseTo(24, 9);
1418
1418
  });
1419
+
1420
+ /**
1421
+ * @see Methode_de_calcul_3CL_DPE_2021-338.pdf - §17.3 p.118 : GES calculées à partir des énergies
1422
+ * consommées réellement par les générateurs.
1423
+ */
1424
+ const calcMultiBatiment = (typeCh, typeEcs, deExtra = {}) => {
1425
+ const ch = [
1426
+ installCh({ cle_repartition_ch: 1 }, [
1427
+ genCh(
1428
+ '3',
1429
+ { conso_ch: 1000, conso_ch_depensier: 1200 },
1430
+ { enum_type_generateur_ch_id: typeCh, identifiant_reseau_chaleur: '', ...deExtra }
1431
+ )
1432
+ ])
1433
+ ];
1434
+ const ecs = [
1435
+ installEcs({ cle_repartition_ecs: 1 }, [
1436
+ genEcs(
1437
+ '3',
1438
+ { conso_ecs: 500, conso_ecs_depensier: 600 },
1439
+ { enum_type_generateur_ecs_id: typeEcs, identifiant_reseau_chaleur: '', ...deExtra }
1440
+ )
1441
+ ])
1442
+ ];
1443
+ return calc_conso(100, 1, 1, [], ch, ecs, [], 1, 1, '2025-01-22', coef_ep, 1, 1, {}, 'LICIEL')
1444
+ .emission_ges;
1445
+ };
1446
+
1447
+ test.each([
1448
+ // DPE 2373E0201684Z (109/74) : facteur ADEME 0,03 (0,024 sur quelques exports plaquettes)
1449
+ ['109', '74', 0.03],
1450
+ ['110', '75', 0.324],
1451
+ // DPE 2467E0759983V-like (111/76) : 814 DPE LICIEL KO GES-seul à 0,227
1452
+ ['111', '76', 0.227]
1453
+ ])(
1454
+ 'chaudière(s) multi bâtiment %s / %s sans identifiant : facteur de l’énergie %f',
1455
+ (typeCh, typeEcs, facteur) => {
1456
+ const ges = calcMultiBatiment(typeCh, typeEcs);
1457
+ expect(ges.emission_ges_ch).toBeCloseTo(1000 * facteur, 9);
1458
+ expect(ges.emission_ges_ecs).toBeCloseTo(500 * facteur, 9);
1459
+ expect(tv).not.toHaveBeenCalled();
1460
+ }
1461
+ );
1462
+
1463
+ test('PAC multi bâtiment (112 / 77) ou réseau sans identifiant : facteur « autres réseaux » 0,385', () => {
1464
+ const ges = calcMultiBatiment('112', '77');
1465
+ expect(ges.emission_ges_ch).toBeCloseTo(385, 9);
1466
+ expect(ges.emission_ges_ecs).toBeCloseTo(192.5, 9);
1467
+ // Réseau de chaleur classique (ch 71 / ecs 72) sans identifiant
1468
+ const reseau = calcMultiBatiment('71', '72');
1469
+ expect(reseau.emission_ges_ch).toBeCloseTo(385, 9);
1470
+ });
1471
+
1472
+ test('chaudière multi bâtiment avec identifiant réseau : contenu CO2 de l’arrêté prioritaire', () => {
1473
+ mockTables({ R: { reseau_chaleur_2023: { contenu_co2: '0.05', contenu_co2_acv: '0.06' } } });
1474
+ const ges = calcMultiBatiment('111', '76', {
1475
+ identifiant_reseau_chaleur: 'R',
1476
+ date_arrete_reseau_chaleur: '2024-07-05'
1477
+ });
1478
+ expect(ges.emission_ges_ch).toBeCloseTo(60, 9);
1479
+ expect(ges.emission_ges_ecs).toBeCloseTo(30, 9);
1480
+ });
1419
1481
  });
package/engine.js CHANGED
@@ -33,7 +33,7 @@ import getFicheTechnique from './ficheTechnique.js';
33
33
  import { ProductionENR, ratioProrataPvLogement } from './16.2_production_enr.js';
34
34
  import DpeSanitizerService from './dpe-sanitizer.service.js';
35
35
 
36
- const LIB_VERSION = '1.8.54';
36
+ const LIB_VERSION = '1.8.56';
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.54",
3
+ "version": "1.8.56",
4
4
  "description": "Open Source 3CL-DPE engine",
5
5
  "main": "index.js",
6
6
  "directories": {