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>
192 lines
5.9 KiB
Python
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}")
|