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>
This commit is contained in:
@@ -34,5 +34,19 @@
|
||||
"metric": "total"
|
||||
},
|
||||
"signature": "2f58f241a96afb1f"
|
||||
},
|
||||
"e11_part_mass_material": {
|
||||
"name": "extract_part_mass_material",
|
||||
"filename": "extract_part_mass_material.py",
|
||||
"action": "extract_part_mass_material",
|
||||
"domain": "cad_extraction",
|
||||
"description": "Extract mass, volume, surface area, CoG, and material from NX .prt files via NXOpen.MeasureManager",
|
||||
"params": {
|
||||
"result_type": "part_mass",
|
||||
"metric": "total",
|
||||
"requires_journal": "nx_journals/extract_part_mass_material.py"
|
||||
},
|
||||
"signature": "e11_part_mass_material",
|
||||
"notes": "Requires running NX journal first: run_journal.exe nx_journals/extract_part_mass_material.py <prt_file> -args <prt_path> <output_dir>"
|
||||
}
|
||||
}
|
||||
276
optimization_engine/extractors/extract_part_mass_material.py
Normal file
276
optimization_engine/extractors/extract_part_mass_material.py
Normal file
@@ -0,0 +1,276 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user