File size: 6,657 Bytes
1ce31b0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
425895b
 
 
 
 
 
 
 
 
 
 
1ce31b0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# 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 "",
    }