Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions src/11_rdim_ecs_individuelle_unique.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,81 @@ export function reconstituerRdimEcsIndividuelleUnique(th, instal_ecs, surfaceHab
instal_ecs[0].donnee_entree.rdim_reconstitue = true;
return true;
}

/**
* DPE immeuble avec plusieurs installations ECS individuelles, chacune décrite pour un logement type.
*
* Même convention d'export que l'installation unique (3.0.x, 2.0.x, 1.1.x) : pour chaque
* installation i, `surface_habitable` = Sh_i d'un logement type et `nombre_logement` = N_i, sans
* `rdim`, avec Σ Sh_i × N_i = SH. Tribu traite chaque installation indépendamment avec Rdim_i = N_i :
* Becs_i = Becs_immeuble × Sh_i × Rdim_i / SH / Rdim_i, puis Cecs = Σ Cecs_i × Rdim_i. L'export ADEME
* le confirme : besoin_ecs_i = Becs_immeuble × Sh_i / SH et conso_ecs = Σ conso_ecs_i × N_i.
*
* Sans rdim, Σ Sh_i ≠ SH : Open3CL se replie sur Becs_immeuble / nombre_appartement par
* installation et ne remultiplie pas la conso par N_i.
*
* Gardes :
* - Σ Sh_i × N_i ≈ SH (TOLERANCE_SURFACE_LOGEMENT_TYPE) : exclut les exports où Sh_i est déjà
* la surface totale desservie (Σ Sh_i = SH, ex. N_i fractionnaires) ;
* - au moins un N_i > 1 : sinon rdim = 1 partout, sans effet (LICIEL, nombre_logement = 1).
*
* @see Methode_de_calcul_3CL_DPE_2021-338.pdf - § 11.4 et § 17.1.3.2
* @see Tribu : Calcul_batiment.cs l. 581-588 (boucle par installation, Ratio = Sh·Rdim/SH,
* Becs / Rdim), l. 1660-1662 (Cecs_i / Rdim puis × Rdim) ; Calcul_installation_ECS.cs l. 75
* @see Guide Tribu TE-211208 « Particularités d'une répétition de systèmes d'ECS individuel dans un
* immeuble collectif » (Sh_ecs = surface d'un logement, Rdim = nombre d'installations identiques)
*/

/**
* Rdim implicites des installations ECS individuelles multiples d'un immeuble (dans l'ordre des
* installations), ou null si non concernées.
*
* @param th {string} type de bâtiment (maison, appartement, immeuble)
* @param instal_ecs {Array} installation_ecs
* @param surfaceHabitableImmeuble {number|string} surface habitable de l'immeuble (SH)
* @return {number[]|null}
*/
export function rdimsImplicitesEcsIndividuellesMultiples(th, instal_ecs, surfaceHabitableImmeuble) {
if (th !== 'immeuble' || !Array.isArray(instal_ecs) || instal_ecs.length < 2) return null;

const des = instal_ecs.map((inst) => inst?.donnee_entree);
const concernees = des.every(
(de) => de && String(de.enum_type_installation_id) === '1' && !de.rdim
);
if (!concernees) return null;

const SH = Number(surfaceHabitableImmeuble);
if (!(SH > 0)) return null;

const surfaces = des.map((de) => Number(de.surface_habitable));
const rdims = des.map((de) => Number(de.nombre_logement) || 1);
if (!surfaces.every((sh) => sh > 0) || !rdims.every((n) => n > 0)) return null;
if (!rdims.some((n) => n > 1)) return null;

const sommeShN = surfaces.reduce((acc, sh, i) => acc + sh * rdims[i], 0);
return Math.abs(sommeShN - SH) <= TOLERANCE_SURFACE_LOGEMENT_TYPE * SH ? rdims : null;
}

