Files
Atomizer/projects/isogrid-dev-plate/studies/01_v1_tpe/extract_sandbox_stress.py

164 lines
5.6 KiB
Python
Raw Normal View History

2026-02-19 08:00:15 +00:00
"""
Extract per-element von Mises stress within each sandbox region.
Reads from the Nastran OP2 (stress per element) + NX FEM file (node coordinates),
then filters to elements whose centroids fall inside the sandbox boundary polygon.
This gives the spatial stress field in sandbox 2D coordinates the same schema
that solve_and_extract.py would produce via NXOpen once wired:
{
"nodes_xy": [[x, y], ...], # element centroids (mm)
"stress_values": [...], # von Mises per element (MPa)
"n_elements": int,
}
The sandbox polygon filter (Shapely point-in-polygon) is what maps "whole plate"
OP2 stress back to each individual sandbox region.
"""
from __future__ import annotations
from pathlib import Path
from typing import Dict, Any, List
import numpy as np
from shapely.geometry import Polygon, Point
def extract_sandbox_stress_field(
op2_file: Path,
fem_file: Path,
sandbox_geometry: dict,
subcase: int = 1,
convert_to_mpa: bool = True,
) -> Dict[str, Any]:
"""
Extract Von Mises stress field for one sandbox region.
Args:
op2_file: Nastran OP2 results file
fem_file: NX FEM file (BDF format) for node coordinates
sandbox_geometry: Geometry dict with 'outer_boundary' polygon
subcase: Nastran subcase ID (default 1)
convert_to_mpa: Divide by 1000 (NX kg-mm-s outputs kPa MPa)
Returns:
Dict with 'nodes_xy', 'stress_values', 'n_elements'
Returns empty result (n_elements=0) on any failure.
"""
_empty = {"nodes_xy": [], "stress_values": [], "n_elements": 0}
try:
from pyNastran.op2.op2 import OP2
from pyNastran.bdf.bdf import BDF
except ImportError:
print(" [StressField] pyNastran not available — skipping")
return _empty
# ── 1. Read FEM for node positions ───────────────────────────────────────
try:
bdf = BDF(debug=False)
bdf.read_bdf(str(fem_file), xref=True)
except Exception as e:
print(f" [StressField] FEM read failed: {e}")
return _empty
node_pos: Dict[int, np.ndarray] = {}
for nid, node in bdf.nodes.items():
try:
node_pos[nid] = node.get_position() # [x, y, z] in mm
except Exception:
pass
# ── 2. Read OP2 for per-element stress ────────────────────────────────────
try:
model = OP2(debug=False, log=None)
model.read_op2(str(op2_file))
except Exception as e:
print(f" [StressField] OP2 read failed: {e}")
return _empty
if not hasattr(model, "op2_results") or not hasattr(model.op2_results, "stress"):
print(" [StressField] No stress results in OP2")
return _empty
stress_container = model.op2_results.stress
# Accumulate per-element von Mises (average over integration points)
eid_vm_lists: Dict[int, List[float]] = {}
for etype in ("ctetra", "chexa", "cpenta", "cpyram"):
attr = f"{etype}_stress"
if not hasattr(stress_container, attr):
continue
stress_dict = getattr(stress_container, attr)
if not stress_dict:
continue
available = list(stress_dict.keys())
actual_sub = subcase if subcase in available else available[0]
stress = stress_dict[actual_sub]
if not stress.is_von_mises:
continue
ncols = stress.data.shape[2]
vm_col = 9 if ncols >= 10 else (7 if ncols == 8 else ncols - 1)
itime = 0
for row_idx, (eid, _nid) in enumerate(stress.element_node):
vm = float(stress.data[itime, row_idx, vm_col])
eid_vm_lists.setdefault(eid, []).append(vm)
if not eid_vm_lists:
print(" [StressField] No solid element stress found in OP2")
return _empty
# Average integration points → one value per element
eid_vm = {eid: float(np.mean(vals)) for eid, vals in eid_vm_lists.items()}
# ── 3. Compute element centroids + filter to sandbox ─────────────────────
sandbox_polygon = Polygon(sandbox_geometry["outer_boundary"])
nodes_xy: List[List[float]] = []
stress_vals: List[float] = []
for eid, vm in eid_vm.items():
if eid not in bdf.elements:
continue
try:
nids = bdf.elements[eid].node_ids
except Exception:
continue
pts = [node_pos[nid] for nid in nids if nid in node_pos]
if not pts:
continue
centroid = np.mean(pts, axis=0) # [x, y, z] 3D
x, y = float(centroid[0]), float(centroid[1]) # flat plate → Z constant
if not sandbox_polygon.contains(Point(x, y)):
continue
# Convert kPa → MPa (NX kg-mm-s unit system)
val = vm / 1000.0 if convert_to_mpa else vm
nodes_xy.append([x, y])
stress_vals.append(val)
n = len(stress_vals)
if n > 0:
print(f" [StressField] sandbox '{sandbox_geometry.get('sandbox_id', '?')}': "
f"{n} elements max={max(stress_vals):.1f} MPa")
else:
print(f" [StressField] sandbox '{sandbox_geometry.get('sandbox_id', '?')}': "
f"0 elements in polygon (check coordinate frame)")
return {
"nodes_xy": nodes_xy,
"stress_values": stress_vals,
"n_elements": n,
}