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>
This commit is contained in:
191
optimization_engine/extractors/bdf_mass_extractor.py
Normal file
191
optimization_engine/extractors/bdf_mass_extractor.py
Normal file
@@ -0,0 +1,191 @@
|
||||
"""
|
||||
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}")
|
||||
Reference in New Issue
Block a user