CoolFace
Apppublic

Adilkhan01/Structural_Analysis_Virtual_Lab_PIML

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
app.py741 linesDownload Raw Back to root
1# app.py — Structural Analysis Virtual Lab (Warren Truss)2# ✅ ML surrogate REMOVED (no torch, no predicted outputs)3# ✅ Allowable checks kept (stress + deflection ratio slider)4# ✅ Persistent CSV logging kept (solver truth only)5# ✅ Excel export (in-session) kept6# ✅ Uniform dead + live loads kept7# ✅ Click-to-move load + click-to-cycle supports kept8 9import os10import math11import time12import numpy as np13import matplotlib14matplotlib.use("Agg")15import matplotlib.pyplot as plt16import gradio as gr17import inspect18import pandas as pd19 20# ============================================================21# Persistent storage paths (HF Spaces)22# ============================================================23DATA_DIR = "/data" if os.path.isdir("/data") else "."24CASES_CSV = os.path.join(DATA_DIR, "truss_cases.csv")25 26# ============================================================27# Matplotlib Figure -> NumPy Image (HF-safe)28# ============================================================29def fig_to_image(fig):30    fig.canvas.draw()31    if hasattr(fig.canvas, "buffer_rgba"):32        buf = np.asarray(fig.canvas.buffer_rgba())33        img = np.array(buf)[..., :3]34    else:35        w, h = fig.canvas.get_width_height()36        buf = np.frombuffer(fig.canvas.tostring_argb(), dtype=np.uint8).reshape(h, w, 4)37        img = buf[:, :, [1, 2, 3]]38    plt.close(fig)39    return img40 41# ============================================================42# Warren truss geometry43# ============================================================44def build_warren_truss(L, H, panels):45    dx = L / panels46    nodes = [(i * dx, 0.0) for i in range(panels + 1)]47    top0 = len(nodes)48    nodes += [(i * dx + dx / 2.0, H) for i in range(panels)]49 50    members = []51    for i in range(panels):52        members.append((i, i + 1))                       # bottom chord53    for i in range(panels - 1):54        members.append((top0 + i, top0 + i + 1))        # top chord55    for i in range(panels):56        members.append((top0 + i, i))                   # diagonals57        members.append((top0 + i, i + 1))58    return np.array(nodes, dtype=float), members59 60def right_support_index(nodes):61    idx, bestx = 0, -1e1862    for i, (x, y) in enumerate(nodes):63        if abs(y) < 1e-12 and x > bestx:64            bestx = x65            idx = i66    return idx67 68# ============================================================69# Supports70# ============================================================71SUPPORT_TYPES = ["Pin", "Roller-Y", "Free"]72 73def support_fixed_dofs(support_type, node_index):74    if support_type == "Pin":75        return [2 * node_index, 2 * node_index + 1]76    if support_type == "Roller-Y":77        return [2 * node_index + 1]78    if support_type == "Free":79        return []80    return [2 * node_index, 2 * node_index + 1]81 82def cycle_support(s):83    i = SUPPORT_TYPES.index(s) if s in SUPPORT_TYPES else 084    return SUPPORT_TYPES[(i + 1) % len(SUPPORT_TYPES)]85 86# ============================================================87# Uniform load -> equivalent nodal loads (bottom nodes)88# ============================================================89def add_uniform_bottom_loads(F, L, panels, w_total_kN_per_m):90    w = float(w_total_kN_per_m) * 1000.0  # N/m91    if abs(w) < 1e-12:92        return93    dx = L / panels94    for i in range(panels + 1):95        tributary = dx if (i not in (0, panels)) else (dx / 2.0)96        F[2 * i + 1] += -w * tributary97 98# ============================================================99# Direct stiffness truss solver100# ============================================================101def solve_truss(L, H, panels, E_GPa, A_cm2,102               P_kN, load_node, left_support, right_support,103               enable_uniform, wD_kNm, wL_kNm):104 105    nodes, members = build_warren_truss(L, H, panels)106    n = len(nodes)107    dof = 2 * n108 109    E = float(E_GPa) * 1e9110    A = float(A_cm2) * 1e-4111    P = float(P_kN) * 1000.0112 113    K = np.zeros((dof, dof), dtype=float)114    F = np.zeros((dof,), dtype=float)115 116    # point load117    F[2 * load_node + 1] += -P118 119    # uniform loads (dead+live)120    w_total = (float(wD_kNm) + float(wL_kNm)) if enable_uniform else 0.0121    if enable_uniform:122        add_uniform_bottom_loads(F, float(L), int(panels), w_total)123 124    member_geom = []125    for (ni, nj) in members:126        xi, yi = nodes[ni]127        xj, yj = nodes[nj]128        dx = xj - xi129        dy = yj - yi130        Lm = math.sqrt(dx*dx + dy*dy)131        c = dx / Lm132        s = dy / Lm133        k = (E * A) / Lm134 135        k11 = k * c * c136        k12 = k * c * s137        k22 = k * s * s138 139        ke = np.array([140            [ k11,  k12, -k11, -k12],141            [ k12,  k22, -k12, -k22],142            [-k11, -k12,  k11,  k12],143            [-k12, -k22,  k12,  k22],144        ], dtype=float)145 146        dofs = [2*ni, 2*ni+1, 2*nj, 2*nj+1]147        for a in range(4):148            for b in range(4):149                K[dofs[a], dofs[b]] += ke[a, b]150 151        member_geom.append((ni, nj, Lm, c, s))152 153    left_node = 0154    right_node = right_support_index(nodes)155 156    fixed = []157    fixed += support_fixed_dofs(left_support, left_node)158    fixed += support_fixed_dofs(right_support, right_node)159    fixed = sorted(set(fixed))160 161    free = [i for i in range(dof) if i not in fixed]162    if len(free) == 0:163        raise ValueError("All DOFs fixed. Choose supports that allow deformation.")164 165    Kff = K[np.ix_(free, free)]166    Ff = F[free]167 168    try:169        uf = np.linalg.solve(Kff, Ff)170    except np.linalg.LinAlgError:171        raise ValueError("Unstable/singular system. Try Pin + Roller-Y.")172 173    U = np.zeros((dof,), dtype=float)174    U[free] = uf175 176    R = K @ U - F177 178    forces = []179    for (ni, nj, Lm, c, s) in member_geom:180        ui, vi = U[2*ni], U[2*ni+1]181        uj, vj = U[2*nj], U[2*nj+1]182        N = (E*A/Lm) * ((-c)*ui + (-s)*vi + (c)*uj + (s)*vj)183        forces.append(N)184    forces = np.array(forces, dtype=float)185 186    disp = np.sqrt(U[0::2]**2 + U[1::2]**2)187    return nodes, members, forces, disp, R, U, left_node, right_node, fixed, w_total188 189# ============================================================190# Persistent CSV logging (solver truth only)191# ============================================================192def append_case_to_csv(row_dict):193    df = pd.DataFrame([row_dict])194    os.makedirs(DATA_DIR, exist_ok=True)195    if os.path.exists(CASES_CSV):196        df.to_csv(CASES_CSV, mode="a", header=False, index=False)197    else:198        df.to_csv(CASES_CSV, mode="w", header=True, index=False)199 200# ============================================================201# Render helpers202# ============================================================203def member_color(N, eps=1e-6):204    if abs(N) < eps:205        return "gray"206    return "green" if N > 0 else "red"207 208def render_truss_image(L, H, panels, load_node, left_support, right_support,209                       forces=None, enable_uniform=False, w_total_kNm=0.0):210    nodes, members = build_warren_truss(L, H, panels)211    left_node = 0212    right_node = right_support_index(nodes)213 214    fig, ax = plt.subplots(figsize=(9, 3.9), dpi=140)215    ax.set_title("Edit Mode: Place Load / Edit Supports — click on node or support", fontsize=10)216 217    for k, (i, j) in enumerate(members):218        col = "black" if forces is None else member_color(forces[k])219        ax.plot([nodes[i, 0], nodes[j, 0]], [nodes[i, 1], nodes[j, 1]], lw=2, color=col)220 221    ax.scatter(nodes[:, 0], nodes[:, 1], s=25, color="black", zorder=3)222 223    for idx, (x, y) in enumerate(nodes):224        ax.text(x, y + 0.045 * H, f"N{idx}", fontsize=8, ha="center", color="#333")225 226    ax.scatter([nodes[left_node, 0], nodes[right_node, 0]], [0, 0], s=80, color="black", zorder=4)227    ax.text(nodes[left_node, 0], -0.18 * H, f"Left: {left_support}", ha="center", fontsize=9)228    ax.text(nodes[right_node, 0], -0.18 * H, f"Right: {right_support}", ha="center", fontsize=9)229 230    xL, yL = nodes[load_node]231    ax.annotate("", xy=(xL, yL + 0.02 * H), xytext=(xL, yL + 0.30 * H),232                arrowprops=dict(arrowstyle="->", lw=2, color="blue"))233    ax.text(xL, yL + 0.34 * H, f"P @ N{load_node}", ha="center", fontsize=9, color="blue")234 235    if enable_uniform and abs(w_total_kNm) > 1e-12:236        y_arrow_top = 1.10 * H237        y_arrow_bot = 0.75 * H238        for xx in np.linspace(0.0, L, 9):239            ax.annotate("", xy=(xx, y_arrow_bot), xytext=(xx, y_arrow_top),240                        arrowprops=dict(arrowstyle="->", lw=1.6, color="#666"))241        ax.text(L / 2, y_arrow_top + 0.02 * H, f"w = {w_total_kNm:.2f} kN/m (dead+live)",242                ha="center", fontsize=9, color="#444")243 244    if forces is not None:245        ax.text(0.02, 0.98, "Green=Tension  Red=Compression  Gray≈0",246                transform=ax.transAxes, va="top", fontsize=9,247                bbox=dict(boxstyle="round,pad=0.2", fc="white", ec="#ccc"))248 249    x_min = -0.05 * L250    x_max = 1.05 * L251    ax.set_xlim(x_min, x_max)252    ax.set_ylim(-0.32 * H, 1.35 * H)253    ax.axis("off")254    fig.tight_layout()255 256    img = fig_to_image(fig)257    meta = {"x_min": x_min, "x_max": x_max, "img_w": img.shape[1]}258    return img, meta259 260# ============================================================261# ATTRACTIVE SFD + BMD (no arrows, no min/max labels)262# ============================================================263def render_sfd_bmd_image(L, P_kN, a, enable_uniform=False, w_total_kNm=0.0):264    P = float(P_kN)265    a = float(a)266    w = float(w_total_kNm) if enable_uniform else 0.0267    x = np.linspace(0, L, 800)268 269    # Equivalent-beam reactions (simple support assumption)270    R1 = (P * (L - a) / L) + (w * L / 2.0)271 272    # Shear and moment273    V = R1 - w * x - np.where(x >= a, P, 0.0)274    M = R1 * x - w * (x ** 2) / 2.0 - np.where(x >= a, P * (x - a), 0.0)275 276    fig, axs = plt.subplots(2, 1, figsize=(9, 6.2), dpi=160)277 278    # ---------------- SFD ----------------279    ax = axs[0]280    ax.plot(x, V, lw=2.2)281    ax.axhline(0, lw=1.0, alpha=0.7)282    ax.axvline(a, lw=1.2, linestyle="--", alpha=0.8)283 284    ax.fill_between(x, 0, V, where=(V >= 0), alpha=0.15, interpolate=True)285    ax.fill_between(x, 0, V, where=(V < 0),  alpha=0.15, interpolate=True)286 287    ax.set_title("Shear Force Diagram (SFD) — Equivalent Beam", fontsize=12, pad=8)288    ax.set_ylabel("Shear V (kN)", fontsize=10)289    ax.set_xlim(0, L)290    ax.grid(True, alpha=0.25)291 292    # Minimal label only (no arrows, no extrema)293    ax.text(a, ax.get_ylim()[1] * 0.92, f"Load @ x={a:.2f} m",294            ha="center", va="top", fontsize=9,295            bbox=dict(boxstyle="round,pad=0.25", fc="white", ec="#cccccc", alpha=0.9))296 297    # ---------------- BMD ----------------298    ax = axs[1]299    ax.plot(x, M, lw=2.2)300    ax.axhline(0, lw=1.0, alpha=0.7)301    ax.axvline(a, lw=1.2, linestyle="--", alpha=0.8)302 303    ax.fill_between(x, 0, M, where=(M >= 0), alpha=0.15, interpolate=True)304    ax.fill_between(x, 0, M, where=(M < 0),  alpha=0.15, interpolate=True)305 306    ax.set_title("Bending Moment Diagram (BMD) — Equivalent Beam", fontsize=12, pad=8)307    ax.set_xlabel("Span x (m)", fontsize=10)308    ax.set_ylabel("Moment M (kN·m)", fontsize=10)309    ax.set_xlim(0, L)310    ax.grid(True, alpha=0.25)311 312    load_lines = [f"Point load P = {P:.2f} kN at x = {a:.2f} m"]313    if enable_uniform and abs(w) > 1e-12:314        load_lines.append(f"Uniform load w = {w:.2f} kN/m")315    ax.text(0.01, 0.97, "\n".join(load_lines),316            transform=ax.transAxes, va="top", ha="left", fontsize=9,317            bbox=dict(boxstyle="round,pad=0.3", fc="white", ec="#cccccc", alpha=0.9))318 319    fig.tight_layout()320    return fig_to_image(fig)321 322def make_node_table(nodes):323    return [[f"N{i}", float(x), float(y)] for i, (x, y) in enumerate(nodes)]324 325def make_member_table(nodes, members, forces):326    rows = []327    for m_id, ((i, j), N) in enumerate(zip(members, forces)):328        xi, yi = nodes[i]; xj, yj = nodes[j]329        Lm = math.hypot(xj - xi, yj - yi)330        tc = "Tension" if N >= 0 else "Compression"331        rows.append([f"M{m_id}", f"N{i}", f"N{j}", float(Lm), float(N), tc])332    return rows333 334# ============================================================335# Click logic336# ============================================================337def click_to_model_x(x_pix, meta):338    x_min, x_max, W = meta["x_min"], meta["x_max"], meta["img_w"]339    return x_min + (x_pix / max(1.0, (W - 1))) * (x_max - x_min)340 341def nearest_node_by_x(x, nodes):342    return int(np.argmin(np.abs(nodes[:, 0] - x)))343 344# ============================================================345# Export Excel (in-session buffers)346# ============================================================347def export_excel(state):348    cases = state.get("dataset_cases", [])349    mems = state.get("dataset_members", [])350    if len(cases) == 0:351        raise gr.Error("No in-session data yet. Run analysis with 'Auto-record dataset' enabled.")352    df_cases = pd.DataFrame(cases)353    df_mems = pd.DataFrame(mems)354    out_path = "/tmp/truss_dataset.xlsx"355    with pd.ExcelWriter(out_path, engine="openpyxl") as writer:356        df_cases.to_excel(writer, index=False, sheet_name="cases")357        df_mems.to_excel(writer, index=False, sheet_name="member_forces")358    return out_path359 360# ============================================================361# Gradio Image safe + CSS362# ============================================================363def make_image(**kwargs):364    sig = inspect.signature(gr.Image.__init__)365    if "sources" in sig.parameters:366        kwargs["sources"] = []367    return gr.Image(**kwargs)368 369CSS_HIDE_UPLOAD = """370div[data-testid="image-upload"] { display: none !important; }371div[data-testid="image-source"] { display: none !important; }372button[aria-label="Clear"] { display: none !important; }373"""374 375# ============================================================376# State init377# ============================================================378def init_state():379    s = {380        "L": 12.0, "H": 2.5, "panels": 8,381        "E": 200.0, "A": 8.0, "P": 30.0,382        "load_node": 4,383        "left_support": "Pin",384        "right_support": "Roller-Y",385        "enable_uniform": False,386        "wD": 0.0, "wL": 0.0,387        "meta": None,388 389        # Allowables defaults390        "enable_checks": True,391        "allow_stress_mpa": 250.0,392        "allow_defl_ratio": 360,  # L/360393 394        # in-session buffers395        "dataset_cases": [], "dataset_members": [], "case_id": 0,396    }397 398    img, meta = render_truss_image(399        s["L"], s["H"], s["panels"], s["load_node"],400        s["left_support"], s["right_support"],401        forces=None, enable_uniform=s["enable_uniform"], w_total_kNm=0.0402    )403    s["meta"] = meta404    return s, img405 406# ============================================================407# Refresh plot when UI changes408# ============================================================409def refresh_plot(state, left_support, right_support, enable_uniform, wD, wL):410    state["left_support"] = left_support411    state["right_support"] = right_support412    state["enable_uniform"] = bool(enable_uniform)413    state["wD"] = float(wD)414    state["wL"] = float(wL)415    w_total = (state["wD"] + state["wL"]) if state["enable_uniform"] else 0.0416 417    img, meta = render_truss_image(418        state["L"], state["H"], state["panels"], state["load_node"],419        state["left_support"], state["right_support"],420        forces=None, enable_uniform=state["enable_uniform"], w_total_kNm=w_total421    )422    state["meta"] = meta423    return state, img424 425# ============================================================426# Click callback (move load / cycle supports)427# ============================================================428def on_image_click(evt: gr.SelectData, state, edit_mode, left_support_ui, right_support_ui):429    if state.get("meta") is None:430        return state, None, "No meta; run once.", left_support_ui, right_support_ui431 432    if not (isinstance(evt.index, (list, tuple)) and len(evt.index) >= 1):433        return state, None, "Click detected but coordinates missing.", left_support_ui, right_support_ui434 435    x_pix = float(evt.index[0])436    x = click_to_model_x(x_pix, state["meta"])437 438    nodes, _ = build_warren_truss(state["L"], state["H"], int(state["panels"]))439    left_x = nodes[0, 0]440    right_x = nodes[right_support_index(nodes), 0]441    tol = 0.12 * (state["L"] / max(1, int(state["panels"])))442 443    if edit_mode == "Edit Supports":444        if abs(x - left_x) <= tol:445            state["left_support"] = cycle_support(state["left_support"])446            msg = f"✅ Left support → {state['left_support']}"447        elif abs(x - right_x) <= tol:448            state["right_support"] = cycle_support(state["right_support"])449            msg = f"✅ Right support → {state['right_support']}"450        else:451            msg = "Click near LEFT or RIGHT support to cycle type."452    else:453        state["load_node"] = nearest_node_by_x(x, nodes)454        msg = f"✅ Load moved to node N{state['load_node']}"455 456    w_total = (state["wD"] + state["wL"]) if state["enable_uniform"] else 0.0457    img, meta = render_truss_image(458        state["L"], state["H"], state["panels"], state["load_node"],459        state["left_support"], state["right_support"],460        forces=None, enable_uniform=state["enable_uniform"], w_total_kNm=w_total461    )462    state["meta"] = meta463    return state, img, msg, state["left_support"], state["right_support"]464 465# ============================================================466# Run analysis: solver + allowables + persist data467# ============================================================468def run_analysis(L, H, E, A, P, panels_value,469                 enable_uniform, wD, wL,470                 enable_checks, allow_stress_mpa, allow_defl_ratio,471                 auto_record, left_support, right_support,472                 state):473 474    # Update state from UI475    state["L"] = float(L); state["H"] = float(H); state["E"] = float(E); state["A"] = float(A)476    state["P"] = float(P); state["panels"] = int(panels_value)477    state["enable_uniform"] = bool(enable_uniform)478    state["wD"] = float(wD); state["wL"] = float(wL)479    state["left_support"] = left_support; state["right_support"] = right_support480 481    # Allowables482    state["enable_checks"] = bool(enable_checks)483    state["allow_stress_mpa"] = float(allow_stress_mpa)484    state["allow_defl_ratio"] = int(allow_defl_ratio)485 486    nodes0, _ = build_warren_truss(state["L"], state["H"], state["panels"])487    state["load_node"] = int(max(0, min(len(nodes0) - 1, int(state["load_node"]))))488 489    # ---------- exact solver ----------490    try:491        nodes, members, forces, disp, R, U, left_node, right_node, fixed, w_total = solve_truss(492            state["L"], state["H"], state["panels"],493            state["E"], state["A"], state["P"],494            state["load_node"],495            state["left_support"], state["right_support"],496            state["enable_uniform"], state["wD"], state["wL"]497        )498    except Exception as e:499        w_total0 = (state["wD"] + state["wL"]) if state["enable_uniform"] else 0.0500        img, meta = render_truss_image(501            state["L"], state["H"], state["panels"], state["load_node"],502            state["left_support"], state["right_support"],503            forces=None, enable_uniform=state["enable_uniform"], w_total_kNm=w_total0504        )505        state["meta"] = meta506        return f"❌ Analysis error: {str(e)}", img, None, [], [], state, None507 508    # reactions509    RyL = float(R[2 * left_node + 1]) if (2 * left_node + 1) in fixed else 0.0510    RyR = float(R[2 * right_node + 1]) if (2 * right_node + 1) in fixed else 0.0511 512    # displacements513    disp_mag_max = float(np.max(disp))514    uy = U[1::2]515    vmax = float(uy[np.argmax(np.abs(uy))])516 517    load_x = float(nodes[state["load_node"], 0])518 519    # ---------- Allowable checks ----------520    A_m2 = float(state["A"]) * 1e-4521    max_force = float(np.max(np.abs(forces))) if len(forces) else 0.0522    max_stress_mpa = (max_force / max(1e-12, A_m2)) / 1e6523 524    allow_stress = float(state["allow_stress_mpa"])525    allow_ratio = max(1, int(state["allow_defl_ratio"]))526    allow_defl_m = float(state["L"]) / allow_ratio527 528    stress_ok = (max_stress_mpa <= allow_stress)529    defl_ok = (abs(vmax) <= allow_defl_m)530 531    if state["enable_checks"]:532        checks_text = (533            "\n--- Allowable Checks ---\n"534            f"Max axial stress = {max_stress_mpa:.2f} MPa  | allowable = {allow_stress:.2f} MPa  | "535            f"{'PASS ✅' if stress_ok else 'FAIL ❌'}\n"536            f"Max vertical deflection = {abs(vmax)*1000:.3f} mm | allowable = (L/{allow_ratio}) = {allow_defl_m*1000:.3f} mm | "537            f"{'PASS ✅' if defl_ok else 'FAIL ❌'}\n"538        )539    else:540        checks_text = "\n--- Allowable Checks ---\nDisabled\n"541 542    # ---------- persistent CSV (solver truth only) ----------543    row = {544        "timestamp": int(time.time()),545        "L_m": state["L"],546        "H_m": state["H"],547        "panels": int(state["panels"]),548        "E_GPa": state["E"],549        "A_cm2": state["A"],550        "P_kN": state["P"],551        "load_node": int(state["load_node"]),552        "left_support": state["left_support"],553        "right_support": state["right_support"],554        "enable_uniform": bool(state["enable_uniform"]),555        "w_dead_kN_per_m": float(state["wD"]),556        "w_live_kN_per_m": float(state["wL"]),557        "w_total_kN_per_m": float(w_total),558        "RyL_N": RyL,559        "RyR_N": RyR,560        "umax_m": disp_mag_max,561        "vmax_m": vmax,562        "enable_checks": bool(state["enable_checks"]),563        "allow_stress_mpa": float(state["allow_stress_mpa"]),564        "allow_defl_ratio": int(state["allow_defl_ratio"]),565        "max_stress_mpa": float(max_stress_mpa),566        "allow_defl_m": float(allow_defl_m),567        "stress_ok": bool(stress_ok),568        "defl_ok": bool(defl_ok),569    }570    append_case_to_csv(row)571 572    # ---------- diagrams ----------573    diag_img = render_sfd_bmd_image(574        state["L"], state["P"], load_x,575        enable_uniform=state["enable_uniform"], w_total_kNm=w_total576    )577 578    truss_img, meta = render_truss_image(579        state["L"], state["H"], state["panels"], state["load_node"],580        state["left_support"], state["right_support"],581        forces=forces, enable_uniform=state["enable_uniform"], w_total_kNm=w_total582    )583    state["meta"] = meta584 585    node_table = make_node_table(nodes)586    member_table = make_member_table(nodes, members, forces)587 588    # ---------- in-session dataset record (Excel export) ----------589    if auto_record:590        case_id = int(state.get("case_id", 0))591        state["case_id"] = case_id + 1592 593        state["dataset_cases"].append({594            "case_id": case_id,595            "timestamp": int(time.time()),596            "L_m": state["L"],597            "H_m": state["H"],598            "panels": state["panels"],599            "E_GPa": state["E"],600            "A_cm2": state["A"],601            "P_kN": state["P"],602            "load_node": int(state["load_node"]),603            "load_x_m": load_x,604            "left_support": state["left_support"],605            "right_support": state["right_support"],606            "enable_uniform": bool(state["enable_uniform"]),607            "w_dead_kN_per_m": float(state["wD"]),608            "w_live_kN_per_m": float(state["wL"]),609            "w_total_kN_per_m": float(w_total),610            "RyL_N": RyL, "RyR_N": RyR,611            "umax_m": disp_mag_max,612            "vmax_m": vmax,613            "enable_checks": bool(state["enable_checks"]),614            "allow_stress_mpa": float(state["allow_stress_mpa"]),615            "allow_defl_ratio": int(state["allow_defl_ratio"]),616            "max_stress_mpa": float(max_stress_mpa),617            "allow_defl_m": float(allow_defl_m),618            "stress_ok": bool(stress_ok),619            "defl_ok": bool(defl_ok),620        })621 622        for mid, (ij, N) in enumerate(zip(members, forces)):623            i, j = ij624            state["dataset_members"].append({625                "case_id": case_id,626                "member_id": mid,627                "i": int(i),628                "j": int(j),629                "force_N": float(N),630                "state": "Tension" if N >= 0 else "Compression"631            })632 633    # ---------- output ----------634    out = (635        "MODE: Exact (Solver)\n"636        f"Load at N{state['load_node']} (x≈{load_x:.2f} m)\n"637        f"Supports: Left={state['left_support']} | Right={state['right_support']}\n\n"638        f"Point load P = {state['P']:.2f} kN\n"639        f"Uniform loads: enabled={state['enable_uniform']} | wD={state['wD']:.2f} kN/m | wL={state['wL']:.2f} kN/m | wTotal={w_total:.2f} kN/m\n\n"640        f"Max displacement |u|max = {disp_mag_max*1000:.3f} mm\n"641        f"Max vertical displacement vmax = {vmax*1000:.3f} mm\n\n"642        "Vertical reactions (only where restrained)\n"643        f"RyL={RyL:.2f} N, RyR={RyR:.2f} N\n"644        f"{checks_text}"645        "\nData logging:\n"646        f"- Persistent CSV: {CASES_CSV}\n"647    )648 649    return out, truss_img, diag_img, node_table, member_table, state, None650 651# ============================================================652# UI653# ============================================================654(state0, init_img) = init_state()655 656with gr.Blocks(title="Structural Analysis Virtual Lab — Warren Truss", css=CSS_HIDE_UPLOAD) as demo:657    gr.Markdown(658        "## 🏗️ Structural Analysis Virtual Lab — Warren Truss\n"659        "- **Place Load**: click a node to move the point load\n"660        "- **Edit Supports**: click near left/right supports to cycle support type\n"661        "- Uniform dead + live loads supported\n"662        "- ✅ Allowable checks (stress + deflection)\n"663    )664 665    state = gr.State(state0)666 667    with gr.Row():668        with gr.Column(scale=1):669            L = gr.Number(value=12.0, label="Span L (m)")670            H = gr.Number(value=2.5, label="Height H (m)")671            E = gr.Number(value=200.0, label="Young's modulus E (GPa)")672            A = gr.Number(value=8.0, label="Member area A (cm²)")673            P = gr.Number(value=30.0, label="Point load P (kN)")674            panels_display = gr.Number(value=8, precision=0, label="Panels")675 676            left_support_ui = gr.Dropdown(choices=SUPPORT_TYPES, value="Pin", label="Left support type")677            right_support_ui = gr.Dropdown(choices=SUPPORT_TYPES, value="Roller-Y", label="Right support type")678 679            gr.Markdown("### Uniform Loads (kN/m)")680            enable_uniform = gr.Checkbox(value=False, label="Enable uniform dead + live load")681            wD = gr.Number(value=0.0, label="Dead load wD (kN/m)")682            wL = gr.Number(value=0.0, label="Live load wL (kN/m)")683 684            gr.Markdown("### Allowable / Limits")685            enable_checks = gr.Checkbox(value=True, label="Enable allowable checks (stress + deflection)")686            allow_stress = gr.Number(value=250.0, label="Allowable axial stress (MPa)")687            allow_defl_ratio = gr.Slider(minimum=120, maximum=1000, value=360, step=10,688                                         label="Allowable deflection ratio (L / ___)")689 690            edit_mode = gr.Radio(["Place Load", "Edit Supports"], value="Place Load", label="Edit Mode")691            auto_record = gr.Checkbox(value=True, label="Auto-record in-session dataset (for Excel export)")692            msg = gr.Markdown("Click the truss to place load or edit supports.")693            run_btn = gr.Button("Run Analysis (Exact Solver)", variant="primary")694 695            gr.Markdown("### Export in-session dataset")696            export_btn = gr.Button("Export Excel (.xlsx)")697            dataset_file = gr.File(label="Download dataset")698 699        with gr.Column(scale=2):700            truss_img = make_image(value=init_img, type="numpy", interactive=True, label="Truss (click to edit)")701            out_text = gr.Textbox(label="Output", lines=26)702            diag_img = make_image(type="numpy", interactive=False, label="Diagrams (SFD + BMD)")703 704    gr.Markdown("### Node Table")705    node_df = gr.Dataframe(headers=["Node", "x (m)", "y (m)"], interactive=False, wrap=True)706 707    gr.Markdown("### Member Table (Tension/Compression)")708    mem_df = gr.Dataframe(headers=["Member", "i", "j", "Length (m)", "Axial Force (N)", "State"],709                          interactive=False, wrap=True)710 711    left_support_ui.change(refresh_plot, inputs=[state, left_support_ui, right_support_ui, enable_uniform, wD, wL],712                           outputs=[state, truss_img])713    right_support_ui.change(refresh_plot, inputs=[state, left_support_ui, right_support_ui, enable_uniform, wD, wL],714                            outputs=[state, truss_img])715    enable_uniform.change(refresh_plot, inputs=[state, left_support_ui, right_support_ui, enable_uniform, wD, wL],716                          outputs=[state, truss_img])717    wD.change(refresh_plot, inputs=[state, left_support_ui, right_support_ui, enable_uniform, wD, wL],718              outputs=[state, truss_img])719    wL.change(refresh_plot, inputs=[state, left_support_ui, right_support_ui, enable_uniform, wD, wL],720              outputs=[state, truss_img])721 722    truss_img.select(723        on_image_click,724        inputs=[state, edit_mode, left_support_ui, right_support_ui],725        outputs=[state, truss_img, msg, left_support_ui, right_support_ui]726    )727 728    run_btn.click(729        run_analysis,730        inputs=[L, H, E, A, P, panels_display,731                enable_uniform, wD, wL,732                enable_checks, allow_stress, allow_defl_ratio,733                auto_record, left_support_ui, right_support_ui,734                state],735        outputs=[out_text, truss_img, diag_img, node_df, mem_df, state, dataset_file]736    )737 738    export_btn.click(export_excel, inputs=[state], outputs=[dataset_file])739 740demo.launch()741