/**
* Reconstitue le rdim des installations ECS individuelles multiples d'un immeuble décrites pour un
* logement type. Chaque installation reçoit `rdim` = nombre_logement (1 si absent) et le flag
* `rdim_reconstitue`.
*
* @param th {string} type de bâtiment
* @param instal_ecs {Array} installation_ecs
* @param surfaceHabitableImmeuble {number|string} surface habitable de l'immeuble (SH)
* @return {boolean} true si les rdim ont été reconstitués
*/
export function reconstituerRdimEcsIndividuellesMultiples(
th,
instal_ecs,
surfaceHabitableImmeuble
) {
const rdims = rdimsImplicitesEcsIndividuellesMultiples(th, instal_ecs, surfaceHabitableImmeuble);
if (rdims === null) return false;
instal_ecs.forEach((inst, i) => {
inst.donnee_entree.rdim = rdims[i];
inst.donnee_entree.rdim_reconstitue = true;
});
return true;
}
136 changes: 136 additions & 0 deletions src/11_rdim_ecs_individuelle_unique.spec.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { describe, expect, test } from 'vitest';
import {
rdimImpliciteEcsIndividuelleUnique,
rdimsImplicitesEcsIndividuellesMultiples,
reconstituerRdimEcsIndividuelleUnique,
reconstituerRdimEcsIndividuellesMultiples,
TOLERANCE_SURFACE_LOGEMENT_TYPE
} from './11_rdim_ecs_individuelle_unique.js';

Expand Down Expand Up @@ -111,3 +113,137 @@ describe("Rdim de l'installation ECS individuelle unique d'un immeuble", () => {
});
});
});

