@open3cl/engine 1.8.34 → 1.8.36

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/15_conso_aux.js CHANGED
@@ -57,9 +57,20 @@ export function conso_aux_gen(di, de, type, besoin, besoin_dep) {
57
57
  */
58
58
  const Paux_g_ch = g + h * (pe / 1000);
59
59
 
60
- // Pour les installations collectives, la conso des auxiliaires est calculée à partir du besoin de l'appartement
61
- let besoinAppart = besoin * (de[`cle_repartition_${type}`] || 1);
62
- let besoinAppartDep = besoin_dep * (de[`cle_repartition_${type}`] || 1);
60
+ /**
61
+ * Pour les installations collectives, la conso des auxiliaires est calculée à partir du besoin
62
+ * de l'appartement (clé de répartition).
63
+ * Pour une installation de chauffage individuelle (enum_type_installation_id = 1), la clé
64
+ * n'est pas appliquée : le besoin reçu est déjà proratisé par Sc / Sh dans calc_chauffage, et
65
+ * le moteur de référence CSTB (Tribu) n'utilise aucune clé de répartition pour ces
66
+ * installations (DPE.Core : Calcul_installation.cs l.79-80, Calcul_generateur.cs l.1497,
67
+ * Calcul_batiment.cs l.652). La clé reste appliquée au collectif et à l'ECS.
68
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §15.1
69
+ */
70
+ const ignoreCle = type === 'ch' && String(de.enum_type_installation_id) === '1';
71
+ const cle = ignoreCle ? 1 : de[`cle_repartition_${type}`] || 1;
72
+ let besoinAppart = besoin * cle;
73
+ let besoinAppartDep = besoin_dep * cle;
63
74
 
64
75
  /**
65
76
  * Le besoin reçu est déjà celui de l'installation : calc_chauffage le proratise par
@@ -229,6 +229,70 @@ describe('conso_aux_gen - auxiliaires de génération', () => {
229
229
  10
230
230
  );
231
231
  });
232
+
233
+ /**
234
+ * Issue #252 : la clé de répartition du chauffage n'est pas appliquée au besoin d'une
235
+ * installation individuelle (enum_type_installation_id = 1) : ce besoin est déjà proratisé
236
+ * par Sc / Sh, et le moteur CSTB (Tribu) n'utilise aucune clé pour ces installations.
237
+ * La clé reste appliquée au collectif et à l'ECS.
238
+ * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §15.1
239
+ */
240
+ describe('clé de répartition (issue #252)', () => {
241
+ test('chauffage, installation individuelle : la clé de répartition est ignorée', () => {
242
+ const de = {
243
+ enum_type_generateur_ch_id: '90',
244
+ enum_type_installation_id: '1',
245
+ cle_repartition_ch: 0.5
246
+ };
247
+ conso_aux_gen(di, de, 'ch', 1000, 1200, 100);
248
+ // Paux = 20 + 1,6 × 20 = 52 W ; conso = 52 × 1000 / 20000 = 2,6 (sans facteur 0,5)
249
+ expect(di.conso_auxiliaire_generation_ch).toBeCloseTo(2.6, 10);
250
+ expect(di.conso_auxiliaire_generation_ch_depensier).toBeCloseTo(3.12, 10);
251
+ });
252
+
253
+ test('chauffage, installation individuelle (identifiant numérique) : clé ignorée', () => {
254
+ const de = {
255
+ enum_type_generateur_ch_id: '90',
256
+ enum_type_installation_id: 1,
257
+ cle_repartition_ch: 0.5
258
+ };
259
+ conso_aux_gen(di, de, 'ch', 1000, 1200, 100);
260
+ expect(di.conso_auxiliaire_generation_ch).toBeCloseTo(2.6, 10);
261
+ expect(di.conso_auxiliaire_generation_ch_depensier).toBeCloseTo(3.12, 10);
262
+ });
263
+
264
+ test('chauffage, installation collective : la clé de répartition est appliquée', () => {
265
+ const de = {
266
+ enum_type_generateur_ch_id: '90',
267
+ enum_type_installation_id: '2',
268
+ cle_repartition_ch: 0.5
269
+ };
270
+ conso_aux_gen(di, de, 'ch', 1000, 1200, 100);
271
+ // conso = 52 × (1000 × 0,5) / 20000 = 1,3
272
+ expect(di.conso_auxiliaire_generation_ch).toBeCloseTo(1.3, 10);
273
+ expect(di.conso_auxiliaire_generation_ch_depensier).toBeCloseTo(1.56, 10);
274
+ });
275
+
276
+ test("chauffage, type d'installation non renseigné : la clé de répartition est appliquée", () => {
277
+ const de = { enum_type_generateur_ch_id: '90', cle_repartition_ch: 0.5 };
278
+ conso_aux_gen(di, de, 'ch', 1000, 1200, 100);
279
+ expect(di.conso_auxiliaire_generation_ch).toBeCloseTo(1.3, 10);
280
+ expect(di.conso_auxiliaire_generation_ch_depensier).toBeCloseTo(1.56, 10);
281
+ });
282
+
283
+ test('ECS, installation individuelle : la clé de répartition ECS reste appliquée', () => {
284
+ const de = {
285
+ enum_type_generateur_ecs_id: '50',
286
+ enum_type_installation_id: '1',
287
+ cle_repartition_ecs: 0.5,
288
+ cle_repartition_ch: 0.25
289
+ };
290
+ conso_aux_gen(di, de, 'ecs', 800, 1000, 100);
291
+ // Sans clé : 2,08 / 2,6 ; avec la clé ECS 0,5 : 1,04 / 1,3
292
+ expect(di.conso_auxiliaire_generation_ecs).toBeCloseTo(1.04, 10);
293
+ expect(di.conso_auxiliaire_generation_ecs_depensier).toBeCloseTo(1.3, 10);
294
+ });
295
+ });
232
296
  });
