Adilkhan01/Structural_Analysis_Virtual_Lab_PIML
0
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 