Files
Atomizer/optimization_engine/extractors/extract_part_mass_material.py
Antoine 70ac34e3d3 feat: Add E11 Part Mass extractor, document pyNastran mass accuracy issue
New E11 Part Mass Extractor:
- Add nx_journals/extract_part_mass_material.py - NX journal using
  NXOpen.MeasureManager.NewMassProperties() for accurate geometry-based mass
- Add optimization_engine/extractors/extract_part_mass_material.py - Python
  wrapper that reads JSON output from journal
- Add E11 entry to extractors/catalog.json

Documentation Updates:
- SYS_12_EXTRACTOR_LIBRARY.md: Add mass accuracy warning noting pyNastran
  get_mass_breakdown() under-reports ~7% on hex-dominant meshes with
  tet/pyramid fill elements. E11 (geometry .prt) should be preferred over
  E4 (BDF) unless material is overridden at FEM level.
- 01_CHEATSHEET.md: Add mass extraction tip

V14 Config:
- Expand design variable bounds (blank_backface_angle max 4.5°,
  whiffle_triangle_closeness max 80mm, whiffle_min max 60mm)

Testing showed:
- E11 from .prt: 97.66 kg (accurate - matches NX GUI)
- E4 pyNastran get_mass_breakdown(): 90.73 kg (~7% under-reported)

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-11 22:15:36 -05:00

277 lines
8.8 KiB
Python

