Files
Atomizer/optimization_engine/extractors/bdf_mass_extractor.py
Anto01 2b3573ec42 feat: Add AtomizerField training data export and intelligent model discovery
Major additions:
- Training data export system for AtomizerField neural network training
- Bracket stiffness optimization study with 50+ training samples
- Intelligent NX model discovery (auto-detect solutions, expressions, mesh)
- Result extractors module for displacement, stress, frequency, mass
- User-generated NX journals for advanced workflows
- Archive structure for legacy scripts and test outputs
- Protocol documentation and dashboard launcher

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 12:01:50 -05:00

192 lines
5.9 KiB
Python

"""
BDF Mass Extractor
==================
Extract mass from Nastran BDF/DAT file using pyNastran.
This is more reliable than OP2 GRDPNT extraction since the BDF always contains
material properties and element geometry.
Usage:
extractor = BDFMassExtractor(bdf_file="model.dat")
mass_kg = extractor.extract_mass()
"""
from pathlib import Path
from typing import Dict, Any
import numpy as np
try:
from pyNastran.bdf.bdf import read_bdf
except ImportError:
raise ImportError("pyNastran is required. Install with: pip install pyNastran")
class BDFMassExtractor:
"""
Extract mass from Nastran BDF/DAT file.
This extractor calculates mass from:
- Element geometry (GRID nodes, element definitions)
- Material properties (MAT1, etc. with density)
- Physical properties (PSOLID, PSHELL, etc. with thickness)
"""
def __init__(self, bdf_file: str):
"""
Args:
bdf_file: Path to .dat or .bdf file
"""
self.bdf_file = Path(bdf_file)
self._bdf_model = None
def _load_bdf(self):
"""Lazy load BDF file"""
if self._bdf_model is None:
if not self.bdf_file.exists():
raise FileNotFoundError(f"BDF file not found: {self.bdf_file}")
# Read BDF with xref=True to cross-reference cards
self._bdf_model = read_bdf(str(self.bdf_file), xref=True, debug=False)
return self._bdf_model
def extract_mass(self) -> Dict[str, Any]:
"""
Calculate total structural mass from BDF.
Returns:
dict: {
'mass_kg': total mass in kg,
'mass_g': total mass in grams,
'mass_ton': total mass in metric tons,
'cg': [x, y, z] center of gravity (if calculable),
'num_elements': number of elements,
'units': 'ton-mm-sec (Nastran default)',
'breakdown': {element_type: mass} breakdown by element type
}
"""
bdf = self._load_bdf()
# Calculate mass manually by iterating through elements
total_mass_ton = 0.0
breakdown = {}
total_elements = 0
cg_numerator = np.zeros(3) # For weighted CG calculation
for eid, elem in bdf.elements.items():
try:
# Get element mass
elem_mass = elem.Mass() # Returns mass in Nastran units (ton)
total_mass_ton += elem_mass
# Track by element type
elem_type = elem.type
if elem_type not in breakdown:
breakdown[elem_type] = 0.0
breakdown[elem_type] += elem_mass
# Get element centroid for CG calculation
try:
centroid = elem.Centroid()
cg_numerator += elem_mass * np.array(centroid)
except:
pass # Some elements may not have centroid method
total_elements += 1
except Exception as e:
# Some elements might not have Mass() method
# (e.g., rigid elements, SPCs, etc.)
continue
# Calculate center of gravity
if total_mass_ton > 0:
cg = cg_numerator / total_mass_ton
else:
cg = np.zeros(3)
# Convert units: NX uses kg-mm-s where "ton" = 1 kg (not 1000 kg!)
# So the mass returned by pyNastran is already in kg
mass_kg = float(total_mass_ton) # Already in kg
mass_g = mass_kg * 1000.0
# Breakdown is already in kg
breakdown_kg = {k: float(v) for k, v in breakdown.items()}
return {
'mass_kg': mass_kg,
'mass_g': mass_g,
'mass_ton': float(total_mass_ton), # This is actually kg in NX units
'cg': cg.tolist() if hasattr(cg, 'tolist') else list(cg),
'num_elements': total_elements,
'units': 'kg-mm-s (NX default)',
'breakdown': breakdown,
'breakdown_kg': breakdown_kg,
}
def get_material_info(self) -> Dict[str, Any]:
"""
Get material property information from BDF.
Returns:
dict: Material ID -> {density, E, nu, ...}
"""
bdf = self._load_bdf()
materials = {}
for mid, mat in bdf.materials.items():
mat_info = {
'type': mat.type,
'mid': mid,
}
# MAT1 (isotropic)
if hasattr(mat, 'rho') and mat.rho is not None:
mat_info['density'] = float(mat.rho) # ton/mm^3
mat_info['density_kg_m3'] = float(mat.rho * 1e12) # Convert to kg/m^3
if hasattr(mat, 'e'):
mat_info['E'] = float(mat.e) if mat.e is not None else None
if hasattr(mat, 'nu'):
mat_info['nu'] = float(mat.nu) if mat.nu is not None else None
materials[mid] = mat_info
return materials
def extract_mass_from_bdf(bdf_file: str) -> float:
"""
Convenience function to extract mass in kg.
Args:
bdf_file: Path to .dat or .bdf file
Returns:
Mass in kilograms
"""
extractor = BDFMassExtractor(bdf_file)
result = extractor.extract_mass()
return result['mass_kg']
if __name__ == "__main__":
# Example usage
import sys
if len(sys.argv) > 1:
bdf_file = sys.argv[1]
extractor = BDFMassExtractor(bdf_file)
# Extract mass
mass_result = extractor.extract_mass()
print(f"Mass: {mass_result['mass_kg']:.6f} kg ({mass_result['mass_g']:.2f} g)")
print(f"CG: {mass_result['cg']}")
print(f"Elements: {mass_result['num_elements']}")
print(f"Element breakdown: {mass_result['breakdown']}")
# Get material info
materials = extractor.get_material_info()
print(f"\nMaterials:")
for mid, mat_info in materials.items():
print(f" MAT{mid}: {mat_info}")