@open3cl/engine 1.8.7 → 1.8.9

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,248 @@
1
+ /**
2
+ * Puissance nominale d'un générateur à combustion collectif virtualisé (DPE appartement).
3
+ *
4
+ * 17.2.1 - Génération d’un DPE à l’appartement / Traitement des usages collectifs :
5
+ * le générateur collectif est « virtualisé » pour le logement. Le champ `pn` attend alors la
6
+ * puissance virtualisée Pe = a × Pn(collectif) (cf. documentation XSD de `pn`), `a` étant le
7
+ * `ratio_virtualisation` de l'installation. Le moteur retrouve Pn(collectif) = pn / a pour lire
8
+ * les tables (critère de puissance, rendements rpn/rpint, pertes à l'arrêt qp0).
9
+ *
10
+ * Tous les logiciels ne respectent pas cette convention (issue #124) : certains (ex. DPEWin)
11
+ * saisissent dans `pn` la puissance du générateur collectif (Pn), d'autres (ex. Liciel) la
12
+ * puissance virtualisée (Pe). Le moteur ne peut pas le savoir a priori, mais les valeurs
13
+ * intermédiaires stockées dans le DPE (rpn, qp0) trahissent la convention réellement utilisée
14
+ * par le logiciel. Même esprit que l'issue #75 (volume de stockage ECS collectif / individualisé).
15
+ *
16
+ * Hypothèses confrontées aux valeurs du DPE :
17
+ * - individualisée (convention 3CL, comportement par défaut) : pn = Pe -> Pn(collectif) = pn / a ;
18
+ * - collective virtualisée : pn = Pn(collectif) mais le logiciel a bien virtualisé qp0
19
+ * -> on ramène pn à Pe = pn × a (la troncature éventuelle de Pn saisi est sans effet notable :
20
+ * on recalcule Pe à partir de Pn plutôt que de chercher une valeur non tronquée) ;
21
+ * - collective non virtualisée : pn = Pn(collectif) et le logiciel n'a pas virtualisé du tout
22
+ * (rendements et qp0 calculés sur Pn) -> reproduit uniquement en mode `bug_for_bug_compat`,
23
+ * sinon on applique la méthode (pn ramené à Pe = pn × a).
24
+ */
25
+
26
+ /**
27
+ * Nombre de décimales d'un nombre (notation décimale ou scientifique).
28
+ * @param valeur {number}
29
+ * @returns {number}
30
+ */
31
+ export function nombreDecimales(valeur) {
32
+ const [mantisse, exposant] = String(valeur).toLowerCase().split('e');
33
+ const decimales = (mantisse.split('.')[1] || '').length;
34
+ return Math.max(0, decimales - Number(exposant || 0));
35
+ }
36
+
37
+ /**
38
+ * Vrai si la valeur calculée correspond à la valeur stockée dans le DPE, compte tenu de
39
+ * l'arrondi de la valeur stockée et d'une tolérance relative de 0,1 % couvrant la troncature de
40
+ * la puissance saisie. Les logiciels arrondissent au plus à 2 décimales (0.9 vaut 0.90) : la
41
+ * tolérance d'arrondi est la demi-unité de la dernière décimale, au plus 0,005.
42
+ *
43
+ * @param stockee {number} valeur du DPE
44
+ * @param calculee {number} valeur recalculée
45
+ * @returns {boolean}
46
+ */
47
+ export function valeurCoherente(stockee, calculee) {
48
+ const tolerance = Math.max(
49
+ 0.5 * 10 ** -Math.max(nombreDecimales(stockee), 2),
50
+ 1e-3 * Math.abs(stockee)
51
+ );
52
+ return Math.abs(stockee - calculee) <= tolerance * (1 + 1e-9);
53
+ }
54
+
55
+ /**
56
+ * qp0 stocké en W ou en kW selon les logiciels : on accepte les deux unités.
57
+ * @param qp0Stocke {number} qp0 du DPE (W ou kW)
58
+ * @param qp0Calcule {number} qp0 recalculé (W)
59
+ * @returns {boolean}
60
+ */
61
+ export function qp0Coherent(qp0Stocke, qp0Calcule) {
62
+ return valeurCoherente(qp0Stocke, qp0Calcule) || valeurCoherente(qp0Stocke, qp0Calcule / 1000);
63
+ }
64
+
65
+ function estNombre(valeur) {
66
+ return typeof valeur === 'number' && Number.isFinite(valeur);
67
+ }
68
+
69
+ /**
70
+ * Inverse une formule de rendement de la table generateur_combustion, de la forme
71
+ * « A + B × log10(Pn) » (en %, Pn en kW) : renvoie Pn (W) tel que formule(Pn) = rendement.
72
+ * La forme est vérifiée numériquement (affine en log10(Pn)) : toute autre forme, ou un rendement
73
+ * indépendant de Pn (B = 0), n'est pas inversible.
74
+ *
75
+ * @param rendement {number} rendement stocké dans le DPE (fraction)
76
+ * @param formule {(pnW: number) => number} rendement (fraction) en fonction de Pn (W)
77
+ * @returns {number|undefined} Pn (W)
78
+ */
79
+ export function inverserRendement(rendement, formule) {
80
+ const a = formule(1000);
81
+ const b = formule(10000) - a;
82
+ const affine = Math.abs(formule(100000) - (a + 2 * b)) <= 1e-9;
83
+ if (!estNombre(a) || !estNombre(b) || !affine || Math.abs(b) < 1e-9) {
84
+ return undefined;
85
+ }
86
+ return 1000 * 10 ** ((rendement - a) / b);
87
+ }
88
+
89
+ /**
90
+ * Puissance du générateur collectif Pn (W) retrouvée à partir du rpn stocké dans le DPE, en
91
+ * inversant la formule rpn de la ligne de table. La ligne dépendant elle-même de Pn (critère de
92
+ * puissance), la ligne est re-sélectionnée sur le Pn obtenu jusqu'à stabilité (3 itérations au
93
+ * plus) : le rpn de la ligne finale évalué sur Pn doit redonner exactement le rpn du DPE.
94
+ *
95
+ * @param pnCollectif {number} Pn (W) servant à sélectionner la première ligne
96
+ * @param rpnDpe {number}
97
+ * @param formuleRpn {(pnCollectifW: number) => ((pnW: number) => number)|undefined} formule rpn
98
+ * (fraction, fonction de Pn en W) de la ligne de table sélectionnée pour un Pn donné
99
+ * @returns {number|undefined} Pn (W)
100
+ */
101
+ export function puissanceDepuisRpn(pnCollectif, rpnDpe, formuleRpn) {
102
+ let reference = pnCollectif;
103
+ for (let iteration = 0; iteration < 3; iteration++) {
104
+ const formule = formuleRpn(reference);
105
+ const pn = formule && inverserRendement(rpnDpe, formule);
106
+ if (!estNombre(pn)) {
107
+ return undefined;
108
+ }
109
+ const formuleFinale = formuleRpn(pn);
110
+ if (formuleFinale && Math.abs(formuleFinale(pn) - rpnDpe) <= 1e-9) {
111
+ return pn;
112
+ }
113
+ reference = pn;
114
+ }
115
+ return undefined;
116
+ }
117
+
118
+ /**
119
+ * Vrai si la puissance saisie est une puissance de référence décalée d'une puissance de dix
120
+ * (virgule mal placée ou zéro perdu lors de la saisie : 69600 W pour 696 kW).
121
+ *
122
+ * @param pnSaisi {number} puissance saisie dans le DPE (W)
123
+ * @param puissance {number} puissance de référence (W)
124
+ * @returns {boolean}
125
+ */
126
+ export function decalageDecimal(pnSaisi, puissance) {
127
+ return [-3, -2, -1, 1, 2, 3].some((k) => valeurCoherente(pnSaisi, puissance * 10 ** k));
128
+ }
129
+
130
+ /**
131
+ * 4e hypothèse : pn saisi incohérent avec le rpn stocké, que l'on interprète pn comme Pe ou comme
132
+ * Pn(collectif) (ex. puissance tronquée d'un facteur 10). Le rpn stocké, calculé par le logiciel
133
+ * à partir de la table, permet de retrouver Pn(collectif) en inversant la formule rpn de la table ;
134
+ * on retient alors Pe = a × Pn (convention 3CL).
135
+ *
136
+ * Le Pn retrouvé doit être confirmé par une autre valeur du DPE :
137
+ * - rpint stocké et recalculé : doit être cohérent (sinon abandon) et confirme l'hypothèse ;
138
+ * - qp0 stocké cohérent (W ou kW, virtualisé ou non) avec le qp0 recalculé ;
139
+ * - ou pn saisi égal à Pn ou Pe à une puissance de dix près (erreur de saisie de la puissance).
140
+ *
141
+ * @param pn {number} puissance nominale saisie dans le DPE (W)
142
+ * @param ratio {number} ratio de virtualisation (0 < ratio < 1)
143
+ * @param valeursDpe {{rpn: number, rpint?: number, qp0?: number}}
144
+ * @param caracteristiques {(pn: number, ratio: number) => {rpn?: number, rpint?: number, qp0?: number}}
145
+ * @param formuleRpn {(pnCollectif: number) => ((pnCollectif: number) => number)|undefined}
146
+ * @returns {{pn: number, ratio: number, convention: 'puissance_depuis_rpn', pnCollectif: number}|undefined}
147
+ */
148
+ export function puissanceRecalculeeDepuisRpn(pn, ratio, valeursDpe, caracteristiques, formuleRpn) {
149
+ const pnCollectif = puissanceDepuisRpn(pn / ratio, valeursDpe.rpn, formuleRpn);
150
+ if (!estNombre(pnCollectif)) {
151
+ return undefined;
152
+ }
153
+
154
+ const pe = pnCollectif * ratio;
155
+ const recalcule = caracteristiques(pe, ratio);
156
+ const { rpint: rpintDpe, qp0: qp0Dpe } = valeursDpe;
157
+
158
+ const rpintControle = estNombre(rpintDpe) && estNombre(recalcule.rpint);
159
+ if (rpintControle && !valeurCoherente(rpintDpe, recalcule.rpint)) {
160
+ return undefined;
161
+ }
162
+
163
+ const qp0Confirme =
164
+ estNombre(qp0Dpe) &&
165
+ [recalcule.qp0, caracteristiques(pnCollectif, 1).qp0].some(
166
+ (qp0) => estNombre(qp0) && qp0Coherent(qp0Dpe, qp0)
167
+ );
168
+
169
+ if (rpintControle || qp0Confirme || decalageDecimal(pn, pnCollectif) || decalageDecimal(pn, pe)) {
170
+ return { pn: pe, ratio, convention: 'puissance_depuis_rpn', pnCollectif };
171
+ }
172
+ return undefined;
173
+ }
174
+
175
+ /**
176
+ * Détermine la puissance nominale (pn) et le ratio de virtualisation à utiliser pour calculer les
177
+ * caractéristiques (rpn, rpint, qp0) d'un générateur à combustion collectif virtualisé.
178
+ *
179
+ * @param pn {number} puissance nominale saisie dans le DPE (W)
180
+ * @param ratio {number} ratio de virtualisation de l'installation
181
+ * @param valeursDpe {{rpn?: number, rpint?: number, qp0?: number}} valeurs intermédiaires
182
+ * stockées dans le DPE
183
+ * @param caracteristiques {(pn: number, ratio: number) => {rpn?: number, rpint?: number, qp0?: number}}
184
+ * calcul des caractéristiques (rpn, rpint, qp0 en W) pour un couple (pn, ratio) ; une
185
+ * caractéristique non recalculée (saisie) est renvoyée `undefined`
186
+ * @param bugForBugCompat {boolean} reproduire un calcul non virtualisé du logiciel
187
+ * @param [formuleRpn] {(pnCollectif: number) => ((pnCollectif: number) => number)|undefined}
188
+ * formule rpn (fraction, fonction de Pn collectif en W) de la ligne de table sélectionnée pour
189
+ * un Pn collectif donné ; absente, la 4e hypothèse (pn recalculé depuis rpn) n'est pas testée
190
+ * @returns {{pn: number, ratio: number, convention: 'individualisee'|'collective_virtualisee'|'collective_non_virtualisee'|'puissance_depuis_rpn', pnCollectif?: number}}
191
+ */
192
+ export function conventionPuissanceNominale(
193
+ pn,
194
+ ratio,
195
+ valeursDpe,
196
+ caracteristiques,
197
+ bugForBugCompat,
198
+ formuleRpn
199
+ ) {
200
+ const individualisee = { pn, ratio, convention: 'individualisee' };
201
+ const rpnDpe = valeursDpe?.rpn;
202
+ const qp0Dpe = valeursDpe?.qp0;
203
+
204
+ if (!(ratio > 0 && ratio < 1) || !pn || !estNombre(rpnDpe)) {
205
+ return individualisee;
206
+ }
207
+
208
+ /**
209
+ * Le rendement à pleine charge rpn dépend de Pn(collectif) : s'il est cohérent avec pn / a,
210
+ * le pn saisi est bien la puissance virtualisée (convention 3CL).
211
+ */
212
+ const rpnIndividualise = caracteristiques(pn, ratio).rpn;
213
+ if (!estNombre(rpnIndividualise) || valeurCoherente(rpnDpe, rpnIndividualise)) {
214
+ return individualisee;
215
+ }
216
+
217
+ /**
218
+ * rpn cohérent avec pn : le logiciel a considéré pn comme la puissance du générateur collectif.
219
+ */
220
+ const virtualisee = caracteristiques(pn * ratio, ratio);
221
+ if (!valeurCoherente(rpnDpe, virtualisee.rpn)) {
222
+ /**
223
+ * pn n'est ni Pe ni Pn(collectif) : Pn(collectif) est retrouvé à partir de rpn (4e hypothèse).
224
+ */
225
+ return (
226
+ (formuleRpn &&
227
+ puissanceRecalculeeDepuisRpn(pn, ratio, valeursDpe, caracteristiques, formuleRpn)) ||
228
+ individualisee
229
+ );
230
+ }
231
+
232
+ /**
233
+ * qp0 permet de savoir si le logiciel a malgré tout virtualisé les pertes à l'arrêt.
234
+ * Faute de qp0 exploitable, rpn suffit à établir que pn est la puissance collective.
235
+ */
236
+ if (!estNombre(qp0Dpe) || !estNombre(virtualisee.qp0) || qp0Coherent(qp0Dpe, virtualisee.qp0)) {
237
+ return { pn: pn * ratio, ratio, convention: 'collective_virtualisee' };
238
+ }
239
+
240
+ const nonVirtualisee = caracteristiques(pn, 1);
241
+ if (estNombre(nonVirtualisee.qp0) && qp0Coherent(qp0Dpe, nonVirtualisee.qp0)) {
242
+ return bugForBugCompat
243
+ ? { pn, ratio: 1, convention: 'collective_non_virtualisee' }
244
+ : { pn: pn * ratio, ratio, convention: 'collective_virtualisee' };
245
+ }
246
+
247
+ return individualisee;
248
+ }