# 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