/**
* @see Methode_de_calcul_3CL_DPE_2021-338.pdf - § 11.4 et § 17.1.3.2
* @see Tribu : Calcul_batiment.cs l. 581-588 et l. 1660-1662 ; Calcul_installation_ECS.cs l. 75
*/
describe("Rdim des installations ECS individuelles multiples d'un immeuble", () => {
/** Installation ECS individuelle décrite pour un logement type */
const inst = (surface_habitable, nombre_logement, de = {}) => ({
donnee_entree: { enum_type_installation_id: '1', surface_habitable, nombre_logement, ...de }
});
/** Cas type 2244E3039157K : 48,82 × 2 + 78,09 × 6 = 566,18 ≈ SH = 566,16 */
const ecs = () => [inst(48.82, 2), inst(78.09, 6)];

describe('rdimsImplicitesEcsIndividuellesMultiples', () => {
test('installations individuelles, Σ Sh × N = SH, sans rdim : rdim_i = nombre_logement_i', () => {
expect(rdimsImplicitesEcsIndividuellesMultiples('immeuble', ecs(), 566.16)).toEqual([2, 6]);
expect(
rdimsImplicitesEcsIndividuellesMultiples(
'immeuble',
[
inst('48.82', '2', { enum_type_installation_id: 1 }),
inst('78.09', '6', { enum_type_installation_id: 1 })
],
'566.16'
)
).toEqual([2, 6]);
});

test('nombre_logement absent sur une installation : rdim = 1 pour celle-ci', () => {
// 100 × 4 + 50 × 1 = 450
expect(
rdimsImplicitesEcsIndividuellesMultiples(
'immeuble',
[inst(100, 4), inst(50, undefined)],
450
)
).toEqual([4, 1]);
});

test('tolérance de 5 % sur Σ Sh × N ≈ SH', () => {
// Σ = 100 × 4 + 50 × 2 = 500 ; |500 − SH| ≤ 0,05 × SH ⇔ 476,19 ≤ SH ≤ 526,31
const deux = () => [inst(100, 4), inst(50, 2)];
expect(rdimsImplicitesEcsIndividuellesMultiples('immeuble', deux(), 526)).toEqual([4, 2]);
expect(rdimsImplicitesEcsIndividuellesMultiples('immeuble', deux(), 477)).toEqual([4, 2]);
expect(rdimsImplicitesEcsIndividuellesMultiples('immeuble', deux(), 527)).toBeNull();
expect(rdimsImplicitesEcsIndividuellesMultiples('immeuble', deux(), 476)).toBeNull();
});

test('surfaces exportées comme surface totale desservie (Σ Sh = SH) : non concerné', () => {
// Cas type 2224E1374434V : 74,4 × 3 + 31 × 1 = 254,2 ≠ SH = 105,4 = Σ Sh
expect(
rdimsImplicitesEcsIndividuellesMultiples('immeuble', [inst(74.4, 3), inst(31, 1)], 105.4)
).toBeNull();
});

test('tous les nombre_logement à 1 (LICIEL) : non concerné', () => {
expect(
rdimsImplicitesEcsIndividuellesMultiples('immeuble', [inst(60, 1), inst(40, 1)], 100)
).toBeNull();
});

test('un rdim exporté sur une installation : on conserve la saisie', () => {
expect(
rdimsImplicitesEcsIndividuellesMultiples(
'immeuble',
[inst(48.82, 2, { rdim: 2 }), inst(78.09, 6)],
566.16
)
).toBeNull();
});

test('une installation collective : non concerné', () => {
expect(
rdimsImplicitesEcsIndividuellesMultiples(
'immeuble',
[inst(48.82, 2), inst(78.09, 6, { enum_type_installation_id: '2' })],
566.16
)
).toBeNull();
});

test('maison ou appartement : non concerné', () => {
expect(rdimsImplicitesEcsIndividuellesMultiples('maison', ecs(), 566.16)).toBeNull();
expect(rdimsImplicitesEcsIndividuellesMultiples('appartement', ecs(), 566.16)).toBeNull();
});

test('installation unique : non concerné (cf. rdimImpliciteEcsIndividuelleUnique)', () => {
expect(rdimsImplicitesEcsIndividuellesMultiples('immeuble', [inst(64, 10)], 640)).toBeNull();
});

test('données manquantes ou invalides : non concerné', () => {
expect(rdimsImplicitesEcsIndividuellesMultiples('immeuble', undefined, 566.16)).toBeNull();
expect(rdimsImplicitesEcsIndividuellesMultiples('immeuble', [], 566.16)).toBeNull();
expect(
rdimsImplicitesEcsIndividuellesMultiples('immeuble', [inst(48.82, 2), {}], 566.16)
).toBeNull();
expect(
rdimsImplicitesEcsIndividuellesMultiples('immeuble', [inst(48.82, 2), null], 566.16)
).toBeNull();
expect(rdimsImplicitesEcsIndividuellesMultiples('immeuble', ecs(), undefined)).toBeNull();
expect(rdimsImplicitesEcsIndividuellesMultiples('immeuble', ecs(), 0)).toBeNull();
expect(
rdimsImplicitesEcsIndividuellesMultiples(
'immeuble',
[inst(48.82, 2), inst(undefined, 6)],
566.16
)
).toBeNull();
expect(
rdimsImplicitesEcsIndividuellesMultiples('immeuble', [inst(100, 4), inst(50, -2)], 300)
).toBeNull();
});
});

describe('reconstituerRdimEcsIndividuellesMultiples', () => {
test('installations concernées : rdim et flag positionnés sur chacune', () => {
const installations = ecs();
expect(reconstituerRdimEcsIndividuellesMultiples('immeuble', installations, 566.16)).toBe(
true
);
expect(installations[0].donnee_entree).toMatchObject({ rdim: 2, rdim_reconstitue: true });
expect(installations[1].donnee_entree).toMatchObject({ rdim: 6, rdim_reconstitue: true });
});

test('installations non concernées : inchangées', () => {
const installations = [inst(60, 1), inst(40, 1)];
expect(reconstituerRdimEcsIndividuellesMultiples('immeuble', installations, 100)).toBe(false);
installations.forEach((i) => {
expect(i.donnee_entree.rdim).toBeUndefined();
expect(i.donnee_entree.rdim_reconstitue).toBeUndefined();
});
});
});
});
10 changes: 9 additions & 1 deletion src/engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ import getFicheTechnique from './ficheTechnique.js';
import { ProductionENR, ratioProrataPvLogement } from './16.2_production_enr.js';
import DpeSanitizerService from './dpe-sanitizer.service.js';
import { reconstituerRdimDpeGenereImmeuble } from './17.2_rdim_dpe_genere_immeuble.js';
import { reconstituerRdimEcsIndividuelleUnique } from './11_rdim_ecs_individuelle_unique.js';
import {
reconstituerRdimEcsIndividuelleUnique,
reconstituerRdimEcsIndividuellesMultiples
} from './11_rdim_ecs_individuelle_unique.js';

