sanjuhs's picture
Upload folder using huggingface_hub
6de1b61 verified
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.",
}