SPB Git
12.1 KB · 187 lines python
1"""Tests du chapitre 21 — coût neuf du sujet du cas 1, IPCB Montréal, sensibilité et exercices.2Tolérances : 0,01 $ sur les montants recalculés depuis le devis ; 0,5 $ sur les montants arrondis cités dans le texte ;31e-6 sur les identités ; valeurs IPCB exactes (lecture du CSV brut de Statistique Canada)."""4import sys, pathlib, math5import numpy as np, pytest6sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))7import ch21_cout_neuf as cas8from traite import cout as ct910TOL = 0.5111213@pytest.fixture(scope="module")14def data():15    devis, sujet = cas.charger()16    return {"devis": devis, "sujet": sujet, "g": cas.regrouper(devis), "cn": cas.cout_neuf_sujet(devis), "sup": cas.superficies(sujet, devis), "ipcb": cas.ipcb_montreal()}171819class TestDevis:20    def test_structure(self, data):21        d = data["devis"]22        assert len(d) == 38 and int((~d["indirect"]).sum()) == 33 and int(d["indirect"].sum()) == 523        assert np.allclose(d["quantite"] * d["prix_unitaire"], d["montant"], atol=0.011)2425    def test_totaux_cites(self, data):26        cn = data["cn"]27        assert cn["direct"] == pytest.approx(298826.17, abs=0.01)28        assert cn["indirects"] == pytest.approx(62598.68, abs=0.01)29        assert cn["direct"] + cn["indirects"] == pytest.approx(361424.85, abs=0.01)30        assert cn["taux_indirects"] == pytest.approx(0.2095, abs=5e-5)31        assert cn["incitatif"] == pytest.approx(36142.49, abs=0.01)32        assert cn["total"] == pytest.approx(397567.33, abs=0.01)3334    def test_regroupement(self, data):35        g = data["g"]36        attendu = {"Fondations et site": 39818.04, "Structure et charpente": 53852.16, "Enveloppe": 70350.56, "Finition intérieure": 67092.26,37                   "Mécanique et électricité": 51105.45, "Aménagements extérieurs": 16607.70, "Coûts indirects": 62598.68}38        for k, v in attendu.items():39            assert g.loc[k, "montant"] == pytest.approx(v, abs=0.01)40        assert int(g["postes"].sum()) == 3841        assert g["montant"].sum() == pytest.approx(data["cn"]["direct"] + data["cn"]["indirects"], abs=0.01)42        assert g.loc["Enveloppe", "part_direct_indirect"] == pytest.approx(0.195, abs=5e-4)4344    def test_identite_multiplicative(self, data):45        cn = data["cn"]46        assert cn["total"] == pytest.approx(cn["direct"] * (1 + cn["taux_indirects"]) * 1.10, rel=1e-9)47        parts = (cn["direct"] / cn["total"], cn["indirects"] / cn["total"], cn["incitatif"] / cn["total"])48        assert parts[0] == pytest.approx(0.752, abs=5e-4) and parts[1] == pytest.approx(0.157, abs=5e-4) and parts[2] == pytest.approx(0.0909, abs=5e-4)49        assert sum(parts) == pytest.approx(1.0, abs=1e-9)5051    def test_variante_assiette_direct(self, data):52        cn = data["cn"]; alt = cas.cout_neuf_sujet(data["devis"], 0.10, "direct")53        assert alt["incitatif"] == pytest.approx(29882.62, abs=0.01) and alt["total"] == pytest.approx(391307.47, abs=0.01)54        assert cn["total"] - alt["total"] == pytest.approx(cn["direct"] * cn["taux_indirects"] * 0.10, rel=1e-9)55        assert cn["total"] - alt["total"] == pytest.approx(6259.87, abs=0.01)5657    def test_unitaires(self, data):58        cn, sup = data["cn"], data["sup"]59        assert sup["habitable_m2"] == 165 and sup["brute_etages_m2"] == 19060        assert sup["habitable_pi2"] == pytest.approx(1776, abs=0.6) and sup["brute_etages_pi2"] == pytest.approx(2045, abs=0.6)61        assert cn["total"] / 165 == pytest.approx(2409, abs=TOL) and cn["total"] / 165 / cas.PI2_PAR_M2 == pytest.approx(223.8, abs=0.05)62        assert cn["total"] / 190 == pytest.approx(2092, abs=TOL) and cn["total"] / 190 / cas.PI2_PAR_M2 == pytest.approx(194.4, abs=0.05)63        di = cn["direct"] + cn["indirects"]64        assert di / 165 == pytest.approx(2190, abs=TOL) and di / 190 / cas.PI2_PAR_M2 == pytest.approx(176.7, abs=0.05)6566    def test_comparaison_altus(self, data):67        sup = data["sup"]; cn = data["cn"]68        lo, hi, mid = 155 * sup["brute_etages_pi2"], 215 * sup["brute_etages_pi2"], 185 * sup["brute_etages_pi2"]69        assert lo == pytest.approx(316997, abs=1) and hi == pytest.approx(439705, abs=1) and mid == pytest.approx(378351, abs=1)70        assert lo < cn["total"] < hi and cn["total"] / mid - 1 == pytest.approx(0.051, abs=2e-3)71        assert 185 * cas.PI2_PAR_M2 == pytest.approx(1991.3, abs=0.05)72        assert 155 * sup["habitable_pi2"] == pytest.approx(275287, abs=1) and 215 * sup["habitable_pi2"] == pytest.approx(381849, abs=1)737475class TestIPCB:76    def test_series(self, data):77        ip = data["ipcb"]78        mi = ip[ip["type"] == "Maison individuelle [621112]"]79        assert len(mi) == 39 and mi["periode"].iloc[0] == "2017-01" and mi["periode"].iloc[-1] == "2026-07"80        assert mi["indice"].iloc[0] == 66.1 and mi["indice"].iloc[-1] == 116.581        res = ip[ip["type"] == "Bâtiments résidentiels [621]"]82        assert res["indice"].iloc[0] == 65.5 and res["indice"].iloc[-1] == 117.88384    def test_valeurs_tableau(self, data):85        ip = data["ipcb"]86        attendu = {"Bâtiments résidentiels [621]": [65.5, 71.6, 86.0, 99.9, 108.5, 117.8], "Maison individuelle [621112]": [66.1, 71.9, 86.3, 100.0, 107.3, 116.5],87                   "Immeuble à hauteur restreinte (moins de cinq étages) [62111]": [64.8, 70.9, 85.9, 99.7, 108.5, 118.5],88                   "Tour d'habitations (cinq étages ou plus) [621111]": [69.9, 76.7, 85.6, 100.6, 110.8, 119.5], "Bâtiments non résidentiels [622]": [67.3, 75.0, 85.3, 100.4, 108.5, 114.4]}89        for t, vals in attendu.items():90            for p, v in zip(["2017-01", "2019-07", "2021-07", "2023-07", "2025-07", "2026-07"], vals):91                assert cas.indice(ip, t, p) == v, (t, p)9293    def test_ratios_et_variations(self, data):94        ip = data["ipcb"]; mi = "Maison individuelle [621112]"95        assert cas.indice(ip, mi, "2026-07") / cas.indice(ip, mi, "2025-07") - 1 == pytest.approx(0.0857, abs=5e-4)96        assert cas.indice(ip, mi, "2026-07") / cas.indice(ip, mi, "2017-01") == pytest.approx(1.7625, abs=5e-4)97        assert cas.indice(ip, mi, "2026-07") / cas.indice(ip, mi, "2019-07") == pytest.approx(1.620, abs=5e-4)98        assert (1.7625 ** (1 / 9.5) - 1) == pytest.approx(0.0615, abs=5e-4)99        assert cas.indice(ip, mi, "2022-10") / cas.indice(ip, mi, "2020-01") - 1 == pytest.approx(0.36, abs=5e-3)100        nr = "Bâtiments non résidentiels [622]"101        r_nr = cas.indice(ip, nr, "2026-07") / cas.indice(ip, nr, "2019-07")102        assert r_nr == pytest.approx(1.525, abs=5e-4) and r_nr / 1.6203 - 1 == pytest.approx(-0.059, abs=1e-3)103104    def test_indexation_exemples(self, data):105        ip = data["ipcb"]; mi = "Maison individuelle [621112]"; cn = data["cn"]["total"]106        assert ct.indexer_cout(cn, cas.indice(ip, mi, "2026-07"), cas.indice(ip, mi, "2021-07")) == pytest.approx(294507, abs=1)107        assert ct.indexer_cout(cn, cas.indice(ip, mi, "2026-07"), cas.indice(ip, mi, "2019-07")) == pytest.approx(245366, abs=1)108        assert ct.indexer_cout(290000, cas.indice(ip, mi, "2019-07"), cas.indice(ip, mi, "2026-07")) == pytest.approx(469889, abs=1)109110111class TestSensibilite:112    def test_grille(self, data):113        cn = data["cn"]114        ti = np.array([0.12, 0.15, 0.18, cn["taux_indirects"], 0.24, 0.27]); tc = np.array([0.05, 0.08, 0.10, 0.12, 0.15])115        G = cas.grille_sensibilite(cn["direct"], ti, tc)116        assert G.min() == pytest.approx(351420, abs=1) and G.max() == pytest.approx(436436, abs=1)117        assert G.max() / G.min() - 1 == pytest.approx(0.242, abs=1e-3)118        assert G[3, 2] == pytest.approx(cn["total"], abs=0.01)119        # effet marginal d'un point de taux120        assert (G[4, 2] - G[1, 2]) / 9 == pytest.approx(3290, abs=5) and (G[3, 4] - G[3, 0]) / 10 == pytest.approx(3614, abs=5)121122    def test_tornade(self, data):123        cn = data["cn"]; ip = data["ipcb"]; mi = "Maison individuelle [621112]"124        var = cas.indice(ip, mi, "2026-07") / cas.indice(ip, mi, "2025-07") - 1125        t = cas.tornade(cn["direct"], cn["taux_indirects"], 0.10, (1 / (1 + var), 1 + var), (-0.05, -0.20))126        d = {o["param"]: o for o in t}127        assert d["Taux d'indirects"]["bas"] == pytest.approx(381132, abs=1) and d["Taux d'indirects"]["haut"] == pytest.approx(414003, abs=1)128        assert d["Taux d'incitatif"]["bas"] == pytest.approx(379496, abs=1) and d["Taux d'incitatif"]["haut"] == pytest.approx(415639, abs=1)129        assert d["Indice de coûts (date)"]["bas"] == pytest.approx(366171, abs=1) and d["Indice de coûts (date)"]["haut"] == pytest.approx(431655, abs=1)130        assert (431655 - 366171) / cn["total"] == pytest.approx(0.165, abs=1e-3)131132    def test_elasticite(self, data):133        um = data["cn"]["total"] / 165134        assert ct.cout_par_superficie_ajuste(um, 165, 200, -0.05) * 200 == pytest.approx(477287, abs=1)135        assert ct.cout_par_superficie_ajuste(um, 165, 200, -0.20) * 200 == pytest.approx(463711, abs=1)136        assert ct.cout_par_superficie_ajuste(um, 165, 140, -0.05) * 140 == pytest.approx(340112, abs=1)137        assert ct.cout_par_superficie_ajuste(um, 165, 140, -0.20) * 140 == pytest.approx(348599, abs=1)138        assert ct.cout_par_superficie_ajuste(2300, 200, 165, -0.10) * 165 == pytest.approx(386871, abs=1)139        assert 2300 * 165 == 379500140        # élasticité du coût total = 1 + ε141        assert (ct.cout_par_superficie_ajuste(1, 100, 120, -0.1) * 120) / (1 * 100) == pytest.approx(1.2 ** 0.9, rel=1e-12)142143144class TestExercices:145    def test_ex1(self):146        a = ct.cout_neuf(250000, 0.15, 0.10, "direct+indirect"); b = ct.cout_neuf(250000, 0.15, 0.10, "direct")147        assert a["total"] == 316250 and b["total"] == 312500 and a["total"] - b["total"] == 3750148        assert a["direct"] / a["total"] == pytest.approx(0.791, abs=5e-4) and a["indirects"] / a["total"] == pytest.approx(0.119, abs=5e-4)149150    def test_ex2(self, data):151        ip = data["ipcb"]152        assert ct.indexer_cout(310000, 86.3, 116.5) == pytest.approx(418482, abs=1)153        assert ct.indexer_cout(310000, cas.indice(ip, "Bâtiments non résidentiels [622]", "2021-07"), cas.indice(ip, "Bâtiments non résidentiels [622]", "2026-07")) == pytest.approx(415756, abs=1)154155    def test_ex3(self):156        repro = 0.7 * 520000 - (0.7 * 40000 + 12000); rempl = 0.7 * 480000 - 12000157        assert repro == rempl == 324000 and rempl - 40000 == 284000158159    def test_ex4(self):160        k = cas.PI2_PAR_M2161        assert 155 * k == pytest.approx(1668.4, abs=0.05) and 215 * k == pytest.approx(2314.2, abs=0.05)162        u = ct.cout_par_superficie_ajuste(185 * k, 190, 210, -0.10)163        assert u == pytest.approx(1971.5, abs=0.05) and u * 210 == pytest.approx(414013, abs=1) and 185 * k * 210 == pytest.approx(418178, abs=1)164165    def test_ex5(self, data):166        d = data["devis"]; sup = float(d.loc[d["poste"] == "Supervision de chantier (salaires)", "montant"].iloc[0])167        assert sup == pytest.approx(35708.66, abs=0.01)168        D2 = data["cn"]["direct"] + sup; I2 = data["cn"]["indirects"] - sup169        assert I2 / D2 == pytest.approx(0.0804, abs=5e-5) and D2 + I2 == pytest.approx(361424.85, abs=0.01)170        assert ct.cout_neuf(D2, I2 / D2, 0.10)["total"] == pytest.approx(397567.33, abs=0.01)171        assert ct.cout_neuf(D2, I2 / D2, 0.10, "direct")["total"] == pytest.approx(394878.33, abs=0.01)172173    def test_ex6(self):174        a, b, A = 1500, 900, 95175        for kappa, part, eps, c in [(4.0, 0.1976, -0.0988, 1869.4), (3 * math.sqrt(2), 0.2071, -0.1035, 1891.8)]:176            P = kappa * math.sqrt(A); w = b * P / (a * A + b * P)177            assert w == pytest.approx(part, abs=5e-4) and -0.5 * w == pytest.approx(eps, abs=5e-4) and (a * A + b * P) / A == pytest.approx(c, abs=0.05)178        assert 1500 + 900 * 4 / math.sqrt(190) == pytest.approx(1761.2, abs=0.05)179        assert 1869.4 * 2 ** -0.0988 == pytest.approx(1745.7, abs=0.1)180181    def test_ex7(self, data):182        g = data["g"]; cn = data["cn"]183        Dass = cn["direct"] - g.loc["Fondations et site", "montant"] - g.loc["Aménagements extérieurs", "montant"]184        r = ct.cout_neuf(Dass, cn["taux_indirects"], 0.10)185        assert Dass == pytest.approx(242400.43, abs=0.01) and r["indirects"] == pytest.approx(50778.51, abs=0.01) and r["total"] == pytest.approx(322497, abs=1)186        assert 180000 + 0.65 * cn["total"] == pytest.approx(438419, abs=1)187
Files · 81b1896e4502a619cf4f142fb8809253a8ff0bf3
Menu

Theme