233
297
 
234
298
  /**
@@ -3,6 +3,38 @@ import { mois_liste, tv } from './utils.js';
3
3
  import tvs from './tv.js';
4
4
  import { DEFAULT_COEFF_EP } from './conso.js';
5
5
 
6
+ /**
7
+ * Méthodes d'application « appartement chauffage individuel ECS individuel » : l'installation PV
8
+ * déclarée est celle du logement, aucune proratisation (exports ADEME non proratisés, cf. 2528E1249844E).
9
+ */
10
+ const MAP_APPARTEMENT_INDIVIDUEL = ['2', '22', '25'];
11
+
12
+ /**
13
+ * Coefficient de proratisation au logement de la production d'une installation PV d'immeuble.
14
+ *
15
+ * Le moteur de référence (Tribu, `Calcul_batiment.cs` l.1142-1156) calcule le taux de couverture
16
+ * Tcv = Ppv / Celec_tot avec la surface du logement : pour un appartement desservi par une
17
+ * installation d'immeuble, la surface de capteurs doit être ramenée au logement
18
+ * (S_capteur × Sh_logement / Sh_immeuble), comme pour les générateurs collectifs (l.253, l.303).
19
+ *
20
+ * @param th {string} type de bâtiment ('maison', 'appartement', 'immeuble')
21
+ * @param map_id {string} enum_methode_application_dpe_log_id
22
+ * @param surfaceLogement {number|string} surface_habitable_logement
23
+ * @param surfaceImmeuble {number|string} surface_habitable_immeuble
24
+ * @returns {number} 1 si aucune proratisation, sinon Sh_logement / Sh_immeuble
25
+ */
26
+ export function ratioProrataPvLogement(th, map_id, surfaceLogement, surfaceImmeuble) {
27
+ const shl = Number(surfaceLogement);
28
+ const shi = Number(surfaceImmeuble);
29
+ if (th !== 'appartement' || MAP_APPARTEMENT_INDIVIDUEL.includes(String(map_id))) {
30
+ return 1;
31
+ }
32
+ if (!(shl > 0) || !(shi > shl)) {
33
+ return 1;
34
+ }
35
+ return shl / shi;
36
+ }
37
+
6
38
  export class ProductionENR {
7
39
  /**
8
40
  * Taux d'autoproduction limite par poste Taplpi (§16.2).
@@ -29,8 +61,9 @@ export class ProductionENR {
29
61
  * @param th {string}
30
62
  * @param zc_id {string}
31
63
  * @param coeff_ep_override {number?}
64
+ * @param ratioPv {number?} proratisation au logement d'une installation d'immeuble (cf. ratioProrataPvLogement)
32
65
  */
33
- calculateEnr(productionElecEnr, conso, Sh, th, zc_id, coeff_ep_override) {
66
+ calculateEnr(productionElecEnr, conso, Sh, th, zc_id, coeff_ep_override, ratioPv = 1) {
34
67
  const productionElectricite = {
35
68
  conso_elec_ac: 0,
36
69
  production_pv: 0,
@@ -49,7 +82,15 @@ export class ProductionENR {
49
82
 
50
83
  if (productionElecEnr && productionElecEnr.donnee_entree?.presence_production_pv === 1) {
51
84
  // Calcul de l'électricité auto-consommée pour chaque enveloppe
52
- this.calculateConsoElecAc(productionElectricite, productionElecEnr, conso, zc_id, th, Sh);
85
+ this.calculateConsoElecAc(
86
+ productionElectricite,
87
+ productionElecEnr,
88
+ conso,
89
+ zc_id,
90
+ th,
91
+ Sh,
92
+ ratioPv
93
+ );
53
94
 
54
95
  // Mise à jour des consommations d'énergie finale en minorant l'énergie consommée par l'énergie autoconsommée par le poste
55
96
  this.updateEfConso(productionElectricite, conso, Sh);
@@ -78,10 +119,19 @@ export class ProductionENR {
78
119
  * @param zc_id
79
120
  * @param th
80
121
  * @param Sh
122
+ * @param ratioPv {number?}
81
123
  */
82
- calculateConsoElecAc(productionElectricite, productionElecEnr, conso, zc_id, th, Sh) {
124
+ calculateConsoElecAc(
125
+ productionElectricite,
126
+ productionElecEnr,
127
+ conso,
128
+ zc_id,
129
+ th,
130
+ Sh,
131
+ ratioPv = 1
132
+ ) {
83
133
  // Production d’électricité par des capteurs photovoltaïques Ppv (en kWh/m²)
84
- const Ppv = this.getPpv(productionElecEnr, zc_id);
134
+ const Ppv = this.getPpv(productionElecEnr, zc_id, ratioPv);
85
135
 
86
136
  // Consommation annuelle d’électricité pour les autres usages (kWhef/an)
87
137
  const CelecTotAu = this.getCelecAu(th, Sh);
@@ -342,9 +392,11 @@ export class ProductionENR {
342
392
  *
343
393
  * @param productionElecEnr
344
394
  * @param zc_id
395
+ * @param ratioPv {number?} proratisation au logement (1 = aucune). Non appliquée aux installations
396
+ * qui exportent un ratio_virtualisation (déjà virtualisées par le logiciel).
345
397
  * @returns {number}
346
398
  */
347
- getPpv(productionElecEnr, zc_id) {
399
+ getPpv(productionElecEnr, zc_id, ratioPv = 1) {
348
400
  const ePvValues = tvs.e_pv;
349
401
  const zc = enums.zone_climatique[zc_id];
350
402
 
@@ -370,8 +422,23 @@ export class ProductionENR {
370
422
  */
371
423
  const k = row.coef_orientation_pv;
372
424
 
373
- // Surface des panneaux photovoltaïques orientés et inclinés de la même manière (m²)
374
- const Scapteur = 1.6 * panneaux_pv.nombre_module || panneaux_pv.surface_totale_capteurs;
425
+ /**
426
+ * Surface des panneaux photovoltaïques orientés et inclinés de la même manière (m²).
427
+ * Le moteur de référence n'a que S_capteur en entrée (Tribu `photovoltaique.cs` l.8) : la
428
+ * surface saisie est prioritaire, le forfait 1,6 m²/module ne sert qu'à défaut de surface.
429
+ */
430
+ let Scapteur = panneaux_pv.surface_totale_capteurs || 1.6 * panneaux_pv.nombre_module;
431
+
432
+ /**
433
+ * Installation d'immeuble : surface ramenée au logement (Sh_logement / Sh_immeuble).
434
+ * Si l'installation exporte un ratio_virtualisation, le logiciel de saisie a déjà virtualisé
435
+ * l'installation au logement et la production ADEME n'est pas proratisée (corpus : 36 exports
436
+ * LICIEL, autoconsommation ADEME reproduite sans proratisation, jamais avec ; ex. 2501E3381654V,
437
+ * 2369E0282894H, 2469E2989760O) : pas de proratisation supplémentaire.
438
+ */
439
+ if (ratioPv !== 1 && panneaux_pv.ratio_virtualisation == null) {
440
+ Scapteur *= ratioPv;
441
+ }
375
442
 
376
443
  // Rendement moyen des modules
377
444
  const r = 0.17;
@@ -1,4 +1,4 @@
1
- import { ProductionENR } from './16.2_production_enr.js';
1
+ import { ProductionENR, ratioProrataPvLogement } from './16.2_production_enr.js';
2
2
  import { describe, expect, test, it } from 'vitest';
3
3
 
4
4
  describe('production ENR unit tests', () => {
@@ -47,8 +47,12 @@ describe('production ENR unit tests', () => {
47
47
  });
48
48
 
49
49
  it.each([
50
- [2826.8015616000007, 8, 9.6],
51
- [2120.1011712, undefined, 9.6]
50
+ // surface saisie prioritaire sur le forfait 1,6 m²/module (Tribu : seule S_capteur en entrée)
51
+ [2120.1011712, 8, 9.6],
52
+ [2120.1011712, undefined, 9.6],
53
+ // à défaut de surface : 1,6 m² × nombre de modules
54
+ [2826.8015616000007, 8, undefined],
55
+ [2826.8015616000007, 8, 0]
52
56
  ])(
53
57
  'should get ppv %s with nombre module %s and surface totale capteur %s',
54
58
  (ppv, nombre_module, surface_totale_capteurs) => {
@@ -68,6 +72,97 @@ describe('production ENR unit tests', () => {
68
72
  }
69
73
  );
70
74
 
75
+ describe('proratisation au logement des installations PV d’immeuble', () => {
76
+ const panneau = (extra = {}) => ({
77
+ panneaux_pv_collection: {
78
+ panneaux_pv: [
79
+ {
80
+ surface_totale_capteurs: 9.6,
81
+ nombre_module: 8,
82
+ enum_orientation_pv_id: 1,
83
+ enum_inclinaison_pv_id: 2,
84
+ ...extra
85
+ }
86
+ ]
87
+ }
88
+ });
89
+
90
+ test('ratio 1 par défaut : production de l’installation complète', () => {
91
+ expect(productionENR.getPpv(panneau(), 1)).toBe(2120.1011712);
92
+ });
93
+
94
+ test('la production est proratisée par Sh_logement / Sh_immeuble', () => {
95
+ expect(productionENR.getPpv(panneau(), 1, 0.25)).toBeCloseTo(2120.1011712 * 0.25, 9);
96
+ });
97
+
98
+ test.each([0.1, 0.01, 0])(
99
+ 'installation déjà virtualisée par le logiciel (ratio_virtualisation %s) : pas de proratisation',
100
+ (ratio_virtualisation) => {
101
+ expect(productionENR.getPpv(panneau({ ratio_virtualisation }), 1, 0.25)).toBe(2120.1011712);
102
+ }
103
+ );
104
+
105
+ test('le ratio_virtualisation est ignoré sans proratisation (maison, appartement individuel)', () => {
106
+ expect(productionENR.getPpv(panneau({ ratio_virtualisation: 0.1 }), 1, 1)).toBe(2120.1011712);
107
+ });
108
+
109
+ test('2469E1760405R : 2 m² saisis, 18 modules, Sh 67,42 / 1 921,43 m²', () => {
110
+ // Ppv = S × Sh_log / Sh_imm × Σ k·0,17·Epv·0,86 : la production dépend de la surface
111
+ // retenue (2 m², pas 1,6 × 18 = 28,8 m²) et est ramenée au logement.
112
+ const ratio = ratioProrataPvLogement('appartement', '5', 67.42, 1921.43);
113
+ const pvImmeuble = productionENR.getPpv(
114
+ panneau({ surface_totale_capteurs: 2, nombre_module: 18 }),
115
+ 1
116
+ );
117
+ const pvLogement = productionENR.getPpv(
118
+ panneau({ surface_totale_capteurs: 2, nombre_module: 18 }),
119
+ 1,
120
+ ratio
121
+ );
122
+ expect(pvImmeuble).toBeCloseTo((2120.1011712 / 9.6) * 2, 9);
123
+ expect(pvLogement).toBeCloseTo(pvImmeuble * (67.42 / 1921.43), 9);
124
+ });
125
+
126
+ test('calculateEnr transmet le ratio à la production PV', () => {
127
+ const conso = {
128
+ ef_conso: { conso_ecs: 1000, conso_5_usages: 1500 },
129
+ ep_conso: { ep_conso_5_usages: 10000 },
130
+ sortie_par_energie_collection: {
131
+ sortie_par_energie: [{ enum_type_energie_id: '1', conso_5_usages: 1500 }]
132
+ }
133
+ };
134
+ const ret = productionENR.calculateEnr(
135
+ { donnee_entree: { presence_production_pv: 1 }, ...panneau() },
136
+ conso,
137
+ 10,
138
+ 'appartement',
139
+ 1,
140
+ undefined,
141
+ 0.5
142
+ );
143
+ expect(ret.production_pv).toBeCloseTo(2120.1011712 * 0.5, 9);
144
+ });
145
+ });
146
+
147
+ describe('ratioProrataPvLogement', () => {
148
+ test.each([
149
+ ['appartement, installation d’immeuble (map 5)', 'appartement', '5', 50, 200, 0.25],
150
+ ['appartement issu d’un DPE immeuble (map 33)', 'appartement', '33', 50, 200, 0.25],
151
+ ['surfaces sous forme de chaînes', 'appartement', '32', '48.3', '1113.35', 48.3 / 1113.35],
152
+ ['maison', 'maison', '1', 50, 200, 1],
153
+ ['immeuble', 'immeuble', '6', 200, 200, 1],
154
+ ['appartement chauffage + ECS individuels (map 2)', 'appartement', '2', 50, 200, 1],
155
+ ['appartement RT2012 individuel (map 22)', 'appartement', '22', 50, 200, 1],
156
+ ['appartement RE2020 individuel (map 25)', 'appartement', '25', 50, 200, 1],
157
+ ['surface immeuble absente', 'appartement', '5', 50, undefined, 1],
158
+ ['surface logement absente', 'appartement', '5', undefined, 200, 1],
159
+ ['surface immeuble égale à celle du logement', 'appartement', '5', 50, 50, 1],
160
+ ['surface immeuble inférieure à celle du logement', 'appartement', '5', 50, 40, 1]
161
+ ])('%s', (_label, th, mapId, shl, shi, attendu) => {
162
+ expect(ratioProrataPvLogement(th, mapId, shl, shi)).toBeCloseTo(attendu, 12);
163
+ });
164
+ });
165
+
71
166
  test('should update ef conso', () => {
72
167
  const productionElectricite = {
73
168
  conso_elec_ac_fr: 100,
package/9_chauffage.js CHANGED
@@ -69,6 +69,7 @@ export default function calc_chauffage(
69
69
 
70
70
  genChDe.ratio_virtualisation = de.ratio_virtualisation || 1;
71
71
  genChDe.cle_repartition_ch = de.cle_repartition_ch || 1;
72
+ genChDe.enum_type_installation_id = de.enum_type_installation_id;
72
73
  genChDe.surface_chauffee = de.surface_chauffee || Sh;
73
74
  genChDe.rdim_installation_ch = isImmeuble(map_id) ? Number(de.rdim) || 1 : 1;
74
75
  genChDe.nombre_niveau_installation_ch = de.nombre_niveau_installation_ch || 1;
@@ -219,6 +219,15 @@ describe('calc_chauffage - cas nominal (1 générateur, 1 émetteur)', () => {
219
219
  expect(genDe.nombre_niveau_installation_ch).toBe(4);
220
220
  });
221
221
 
222
+ test("propage le type d'installation au générateur (clé de répartition, issue #252)", () => {
223
+ const ch = installation([generateur()], [emetteur('1')], {});
224
+ ch.donnee_entree.enum_type_installation_id = '1';
225
+ appel(ch);
226
+
227
+ const genDe = ch.generateur_chauffage_collection.generateur_chauffage[0].donnee_entree;
228
+ expect(genDe.enum_type_installation_id).toBe('1');
229
+ });
230
+
222
231
  test('prorata unitaire : le besoin complet est transmis au générateur', () => {
223
232
  const ch = installation([generateur()], [emetteur('1')]);
224
233
  appel(ch, { bch: 800 });
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.33`** · branche `main` · généré le 2026-10-09
373
+ > **Version `1.8.35`** · branche `main` · généré le 2026-10-09
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 986</strong><br/><sub>DPE analysés</sub></td>
380
- <td align="center"><strong>60 961</strong><br/><sub>DPE conformes</sub></td>
381
- <td align="center"><strong>67,74 %</strong><br/><sub>réussite globale</sub></td>
379
+ <td align="center"><strong>89 984</strong><br/><sub>DPE analysés</sub></td>
380
+ <td align="center"><strong>61 950</strong><br/><sub>DPE conformes</sub></td>
381
+ <td align="center"><strong>68,85 %</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,13 %** | `█████████░░░░░░░░░░░` | 4 613 / 9 999 |
387
+ | 🔴 | **Généraliste**<br/><sub>`corpus_dpe.csv`</sub> | **46,18 %** | `█████████░░░░░░░░░░░` | 4 618 / 9 999 |
388
388
  | 🟢 | **Appartement · chauffage individuel (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_individuel_2025.csv`</sub> | **92,53 %** | `███████████████████░` | 9 253 / 10 000 |
389
389
  | 🟢 | **Logement individuel (2025)**<br/><sub>`dpe_logement_individuel_2025.csv`</sub> | **89,54 %** | `██████████████████░░` | 8 947 / 9 992 |
390
- | 🟢 | **Maison individuelle (2025)**<br/><sub>`dpe_maison_individuelle_2025.csv`</sub> | **88,60 %** | `██████████████████░░` | 8 859 / 9 999 |
391
- | 🟡 | **Immeuble · chauffage individuel**<br/><sub>`dpe_immeuble_chauffage_individuel.csv`</sub> | **77,02 %** | `███████████████░░░░░` | 7 700 / 9 998 |
392
- | 🟡 | **Appartement · chauffage collectif (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_collectif_2025.csv`</sub> | **71,59 %** | `██████████████░░░░░░` | 7 159 / 10 000 |
393
- | 🟡 | **Immeuble · chauffage collectif**<br/><sub>`dpe_immeuble_chauffage_collectif.csv`</sub> | **65,11 %** | `█████████████░░░░░░░` | 6 510 / 9 999 |
394
- | 🔴 | **Immeuble · chauffage mixte**<br/><sub>`dpe_immeuble_chauffage_mixte.csv`</sub> | **52,15 %** | `██████████░░░░░░░░░░` | 5 214 / 9 999 |
390
+ | 🟢 | **Maison individuelle (2025)**<br/><sub>`dpe_maison_individuelle_2025.csv`</sub> | **88,61 %** | `██████████████████░░` | 8 860 / 9 999 |
391
+ | 🟡 | **Immeuble · chauffage individuel**<br/><sub>`dpe_immeuble_chauffage_individuel.csv`</sub> | **84,21 %** | `█████████████████░░░` | 8 419 / 9 998 |
392
+ | 🟡 | **Appartement · chauffage collectif (2025)**<br/><sub>`dpe_appartement_individuel_chauffage_collectif_2025.csv`</sub> | **71,60 %** | `██████████████░░░░░░` | 7 160 / 10 000 |
393
+ | 🟡 | **Immeuble · chauffage collectif**<br/><sub>`dpe_immeuble_chauffage_collectif.csv`</sub> | **66,19 %** | `█████████████░░░░░░░` | 6 618 / 9 999 |
394
+ | 🔴 | **Immeuble · chauffage mixte**<br/><sub>`dpe_immeuble_chauffage_mixte.csv`</sub> | **53,71 %** | `███████████░░░░░░░░░` | 5 369 / 9 997 |
395
395
  | 🔴 | **Individuel généré depuis l'immeuble (2026)**<br/><sub>`dpe_individuel_a_partir_dpe_immeuble_2026.csv`</sub> | **27,06 %** | `█████░░░░░░░░░░░░░░░` | 2 706 / 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 952 calculs.
401
+ > Durée de l’appel à `calcul_3cl` par DPE, sur 89 950 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,69 ms</strong><br/><sub>moyenne</sub></td>
407
- <td align="center"><strong>5,00 ms</strong><br/><sub>médiane</sub></td>
406
+ <td align="center"><strong>5,06 ms</strong><br/><sub>moyenne</sub></td>
407
+ <td align="center"><strong>4,41 ms</strong><br/><sub>médiane</sub></td>
408
408
  <td align="center"><strong>0,11 ms</strong><br/><sub>min</sub></td>
409
- <td align="center"><strong>479,4 ms</strong><br/><sub>max</sub></td>
409
+ <td align="center"><strong>768,9 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,39 ms | 4,95 ms | 0,25 ms | 38,8 ms | 9,87 ms | 13,9 ms |
416
- | **Appartement · chauffage individuel (2025)** | 4,01 ms | 3,64 ms | 1,41 ms | 28,6 ms | 6,92 ms | 9,06 ms |
417
- | **Logement individuel (2025)** | 5,43 ms | 4,82 ms | 1,69 ms | 37,7 ms | 10,1 ms | 14,1 ms |
418
- | **Maison individuelle (2025)** | 7,12 ms | 6,44 ms | 2,19 ms | 48,5 ms | 12,6 ms | 16,7 ms |
419
- | **Immeuble · chauffage individuel** | 7,03 ms | 5,96 ms | 0,11 ms | 81,7 ms | 14,9 ms | 22,9 ms |
420
- | **Appartement · chauffage collectif (2025)** | 4,34 ms | 3,91 ms | 1,41 ms | 49,2 ms | 7,18 ms | 9,95 ms |
421
- | **Immeuble · chauffage collectif** | 6,26 ms | 5,48 ms | 0,16 ms | 139,4 ms | 11,5 ms | 20,2 ms |
422
- | **Immeuble · chauffage mixte** | 5,33 ms | 4,36 ms | 0,18 ms | 479,4 ms | 11,0 ms | 17,8 ms |
423
- | **Individuel généré depuis l'immeuble (2026)** | 6,30 ms | 5,41 ms | 1,57 ms | 43,6 ms | 12,2 ms | 18,1 ms |
415
+ | **Généraliste** | 4,20 ms | 3,76 ms | 0,22 ms | 34,2 ms | 7,69 ms | 10,6 ms |
416
+ | **Appartement · chauffage individuel (2025)** | 3,75 ms | 3,39 ms | 1,47 ms | 29,4 ms | 6,55 ms | 8,36 ms |
417
+ | **Logement individuel (2025)** | 4,83 ms | 4,30 ms | 1,55 ms | 29,3 ms | 9,03 ms | 12,1 ms |
418
+ | **Maison individuelle (2025)** | 5,88 ms | 5,27 ms | 1,84 ms | 50,5 ms | 10,7 ms | 13,9 ms |
419
+ | **Immeuble · chauffage individuel** | 6,26 ms | 5,33 ms | 0,11 ms | 74,5 ms | 12,9 ms | 19,8 ms |
420
+ | **Appartement · chauffage collectif (2025)** | 4,16 ms | 3,75 ms | 1,41 ms | 35,8 ms | 6,88 ms | 8,69 ms |
421
+ | **Immeuble · chauffage collectif** | 5,42 ms | 4,62 ms | 0,12 ms | 106,1 ms | 10,1 ms | 17,7 ms |
422
+ | **Immeuble · chauffage mixte** | 5,23 ms | 4,26 ms | 0,17 ms | 768,9 ms | 10,7 ms | 17,1 ms |
423
+ | **Individuel généré depuis l'immeuble (2026)** | 5,81 ms | 5,00 ms | 1,65 ms | 42,2 ms | 11,0 ms | 16,4 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
@@ -30,10 +30,10 @@ import {
30
30
  } from './utils.js';
31
31
  import { Inertie } from './7_inertie.js';
32
32
  import getFicheTechnique from './ficheTechnique.js';
33
- import { ProductionENR } from './16.2_production_enr.js';
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.34';
36
+ const LIB_VERSION = '1.8.36';
37
37
 
38
38
  const dpeSanitizerService = new DpeSanitizerService();
39
39
 
@@ -598,12 +598,22 @@ export function calcul_3cl(inputDpe, options) {
598
598
  th === 'immeuble' ? Nb_lgt : 1
599
599
  );
600
600
 
601
+ // Proratisation au logement d'une installation PV d'immeuble (appartement)
602
+ const ratioPv = ratioProrataPvLogement(
603
+ th,
604
+ map_id,
605
+ cg.surface_habitable_logement,
606
+ cg.surface_habitable_immeuble
607
+ );
608
+
601
609
  const production_electricite = productionENR.calculateEnr(
602
610
  dpe.logement.production_elec_enr,
603
611
  conso,
604
612
  Sh,
605
613
  th,
606
- zc_id
614
+ zc_id,
615
+ undefined,
616
+ ratioPv
607
617
  );
608
618
 
609
619
  const conso1_7 = calc_conso(
@@ -627,7 +637,8 @@ export function calcul_3cl(inputDpe, options) {
627
637
  Sh,
628
638
  th,
629
639
  zc_id,
630
- COEFF_EP_1_7
640
+ COEFF_EP_1_7,
641
+ ratioPv
631
642
  );
632
643
 
633
644
  const conso2_3 = calc_conso(
@@ -651,7 +662,8 @@ export function calcul_3cl(inputDpe, options) {
651
662
  Sh,
652
663
  th,
653
664
  zc_id,
654
- COEFF_EP_2_3
665
+ COEFF_EP_2_3,
666
+ ratioPv
655
667
  );
656
668
 
657
669
  // get all baie_vitree orientations
package/engine.spec.js CHANGED
@@ -75,8 +75,12 @@ vi.mock('./ficheTechnique.js', () => ({ default: vi.fn() }));
75
75
  // On expose des méthodes mockées récupérables dans les tests.
76
76
  const calculateInertie = vi.fn();
77
77
  const calculateEnr = vi.fn();
78
+ const ratioProrataPvLogement = vi.fn();
78
79
  vi.mock('./7_inertie.js', () => ({ Inertie: vi.fn(() => ({ calculateInertie })) }));
79
- vi.mock('./16.2_production_enr.js', () => ({ ProductionENR: vi.fn(() => ({ calculateEnr })) }));
80
+ vi.mock('./16.2_production_enr.js', () => ({
81
+ ProductionENR: vi.fn(() => ({ calculateEnr })),
82
+ ratioProrataPvLogement: (...args) => ratioProrataPvLogement(...args)
83
+ }));
80
84
 
81
85
  // Le service de sanitisation est instancié au chargement puis appelé : `execute`
82
86
  // renvoie l'entrée telle quelle.
@@ -153,6 +157,8 @@ beforeEach(() => {
153
157
  };
154
158
  vi.mocked(calc_conso).mockReturnValue(consoResult);
155
159
  vi.mocked(calculateEnr).mockReturnValue({ pv: 1 });
160
+ ratioProrataPvLogement.mockReset();
161
+ ratioProrataPvLogement.mockReturnValue(0.25);
156
162
  vi.mocked(calc_confort_ete).mockReturnValue('confort');
157
163
  vi.mocked(calc_qualite_isolation).mockReturnValue('qualite');
158
164
  });
@@ -426,17 +432,24 @@ describe('calcul_3cl - run intégral (maison)', () => {
426
432
  expect(calc_conso.mock.calls[0][7]).toBe(1);
427
433
  expect(calc_conso.mock.calls[0][8]).toBe(1);
428
434
 
429
- // 3 calculs de production ENR ; le 1ᵉʳ sans coefficient.
435
+ // Coefficient de proratisation PV calculé à partir du type de bâtiment, de la méthode et des surfaces.
436
+ expect(ratioProrataPvLogement).toHaveBeenCalledWith('maison', '1', 80, 200);
437
+
438
+ // 3 calculs de production ENR ; le 1ᵉʳ sans coefficient ; le ratio PV est transmis aux 3 passes.
430
439
  expect(calculateEnr).toHaveBeenCalledTimes(3);
431
440
  expect(calculateEnr.mock.calls[0]).toEqual([
432
441
  dpe.logement.production_elec_enr,
433
442
  consoResult,
434
443
  80,
435
444
  'maison',
436
- 'zc1'
445
+ 'zc1',
446
+ undefined,
447
+ 0.25
437
448
  ]);
438
449
  expect(calculateEnr.mock.calls[1][5]).toBe(1.7);
439
450
  expect(calculateEnr.mock.calls[2][5]).toBe(2.3);
451
+ expect(calculateEnr.mock.calls[1][6]).toBe(0.25);
452
+ expect(calculateEnr.mock.calls[2][6]).toBe(0.25);
440
453
 
441
454
  // Sorties agrégées.
442
455
  expect(dpe.logement.sortie.deperdition).toEqual({ deperdition_enveloppe: 300 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open3cl/engine",
3
- "version": "1.8.34",
3
+ "version": "1.8.36",
4
4
  "description": "Open Source 3CL-DPE engine",
5
5
  "main": "index.js",
6
6
  "directories": {