- 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>
756 lines
21 KiB
Markdown
756 lines
21 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
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
|
|
|
|
```python
|
|
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:**
|
|
```python
|
|
# 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:**
|
|
```python
|
|
# 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:**
|
|
```python
|
|
# Eigenvectors
|
|
eig1 = op2.eigenvectors[1]
|
|
|
|
# Extract mode 2
|
|
mode2 = eig1.data[imode2, :, :]
|
|
|
|
# Frequencies
|
|
eigenvalues = op2.eigenvalues[1]
|
|
frequencies = eigenvalues.freqs
|
|
```
|
|
|
|
### Reading Geometry from BDF
|
|
|
|
```python
|
|
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)
|
|
|
|
```python
|
|
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
|
|
|
|
```bash
|
|
pip install pyvista
|
|
```
|
|
|
|
### Creating Mesh from Nastran Data
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
# 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
|
|
|
|
```python
|
|
# 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:
|
|
|
|
```python
|
|
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)
|
|
1. Install pyNastran + PyVista
|
|
2. Create `NastranVisualizer` class
|
|
3. Integrate into `IntelligentOptimizer` post-trial callback
|
|
4. Generate 3 images per trial: stress, displacement, deformed shape
|
|
5. Test with bracket study
|
|
|
|
### Phase 2: Dashboard Integration (1 day)
|
|
1. Add `visualizations/` directory to study structure
|
|
2. Store image paths in `trial.user_attrs`
|
|
3. Create React component: `TrialVisualizationGallery`
|
|
4. Display images in dashboard trial detail view
|
|
|
|
### Phase 3: Advanced Features (optional, 2-3 days)
|
|
1. Eigenmode animation (GIF generation)
|
|
2. Section cut views
|
|
3. Multiple camera angles
|
|
4. Custom color scales
|
|
5. Comparison view (overlay 2 trials)
|
|
|
|
---
|
|
|
|
## 10. Installation & Testing
|
|
|
|
### Install Dependencies
|
|
|
|
```bash
|
|
# Install pyNastran
|
|
pip install pyNastran
|
|
|
|
# Install PyVista
|
|
pip install pyvista
|
|
|
|
# Optional: For HDF5 support
|
|
pip install h5py
|
|
```
|
|
|
|
### Quick Test
|
|
|
|
```python
|
|
# 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**:
|
|
1. Install pyNastran + PyVista in Atomizer environment
|
|
2. Implement `NastranVisualizer` class
|
|
3. Integrate visualization callback in optimization loop
|
|
4. Test with existing bracket study
|
|
5. 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
|