# Copyright (c) Meta Platforms, Inc. and affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. import math import os import uuid from typing import Dict, List, Optional, Tuple from anastruct import SystemElements try: from ..models import MATERIALS except (ImportError, ValueError): from models import MATERIALS def _member_length(n1: Dict, n2: Dict) -> float: return math.sqrt((n2["x"] - n1["x"]) ** 2 + (n2["y"] - n1["y"]) ** 2) def _member_mass(length: float, section_area: float, material: str) -> float: density = MATERIALS[material]["density"] return density * section_area * length def _member_cost(mass_kg: float, material: str) -> float: return mass_kg * MATERIALS[material]["cost_per_kg"] def run_simulation( nodes: List[Dict], members: List[Dict], supports: List[Dict], loads: List[Dict], constraints: Dict, static_dir: str = "/tmp/bridge_forge_static", ) -> Dict: os.makedirs(static_dir, exist_ok=True) if not nodes or not members: return { "structural_status": "fail", "max_deflection_mm": 0.0, "max_stress_ratio": 0.0, "failed_members": [], "total_mass_kg": 0.0, "cost_inr": 0.0, "member_count": 0, "visualization_url": "", "errors": ["No nodes or members defined"], } node_map = {n["node_id"]: n for n in nodes} ss = SystemElements() element_ids = {} member_props = {} for m in members: n1 = node_map.get(m["node_start"]) n2 = node_map.get(m["node_end"]) if n1 is None or n2 is None: continue material = m.get("material", "steel") section_area = m.get("section_area", 0.01) mat = MATERIALS.get(material, MATERIALS["steel"]) E_kn = mat["E"] / 1000 EA = E_kn * section_area elem_id = ss.add_truss_element( location=[[n1["x"], n1["y"]], [n2["x"], n2["y"]]], EA=EA, ) if isinstance(elem_id, (list, tuple)): elem_id = elem_id[0] element_ids[m["member_id"]] = elem_id length = _member_length(n1, n2) mass = _member_mass(length, section_area, material) cost = _member_cost(mass, material) member_props[m["member_id"]] = { "material": material, "section_area": section_area, "length": length, "mass": mass, "cost": cost, "yield_stress": mat["yield_stress"], "E": mat["E"], } for sup in supports: nid = sup["node_id"] n = node_map.get(nid) if n is None: continue node_id_in_ss = ss.find_node_id(vertex=[n["x"], n["y"]]) if node_id_in_ss is None: continue if sup["support_type"] == "pin": ss.add_support_hinged(node_id=node_id_in_ss) elif sup["support_type"] == "roller": ss.add_support_roll(node_id=node_id_in_ss) seismic_zone = constraints.get("seismic_zone", 0) lateral_factor = {0: 0, 1: 0.02, 2: 0.04, 3: 0.06, 4: 0.10, 5: 0.16}.get( seismic_zone, 0 ) for ld in loads: nid = ld["node_id"] n = node_map.get(nid) if n is None: continue node_id_in_ss = ss.find_node_id(vertex=[n["x"], n["y"]]) if node_id_in_ss is None: continue Fx = ld.get("Fx", 0.0) Fy = ld.get("Fy", 0.0) if lateral_factor > 0: Fx += abs(Fy) * lateral_factor ss.point_load(node_id=node_id_in_ss, Fx=Fx, Fy=Fy) try: ss.solve() except Exception as e: return { "structural_status": "fail", "max_deflection_mm": 0.0, "max_stress_ratio": 0.0, "failed_members": [], "total_mass_kg": 0.0, "cost_inr": 0.0, "member_count": len(members), "visualization_url": "", "errors": [str(e)], } max_deflection_m = 0.0 try: for node_id_ss in ss.node_map: displacements = ss.get_node_displacements(node_id=node_id_ss) if displacements: ux = float(displacements.get("ux", 0.0)) uy = float(displacements.get("uy", 0.0)) defl = math.sqrt(ux**2 + uy**2) max_deflection_m = max(max_deflection_m, defl) except Exception: pass max_deflection_mm = max_deflection_m * 1000 max_stress_ratio = 0.0 failed_members_list = [] for mid, eid in element_ids.items(): props = member_props[mid] try: element = ss.element_map.get(eid) if element is None: continue N = element.N_1 if hasattr(element, "N_1") else 0.0 if N is None: N = 0.0 N_newtons = abs(float(N)) * 1000 actual_stress = N_newtons / props["section_area"] ratio = actual_stress / props["yield_stress"] max_stress_ratio = max(max_stress_ratio, ratio) if ratio > 1.0: failed_members_list.append(mid) except Exception: pass total_mass = sum(p["mass"] for p in member_props.values()) total_cost = sum(p["cost"] for p in member_props.values()) span = 0.0 if nodes: xs = [n["x"] for n in nodes] span = max(xs) - min(xs) deflection_limit_m = max(span / 10, 0.5) if max_deflection_m > deflection_limit_m: structural_status = "fail" elif max_stress_ratio <= 1.0 and len(failed_members_list) == 0: structural_status = "pass" else: structural_status = "fail" viz_filename = f"bridge_{uuid.uuid4().hex[:8]}.png" viz_path = os.path.join(static_dir, viz_filename) try: fig = ss.show_structure(show=False, verbosity=0) if fig is not None: fig.savefig(viz_path, dpi=100, bbox_inches="tight") import matplotlib.pyplot as plt plt.close(fig) except Exception: viz_path = "" return { "structural_status": structural_status, "max_deflection_mm": round(max_deflection_mm, 4), "max_stress_ratio": round(max_stress_ratio, 4), "failed_members": failed_members_list, "total_mass_kg": round(total_mass, 2), "cost_inr": round(total_cost, 2), "member_count": len(members), "visualization_url": f"/static/{viz_filename}" if viz_path else "", }