Documentation: - Add docs/06_PHYSICS/ with Zernike fundamentals and OPD method docs - Add docs/guides/CMA-ES_EXPLAINED.md optimization guide - Update CLAUDE.md and ATOMIZER_CONTEXT.md with current architecture - Update OP_01_CREATE_STUDY protocol Planning: - Add DYNAMIC_RESPONSE plans for random vibration/PSD support - Add OPTIMIZATION_ENGINE_MIGRATION_PLAN for code reorganization Insights System: - Update design_space, modal_analysis, stress_field, thermal_field insights - Improve error handling and data validation NX Journals: - Add analyze_wfe_zernike.py for Zernike WFE analysis - Add capture_study_images.py for automated screenshots - Add extract_expressions.py and introspect_part.py utilities - Add user_generated_journals/journal_top_view_image_taking.py Tests & Tools: - Add comprehensive Zernike OPD test suite - Add audit_v10 tests for WFE validation - Add tools for Pareto graphs and mirror data extraction - Add migrate_studies_to_topics.py utility Knowledge Base: - Initialize LAC (Learning Atomizer Core) with failure/success patterns Dashboard: - Update Setup.tsx and launch_dashboard.py - Add restart-dev.bat helper script 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
621 lines
20 KiB
Python
621 lines
20 KiB
Python
"""
|
|
NX Journal: Comprehensive Part Introspection Tool
|
|
===================================================
|
|
|
|
This journal performs deep introspection of an NX .prt file and extracts:
|
|
- All expressions (user and internal, with values, units, formulas)
|
|
- Mass properties (mass, volume, surface area, center of gravity)
|
|
- Material properties (name, density, all material attributes)
|
|
- Body information (solid bodies, sheet bodies, body attributes)
|
|
- Part attributes (all user-defined attributes)
|
|
- Groups (all groups and their members)
|
|
- Features (all features in the part)
|
|
- References (linked parts, assembly components)
|
|
- Datum planes, coordinate systems
|
|
- Units system
|
|
|
|
Usage:
|
|
run_journal.exe introspect_part.py <prt_file_path> [output_dir]
|
|
|
|
Output:
|
|
_temp_introspection.json - Comprehensive JSON with all extracted data
|
|
|
|
Author: Atomizer
|
|
Created: 2025-12-19
|
|
Version: 1.0
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import json
|
|
import NXOpen
|
|
import NXOpen.UF
|
|
|
|
|
|
def get_expressions(part):
|
|
"""Extract all expressions from the part."""
|
|
expressions = {
|
|
'user': [],
|
|
'internal': [],
|
|
'total_count': 0,
|
|
'user_count': 0
|
|
}
|
|
|
|
try:
|
|
for expr in part.Expressions:
|
|
try:
|
|
expr_data = {
|
|
'name': expr.Name,
|
|
'value': expr.Value,
|
|
'rhs': expr.RightHandSide if hasattr(expr, 'RightHandSide') else None,
|
|
'units': expr.Units.Name if expr.Units else None,
|
|
'type': str(expr.Type) if hasattr(expr, 'Type') else 'Unknown',
|
|
}
|
|
|
|
# Determine if internal (p0, p1, p123, etc.)
|
|
name = expr.Name
|
|
is_internal = False
|
|
if name.startswith('p') and len(name) > 1:
|
|
rest = name[1:].replace('.', '').replace('_', '')
|
|
if rest.isdigit():
|
|
is_internal = True
|
|
|
|
if is_internal:
|
|
expressions['internal'].append(expr_data)
|
|
else:
|
|
expressions['user'].append(expr_data)
|
|
|
|
except Exception as e:
|
|
pass
|
|
|
|
expressions['total_count'] = len(expressions['user']) + len(expressions['internal'])
|
|
expressions['user_count'] = len(expressions['user'])
|
|
|
|
except Exception as e:
|
|
expressions['error'] = str(e)
|
|
|
|
return expressions
|
|
|
|
|
|
def get_all_solid_bodies(part):
|
|
"""Get all solid bodies from the part."""
|
|
bodies = []
|
|
try:
|
|
for body in part.Bodies:
|
|
if body.IsSolidBody:
|
|
bodies.append(body)
|
|
except Exception as e:
|
|
pass
|
|
return bodies
|
|
|
|
|
|
def get_mass_properties(part, bodies):
|
|
"""Extract mass properties using MeasureManager."""
|
|
results = {
|
|
'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],
|
|
'num_bodies': len(bodies),
|
|
'success': False
|
|
}
|
|
|
|
if not bodies:
|
|
return results
|
|
|
|
try:
|
|
measureManager = part.MeasureManager
|
|
bodyArray = list(bodies)
|
|
|
|
# Build mass_units array
|
|
uc = part.UnitCollection
|
|
mass_units = [
|
|
uc.GetBase("Area"),
|
|
uc.GetBase("Volume"),
|
|
uc.GetBase("Mass"),
|
|
uc.GetBase("Length")
|
|
]
|
|
|
|
measureBodies = measureManager.NewMassProperties(mass_units, 0.99, bodyArray)
|
|
|
|
if measureBodies:
|
|
try:
|
|
results['mass_kg'] = measureBodies.Mass
|
|
results['mass_g'] = results['mass_kg'] * 1000.0
|
|
except:
|
|
pass
|
|
|
|
try:
|
|
results['volume_mm3'] = measureBodies.Volume
|
|
except:
|
|
pass
|
|
|
|
try:
|
|
results['surface_area_mm2'] = measureBodies.Area
|
|
except:
|
|
pass
|
|
|
|
try:
|
|
cog = measureBodies.Centroid
|
|
if cog:
|
|
results['center_of_gravity_mm'] = [cog.X, cog.Y, cog.Z]
|
|
except:
|
|
pass
|
|
|
|
results['success'] = True
|
|
|
|
try:
|
|
measureBodies.Dispose()
|
|
except:
|
|
pass
|
|
|
|
except Exception as e:
|
|
results['error'] = str(e)
|
|
|
|
return results
|
|
|
|
|
|
def get_materials(part, bodies):
|
|
"""Extract all materials from the part."""
|
|
materials = {
|
|
'assigned': [],
|
|
'available': [],
|
|
'library': []
|
|
}
|
|
|
|
# Get materials assigned to bodies
|
|
for body in bodies:
|
|
try:
|
|
phys_mat = body.GetPhysicalMaterial()
|
|
if phys_mat:
|
|
mat_info = {
|
|
'name': phys_mat.Name,
|
|
'body': body.Name if hasattr(body, 'Name') else 'Unknown',
|
|
'properties': {}
|
|
}
|
|
|
|
# Try to get common material properties
|
|
prop_names = ['Density', 'YoungModulus', 'PoissonRatio',
|
|
'ThermalExpansionCoefficient', 'ThermalConductivity',
|
|
'SpecificHeat', 'YieldStrength', 'UltimateStrength']
|
|
for prop_name in prop_names:
|
|
try:
|
|
val = phys_mat.GetPropertyValue(prop_name)
|
|
if val is not None:
|
|
mat_info['properties'][prop_name] = float(val)
|
|
except:
|
|
pass
|
|
|
|
materials['assigned'].append(mat_info)
|
|
except:
|
|
pass
|
|
|
|
# Get all materials in part via PhysicalMaterialManager
|
|
try:
|
|
pmm = part.PhysicalMaterialManager
|
|
if pmm:
|
|
all_mats = pmm.GetAllPhysicalMaterials()
|
|
for mat in all_mats:
|
|
try:
|
|
mat_info = {
|
|
'name': mat.Name,
|
|
'properties': {}
|
|
}
|
|
prop_names = ['Density', 'YoungModulus', 'PoissonRatio']
|
|
for prop_name in prop_names:
|
|
try:
|
|
val = mat.GetPropertyValue(prop_name)
|
|
if val is not None:
|
|
mat_info['properties'][prop_name] = float(val)
|
|
except:
|
|
pass
|
|
materials['available'].append(mat_info)
|
|
except:
|
|
pass
|
|
except Exception as e:
|
|
materials['pmm_error'] = str(e)
|
|
|
|
return materials
|
|
|
|
|
|
def get_body_info(part):
|
|
"""Get detailed body information."""
|
|
body_info = {
|
|
'solid_bodies': [],
|
|
'sheet_bodies': [],
|
|
'counts': {
|
|
'solid': 0,
|
|
'sheet': 0,
|
|
'total': 0
|
|
}
|
|
}
|
|
|
|
try:
|
|
for body in part.Bodies:
|
|
body_data = {
|
|
'name': body.Name if hasattr(body, 'Name') else 'Unknown',
|
|
'is_solid': body.IsSolidBody,
|
|
'is_sheet': body.IsSheetBody if hasattr(body, 'IsSheetBody') else False,
|
|
'attributes': []
|
|
}
|
|
|
|
# Get body attributes
|
|
try:
|
|
attrs = body.GetUserAttributes()
|
|
for attr in attrs:
|
|
try:
|
|
body_data['attributes'].append({
|
|
'title': attr.Title,
|
|
'type': str(attr.Type),
|
|
'value': attr.StringValue if hasattr(attr, 'StringValue') else str(attr.Value)
|
|
})
|
|
except:
|
|
pass
|
|
except:
|
|
pass
|
|
|
|
if body.IsSolidBody:
|
|
body_info['solid_bodies'].append(body_data)
|
|
body_info['counts']['solid'] += 1
|
|
else:
|
|
body_info['sheet_bodies'].append(body_data)
|
|
body_info['counts']['sheet'] += 1
|
|
|
|
body_info['counts']['total'] = body_info['counts']['solid'] + body_info['counts']['sheet']
|
|
|
|
except Exception as e:
|
|
body_info['error'] = str(e)
|
|
|
|
return body_info
|
|
|
|
|
|
def get_part_attributes(part):
|
|
"""Get all part-level attributes."""
|
|
attributes = []
|
|
|
|
try:
|
|
attrs = part.GetUserAttributes()
|
|
for attr in attrs:
|
|
try:
|
|
attr_data = {
|
|
'title': attr.Title,
|
|
'type': str(attr.Type),
|
|
}
|
|
|
|
# Get value based on type
|
|
try:
|
|
if hasattr(attr, 'StringValue'):
|
|
attr_data['value'] = attr.StringValue
|
|
elif hasattr(attr, 'Value'):
|
|
attr_data['value'] = attr.Value
|
|
elif hasattr(attr, 'IntegerValue'):
|
|
attr_data['value'] = attr.IntegerValue
|
|
elif hasattr(attr, 'RealValue'):
|
|
attr_data['value'] = attr.RealValue
|
|
except:
|
|
attr_data['value'] = 'Unknown'
|
|
|
|
attributes.append(attr_data)
|
|
except:
|
|
pass
|
|
|
|
except Exception as e:
|
|
pass
|
|
|
|
return attributes
|
|
|
|
|
|
def get_groups(part):
|
|
"""Get all groups in the part."""
|
|
groups = []
|
|
|
|
try:
|
|
# NX stores groups in a collection
|
|
if hasattr(part, 'Groups'):
|
|
for group in part.Groups:
|
|
try:
|
|
group_data = {
|
|
'name': group.Name if hasattr(group, 'Name') else 'Unknown',
|
|
'member_count': 0,
|
|
'members': []
|
|
}
|
|
|
|
# Try to get group members
|
|
try:
|
|
members = group.GetMembers()
|
|
group_data['member_count'] = len(members) if members else 0
|
|
for member in members[:10]: # Limit to first 10 for readability
|
|
try:
|
|
group_data['members'].append(str(type(member).__name__))
|
|
except:
|
|
pass
|
|
except:
|
|
pass
|
|
|
|
groups.append(group_data)
|
|
except:
|
|
pass
|
|
except Exception as e:
|
|
pass
|
|
|
|
return groups
|
|
|
|
|
|
def get_features(part):
|
|
"""Get summary of features in the part."""
|
|
features = {
|
|
'total_count': 0,
|
|
'by_type': {},
|
|
'first_10': []
|
|
}
|
|
|
|
try:
|
|
count = 0
|
|
for feature in part.Features:
|
|
try:
|
|
feat_type = str(type(feature).__name__)
|
|
|
|
# Count by type
|
|
if feat_type in features['by_type']:
|
|
features['by_type'][feat_type] += 1
|
|
else:
|
|
features['by_type'][feat_type] = 1
|
|
|
|
# Store first 10 for reference
|
|
if count < 10:
|
|
features['first_10'].append({
|
|
'name': feature.Name if hasattr(feature, 'Name') else 'Unknown',
|
|
'type': feat_type
|
|
})
|
|
|
|
count += 1
|
|
except:
|
|
pass
|
|
|
|
features['total_count'] = count
|
|
|
|
except Exception as e:
|
|
features['error'] = str(e)
|
|
|
|
return features
|
|
|
|
|
|
def get_datums(part):
|
|
"""Get datum planes and coordinate systems."""
|
|
datums = {
|
|
'planes': [],
|
|
'csys': [],
|
|
'axes': []
|
|
}
|
|
|
|
try:
|
|
# Datum planes
|
|
if hasattr(part, 'Datums'):
|
|
for datum in part.Datums:
|
|
try:
|
|
datum_type = str(type(datum).__name__)
|
|
datum_name = datum.Name if hasattr(datum, 'Name') else 'Unknown'
|
|
|
|
if 'Plane' in datum_type:
|
|
datums['planes'].append(datum_name)
|
|
elif 'Csys' in datum_type or 'Coordinate' in datum_type:
|
|
datums['csys'].append(datum_name)
|
|
elif 'Axis' in datum_type:
|
|
datums['axes'].append(datum_name)
|
|
except:
|
|
pass
|
|
except Exception as e:
|
|
datums['error'] = str(e)
|
|
|
|
return datums
|
|
|
|
|
|
def get_units_info(part):
|
|
"""Get unit system information."""
|
|
units_info = {
|
|
'base_units': {},
|
|
'system': 'Unknown'
|
|
}
|
|
|
|
try:
|
|
uc = part.UnitCollection
|
|
|
|
# Get common base units
|
|
unit_types = ['Length', 'Mass', 'Time', 'Temperature', 'Angle',
|
|
'Area', 'Volume', 'Force', 'Pressure', 'Density']
|
|
for unit_type in unit_types:
|
|
try:
|
|
base_unit = uc.GetBase(unit_type)
|
|
if base_unit:
|
|
units_info['base_units'][unit_type] = base_unit.Name
|
|
except:
|
|
pass
|
|
|
|
# Determine system from length unit
|
|
if 'Length' in units_info['base_units']:
|
|
length_unit = units_info['base_units']['Length'].lower()
|
|
if 'mm' in length_unit or 'millimeter' in length_unit:
|
|
units_info['system'] = 'Metric (mm)'
|
|
elif 'meter' in length_unit and 'milli' not in length_unit:
|
|
units_info['system'] = 'Metric (m)'
|
|
elif 'inch' in length_unit or 'in' in length_unit:
|
|
units_info['system'] = 'Imperial (inch)'
|
|
|
|
except Exception as e:
|
|
units_info['error'] = str(e)
|
|
|
|
return units_info
|
|
|
|
|
|
def get_linked_parts(theSession, working_dir):
|
|
"""Get information about linked/associated parts."""
|
|
linked_parts = {
|
|
'loaded_parts': [],
|
|
'fem_parts': [],
|
|
'sim_parts': [],
|
|
'idealized_parts': []
|
|
}
|
|
|
|
try:
|
|
for part in theSession.Parts:
|
|
try:
|
|
part_name = part.Name if hasattr(part, 'Name') else str(part)
|
|
part_path = part.FullPath if hasattr(part, 'FullPath') else 'Unknown'
|
|
|
|
part_info = {
|
|
'name': part_name,
|
|
'path': part_path,
|
|
'leaf_name': part.Leaf if hasattr(part, 'Leaf') else part_name
|
|
}
|
|
|
|
name_lower = part_name.lower()
|
|
if '_sim' in name_lower or name_lower.endswith('.sim'):
|
|
linked_parts['sim_parts'].append(part_info)
|
|
elif '_fem' in name_lower or name_lower.endswith('.fem'):
|
|
if '_i.prt' in name_lower or '_i' in name_lower:
|
|
linked_parts['idealized_parts'].append(part_info)
|
|
else:
|
|
linked_parts['fem_parts'].append(part_info)
|
|
elif '_i.prt' in name_lower:
|
|
linked_parts['idealized_parts'].append(part_info)
|
|
else:
|
|
linked_parts['loaded_parts'].append(part_info)
|
|
|
|
except:
|
|
pass
|
|
|
|
except Exception as e:
|
|
linked_parts['error'] = str(e)
|
|
|
|
return linked_parts
|
|
|
|
|
|
def main(args):
|
|
"""Main entry point for NX journal."""
|
|
|
|
if len(args) < 1:
|
|
print("ERROR: No .prt file path provided")
|
|
print("Usage: run_journal.exe introspect_part.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"[INTROSPECT] " + "="*60)
|
|
print(f"[INTROSPECT] NX COMPREHENSIVE PART INTROSPECTION")
|
|
print(f"[INTROSPECT] " + "="*60)
|
|
print(f"[INTROSPECT] Part: {prt_filename}")
|
|
print(f"[INTROSPECT] Output: {output_dir}")
|
|
|
|
results = {
|
|
'part_file': prt_filename,
|
|
'part_path': prt_file_path,
|
|
'success': False,
|
|
'error': None,
|
|
'expressions': {},
|
|
'mass_properties': {},
|
|
'materials': {},
|
|
'bodies': {},
|
|
'attributes': [],
|
|
'groups': [],
|
|
'features': {},
|
|
'datums': {},
|
|
'units': {},
|
|
'linked_parts': {}
|
|
}
|
|
|
|
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"[INTROSPECT] Opening part file...")
|
|
basePart, partLoadStatus = theSession.Parts.OpenActiveDisplay(
|
|
prt_file_path,
|
|
NXOpen.DisplayPartOption.AllowAdditional
|
|
)
|
|
partLoadStatus.Dispose()
|
|
|
|
workPart = theSession.Parts.Work
|
|
print(f"[INTROSPECT] Loaded: {workPart.Name}")
|
|
|
|
# Extract all data
|
|
print(f"[INTROSPECT] Extracting expressions...")
|
|
results['expressions'] = get_expressions(workPart)
|
|
print(f"[INTROSPECT] Found {results['expressions']['user_count']} user expressions")
|
|
|
|
print(f"[INTROSPECT] Extracting body info...")
|
|
results['bodies'] = get_body_info(workPart)
|
|
print(f"[INTROSPECT] Found {results['bodies']['counts']['solid']} solid bodies")
|
|
|
|
print(f"[INTROSPECT] Extracting mass properties...")
|
|
bodies = get_all_solid_bodies(workPart)
|
|
results['mass_properties'] = get_mass_properties(workPart, bodies)
|
|
print(f"[INTROSPECT] Mass: {results['mass_properties']['mass_kg']:.4f} kg")
|
|
|
|
print(f"[INTROSPECT] Extracting materials...")
|
|
results['materials'] = get_materials(workPart, bodies)
|
|
print(f"[INTROSPECT] Found {len(results['materials']['assigned'])} assigned materials")
|
|
|
|
print(f"[INTROSPECT] Extracting attributes...")
|
|
results['attributes'] = get_part_attributes(workPart)
|
|
print(f"[INTROSPECT] Found {len(results['attributes'])} part attributes")
|
|
|
|
print(f"[INTROSPECT] Extracting groups...")
|
|
results['groups'] = get_groups(workPart)
|
|
print(f"[INTROSPECT] Found {len(results['groups'])} groups")
|
|
|
|
print(f"[INTROSPECT] Extracting features...")
|
|
results['features'] = get_features(workPart)
|
|
print(f"[INTROSPECT] Found {results['features']['total_count']} features")
|
|
|
|
print(f"[INTROSPECT] Extracting datums...")
|
|
results['datums'] = get_datums(workPart)
|
|
print(f"[INTROSPECT] Found {len(results['datums']['planes'])} datum planes")
|
|
|
|
print(f"[INTROSPECT] Extracting units...")
|
|
results['units'] = get_units_info(workPart)
|
|
print(f"[INTROSPECT] System: {results['units']['system']}")
|
|
|
|
print(f"[INTROSPECT] Extracting linked parts...")
|
|
results['linked_parts'] = get_linked_parts(theSession, working_dir)
|
|
print(f"[INTROSPECT] Found {len(results['linked_parts']['loaded_parts'])} loaded parts")
|
|
|
|
results['success'] = True
|
|
print(f"[INTROSPECT] ")
|
|
print(f"[INTROSPECT] INTROSPECTION COMPLETE!")
|
|
print(f"[INTROSPECT] " + "="*60)
|
|
|
|
# Summary
|
|
print(f"[INTROSPECT] SUMMARY:")
|
|
print(f"[INTROSPECT] Expressions: {results['expressions']['user_count']} user, {len(results['expressions']['internal'])} internal")
|
|
print(f"[INTROSPECT] Mass: {results['mass_properties']['mass_kg']:.4f} kg ({results['mass_properties']['mass_g']:.2f} g)")
|
|
print(f"[INTROSPECT] Bodies: {results['bodies']['counts']['solid']} solid, {results['bodies']['counts']['sheet']} sheet")
|
|
print(f"[INTROSPECT] Features: {results['features']['total_count']}")
|
|
print(f"[INTROSPECT] Materials: {len(results['materials']['assigned'])} assigned")
|
|
|
|
except Exception as e:
|
|
results['error'] = str(e)
|
|
results['success'] = False
|
|
print(f"[INTROSPECT] FATAL ERROR: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
|
|
# Write results
|
|
output_file = os.path.join(output_dir, "_temp_introspection.json")
|
|
with open(output_file, 'w') as f:
|
|
json.dump(results, f, indent=2)
|
|
print(f"[INTROSPECT] Results written to: {output_file}")
|
|
|
|
return results['success']
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main(sys.argv[1:])
|