Files
Atomizer/docs/07_DEVELOPMENT/NASTRAN_VISUALIZATION_RESEARCH.md
Anto01 e3bdb08a22 feat: Major update with validators, skills, dashboard, and docs reorganization
- 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>
2025-11-25 19:23:58 -05:00

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()

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

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

# 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:

  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:

PyVista:

Alternative Libraries:


Document Maintained By: Atomizer Development Team Last Updated: 2025-11-21