"""Tests du chapitre 12 — mesurage, superficies, propagation d'erreur (cas 1, données synthétiques). Tolérances : 1e-6 sur les identités géométriques ; 0,5 $ sur les montants cités arrondis au dollar ; 5e-4 sur les proportions ; simulation d'atténuation : moyenne à ±2 écarts-types de la valeur théorique (graine fixée → déterministe).""" import sys, pathlib, math import numpy as np, pytest sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases")) import ch12_mesurage as cas @pytest.fixture(scope="module") def r(): return cas.tout() class TestConversions: def test_facteurs(self): assert cas.M2_PAR_PI2 == 0.3048 ** 2 assert cas.PI2_PAR_M2 == pytest.approx(1 / cas.M2_PAR_PI2, rel=1e-6) assert 165 * cas.PI2_PAR_M2 == pytest.approx(1776.0, abs=0.05) assert 190.08 * cas.PI2_PAR_M2 == pytest.approx(2046.0, abs=0.05) assert 2000 * cas.M2_PAR_PI2 == pytest.approx(185.8, abs=0.05) class TestPlan: def test_superficies_sujet(self, r): s = r["sup"] assert s["aire_sol_m2"] == pytest.approx(95.04, abs=1e-9) and s["perimetre_sol_m"] == pytest.approx(39.84, abs=1e-9) assert s["aire_etages_m2"] == pytest.approx(190.08, abs=1e-9) and s["perimetre_etages_m"] == pytest.approx(79.68, abs=1e-9) assert s["aire_int_niveau_m2"] == pytest.approx(84.1984, abs=1e-6) assert s["habitable_m2"] == pytest.approx(165.0, abs=0.01) # cohérent avec cas1_gatineau_sujet.json (165 m²) assert s["nette_m2"] == pytest.approx(159.94, abs=0.01) assert s["habitable_sous_sol_m2"] == pytest.approx(235.0, abs=0.01) assert s["garage_m2"] == pytest.approx(22.33, abs=0.01) and s["lot_m2"] == pytest.approx(620.0, abs=0.05) assert cas.PLAN["sous_sol_plancher_sous_sol_m"] > 0.4 # le sous-sol n'est pas un étage au sens du MEFQ def test_formule_murs(self, r): s = r["sup"] assert s["ratio_int_ext"] == pytest.approx(s["ratio_murs_formule"], abs=1e-12) assert s["ratio_int_ext"] == pytest.approx(0.8859, abs=5e-5) assert s["aire_etages_m2"] / s["habitable_m2"] == pytest.approx(1.152, abs=5e-4) class TestPrixUnitaires: def test_valeurs(self, r): pu = r["pu"].set_index("convention"); V = r["M"]["V"] assert V == pytest.approx(637952.85, abs=0.5) assert pu.loc["Superficie habitable (convention de l'ouvrage)", "dollars_m2"] == pytest.approx(3866, abs=0.5) assert pu.loc["Aire des étages, brute (MEFQ R0702A)", "dollars_m2"] == pytest.approx(3356, abs=0.5) assert pu.loc["Habitable + sous-sol fini (fiches de courtage)", "dollars_m2"] == pytest.approx(2715, abs=0.5) assert pu.loc["Superficie nette (utile)", "dollars_m2"] == pytest.approx(3989, abs=0.5) assert pu.loc["Aire au sol (MEFQ R0701A)", "dollars_m2"] == pytest.approx(6712, abs=0.5) assert pu.loc["Superficie habitable (convention de l'ouvrage)", "dollars_pi2"] == pytest.approx(359.2, abs=0.05) assert pu.loc["Aire des étages, brute (MEFQ R0702A)", "dollars_pi2"] == pytest.approx(311.8, abs=0.05) class TestPropagation: def test_derivee(self, r): M = r["M"] assert M["beta"] == pytest.approx(0.539, abs=5e-4) assert r["dVdS"] == pytest.approx(M["beta"] * M["V"] / M["S"], rel=1e-12) assert r["dVdS"] == pytest.approx(2084, abs=0.5) assert r["dVdS"] / (M["V"] / M["S"]) == pytest.approx(M["beta"], rel=1e-12) def test_cinq_pour_cent(self, r): p = r["prop"].set_index("erreur_S_pct") assert p.loc[5.0, "lineaire_dollars"] == pytest.approx(17195, abs=0.5) and p.loc[5.0, "lineaire_pct"] == pytest.approx(2.695, abs=5e-4) assert p.loc[5.0, "exact_dollars"] == pytest.approx(17002, abs=0.5) and p.loc[5.0, "exact_pct"] == pytest.approx(2.665, abs=5e-4) assert p.loc[-5.0, "exact_dollars"] == pytest.approx(-17398, abs=0.5) assert p.loc[5.0, "naif_dollars"] == pytest.approx(31898, abs=0.5) assert p.loc[10.0, "exact_pct"] == pytest.approx(5.272, abs=5e-4) and p.loc[-10.0, "exact_pct"] == pytest.approx(-5.521, abs=5e-4) # identité : effet exact = (1+e)^β − 1 ; linéaire = β e for e in (-0.1, -0.05, 0.05, 0.1): assert p.loc[100 * e, "exact_pct"] == pytest.approx(100 * ((1 + e) ** r["M"]["beta"] - 1), rel=1e-9) def test_melange_conventions(self, r): m = r["mel"] assert m["ecart_pct"] == pytest.approx(15.20, abs=5e-3) assert m["sujet_brut_pct"] == pytest.approx(7.93, abs=5e-3) and m["sujet_brut_dollars"] == pytest.approx(50573, abs=0.5) assert m["sujet_hab_pct"] == pytest.approx(-7.35, abs=5e-3) and m["sujet_hab_dollars"] == pytest.approx(-46858, abs=0.5) assert (1 + m["sujet_brut_pct"] / 100) * (1 + m["sujet_hab_pct"] / 100) == pytest.approx(1.0, abs=1e-9) def test_comparable(self, r): c, ec = r["comp"], r["ec"] assert c["id_vente"] == "G0404" and c["prix_vente"] == 506500 and c["superficie_habitable_m2"] == 160 assert ec["A_vrai"] == pytest.approx(8472, abs=0.5) and ec["A_obs"] == pytest.approx(-8086, abs=0.5) assert ec["erreur"] == pytest.approx(-16558, abs=0.5) and ec["erreur_approx"] == pytest.approx(-17065, abs=0.5) def test_variance_combinee(self, r): vc = r["vc"] assert vc["base"]["relatif"] == pytest.approx(0.01181, abs=5e-6) and vc["base"]["dollars"] == pytest.approx(7535, abs=0.5) assert vc["base"]["part_sujet"] == pytest.approx(5 / 6, rel=1e-9) assert vc["sujet_seul"]["dollars"] == pytest.approx(6878, abs=0.5) and vc["comparables_seuls"]["dollars"] == pytest.approx(3076, abs=0.5) assert vc["cinq_pct"]["dollars"] == pytest.approx(17468, abs=0.5) assert vc["base"]["relatif"] ** 2 == pytest.approx(vc["sujet_seul"]["relatif"] ** 2 + vc["comparables_seuls"]["relatif"] ** 2, rel=1e-9) class TestAttenuation: def test_theorie_et_simulation(self, r): att = r["att"].set_index("sigma_u") assert att.attrs["var_e"] == pytest.approx(0.0609, abs=5e-4) and att.attrs["sd_lnS"] == pytest.approx(0.2487, abs=5e-4) assert att.loc[0.0, "beta_moyen"] == pytest.approx(r["M"]["beta"], rel=1e-9) for s in (0.05, 0.10, 0.20): lam = att.attrs["var_e"] / (att.attrs["var_e"] + s ** 2) assert att.loc[s, "lambda_theorique"] == pytest.approx(lam, rel=1e-12) assert abs(att.loc[s, "beta_moyen"] - att.loc[s, "beta_attendu"]) <= 2 * att.loc[s, "beta_ecart_type"] + 1e-3 assert att.loc[0.10, "lambda_theorique"] == pytest.approx(0.859, abs=5e-4) and att.loc[0.20, "lambda_theorique"] == pytest.approx(0.604, abs=5e-4) assert att.loc[0.10, "beta_moyen"] == pytest.approx(0.465, abs=5e-3) assert att["beta_moyen"].is_monotonic_decreasing assert att["V_maison_120"].is_monotonic_increasing and att["V_sujet_165"].is_monotonic_decreasing class TestExercices: def test_ex1(self): # 28 pi × 42 pi, deux étages a_pi2 = 28 * 42; a_m2 = a_pi2 * cas.M2_PAR_PI2 assert a_pi2 == 1176 and a_m2 == pytest.approx(109.25, abs=0.005) assert 2 * a_m2 == pytest.approx(218.5, abs=0.05) and 2 * a_pi2 == 2352 assert 2 * (28 + 42) * 0.3048 == pytest.approx(42.67, abs=0.005) def test_ex3(self): L, W, t = 11.0, 9.0, 0.30 A = L * W; P = 2 * (L + W); Ai = (L - 2 * t) * (W - 2 * t) assert A == 99.0 and P == 40.0 and Ai == pytest.approx(87.36, abs=1e-9) assert Ai / A == pytest.approx(1 - t * P / A + 4 * t ** 2 / A, rel=1e-12) assert 2 * Ai - 3.2 == pytest.approx(171.52, abs=1e-9) assert 2 * A / (2 * Ai - 3.2) == pytest.approx(1.154, abs=5e-4) def test_ex4(self): V, S, b, sig = 520000, 140.0, 0.55, 0.03 dVdS = b * V / S assert dVdS == pytest.approx(2042.86, abs=0.01) and dVdS * sig * S == pytest.approx(8580, abs=0.5) assert V * (1.03 ** b - 1) == pytest.approx(8523, abs=0.5) and V * (0.97 ** b - 1) == pytest.approx(-8639, abs=0.5) assert (V / S) * sig * S == pytest.approx(15600, abs=0.5) def test_ex5(self, r): b = r["M"]["beta"] assert 100 * (math.exp(b * math.log(205 / 178)) - 1) == pytest.approx(7.91, abs=0.01) # comparable : 560 000 $, 172 m² habitables, sujet 178 m² ; sujet mesuré 205 m² bruts par erreur A_ok = 560000 * ((178 / 172) ** b - 1); A_faux = 560000 * ((205 / 172) ** b - 1) assert A_ok == pytest.approx(10447, abs=0.5) and A_faux == pytest.approx(55572, abs=0.5) def test_ex6(self, r): var_e = r["att"].attrs["var_e"] assert var_e / (var_e + 0.08 ** 2) == pytest.approx(0.905, abs=5e-4) assert 0.905 * 0.539 == pytest.approx(0.488, abs=5e-4)