"""Tests du chapitre 11 — inspection synthétique du sujet du cas 1, convention d'âge effectif, dépréciation ventilée, sensibilité et exercices. Tolérances : 0,01 $ sur les montants recalculés ; 0,5 $ sur les montants arrondis cités ; 1e-9 sur les identités.""" import sys, pathlib import numpy as np, pytest sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases")) import ch11_inspection as cas from traite import cout as ct @pytest.fixture(scope="module") def data(): devis, sujet, cn = cas.charger() f = cas.fiche(devis, cn); trav = cas.travaux(devis, cn) return {"devis": devis, "sujet": sujet, "cn": cn, "f": f, "trav": trav, "dv": cas.depreciation(f, trav, cn), "sens": cas.sensibilite(f, trav, cn)} class TestConvention: def test_bornes_et_monotonie(self): L = 60.0 for a in np.linspace(0, 80, 17): prev = -1.0 for c in range(1, 6): v = cas.age_effectif(a, L, c) assert 0.0 <= v <= L and v >= prev # croissant avec la note d'état, borné à [0, L] prev = v for c in range(1, 6): # croissant avec l'âge chronologique vals = [cas.age_effectif(a, L, c) for a in range(0, 61)] assert all(x <= y for x, y in zip(vals, vals[1:])) def test_cas_limites(self): assert cas.age_effectif(0, 60, 1) == pytest.approx(1.5) # neuf et excellent : 0,5 × 0,05 × 60 assert cas.age_effectif(60, 60, 5) == 60.0 # en fin de vie et mauvais : borne L assert cas.age_effectif(28, 60, 3) == pytest.approx(29.0) # sujet : (1 − 0,5) × 28 + 0,5 × 0,5 × 60 assert cas.age_effectif(28, 60, 3, lam=0.0) == 28.0 # λ = 0 : âge chronologique assert cas.age_effectif(28, 60, 3, lam=1.0) == 30.0 # λ = 1 : φ(3) L assert cas.age_effectif(28, 20, 4) == 20.0 # VRC : 14 + 7,5 = 21,5 → borné à 20 def test_phi(self): assert list(cas.PHI.values()) == [0.05, 0.25, 0.50, 0.75, 1.00] class TestFiche: def test_couverture_du_devis(self, data): f, cn = data["f"], data["cn"] assert len(f) == 20 and int(f["longue_duree"].sum()) == 1 assert f["cout_direct"].sum() == pytest.approx(cn["direct"], abs=0.01) assert f["cout_neuf"].sum() == pytest.approx(cn["total"], abs=0.01) assert cas.facteur_majoration(cn) == pytest.approx(1.33043, abs=5e-6) assert cn["total"] == pytest.approx(397567.33, abs=0.01) def test_valeurs_citees(self, data): f = data["f"].set_index("composante") assert f.loc["Couverture (bardeaux d'asphalte)", "age_effectif"] == pytest.approx(12.0) assert f.loc["Couverture (bardeaux d'asphalte)", "cout_neuf"] == pytest.approx(10217.70, abs=0.01) assert f.loc["Gouttières et solins", "age_effectif"] == pytest.approx(23.375) assert f.loc["Fenêtres (PVC, double vitrage)", "age_effectif"] == pytest.approx(21.5) assert f.loc["Fenêtres (PVC, double vitrage)", "cout_neuf"] == pytest.approx(22311.42, abs=0.01) assert f.loc["Ventilateur récupérateur de chaleur (VRC)", "ratio"] == pytest.approx(1.0) assert f.loc["Structure, fondations, dalle, drain, isolation, cloisons", "cout_neuf"] == pytest.approx(172611.24, abs=0.01) assert f.loc["Structure, fondations, dalle, drain, isolation, cloisons", "age_effectif"] == pytest.approx(29.0) assert f.loc["Structure, fondations, dalle, drain, isolation, cloisons", "vie_restante"] == pytest.approx(31.0) courtes = data["f"][~data["f"]["longue_duree"]] assert courtes["cout_neuf"].sum() == pytest.approx(224956.09, abs=0.01) assert courtes["cout_neuf"].sum() / data["cn"]["total"] == pytest.approx(0.566, abs=5e-4) def test_indice_etat(self, data): f = data["f"] assert cas.indice_etat(f) == pytest.approx(2.809, abs=5e-4) assert f["etat"].value_counts().to_dict() == {2: 7, 3: 8, 4: 4, 5: 1} r = (f["cout_neuf"] * f["ratio"]).sum() / f["cout_neuf"].sum() assert r == pytest.approx(0.547, abs=5e-4) and r * 60 == pytest.approx(32.8, abs=0.05) class TestTravaux: def test_total(self, data): t = data["trav"] assert len(t) == 5 assert t["cout"].sum() == pytest.approx(10479.98, abs=0.01) assert t["cout"].sum() / data["cn"]["total"] == pytest.approx(0.0264, abs=5e-5) assert t.set_index("poste_reference").loc["Entrée asphaltée", "cout"] == pytest.approx(3438.43, abs=0.01) assert t.set_index("poste_reference").loc["Fenêtres (PVC double vitrage)", "cout"] == pytest.approx(2788.93, abs=0.01) class TestDepreciation: def test_identites(self, data): dv, cn = data["dv"], data["cn"] assert dv["total"] == pytest.approx(dv["curable"] + sum(dv["courte_duree"].values()) + dv["longue_duree"], rel=1e-12) courtes_hors_entree = data["f"][(~data["f"]["longue_duree"]) & (data["f"]["composante"] != "Entrée asphaltée")]["cout_neuf"].sum() assert dv["base_longue"] == pytest.approx(cn["total"] - dv["curable"] - courtes_hors_entree, abs=1e-6) assert dv["longue_duree"] == pytest.approx(dv["base_longue"] * 29.0 / 60.0, rel=1e-12) assert dv["contribution_batiment"] == pytest.approx(cn["total"] - dv["total"], rel=1e-12) assert len(dv["courte_duree"]) == 18 def test_valeurs_citees(self, data): dv = data["dv"] assert dv["curable"] == pytest.approx(10479.98, abs=0.01) assert sum(dv["courte_duree"].values()) == pytest.approx(130584.55, abs=0.01) assert dv["base_longue"] == pytest.approx(165569.69, abs=0.01) assert dv["longue_duree"] == pytest.approx(80025.35, abs=0.01) assert dv["total"] == pytest.approx(221089.88, abs=0.01) assert dv["taux"] == pytest.approx(0.5561, abs=5e-5) assert dv["contribution_batiment"] == pytest.approx(176477.45, abs=0.01) assert dv["age_vie_simple"] == pytest.approx(192157.55, abs=0.01) assert dv["age_vie_simple"] / dv["cout_neuf"] == pytest.approx(0.4833, abs=5e-5) assert dv["total"] - dv["age_vie_simple"] == pytest.approx(28932.34, abs=0.01) assert ct.depreciation_age_vie(dv["cout_neuf"], 28, 60) == pytest.approx(185531.42, abs=0.01) assert dv["courte_duree"]["Ventilateur récupérateur de chaleur (VRC)"] == pytest.approx(7501.27, abs=0.01) assert dv["courte_duree"]["Fenêtres (PVC, double vitrage)"] == pytest.approx(15989.85, abs=0.01) class TestSensibilite: def test_grille(self, data): s = data["sens"].set_index(["lambda", "etat_structure"]) assert s.loc[(0.5, 3), "taux"] == pytest.approx(0.5561, abs=5e-5) assert s.loc[(0.0, 3), "taux"] == pytest.approx(0.6368, abs=5e-5) assert s.loc[(1.0, 1), "taux"] == pytest.approx(0.2738, abs=5e-5) assert s.loc[(1.0, 5), "taux"] == pytest.approx(0.6694, abs=5e-5) assert s.loc[(0.5, 1), "taux"] == pytest.approx(0.4624, abs=5e-5) assert s.loc[(0.5, 5), "taux"] == pytest.approx(0.6602, abs=5e-5) for lam in (0.0, 0.25, 0.5, 0.75, 1.0): t = [s.loc[(lam, e), "taux"] for e in range(1, 6)] assert all(x <= y + 1e-12 for x, y in zip(t, t[1:])) # croissante avec la note d'état # à λ = 0 la note d'état n'intervient pas assert len({round(s.loc[(0.0, e), "taux"], 9) for e in range(1, 6)}) == 1 assert s.loc[(1.0, 5), "age_effectif_structure"] == 60.0 and s.loc[(1.0, 1), "age_effectif_structure"] == pytest.approx(3.0) class TestValeurInformation: def test_regle(self): v = cas.valeur_information(0.10, 3000.0, 50.0) assert v["perte_esperee"] == 300.0 and v["valeur_info"] == 250.0 and v["recommander"] v = cas.valeur_information(0.05, 25000.0, 1200.0) assert v["valeur_info"] == pytest.approx(50.0) and v["recommander"] v = cas.valeur_information(0.02, 25000.0, 1200.0) assert v["valeur_info"] == pytest.approx(-700.0) and not v["recommander"] # seuil de probabilité : p* = t / C assert 1200.0 / 25000.0 == pytest.approx(0.048) class TestExercices: """Chiffres des solutions (solutions/ch11.tex).""" def test_ex3_ages_effectifs(self): # toiture 12 ans / L 20 / état 4 ; chauffe-eau 9 / 12 / état 2 ; fenêtres 35 / 30 / état 2 ; structure 45 / 60 / état 2 assert cas.age_effectif(12, 20, 4) == pytest.approx(13.5) assert cas.age_effectif(9, 12, 2) == pytest.approx(6.0) assert cas.age_effectif(35, 30, 2) == pytest.approx(21.25) assert cas.age_effectif(45, 60, 2) == pytest.approx(30.0) assert ct.vie_restante(60, 30.0) == 30.0 def test_ex4_ventilee(self): comps = [ct.Composante("toiture", 12000.0, 13.5, 20), ct.Composante("chauffe-eau", 1500.0, 6.0, 12), ct.Composante("fenêtres", 24000.0, 21.25, 30)] dv = ct.depreciation_ventilee(300000.0, 4500.0, comps, 30.0, 60.0) assert dv["courte_duree"]["toiture"] == pytest.approx(8100.0) assert dv["courte_duree"]["chauffe-eau"] == pytest.approx(750.0) assert dv["courte_duree"]["fenêtres"] == pytest.approx(17000.0) assert dv["base_longue"] == pytest.approx(258000.0) assert dv["longue_duree"] == pytest.approx(129000.0) assert dv["total"] == pytest.approx(159350.0) assert dv["total"] / 300000.0 == pytest.approx(0.53117, abs=5e-5) assert ct.depreciation_age_vie(300000.0, 30.0, 60.0) == pytest.approx(150000.0) def test_ex5_valeur_information(self): v = cas.valeur_information(0.08, 20000.0, 900.0) assert v["perte_esperee"] == pytest.approx(1600.0) and v["valeur_info"] == pytest.approx(700.0) assert 900.0 / 20000.0 == pytest.approx(0.045) # dosimètre radon : 50 $ ; seuil p* pour une correction de 2 500 $ assert 50.0 / 2500.0 == pytest.approx(0.02) def test_ex6_variante_lambda(self, data): s = data["sens"].set_index(["lambda", "etat_structure"]) assert s.loc[(0.75, 3), "taux"] == pytest.approx(0.5093, abs=5e-5) assert s.loc[(0.25, 3), "taux"] == pytest.approx(0.5977, abs=5e-5) assert s.loc[(0.75, 3), "age_effectif_structure"] == pytest.approx(29.5) assert s.loc[(0.25, 3), "age_effectif_structure"] == pytest.approx(28.5) assert s.loc[(0.25, 3), "depreciation"] == pytest.approx(237631.21, abs=0.01) assert s.loc[(0.75, 3), "depreciation"] == pytest.approx(202500.46, abs=0.01) assert s.loc[(0.25, 3), "depreciation"] - s.loc[(0.75, 3), "depreciation"] == pytest.approx(35130.75, abs=0.01) d25 = cas.depreciation(data["f"], data["trav"], data["cn"], 0.25); d75 = cas.depreciation(data["f"], data["trav"], data["cn"], 0.75) assert d25["courte_duree"]["Revêtement extérieur (brique et fibrociment)"] - d75["courte_duree"]["Revêtement extérieur (brique et fibrociment)"] == pytest.approx(8568.74, abs=0.01) assert d25["courte_duree"]["Armoires de cuisine et comptoirs"] - d75["courte_duree"]["Armoires de cuisine et comptoirs"] == pytest.approx(5000.84, abs=0.01) assert d25["courte_duree"]["Fenêtres (PVC, double vitrage)"] - d75["courte_duree"]["Fenêtres (PVC, double vitrage)"] == pytest.approx(4834.14, abs=0.01) assert d25["longue_duree"] - d75["longue_duree"] == pytest.approx(-2759.49, abs=0.01)