From b518f494684b7863a9ef00446176b55cec2683b6 Mon Sep 17 00:00:00 2001 From: Hermes Bot Date: Wed, 28 Oct 2026 10:42:57 +0000 Subject: [PATCH] feat(ventilation): ne reprendre la ligne q4pa_conv du dpe qu'en analyse de corpus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Un client contrôle les DPE soumis en les recalculant avec le moteur : un écart de plus de 5 % entraîne un rejet. Certains logiciels (LICIEL notamment) retiennent une ligne q4pa_conv qui contredit l'isolation déclarée des parois ou les joints des menuiseries ; en la reprenant (#312, #294) le moteur masquait l'erreur. Les scénarios de travaux (Kardino) propageaient aussi une ligne devenue obsolète. Nouvelle option `compat_for_corpus_analysis` de calcul_3cl / calcul_3cl_xml (défaut false) : - false : q4pa_conv est toujours recalculé selon la méthode, une ligne contradictoire est seulement signalée ; - true : reprise de la ligne du DPE (#312) et des joints quand presence_joint n'est pas exporté (#294), en legacy comme dans la nouvelle architecture, pour reproduire le corpus ADEME. Le drapeau est repositionné à chaque appel. Les specs de corpus, le worker du rapport de corpus et le benchmark activent l'option : leurs résultats sont inchangés. Changement de comportement par défaut sur q4pa_conv : montée de version mineure. --- README.md | 13 ++- src/4_ventilation.js | 22 ++++- src/4_ventilation.spec.js | 89 +++++++++++++++++-- src/engine.js | 11 ++- src/engine.spec.js | 48 ++++++++++ .../deperdition-ventilation.service.js | 6 +- .../deperdition-ventilation.service.spec.js | 30 +++++-- src/utils.js | 23 +++++ src/utils.spec.js | 15 ++++ test/corpus/corpus_worker.mjs | 4 +- test/open3cl-bug-tribu-zone-2hd.spec.js | 4 +- test/open3cl-chaudiere-collective-pn.spec.js | 8 +- test/open3cl-coeff-1-7.spec.js | 10 +-- ...open3cl-periode-emetteur-temp-fonc.spec.js | 6 +- ...cl-perte-recup-generateur-ecs-seul.spec.js | 4 +- test/open3cl-q4pa-ligne-dpe.spec.js | 43 +++++++-- ...open3cl-qp0-ventilateur-combustion.spec.js | 4 +- test/open3cl-rcu-ecs-isole.spec.js | 4 +- test/open3cl-rcu.spec.js | 4 +- test/open3cl-sorties-dpe-immeuble.spec.js | 18 ++-- test/open3cl-sorties-ventilation.spec.js | 13 ++- ..._conso_auxiliaire_distribution_ecs.spec.js | 8 +- test/open3cl_ecs.spec.js | 4 +- test/open3cl_ecs_auxiliaires.spec.js | 4 +- test/open3cl_misc.spec.js | 36 +++++--- test/open3cl_mur.spec.js | 4 +- test/open3cl_pertes_generateur.spec.js | 4 +- test/open3cl_ponts_thermique.spec.js | 10 ++- test/open3cl_production_enr.spec.js | 4 +- test/run_benchmark.js | 3 +- test/test-helpers.js | 10 +++ .../open3cl-coeff-1-7.spec.js" | 9 +- 32 files changed, 377 insertions(+), 98 deletions(-) diff --git a/README.md b/README.md index 5034af892..c5d771b81 100644 --- a/README.md +++ b/README.md @@ -251,11 +251,18 @@ const c = calcul_3cl(dpeData, { sanitize: false }); // Depuis un XML ADEME const d = calcul_3cl_xml(xmlString); const e = calcul_3cl_xml(xmlString, { sanitize: false }); + +// Analyse du corpus ADEME uniquement : reproduire les choix du logiciel de saisie +const f = calcul_3cl(dpeData, { sanitize: true, compat_for_corpus_analysis: true }); ``` -| Option | Défaut | Effet | -| :--------- | :----- | :----------------------------------------------------------------------------------------------------- | -| `sanitize` | `true` | Normalise et corrige les incohérences du DPE d'entrée avant calcul. Désactivez-le pour un calcul brut. | +| Option | Défaut | Effet | +| :--------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `sanitize` | `true` | Normalise et corrige les incohérences du DPE d'entrée avant calcul. Désactivez-le pour un calcul brut. | +| `compat_for_corpus_analysis` | `false` | Reprend la ligne `q4pa_conv` choisie par le logiciel de saisie même si elle contredit l'isolation des parois ou les joints déclarés, pour reproduire les DPE du corpus ADEME. À n'utiliser que pour l'analyse de corpus. Désactivé, le moteur recalcule `q4pa_conv` selon la méthode (contrôle de cohérence d'un DPE, scénarios de travaux). | + +> Dès qu'un objet `options` est passé, `sanitize` vaut `false` s'il n'est pas précisé : indiquez +> `sanitize: true` avec `compat_for_corpus_analysis`.
Exemple d'objet JSON d'entrée (partiel) diff --git a/src/4_ventilation.js b/src/4_ventilation.js index 74aa28c8a..7e2270db3 100644 --- a/src/4_ventilation.js +++ b/src/4_ventilation.js @@ -1,4 +1,10 @@ -import { tv, requestInputID, requestInput, bug_for_bug_compat } from './utils.js'; +import { + tv, + requestInputID, + requestInput, + bug_for_bug_compat, + compat_for_corpus_analysis +} from './utils.js'; import calc_pvent from './5_conso_ventilation.js'; function tv_debits_ventilation(di, de, du) { @@ -119,6 +125,11 @@ function tv_q4pa_conv(di, de, cg, mur_list, ph_list, porte_list, bv_list) { * Si la ligne saisie correspond à la période de construction du DPE, on la reprend en entier * (isolation et joints) pour reproduire le calcul du logiciel. * + * Cette reprise n'est faite qu'avec l'option `compat_for_corpus_analysis` (analyse du corpus + * ADEME). Par défaut, la ligne contradictoire est seulement signalée et q4pa_conv est + * recalculé selon la méthode : un contrôle de cohérence du DPE doit détecter l'erreur et un + * scénario de travaux ne doit pas la propager. + * * Sens unique : la reprise ne s'applique que si la ligne indique des surfaces isolées alors que * le recalcul les donne non isolées. Dans le sens inverse (ligne « non isolées », recalcul * « isolées »), le DPE a pu être modifié par l'appelant (scénario d'isolation des parois) : @@ -127,6 +138,7 @@ function tv_q4pa_conv(di, de, cg, mur_list, ph_list, porte_list, bv_list) { */ if (rowQ4paConv.isolation_surfaces !== isolation_surfaces) { if ( + compat_for_corpus_analysis && isLigneQ4paSurfacesIsolees(rowQ4paConv, isolation_surfaces) && isLigneQ4paMemePeriode(rowQ4paConv, cg) ) { @@ -147,10 +159,14 @@ function tv_q4pa_conv(di, de, cg, mur_list, ph_list, porte_list, bv_list) { * Issue #294 : aucune menuiserie n'exporte `presence_joint`. Aucune donnée ne contredit alors * la ligne q4pa_conv retenue par le logiciel (ex. ligne 12 « avant 1948 avec joints », 2,5) : * on reprend sa valeur `presence_joints_menuiserie` au lieu de considérer les menuiseries - * sans joint. + * sans joint. Uniquement avec l'option `compat_for_corpus_analysis`. * @see : Methode_de_calcul_3CL_DPE_2021-338.pdf - §4 */ - if (!ligneDpeReprise && presenceJointNonRenseignee(porte_list, bv_list)) { + if ( + compat_for_corpus_analysis && + !ligneDpeReprise && + presenceJointNonRenseignee(porte_list, bv_list) + ) { pjt = rowQ4paConv.presence_joints_menuiserie === '1' ? '1' : '0'; } else if (!ligneDpeReprise && rowQ4paConv.presence_joints_menuiserie !== pjt) { console.error( diff --git a/src/4_ventilation.spec.js b/src/4_ventilation.spec.js index 3246e1fcb..1c792253e 100644 --- a/src/4_ventilation.spec.js +++ b/src/4_ventilation.spec.js @@ -4,7 +4,8 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; * Dépendances mockées pour isoler le module de ventilation : * - `tv` : accès aux tables `debits_ventilation` / `q4pa_conv`, on contrôle les lignes ; * - `requestInput` / `requestInputID` : passe-plats vers les données d'entrée ; - * - `bug_for_bug_compat` : désactivé pour isoler le comportement nominal ; + * - `bug_for_bug_compat` / `compat_for_corpus_analysis` : désactivés pour isoler le comportement + * nominal ; * - `calc_pvent` (5_conso_ventilation.js) : espion, la conso de ventilation est testée ailleurs. */ vi.mock('./utils.js', () => ({ @@ -12,7 +13,8 @@ vi.mock('./utils.js', () => ({ tv: vi.fn(), requestInput: (de, du, field) => de[field], requestInputID: (de, du, field) => de[`enum_${field}_id`], - bug_for_bug_compat: false + bug_for_bug_compat: false, + compat_for_corpus_analysis: false })); vi.mock('./5_conso_ventilation.js', () => ({ @@ -301,10 +303,12 @@ describe('calc_ventilation - orchestration', () => { */ describe('calc_ventilation - compatibilité bug_for_bug_compat activée', () => { /** - * Recharge le module avec `bug_for_bug_compat = true`. Le double de `tv` renvoie une ligne - * `q4pa_conv` distincte selon que le matcher cible l'identifiant déjà saisi ou le matcher complet. + * Recharge le module avec `bug_for_bug_compat = true` et `compat_for_corpus_analysis` (vrai par + * défaut : reprise de la ligne du DPE pour l'analyse de corpus). Le double de `tv` renvoie une + * ligne `q4pa_conv` distincte selon que le matcher cible l'identifiant déjà saisi ou le matcher + * complet. */ - async function chargerAvecBug(rowParId) { + async function chargerAvecBug(rowParId, compat = true) { vi.resetModules(); const tvBug = vi.fn((table, matcher) => { if (table === 'debits_ventilation') return { ...ROW_DEBITS }; @@ -318,7 +322,8 @@ describe('calc_ventilation - compatibilité bug_for_bug_compat activée', () => tv: tvBug, requestInput: (de, du, field) => de[field], requestInputID: (de, du, field) => de[`enum_${field}_id`], - bug_for_bug_compat: true + bug_for_bug_compat: true, + compat_for_corpus_analysis: compat })); vi.doMock('./5_conso_ventilation.js', () => ({ default: vi.fn() })); const { default: calc_ventilation_bug } = await import('./4_ventilation.js'); @@ -625,6 +630,78 @@ describe('calc_ventilation - compatibilité bug_for_bug_compat activée', () => errorSpy.mockRestore(); }); }); + + /** + * Sans l'option `compat_for_corpus_analysis` (comportement par défaut) : la ligne q4pa_conv du DPE + * n'est jamais reprise, q4pa_conv est recalculé selon la méthode. Les écarts sont seulement + * signalés (contrôle de cohérence d'un DPE, scénarios de travaux). + */ + describe('sans compat_for_corpus_analysis : ligne du DPE jamais reprise', () => { + const murNonIsole = [ + { + donnee_intermediaire: { b: 1 }, + donnee_entree: { enum_type_isolation_id: '2', surface_paroi_opaque: 100 } + } + ]; + const vtLigne = (id) => ({ + donnee_entree: { enum_type_ventilation_id: '1', tv_q4pa_conv_id: id, description: 'v' } + }); + + test('ligne « surfaces isolées » de la même période (#312) : isolation et joints recalculés', async () => { + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + const { calc_ventilation_bug, tvBug } = await chargerAvecBug( + { + q4pa_conv: '2', + tv_q4pa_conv_id: '11', + enum_periode_construction_id: '1', + isolation_surfaces: '1', + presence_joints_menuiserie: '1' + }, + false + ); + const baieSansJoint = [{ donnee_entree: { surface_totale_baie: 10, presence_joint: 0 } }]; + + calc_ventilation_bug(vtLigne(11), CG, 'th', 80, 100, murNonIsole, [], [], baieSansJoint); + + expect(tvBug).toHaveBeenLastCalledWith( + 'q4pa_conv', + expect.objectContaining({ isolation_surfaces: '0', presence_joints_menuiserie: '0' }) + ); + expect(errorSpy).toHaveBeenCalledWith( + expect.stringContaining('La valeur recalculée isolation_surfaces = 0 est utilisée') + ); + expect(errorSpy).toHaveBeenCalledWith( + expect.stringContaining('La valeur recalculée presence_joints_menuiserie = 0 est utilisée') + ); + expect(errorSpy).not.toHaveBeenCalledWith(expect.stringContaining('est conservée')); + errorSpy.mockRestore(); + }); + + test('presence_joint non exporté (#294) : joints recalculés à « 0 » et écart signalé', async () => { + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + const { calc_ventilation_bug, tvBug } = await chargerAvecBug( + { + q4pa_conv: '2.5', + tv_q4pa_conv_id: '12', + isolation_surfaces: '0', + presence_joints_menuiserie: '1' + }, + false + ); + const baies = [{ donnee_entree: { surface_totale_baie: 10 } }]; + + calc_ventilation_bug(vtLigne(12), CG, 'th', 80, 100, murNonIsole, [], [], baies); + + expect(tvBug).toHaveBeenLastCalledWith( + 'q4pa_conv', + expect.objectContaining({ isolation_surfaces: '0', presence_joints_menuiserie: '0' }) + ); + expect(errorSpy).toHaveBeenCalledWith( + expect.stringContaining('La valeur recalculée presence_joints_menuiserie = 0 est utilisée') + ); + errorSpy.mockRestore(); + }); + }); }); describe('presenceJointNonRenseignee', () => { diff --git a/src/engine.js b/src/engine.js index 059aa3278..a0d5af37b 100644 --- a/src/engine.js +++ b/src/engine.js @@ -24,6 +24,7 @@ import { containsAnySubstring, isEffetJoule, set_bug_for_bug_compat, + set_compat_for_corpus_analysis, use_enum_as_string, useEnumAsString, xmlParser @@ -58,7 +59,7 @@ export function getVersion() { /** * Run the engine with a full dpe xml content * @param dpeXmlContent {string} A full dpe xml content - * @param options {{sanitize: boolean}?} + * @param options {{sanitize?: boolean, compat_for_corpus_analysis?: boolean}?} voir `calcul_3cl` * @return {FullDpe} */ export function calcul_3cl_xml(dpeXmlContent, options) { @@ -70,12 +71,18 @@ export function calcul_3cl_xml(dpeXmlContent, options) { /** * Run the engine with a javascript plain object dpe * @param inputDpe {FullDpe} A full plain object dpe - * @param options {{sanitize: boolean}?} + * @param options {{sanitize?: boolean, compat_for_corpus_analysis?: boolean}?} + * - `sanitize` (défaut `true` si `options` est absent) : nettoyage du DPE d'entrée ; + * - `compat_for_corpus_analysis` (défaut `false`) : reprend les valeurs choisies par le logiciel de + * saisie qui contredisent les données d'entrée (ligne q4pa_conv, issues #312 et #294), pour + * reproduire les DPE du corpus ADEME. À réserver à l'analyse de corpus : jamais pour le contrôle + * d'un DPE ni pour les scénarios de travaux. * @return {FullDpe} */ export function calcul_3cl(inputDpe, options) { set_bug_for_bug_compat(); if (!options) options = { sanitize: true }; + set_compat_for_corpus_analysis(options.compat_for_corpus_analysis === true); let dpe; if (options.sanitize) { dpe = dpeSanitizerService.execute(inputDpe); diff --git a/src/engine.spec.js b/src/engine.spec.js index 28a0b5e90..e37baffd4 100644 --- a/src/engine.spec.js +++ b/src/engine.spec.js @@ -21,6 +21,7 @@ let useEnumAsStringFlag = false; vi.mock('./utils.js', () => ({ set_bug_for_bug_compat: vi.fn(), + set_compat_for_corpus_analysis: vi.fn(), add_references: vi.fn(), get bug_for_bug_compat() { return bugForBugCompat; @@ -104,6 +105,7 @@ const { collectionCanBeEmpty, containsAnySubstring, isEffetJoule, + set_compat_for_corpus_analysis, useEnumAsString, xmlParser } = await import('./utils.js'); @@ -365,6 +367,52 @@ describe('calcul_3cl - garde-fous précoces', () => { }); }); +/** + * Option `compat_for_corpus_analysis` : reprise des valeurs du logiciel de saisie (ligne q4pa_conv) + * réservée à l'analyse du corpus ADEME. Désactivée par défaut et repositionnée à chaque appel. + */ +describe('calcul_3cl - option compat_for_corpus_analysis', () => { + const dpeNonSupporte = () => ({ administratif: { enum_modele_dpe_id: '99' } }); + + beforeEach(() => { + vi.spyOn(console, 'error').mockImplementation(() => {}); + }); + + test('sans options : désactivée', () => { + calcul_3cl(dpeNonSupporte()); + expect(set_compat_for_corpus_analysis).toHaveBeenCalledExactlyOnceWith(false); + }); + + test('options sans le champ : désactivée', () => { + calcul_3cl(dpeNonSupporte(), { sanitize: false }); + expect(set_compat_for_corpus_analysis).toHaveBeenCalledExactlyOnceWith(false); + }); + + test('valeur non booléenne « truthy » : désactivée (seul true active la reprise)', () => { + calcul_3cl(dpeNonSupporte(), { sanitize: false, compat_for_corpus_analysis: 'true' }); + expect(set_compat_for_corpus_analysis).toHaveBeenCalledExactlyOnceWith(false); + }); + + test('true : activée, sans changer la sanitisation demandée', () => { + const dpe = dpeNonSupporte(); + calcul_3cl(dpe, { sanitize: true, compat_for_corpus_analysis: true }); + expect(set_compat_for_corpus_analysis).toHaveBeenCalledExactlyOnceWith(true); + expect(sanitizeExecute).toHaveBeenCalledWith(dpe); + }); + + test('repositionnée à chaque appel : un appel par défaut après un appel activé la désactive', () => { + calcul_3cl(dpeNonSupporte(), { sanitize: false, compat_for_corpus_analysis: true }); + calcul_3cl(dpeNonSupporte()); + expect(vi.mocked(set_compat_for_corpus_analysis).mock.calls).toEqual([[true], [false]]); + }); + + test('calcul_3cl_xml transmet l’option', () => { + vi.mocked(xmlParser.parse).mockReturnValue({ dpe: dpeNonSupporte() }); + calcul_3cl_xml('', { sanitize: false, compat_for_corpus_analysis: true }); + expect(set_compat_for_corpus_analysis).toHaveBeenCalledExactlyOnceWith(true); + }); +}); + describe('calcul_3cl_xml - garde sur le modèle de DPE', () => { test('parse le XML puis retourne null pour un modèle non supporté', () => { const dpe = { administratif: { enum_modele_dpe_id: '99' } }; diff --git a/src/features/engine/domain/enveloppe/ventilation/deperdition-ventilation.service.js b/src/features/engine/domain/enveloppe/ventilation/deperdition-ventilation.service.js index 668326769..4347ef664 100644 --- a/src/features/engine/domain/enveloppe/ventilation/deperdition-ventilation.service.js +++ b/src/features/engine/domain/enveloppe/ventilation/deperdition-ventilation.service.js @@ -1,7 +1,7 @@ import { DeperditionService } from '../deperdition.service.js'; import { logger } from '../../../../../core/util/logger/log-service.js'; -import { bug_for_bug_compat } from '../../../../../utils.js'; +import { bug_for_bug_compat, compat_for_corpus_analysis } from '../../../../../utils.js'; import { TypeHabitation } from '../../../../dpe/domain/models/type-habitation.model.js'; import { TypeVentilation } from '../../../../dpe/domain/models/type-ventilation.model.js'; @@ -100,12 +100,14 @@ export class DeperditionVentilationService extends DeperditionService { presenceJointsMenuiserie = '1'; } else if ( bug_for_bug_compat && + compat_for_corpus_analysis && presenceJointNonRenseignee && this.tvStore.getQ4paConvById(ventilationDE.tv_q4pa_conv_id) ?.presence_joints_menuiserie === '1' ) { // Issue #294 : aucune menuiserie n'exporte presence_joint, rien ne contredit la ligne - // q4pa_conv « avant 1948 avec joints » retenue par le DPE => on la reprend. + // q4pa_conv « avant 1948 avec joints » retenue par le DPE => on la reprend, uniquement + // pour l'analyse du corpus ADEME (option compat_for_corpus_analysis). presenceJointsMenuiserie = '1'; } } diff --git a/src/features/engine/domain/enveloppe/ventilation/deperdition-ventilation.service.spec.js b/src/features/engine/domain/enveloppe/ventilation/deperdition-ventilation.service.spec.js index f227d59f7..f77d1b7fb 100644 --- a/src/features/engine/domain/enveloppe/ventilation/deperdition-ventilation.service.spec.js +++ b/src/features/engine/domain/enveloppe/ventilation/deperdition-ventilation.service.spec.js @@ -1,11 +1,14 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; -/** `bug_for_bug_compat` (utils.js) piloté par le test. */ -const state = vi.hoisted(() => ({ bug: false })); +/** `bug_for_bug_compat` et `compat_for_corpus_analysis` (utils.js) pilotés par le test. */ +const state = vi.hoisted(() => ({ bug: false, compat: false })); vi.mock('../../../../../utils.js', async (importOriginal) => ({ ...(await importOriginal()), get bug_for_bug_compat() { return state.bug; + }, + get compat_for_corpus_analysis() { + return state.compat; } })); @@ -32,6 +35,7 @@ describe('DeperditionVentilationService.q4paConv', () => { }; service = new DeperditionVentilationService(tvStore); state.bug = false; + state.compat = false; }); test('Q4Pa mesuré saisi : utilisé directement', () => { @@ -52,6 +56,7 @@ describe('DeperditionVentilationService.q4paConv', () => { describe('presence_joint non exporté (issue #294)', () => { test('hors bug_for_bug_compat : la ligne du DPE est ignorée', () => { state.bug = false; + state.compat = true; tvStore.getQ4paConvById.mockReturnValue({ presence_joints_menuiserie: '1' }); service.q4paConv({ tv_q4pa_conv_id: 12 }, ctx, 0, 100, 0, 10, true); @@ -59,8 +64,20 @@ describe('DeperditionVentilationService.q4paConv', () => { expect(tvStore.getQ4paConv).toHaveBeenCalledWith('1', 'APPARTEMENT', '0', undefined); }); - test('bug_for_bug_compat et ligne DPE « avec joints » : joints repris', () => { + test('par défaut (sans compat_for_corpus_analysis) : la ligne du DPE est ignorée', () => { state.bug = true; + state.compat = false; + tvStore.getQ4paConvById.mockReturnValue({ presence_joints_menuiserie: '1' }); + + service.q4paConv({ tv_q4pa_conv_id: 12 }, ctx, 0, 100, 0, 10, true); + + expect(tvStore.getQ4paConvById).not.toHaveBeenCalled(); + expect(tvStore.getQ4paConv).toHaveBeenCalledWith('1', 'APPARTEMENT', '0', undefined); + }); + + test('compat_for_corpus_analysis et ligne DPE « avec joints » : joints repris', () => { + state.bug = true; + state.compat = true; tvStore.getQ4paConvById.mockReturnValue({ presence_joints_menuiserie: '1' }); service.q4paConv({ tv_q4pa_conv_id: 12 }, ctx, 0, 100, 0, 10, true); @@ -69,8 +86,9 @@ describe('DeperditionVentilationService.q4paConv', () => { expect(tvStore.getQ4paConv).toHaveBeenCalledWith('1', 'APPARTEMENT', '0', '1'); }); - test('bug_for_bug_compat mais presence_joint renseigné : pas de reprise', () => { + test('compat_for_corpus_analysis mais presence_joint renseigné : pas de reprise', () => { state.bug = true; + state.compat = true; tvStore.getQ4paConvById.mockReturnValue({ presence_joints_menuiserie: '1' }); service.q4paConv({ tv_q4pa_conv_id: 12 }, ctx, 0, 100, 0, 10, false); @@ -78,8 +96,9 @@ describe('DeperditionVentilationService.q4paConv', () => { expect(tvStore.getQ4paConv).toHaveBeenCalledWith('1', 'APPARTEMENT', '0', undefined); }); - test('bug_for_bug_compat et ligne DPE sans joints : pas de reprise', () => { + test('compat_for_corpus_analysis et ligne DPE sans joints : pas de reprise', () => { state.bug = true; + state.compat = true; tvStore.getQ4paConvById.mockReturnValue({ presence_joints_menuiserie: '' }); service.q4paConv({ tv_q4pa_conv_id: 1 }, ctx, 0, 100, 0, 10, true); @@ -89,6 +108,7 @@ describe('DeperditionVentilationService.q4paConv', () => { test('surfaces majoritairement isolées : le critère des joints ne s’applique pas', () => { state.bug = true; + state.compat = true; tvStore.getQ4paConvById.mockReturnValue({ presence_joints_menuiserie: '1' }); service.q4paConv({ tv_q4pa_conv_id: 12 }, ctx, 100, 0, 0, 10, true); diff --git a/src/utils.js b/src/utils.js index 156a7aa0c..621187988 100644 --- a/src/utils.js +++ b/src/utils.js @@ -40,6 +40,29 @@ export function set_bug_for_bug_compat() { bug_for_bug_compat = true; } +/** + * Compatibilité réservée à l'analyse du corpus ADEME. + * + * Activée, le moteur reprend certaines valeurs choisies par le logiciel de saisie même + * lorsqu'elles contredisent les données d'entrée (ex. ligne `tv_q4pa_conv_id` incohérente avec + * l'isolation des parois ou les joints des menuiseries, issues #312 et #294), dans le seul but de + * reproduire les résultats des DPE du corpus. + * + * Ne jamais l'activer pour contrôler la cohérence d'un DPE ni pour calculer des scénarios de + * travaux : l'erreur du logiciel de saisie serait alors masquée ou propagée. + * + * Positionnée à chaque appel de `calcul_3cl` à partir de l'option du même nom. + * @type {boolean} + */ +export let compat_for_corpus_analysis = false; + +/** + * @param value {boolean} `true` uniquement pour l'analyse du corpus ADEME + */ +export function set_compat_for_corpus_analysis(value = false) { + compat_for_corpus_analysis = value === true; +} + export function useEnumAsString(jsonObj) { for (const key in jsonObj) { if (jsonObj.hasOwnProperty(key)) { diff --git a/src/utils.spec.js b/src/utils.spec.js index fd3eca768..e4ab56f7b 100644 --- a/src/utils.spec.js +++ b/src/utils.spec.js @@ -35,6 +35,7 @@ vi.mock('./tv.js', () => ({ const { xmlParser, set_bug_for_bug_compat, + set_compat_for_corpus_analysis, useEnumAsString, set_use_enum_as_string, set_tv_match_optimized_version, @@ -171,6 +172,20 @@ describe('Utils unit tests', () => { expect(utils.bug_for_bug_compat).toBe(true); }); + test('compat_for_corpus_analysis : désactivé par défaut, seul true l’active', () => { + expect(utils.compat_for_corpus_analysis).toBe(false); + set_compat_for_corpus_analysis(true); + expect(utils.compat_for_corpus_analysis).toBe(true); + set_compat_for_corpus_analysis(); + expect(utils.compat_for_corpus_analysis).toBe(false); + set_compat_for_corpus_analysis(true); + set_compat_for_corpus_analysis('true'); + expect(utils.compat_for_corpus_analysis).toBe(false); + set_compat_for_corpus_analysis(true); + set_compat_for_corpus_analysis(false); + expect(utils.compat_for_corpus_analysis).toBe(false); + }); + test('set_use_enum_as_string active le drapeau', () => { set_use_enum_as_string(); expect(utils.use_enum_as_string).toBe(true); diff --git a/test/corpus/corpus_worker.mjs b/test/corpus/corpus_worker.mjs index 0a0937ed4..e3805cf4e 100644 --- a/test/corpus/corpus_worker.mjs +++ b/test/corpus/corpus_worker.mjs @@ -125,7 +125,9 @@ const runEngineAndVerifyOutput = (inputDpe, dpeOutputs) => { */ const dpeToCompute = structuredClone(inputDpe); const startTime = hrtime.bigint(); - outputDpe = calcul_3cl(dpeToCompute); + // Analyse du corpus ADEME : reprise des valeurs choisies par le logiciel de saisie (ligne + // q4pa_conv) pour reproduire les résultats des DPE. + outputDpe = calcul_3cl(dpeToCompute, { sanitize: true, compat_for_corpus_analysis: true }); parentPort.postMessage({ action: 'addDpeElapsedTime', elapsedMs: Number(hrtime.bigint() - startTime) / 1e6 diff --git a/test/open3cl-bug-tribu-zone-2hd.spec.js b/test/open3cl-bug-tribu-zone-2hd.spec.js index 0a29c3e3f..c36390be5 100644 --- a/test/open3cl-bug-tribu-zone-2hd.spec.js +++ b/test/open3cl-bug-tribu-zone-2hd.spec.js @@ -1,5 +1,5 @@ import { describe, expect, test } from 'vitest'; -import { getAdemeFileJson } from './test-helpers.js'; +import { getAdemeFileJson, CORPUS_ANALYSIS_OPTIONS } from './test-helpers.js'; import { calcul_3cl } from '../src/index.js'; describe('Unit tests for bug tribu about dh19 value for 2hd zone', () => { @@ -10,7 +10,7 @@ describe('Unit tests for bug tribu about dh19 value for 2hd zone', () => { input.logement.installation_chauffage_collection.installation_chauffage[0]; /** @type {FullDpe} **/ - let output = calcul_3cl(structuredClone(input)); + let output = calcul_3cl(structuredClone(input), CORPUS_ANALYSIS_OPTIONS); const outputGenerateurChauffage = output.logement.installation_chauffage_collection.installation_chauffage[0]; diff --git a/test/open3cl-chaudiere-collective-pn.spec.js b/test/open3cl-chaudiere-collective-pn.spec.js index e2b691153..7f6768346 100644 --- a/test/open3cl-chaudiere-collective-pn.spec.js +++ b/test/open3cl-chaudiere-collective-pn.spec.js @@ -1,5 +1,5 @@ import { calcul_3cl } from '../src/engine.js'; -import { getAdemeFileJson } from './test-helpers.js'; +import { getAdemeFileJson, CORPUS_ANALYSIS_OPTIONS } from './test-helpers.js'; import { set_bug_for_bug_compat, set_tv_match_optimized_version, @@ -38,7 +38,7 @@ describe('Chaudière collective : puissance nominale saisie selon les logiciels test('2595E3549425E (LICIEL) : pn saisi en Pe, convention 3CL conservée et conso conforme', () => { const input = getAdemeFileJson('2595E3549425E'); - const output = calcul_3cl(structuredClone(input)); + const output = calcul_3cl(structuredClone(input), CORPUS_ANALYSIS_OPTIONS); const generateursDpe = generateursChauffage(input); const generateursCalcules = generateursChauffage(output); @@ -67,7 +67,7 @@ describe('Chaudière collective : puissance nominale saisie selon les logiciels test('2592E2935275X (WinDpe) : pn saisi ni en Pe ni en Pn, puissance recalculée depuis rpn', () => { const input = getAdemeFileJson('2592E2935275X'); - const output = calcul_3cl(structuredClone(input)); + const output = calcul_3cl(structuredClone(input), CORPUS_ANALYSIS_OPTIONS); /** * rpn du DPE = 0,938426 = (91 + log10(Pn)) / 100 -> Pn(collectif) = 696 kW, soit @@ -123,7 +123,7 @@ describe('Chaudière collective : puissance nominale saisie selon les logiciels [...generateursChauffage(corrige), ...generateursEcs(corrige)].forEach((generateur) => { generateur.donnee_intermediaire.pn = pe; }); - const output = calcul_3cl(corrige); + const output = calcul_3cl(corrige, CORPUS_ANALYSIS_OPTIONS); const diDpe = generateursChauffage(input)[0].donnee_intermediaire; const di = generateursChauffage(output)[0].donnee_intermediaire; diff --git a/test/open3cl-coeff-1-7.spec.js b/test/open3cl-coeff-1-7.spec.js index 00c330ea7..56101ea8d 100644 --- a/test/open3cl-coeff-1-7.spec.js +++ b/test/open3cl-coeff-1-7.spec.js @@ -1,11 +1,11 @@ import { describe, expect, test } from 'vitest'; -import { getAdemeFileJsonOrDownload } from './test-helpers.js'; +import { getAdemeFileJsonOrDownload, CORPUS_ANALYSIS_OPTIONS } from './test-helpers.js'; import { calcul_3cl } from '../src/index.js'; describe('Open3cl misc unit tests', () => { test('should calculate primary conso with coeff 1.7 (since janvier 2027) for dpe: 2369E2791083C', async () => { const inputDpe = await getAdemeFileJsonOrDownload('2369E2791083C'); - const outputDpe = calcul_3cl(structuredClone(inputDpe)); + const outputDpe = calcul_3cl(structuredClone(inputDpe), CORPUS_ANALYSIS_OPTIONS); expect(outputDpe.logement.sortie.ep_conso.coeff_1_7_classe_bilan_dpe).toBe('F'); expect(outputDpe.logement.sortie.ep_conso.classe_bilan_dpe).toBe('F'); expect(outputDpe.logement.sortie.ep_conso.classe_bilan_dpe_2027).toBe('F'); @@ -25,7 +25,7 @@ describe('Open3cl misc unit tests', () => { test('should calculate primary conso with coeff 1.7 (since janvier 2027) for dpe: 2513E1166911W', async () => { const inputDpe = await getAdemeFileJsonOrDownload('2513E1166911W'); - const outputDpe = calcul_3cl(structuredClone(inputDpe)); + const outputDpe = calcul_3cl(structuredClone(inputDpe), CORPUS_ANALYSIS_OPTIONS); expect(outputDpe.logement.sortie.ep_conso.coeff_1_7_classe_bilan_dpe).toBe('B'); expect(outputDpe.logement.sortie.ep_conso.classe_bilan_dpe).toBe('B'); expect(outputDpe.logement.sortie.ep_conso.classe_bilan_dpe_2027).toBe('B'); @@ -45,7 +45,7 @@ describe('Open3cl misc unit tests', () => { test('should calculate primary conso with coeff 1.7 (since janvier 2027) for dpe: 2528E1249844E', async () => { const inputDpe = await getAdemeFileJsonOrDownload('2528E1249844E'); - const outputDpe = calcul_3cl(structuredClone(inputDpe)); + const outputDpe = calcul_3cl(structuredClone(inputDpe), CORPUS_ANALYSIS_OPTIONS); expect(outputDpe.logement.sortie.ep_conso.coeff_1_7_classe_bilan_dpe).toBe('A'); expect(outputDpe.logement.sortie.ep_conso.classe_bilan_dpe).toBe('A'); expect(outputDpe.logement.sortie.ep_conso.classe_bilan_dpe_2027).toBe('A'); @@ -65,7 +65,7 @@ describe('Open3cl misc unit tests', () => { test('should calculate primary conso with coeff 1.7 (since janvier 2027) for dpe: 2464E1799476F', async () => { const inputDpe = await getAdemeFileJsonOrDownload('2464E1799476F'); - const outputDpe = calcul_3cl(structuredClone(inputDpe)); + const outputDpe = calcul_3cl(structuredClone(inputDpe), CORPUS_ANALYSIS_OPTIONS); expect(outputDpe.logement.sortie.ep_conso.coeff_1_7_ep_conso_5_usages).toBeCloseTo( 33669.79, 0.5 diff --git a/test/open3cl-periode-emetteur-temp-fonc.spec.js b/test/open3cl-periode-emetteur-temp-fonc.spec.js index ba451a642..efcbcc2b1 100644 --- a/test/open3cl-periode-emetteur-temp-fonc.spec.js +++ b/test/open3cl-periode-emetteur-temp-fonc.spec.js @@ -1,5 +1,5 @@ import { calcul_3cl } from '../src/engine.js'; -import { getAdemeFileJson } from './test-helpers.js'; +import { getAdemeFileJson, CORPUS_ANALYSIS_OPTIONS } from './test-helpers.js'; import { set_bug_for_bug_compat, set_tv_match_optimized_version, @@ -34,7 +34,7 @@ describe("Période d'installation des émetteurs déduite des temp_fonc du DPE ( test('2459E0028946S (4.1.1, construction 1920, chaudière hybride) : 32/60 (« après 2000 ») au lieu de 38/80', () => { const input = getAdemeFileJson('2459E0028946S'); - const output = calcul_3cl(structuredClone(input)); + const output = calcul_3cl(structuredClone(input), CORPUS_ANALYSIS_OPTIONS); const diDpe = generateursChauffage(input)[0].donnee_intermediaire; const generateur = generateursChauffage(output)[0]; @@ -56,7 +56,7 @@ describe("Période d'installation des émetteurs déduite des temp_fonc du DPE ( test('2287E0373232M (LICIEL, construction 1947, chaudière fioul) : 52,5/70 au lieu de 55,5/80', () => { const input = getAdemeFileJson('2287E0373232M'); - const output = calcul_3cl(structuredClone(input)); + const output = calcul_3cl(structuredClone(input), CORPUS_ANALYSIS_OPTIONS); const diDpe = generateursChauffage(input)[0].donnee_intermediaire; const generateur = generateursChauffage(output)[0]; diff --git a/test/open3cl-perte-recup-generateur-ecs-seul.spec.js b/test/open3cl-perte-recup-generateur-ecs-seul.spec.js index bde133109..cda5e18c5 100644 --- a/test/open3cl-perte-recup-generateur-ecs-seul.spec.js +++ b/test/open3cl-perte-recup-generateur-ecs-seul.spec.js @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, test } from 'vitest'; -import { getAdemeFileJson } from './test-helpers.js'; +import { getAdemeFileJson, CORPUS_ANALYSIS_OPTIONS } from './test-helpers.js'; import { calcul_3cl } from '../src/index.js'; import { set_bug_for_bug_compat } from '../src/utils.js'; @@ -24,7 +24,7 @@ describe('Pertes récupérées d’un générateur ECS seul - issue #153', () => /** @type {FullDpe} */ const input = getAdemeFileJson('2630E0016701S'); /** @type {FullDpe} */ - const output = calcul_3cl(structuredClone(input)); + const output = calcul_3cl(structuredClone(input), CORPUS_ANALYSIS_OPTIONS); const abIn = input.logement.sortie.apport_et_besoin; const abOut = output.logement.sortie.apport_et_besoin; diff --git a/test/open3cl-q4pa-ligne-dpe.spec.js b/test/open3cl-q4pa-ligne-dpe.spec.js index bc4a66d4f..24caaa90f 100644 --- a/test/open3cl-q4pa-ligne-dpe.spec.js +++ b/test/open3cl-q4pa-ligne-dpe.spec.js @@ -1,5 +1,5 @@ import { describe, expect, test } from 'vitest'; -import { getAdemeFileJson } from './test-helpers.js'; +import { getAdemeFileJson, CORPUS_ANALYSIS_OPTIONS } from './test-helpers.js'; import { calcul_3cl } from '../src/index.js'; import { set_bug_for_bug_compat, set_tv_match_optimized_version } from '../src/utils.js'; @@ -7,16 +7,18 @@ import { set_bug_for_bug_compat, set_tv_match_optimized_version } from '../src/u * Non-régression https://github.com/Open3CL/engine/issues/312 * * Le DPE 2287E1201982M (LICIEL) retient la ligne q4pa_conv 10 « avant 1948, surfaces isolées » - * (q4pa_conv = 2) alors que toutes ses parois sont non isolées (ligne 1 : 4,6). En mode - * bug_for_bug_compat, la ligne du DPE est reprise : q4pa_conv, hperm et conso_ch sont ceux du DPE. + * (q4pa_conv = 2) alors que toutes ses parois sont non isolées (ligne 1 : 4,6). + * - option `compat_for_corpus_analysis` : la ligne du DPE est reprise, q4pa_conv, hperm et conso_ch + * sont ceux du DPE (analyse du corpus ADEME) ; + * - par défaut : q4pa_conv est recalculé selon la méthode (contrôle de cohérence du DPE). */ -describe('q4pa_conv repris de la ligne du DPE - issue #312', () => { - test('2287E1201982M : q4pa_conv, hperm et conso_ch identiques au DPE', () => { +describe('q4pa_conv et ligne tv_q4pa_conv_id du DPE - issue #312', () => { + test('2287E1201982M avec compat_for_corpus_analysis : q4pa_conv, hperm et conso_ch identiques au DPE', () => { set_bug_for_bug_compat(); set_tv_match_optimized_version(); /** @type {FullDpe} */ const input = getAdemeFileJson('2287E1201982M'); - const output = calcul_3cl(structuredClone(input)); + const output = calcul_3cl(structuredClone(input), CORPUS_ANALYSIS_OPTIONS); const ventIn = input.logement.ventilation_collection.ventilation[0].donnee_intermediaire; const ventOut = output.logement.ventilation_collection.ventilation[0].donnee_intermediaire; @@ -28,10 +30,33 @@ describe('q4pa_conv repris de la ligne du DPE - issue #312', () => { ); }); + test('2287E1201982M par défaut : q4pa_conv recalculé selon la méthode (parois non isolées)', () => { + set_bug_for_bug_compat(); + set_tv_match_optimized_version(); + /** @type {FullDpe} */ + const input = getAdemeFileJson('2287E1201982M'); + const output = calcul_3cl(structuredClone(input)); + + const ventOut = output.logement.ventilation_collection.ventilation[0].donnee_intermediaire; + expect(ventOut.q4pa_conv).toBe(4.6); + }); + + test('l’option n’est pas conservée d’un appel à l’autre', () => { + set_bug_for_bug_compat(); + set_tv_match_optimized_version(); + /** @type {FullDpe} */ + const input = getAdemeFileJson('2287E1201982M'); + calcul_3cl(structuredClone(input), CORPUS_ANALYSIS_OPTIONS); + const output = calcul_3cl(structuredClone(input), { sanitize: true }); + + const ventOut = output.logement.ventilation_collection.ventilation[0].donnee_intermediaire; + expect(ventOut.q4pa_conv).toBe(4.6); + }); + /** - * Sens unique : le DPE 2469E2650374G retient la ligne q4pa_conv 1 « avant 1948, surfaces non - * isolées » (4,6). Un scénario de rénovation isole tous ses murs puis recalcule le DPE modifié : - * la ligne du DPE (non isolée) contredit alors les parois isolées et ne doit pas être reprise. + * Le DPE 2469E2650374G retient la ligne q4pa_conv 1 « avant 1948, surfaces non isolées » (4,6). + * Un scénario de rénovation (calcul par défaut) isole tous ses murs puis recalcule le DPE + * modifié : la ligne du DPE contredit alors les parois isolées et n'est pas reprise. */ test('2469E2650374G murs isolés : la ligne non isolée du DPE n’est pas reprise', () => { set_bug_for_bug_compat(); diff --git a/test/open3cl-qp0-ventilateur-combustion.spec.js b/test/open3cl-qp0-ventilateur-combustion.spec.js index b9000dae4..3e2b5639b 100644 --- a/test/open3cl-qp0-ventilateur-combustion.spec.js +++ b/test/open3cl-qp0-ventilateur-combustion.spec.js @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, test } from 'vitest'; -import { getAdemeFileJson } from './test-helpers.js'; +import { getAdemeFileJson, CORPUS_ANALYSIS_OPTIONS } from './test-helpers.js'; import { calcul_3cl } from '../src/index.js'; import { set_bug_for_bug_compat } from '../src/utils.js'; @@ -25,7 +25,7 @@ describe('Qp0 selon le ventilateur de combustion - issue #35', () => { /** @type {FullDpe} */ const input = getAdemeFileJson('2459E3195669T'); /** @type {FullDpe} */ - const output = calcul_3cl(structuredClone(input)); + const output = calcul_3cl(structuredClone(input), CORPUS_ANALYSIS_OPTIONS); const chaudiere = (dpe) => [dpe.logement.installation_chauffage_collection.installation_chauffage] diff --git a/test/open3cl-rcu-ecs-isole.spec.js b/test/open3cl-rcu-ecs-isole.spec.js index 0206a96f2..99dbeb648 100644 --- a/test/open3cl-rcu-ecs-isole.spec.js +++ b/test/open3cl-rcu-ecs-isole.spec.js @@ -1,5 +1,5 @@ import { describe, expect, test } from 'vitest'; -import { getAdemeFileJson } from './test-helpers.js'; +import { getAdemeFileJson, CORPUS_ANALYSIS_OPTIONS } from './test-helpers.js'; import { calcul_3cl } from '../src/index.js'; /** @@ -18,7 +18,7 @@ describe('ECS RCU isolé / non isolé - issue #150', () => { test('rendement_generation_stockage doit être proche entre DPE entrée et DPE sortie pour 2694E0480609U', () => { /** @type {FullDpe} */ const input = getAdemeFileJson('2694E0480609U'); - const output = calcul_3cl(structuredClone(input)); + const output = calcul_3cl(structuredClone(input), CORPUS_ANALYSIS_OPTIONS); expect( input.logement.installation_ecs_collection.installation_ecs[0].generateur_ecs_collection diff --git a/test/open3cl-rcu.spec.js b/test/open3cl-rcu.spec.js index 7b74d0260..694834e89 100644 --- a/test/open3cl-rcu.spec.js +++ b/test/open3cl-rcu.spec.js @@ -1,5 +1,5 @@ import { describe, expect, test } from 'vitest'; -import { getAdemeFileJson } from './test-helpers.js'; +import { getAdemeFileJson, CORPUS_ANALYSIS_OPTIONS } from './test-helpers.js'; import { calcul_3cl } from '../src/index.js'; describe('RCU unit tests', () => { @@ -8,7 +8,7 @@ describe('RCU unit tests', () => { let input = getAdemeFileJson('2569E2704431K'); /** @type {FullDpe} **/ - let output = calcul_3cl(structuredClone(input)); + let output = calcul_3cl(structuredClone(input), CORPUS_ANALYSIS_OPTIONS); expect( output.logement.sortie.emission_ges.emission_ges_5_usages_m2 - input.logement.sortie.emission_ges.emission_ges_5_usages_m2 diff --git a/test/open3cl-sorties-dpe-immeuble.spec.js b/test/open3cl-sorties-dpe-immeuble.spec.js index a2f0d17a3..9fdd88e22 100644 --- a/test/open3cl-sorties-dpe-immeuble.spec.js +++ b/test/open3cl-sorties-dpe-immeuble.spec.js @@ -1,5 +1,9 @@ import { describe, expect, test } from 'vitest'; -import { getAdemeFileJson, getAdemeFileJsonOrDownload } from './test-helpers.js'; +import { + getAdemeFileJson, + getAdemeFileJsonOrDownload, + CORPUS_ANALYSIS_OPTIONS +} from './test-helpers.js'; import { calcul_3cl } from '../src/index.js'; import { expect_or } from './utils.js'; @@ -26,7 +30,7 @@ describe('DPE immeuble unit tests', () => { const input = getAdemeFileJson(dpe); /** @type {FullDpe} **/ - const output = calcul_3cl(structuredClone(input)); + const output = calcul_3cl(structuredClone(input), CORPUS_ANALYSIS_OPTIONS); const diffBch = (Math.abs( @@ -71,7 +75,7 @@ describe('DPE immeuble unit tests', () => { let input = getAdemeFileJson(code); /** @type {FullDpe} **/ - let output = calcul_3cl(structuredClone(input)); + let output = calcul_3cl(structuredClone(input), CORPUS_ANALYSIS_OPTIONS); const inputEcsRecup = input.logement.sortie.apport_et_besoin.pertes_distribution_ecs_recup; const outputEcsRecup = output.logement.sortie.apport_et_besoin.pertes_distribution_ecs_recup; @@ -92,7 +96,7 @@ describe('DPE immeuble unit tests', () => { .generateur_chauffage_collection.generateur_chauffage[0]; /** @type {FullDpe} **/ - let output = calcul_3cl(structuredClone(input)); + let output = calcul_3cl(structuredClone(input), CORPUS_ANALYSIS_OPTIONS); const outputGenerateurChauffage = output.logement.installation_chauffage_collection.installation_chauffage[0] .generateur_chauffage_collection.generateur_chauffage[0]; @@ -108,7 +112,7 @@ describe('DPE immeuble unit tests', () => { let input = await getAdemeFileJsonOrDownload('2474E3798234X'); /** @type {FullDpe} **/ - let output = calcul_3cl(structuredClone(input)); + let output = calcul_3cl(structuredClone(input), CORPUS_ANALYSIS_OPTIONS); expect(input.logement.sortie.ep_conso.ep_conso_5_usages_m2).toBeCloseTo( output.logement.sortie.ep_conso.ep_conso_5_usages_m2 @@ -120,7 +124,7 @@ describe('DPE immeuble unit tests', () => { let input = await getAdemeFileJsonOrDownload('2313E0178624T'); /** @type {FullDpe} **/ - let output = calcul_3cl(structuredClone(input)); + let output = calcul_3cl(structuredClone(input), CORPUS_ANALYSIS_OPTIONS); const inputGenerateurChauffage = input.logement.installation_ecs_collection.installation_ecs[0].generateur_ecs_collection @@ -138,7 +142,7 @@ describe('DPE immeuble unit tests', () => { let input = await getAdemeFileJsonOrDownload('2574E3388253L'); /** @type {FullDpe} **/ - let output = calcul_3cl(structuredClone(input)); + let output = calcul_3cl(structuredClone(input), CORPUS_ANALYSIS_OPTIONS); const inputGenerateurChauffage = input.logement.installation_chauffage_collection.installation_chauffage[0] diff --git a/test/open3cl-sorties-ventilation.spec.js b/test/open3cl-sorties-ventilation.spec.js index 572a0bdb7..aa2c06ab1 100644 --- a/test/open3cl-sorties-ventilation.spec.js +++ b/test/open3cl-sorties-ventilation.spec.js @@ -1,4 +1,8 @@ -import { getAdemeFileJson, getAdemeFileJsonOrDownload } from './test-helpers.js'; +import { + getAdemeFileJson, + getAdemeFileJsonOrDownload, + CORPUS_ANALYSIS_OPTIONS +} from './test-helpers.js'; import { calcul_3cl } from '../src/index.js'; import { describe, expect, test, beforeEach } from 'vitest'; import { set_bug_for_bug_compat } from '../src/utils.js'; @@ -11,7 +15,8 @@ describe('calcul de déperdition par ventilation', () => { test('Les surfaces de déperdition ne doivent pas prendre en compte les surfaces déperditives (b > 0)', () => { const inputDpe = getAdemeFileJson('2213E0696993Z'); - const deperdition = calcul_3cl(structuredClone(inputDpe)).logement.sortie.deperdition; + const deperdition = calcul_3cl(structuredClone(inputDpe), CORPUS_ANALYSIS_OPTIONS).logement + .sortie.deperdition; expect(deperdition.hperm).toBe(5.21254089805847); expect(deperdition.hvent).toBe(58.352976000000005); }); @@ -19,7 +24,7 @@ describe('calcul de déperdition par ventilation', () => { test('Conso vmc sf gaz', async () => { const inputDpe = await getAdemeFileJsonOrDownload('2578E1899852K'); - const outputDpe = calcul_3cl(structuredClone(inputDpe)); + const outputDpe = calcul_3cl(structuredClone(inputDpe), CORPUS_ANALYSIS_OPTIONS); expect(inputDpe.logement.sortie.ep_conso.ep_conso_auxiliaire_ventilation).toBeCloseTo( outputDpe.logement.sortie.ep_conso.ep_conso_auxiliaire_ventilation ); @@ -28,7 +33,7 @@ describe('calcul de déperdition par ventilation', () => { test('q4paconv si 50% surface presence joint', async () => { const inputDpe = await getAdemeFileJsonOrDownload('2475E0480474U'); - const outputDpe = calcul_3cl(structuredClone(inputDpe)); + const outputDpe = calcul_3cl(structuredClone(inputDpe), CORPUS_ANALYSIS_OPTIONS); expect( inputDpe.logement.ventilation_collection.ventilation[0].donnee_intermediaire.q4pa_conv ).toBeCloseTo( diff --git a/test/open3cl_conso_auxiliaire_distribution_ecs.spec.js b/test/open3cl_conso_auxiliaire_distribution_ecs.spec.js index eb68d14a2..6ddafb6e8 100644 --- a/test/open3cl_conso_auxiliaire_distribution_ecs.spec.js +++ b/test/open3cl_conso_auxiliaire_distribution_ecs.spec.js @@ -1,5 +1,5 @@ import { calcul_3cl } from '../src/engine.js'; -import { getAdemeFileJsonOrDownload } from './test-helpers.js'; +import { getAdemeFileJsonOrDownload, CORPUS_ANALYSIS_OPTIONS } from './test-helpers.js'; import { set_bug_for_bug_compat } from '../src/utils.js'; import { describe, expect, test } from 'vitest'; @@ -19,7 +19,7 @@ describe('Conso auxiliaire distribution ECS - Cas immeuble (issue #157)', () => test('DPE immeuble bouclé (2592E0278308W) : conso auxiliaire distribution ECS ≈ 400.53 kWh', async () => { set_bug_for_bug_compat(); const input = await getAdemeFileJsonOrDownload('2592E0278308W'); - const output = calcul_3cl(structuredClone(input)); + const output = calcul_3cl(structuredClone(input), CORPUS_ANALYSIS_OPTIONS); // La conso totale de l'auxiliaire de distribution ECS pour l'immeuble // est la somme des deux installations (400.5335739 kWh) @@ -47,7 +47,7 @@ describe('Conso auxiliaire distribution ECS - Cas immeuble (issue #157)', () => test('DPE immeuble traçage (2231E1326114Q) : conso auxiliaire distribution ECS ≈ 2504.61 kWh', async () => { set_bug_for_bug_compat(); const input = await getAdemeFileJsonOrDownload('2231E1326114Q'); - const output = calcul_3cl(structuredClone(input)); + const output = calcul_3cl(structuredClone(input), CORPUS_ANALYSIS_OPTIONS); const ef_conso = output.logement.sortie.ef_conso; expect(ef_conso.conso_auxiliaire_distribution_ecs).toBeCloseTo(2504.61, 0); @@ -67,7 +67,7 @@ describe('Conso auxiliaire distribution ECS - Cas immeuble (issue #157)', () => set_bug_for_bug_compat(); const input = await getAdemeFileJsonOrDownload('2321E0998807O'); // Ce DPE a enum_bouclage=2 (bouclé), on vérifie juste que la valeur est > 0 - const output = calcul_3cl(structuredClone(input)); + const output = calcul_3cl(structuredClone(input), CORPUS_ANALYSIS_OPTIONS); const ecs_list = output.logement.installation_ecs_collection.installation_ecs; const bouclage = ecs_list[0]?.donnee_entree?.enum_bouclage_reseau_ecs_id; if (String(bouclage) === '1') { diff --git a/test/open3cl_ecs.spec.js b/test/open3cl_ecs.spec.js index 18cb084a7..3b3b875d4 100644 --- a/test/open3cl_ecs.spec.js +++ b/test/open3cl_ecs.spec.js @@ -1,5 +1,5 @@ import { calcul_3cl } from '../src/engine.js'; -import { getAdemeFileJsonOrDownload } from './test-helpers.js'; +import { getAdemeFileJsonOrDownload, CORPUS_ANALYSIS_OPTIONS } from './test-helpers.js'; import { set_bug_for_bug_compat } from '../src/utils.js'; import { describe, expect, test } from 'vitest'; @@ -11,7 +11,7 @@ describe('Ecs unit tests', () => { let input = await getAdemeFileJsonOrDownload(dpeCode); /** @type {FullDpe} **/ - let output = calcul_3cl(structuredClone(input)); + let output = calcul_3cl(structuredClone(input), CORPUS_ANALYSIS_OPTIONS); expect(input.logement.sortie.ep_conso.ep_conso_ecs).toBeCloseTo( output.logement.sortie.ep_conso.ep_conso_ecs, diff --git a/test/open3cl_ecs_auxiliaires.spec.js b/test/open3cl_ecs_auxiliaires.spec.js index 1a59aeae5..54edecb23 100644 --- a/test/open3cl_ecs_auxiliaires.spec.js +++ b/test/open3cl_ecs_auxiliaires.spec.js @@ -1,5 +1,5 @@ import { calcul_3cl } from '../src/engine.js'; -import { getAdemeFileJsonOrDownload } from './test-helpers.js'; +import { getAdemeFileJsonOrDownload, CORPUS_ANALYSIS_OPTIONS } from './test-helpers.js'; import { set_bug_for_bug_compat } from '../src/utils.js'; import { describe, expect, test } from 'vitest'; @@ -9,7 +9,7 @@ describe('Ecs auxiliaires unit tests', () => { let input = await getAdemeFileJsonOrDownload('2674E0057251A'); /** @type {FullDpe} **/ - let output = calcul_3cl(structuredClone(input)); + let output = calcul_3cl(structuredClone(input), CORPUS_ANALYSIS_OPTIONS); expect(input.logement.sortie.ef_conso.conso_auxiliaire_distribution_ecs).toBeCloseTo( output.logement.sortie.ef_conso.conso_auxiliaire_distribution_ecs diff --git a/test/open3cl_misc.spec.js b/test/open3cl_misc.spec.js index 798809c60..1a3a63beb 100644 --- a/test/open3cl_misc.spec.js +++ b/test/open3cl_misc.spec.js @@ -1,10 +1,13 @@ import { calcul_3cl } from '../src/engine.js'; -import { getAdemeFileJson } from './test-helpers.js'; +import { getAdemeFileJson, CORPUS_ANALYSIS_OPTIONS } from './test-helpers.js'; import { describe, expect, test } from 'vitest'; describe('Open3cl misc unit tests', () => { test('should match ujn in tv with a two digits precision uw number', () => { - const output = calcul_3cl(structuredClone(getAdemeFileJson('2302E4043473J'))); + const output = calcul_3cl( + structuredClone(getAdemeFileJson('2302E4043473J')), + CORPUS_ANALYSIS_OPTIONS + ); /** * Si uw_saisi : 2.35 ce ne match aucune valeur dans le fichier tv.js (le plus proche, c'est 2.3 ou 2.4). @@ -16,7 +19,10 @@ describe('Open3cl misc unit tests', () => { }); test('regular expression with "+" character should be accepted for isolation ITE + ITI', () => { - const output = calcul_3cl(structuredClone(getAdemeFileJson('2302E4043473J'))); + const output = calcul_3cl( + structuredClone(getAdemeFileJson('2302E4043473J')), + CORPUS_ANALYSIS_OPTIONS + ); /** * Erreur d'expression régulière sur le type d'isolation, si ITE+ITI le caractère "+" @@ -35,7 +41,10 @@ describe('Open3cl misc unit tests', () => { * Or, on recherchait systématiquement une correspondance dans cet onglet * avec une classe d'inertie ce qui ne matchait aucune correspondance pour les immeubles. */ - const output = calcul_3cl(structuredClone(getAdemeFileJson('2344E0308327N'))); + const output = calcul_3cl( + structuredClone(getAdemeFileJson('2344E0308327N')), + CORPUS_ANALYSIS_OPTIONS + ); expect( output.logement.installation_chauffage_collection.installation_chauffage[0] .emetteur_chauffage_collection.emetteur_chauffage[0].donnee_intermediaire @@ -48,7 +57,7 @@ describe('Open3cl misc unit tests', () => { input.logement.enveloppe.plancher_haut_collection.plancher_haut[0].donnee_entree.enum_methode_saisie_u_id = 9; input.logement.enveloppe.plancher_haut_collection.plancher_haut[0].donnee_entree.uph_saisi = 0.18; - const output = calcul_3cl(structuredClone(input)); + const output = calcul_3cl(structuredClone(input), CORPUS_ANALYSIS_OPTIONS); expect( output.logement.enveloppe.plancher_haut_collection.plancher_haut[0].donnee_intermediaire ).toMatchObject({ b: 0, uph: 0.18 }); @@ -66,7 +75,10 @@ describe('Open3cl misc unit tests', () => { }); test('should be able to process a dpe with empty plancher_bas_collection and plancher_haut_collection', () => { - const output = calcul_3cl(structuredClone(getAdemeFileJson('2421E0125604W'))); + const output = calcul_3cl( + structuredClone(getAdemeFileJson('2421E0125604W')), + CORPUS_ANALYSIS_OPTIONS + ); expect(output.logement.enveloppe.plancher_bas_collection).toEqual({ plancher_bas: [] }); expect(output.logement.enveloppe.plancher_haut_collection).toEqual({ plancher_haut: [] }); @@ -78,7 +90,7 @@ describe('Open3cl misc unit tests', () => { const input = structuredClone(getAdemeFileJson('2421E0125604W')); const inputTvMurId = input.logement.enveloppe.mur_collection.mur[0].donnee_entree.tv_umur_id; - const output = calcul_3cl(structuredClone(input)); + const output = calcul_3cl(structuredClone(input), CORPUS_ANALYSIS_OPTIONS); const outputTvMurId = output.logement.enveloppe.mur_collection.mur[0].donnee_entree.tv_umur_id; expect(inputTvMurId).toBe(outputTvMurId); @@ -90,7 +102,7 @@ describe('Open3cl misc unit tests', () => { /** * @type {FullDpe} */ - const output = calcul_3cl(structuredClone(input)); + const output = calcul_3cl(structuredClone(input), CORPUS_ANALYSIS_OPTIONS); expect( output.logement.installation_chauffage_collection.installation_chauffage[0] .emetteur_chauffage_collection.emetteur_chauffage[0].donnee_entree @@ -107,7 +119,7 @@ describe('Open3cl misc unit tests', () => { let input = getAdemeFileJson('2383E0460002R'); /** @type {FullDpe} **/ - let output = calcul_3cl(structuredClone(input)); + let output = calcul_3cl(structuredClone(input), CORPUS_ANALYSIS_OPTIONS); expect(input.logement.sortie.apport_et_besoin.surface_sud_equivalente).toBeCloseTo( output.logement.sortie.apport_et_besoin.surface_sud_equivalente ); @@ -116,7 +128,7 @@ describe('Open3cl misc unit tests', () => { input = getAdemeFileJson('2569E2636278D'); /** @type {FullDpe} **/ - output = calcul_3cl(structuredClone(input)); + output = calcul_3cl(structuredClone(input), CORPUS_ANALYSIS_OPTIONS); expect(input.logement.sortie.apport_et_besoin.surface_sud_equivalente).toBeCloseTo( output.logement.sortie.apport_et_besoin.surface_sud_equivalente ); @@ -127,7 +139,7 @@ describe('Open3cl misc unit tests', () => { let input = getAdemeFileJson('2569E2636278D'); /** @type {FullDpe} **/ - let output = calcul_3cl(structuredClone(input)); + let output = calcul_3cl(structuredClone(input), CORPUS_ANALYSIS_OPTIONS); expect(input.logement.sortie.ep_conso.ep_conso_ch).toBeCloseTo( output.logement.sortie.ep_conso.ep_conso_ch, 1 @@ -139,7 +151,7 @@ describe('Open3cl misc unit tests', () => { let input = getAdemeFileJson('2375E3009989U'); /** @type {FullDpe} **/ - let output = calcul_3cl(structuredClone(input)); + let output = calcul_3cl(structuredClone(input), CORPUS_ANALYSIS_OPTIONS); expect(input.logement.sortie.ep_conso).toEqual(output.logement.sortie.ep_conso); }); }); diff --git a/test/open3cl_mur.spec.js b/test/open3cl_mur.spec.js index 8c8aff506..f7c627778 100644 --- a/test/open3cl_mur.spec.js +++ b/test/open3cl_mur.spec.js @@ -1,5 +1,5 @@ import { calcul_3cl } from '../src/engine.js'; -import { getAdemeFileJsonOrDownload } from './test-helpers.js'; +import { getAdemeFileJsonOrDownload, CORPUS_ANALYSIS_OPTIONS } from './test-helpers.js'; import { set_bug_for_bug_compat } from '../src/utils.js'; import { describe, expect, test } from 'vitest'; @@ -9,7 +9,7 @@ describe('Murs unit tests', () => { let input = await getAdemeFileJsonOrDownload('2593E3377930D'); /** @type {FullDpe} **/ - let output = calcul_3cl(structuredClone(input)); + let output = calcul_3cl(structuredClone(input), CORPUS_ANALYSIS_OPTIONS); expect(input.logement.enveloppe.mur_collection.mur[0].donnee_intermediaire.umur0).toBe( output.logement.enveloppe.mur_collection.mur[0].donnee_intermediaire.umur0 diff --git a/test/open3cl_pertes_generateur.spec.js b/test/open3cl_pertes_generateur.spec.js index 107acdaa5..10421f76e 100644 --- a/test/open3cl_pertes_generateur.spec.js +++ b/test/open3cl_pertes_generateur.spec.js @@ -1,5 +1,5 @@ import { calcul_3cl } from '../src/engine.js'; -import { getAdemeFileJsonOrDownload } from './test-helpers.js'; +import { getAdemeFileJsonOrDownload, CORPUS_ANALYSIS_OPTIONS } from './test-helpers.js'; import { set_bug_for_bug_compat } from '../src/utils.js'; import { beforeEach, describe, expect, test } from 'vitest'; @@ -12,7 +12,7 @@ describe('Pertes generateur ch unit tests', () => { let input = await getAdemeFileJsonOrDownload('2684E0017144V'); /** @type {FullDpe} **/ - let output = calcul_3cl(structuredClone(input)); + let output = calcul_3cl(structuredClone(input), CORPUS_ANALYSIS_OPTIONS); expect(input.logement.sortie.apport_et_besoin.pertes_generateur_ch_recup * 1000).toBeCloseTo( output.logement.sortie.apport_et_besoin.pertes_generateur_ch_recup, 0.5 diff --git a/test/open3cl_ponts_thermique.spec.js b/test/open3cl_ponts_thermique.spec.js index 80ba04868..230fb718c 100644 --- a/test/open3cl_ponts_thermique.spec.js +++ b/test/open3cl_ponts_thermique.spec.js @@ -1,5 +1,9 @@ import { calcul_3cl } from '../src/engine.js'; -import { getAdemeFileJson, getAdemeFileJsonOrDownload } from './test-helpers.js'; +import { + getAdemeFileJson, + getAdemeFileJsonOrDownload, + CORPUS_ANALYSIS_OPTIONS +} from './test-helpers.js'; import { describe, expect, test, beforeEach } from 'vitest'; import { set_bug_for_bug_compat } from '../src/utils.js'; @@ -14,7 +18,7 @@ describe('Ponts thermiques unit tests', () => { input.logement.enveloppe.pont_thermique_collection.pont_thermique[2].donnee_intermediaire.k; /** @type {FullDpe} **/ - let output = calcul_3cl(structuredClone(input)); + let output = calcul_3cl(structuredClone(input), CORPUS_ANALYSIS_OPTIONS); const outputPt = output.logement.enveloppe.pont_thermique_collection.pont_thermique[2].donnee_intermediaire.k; @@ -28,7 +32,7 @@ describe('Ponts thermiques unit tests', () => { let input = await getAdemeFileJsonOrDownload('2321E3872263C'); /** @type {FullDpe} **/ - let output = calcul_3cl(structuredClone(input)); + let output = calcul_3cl(structuredClone(input), CORPUS_ANALYSIS_OPTIONS); input.logement.enveloppe.pont_thermique_collection.pont_thermique.forEach((ptInput, index) => { expect(ptInput.donnee_intermediaire.k).toBeCloseTo( output.logement.enveloppe.pont_thermique_collection.pont_thermique[index] diff --git a/test/open3cl_production_enr.spec.js b/test/open3cl_production_enr.spec.js index f4850ede0..684badc2b 100644 --- a/test/open3cl_production_enr.spec.js +++ b/test/open3cl_production_enr.spec.js @@ -1,5 +1,5 @@ import { calcul_3cl } from '../src/engine.js'; -import { getAdemeFileJsonOrDownload } from './test-helpers.js'; +import { getAdemeFileJsonOrDownload, CORPUS_ANALYSIS_OPTIONS } from './test-helpers.js'; import { set_bug_for_bug_compat } from '../src/utils.js'; import { describe, expect, test } from 'vitest'; @@ -9,7 +9,7 @@ describe('Production enr unit tests', () => { let input = await getAdemeFileJsonOrDownload(dpeCode); /** @type {FullDpe} **/ - let output = calcul_3cl(structuredClone(input)); + let output = calcul_3cl(structuredClone(input), CORPUS_ANALYSIS_OPTIONS); expect(input.logement.sortie.ef_conso.conso_5_usages_m2).toBeCloseTo( output.logement.sortie.ef_conso.conso_5_usages_m2 diff --git a/test/run_benchmark.js b/test/run_benchmark.js index c30ceaeea..8258abc72 100644 --- a/test/run_benchmark.js +++ b/test/run_benchmark.js @@ -12,7 +12,8 @@ const dpeSanitizerService = new DpeSanitizerService(); const cleanedDpe = dpeSanitizerService.execute(input); const start_time = hrtime.bigint(); -for (let i = 0; i < 200; i++) calcul_3cl(structuredClone(cleanedDpe), { sanitize: false }); +for (let i = 0; i < 200; i++) + calcul_3cl(structuredClone(cleanedDpe), { sanitize: false, compat_for_corpus_analysis: true }); const end_time = hrtime.bigint(); console.log(`Completed in ${(end_time - start_time) / 1000000n} milliseconds`); diff --git a/test/test-helpers.js b/test/test-helpers.js index 7b7a58cb2..3d6851985 100644 --- a/test/test-helpers.js +++ b/test/test-helpers.js @@ -55,3 +55,13 @@ export function getResultFile(ademeId) { const data = fs.readFileSync(dpeResultFile, { encoding: 'utf8', flag: 'r' }); return JSON.parse(data); } + +/** + * Options de `calcul_3cl` pour l'analyse du corpus ADEME : DPE nettoyé (comportement par défaut) et + * reprise des valeurs choisies par le logiciel de saisie (`compat_for_corpus_analysis`), afin de + * reproduire les résultats des DPE. + */ +export const CORPUS_ANALYSIS_OPTIONS = Object.freeze({ + sanitize: true, + compat_for_corpus_analysis: true +}); diff --git "a/test/\342\200\216test/open3cl-coeff-1-7.spec.js" "b/test/\342\200\216test/open3cl-coeff-1-7.spec.js" index 45f832537..095719122 100644 --- "a/test/\342\200\216test/open3cl-coeff-1-7.spec.js" +++ "b/test/\342\200\216test/open3cl-coeff-1-7.spec.js" @@ -1,3 +1,4 @@ +import { CORPUS_ANALYSIS_OPTIONS } from '../test-helpers.js'; import { describe, expect, test } from 'vitest'; import fs from 'node:fs'; import { calcul_3cl, get_conso_coeff_1_7_2027 } from '../../src/index.js'; @@ -23,7 +24,7 @@ describe('Open3cl CEP 1.7 (janvier 2027) unit tests', () => { const inputDpe = loadFixture(fixtures[0].replace('.json', '')); expect(inputDpe).not.toBeNull(); - const outputDpe = calcul_3cl(structuredClone(inputDpe)); + const outputDpe = calcul_3cl(structuredClone(inputDpe), CORPUS_ANALYSIS_OPTIONS); expect(outputDpe.logement.sortie.ep_conso.ep_conso_5_usages_2027).toBeDefined(); expect(outputDpe.logement.sortie.ep_conso.ep_conso_5_usages_2027_m2).toBeDefined(); @@ -40,7 +41,7 @@ describe('Open3cl CEP 1.7 (janvier 2027) unit tests', () => { for (const file of fixtures.slice(0, 10)) { const inputDpe = loadFixture(file.replace('.json', '')); if (!inputDpe) continue; - const outputDpe = calcul_3cl(structuredClone(inputDpe)); + const outputDpe = calcul_3cl(structuredClone(inputDpe), CORPUS_ANALYSIS_OPTIONS); const ep2026 = outputDpe.logement.sortie.ep_conso.ep_conso_5_usages_2026; const ep2027 = outputDpe.logement.sortie.ep_conso.ep_conso_5_usages_2027; // 1.7 ≤ 1.9, so 2027 energy consumption must be ≤ 2026 @@ -59,7 +60,7 @@ describe('Open3cl CEP 1.7 (janvier 2027) unit tests', () => { for (const file of fixtures) { const inputDpe = loadFixture(file.replace('.json', '')); if (!inputDpe) continue; - const outputDpe = calcul_3cl(structuredClone(inputDpe)); + const outputDpe = calcul_3cl(structuredClone(inputDpe), CORPUS_ANALYSIS_OPTIONS); const ep2026 = outputDpe.logement.sortie.ep_conso.ep_conso_5_usages_2026; const ep2027 = outputDpe.logement.sortie.ep_conso.ep_conso_5_usages_2027; expect(ep2027).toBeLessThanOrEqual(ep2026 + 0.001); @@ -76,7 +77,7 @@ describe('Open3cl CEP 1.7 (janvier 2027) unit tests', () => { const inputDpe = loadFixture(fixtures[0].replace('.json', '')); expect(inputDpe).not.toBeNull(); - const outputDpe = calcul_3cl(structuredClone(inputDpe)); + const outputDpe = calcul_3cl(structuredClone(inputDpe), CORPUS_ANALYSIS_OPTIONS); const result1_7 = get_conso_coeff_1_7_2027(outputDpe); expect(result1_7.ep_conso_5_usages).toBeDefined();