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:
@@ -49,6 +49,9 @@ requires_skills:
|
||||
| Zernike builder | E10 | `ZernikeObjectiveBuilder(op2_finder)` |
|
||||
| Part mass + material | E11 | `extract_part_mass_material(prt_file)` → mass, volume, material |
|
||||
|
||||
> **Mass extraction tip**: Always use E11 (geometry .prt) over E4 (BDF) for accuracy.
|
||||
> pyNastran under-reports mass ~7% on hex-dominant meshes with tet/pyramid fills.
|
||||
|
||||
**Full details**: See `SYS_12_EXTRACTOR_LIBRARY.md` or `modules/extractors-catalog.md`
|
||||
|
||||
---
|
||||
|
||||
@@ -195,6 +195,18 @@ material = extractor.material_name
|
||||
- `NXOpen.Body.GetBodies()`
|
||||
- `NXOpen.PhysicalMaterial`
|
||||
|
||||
**IMPORTANT - Mass Accuracy Note**:
|
||||
> **Always prefer E11 (geometry-based) over E4 (BDF-based) for mass extraction.**
|
||||
>
|
||||
> Testing on hex-dominant meshes with tet/pyramid fill elements revealed that:
|
||||
> - **E11 from .prt**: 97.66 kg (accurate - matches NX GUI)
|
||||
> - **E4 pyNastran get_mass_breakdown()**: 90.73 kg (~7% under-reported)
|
||||
> - **E4 pyNastran sum(elem.Volume())*rho**: 100.16 kg (~2.5% over-reported)
|
||||
>
|
||||
> The `get_mass_breakdown()` function in pyNastran has known issues with mixed-element
|
||||
> meshes (CHEXA + CPENTA + CPYRAM + CTETRA). Use E11 with the NX journal for reliable
|
||||
> mass values. Only use E4 if material properties are overridden at FEM level.
|
||||
|
||||
### E6: Field Data Extraction
|
||||
|
||||
**Module**: `optimization_engine.extractors.field_data_extractor`
|
||||
|
||||
328
nx_journals/extract_part_mass_material.py
Normal file
328
nx_journals/extract_part_mass_material.py
Normal file
@@ -0,0 +1,328 @@
|
||||
"""
|
||||
NX Journal Script to Extract Part Mass and Material Properties
|
||||
|
||||
This script extracts:
|
||||
- Mass (kg)
|
||||
- Volume (mm^3)
|
||||
- Surface area (mm^2)
|
||||
- Center of gravity (mm)
|
||||
- Material name (if assigned)
|
||||
- Density (kg/mm^3)
|
||||
|
||||
Results are written to _temp_part_properties.json in the working directory.
|
||||
|
||||
Usage:
|
||||
run_journal.exe extract_part_mass_material.py <prt_file_path> [output_dir]
|
||||
|
||||
NX Open APIs Used:
|
||||
- NXOpen.MeasureManager.NewMassProperties()
|
||||
- NXOpen.MeasureBodies
|
||||
- NXOpen.Body.GetBodies()
|
||||
- NXOpen.PhysicalMaterial
|
||||
|
||||
Author: Atomizer
|
||||
Created: 2025-12-05
|
||||
Version: 1.0
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import NXOpen
|
||||
import NXOpen.UF
|
||||
|
||||
|
||||
def get_all_solid_bodies(part):
|
||||
"""Get all solid bodies from the part."""
|
||||
bodies = []
|
||||
try:
|
||||
# Get bodies from the part
|
||||
for body in part.Bodies:
|
||||
if body.IsSolidBody:
|
||||
bodies.append(body)
|
||||
except Exception as e:
|
||||
print(f"[JOURNAL] Warning getting bodies: {e}")
|
||||
return bodies
|
||||
|
||||
|
||||
def get_material_info(part, body):
|
||||
"""Extract material information from a body if assigned."""
|
||||
material_info = {
|
||||
'name': None,
|
||||
'density': None,
|
||||
'density_unit': 'kg/mm^3'
|
||||
}
|
||||
|
||||
# Method 1: Try to get physical material directly from body
|
||||
try:
|
||||
phys_mat = body.GetPhysicalMaterial()
|
||||
if phys_mat:
|
||||
material_info['name'] = phys_mat.Name
|
||||
print(f"[JOURNAL] Material from body: {phys_mat.Name}")
|
||||
|
||||
# Try to get density property
|
||||
try:
|
||||
density_prop = phys_mat.GetPropertyValue("Density")
|
||||
if density_prop:
|
||||
material_info['density'] = float(density_prop)
|
||||
print(f"[JOURNAL] Density: {density_prop}")
|
||||
except Exception as de:
|
||||
print(f"[JOURNAL] Could not get density: {de}")
|
||||
except Exception as e:
|
||||
print(f"[JOURNAL] GetPhysicalMaterial failed: {e}")
|
||||
|
||||
# Method 2: If no material from body, try part-level material assignment
|
||||
if material_info['name'] is None:
|
||||
try:
|
||||
# Check if part has PhysicalMaterialManager
|
||||
pmm = part.PhysicalMaterialManager
|
||||
if pmm:
|
||||
# Get all materials in the part
|
||||
materials = pmm.GetAllPhysicalMaterials()
|
||||
if materials and len(materials) > 0:
|
||||
# Use the first material as default
|
||||
mat = materials[0]
|
||||
material_info['name'] = mat.Name
|
||||
print(f"[JOURNAL] Material from PhysicalMaterialManager: {mat.Name}")
|
||||
|
||||
try:
|
||||
density_prop = mat.GetPropertyValue("Density")
|
||||
if density_prop:
|
||||
material_info['density'] = float(density_prop)
|
||||
except:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"[JOURNAL] PhysicalMaterialManager failed: {e}")
|
||||
|
||||
# Method 3: Try using body attributes
|
||||
if material_info['name'] is None:
|
||||
try:
|
||||
# Some NX versions store material as an attribute
|
||||
attrs = body.GetUserAttributes()
|
||||
for attr in attrs:
|
||||
if 'material' in attr.Title.lower():
|
||||
material_info['name'] = attr.StringValue
|
||||
print(f"[JOURNAL] Material from attribute: {attr.StringValue}")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"[JOURNAL] Body attributes failed: {e}")
|
||||
|
||||
return material_info
|
||||
|
||||
|
||||
def extract_mass_properties(theSession, part, bodies):
|
||||
"""
|
||||
Extract mass properties using MeasureManager.
|
||||
|
||||
API Reference: https://www.nxjournaling.com/content/mass-properties-using-python
|
||||
|
||||
Returns dict with mass, volume, area, center of gravity.
|
||||
"""
|
||||
results = {
|
||||
'mass_kg': 0.0,
|
||||
'volume_mm3': 0.0,
|
||||
'surface_area_mm2': 0.0,
|
||||
'center_of_gravity_mm': [0.0, 0.0, 0.0],
|
||||
'moments_of_inertia': None,
|
||||
'num_bodies': len(bodies)
|
||||
}
|
||||
|
||||
if not bodies:
|
||||
print("[JOURNAL] No solid bodies found in part")
|
||||
return results
|
||||
|
||||
try:
|
||||
# Get the measure manager
|
||||
measureManager = part.MeasureManager
|
||||
|
||||
# Convert bodies list to array for NX API
|
||||
bodyArray = bodies if isinstance(bodies, list) else list(bodies)
|
||||
|
||||
# Get unit collection and build mass_units array
|
||||
# API requires: [Area, Volume, Mass, Length] base units
|
||||
uc = part.UnitCollection
|
||||
mass_units = [
|
||||
uc.GetBase("Area"),
|
||||
uc.GetBase("Volume"),
|
||||
uc.GetBase("Mass"),
|
||||
uc.GetBase("Length")
|
||||
]
|
||||
|
||||
# Create mass properties measurement
|
||||
# Signature: NewMassProperties(mass_units, accuracy, objects)
|
||||
measureBodies = measureManager.NewMassProperties(mass_units, 0.99, bodyArray)
|
||||
print("[JOURNAL] Using NewMassProperties(mass_units, accuracy, bodies) API")
|
||||
|
||||
# Get the results
|
||||
if measureBodies:
|
||||
# Mass
|
||||
try:
|
||||
results['mass_kg'] = measureBodies.Mass
|
||||
print(f"[JOURNAL] Raw mass value: {measureBodies.Mass}")
|
||||
except AttributeError as e:
|
||||
print(f"[JOURNAL] Mass attribute error: {e}")
|
||||
|
||||
# Volume
|
||||
try:
|
||||
results['volume_mm3'] = measureBodies.Volume
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
# Surface area
|
||||
try:
|
||||
results['surface_area_mm2'] = measureBodies.Area
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
# Center of gravity
|
||||
try:
|
||||
cog = measureBodies.Centroid
|
||||
if cog:
|
||||
results['center_of_gravity_mm'] = [cog.X, cog.Y, cog.Z]
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
# Moments of inertia - NewMassProperties doesn't provide inertia tensors
|
||||
# Would need different API for that
|
||||
results['moments_of_inertia'] = None
|
||||
|
||||
# Dispose
|
||||
try:
|
||||
measureBodies.Dispose()
|
||||
except:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
print(f"[JOURNAL] Error in mass properties extraction: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def main(args):
|
||||
"""
|
||||
Main entry point for NX journal.
|
||||
|
||||
Args:
|
||||
args[0]: .prt file path
|
||||
args[1]: output directory (optional, defaults to prt directory)
|
||||
"""
|
||||
if len(args) < 1:
|
||||
print("ERROR: No .prt file path provided")
|
||||
print("Usage: run_journal.exe extract_part_mass_material.py <prt_file> [output_dir]")
|
||||
return False
|
||||
|
||||
prt_file_path = args[0]
|
||||
output_dir = args[1] if len(args) > 1 else os.path.dirname(prt_file_path)
|
||||
|
||||
prt_filename = os.path.basename(prt_file_path)
|
||||
|
||||
print(f"[JOURNAL] " + "="*60)
|
||||
print(f"[JOURNAL] NX PART MASS & MATERIAL EXTRACTOR")
|
||||
print(f"[JOURNAL] " + "="*60)
|
||||
print(f"[JOURNAL] Part file: {prt_filename}")
|
||||
print(f"[JOURNAL] Output dir: {output_dir}")
|
||||
|
||||
results = {
|
||||
'part_file': prt_filename,
|
||||
'part_path': prt_file_path,
|
||||
'mass_kg': 0.0,
|
||||
'mass_g': 0.0,
|
||||
'volume_mm3': 0.0,
|
||||
'surface_area_mm2': 0.0,
|
||||
'center_of_gravity_mm': [0.0, 0.0, 0.0],
|
||||
'moments_of_inertia': None,
|
||||
'material': {
|
||||
'name': None,
|
||||
'density': None,
|
||||
'density_unit': 'kg/mm^3'
|
||||
},
|
||||
'num_bodies': 0,
|
||||
'success': False,
|
||||
'error': None
|
||||
}
|
||||
|
||||
try:
|
||||
theSession = NXOpen.Session.GetSession()
|
||||
|
||||
# Set load options
|
||||
working_dir = os.path.dirname(prt_file_path)
|
||||
theSession.Parts.LoadOptions.ComponentLoadMethod = NXOpen.LoadOptions.LoadMethod.FromDirectory
|
||||
theSession.Parts.LoadOptions.SetSearchDirectories([working_dir], [True])
|
||||
|
||||
# Open the part file
|
||||
print(f"[JOURNAL] Opening part file...")
|
||||
basePart, partLoadStatus = theSession.Parts.OpenActiveDisplay(
|
||||
prt_file_path,
|
||||
NXOpen.DisplayPartOption.AllowAdditional
|
||||
)
|
||||
partLoadStatus.Dispose()
|
||||
|
||||
workPart = theSession.Parts.Work
|
||||
print(f"[JOURNAL] Loaded part: {workPart.Name}")
|
||||
|
||||
# Get all solid bodies
|
||||
bodies = get_all_solid_bodies(workPart)
|
||||
print(f"[JOURNAL] Found {len(bodies)} solid bodies")
|
||||
|
||||
if bodies:
|
||||
# Extract mass properties
|
||||
mass_props = extract_mass_properties(theSession, workPart, bodies)
|
||||
results.update(mass_props)
|
||||
results['mass_g'] = results['mass_kg'] * 1000.0
|
||||
|
||||
# Get material from first body (typically all bodies have same material)
|
||||
material_info = get_material_info(workPart, bodies[0])
|
||||
results['material'] = material_info
|
||||
|
||||
results['success'] = True
|
||||
|
||||
print(f"[JOURNAL] ")
|
||||
print(f"[JOURNAL] RESULTS:")
|
||||
print(f"[JOURNAL] Mass: {results['mass_kg']:.6f} kg ({results['mass_g']:.2f} g)")
|
||||
print(f"[JOURNAL] Volume: {results['volume_mm3']:.2f} mm^3")
|
||||
print(f"[JOURNAL] Surface Area: {results['surface_area_mm2']:.2f} mm^2")
|
||||
print(f"[JOURNAL] CoG: [{results['center_of_gravity_mm'][0]:.2f}, {results['center_of_gravity_mm'][1]:.2f}, {results['center_of_gravity_mm'][2]:.2f}] mm")
|
||||
if material_info['name']:
|
||||
print(f"[JOURNAL] Material: {material_info['name']}")
|
||||
if material_info['density']:
|
||||
print(f"[JOURNAL] Density: {material_info['density']} {material_info['density_unit']}")
|
||||
else:
|
||||
results['error'] = "No solid bodies found in part"
|
||||
print(f"[JOURNAL] ERROR: No solid bodies found")
|
||||
|
||||
# Write results to JSON file
|
||||
output_file = os.path.join(output_dir, "_temp_part_properties.json")
|
||||
with open(output_file, 'w') as f:
|
||||
json.dump(results, f, indent=2)
|
||||
print(f"[JOURNAL] Results written to: {output_file}")
|
||||
|
||||
# Also write simple mass value for backward compatibility
|
||||
mass_file = os.path.join(output_dir, "_temp_mass.txt")
|
||||
with open(mass_file, 'w') as f:
|
||||
f.write(str(results['mass_kg']))
|
||||
print(f"[JOURNAL] Mass written to: {mass_file}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
results['error'] = str(e)
|
||||
results['success'] = False
|
||||
print(f"[JOURNAL] FATAL ERROR: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# Still write results file even on error
|
||||
output_file = os.path.join(output_dir, "_temp_part_properties.json")
|
||||
try:
|
||||
with open(output_file, 'w') as f:
|
||||
json.dump(results, f, indent=2)
|
||||
except:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv[1:])
|
||||
@@ -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)
|
||||
@@ -77,7 +77,7 @@
|
||||
"name": "whiffle_min",
|
||||
"expression_name": "whiffle_min",
|
||||
"min": 30.0,
|
||||
"max": 55.0,
|
||||
"max": 60.0,
|
||||
"baseline": 40.55,
|
||||
"units": "mm",
|
||||
"enabled": true
|
||||
@@ -95,7 +95,7 @@
|
||||
"name": "whiffle_triangle_closeness",
|
||||
"expression_name": "whiffle_triangle_closeness",
|
||||
"min": 50.0,
|
||||
"max": 65.0,
|
||||
"max": 80.0,
|
||||
"baseline": 60.00,
|
||||
"units": "mm",
|
||||
"enabled": true
|
||||
@@ -104,7 +104,7 @@
|
||||
"name": "blank_backface_angle",
|
||||
"expression_name": "blank_backface_angle",
|
||||
"min": 4.1,
|
||||
"max": 4.2,
|
||||
"max": 4.5,
|
||||
"baseline": 4.15,
|
||||
"units": "degrees",
|
||||
"enabled": true
|
||||
|
||||
Reference in New Issue
Block a user