- Add validation framework (config, model, results, study validators) - Add Claude Code skills (create-study, run-optimization, generate-report, troubleshoot, analyze-model) - Add Atomizer Dashboard (React frontend + FastAPI backend) - Reorganize docs into structured directories (00-09) - Add neural surrogate modules and training infrastructure - Add multi-objective optimization support 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
21 KiB
Nastran Visualization Research: OP2/BDF/DAT File Processing
Research Date: 2025-11-21 Purpose: Investigate methods to visualize geometry/mesh and generate images of FEA metrics from Nastran files across optimization iterations
Executive Summary
Recommendation: Use pyNastran + PyVista combination for Atomizer visualization needs.
- pyNastran: Read OP2/BDF files, extract results (stress, displacement, eigenvalues)
- PyVista: Generate 3D visualizations and save images programmatically
This approach provides: ✅ Programmatic image generation (no GUI needed) ✅ Full automation for optimization iterations ✅ Rich visualization (mesh, stress contours, displacement plots) ✅ Dashboard integration ready (save PNGs for React dashboard) ✅ Lightweight (no commercial FEA software required)
1. pyNastran Overview
What It Does
pyNastran is a Python library for reading/writing/processing Nastran files:
- BDF (Input Files): Geometry, mesh, materials, boundary conditions
- OP2 (Results Files): Stress, strain, displacement, eigenvalues, etc.
- F06 (Text Output): Less structured, slower to parse
GitHub: https://github.com/SteveDoyle2/pyNastran Docs: https://pynastran-git.readthedocs.io/
Key Features
✅ Fast OP2 Reading: Vectorized, optimized for large files ✅ 427+ Supported Cards: Comprehensive BDF support ✅ HDF5 Export: For massive files (reduces memory usage) ✅ Result Extraction: Displacement, stress, strain, eigenvalues, SPC/MPC forces ✅ Built-in GUI: VTK-based viewer (optional, not needed for automation) ✅ SORT2 Support: Handles frequency/time-domain results
Supported Results
From OP2 files:
- Displacement, velocity, acceleration
- Temperature
- Eigenvectors & eigenvalues
- Element stress/strain (CQUAD4, CTRIA3, CBAR, CBEAM, CTETRA, etc.)
- SPC/MPC forces
- Grid point forces
- Strain energy
Installation
pip install pyNastran
Dependencies:
- numpy, scipy
- h5py (for HDF5 support)
- matplotlib (optional, for basic plotting)
- vtk (optional, for GUI only)
- PyQt5/PySide2 (optional, for GUI only)
Python Support: 3.9-3.12
2. Reading OP2 Files with pyNastran
Basic Usage
from pyNastran.op2.op2 import read_op2
# Read OP2 file (with optional pandas DataFrames)
op2 = read_op2('simulation.op2', build_dataframe=True, debug=False)
# Quick overview
print(op2.get_op2_stats())
Accessing Results
Displacement Results:
# Get displacements for subcase 1
disp = op2.displacements[1] # subcase ID
# NumPy array: [n_times, n_nodes, 6] (tx, ty, tz, rx, ry, rz)
displacement_data = disp.data
# Node IDs
node_ids = disp.node_gridtype[:, 0]
# Pandas DataFrame (if build_dataframe=True)
disp_df = disp.data_frame
Stress Results:
# Element stress (e.g., CQUAD4 plate elements)
plate_stress = op2.cquad4_stress[1] # subcase ID
# Data array: [n_times, n_elements, n_values]
# For CQUAD4: [fiber_distance, oxx, oyy, txy, angle, omax, omin, von_mises]
von_mises = plate_stress.data[itime, :, 7] # Von Mises stress
# Element IDs
element_ids = plate_stress.element_node[:, 0]
Eigenvalue Results:
# Eigenvectors
eig1 = op2.eigenvectors[1]
# Extract mode 2
mode2 = eig1.data[imode2, :, :]
# Frequencies
eigenvalues = op2.eigenvalues[1]
frequencies = eigenvalues.freqs
Reading Geometry from BDF
from pyNastran.bdf.bdf import read_bdf
# Read geometry
model = read_bdf('model.bdf')
# Access nodes
for nid, node in model.nodes.items():
xyz = node.get_position()
print(f"Node {nid}: {xyz}")
# Access elements
for eid, element in model.elements.items():
node_ids = element.node_ids
print(f"Element {eid}: nodes {node_ids}")
Reading Geometry from OP2 (with OP2Geom)
from pyNastran.op2.op2_geom import read_op2_geom
# Read OP2 with embedded geometry
model = read_op2_geom('simulation.op2')
# Now model has both geometry and results
nodes = model.nodes
elements = model.elements
displacements = model.displacements[1]
3. PyVista for 3D Visualization
What It Does
PyVista is a Python wrapper for VTK providing:
- 3D mesh visualization
- Scalar field mapping (stress, temperature)
- Vector field plotting (displacement)
- Programmatic screenshot generation (no GUI needed)
GitHub: https://github.com/pyvista/pyvista Docs: https://docs.pyvista.org/
Installation
pip install pyvista
Creating Mesh from Nastran Data
import pyvista as pv
import numpy as np
# Example: Create mesh from pyNastran nodes/elements
def create_pyvista_mesh(model, op2, subcase=1):
"""Create PyVista mesh with displacement and stress data."""
# Get nodes
node_ids = sorted(model.nodes.keys())
points = np.array([model.nodes[nid].get_position() for nid in node_ids])
# Get quad elements (CQUAD4)
cells = []
for eid, element in model.elements.items():
if element.type == 'CQUAD4':
# PyVista quad: [4, node1, node2, node3, node4]
nids = element.node_ids
cells.extend([4] + nids)
cells = np.array(cells)
celltypes = np.full(len(cells)//5, pv.CellType.QUAD, dtype=np.uint8)
# Create unstructured grid
mesh = pv.UnstructuredGrid(cells, celltypes, points)
# Add displacement field
disp = op2.displacements[subcase]
disp_vectors = disp.data[0, :, :3] # tx, ty, tz
mesh['displacement'] = disp_vectors
# Add stress (if available)
if subcase in op2.cquad4_stress:
stress = op2.cquad4_stress[subcase]
von_mises = stress.data[0, :, 7] # Von Mises stress
mesh['von_mises_stress'] = von_mises
return mesh
Programmatic Visualization & Screenshot
def generate_stress_plot(mesh, output_file='stress_plot.png'):
"""Generate stress contour plot and save as image."""
# Create off-screen plotter (no GUI window)
plotter = pv.Plotter(off_screen=True, window_size=[1920, 1080])
# Add mesh with stress coloring
plotter.add_mesh(
mesh,
scalars='von_mises_stress',
cmap='jet', # Color map
show_edges=True,
edge_color='black',
scalar_bar_args={
'title': 'Von Mises Stress (MPa)',
'vertical': True,
'position_x': 0.85,
'position_y': 0.1
}
)
# Set camera view
plotter.camera_position = 'iso' # Isometric view
plotter.camera.zoom(1.2)
# Add title
plotter.add_text('Stress Analysis - Trial #5', position='upper_left', font_size=14)
# Save screenshot
plotter.screenshot(output_file, return_img=False, scale=2)
plotter.close()
return output_file
Deformed Mesh Visualization
def generate_deformed_mesh_plot(mesh, scale_factor=100.0, output_file='deformed.png'):
"""Plot deformed mesh with displacement."""
# Warp mesh by displacement vector
warped = mesh.warp_by_vector('displacement', factor=scale_factor)
plotter = pv.Plotter(off_screen=True, window_size=[1920, 1080])
# Original mesh (transparent)
plotter.add_mesh(mesh, opacity=0.2, color='gray', show_edges=True)
# Deformed mesh (colored by displacement magnitude)
plotter.add_mesh(
warped,
scalars='displacement',
cmap='rainbow',
show_edges=True,
scalar_bar_args={'title': 'Displacement Magnitude (mm)'}
)
plotter.camera_position = 'iso'
plotter.screenshot(output_file, scale=2)
plotter.close()
4. Recommended Architecture for Atomizer
Integration Approach
Option A: Lightweight (Recommended)
- Use pyNastran to read OP2 files after each trial
- Use PyVista to generate static PNG images
- Store images in
studies/my_study/2_results/visualizations/trial_XXX_stress.png - Display images in React dashboard via image gallery
Option B: Full 3D (Advanced)
- Export PyVista mesh to VTK/glTF format
- Use Three.js or react-three-fiber in dashboard
- Interactive 3D viewer in browser
Proposed Workflow
Trial Completion
↓
NX Solver writes OP2 file
↓
pyNastran reads OP2 + BDF
↓
Extract: stress, displacement, geometry
↓
PyVista creates mesh + applies results
↓
Generate images:
- stress_contour.png
- displacement.png
- deformed_shape.png
↓
Store in 2_results/visualizations/trial_XXX/
↓
Dashboard polls for new images
↓
Display in React gallery component
File Structure
studies/my_optimization/
├── 1_setup/
│ └── model/
│ ├── model.prt
│ ├── model.sim
│ └── model.bdf ← BDF for geometry
├── 2_results/
│ ├── study.db
│ ├── trial_log.json
│ └── visualizations/ ← NEW: Generated images
│ ├── trial_000/
│ │ ├── stress_vonmises.png
│ │ ├── displacement_magnitude.png
│ │ └── deformed_shape.png
│ ├── trial_001/
│ │ └── ...
│ └── pareto_front/ ← Best designs
│ ├── trial_009_stress.png
│ └── trial_042_stress.png
5. Implementation Example for Atomizer
Visualization Module
# optimization_engine/visualizer.py
"""
Nastran Visualization Module
Generates FEA result visualizations from OP2/BDF files using pyNastran + PyVista.
"""
from pathlib import Path
import numpy as np
import pyvista as pv
from pyNastran.op2.op2_geom import read_op2_geom
from pyNastran.bdf.bdf import read_bdf
class NastranVisualizer:
"""Generate visualizations from Nastran results."""
def __init__(self, bdf_path: Path, output_dir: Path):
"""
Initialize visualizer.
Args:
bdf_path: Path to BDF file (for geometry)
output_dir: Directory to save images
"""
self.bdf_path = bdf_path
self.output_dir = Path(output_dir)
self.output_dir.mkdir(parents=True, exist_ok=True)
# Load geometry once
self.model = read_bdf(str(bdf_path))
def generate_trial_visualizations(self, op2_path: Path, trial_number: int, subcase: int = 1):
"""
Generate all visualizations for a trial.
Args:
op2_path: Path to OP2 results file
trial_number: Trial number
subcase: Nastran subcase ID
Returns:
dict: Paths to generated images
"""
# Read results
op2 = read_op2_geom(str(op2_path))
# Create trial output directory
trial_dir = self.output_dir / f"trial_{trial_number:03d}"
trial_dir.mkdir(exist_ok=True)
# Generate visualizations
images = {}
# 1. Stress contour
if subcase in op2.cquad4_stress:
images['stress'] = self._plot_stress(op2, subcase, trial_dir / 'stress_vonmises.png', trial_number)
# 2. Displacement magnitude
if subcase in op2.displacements:
images['displacement'] = self._plot_displacement(op2, subcase, trial_dir / 'displacement.png', trial_number)
# 3. Deformed shape
if subcase in op2.displacements:
images['deformed'] = self._plot_deformed_shape(op2, subcase, trial_dir / 'deformed_shape.png', trial_number)
return images
def _create_mesh(self, op2, subcase):
"""Create PyVista mesh from Nastran data."""
# Get nodes
node_ids = sorted(self.model.nodes.keys())
points = np.array([self.model.nodes[nid].get_position() for nid in node_ids])
# Get CQUAD4 elements
cells = []
cell_data = []
for eid, element in self.model.elements.items():
if element.type == 'CQUAD4':
nids = [self.model.nodes.get_index(nid) for nid in element.node_ids]
cells.extend([4] + nids)
if not cells:
raise ValueError("No CQUAD4 elements found in model")
cells = np.array(cells)
celltypes = np.full(len(cells)//5, pv.CellType.QUAD, dtype=np.uint8)
mesh = pv.UnstructuredGrid(cells, celltypes, points)
# Add result data
if subcase in op2.displacements:
disp = op2.displacements[subcase]
disp_vectors = disp.data[0, :, :3] # tx, ty, tz
mesh['displacement'] = disp_vectors
mesh['displacement_magnitude'] = np.linalg.norm(disp_vectors, axis=1)
if subcase in op2.cquad4_stress:
stress = op2.cquad4_stress[subcase]
von_mises = stress.data[0, :, 7]
mesh.cell_data['von_mises_stress'] = von_mises
return mesh
def _plot_stress(self, op2, subcase, output_path, trial_number):
"""Plot Von Mises stress contour."""
mesh = self._create_mesh(op2, subcase)
plotter = pv.Plotter(off_screen=True, window_size=[1920, 1080])
plotter.add_mesh(
mesh,
scalars='von_mises_stress',
cmap='jet',
show_edges=True,
edge_color='black',
scalar_bar_args={
'title': 'Von Mises Stress (MPa)',
'vertical': True,
'position_x': 0.85,
'position_y': 0.1,
'fmt': '%.1f'
}
)
plotter.camera_position = 'iso'
plotter.camera.zoom(1.2)
plotter.add_text(f'Stress Analysis - Trial #{trial_number}',
position='upper_left', font_size=14, color='black')
plotter.screenshot(str(output_path), scale=2)
plotter.close()
return output_path
def _plot_displacement(self, op2, subcase, output_path, trial_number):
"""Plot displacement magnitude."""
mesh = self._create_mesh(op2, subcase)
plotter = pv.Plotter(off_screen=True, window_size=[1920, 1080])
plotter.add_mesh(
mesh,
scalars='displacement_magnitude',
cmap='rainbow',
show_edges=True,
edge_color='gray',
scalar_bar_args={
'title': 'Displacement (mm)',
'vertical': True,
'position_x': 0.85,
'position_y': 0.1
}
)
plotter.camera_position = 'iso'
plotter.camera.zoom(1.2)
plotter.add_text(f'Displacement - Trial #{trial_number}',
position='upper_left', font_size=14, color='black')
plotter.screenshot(str(output_path), scale=2)
plotter.close()
return output_path
def _plot_deformed_shape(self, op2, subcase, output_path, trial_number, scale_factor=100.0):
"""Plot deformed vs undeformed shape."""
mesh = self._create_mesh(op2, subcase)
warped = mesh.warp_by_vector('displacement', factor=scale_factor)
plotter = pv.Plotter(off_screen=True, window_size=[1920, 1080])
# Original (transparent)
plotter.add_mesh(mesh, opacity=0.2, color='gray', show_edges=True, edge_color='black')
# Deformed (colored)
plotter.add_mesh(
warped,
scalars='displacement_magnitude',
cmap='rainbow',
show_edges=True,
edge_color='black',
scalar_bar_args={
'title': f'Displacement (mm) [Scale: {scale_factor}x]',
'vertical': True
}
)
plotter.camera_position = 'iso'
plotter.camera.zoom(1.2)
plotter.add_text(f'Deformed Shape - Trial #{trial_number}',
position='upper_left', font_size=14, color='black')
plotter.screenshot(str(output_path), scale=2)
plotter.close()
return output_path
Usage in Optimization Loop
# In optimization_engine/intelligent_optimizer.py
from optimization_engine.visualizer import NastranVisualizer
class IntelligentOptimizer:
def __init__(self, ...):
# ... existing code ...
# Initialize visualizer
bdf_path = self.study_dir.parent / "1_setup" / "model" / f"{self.config['model_name']}.bdf"
viz_dir = self.study_dir / "visualizations"
self.visualizer = NastranVisualizer(bdf_path, viz_dir)
def _run_trial(self, trial):
# ... existing code: update model, solve, extract results ...
# NEW: Generate visualizations after successful solve
if trial.state == optuna.trial.TrialState.COMPLETE:
op2_path = self.get_op2_path(trial.number)
try:
images = self.visualizer.generate_trial_visualizations(
op2_path=op2_path,
trial_number=trial.number,
subcase=1
)
# Store image paths in trial user_attrs for dashboard access
trial.set_user_attr('visualization_images', {
'stress': str(images.get('stress', '')),
'displacement': str(images.get('displacement', '')),
'deformed': str(images.get('deformed', ''))
})
except Exception as e:
print(f"Warning: Visualization failed for trial {trial.number}: {e}")
6. Alternative: Headless pyNastran GUI
pyNastran has a built-in GUI, but it can also be used programmatically for screenshots:
from pyNastran.gui.main_window import MainWindow
# This requires VTK + PyQt5/PySide2 (heavier dependencies)
# Not recommended for automation - use PyVista instead
Verdict: PyVista is simpler and more flexible for automation.
7. From Scratch Alternative
Pros
- Full control
- No external dependencies beyond numpy/matplotlib
Cons
- Reinventing the wheel (mesh handling, element connectivity)
- 2D plots only (matplotlib doesn't do 3D well)
- Labor intensive (weeks of development)
- Limited features (no proper stress contours, deformed shapes)
Verdict: Not recommended. pyNastran + PyVista is mature, well-tested, and saves months of development.
8. Comparison Matrix
| Feature | pyNastran GUI | pyNastran + PyVista | From Scratch |
|---|---|---|---|
| Programmatic | ❌ (requires GUI) | ✅ Off-screen rendering | ✅ |
| Automation | ❌ | ✅ | ✅ |
| 3D Visualization | ✅ | ✅ | ❌ (2D only) |
| Stress Contours | ✅ | ✅ | ⚠️ (basic) |
| Deformed Shapes | ✅ | ✅ | ❌ |
| Development Time | N/A | ~1-2 days | ~3-4 weeks |
| Dependencies | Heavy (VTK, Qt) | Light (numpy, vtk) | Minimal |
| Dashboard Ready | ❌ | ✅ PNG images | ✅ |
| Maintenance | N/A | Low | High |
9. Recommended Implementation Plan
Phase 1: Basic Visualization (1-2 days)
- Install pyNastran + PyVista
- Create
NastranVisualizerclass - Integrate into
IntelligentOptimizerpost-trial callback - Generate 3 images per trial: stress, displacement, deformed shape
- Test with bracket study
Phase 2: Dashboard Integration (1 day)
- Add
visualizations/directory to study structure - Store image paths in
trial.user_attrs - Create React component:
TrialVisualizationGallery - Display images in dashboard trial detail view
Phase 3: Advanced Features (optional, 2-3 days)
- Eigenmode animation (GIF generation)
- Section cut views
- Multiple camera angles
- Custom color scales
- Comparison view (overlay 2 trials)
10. Installation & Testing
Install Dependencies
# Install pyNastran
pip install pyNastran
# Install PyVista
pip install pyvista
# Optional: For HDF5 support
pip install h5py
Quick Test
# test_visualization.py
from pathlib import Path
from optimization_engine.visualizer import NastranVisualizer
# Paths (adjust to your study)
bdf_path = Path("studies/bracket_stiffness_optimization/1_setup/model/Bracket.bdf")
op2_path = Path("studies/bracket_stiffness_optimization/1_setup/model/Bracket.op2")
output_dir = Path("test_visualizations")
# Create visualizer
viz = NastranVisualizer(bdf_path, output_dir)
# Generate images
images = viz.generate_trial_visualizations(op2_path, trial_number=0, subcase=1)
print(f"Generated images: {images}")
11. Key Takeaways
✅ pyNastran + PyVista is the optimal solution ✅ Programmatic image generation without GUI ✅ Production-ready libraries with active development ✅ Dashboard integration via PNG images ✅ Fast implementation (1-2 days vs weeks from scratch) ✅ Extensible for future 3D viewer (Three.js)
Next Steps:
- Install pyNastran + PyVista in Atomizer environment
- Implement
NastranVisualizerclass - Integrate visualization callback in optimization loop
- Test with existing bracket study
- Add image gallery to React dashboard
12. References
pyNastran:
- GitHub: https://github.com/SteveDoyle2/pyNastran
- Docs: https://pynastran-git.readthedocs.io/
- OP2 Demo: https://pynastran-git.readthedocs.io/en/latest/quick_start/op2_demo.html
PyVista:
- GitHub: https://github.com/pyvista/pyvista
- Docs: https://docs.pyvista.org/
- Screenshot Examples: https://docs.pyvista.org/examples/02-plot/screenshot.html
Alternative Libraries:
- OP_Map: https://github.com/felixrlopezm/NASTRAN-OP_Map (built on pyNastran, for Excel export)
- FeResPost: https://ferespost.eu/ (Ruby/Python, commercial)
Document Maintained By: Atomizer Development Team Last Updated: 2025-11-21