const LIB_VERSION = 'OPEN3CL_VERSION';

Expand Down Expand Up @@ -298,6 +301,11 @@ export function calcul_3cl(inputDpe, options) {
* @see https://github.com/Open3CL/engine/issues/288
*/
reconstituerRdimEcsIndividuelleUnique(th, ecs, cg.surface_habitable_immeuble);
/**
* Immeuble avec plusieurs installations ECS individuelles décrites chacune pour un logement type
* (Σ Sh_i × nombre_logement_i ≈ SH, sans rdim) : rdim_i = nombre_logement_i.
*/
reconstituerRdimEcsIndividuellesMultiples(th, ecs, cg.surface_habitable_immeuble);
const Nb_lgt = cg.nombre_appartement || 1;

/**
Expand Down
25 changes: 23 additions & 2 deletions src/engine.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ vi.mock('./17.2_rdim_dpe_genere_immeuble.js', () => ({
reconstituerRdimDpeGenereImmeuble: vi.fn()
}));
vi.mock('./11_rdim_ecs_individuelle_unique.js', () => ({
reconstituerRdimEcsIndividuelleUnique: vi.fn()
reconstituerRdimEcsIndividuelleUnique: vi.fn(),
reconstituerRdimEcsIndividuellesMultiples: vi.fn()
}));

// Les instances Inertie / ProductionENR sont créées au chargement du module.
Expand Down Expand Up @@ -119,7 +120,7 @@ const calc_confort_ete = (await import('./2021_04_13_confort_ete.js')).default;
const calc_qualite_isolation = (await import('./2021_04_13_qualite_isolation.js')).default;
const getFicheTechnique = (await import('./ficheTechnique.js')).default;
const { reconstituerRdimDpeGenereImmeuble } = await import('./17.2_rdim_dpe_genere_immeuble.js');
const { reconstituerRdimEcsIndividuelleUnique } =
const { reconstituerRdimEcsIndividuelleUnique, reconstituerRdimEcsIndividuellesMultiples } =
await import('./11_rdim_ecs_individuelle_unique.js');

/** Objet conso renvoyé par le mock de `calc_conso` (mêmes réf pour les 3 appels). */
Expand Down Expand Up @@ -1256,6 +1257,26 @@ describe('calcul_3cl - rdim de l’installation ECS individuelle unique d’un i
});
});

describe('calcul_3cl - rdim des installations ECS individuelles multiples d’un immeuble', () => {
test('reconstitution appelée avec le type de bâtiment, les installations ECS et SH, avant le calcul ECS', () => {
const dpe = makeDpe();
dpe.logement.caracteristique_generale.surface_habitable_immeuble = 566.16;
vi.mocked(reconstituerRdimEcsIndividuellesMultiples).mockImplementationOnce(() => {
expect(calc_ecs).not.toHaveBeenCalled();
return false;
});

calcul_3cl(dpe, { sanitize: false });

expect(reconstituerRdimEcsIndividuellesMultiples).toHaveBeenCalledWith(
'maison',
dpe.logement.installation_ecs_collection.installation_ecs,
566.16
);
expect(calc_ecs).toHaveBeenCalled();
});
});

describe('get_conso_coeff_1_7_2027', () => {
test('projette la conso EP 2027 pour une maison', () => {
vi.mocked(classe_bilan_dpe).mockReturnValue('C');
Expand Down