spb/traite-evaluation-qc
Public
1"""Tests du chapitre 22 — métré et quantités du sujet du cas 1.2Tolérances : 0,01 $ sur les montants recalculés depuis le devis ; 0,05 m² / m³ / m sur les quantités géométriques citées3(arrondies à deux décimales dans le texte) ; 5e-4 sur les proportions ; identités à 1e-9.4Les montants du chapitre 21 (D, I, C_N) doivent être strictement inchangés."""5import sys, pathlib, math6import numpy as np, pytest7sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))8import ch21_cout_neuf as c219import ch22_metre as cas10from traite import cout as ct1112Q = 0.05131415@pytest.fixture(scope="module")16def data():17 devis, sujet = c21.charger()18 return {"devis": devis, "sujet": sujet, "e": cas.empreinte(), "S": cas.superficies(), "q": cas.quantites_geometriques(),19 "V": cas.verification(devis), "eff": None, "cn": c21.cout_neuf_sujet(devis)}202122class TestCoherenceCh21:23 def test_montants_inchanges(self, data):24 cn = data["cn"]25 assert cn["direct"] == pytest.approx(298826.17, abs=0.01)26 assert cn["indirects"] == pytest.approx(62598.68, abs=0.01)27 assert cn["total"] == pytest.approx(397567.33, abs=0.01)28 assert len(data["devis"]) == 382930 def test_corps_identiques(self, data):31 g = c21.regrouper(data["devis"])32 assert list(g.index) == c21.ORDRE_CORPS and int(g["postes"].sum()) == 383334 def test_bordereau_quantites_fois_prix(self, data):35 d = data["devis"]36 assert np.allclose(d["quantite"] * d["prix_unitaire"], d["montant"], atol=0.011)37 detail = ct.cout_par_composantes(dict(zip(d["poste"], d["quantite"])), dict(zip(d["poste"], d["prix_unitaire"])))38 assert detail["total"] == pytest.approx(361424.85, abs=0.2)394041class TestGeometrie:42 def test_empreinte_et_perimetre(self, data):43 e = data["e"]44 assert e["aire_au_sol"] == pytest.approx(95.04, abs=1e-9)45 assert e["perimetre"] == pytest.approx(39.84, abs=1e-9)46 assert e["garage"] == pytest.approx(24.55, abs=0.005) and e["aire_sous_sol"] == pytest.approx(70.49, abs=0.005)47 # invariance du périmètre d'une forme en L découpée dans un rectangle48 L, W, gx, gy = cas.PLAN["L"], cas.PLAN["W"], cas.PLAN["gx"], cas.PLAN["gy"]49 perim_L = (L - gx) + gy + gx + (W - gy) + L + W # parcours de la forme en L50 assert perim_L == pytest.approx(2 * (L + W), abs=1e-9)5152 def test_superficies_conventions(self, data):53 S = data["S"]54 assert S["aire_etages_mefq"] == pytest.approx(190.08, abs=1e-9)55 assert S["perimetre_etages_mefq"] == pytest.approx(79.68, abs=1e-9)56 assert S["habitable"] == pytest.approx(165.53, abs=0.005)57 assert round(S["habitable"]) == data["sujet"]["superficie_habitable_m2"]58 assert S["nette_hors_sol"] == pytest.approx(135.73, abs=Q)59 assert S["nette_rez"] == pytest.approx(58.93, abs=Q) and S["nette_etage"] == pytest.approx(76.81, abs=Q)60 assert S["sous_sol_net"] == pytest.approx(62.96, abs=Q) and S["sous_sol_fini_net"] == pytest.approx(53.83, abs=Q)61 assert S["aire_etages_mefq"] * c21.PI2_PAR_M2 == pytest.approx(2046, abs=0.6)62 assert S["habitable"] * c21.PI2_PAR_M2 == pytest.approx(1782, abs=0.6)6364 def test_pieces_couvrent_les_niveaux(self, data):65 pcs = data["S"]["pieces"]66 assert pcs.loc[pcs["niveau"] == "rez", "brute"].sum() == pytest.approx(95.04, abs=1e-6)67 assert pcs.loc[pcs["niveau"] == "etage", "brute"].sum() == pytest.approx(95.04, abs=1e-6)68 assert pcs.loc[pcs["niveau"] == "soussol", "brute"].sum() == pytest.approx(70.49, abs=0.01)69 assert (pcs["nette"] > 0).all()7071 def test_cloisons(self):72 assert cas.cloisons("rez") == pytest.approx(21.62, abs=Q)73 assert cas.cloisons("etage") == pytest.approx(47.94, abs=Q)74 assert cas.cloisons("soussol") == pytest.approx(17.12, abs=Q)75 assert cas.separation_garage() == pytest.approx(10.16, abs=1e-9)7677 def test_formules_elementaires(self):78 # aire décalée d'un rectangle = formule exacte79 assert cas.aire_decalee(12 * 7.92, 2 * (12 + 7.92), 0.9) == pytest.approx((12 + 1.8) * (7.92 + 1.8), abs=1e-9)80 # prismoïde d'un parallélépipède = A·h ; d'une pyramide tronquée : cohérent avec la formule exacte81 assert cas.volume_prismoidal(10, 10, 10, 3) == pytest.approx(30)82 a, b, h = 4.0, 6.0, 3.0 # tronc de pyramide à bases carrées83 exact = h / 3 * (a * a + b * b + a * b)84 assert cas.volume_prismoidal(a * a, ((a + b) / 2) ** 2, b * b, h) == pytest.approx(exact, rel=1e-12)85 assert math.sqrt(1 + 0.5 ** 2) == pytest.approx(1.1180, abs=5e-5)868788class TestQuantites:89 def test_mesures_citees(self, data):90 q = data["q"]91 attendu = {"Excavation et remblai": 409.81, "Fondations béton (semelles, murs)": 36.74, "Dalle de sous-sol": 95.04,92 "Drain français et imperméabilisation": 42.24, "Charpente murs et planchers": 325.05, "Fermes de toit": 111.62,93 "Couverture bardeaux d'asphalte": 124.79, "Revêtement extérieur (brique 40 % / fibrociment 60 %)": 238.79,94 "Isolation et pare-vapeur": 419.18, "Gypse et tirage de joints": 740.32, "Peinture intérieure": 740.32,95 "Planchers bois franc": 103.56, "Planchers céramique": 32.17, "Planchers flottants (sous-sol)": 70.49,96 "Gouttières et solins": 48.80, "Balcon et perron": 14.10, "Entrée asphaltée": 55.00}97 for k, v in attendu.items():98 assert q[k]["q"] == pytest.approx(v, abs=Q), k99100 def test_details_cites(self, data):101 q = data["q"]102 d = q["Charpente murs et planchers"]["detail"]103 assert d["planchers"] == pytest.approx(165.53, abs=Q) and d["murs_brut"] == pytest.approx(194.42, abs=Q)104 assert d["ouvertures"] == pytest.approx(34.90, abs=Q) and d["murs_net"] == pytest.approx(159.52, abs=Q)105 f = q["Fondations béton (semelles, murs)"]["detail"]106 assert f["L_axe"] == pytest.approx(38.84, abs=1e-9) and f["V_murs"] == pytest.approx(23.69, abs=Q) and f["V_semelles"] == pytest.approx(6.99, abs=Q)107 x = q["Excavation et remblai"]["detail"]108 assert x["A_bas"] == pytest.approx(109.58, abs=Q) and x["A_mi"] == pytest.approx(141.85, abs=Q) and x["A_haut"] == pytest.approx(177.50, abs=Q)109 assert x["V_sous_sol"] == pytest.approx(370.27, abs=Q) and x["V_garage"] == pytest.approx(15.24, abs=Q) and x["V_racc"] == pytest.approx(24.30, abs=Q)110 g = q["Gypse et tirage de joints"]["detail"]111 assert g["plafonds"] == pytest.approx(214.11, abs=Q) and g["murs"] == pytest.approx(585.60, abs=Q)112 assert g["sep_garage"] == pytest.approx(24.79, abs=Q) and g["ouvertures"] == pytest.approx(84.19, abs=Q)113 r = q["Revêtement extérieur (brique 40 % / fibrociment 60 %)"]["detail"]114 assert r["murs"] == pytest.approx(223.10, abs=Q) and r["pignons"] == pytest.approx(15.68, abs=Q)115 i = q["Isolation et pare-vapeur"]["detail"]116 assert i["fondation"] == pytest.approx(91.37, abs=Q) and i["rive"] == pytest.approx(23.90, abs=Q) and i["sep_murs"] == pytest.approx(24.79, abs=Q)117118 def test_denombrements(self, data):119 q = data["q"]120 assert q["Fenêtres (PVC double vitrage)"]["q"] == 16 and q["Fenêtres (PVC double vitrage)"]["detail"] == {"rez": 6, "etage": 8, "soussol": 2}121 assert q["Portes intérieures et moulures"]["q"] == 14 and q["Plomberie brute et finition"]["q"] == 14 == len(cas.APPAREILS)122 assert q["Portes extérieures"]["q"] == 3 and q["Porte de garage"]["q"] == 1 and q["Escaliers"]["q"] == 2123 assert q["Supervision de chantier (salaires)"]["q"] == 26124125126class TestVerification:127 def test_verdicts(self, data):128 V = data["V"]129 nb = V["verdict"].value_counts().to_dict()130 assert nb == {"conforme": 20, "ratio": 14, "à justifier": 2, "à préciser": 2}131 assert set(V.loc[V["verdict"] == "à justifier", "poste"]) == {"Gypse et tirage de joints", "Peinture intérieure"}132 assert set(V.loc[V["verdict"] == "à préciser", "poste"]) == {"Planchers bois franc", "Planchers céramique"}133 assert len(V) == 38 and int((V["classe"] == "mesuré").sum()) == 17 and int((V["classe"] == "dénombré").sum()) == 7134135 def test_ecarts_cites(self, data):136 V = data["V"].set_index("poste")137 assert V.loc["Gypse et tirage de joints", "ecart"] == pytest.approx(0.194, abs=5e-4)138 assert V.loc["Planchers bois franc", "ecart"] == pytest.approx(0.090, abs=5e-4)139 assert V.loc["Planchers céramique", "ecart"] == pytest.approx(-0.081, abs=5e-4)140 assert V.loc["Excavation et remblai", "ecart"] == pytest.approx(-0.024, abs=5e-4)141 assert V.loc["Fondations béton (semelles, murs)", "ecart"] == pytest.approx(-0.033, abs=5e-4)142 assert V.loc["Charpente murs et planchers", "ecart"] == pytest.approx(-0.015, abs=5e-4)143 assert V.loc["Gypse et tirage de joints", "delta_montant"] == pytest.approx(3305.12, abs=0.01)144 assert V.loc["Peinture intérieure", "delta_montant"] == pytest.approx(1217.61, abs=0.01)145 # planchers finis hors sol : total conforme même si la répartition diffère146 tot_calc = V.loc["Planchers bois franc", "q_calc"] + V.loc["Planchers céramique", "q_calc"]147 tot_devis = V.loc["Planchers bois franc", "q_devis"] + V.loc["Planchers céramique", "q_devis"]148 assert tot_calc == pytest.approx(135.73, abs=Q) and tot_devis == 130 and abs(tot_calc / tot_devis - 1) < 0.05149150 def test_effet_metre_controle(self, data):151 eff = cas.effet_metre_controle(data["devis"], data["V"])152 assert eff["delta_D"] == pytest.approx(3767.56, abs=0.01)153 assert eff["D2"] == pytest.approx(302593.73, abs=0.01)154 assert eff["CN2"] == pytest.approx(401711.66, abs=0.01)155 assert eff["delta_CN"] == pytest.approx(4144.32, abs=0.01)156 assert eff["part_D"] == pytest.approx(0.0126, abs=5e-5) and eff["part_CN"] == pytest.approx(0.0104, abs=5e-5)157 assert eff["mult"] == pytest.approx(1.3304, abs=5e-5)158 # identité : ΔC_N = ΔD × (1 + τE) lorsque les indirects sont fixés en dollars159 assert eff["delta_CN"] == pytest.approx(eff["delta_D"] * 1.10, rel=1e-9)160161162class TestSensibilitePropagation:163 def test_sensibilite(self, data):164 s = cas.sensibilite_quantites(data["devis"]).set_index("corps")165 assert s.loc["Enveloppe", "dCN_+0.10"] == pytest.approx(7739, abs=1) and s.loc["Enveloppe", "pct_+0.10"] == pytest.approx(0.0195, abs=5e-5)166 assert s.loc["Tous les postes directs", "dCN_+0.10"] == pytest.approx(32871, abs=1) and s.loc["Tous les postes directs", "pct_+0.10"] == pytest.approx(0.0827, abs=5e-5)167 assert s.loc["Aménagements extérieurs", "dCN_-0.10"] == pytest.approx(-1827, abs=1)168 # linéarité : ΔC_N = e × montant × (1 + τE)169 assert s.loc["Enveloppe", "dCN_+0.10"] == pytest.approx(0.10 * s.loc["Enveloppe", "montant"] * 1.10, rel=1e-9)170 assert s["part_D"].iloc[:-1].sum() == pytest.approx(1.0, abs=1e-9)171172 def test_propagation(self, data):173 p = cas.propagation(data["devis"])174 assert p["herfindahl"] == pytest.approx(0.0539, abs=5e-5) and p["equiv_uniforme"] == pytest.approx(18.6, abs=0.05)175 assert p["sigma_D_rel"] == pytest.approx(0.0116, abs=5e-5) and p["sigma_D"] == pytest.approx(3467.78, abs=0.01)176 assert p["poste_max"] == "Charpente murs et planchers" and p["max_part"] == pytest.approx(0.132, abs=5e-4)177 # bornes : 1/n ≤ H ≤ 1 et σ indépendants ≤ σ corrélés178 assert 1 / p["n"] <= p["herfindahl"] <= 1 and p["sigma_D_rel"] <= p["sigma_si_parfaitement_correle"]179180 def test_pertes(self, data):181 per = cas.pertes(data["devis"]).set_index("poste")182 assert per.loc["Gypse et tirage de joints", "q_commander"] == pytest.approx(694.4, abs=Q)183 assert per.loc["Couverture bardeaux d'asphalte", "q_commander"] == pytest.approx(137.5, abs=Q)184 assert per["cout_pertes_si_prix_pose"].sum() == pytest.approx(11283.78, abs=0.01)185186 def test_repartition_types(self, data):187 rep = cas.repartition_par_type(data["devis"])188 tot = rep.sum()189 assert tot["mesuré"] == pytest.approx(174351.56, abs=0.01) and tot["dénombré"] == pytest.approx(89850.00, abs=0.01) and tot["forfait"] == pytest.approx(97223.29, abs=0.01)190 assert tot.sum() == pytest.approx(361424.85, abs=0.01)191 assert rep.loc["Enveloppe", "mesuré"] == pytest.approx(47580.84, abs=0.01) and rep.loc["Coûts indirects", "dénombré"] == pytest.approx(35708.66, abs=0.01)192193194class TestExercices:195 """Chiffres des solutions (solutions/ch22.tex)."""196197 def test_ex1_conventions(self):198 L, W, gx, gy = 10.40, 9.15, 3.66, 6.10199 aire_sol = L * W; etages = 2 * aire_sol; hab = etages - gx * gy200 assert aire_sol == pytest.approx(95.16, abs=0.005) and etages == pytest.approx(190.32, abs=0.005)201 assert gx * gy == pytest.approx(22.33, abs=0.005) and hab == pytest.approx(167.99, abs=0.005)202 assert 2 * (L + W) == pytest.approx(39.10, abs=1e-9)203 net = 2 * (L - 0.6) * (W - 0.6) - (gx - 0.3 - 0.075) * (gy - 0.3 - 0.075)204 assert net == pytest.approx(144.81, abs=0.01)205 assert etages * 10.7639 == pytest.approx(2048.6, abs=0.1) and hab * 10.7639 == pytest.approx(1808.2, abs=0.1)206207 def test_ex2_murs_nets(self):208 P = 2 * (10.40 + 9.15); brut = P * 2.44 * 2209 ouv = 12 * 1.68 + 2 * 1.85 + 2.74 * 2.13210 assert brut == pytest.approx(190.81, abs=0.01) and ouv == pytest.approx(29.70, abs=0.01)211 assert brut - ouv == pytest.approx(161.11, abs=0.01) and ouv / brut == pytest.approx(0.156, abs=5e-4)212213 def test_ex3_toiture(self):214 A = (10.40 + 0.8) * (9.15 + 0.8); f = math.sqrt(1 + (8 / 12) ** 2)215 assert A == pytest.approx(111.44, abs=0.01) and f == pytest.approx(1.2019, abs=5e-5)216 assert A * f == pytest.approx(133.94, abs=0.01) and A * f * 1.10 == pytest.approx(147.33, abs=0.01)217 f6 = math.sqrt(1.25)218 assert A * f / (A * f6) - 1 == pytest.approx(0.0750, abs=5e-5)219220 def test_ex4_excavation(self):221 A, P, h, d = 95.16, 39.10, 2.50, 0.90222 rect = cas.aire_decalee(A, P, d) * h223 Ab = cas.aire_decalee(A, P, d); Am = cas.aire_decalee(A, P, d + 0.5 * h / 2); At = cas.aire_decalee(A, P, d + 0.5 * h)224 V = cas.volume_prismoidal(Ab, Am, At, h)225 Am1 = cas.aire_decalee(A, P, d + 1.0 * h / 2); At1 = cas.aire_decalee(A, P, d + 1.0 * h)226 V1 = cas.volume_prismoidal(Ab, Am1, At1, h)227 assert Ab == pytest.approx(133.59, abs=0.01) and rect == pytest.approx(333.98, abs=0.01)228 assert V == pytest.approx(400.88, abs=0.01) and V1 == pytest.approx(474.12, abs=0.01)229 assert V / rect - 1 == pytest.approx(0.200, abs=5e-4) and V1 / rect - 1 == pytest.approx(0.420, abs=5e-4)230231 def test_ex5_prix_unitaire(self):232 m, w, hpu, s, e = 11.50, 0.12, 0.35, 62.0, 0.80233 p = m * (1 + w) + hpu * s + e234 assert m * (1 + w) == pytest.approx(12.88, abs=0.005) and hpu * s == pytest.approx(21.70, abs=0.005) and p == pytest.approx(35.38, abs=0.005)235 assert hpu * s / p == pytest.approx(0.613, abs=5e-4)236 assert p * 1.15 == pytest.approx(40.69, abs=0.005)237 assert (m * (1 + w) + hpu * s * 1.05 + e) / p - 1 == pytest.approx(0.0307, abs=5e-5)238 assert (m * (1 + w) + hpu * 0.9 * s + e) / p - 1 == pytest.approx(-0.0613, abs=5e-5)239240 def test_ex6_propagation(self, data):241 p = cas.propagation(data["devis"], 0.05); p10 = cas.propagation(data["devis"], 0.10)242 assert p10["sigma_D_rel"] == pytest.approx(2 * p["sigma_D_rel"], rel=1e-12)243 # 33 postes de parts égales244 assert 0.05 * math.sqrt(1 / 33) == pytest.approx(0.0087, abs=5e-5)245 # 2 postes concentrés : parts 0,5 / 0,5 → H = 0,5246 assert 0.05 * math.sqrt(0.5) == pytest.approx(0.0354, abs=5e-5)247 assert p["sigma_D"] * 1.3304 == pytest.approx(4613, abs=2)248249 def test_ex7_plain_pied(self):250 L, W = 14.00, 9.00; P = 2 * (L + W); A = L * W251 assert A == 126 and P == 46252 murs = P * 2.44 - (11 * 1.68 + 2 * 1.85); assert murs == pytest.approx(90.06, abs=0.01)253 plafond = (L - 0.6) * (W - 0.6); assert plafond == pytest.approx(112.56, abs=0.01)254 cloisons = 38.0 * 2.44 * 2; assert cloisons == pytest.approx(185.44, abs=0.01)255 gypse = plafond + murs + cloisons - 2 * 9 * 1.85256 assert gypse == pytest.approx(354.76, abs=0.01)257 ecart = gypse / 300 - 1; assert ecart == pytest.approx(0.1825, abs=5e-4)258 delta = (gypse - 300) * 27.47 + (gypse - 300) * 10.12259 assert delta == pytest.approx(2058.3, abs=0.5)260 toit = (L + 0.8) * (W + 0.8) * math.sqrt(1.25); assert toit == pytest.approx(162.19, abs=0.01)261 assert toit / 150 - 1 == pytest.approx(0.0813, abs=5e-4)262