spb/traite-evaluation-qc
Public
1"""Tests du chapitre 11 — inspection synthétique du sujet du cas 1, convention d'âge effectif, dépréciation ventilée,2sensibilité et exercices. Tolérances : 0,01 $ sur les montants recalculés ; 0,5 $ sur les montants arrondis cités ;31e-9 sur les identités."""4import sys, pathlib5import numpy as np, pytest6sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))7import ch11_inspection as cas8from traite import cout as ct91011@pytest.fixture(scope="module")12def data():13 devis, sujet, cn = cas.charger()14 f = cas.fiche(devis, cn); trav = cas.travaux(devis, cn)15 return {"devis": devis, "sujet": sujet, "cn": cn, "f": f, "trav": trav, "dv": cas.depreciation(f, trav, cn), "sens": cas.sensibilite(f, trav, cn)}161718class TestConvention:19 def test_bornes_et_monotonie(self):20 L = 60.021 for a in np.linspace(0, 80, 17):22 prev = -1.023 for c in range(1, 6):24 v = cas.age_effectif(a, L, c)25 assert 0.0 <= v <= L and v >= prev # croissant avec la note d'état, borné à [0, L]26 prev = v27 for c in range(1, 6): # croissant avec l'âge chronologique28 vals = [cas.age_effectif(a, L, c) for a in range(0, 61)]29 assert all(x <= y for x, y in zip(vals, vals[1:]))3031 def test_cas_limites(self):32 assert cas.age_effectif(0, 60, 1) == pytest.approx(1.5) # neuf et excellent : 0,5 × 0,05 × 6033 assert cas.age_effectif(60, 60, 5) == 60.0 # en fin de vie et mauvais : borne L34 assert cas.age_effectif(28, 60, 3) == pytest.approx(29.0) # sujet : (1 − 0,5) × 28 + 0,5 × 0,5 × 6035 assert cas.age_effectif(28, 60, 3, lam=0.0) == 28.0 # λ = 0 : âge chronologique36 assert cas.age_effectif(28, 60, 3, lam=1.0) == 30.0 # λ = 1 : φ(3) L37 assert cas.age_effectif(28, 20, 4) == 20.0 # VRC : 14 + 7,5 = 21,5 → borné à 203839 def test_phi(self):40 assert list(cas.PHI.values()) == [0.05, 0.25, 0.50, 0.75, 1.00]414243class TestFiche:44 def test_couverture_du_devis(self, data):45 f, cn = data["f"], data["cn"]46 assert len(f) == 20 and int(f["longue_duree"].sum()) == 147 assert f["cout_direct"].sum() == pytest.approx(cn["direct"], abs=0.01)48 assert f["cout_neuf"].sum() == pytest.approx(cn["total"], abs=0.01)49 assert cas.facteur_majoration(cn) == pytest.approx(1.33043, abs=5e-6)50 assert cn["total"] == pytest.approx(397567.33, abs=0.01)5152 def test_valeurs_citees(self, data):53 f = data["f"].set_index("composante")54 assert f.loc["Couverture (bardeaux d'asphalte)", "age_effectif"] == pytest.approx(12.0)55 assert f.loc["Couverture (bardeaux d'asphalte)", "cout_neuf"] == pytest.approx(10217.70, abs=0.01)56 assert f.loc["Gouttières et solins", "age_effectif"] == pytest.approx(23.375)57 assert f.loc["Fenêtres (PVC, double vitrage)", "age_effectif"] == pytest.approx(21.5)58 assert f.loc["Fenêtres (PVC, double vitrage)", "cout_neuf"] == pytest.approx(22311.42, abs=0.01)59 assert f.loc["Ventilateur récupérateur de chaleur (VRC)", "ratio"] == pytest.approx(1.0)60 assert f.loc["Structure, fondations, dalle, drain, isolation, cloisons", "cout_neuf"] == pytest.approx(172611.24, abs=0.01)61 assert f.loc["Structure, fondations, dalle, drain, isolation, cloisons", "age_effectif"] == pytest.approx(29.0)62 assert f.loc["Structure, fondations, dalle, drain, isolation, cloisons", "vie_restante"] == pytest.approx(31.0)63 courtes = data["f"][~data["f"]["longue_duree"]]64 assert courtes["cout_neuf"].sum() == pytest.approx(224956.09, abs=0.01)65 assert courtes["cout_neuf"].sum() / data["cn"]["total"] == pytest.approx(0.566, abs=5e-4)6667 def test_indice_etat(self, data):68 f = data["f"]69 assert cas.indice_etat(f) == pytest.approx(2.809, abs=5e-4)70 assert f["etat"].value_counts().to_dict() == {2: 7, 3: 8, 4: 4, 5: 1}71 r = (f["cout_neuf"] * f["ratio"]).sum() / f["cout_neuf"].sum()72 assert r == pytest.approx(0.547, abs=5e-4) and r * 60 == pytest.approx(32.8, abs=0.05)737475class TestTravaux:76 def test_total(self, data):77 t = data["trav"]78 assert len(t) == 579 assert t["cout"].sum() == pytest.approx(10479.98, abs=0.01)80 assert t["cout"].sum() / data["cn"]["total"] == pytest.approx(0.0264, abs=5e-5)81 assert t.set_index("poste_reference").loc["Entrée asphaltée", "cout"] == pytest.approx(3438.43, abs=0.01)82 assert t.set_index("poste_reference").loc["Fenêtres (PVC double vitrage)", "cout"] == pytest.approx(2788.93, abs=0.01)838485class TestDepreciation:86 def test_identites(self, data):87 dv, cn = data["dv"], data["cn"]88 assert dv["total"] == pytest.approx(dv["curable"] + sum(dv["courte_duree"].values()) + dv["longue_duree"], rel=1e-12)89 courtes_hors_entree = data["f"][(~data["f"]["longue_duree"]) & (data["f"]["composante"] != "Entrée asphaltée")]["cout_neuf"].sum()90 assert dv["base_longue"] == pytest.approx(cn["total"] - dv["curable"] - courtes_hors_entree, abs=1e-6)91 assert dv["longue_duree"] == pytest.approx(dv["base_longue"] * 29.0 / 60.0, rel=1e-12)92 assert dv["contribution_batiment"] == pytest.approx(cn["total"] - dv["total"], rel=1e-12)93 assert len(dv["courte_duree"]) == 189495 def test_valeurs_citees(self, data):96 dv = data["dv"]97 assert dv["curable"] == pytest.approx(10479.98, abs=0.01)98 assert sum(dv["courte_duree"].values()) == pytest.approx(130584.55, abs=0.01)99 assert dv["base_longue"] == pytest.approx(165569.69, abs=0.01)100 assert dv["longue_duree"] == pytest.approx(80025.35, abs=0.01)101 assert dv["total"] == pytest.approx(221089.88, abs=0.01)102 assert dv["taux"] == pytest.approx(0.5561, abs=5e-5)103 assert dv["contribution_batiment"] == pytest.approx(176477.45, abs=0.01)104 assert dv["age_vie_simple"] == pytest.approx(192157.55, abs=0.01)105 assert dv["age_vie_simple"] / dv["cout_neuf"] == pytest.approx(0.4833, abs=5e-5)106 assert dv["total"] - dv["age_vie_simple"] == pytest.approx(28932.34, abs=0.01)107 assert ct.depreciation_age_vie(dv["cout_neuf"], 28, 60) == pytest.approx(185531.42, abs=0.01)108 assert dv["courte_duree"]["Ventilateur récupérateur de chaleur (VRC)"] == pytest.approx(7501.27, abs=0.01)109 assert dv["courte_duree"]["Fenêtres (PVC, double vitrage)"] == pytest.approx(15989.85, abs=0.01)110111112class TestSensibilite:113 def test_grille(self, data):114 s = data["sens"].set_index(["lambda", "etat_structure"])115 assert s.loc[(0.5, 3), "taux"] == pytest.approx(0.5561, abs=5e-5)116 assert s.loc[(0.0, 3), "taux"] == pytest.approx(0.6368, abs=5e-5)117 assert s.loc[(1.0, 1), "taux"] == pytest.approx(0.2738, abs=5e-5)118 assert s.loc[(1.0, 5), "taux"] == pytest.approx(0.6694, abs=5e-5)119 assert s.loc[(0.5, 1), "taux"] == pytest.approx(0.4624, abs=5e-5)120 assert s.loc[(0.5, 5), "taux"] == pytest.approx(0.6602, abs=5e-5)121 for lam in (0.0, 0.25, 0.5, 0.75, 1.0):122 t = [s.loc[(lam, e), "taux"] for e in range(1, 6)]123 assert all(x <= y + 1e-12 for x, y in zip(t, t[1:])) # croissante avec la note d'état124 # à λ = 0 la note d'état n'intervient pas125 assert len({round(s.loc[(0.0, e), "taux"], 9) for e in range(1, 6)}) == 1126 assert s.loc[(1.0, 5), "age_effectif_structure"] == 60.0 and s.loc[(1.0, 1), "age_effectif_structure"] == pytest.approx(3.0)127128129class TestValeurInformation:130 def test_regle(self):131 v = cas.valeur_information(0.10, 3000.0, 50.0)132 assert v["perte_esperee"] == 300.0 and v["valeur_info"] == 250.0 and v["recommander"]133 v = cas.valeur_information(0.05, 25000.0, 1200.0)134 assert v["valeur_info"] == pytest.approx(50.0) and v["recommander"]135 v = cas.valeur_information(0.02, 25000.0, 1200.0)136 assert v["valeur_info"] == pytest.approx(-700.0) and not v["recommander"]137 # seuil de probabilité : p* = t / C138 assert 1200.0 / 25000.0 == pytest.approx(0.048)139140141class TestExercices:142 """Chiffres des solutions (solutions/ch11.tex)."""143144 def test_ex3_ages_effectifs(self):145 # toiture 12 ans / L 20 / état 4 ; chauffe-eau 9 / 12 / état 2 ; fenêtres 35 / 30 / état 2 ; structure 45 / 60 / état 2146 assert cas.age_effectif(12, 20, 4) == pytest.approx(13.5)147 assert cas.age_effectif(9, 12, 2) == pytest.approx(6.0)148 assert cas.age_effectif(35, 30, 2) == pytest.approx(21.25)149 assert cas.age_effectif(45, 60, 2) == pytest.approx(30.0)150 assert ct.vie_restante(60, 30.0) == 30.0151152 def test_ex4_ventilee(self):153 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)]154 dv = ct.depreciation_ventilee(300000.0, 4500.0, comps, 30.0, 60.0)155 assert dv["courte_duree"]["toiture"] == pytest.approx(8100.0)156 assert dv["courte_duree"]["chauffe-eau"] == pytest.approx(750.0)157 assert dv["courte_duree"]["fenêtres"] == pytest.approx(17000.0)158 assert dv["base_longue"] == pytest.approx(258000.0)159 assert dv["longue_duree"] == pytest.approx(129000.0)160 assert dv["total"] == pytest.approx(159350.0)161 assert dv["total"] / 300000.0 == pytest.approx(0.53117, abs=5e-5)162 assert ct.depreciation_age_vie(300000.0, 30.0, 60.0) == pytest.approx(150000.0)163164 def test_ex5_valeur_information(self):165 v = cas.valeur_information(0.08, 20000.0, 900.0)166 assert v["perte_esperee"] == pytest.approx(1600.0) and v["valeur_info"] == pytest.approx(700.0)167 assert 900.0 / 20000.0 == pytest.approx(0.045)168 # dosimètre radon : 50 $ ; seuil p* pour une correction de 2 500 $169 assert 50.0 / 2500.0 == pytest.approx(0.02)170171 def test_ex6_variante_lambda(self, data):172 s = data["sens"].set_index(["lambda", "etat_structure"])173 assert s.loc[(0.75, 3), "taux"] == pytest.approx(0.5093, abs=5e-5)174 assert s.loc[(0.25, 3), "taux"] == pytest.approx(0.5977, abs=5e-5)175 assert s.loc[(0.75, 3), "age_effectif_structure"] == pytest.approx(29.5)176 assert s.loc[(0.25, 3), "age_effectif_structure"] == pytest.approx(28.5)177 assert s.loc[(0.25, 3), "depreciation"] == pytest.approx(237631.21, abs=0.01)178 assert s.loc[(0.75, 3), "depreciation"] == pytest.approx(202500.46, abs=0.01)179 assert s.loc[(0.25, 3), "depreciation"] - s.loc[(0.75, 3), "depreciation"] == pytest.approx(35130.75, abs=0.01)180 d25 = cas.depreciation(data["f"], data["trav"], data["cn"], 0.25); d75 = cas.depreciation(data["f"], data["trav"], data["cn"], 0.75)181 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)182 assert d25["courte_duree"]["Armoires de cuisine et comptoirs"] - d75["courte_duree"]["Armoires de cuisine et comptoirs"] == pytest.approx(5000.84, abs=0.01)183 assert d25["courte_duree"]["Fenêtres (PVC, double vitrage)"] - d75["courte_duree"]["Fenêtres (PVC, double vitrage)"] == pytest.approx(4834.14, abs=0.01)184 assert d25["longue_duree"] - d75["longue_duree"] == pytest.approx(-2759.49, abs=0.01)185