Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import math | |
| from typing import Any | |
| import numpy as np | |
| from .load_manager import infer_load_case | |
| from .mesh import build_structured_tet_mesh | |
| from .models import MATERIALS, Design, Material | |
| def isotropic_elasticity_matrix(material: Material) -> np.ndarray: | |
| e = material.young_mpa | |
| nu = material.poisson | |
| lam = e * nu / ((1 + nu) * (1 - 2 * nu)) | |
| mu = e / (2 * (1 + nu)) | |
| return np.array( | |
| [ | |
| [lam + 2 * mu, lam, lam, 0, 0, 0], | |
| [lam, lam + 2 * mu, lam, 0, 0, 0], | |
| [lam, lam, lam + 2 * mu, 0, 0, 0], | |
| [0, 0, 0, mu, 0, 0], | |
| [0, 0, 0, 0, mu, 0], | |
| [0, 0, 0, 0, 0, mu], | |
| ], | |
| dtype=float, | |
| ) | |
| def tet_stiffness(points: np.ndarray, material: Material) -> tuple[np.ndarray, np.ndarray, float] | None: | |
| m = np.column_stack([np.ones(4), points]) | |
| det = float(np.linalg.det(m)) | |
| volume = abs(det) / 6 | |
| if volume < 1e-9: | |
| return None | |
| inv = np.linalg.inv(m) | |
| b, c, d = inv[1, :], inv[2, :], inv[3, :] | |
| bmat = np.zeros((6, 12), dtype=float) | |
| for i in range(4): | |
| col = 3 * i | |
| bmat[0, col] = b[i] | |
| bmat[1, col + 1] = c[i] | |
| bmat[2, col + 2] = d[i] | |
| bmat[3, col] = c[i] | |
| bmat[3, col + 1] = b[i] | |
| bmat[4, col + 1] = d[i] | |
| bmat[4, col + 2] = c[i] | |
| bmat[5, col] = d[i] | |
| bmat[5, col + 2] = b[i] | |
| dmat = isotropic_elasticity_matrix(material) | |
| return bmat.T @ dmat @ bmat * volume, bmat, volume | |
| def von_mises(stress: np.ndarray) -> float: | |
| sx, sy, sz, txy, tyz, txz = stress | |
| return float(math.sqrt(0.5 * ((sx - sy) ** 2 + (sy - sz) ** 2 + (sz - sx) ** 2) + 3 * (txy**2 + tyz**2 + txz**2))) | |
| def estimate_mass_g(design: Design) -> float: | |
| material = MATERIALS[design.material] | |
| volume = design.base_length_mm * design.base_width_mm * design.base_thickness_mm | |
| for hole in design.fixed_holes: | |
| volume -= math.pi * hole.radius**2 * design.base_thickness_mm | |
| for feature in design.features: | |
| if feature.type == "lightening_hole": | |
| volume -= math.pi * feature.radius**2 * design.base_thickness_mm | |
| elif feature.type == "boss": | |
| volume += math.pi * max(feature.radius, 1) ** 2 * max(feature.height, 1) | |
| elif feature.type == "rib": | |
| length = math.hypot(feature.x2 - feature.x, feature.y2 - feature.y) | |
| volume += length * max(feature.width, 1) * max(feature.height, 1) * 0.85 | |
| return round(max(volume, 1) * material.density_g_cm3 / 1000, 2) | |
| def solve_3d_linear_elasticity(design: Design, prompt: str = "") -> dict[str, Any]: | |
| material = MATERIALS[design.material] | |
| load_case = infer_load_case(prompt, design) | |
| mesh = build_structured_tet_mesh(design) | |
| if len(mesh.nodes) < 8 or len(mesh.tets) < 6: | |
| raise ValueError("3D mesh has too few solid elements.") | |
| dof_count = len(mesh.nodes) * 3 | |
| stiffness = np.zeros((dof_count, dof_count), dtype=float) | |
| force = np.zeros(dof_count, dtype=float) | |
| element_cache: list[dict[str, Any]] = [] | |
| node_points = np.array([[node["x"], node["y"], node["z"]] for node in mesh.nodes], dtype=float) | |
| for tet in mesh.tets: | |
| points = node_points[np.array(tet)] | |
| result = tet_stiffness(points, material) | |
| if result is None: | |
| continue | |
| ke, bmat, volume = result | |
| dofs = np.array([dof for node_id in tet for dof in (node_id * 3, node_id * 3 + 1, node_id * 3 + 2)]) | |
| stiffness[np.ix_(dofs, dofs)] += ke | |
| element_cache.append({"tet": tet, "bmat": bmat, "volume": volume}) | |
| load_x, load_y, load_z = load_case["load_point"] | |
| candidates = sorted( | |
| [ | |
| ( | |
| node["id"], | |
| math.sqrt( | |
| ((node["x"] - load_x) / max(mesh.length, 1)) ** 2 | |
| + ((node["y"] - load_y) / max(mesh.width, 1)) ** 2 | |
| + ((node["z"] - load_z) / max(mesh.height, 1)) ** 2 | |
| ), | |
| ) | |
| for node in mesh.nodes | |
| ], | |
| key=lambda item: item[1], | |
| )[:6] | |
| for node_id, _ in candidates: | |
| force[node_id * 3 + 2] -= load_case["effective_load_n"] / len(candidates) | |
| fixed_nodes = [node["id"] for node in mesh.nodes if node["x"] <= mesh.length * 0.001] | |
| fixed_dofs = {dof for node_id in fixed_nodes for dof in (node_id * 3, node_id * 3 + 1, node_id * 3 + 2)} | |
| free_dofs = np.array([idx for idx in range(dof_count) if idx not in fixed_dofs]) | |
| displacement = np.zeros(dof_count, dtype=float) | |
| displacement[free_dofs] = np.linalg.solve(stiffness[np.ix_(free_dofs, free_dofs)], force[free_dofs]) | |
| dmat = isotropic_elasticity_matrix(material) | |
| element_results = [] | |
| max_vm = 0.0 | |
| max_strain = 0.0 | |
| for item in element_cache: | |
| tet = item["tet"] | |
| dofs = np.array([dof for node_id in tet for dof in (node_id * 3, node_id * 3 + 1, node_id * 3 + 2)]) | |
| strain = item["bmat"] @ displacement[dofs] | |
| stress = dmat @ strain | |
| vm = von_mises(stress) | |
| strain_mag = float(np.linalg.norm(strain)) | |
| max_vm = max(max_vm, vm) | |
| max_strain = max(max_strain, strain_mag) | |
| centroid = node_points[np.array(tet)].mean(axis=0) | |
| element_results.append( | |
| { | |
| "centroid": {"x": round(float(centroid[0]), 3), "y": round(float(centroid[1]), 3), "z": round(float(centroid[2]), 3)}, | |
| "von_mises_mpa": round(vm, 3), | |
| "strain_microstrain": round(strain_mag * 1_000_000, 1), | |
| "utilization": round(max(0.0, min(1.0, vm / material.yield_mpa)), 3), | |
| } | |
| ) | |
| displacements = [] | |
| for node in mesh.nodes: | |
| ux, uy, uz = displacement[node["id"] * 3 : node["id"] * 3 + 3] | |
| displacements.append( | |
| { | |
| "id": node["id"], | |
| "x": round(node["x"], 3), | |
| "y": round(node["y"], 3), | |
| "z": round(node["z"], 3), | |
| "ux_mm": round(float(ux), 6), | |
| "uy_mm": round(float(uy), 6), | |
| "uz_mm": round(float(uz), 6), | |
| "magnitude_mm": round(float(np.linalg.norm([ux, uy, uz])), 6), | |
| } | |
| ) | |
| loaded_node_ids = [node_id for node_id, _ in candidates] | |
| tip_deflection = float(np.mean([abs(displacement[node_id * 3 + 2]) for node_id in loaded_node_ids])) | |
| mass_g = estimate_mass_g(design) | |
| safety_factor = material.yield_mpa / max(max_vm, 1e-6) | |
| thermal_rise = 0.0 | |
| score = max(0.0, min(1.0, 0.38 * min(1.0, max(0.0, (safety_factor - 0.8) / 2.2)) + 0.28 * min(1.0, max(0.0, 1 - tip_deflection / 8)) + 0.2 * min(1.0, max(0.0, 1 - mass_g / 90)) + 0.14)) | |
| hotspots = sorted(element_results, key=lambda item: item["utilization"], reverse=True)[:5] | |
| return { | |
| "method": "Python 3D linear tetrahedral elasticity", | |
| "nodes": mesh.nodes, | |
| "tets": mesh.tets, | |
| "element_results": element_results, | |
| "displacements": displacements, | |
| "max_stress_mpa": round(max_vm, 2), | |
| "max_strain_microstrain": round(max_strain * 1_000_000, 1), | |
| "tip_deflection_mm": round(tip_deflection, 3), | |
| "max_displacement_mm": round(max(item["magnitude_mm"] for item in displacements), 3), | |
| "safety_factor": round(safety_factor, 2), | |
| "mass_g": mass_g, | |
| "thermal_delta_c_proxy": thermal_rise, | |
| "manufacturability": 1.0, | |
| "score": round(score, 3), | |
| "force_vector_n": load_case["vector_n"], | |
| "load_case": load_case, | |
| "stress_regions": [ | |
| { | |
| "label": "3D tetra element", | |
| "x": item["centroid"]["x"], | |
| "y": item["centroid"]["y"], | |
| "z": item["centroid"]["z"], | |
| "severity": item["utilization"], | |
| } | |
| for item in hotspots | |
| ], | |
| "verdict": "promising" if score > 0.72 and safety_factor >= 1.8 else "needs iteration", | |
| "caveat": "Coarse 3D linear tetrahedral FEA for rapid iteration. Use Gmsh + scikit-fem/FEniCSx for production certification.", | |
| } | |