spb/traite-evaluation-qc
Public
1"""Tests du chapitre 41 — PHMU, terrains et développement (DONNÉES SYNTHÉTIQUES du cas 1 ; zonage FICTIF ; hypothèses déclarées).2Tolérances : 1 $ sur les montants cités arrondis au dollar ; 0,05 point sur les pourcentages à une décimale ; 1e-9 sur les identités.3Les valeurs « citées » sont celles de manuscript/chapters/ch41.tex et de solutions/ch41.tex."""4import sys, pathlib, math5import numpy as np, pytest6sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))7import ch41_phmu as cas8import ch08_zonage as c089from traite import finance as fin1011TOL = 1.0121314@pytest.fixture(scope="module")15def R():16 return cas.exemple_complet()171819class TestIntrants:20 def test_couts_ch21(self, R):21 cu = R["couts"]22 assert cu["direct"] == pytest.approx(298826.17, abs=0.01) and cu["indirects"] == pytest.approx(62598.68, abs=0.01)23 assert cu["cout_neuf"] == pytest.approx(397567.33, abs=0.01)24 assert cu["di_m2_maison"] == pytest.approx(2190.45, abs=0.01)25 assert cu["ratio_ipcb_plex"] == pytest.approx(118.5 / 116.5, rel=1e-12) and round(cu["ratio_ipcb_plex"], 4) == 1.017226 assert cu["di_m2_plex"] == pytest.approx(2228.06, abs=0.01)2728 def test_prix_sortie(self, R):29 u = R["usages"]30 assert u["S0"]["vente"]["prix_isolee_equiv"] == pytest.approx(637953, abs=TOL) # prédiction hédonique du sujet (ch. 18)31 assert u["U"]["vente"]["gdv"] == pytest.approx(720727, abs=TOL)32 assert u["D"]["vente"]["prix_isolee_equiv"] == pytest.approx(550726, abs=TOL) and u["D"]["vente"]["prix_unite"] == pytest.approx(495654, abs=TOL)33 assert u["D"]["vente"]["gdv"] == pytest.approx(991307, abs=TOL)34 assert u["T"]["vente"]["prix_isolee_equiv"] == pytest.approx(480573, abs=TOL) and u["T"]["vente"]["prix_unite"] == pytest.approx(408487, abs=TOL)35 assert u["T"]["vente"]["gdv"] == pytest.approx(1225460, abs=TOL)36 assert u["Q"]["vente"]["gdv"] == pytest.approx(1371384, abs=TOL)37 assert u["D"]["vente"]["prix_unite"] == pytest.approx(0.90 * u["D"]["vente"]["prix_isolee_equiv"], rel=1e-12)38 assert u["T"]["vente"]["prix_unite"] == pytest.approx(0.85 * u["T"]["vente"]["prix_isolee_equiv"], rel=1e-12)3940 def test_en_bloc_cas2(self, R):41 tb = R["usages"]["T_bloc"]42 assert tb["prix_par_logement_triplex"] == pytest.approx(281000, abs=TOL) and tb["gdv"] == pytest.approx(843000, abs=TOL)43 assert tb["promoteur"]["valeur_residuelle"] == pytest.approx(-28102, abs=TOL) and tb["promoteur"]["reste"] < 0444546class TestQuatreTests:47 def test_legal(self, R):48 u = R["usages"]49 assert u["U"]["legal"]["logements_admissibles"] == 3 and u["U"]["legal"]["aire_max_m2"] == pytest.approx(372.0)50 assert u["D"]["legal"]["aire_brute_m2"] == pytest.approx(240 / 0.85) and u["D"]["legal"]["permis"]51 assert u["T"]["legal"]["aire_brute_m2"] == pytest.approx(300 / 0.85) and u["T"]["legal"]["permis"]52 assert not u["Q"]["legal"]["permis"] and not u["Q"]["legal"]["ok_cos"] and not u["Q"]["legal"]["ok_logements"]53 assert c08.densite_admissible(620, 150, 3, 0.60, 450)["logements_par_densite"] == 4 # la densité seule admettrait 4 ; le plafond de la grille (3) l'emporte5455 def test_physique(self, R):56 for k in ("U", "D", "T"):57 tp = R["usages"][k]["physique"]58 assert tp["possible"] and tp["etages_requis"] <= 3 and tp["empreinte_max_m2"] == pytest.approx(16 * 16)596061class TestResiduel:62 def test_valeurs_citees_promoteur(self, R):63 u = R["usages"]64 attendu = {"U": (158849, 185550, 6814, 108109, 28829), "D": (190541, 225936, 12290, 148696, 39652), "T": (229265, 275967, 18239, 183819, 49018), "Q": (244935, 299288, 23994, 205708, 54855)}65 for k, (vl, reste, F, marge, frais) in attendu.items():66 r = u[k]["promoteur"]67 assert r["valeur_residuelle"] == pytest.approx(vl, abs=TOL) and r["reste"] == pytest.approx(reste, abs=TOL)68 assert r["financement"] == pytest.approx(F, abs=TOL) and r["marge"] == pytest.approx(marge, abs=TOL) and r["frais_vente"] == pytest.approx(frais, abs=TOL)69 assert u["T"]["promoteur"]["couts_construction"] == pytest.approx(668417, abs=TOL) and u["T"]["promoteur"]["contribution_parcs"] == pytest.approx(22927, abs=TOL)70 assert u["T"]["promoteur"]["v_l_statique"] == pytest.approx(250879, abs=TOL) and u["T"]["promoteur"]["facteur_temps"] == pytest.approx(1.0619 ** 1.5, rel=1e-12)71 assert u["D"]["promoteur"]["couts_construction"] == pytest.approx(534734, abs=TOL) and u["U"]["promoteur"]["couts_construction"] == pytest.approx(361425, abs=TOL)7273 def test_valeurs_citees_occupant(self, R):74 u = R["usages"]75 assert u["U"]["occupant"]["valeur_residuelle"] == pytest.approx(266187, abs=TOL) and u["U"]["occupant"]["marge"] == pytest.approx(39824, abs=TOL)76 assert u["D"]["occupant"]["valeur_residuelle"] == pytest.approx(330792, abs=TOL) and u["T"]["occupant"]["valeur_residuelle"] == pytest.approx(399479, abs=TOL)77 # marge sur le coût = 10 % de (C + D + F)78 r = u["U"]["occupant"]; assert r["marge"] == pytest.approx(0.10 * (r["couts_construction"] + r["demolition"] + r["financement"]), rel=1e-12)7980 def test_identites(self, R):81 r = R["usages"]["T"]["promoteur"]; h = cas.HYP82 # reste = GDV − frais − C − D − F − marge ; V_L(1+τ)(1+r)^T = reste ; parcs = τ V_L83 assert r["reste"] == pytest.approx(r["gdv"] - r["frais_vente"] - r["couts_construction"] - r["demolition"] - r["financement"] - r["marge"], rel=1e-12)84 assert r["valeur_residuelle"] * (1 + h["taux_parcs"]) * r["facteur_temps"] == pytest.approx(r["reste"], rel=1e-12)85 assert r["contribution_parcs"] == pytest.approx(h["taux_parcs"] * r["valeur_residuelle"], rel=1e-12)86 # intérêts intérimaires : I = L i T_eff / 2 avec T_eff = T(1 − part mise de fonds)87 L = h["part_pret"] * (r["couts_construction"] + r["demolition"]); T_eff = 1.5 * (1 - (1 - h["part_pret"]))88 assert r["financement"] == pytest.approx(L * h["taux_pret"] * T_eff / 2, rel=1e-12)89 # forme linéaire V_L = a·GDV − b′ et seuil90 a, b_prime = cas._a_b(r, h)91 assert a * r["gdv"] - b_prime == pytest.approx(r["valeur_residuelle"], rel=1e-10)92 s = cas.seuil_gdv(r, R["v_act"]); r2 = cas.residuel_developpement(s, 300.0, R["couts"]["di_m2_plex"], 1.5, h)93 assert r2["valeur_residuelle"] == pytest.approx(R["v_act"], rel=1e-9)9495 def test_version_statique_egale_ch08(self, R):96 """Avec r = 0, marge et financement en % du GDV et frais de vente nuls, la fonction retrouve valeur_residuelle_redeveloppement du ch. 8."""97 gdv, s_tot, di = 1_000_000.0, 300.0, 2000.098 h = {**cas.HYP, "frais_vente": 0.0, "r_portage": 0.0}99 r = cas.residuel_developpement(gdv, s_tot, di, 1.0, h)100 ref = c08.valeur_residuelle_redeveloppement(3, 100.0, gdv / 300.0, di, 0.0, h["demolition"], h["marge"], r["financement"] / gdv, h["taux_parcs"])101 assert r["valeur_residuelle"] == pytest.approx(ref["valeur_residuelle"], rel=1e-10)102103 def test_seuils_et_conclusion(self, R):104 assert R["seuil_gdv_T"] == pytest.approx(1776390, abs=TOL) and R["hausse_requise_T"] * 100 == pytest.approx(45.0, abs=0.05)105 assert R["seuil_gdv_T_marche"] == pytest.approx(1296396, abs=TOL) and R["hausse_requise_T_marche"] * 100 == pytest.approx(5.8, abs=0.05)106 assert R["phmu_vacant"] == "U" and R["phmu_vacant_promoteur"] == "T" and R["phmu_ameliore"] == "S0"107 assert R["contribution_batiment"] == pytest.approx(323000) and all(p == 0 for p in R["primes"].values())108109 def test_sensibilite_citee(self, R):110 s = R["sensibilite"].set_index("parametre")111 assert s.loc["Prix des unités", "bas"] == pytest.approx(105569, abs=TOL) and s.loc["Prix des unités", "haut"] == pytest.approx(352962, abs=TOL)112 assert s.loc["Coût de construction", "bas"] == pytest.approx(286246, abs=TOL) and s.loc["Coût de construction", "haut"] == pytest.approx(172285, abs=TOL)113 assert s.loc["Marge du promoteur", "bas"] == pytest.approx(280169, abs=TOL) and s.loc["Marge du promoteur", "haut"] == pytest.approx(178362, abs=TOL)114 assert s.loc["Durée totale", "bas"] == pytest.approx(241459, abs=TOL) and s.loc["Durée totale", "haut"] == pytest.approx(217582, abs=TOL)115 assert s.loc["Taux (prêt et portage)", "bas"] == pytest.approx(236137, abs=TOL) and s.loc["Taux (prêt et portage)", "haut"] == pytest.approx(221939, abs=TOL)116 assert s.loc["Contribution pour fins de parcs", "bas"] == pytest.approx(252192, abs=TOL)117 assert s.loc["Frais de vente", "bas"] == pytest.approx(249627, abs=TOL) and s.loc["Frais de vente", "haut"] == pytest.approx(208904, abs=TOL)118 # la hausse de prix de 15 % fait passer le triplex (promoteur) au-dessus de la maison neuve (occupant) : bascule de la PHMU du terrain vacant119 assert s.loc["Prix des unités", "haut"] > R["usages"]["U"]["occupant"]["valeur_residuelle"] > s.loc["Prix des unités", "base"]120121122class TestOption:123 def test_parametres(self, R):124 o = R["option"]125 assert o["u"] == pytest.approx(math.exp(0.10 * math.sqrt(5)), rel=1e-12) and o["d"] == pytest.approx(1 / o["u"], rel=1e-12)126 assert round(o["u"], 4) == 1.2506 and round(o["d"], 4) == 0.7996 and round(o["F"], 4) == 0.8860 and round(o["q"], 4) == 0.1915 and round(o["disc"], 4) == 0.9057127 assert 0 < o["q"] < 1 and o["F"] == pytest.approx(((1.02) / 1.045) ** 5, rel=1e-12)128 assert round(R["a"], 4) == 0.6729 and R["b"] == pytest.approx(1195378, abs=TOL)129130 def test_noeuds_cites(self, R):131 n = R["option"]["noeuds"]132 assert n[(1, 1)]["gdv"] == pytest.approx(1532535, abs=TOL) and n[(1, 0)]["gdv"] == pytest.approx(979914, abs=TOL)133 assert n[(2, 2)]["gdv"] == pytest.approx(1916556, abs=TOL) and n[(2, 1)]["gdv"] == pytest.approx(1225460, abs=TOL) and n[(2, 0)]["gdv"] == pytest.approx(783568, abs=TOL)134 assert n[(2, 2)]["exercice"] == pytest.approx(94322, abs=TOL) and n[(2, 1)]["exercice"] == 0 and n[(2, 0)]["exercice"] == 0135 assert n[(1, 1)]["continuation"] == pytest.approx(16357, abs=TOL) and n[(1, 1)]["exercice"] == 0 and not n[(1, 1)]["exercer"]136 assert n[(0, 0)]["valeur"] == pytest.approx(2837, abs=TOL)137138 def test_prime_et_identites(self, R):139 o = R["option"]140 assert o["prime_americaine"] == pytest.approx(2837, abs=TOL) and o["prime_europeenne"] == pytest.approx(o["prime_americaine"], rel=1e-12)141 assert o["van_immediate"] == 0.0 and o["prime_americaine"] >= o["van_immediate"]142 # valeur actualisée risque-neutre du seul nœud dans la monnaie : q² × 94 322 × disc²143 assert o["prime_americaine"] == pytest.approx(o["q"] ** 2 * o["noeuds"][(2, 2)]["exercice"] * o["disc"] ** 2, rel=1e-10)144 assert o["gdv_seuil_van"] == pytest.approx(R["seuil_gdv_T"], rel=1e-10)145 assert R["frontiere_t1"] == pytest.approx(1874959, abs=TOL) and R["frontiere_t1"] > o["gdv_seuil_van"]146 assert o["prime_americaine"] / R["v_act"] * 100 == pytest.approx(0.47, abs=0.005)147148 def test_sensibilite_option(self, R):149 s = R["sens_option"]150 assert s[600000.0][0.05] == 0.0 and s[600000.0][0.10] == pytest.approx(2837, abs=TOL) and s[600000.0][0.15] == pytest.approx(21420, abs=TOL)151 assert s[600000.0][0.20] == pytest.approx(47939, abs=TOL) and s[600000.0][0.25] == pytest.approx(77556, abs=TOL)152 assert s[450000.0][0.10] == pytest.approx(7348, abs=TOL) and s[300000.0][0.10] == pytest.approx(23568, abs=TOL) and s[300000.0][0.25] == pytest.approx(132208, abs=TOL)153 for v in s: # la prime croît avec σ et décroît avec la valeur en usage154 vals = list(s[v].values()); assert all(b >= a for a, b in zip(vals, vals[1:]))155 assert s[300000.0][0.15] > s[450000.0][0.15] > s[600000.0][0.15]156157 def test_americaine_superieure_europeenne_quand_exercice_anticipe(self):158 """Avec un rendement de détention élevé et une forte volatilité, l'exercice anticipé a de la valeur."""159 o = cas.option_redeveloppement(1_000_000, 0.7, 500_000, 0.30, 5.0, 0.02, 0.08, 2)160 assert o["prime_americaine"] >= o["prime_europeenne"] and o["prime_americaine"] >= o["van_immediate"]161 assert any(nd.get("exercer") for nd in o["noeuds"].values())162163 def test_frontiere_coherente(self):164 a, b = 0.7, 1_000_000.0165 f = cas.frontiere_exercice_t1(a, b, 0.20, 5.0, 0.02, 0.045)166 p = cas.parametres_binomiaux(0.20, 5.0, 0.02, 0.045)167 cont = p["disc"] * (p["q"] * max(a * f * p["u"] - b, 0) + (1 - p["q"]) * max(a * f * p["d"] - b, 0))168 assert a * f - b == pytest.approx(cont, rel=1e-6) and f >= b / a169170171class TestCapozzaHelsley:172 def test_valeurs_citees(self, R):173 ch = R["capozza"]; cr = ch["resultat"]174 assert ch["R_A"] == pytest.approx(27000, abs=TOL) and ch["R_T"] == pytest.approx(55146, abs=TOL) and ch["C"] == pytest.approx(716656, abs=TOL)175 assert cr["R_certitude"] == pytest.approx(59250, abs=TOL) and cr["prime_irreversibilite"] == pytest.approx(13249, abs=TOL) and cr["R_seuil"] == pytest.approx(72498, abs=TOL)176 assert ch["hausse_requise_certitude"] * 100 == pytest.approx(7.4, abs=0.05) and ch["hausse_requise_incertitude"] * 100 == pytest.approx(31.5, abs=0.05)177178 def test_limite_certitude_et_racine(self):179 r, g, s = 0.045, 551.0, 5515.0180 out = cas.seuil_capozza_helsley(27000, 716656, r, g, s); al = out["alpha"]181 assert 0.5 * s * s * al * al + g * al - r == pytest.approx(0, abs=1e-9) # α racine positive de ½σ²α² + gα − r = 0182 assert cas.seuil_capozza_helsley(27000, 716656, r, g, 0.0)["R_seuil"] == pytest.approx(27000 + r * 716656, rel=1e-12)183 assert cas.seuil_capozza_helsley(27000, 716656, r, g, 1e-3)["prime_irreversibilite"] == pytest.approx(0.0, abs=1e-6)184 assert out["prime_irreversibilite"] > 0 and cas.seuil_capozza_helsley(27000, 716656, r, g, 2 * s)["prime_irreversibilite"] > out["prime_irreversibilite"]185186187class TestExercices:188 """Chiffres des solutions (solutions/ch41.tex)."""189 def test_ex2_residuel_statique(self):190 # 6 logements de 95 m² à 4 900 $/m² ; coûts 2 300 $/m² ; indirects 15 % ; démolition 45 000 $ ; marge 15 % ; financement 3 % ; parcs 10 %191 r = c08.valeur_residuelle_redeveloppement(6, 95.0, 4900.0, 2300.0, 0.15, 45000.0, 0.15, 0.03, 0.10)192 assert r["gdv"] == pytest.approx(2793000) and r["couts_directs"] == pytest.approx(1311000) and r["couts_indirects"] == pytest.approx(196650)193 assert r["reste_avant_parcs"] == pytest.approx(737610) and r["valeur_residuelle"] == pytest.approx(670554.5, abs=0.1)194 # (b) sans parcs ni financement : 737 610 + 83 790 = 821 400195 r2 = c08.valeur_residuelle_redeveloppement(6, 95.0, 4900.0, 2300.0, 0.15, 45000.0, 0.15, 0.0, 0.0)196 assert r2["valeur_residuelle"] == pytest.approx(821400)197198 def test_ex3_calendrier(self):199 h = {**cas.HYP, "demolition": 0.0}200 r = cas.residuel_developpement(1_800_000, 400.0, 2300.0, 2.0, h)201 assert r["couts_construction"] == pytest.approx(920000) and r["pret"] == pytest.approx(690000)202 assert r["financement"] == pytest.approx(690000 * 0.0619 * 1.5 / 2, rel=1e-12) and r["financement"] == pytest.approx(32033.25, abs=0.01)203 assert r["reste"] == pytest.approx(1800000 - 72000 - 920000 - 32033.25 - 270000, abs=0.01)204 assert r["valeur_residuelle"] == pytest.approx(505966.75 / (1.1 * 1.0619 ** 2), rel=1e-9) and r["valeur_residuelle"] == pytest.approx(407908, abs=TOL)205 r0 = cas.residuel_developpement(1_800_000, 400.0, 2300.0, 2.0, {**h, "r_portage": 0.0})206 assert r0["valeur_residuelle"] == pytest.approx(459970, abs=TOL)207208 def test_ex5_binomial(self):209 o = cas.option_redeveloppement(1_000_000, 0.70, 800_000, 0.15, 5.0, 0.02, 0.04, 2)210 assert round(o["u"], 4) == 1.3985 and round(o["d"], 4) == 0.7150 and round(o["q"], 4) == 0.2815 and round(o["F"], 4) == 0.9075211 assert o["noeuds"][(2, 2)]["exercice"] == pytest.approx(0.70 * 1_000_000 * o["u"] ** 2 - 800_000, rel=1e-12) and o["noeuds"][(2, 2)]["exercice"] == pytest.approx(569089, abs=TOL)212 assert o["prime_americaine"] == pytest.approx(45636, abs=TOL) and o["prime_europeenne"] == pytest.approx(37007, abs=TOL) and o["van_immediate"] == 0.0213 assert o["noeuds"][(1, 1)]["exercice"] == pytest.approx(178960, abs=TOL) and o["noeuds"][(1, 1)]["continuation"] == pytest.approx(145121, abs=TOL)214 assert o["noeuds"][(1, 1)]["exercer"] # exercer vaut plus qu'attendre au nœud haut : option américaine > européenne215 assert o["gdv_seuil_van"] == pytest.approx(800000 / 0.70, rel=1e-12)216217 def test_ex6_capozza(self):218 out = cas.seuil_capozza_helsley(18000, 600000, 0.05, 400, 4000)219 assert out["R_certitude"] == pytest.approx(48000) and out["prime_irreversibilite"] == pytest.approx(9266, abs=TOL) and out["R_seuil"] == pytest.approx(57266, abs=TOL)220 assert out["R_seuil"] / 42000 - 1 == pytest.approx(0.3635, abs=5e-4) # rente courante de 42 000 $ : hausse requise de 36,3 %221222 def test_ex4_teardown(self):223 # prix 455 000 $ ; démolition 28 000 $ ; valeur du terrain extraite = 483 000 $ ; contribution du bâtiment −28 000 $224 assert 455000 + 28000 == 483000225 # ratio V_L / P = 1,06 > 1 ; part du bâtiment négative226 assert (455000 - 483000) / 455000 == pytest.approx(-0.0615, abs=1e-4)227228 def test_ex1_grille(self):229 g = c08.densite_admissible(900, 200, 4, 0.70, 450)230 assert g["logements_admissibles"] == 4 and g["aire_plancher_max_m2"] == pytest.approx(630.0)231 assert 6 * 80 / 0.85 == pytest.approx(564.7, abs=0.05) and 4 * 110 / 0.85 == pytest.approx(517.6, abs=0.05)232233 def test_ex7_variantes(self, R):234 h7 = {**cas.HYP, "marge": 0.12, "frais_vente": 0.03}235 attendu = {"U": 183529, "D": 223981, "T": 269989}236 for k, v in attendu.items():237 u = R["usages"][k]238 assert cas.residuel_developpement(u["vente"]["gdv"], u["vente"]["s_total"], u["di_m2"], u["programme"]["duree_ans"], h7)["valeur_residuelle"] == pytest.approx(v, abs=TOL)239 rT = R["usages"]["T"]["promoteur"]; s = cas.seuil_gdv(rT, R["usages"]["U"]["occupant"]["valeur_residuelle"])240 assert s == pytest.approx(1280327, abs=TOL) and (s / rT["gdv"] - 1) * 100 == pytest.approx(4.5, abs=0.05)241 a_, b_ = cas.coefficients_option(rT, 350_000.0)242 assert b_ - 350_000 == pytest.approx(595378, abs=TOL) and b_ / a_ == pytest.approx(1404877, abs=TOL)243 assert cas.option_redeveloppement(rT["gdv"], a_, b_, 0.10, 5.0, 0.02, 0.045, 2)["prime_americaine"] == pytest.approx(14897, abs=TOL)244 assert cas.option_redeveloppement(rT["gdv"], a_, b_, 0.15, 5.0, 0.02, 0.045, 2)["prime_americaine"] == pytest.approx(47091, abs=TOL)245246 def test_ex6_sensibilite_sigma(self):247 assert cas.seuil_capozza_helsley(18000, 600000, 0.05, 400, 2000)["prime_irreversibilite"] == pytest.approx(3483, abs=TOL)248 assert cas.seuil_capozza_helsley(18000, 600000, 0.05, 400, 8000)["prime_irreversibilite"] == pytest.approx(21612, abs=TOL)249250 def test_marges(self):251 assert fin.marge_sur_prix(0.10) == pytest.approx(1 / 11) and fin.marge_sur_cout(0.15) == pytest.approx(0.15 / 0.85)252