"""
Extract Part Mass and Material Properties from NX Part Files
This extractor reads mass and material data from a temp file written by
the NX journal: nx_journals/extract_part_mass_material.py
The journal uses NXOpen.MeasureManager to extract:
- Mass (kg)
- Volume (mm^3)
- Surface area (mm^2)
- Center of gravity (mm)
- Material name and density
NX Open APIs Used (by journal):
- NXOpen.MeasureManager.NewMassProperties()
- NXOpen.MeasureBodies
- NXOpen.Body.GetBodies()
- NXOpen.PhysicalMaterial
Author: Atomizer
Created: 2025-12-05
Version: 1.0
"""
from pathlib import Path
from typing import Dict, Any, Optional, Union
import json
def extract_part_mass_material(
prt_file: Union[str, Path],
properties_file: Optional[Union[str, Path]] = None
) -> Dict[str, Any]:
"""
Extract mass and material properties from NX part file.
This function reads from a temp JSON file that must be created by
running the NX journal: nx_journals/extract_part_mass_material.py
Args:
prt_file: Path to .prt file (used to locate temp file)
properties_file: Optional explicit path to _temp_part_properties.json
If not provided, looks in same directory as prt_file
Returns:
Dictionary containing:
- 'mass_kg': Mass in kilograms (float)
- 'mass_g': Mass in grams (float)
- 'volume_mm3': Volume in mm^3 (float)
- 'surface_area_mm2': Surface area in mm^2 (float)
- 'center_of_gravity_mm': [x, y, z] in mm (list)
- 'moments_of_inertia': {'Ixx', 'Iyy', 'Izz', 'unit'} or None
- 'material': {'name', 'density', 'density_unit'} (dict)
- 'num_bodies': Number of solid bodies (int)
Raises:
FileNotFoundError: If prt file or temp properties file not found
ValueError: If temp file has invalid format or extraction failed
Example:
>>> result = extract_part_mass_material('model.prt')
>>> print(f"Mass: {result['mass_kg']:.3f} kg")
Mass: 1.234 kg
>>> print(f"Material: {result['material']['name']}")
Material: Aluminum_6061
Note:
Before calling this function, you must run the NX journal to
create the temp file:
```
run_journal.exe extract_part_mass_material.py model.prt
```
"""
prt_file = Path(prt_file)
if not prt_file.exists():
raise FileNotFoundError(f"Part file not found: {prt_file}")
# Determine properties file location
if properties_file:
props_file = Path(properties_file)
else:
props_file = prt_file.parent / "_temp_part_properties.json"
if not props_file.exists():
raise FileNotFoundError(
f"Part properties temp file not found: {props_file}\n"
f"Run the NX journal first:\n"
f" run_journal.exe extract_part_mass_material.py {prt_file}"
)
# Read and parse JSON
try:
with open(props_file, 'r') as f:
data = json.load(f)
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON in properties file: {e}")
# Check for extraction errors
if not data.get('success', False):
error_msg = data.get('error', 'Unknown error during extraction')
raise ValueError(f"NX extraction failed: {error_msg}")
# Build result dictionary
result = {
'mass_kg': float(data.get('mass_kg', 0.0)),
'mass_g': float(data.get('mass_g', 0.0)),
'volume_mm3': float(data.get('volume_mm3', 0.0)),
'surface_area_mm2': float(data.get('surface_area_mm2', 0.0)),
'center_of_gravity_mm': data.get('center_of_gravity_mm', [0.0, 0.0, 0.0]),
'moments_of_inertia': data.get('moments_of_inertia'),
'material': data.get('material', {'name': None, 'density': None, 'density_unit': 'kg/mm^3'}),
'num_bodies': int(data.get('num_bodies', 0)),
'part_file': data.get('part_file', prt_file.name),
}
print(f"[OK] Part mass: {result['mass_kg']:.6f} kg ({result['mass_g']:.2f} g)")
if result['material'].get('name'):
print(f"[OK] Material: {result['material']['name']}")
return result
def extract_part_mass(
prt_file: Union[str, Path],
properties_file: Optional[Union[str, Path]] = None
) -> float:
"""
Convenience function to extract just the mass in kg.
Args:
prt_file: Path to .prt file
properties_file: Optional explicit path to temp file
Returns:
Mass in kilograms (float)
Example:
>>> mass = extract_part_mass('model.prt')
>>> print(f"Mass: {mass:.3f} kg")
Mass: 1.234 kg
"""
result = extract_part_mass_material(prt_file, properties_file)
return result['mass_kg']
def extract_part_material(
prt_file: Union[str, Path],
properties_file: Optional[Union[str, Path]] = None
) -> Dict[str, Any]:
"""
Convenience function to extract just material info.
Args:
prt_file: Path to .prt file
properties_file: Optional explicit path to temp file
Returns:
Dictionary with 'name', 'density', 'density_unit'
Example:
>>> mat = extract_part_material('model.prt')
>>> print(f"Material: {mat['name']}, Density: {mat['density']}")
Material: Steel_304, Density: 7.93e-06
"""
result = extract_part_mass_material(prt_file, properties_file)
return result['material']
class PartMassExtractor:
"""
Class-based extractor for part mass and material with caching.
Use this when you need to extract properties from multiple parts
or want to cache results.
Example:
>>> extractor = PartMassExtractor('model.prt')
>>> result = extractor.extract()
>>> print(result['mass_kg'])
1.234
"""
def __init__(
self,
prt_file: Union[str, Path],
properties_file: Optional[Union[str, Path]] = None
):
"""
Initialize the extractor.
Args:
prt_file: Path to .prt file
properties_file: Optional explicit path to temp file
"""
self.prt_file = Path(prt_file)
self.properties_file = Path(properties_file) if properties_file else None
self._cached_result = None
def extract(self, use_cache: bool = True) -> Dict[str, Any]:
"""
Extract mass and material properties.
Args:
use_cache: If True, returns cached result if available
Returns:
Dictionary with all extracted properties
"""
if use_cache and self._cached_result is not None:
return self._cached_result
self._cached_result = extract_part_mass_material(
self.prt_file,
self.properties_file
)
return self._cached_result
@property
def mass_kg(self) -> float:
"""Get mass in kg (extracts if needed)."""
return self.extract()['mass_kg']
@property
def mass_g(self) -> float:
"""Get mass in grams (extracts if needed)."""
return self.extract()['mass_g']
@property
def material_name(self) -> Optional[str]:
"""Get material name (extracts if needed)."""
return self.extract()['material'].get('name')
@property
def density(self) -> Optional[float]:
"""Get material density in kg/mm^3 (extracts if needed)."""
return self.extract()['material'].get('density')
def clear_cache(self):
"""Clear the cached result."""
self._cached_result = None
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print(f"Usage: python {sys.argv[0]} <prt_file> [properties_file]")
sys.exit(1)
prt_file = Path(sys.argv[1])
props_file = Path(sys.argv[2]) if len(sys.argv) > 2 else None
try:
result = extract_part_mass_material(prt_file, props_file)
print("\n" + "="*50)
print("PART MASS & MATERIAL EXTRACTION RESULTS")
print("="*50)
print(f"Part File: {result['part_file']}")
print(f"Mass: {result['mass_kg']:.6f} kg ({result['mass_g']:.2f} g)")
print(f"Volume: {result['volume_mm3']:.2f} mm^3")
print(f"Surface Area: {result['surface_area_mm2']:.2f} mm^2")
print(f"Center of Gravity: {result['center_of_gravity_mm']} mm")
print(f"Num Bodies: {result['num_bodies']}")
if result['material']['name']:
print(f"Material: {result['material']['name']}")
if result['material']['density']:
print(f"Density: {result['material']['density']} {result['material']['density_unit']}")
if result['moments_of_inertia']:
moi = result['moments_of_inertia']
print(f"Moments of Inertia: Ixx={moi['Ixx']}, Iyy={moi['Iyy']}, Izz={moi['Izz']} {moi['unit']}")
except Exception as e:
print(f"\nERROR: {e}")
import traceback
traceback.print_exc()
sys.exit(1)