SPB Git
30.9 KB · 427 lines python
1#!/usr/bin/env python32"""Chapitre 22 — métré et quantités du sujet du cas 1 (Gatineau, DONNÉES SYNTHÉTIQUES).34Le devis de 38 postes (data/synthetic/cas1_gatineau_couts.csv) est celui du chapitre 21 : mêmes postes, mêmes5quantités, mêmes montants (D = 298 826,17 $, I = 62 598,68 $, C_N = 397 567,33 $). Le présent script :6  * fixe la géométrie du sujet (plan coté : empreinte 12,00 × 7,92 m = 95,0 m², garage intégré 3,96 × 6,20 m,7    deux étages, sous-sol sous la partie habitable) et en déduit les superficies selon plusieurs conventions8    (aire au sol et aire des étages au sens du MEFQ, superficie habitable, aire nette intérieure) ;9  * recalcule, par des règles géométriques déclarées, la quantité de chacun des postes « mesurés » du devis10    (m³, m², m lin.) et dénombre les postes « comptés » (unités, appareils) ; compare au devis (écart, verdict) ;11  * chiffre l'effet des écarts du métré de contrôle et d'erreurs de quantité sur D et C_N (via ch21_cout_neuf) ;12  * illustre les pertes et chutes (taux déclarés, hypothèses pédagogiques) et la propagation des erreurs de mesure ;13  * imprime le code TikZ du plan coté (--tikz) pour le chapitre.1415Usage (racine du dépôt) : PYTHONPATH=analysis/python python3 analysis/python/cases/ch22_metre.py [--tikz]16Fonctions de coût : traite.cout (cout_neuf, cout_par_composantes) et ch21_cout_neuf (CORPS, regrouper, cout_neuf_sujet)."""17from __future__ import annotations18import sys, math, pathlib19import numpy as np, pandas as pd20sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))21sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))22from traite import cout as ct23import ch21_cout_neuf as c212425ROOT = pathlib.Path(__file__).resolve().parents[3]26PI2_PAR_M2 = c21.PI2_PAR_M227MULT = None  # (1+τI)(1+τE) du cas de base, rempli par cout_neuf_sujet2829# ----------------------------------------------------------------------------- géométrie du sujet (hypothèses du plan)30PLAN = {31    "L": 12.00,          # façade (m), face externe des murs32    "W": 7.92,           # profondeur (m)33    "gx": 3.96, "gy": 6.30,   # garage intégré (coin avant gauche), face externe34    "t_ext": 0.30,       # épaisseur des murs extérieurs hors sol (ossature 2×6, revêtement, finition)35    "t_sep": 0.15,       # séparation garage / maison36    "t_int": 0.10,       # cloisons intérieures37    "t_fond": 0.25,      # murs de fondation (10 po)38    "h_fond": 2.44,      # hauteur des murs de fondation (dalle → dessus du mur)39    "sem_l": 0.60, "sem_h": 0.30,       # semelles filantes (largeur × hauteur)40    "n_col": 4, "sem_col": 0.90 * 0.90 * 0.30,   # semelles de colonnes41    "h_mur": 2.44,       # hauteur d'ossature d'un étage (murs)42    "h_etage": 2.75,     # plancher à plancher43    "h_revet": 5.60,     # hauteur revêtue (dessus de fondation → soffite), deux étages44    "pente": 0.5,        # 6:1245    "debord": 0.40,      # débords de toit46    "prof_exc": 2.60,    # profondeur d'excavation (sous-sol + semelle + pierre nette)47    "degag": 0.90,       # dégagement de travail au pied des murs48    "talus": 0.5,        # pente des talus (horizontal / vertical)49    "gar_h_gel": 1.50, "gar_t": 0.25, "gar_sem_l": 0.50, "gar_sem_h": 0.25,   # murs hors gel du garage50    "tranchee_gar": (1.00, 1.50),       # largeur, profondeur de la tranchée des murs hors gel51    "tranchee_racc": (15.0, 0.90, 1.80),  # longueur, largeur, profondeur de la tranchée aqueduc/égout52    "fen_hs": (14, 1.20 * 1.40), "fen_ss": (2, 1.70),   # fenêtres hors sol / sous-sol (nombre, aire unitaire m²)53    "porte_ext": (3, 0.91 * 2.03), "porte_gar": (1, 2.74 * 2.13), "porte_int": (14, 0.91 * 2.03),54    "descentes": (4, 5.80),             # descentes pluviales (nombre, longueur)55    "rive": 0.30,        # hauteur des solives de rive (isolation)56    "perron": (1.50, 2.40), "balcon": (3.00, 3.50), "entree": (5.00, 11.00),57    "drain_raccord": 1.00,  # raccord du drain au puisard58}5960# Pièces : (x0, y0, x1, y1) en coordonnées de face externe (origine : coin avant gauche ; x le long de la façade),61# nom, type de plancher. Les cloisons sont déduites des limites communes entre pièces d'un même niveau.62PIECES = {63    "rez": [64        (0.00, 0.00, 3.96, 6.30, "Garage", "béton"),65        (0.00, 6.30, 3.96, 7.92, "Buanderie", "céramique"),66        (3.96, 0.00, 6.30, 3.10, "Vestibule et escaliers", "céramique"),67        (3.96, 3.10, 5.60, 4.70, "Salle d'eau", "céramique"),68        (5.60, 3.10, 6.30, 4.70, "Garde-robe", "bois franc"),69        (3.96, 4.70, 8.50, 7.92, "Cuisine", "céramique"),70        (6.30, 0.00, 12.00, 4.70, "Salon", "bois franc"),71        (8.50, 4.70, 12.00, 7.92, "Salle à manger", "bois franc"),72    ],73    "etage": [74        (0.00, 0.00, 2.70, 3.10, "Salle de bain", "céramique"),75        (2.70, 0.00, 3.96, 3.10, "Lingerie", "bois franc"),76        (3.96, 0.00, 5.00, 3.10, "Trémie d'escalier", "trémie"),77        (5.00, 0.00, 6.30, 3.10, "Palier", "bois franc"),78        (6.30, 0.00, 12.00, 3.10, "Chambre principale", "bois franc"),79        (0.00, 3.10, 2.70, 4.10, "Garde-robe", "bois franc"),80        (2.70, 3.10, 8.30, 4.10, "Corridor", "bois franc"),81        (8.30, 3.10, 12.00, 7.92, "Chambre 2", "bois franc"),82        (4.90, 4.10, 8.30, 7.92, "Chambre 3", "bois franc"),83        (0.00, 4.10, 4.90, 7.92, "Chambre 4", "bois franc"),84    ],85    "soussol": [   # sous la partie habitable seulement (le garage est sur dalle au sol)86        (0.00, 6.30, 3.96, 7.92, "Rangement", "flottant"),87        (3.96, 0.00, 5.00, 3.10, "Escalier", "escalier"),88        (3.96, 3.10, 5.00, 7.92, "Hall", "flottant"),89        (5.00, 0.00, 12.00, 4.70, "Salle familiale", "flottant"),90        (5.00, 4.70, 7.50, 7.92, "Salle mécanique", "béton"),91        (7.50, 4.70, 12.00, 7.92, "Bureau", "flottant"),92    ],93}94# placards et walk-in (cloisons supplémentaires, m linéaires, étage)95CLOISONS_SUPPL = {"rez": 0.0, "etage": 1.50 + 1.80 + 3 * 1.80, "soussol": 0.0}96FENETRES = {"rez": {"Salon": 2, "Salle à manger": 2, "Cuisine": 1, "Salle d'eau": 1},97            "etage": {"Chambre principale": 2, "Chambre 2": 2, "Chambre 3": 1, "Chambre 4": 2, "Salle de bain": 1},98            "soussol": {"Salle familiale": 1, "Bureau": 1}}99PORTES_INT = {"rez": ["Salle d'eau", "Garde-robe", "Buanderie", "Garage (porte coupe-feu)"],100              "etage": ["Chambre principale", "Chambre 2", "Chambre 3", "Chambre 4", "Salle de bain", "Lingerie", "Walk-in"],101              "soussol": ["Bureau", "Salle mécanique", "Rangement"]}102APPAREILS = ["Évier de cuisine", "Lave-vaisselle (raccord)", "Réfrigérateur (raccord d'eau)", "Laveuse (raccord)", "Cuve de buanderie",103             "Lavabo (salle d'eau)", "Toilette (salle d'eau)", "Lavabo (salle de bain)", "Toilette (salle de bain)", "Baignoire", "Douche",104             "Robinet extérieur avant", "Robinet extérieur arrière", "Drain de plancher"]105106# Taux de pertes et chutes (HYPOTHÈSES PÉDAGOGIQUES déclarées, non sourcées) : quantité à commander = quantité mesurée × (1 + w)107PERTES = {"Gypse et tirage de joints": 0.12, "Couverture bardeaux d'asphalte": 0.10, "Planchers bois franc": 0.08,108          "Planchers céramique": 0.10, "Planchers flottants (sous-sol)": 0.08, "Revêtement extérieur (brique 40 % / fibrociment 60 %)": 0.10,109          "Isolation et pare-vapeur": 0.05, "Charpente murs et planchers": 0.10, "Fondations béton (semelles, murs)": 0.05, "Dalle de sous-sol": 0.05}110111TOL_CONFORME, TOL_PRECISER = 0.05, 0.10   # verdicts du métré de contrôle112113114# ----------------------------------------------------------------------------- géométrie élémentaire115116def aire(p): return (p[2] - p[0]) * (p[3] - p[1])117118def empreinte() -> dict:119    P = PLAN120    brute = P["L"] * P["W"]; garage = P["gx"] * P["gy"]121    perim = 2 * (P["L"] + P["W"])   # le périmètre d'une forme en L découpée dans un rectangle est celui du rectangle122    return {"aire_au_sol": brute, "garage": garage, "aire_sous_sol": brute - garage, "perimetre": perim,123            "aire_etages_mefq": 2 * brute, "perimetre_etages_mefq": 2 * perim}124125def _epaisseur_cote(x0, y0, x1, y1, cote) -> float:126    """Demi-épaisseur du mur à déduire sur un côté d'une pièce : mur extérieur (face externe → face interne : t_ext),127    séparation garage (t_sep/2) ou cloison (t_int/2)."""128    P = PLAN129    if cote == "x0" and x0 == 0.0: return P["t_ext"]130    if cote == "x1" and x1 == P["L"]: return P["t_ext"]131    if cote == "y0" and y0 == 0.0: return P["t_ext"]132    if cote == "y1" and y1 == P["W"]: return P["t_ext"]133    # séparation garage/maison : x = gx pour y < gy ; y = gy pour x < gx134    if (cote in ("x0", "x1") and (x0 == P["gx"] or x1 == P["gx"]) and y0 < P["gy"]) or \135       (cote in ("y0", "y1") and (y0 == P["gy"] or y1 == P["gy"]) and x0 < P["gx"]):136        return P["t_sep"] / 2137    return P["t_int"] / 2138139def pieces_nettes(niveau: str) -> pd.DataFrame:140    rows = []141    for (x0, y0, x1, y1, nom, fini) in PIECES[niveau]:142        w = (x1 - x0) - _epaisseur_cote(x0, y0, x1, y1, "x0") - _epaisseur_cote(x0, y0, x1, y1, "x1")143        h = (y1 - y0) - _epaisseur_cote(x0, y0, x1, y1, "y0") - _epaisseur_cote(x0, y0, x1, y1, "y1")144        rows.append({"niveau": niveau, "piece": nom, "fini": fini, "brute": (x1 - x0) * (y1 - y0), "nette": w * h,145                     "perimetre_net": 2 * (w + h), "largeur": w, "profondeur": h, "x0": x0, "y0": y0, "x1": x1, "y1": y1})146    return pd.DataFrame(rows)147148def cloisons(niveau: str) -> float:149    """Longueur des cloisons intérieures (m) : limites communes entre pièces, hors murs extérieurs et hors séparation garage."""150    P = PLAN; segs = []151    pcs = PIECES[niveau]152    for i, a in enumerate(pcs):153        for b in pcs[i + 1:]:154            # côtés verticaux communs155            for xa, xb in ((a[2], b[0]), (a[0], b[2])):156                if abs(xa - xb) < 1e-9:157                    lo, hi = max(a[1], b[1]), min(a[3], b[3])158                    if hi - lo > 1e-9:159                        if abs(xa - P["gx"]) < 1e-9 and lo < P["gy"]: continue   # séparation garage160                        segs.append(hi - lo)161            for ya, yb in ((a[3], b[1]), (a[1], b[3])):162                if abs(ya - yb) < 1e-9:163                    lo, hi = max(a[0], b[0]), min(a[2], b[2])164                    if hi - lo > 1e-9:165                        if abs(ya - P["gy"]) < 1e-9 and lo < P["gx"]: continue166                        segs.append(hi - lo)167    return sum(segs) + CLOISONS_SUPPL[niveau]168169def separation_garage() -> float:170    return PLAN["gx"] + PLAN["gy"]171172173def superficies() -> dict:174    """Superficies du sujet selon les conventions : MEFQ (aire au sol, aire des étages), habitable (brute hors garage),175    nette intérieure (pièces hors murs, hors trémie), sous-sol."""176    e = empreinte()177    rez, et, ss = pieces_nettes("rez"), pieces_nettes("etage"), pieces_nettes("soussol")178    nette_rez = rez.loc[rez["piece"] != "Garage", "nette"].sum()179    nette_etage = et.loc[et["fini"] != "trémie", "nette"].sum()180    return {"aire_au_sol": e["aire_au_sol"], "aire_etages_mefq": e["aire_etages_mefq"], "perimetre_etages_mefq": e["perimetre_etages_mefq"],181            "garage_brut": e["garage"], "habitable": e["aire_etages_mefq"] - e["garage"],182            "nette_rez": nette_rez, "nette_etage": nette_etage, "nette_hors_sol": nette_rez + nette_etage,183            "sous_sol_brut": e["aire_sous_sol"], "sous_sol_net": ss["nette"].sum(),184            "sous_sol_fini_net": ss.loc[ss["fini"] == "flottant", "nette"].sum(),185            "trémie": et.loc[et["fini"] == "trémie", "nette"].sum(), "pieces": pd.concat([rez, et, ss], ignore_index=True)}186187188# ----------------------------------------------------------------------------- règles de mesure (quantités géométriques)189190def aire_decalee(A: float, P: float, d: float) -> float:191    """Aire d'un polygone rectiligne décalé vers l'extérieur de d (angles droits) : A + P·d + 4·d² (4 coins nets)."""192    return A + P * d + 4 * d * d193194def volume_prismoidal(A_bas: float, A_mi: float, A_haut: float, h: float) -> float:195    return h / 6.0 * (A_bas + 4 * A_mi + A_haut)196197def quantites_geometriques() -> dict[str, dict]:198    """Quantité recalculée pour chaque poste mesuré ou dénombré du devis, avec la règle appliquée (texte) et le détail."""199    P = PLAN; e = empreinte(); S = superficies(); q = {}200    # --- excavation (prismoïde à talus) + tranchées201    A_b = aire_decalee(e["aire_sous_sol"], e["perimetre"], P["degag"])202    A_m = aire_decalee(e["aire_sous_sol"], e["perimetre"], P["degag"] + P["talus"] * P["prof_exc"] / 2)203    A_t = aire_decalee(e["aire_sous_sol"], e["perimetre"], P["degag"] + P["talus"] * P["prof_exc"])204    V_ss = volume_prismoidal(A_b, A_m, A_t, P["prof_exc"])205    V_gar = separation_garage() * 0 + (P["gx"] + P["gy"]) * P["tranchee_gar"][0] * P["tranchee_gar"][1]206    V_racc = P["tranchee_racc"][0] * P["tranchee_racc"][1] * P["tranchee_racc"][2]207    q["Excavation et remblai"] = {"q": V_ss + V_gar + V_racc, "regle": "prismoïde (fond, mi-hauteur, surface) + tranchées du garage et des raccordements",208                                  "detail": {"A_bas": A_b, "A_mi": A_m, "A_haut": A_t, "V_sous_sol": V_ss, "V_garage": V_gar, "V_racc": V_racc}}209    # --- fondations : murs à l'axe, semelles filantes, semelles de colonnes, murs hors gel du garage210    L_axe = e["perimetre"] - 4 * P["t_fond"]211    V_murs = L_axe * P["h_fond"] * P["t_fond"]; V_sem = L_axe * P["sem_l"] * P["sem_h"]; V_col = P["n_col"] * P["sem_col"]212    L_gar = P["gx"] + P["gy"]213    V_gar_m = L_gar * P["gar_h_gel"] * P["gar_t"]; V_gar_s = L_gar * P["gar_sem_l"] * P["gar_sem_h"]214    q["Fondations béton (semelles, murs)"] = {"q": V_murs + V_sem + V_col + V_gar_m + V_gar_s,215        "regle": "longueur à l'axe × hauteur × épaisseur (murs) + longueur × section (semelles) + semelles de colonnes + murs hors gel du garage",216        "detail": {"L_axe": L_axe, "V_murs": V_murs, "V_semelles": V_sem, "V_colonnes": V_col, "L_garage": L_gar, "V_garage_murs": V_gar_m, "V_garage_semelles": V_gar_s}}217    # --- dalle : aire au sol brute (sous-sol + garage sur sol), convention du devis218    q["Dalle de sous-sol"] = {"q": e["aire_sous_sol"] + e["garage"], "regle": "aire au sol brute (dalle du sous-sol + dalle du garage sur sol)",219                              "detail": {"sous_sol": e["aire_sous_sol"], "garage": e["garage"]}}220    # --- drain : périmètre à la face externe des semelles + raccord221    d_sem = (P["sem_l"] - P["t_fond"]) / 2222    L_drain = e["perimetre"] + 8 * d_sem + P["drain_raccord"]223    q["Drain français et imperméabilisation"] = {"q": L_drain, "regle": "périmètre à la face externe des semelles (+ 8 × débord de semelle) + raccord au puisard",224                                                  "detail": {"debord_semelle": d_sem}}225    # --- charpente : planchers en bois (rez hors garage + étage, aire brute) + murs extérieurs nets d'ouvertures (2 étages)226    A_planchers = e["aire_sous_sol"] + e["aire_au_sol"]227    A_murs_brut = e["perimetre"] * P["h_mur"] * 2228    ouv_hs = P["fen_hs"][0] * P["fen_hs"][1] + P["porte_ext"][0] * P["porte_ext"][1] + P["porte_gar"][0] * P["porte_gar"][1]229    A_murs_net = A_murs_brut - ouv_hs230    q["Charpente murs et planchers"] = {"q": A_planchers + A_murs_net, "regle": "aire brute des planchers en bois (rez hors garage + étage) + périmètre × 2,44 m × 2 − ouvertures",231                                         "detail": {"planchers": A_planchers, "murs_brut": A_murs_brut, "ouvertures": ouv_hs, "murs_net": A_murs_net}}232    # --- toit233    A_proj = (P["L"] + 2 * P["debord"]) * (P["W"] + 2 * P["debord"])234    fact = math.sqrt(1 + P["pente"] ** 2)235    q["Fermes de toit"] = {"q": A_proj, "regle": "projection horizontale du toit, débords compris", "detail": {"L": P["L"] + 2 * P["debord"], "W": P["W"] + 2 * P["debord"]}}236    q["Couverture bardeaux d'asphalte"] = {"q": A_proj * fact, "regle": "projection horizontale × √(1 + pente²), pente 6:12", "detail": {"facteur_pente": fact}}237    # --- revêtement : brut (ouvertures non déduites) + pignons238    h_pignon = P["W"] / 2 * P["pente"]; A_pignons = 2 * 0.5 * P["W"] * h_pignon239    q["Revêtement extérieur (brique 40 % / fibrociment 60 %)"] = {"q": e["perimetre"] * P["h_revet"] + A_pignons,240        "regle": "périmètre × hauteur revêtue + deux pignons ; ouvertures non déduites (convention du devis)", "detail": {"murs": e["perimetre"] * P["h_revet"], "pignons": A_pignons, "h_pignon": h_pignon}}241    # --- isolation : murs ext nets + plafond de l'étage + murs de fondation nets + séparation garage (murs, plafond) + solives de rive242    A_fond = L_axe * P["h_fond"] - P["fen_ss"][0] * P["fen_ss"][1]243    A_sep = separation_garage() * P["h_mur"]; A_rive = 2 * e["perimetre"] * P["rive"]244    q["Isolation et pare-vapeur"] = {"q": A_murs_net + e["aire_au_sol"] + A_fond + A_sep + e["garage"] + A_rive,245        "regle": "murs extérieurs nets + plafond de l'étage + murs de fondation nets + séparation garage (murs et plafond) + solives de rive",246        "detail": {"murs_net": A_murs_net, "plafond": e["aire_au_sol"], "fondation": A_fond, "sep_murs": A_sep, "sep_plafond": e["garage"], "rive": A_rive}}247    # --- gypse et peinture : plafonds des pièces finies + murs des pièces (périmètre net × 2,44) − ouvertures ; garage : plafond + séparation248    pcs = S["pieces"]249    g_plafonds = pcs.loc[(pcs["fini"].isin(["céramique", "bois franc", "flottant"])), "nette"].sum() + e["garage"]250    g_murs = pcs.loc[(pcs["fini"].isin(["céramique", "bois franc", "flottant"])), "perimetre_net"].sum() * P["h_mur"]251    g_sep_garage = separation_garage() * P["h_mur"]   # face côté garage (la face côté maison est dans les périmètres des pièces)252    g_ouv = (P["fen_hs"][0] * P["fen_hs"][1] + P["fen_ss"][0] * P["fen_ss"][1] + P["porte_ext"][0] * P["porte_ext"][1]253             + 2 * P["porte_int"][0] * P["porte_int"][1])254    g_total = g_plafonds + g_murs + g_sep_garage - g_ouv255    q["Gypse et tirage de joints"] = {"q": g_total, "regle": "plafonds des pièces finies + périmètre net des pièces × 2,44 m + séparation côté garage − ouvertures (portes intérieures : deux faces)",256                                       "detail": {"plafonds": g_plafonds, "murs": g_murs, "sep_garage": g_sep_garage, "ouvertures": g_ouv}}257    q["Peinture intérieure"] = {"q": g_total, "regle": "surface de gypse (même mesure)", "detail": {}}258    # --- planchers259    for fini, poste in (("bois franc", "Planchers bois franc"), ("céramique", "Planchers céramique")):260        s = pcs.loc[pcs["fini"] == fini, "nette"].sum()261        q[poste] = {"q": s, "regle": f"aire nette des pièces finies en {fini}", "detail": dict(zip(pcs.loc[pcs["fini"] == fini, "piece"], pcs.loc[pcs["fini"] == fini, "nette"]))}262    q["Planchers flottants (sous-sol)"] = {"q": e["aire_sous_sol"], "regle": "aire brute du sous-sol (convention du devis ; aire finie nette en détail)",263                                            "detail": {"brute": e["aire_sous_sol"], "finie_nette": S["sous_sol_fini_net"]}}264    # --- gouttières : deux rives + descentes265    q["Gouttières et solins"] = {"q": 2 * (P["L"] + 2 * P["debord"]) + P["descentes"][0] * P["descentes"][1], "regle": "deux rives (façade + débords) + descentes",266                                 "detail": {"rives": 2 * (P["L"] + 2 * P["debord"]), "descentes": P["descentes"][0] * P["descentes"][1]}}267    q["Balcon et perron"] = {"q": P["perron"][0] * P["perron"][1] + P["balcon"][0] * P["balcon"][1], "regle": "perron avant + balcon arrière (aires)", "detail": {}}268    q["Entrée asphaltée"] = {"q": P["entree"][0] * P["entree"][1], "regle": "largeur × longueur de l'entrée", "detail": {}}269    # --- dénombrés270    q["Fenêtres (PVC double vitrage)"] = {"q": sum(sum(v.values()) for v in FENETRES.values()), "regle": "dénombrement sur plan (rez, étage, sous-sol)", "detail": {k: sum(v.values()) for k, v in FENETRES.items()}}271    q["Portes extérieures"] = {"q": 3, "regle": "entrée principale, porte-patio, porte de service du garage", "detail": {}}272    q["Porte de garage"] = {"q": 1, "regle": "garage simple", "detail": {}}273    q["Escaliers"] = {"q": 2, "regle": "rez → étage, rez → sous-sol", "detail": {}}274    q["Portes intérieures et moulures"] = {"q": sum(len(v) for v in PORTES_INT.values()), "regle": "dénombrement des portes intérieures", "detail": {k: len(v) for k, v in PORTES_INT.items()}}275    q["Plomberie brute et finition"] = {"q": len(APPAREILS), "regle": "dénombrement des appareils et raccords", "detail": {}}276    q["Supervision de chantier (salaires)"] = {"q": 26, "regle": "durée du chantier : 6 mois = 26 semaines", "detail": {}}277    return q278279280def verification(devis: pd.DataFrame) -> pd.DataFrame:281    """Tableau du métré de contrôle : quantité du devis, quantité recalculée, écart relatif, verdict ; 'forfait' pour les postes non mesurables."""282    q = quantites_geometriques(); rows = []283    for _, r in devis.iterrows():284        if r["poste"] in q:285            qc = q[r["poste"]]["q"]; ecart = qc / r["quantite"] - 1286            classe = "dénombré" if r["unite"] in ("unité", "appareil", "semaine") else "mesuré"287            verdict = "conforme" if abs(ecart) <= TOL_CONFORME else ("à préciser" if abs(ecart) <= TOL_PRECISER else "à justifier")288            rows.append({"poste": r["poste"], "corps": c21.CORPS[r["poste"]], "unite": r["unite"], "classe": classe, "q_devis": r["quantite"], "q_calc": qc,289                         "ecart": ecart, "verdict": verdict, "prix_unitaire": r["prix_unitaire"], "montant": r["montant"],290                         "delta_montant": (qc - r["quantite"]) * r["prix_unitaire"], "regle": q[r["poste"]]["regle"]})291        else:292            rows.append({"poste": r["poste"], "corps": c21.CORPS[r["poste"]], "unite": r["unite"], "classe": "forfait", "q_devis": r["quantite"], "q_calc": np.nan,293                         "ecart": np.nan, "verdict": "ratio", "prix_unitaire": r["prix_unitaire"], "montant": r["montant"], "delta_montant": 0.0,294                         "regle": "non mesurable : contrôle par ratio ($/m² habitable, part de D)"})295    return pd.DataFrame(rows)296297298def effet_metre_controle(devis: pd.DataFrame, verif: pd.DataFrame) -> dict:299    """D et C_N si les quantités mesurées du devis étaient remplacées par les quantités du métré de contrôle (postes directs)."""300    cn = c21.cout_neuf_sujet(devis)301    mult = (1 + cn["taux_indirects"]) * (1 + cn["taux_incitatif"])302    directs = verif[(verif["classe"] != "forfait") & (~verif["poste"].isin([p for p, c in c21.CORPS.items() if c == "Coûts indirects"]))]303    delta_D = float(directs["delta_montant"].sum())304    # indirects du devis inchangés (en dollars) : seul l'incitatif suit (assiette D + I)305    D2 = cn["direct"] + delta_D; I2 = cn["indirects"]306    CN2 = (D2 + I2) * (1 + cn["taux_incitatif"])307    return {"delta_D": delta_D, "D": cn["direct"], "D2": D2, "CN": cn["total"], "CN2": CN2, "delta_CN": CN2 - cn["total"],308            "mult": mult, "delta_CN_si_taux": delta_D * mult, "part_D": delta_D / cn["direct"], "part_CN": (CN2 - cn["total"]) / cn["total"]}309310311def sensibilite_quantites(devis: pd.DataFrame, erreurs=(-0.10, -0.05, 0.05, 0.10)) -> pd.DataFrame:312    """Effet sur C_N d'une erreur relative uniforme sur les quantités d'un corps de métier (indirects du devis en dollars fixes,313    incitatif 10 % sur D + I), et d'une erreur uniforme sur tous les postes directs."""314    cn = c21.cout_neuf_sujet(devis); g = c21.regrouper(devis); rows = []315    for corps in c21.ORDRE_CORPS[:-1] + ["Tous les postes directs"]:316        base = cn["direct"] if corps == "Tous les postes directs" else float(g.loc[corps, "montant"])317        row = {"corps": corps, "montant": base, "part_D": base / cn["direct"]}318        for e in erreurs:319            D2 = cn["direct"] + e * base320            CN2 = (D2 + cn["indirects"]) * (1 + cn["taux_incitatif"])321            row[f"dCN_{e:+.2f}"] = CN2 - cn["total"]322            row[f"pct_{e:+.2f}"] = CN2 / cn["total"] - 1323        rows.append(row)324    return pd.DataFrame(rows)325326327def propagation(devis: pd.DataFrame, sigma_rel: float = 0.05) -> dict:328    """Propagation d'erreurs de mesure indépendantes : σ_D/D = σ_rel · √(Σ ω_k²) pour les postes directs (ω_k = part du poste dans D)."""329    d = devis[~devis["indirect"]]330    w = (d["montant"] / d["montant"].sum()).to_numpy()331    H = float((w ** 2).sum())332    return {"herfindahl": H, "sigma_D_rel": sigma_rel * math.sqrt(H), "sigma_D": sigma_rel * math.sqrt(H) * float(d["montant"].sum()),333            "n": len(d), "equiv_uniforme": 1 / H, "max_part": float(w.max()), "poste_max": d.loc[d["montant"].idxmax(), "poste"],334            "sigma_si_parfaitement_correle": sigma_rel}335336337def pertes(devis: pd.DataFrame) -> pd.DataFrame:338    rows = []339    for poste, w in PERTES.items():340        r = devis[devis["poste"] == poste].iloc[0]341        rows.append({"poste": poste, "unite": r["unite"], "q_mesuree": r["quantite"], "taux": w, "q_commander": r["quantite"] * (1 + w),342                     "surplus": r["quantite"] * w, "cout_pertes_si_prix_pose": r["quantite"] * w * r["prix_unitaire"] / (1 + w)})343    return pd.DataFrame(rows)344345346def repartition_par_type(devis: pd.DataFrame) -> pd.DataFrame:347    """Montant par corps de métier et par type d'unité (mesuré en volume/surface/longueur, dénombré, forfait)."""348    def typ(u):349        if u in ("m³", "m²", "m lin."): return "mesuré"350        if u in ("unité", "appareil", "semaine"): return "dénombré"351        return "forfait"352    d = devis.copy(); d["type"] = d["unite"].map(typ); d["corps"] = d["poste"].map(c21.CORPS)353    t = d.pivot_table(index="corps", columns="type", values="montant", aggfunc="sum", fill_value=0.0).reindex(c21.ORDRE_CORPS)354    return t[[c for c in ("mesuré", "dénombré", "forfait") if c in t.columns]]355356357# ----------------------------------------------------------------------------- TikZ du plan coté358359def tikz_plan(niveau: str, echelle: float = 0.42) -> str:360    """Code TikZ du plan d'un niveau (coordonnées en mètres × échelle cm)."""361    P = PLAN; s = echelle; out = []362    out.append(f"\\begin{{scope}}[scale={s}]")363    out.append(f"\\draw[line width=1.4pt] (0,0) rectangle ({P['L']},{P['W']});")364    for (x0, y0, x1, y1, nom, fini) in PIECES[niveau]:365        if niveau == "soussol" and nom == "Rangement":366            pass367        out.append(f"\\draw[line width=0.5pt] ({x0},{y0}) rectangle ({x1},{y1});")368        cx, cy = (x0 + x1) / 2, (y0 + y1) / 2369        lab = nom.replace("'", "'")370        a = (x1 - x0) * (y1 - y0)371        out.append(f"\\node[font=\\sffamily\\tiny, align=center] at ({cx:.2f},{cy:.2f}) {{{lab}\\\\{a:.1f}\\,m\\textsuperscript{{2}}}};")372    if niveau in ("rez",):373        out.append(f"\\draw[line width=1.1pt] ({P['gx']},0) -- ({P['gx']},{P['gy']}) -- (0,{P['gy']});")374    out.append("\\end{scope}")375    return "\n".join(out)376377378# ----------------------------------------------------------------------------- impression379380def main(tikz: bool = False) -> dict:381    devis, sujet = c21.charger()382    e = empreinte(); S = superficies(); q = quantites_geometriques(); V = verification(devis)383    eff = effet_metre_controle(devis, V); sens = sensibilite_quantites(devis); prop = propagation(devis); per = pertes(devis); rep = repartition_par_type(devis)384    cn = c21.cout_neuf_sujet(devis)385    f = lambda x: f"{x:,.2f}".replace(",", " ")386    if tikz:387        for n in ("rez", "etage", "soussol"):388            print(f"% ---- {n}"); print(tikz_plan(n))389        return {}390    print("== DONNÉES SYNTHÉTIQUES — sujet cas 1 : plan coté", PLAN["L"], "×", PLAN["W"], "m ; garage", PLAN["gx"], "×", PLAN["gy"], "m")391    print(f"== Empreinte (aire au sol MEFQ) {e['aire_au_sol']:.2f} m² ; périmètre au sol {e['perimetre']:.2f} m ; garage {e['garage']:.2f} m² ; sous-sol {e['aire_sous_sol']:.2f} m²")392    print(f"== Aire des étages (MEFQ, 2 étages) {S['aire_etages_mefq']:.2f} m² = {S['aire_etages_mefq']*PI2_PAR_M2:.0f} pi² ; périmètre des étages {S['perimetre_etages_mefq']:.2f} m")393    print(f"== Superficie habitable (brute hors garage) {S['habitable']:.2f} m² = {S['habitable']*PI2_PAR_M2:.0f} pi² ; sujet.json : {sujet['superficie_habitable_m2']} m²")394    print(f"== Aire nette intérieure hors sol {S['nette_hors_sol']:.2f} m² (rez {S['nette_rez']:.2f} + étage {S['nette_etage']:.2f}, trémie {S['trémie']:.2f} déduite) ; sous-sol net {S['sous_sol_net']:.2f} m², fini net {S['sous_sol_fini_net']:.2f} m²")395    print(f"== Cloisons : rez {cloisons('rez'):.2f} m ; étage {cloisons('etage'):.2f} m ; sous-sol {cloisons('soussol'):.2f} m ; séparation garage {separation_garage():.2f} m")396    print("== Pièces (aire brute / nette, m²) :")397    for _, r in S["pieces"].iterrows():398        print(f"   {r['niveau']:8s} {r['piece']:24s} {r['fini']:10s} {r['brute']:6.2f} {r['nette']:6.2f}  ({r['largeur']:.2f} × {r['profondeur']:.2f})")399    print("== Quantités géométriques (détail) :")400    for k, v in q.items():401        print(f"   {k:56s} {v['q']:9.2f}   {v['regle']}")402        if v["detail"]:403            print("        ", {kk: (round(vv, 2) if isinstance(vv, float) else vv) for kk, vv in v["detail"].items()})404    print("== Métré de contrôle (devis vs recalcul) :")405    for _, r in V.iterrows():406        if r["classe"] != "forfait":407            print(f"   {r['poste']:56s} {r['unite']:8s} devis {r['q_devis']:8.2f}  calc {r['q_calc']:8.2f}  écart {r['ecart']*100:+6.1f} %  {r['verdict']:11s}  Δ$ {r['delta_montant']:+9.2f}")408    nb = V["verdict"].value_counts().to_dict(); print("   verdicts :", nb)409    print(f"== Effet du métré de contrôle : ΔD = {f(eff['delta_D'])} $ ({eff['part_D']*100:+.2f} % de D) ; D' = {f(eff['D2'])} $ ; C_N' = {f(eff['CN2'])} $ (Δ {f(eff['delta_CN'])} $, {eff['part_CN']*100:+.2f} %) ; multiplicateur (1+τI)(1+τE) = {eff['mult']:.4f}")410    print(f"== Rappel ch. 21 : D = {f(cn['direct'])} $, I = {f(cn['indirects'])} $, C_N = {f(cn['total'])} $")411    print("== Sensibilité aux erreurs de quantité (ΔC_N en $, par corps, erreur uniforme) :")412    for _, r in sens.iterrows():413        print(f"   {r['corps']:28s} part de D {r['part_D']*100:5.1f} %  " + "  ".join(f"{c[4:]}: {r[c]:+9.0f} ({r['pct_'+c[4:]]*100:+.2f} %)" for c in sens.columns if c.startswith("dCN_")))414    print(f"== Propagation (σ = 5 % par poste direct, indépendants) : Herfindahl {prop['herfindahl']:.4f} (≈ {prop['equiv_uniforme']:.1f} postes équivalents), σ_D/D = {prop['sigma_D_rel']*100:.2f} %, σ_D = {f(prop['sigma_D'])} $ ; poste max {prop['poste_max']} ({prop['max_part']*100:.1f} %) ; si erreurs parfaitement corrélées : {prop['sigma_si_parfaitement_correle']*100:.1f} %")415    print("== Pertes et chutes (hypothèses pédagogiques) :")416    for _, r in per.iterrows():417        print(f"   {r['poste']:56s} {r['q_mesuree']:7.1f} {r['unite']:6s} × (1 + {r['taux']*100:.0f} %) = {r['q_commander']:7.1f} (surplus {r['surplus']:.1f})")418    print(f"   surplus total valorisé au prix posé : {f(per['cout_pertes_si_prix_pose'].sum())} $")419    print("== Répartition par corps et par type d'unité ($) :"); print(rep.round(2).to_string())420    tot = rep.sum(); print("   totaux :", {k: round(v, 2) for k, v in tot.items()}, " parts :", {k: f"{v/tot.sum()*100:.1f} %" for k, v in tot.items()})421    return {"devis": devis, "sujet": sujet, "empreinte": e, "superficies": S, "quantites": q, "verification": V, "effet": eff,422            "sensibilite": sens, "propagation": prop, "pertes": per, "repartition": rep, "cout_neuf": cn}423424425if __name__ == "__main__":426    main(tikz="--tikz" in sys.argv)427
Files · main
Menu

Theme