feat: Major update - Physics docs, Zernike OPD, insights, NX journals, tools

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>
This commit is contained in:
2025-12-23 19:47:37 -05:00
parent e448142599
commit f13563d7ab
43 changed files with 8098 additions and 8 deletions

View File

@@ -0,0 +1,233 @@
#!/usr/bin/env python3
"""
Atomizer Zernike WFE Analyzer
=============================
Analyze Zernike wavefront error from NX Nastran OP2 results.
IMPORTANT: This script requires numpy/scipy. Run from command line with
the atomizer conda environment, NOT from within NX.
Usage:
conda activate atomizer
python analyze_wfe_zernike.py "path/to/solution.op2"
# Or without argument - searches current directory for OP2 files:
python analyze_wfe_zernike.py
Output:
- Zernike coefficients for each subcase
- Relative WFE metrics (filtered RMS)
- Manufacturing workload (J1-J3 filtered)
- Weighted sum calculation
Author: Atomizer
Created: 2025-12-18
"""
import sys
import os
from pathlib import Path
def log(msg):
"""Print to console."""
print(msg)
def find_op2_file(working_dir=None):
"""Find the most recent OP2 file in the working directory."""
if working_dir is None:
working_dir = Path.cwd()
else:
working_dir = Path(working_dir)
# Look for OP2 files
op2_files = list(working_dir.glob("*solution*.op2")) + list(working_dir.glob("*.op2"))
if not op2_files:
# Check subdirectories
op2_files = list(working_dir.glob("**/*solution*.op2"))
if not op2_files:
return None
# Return most recently modified
return max(op2_files, key=lambda p: p.stat().st_mtime)
def analyze_zernike(op2_path):
"""Run Zernike analysis on OP2 file."""
# Add Atomizer to path
atomizer_root = Path(__file__).parent.parent
if str(atomizer_root) not in sys.path:
sys.path.insert(0, str(atomizer_root))
try:
from optimization_engine.extractors import ZernikeExtractor
except ImportError as e:
log(f"ERROR: Could not import ZernikeExtractor: {e}")
log(f"Make sure Atomizer is properly installed.")
log(f"Atomizer root: {atomizer_root}")
return None
log("=" * 70)
log("ZERNIKE WAVEFRONT ERROR ANALYSIS")
log("=" * 70)
log(f"OP2 File: {op2_path.name}")
log(f"Directory: {op2_path.parent}")
log("")
# Create extractor
try:
extractor = ZernikeExtractor(
op2_path,
bdf_path=None,
displacement_unit='mm',
n_modes=50,
filter_orders=4
)
except Exception as e:
log(f"ERROR creating extractor: {e}")
return None
# Get available subcases from the extractor's displacement data
subcases = list(extractor.displacements.keys())
log(f"Available subcases: {subcases}")
log("")
# Standard subcase mapping for M1 mirror
subcase_labels = {
'1': '90 deg (Manufacturing/Polishing)',
'2': '20 deg (Reference)',
'3': '40 deg (Operational)',
'4': '60 deg (Operational)'
}
# Extract absolute Zernike for each subcase
log("-" * 70)
log("ABSOLUTE ZERNIKE ANALYSIS (per subcase)")
log("-" * 70)
results = {}
for sc in subcases:
try:
result = extractor.extract_subcase(sc)
results[sc] = result
label = subcase_labels.get(sc, f'Subcase {sc}')
log(f"\n{label}:")
log(f" Global RMS: {result['global_rms_nm']:.2f} nm")
log(f" Filtered RMS: {result['filtered_rms_nm']:.2f} nm (J4+ only)")
except Exception as e:
log(f" ERROR extracting subcase {sc}: {e}")
# Relative analysis (using subcase 2 as reference)
ref_subcase = '2'
if ref_subcase in subcases:
log("")
log("-" * 70)
log(f"RELATIVE ANALYSIS (vs {subcase_labels.get(ref_subcase, ref_subcase)})")
log("-" * 70)
relative_results = {}
for sc in subcases:
if sc == ref_subcase:
continue
try:
rel = extractor.extract_relative(sc, ref_subcase)
relative_results[sc] = rel
label = subcase_labels.get(sc, f'Subcase {sc}')
log(f"\n{label} vs Reference:")
log(f" Relative Filtered RMS: {rel['relative_filtered_rms_nm']:.2f} nm")
if 'relative_rms_filter_j1to3' in rel:
log(f" J1-J3 Filtered RMS: {rel['relative_rms_filter_j1to3']:.2f} nm")
except Exception as e:
log(f" ERROR: {e}")
# Calculate weighted sum (M1 mirror optimization objectives)
log("")
log("-" * 70)
log("OPTIMIZATION OBJECTIVES")
log("-" * 70)
obj_40_20 = relative_results.get('3', {}).get('relative_filtered_rms_nm', 0)
obj_60_20 = relative_results.get('4', {}).get('relative_filtered_rms_nm', 0)
obj_mfg = relative_results.get('1', {}).get('relative_rms_filter_j1to3', 0)
log(f"\n 40-20 Filtered RMS: {obj_40_20:.2f} nm")
log(f" 60-20 Filtered RMS: {obj_60_20:.2f} nm")
log(f" MFG 90 (J1-J3): {obj_mfg:.2f} nm")
# Weighted sums for different weight configurations
log("")
log("Weighted Sum Calculations:")
# V4 weights: 5*40 + 5*60 + 2*mfg + mass
ws_v4 = 5*obj_40_20 + 5*obj_60_20 + 2*obj_mfg
log(f" V4 weights (5/5/2): {ws_v4:.2f} (+ mass)")
# V5 weights: 5*40 + 5*60 + 3*mfg + mass
ws_v5 = 5*obj_40_20 + 5*obj_60_20 + 3*obj_mfg
log(f" V5 weights (5/5/3): {ws_v5:.2f} (+ mass)")
return {
'absolute': results,
'relative': relative_results,
'objectives': {
'40_20': obj_40_20,
'60_20': obj_60_20,
'mfg_90': obj_mfg,
'ws_v4': ws_v4,
'ws_v5': ws_v5
}
}
return {'absolute': results}
def main(args):
"""Main entry point."""
log("")
log("=" * 70)
log(" ATOMIZER ZERNIKE WFE ANALYZER")
log("=" * 70)
log("")
# Determine OP2 file
op2_path = None
if args and len(args) > 0 and args[0]:
# OP2 path provided as argument
op2_path = Path(args[0])
if not op2_path.exists():
log(f"ERROR: OP2 file not found: {op2_path}")
return
else:
# Try to find OP2 in current directory
log("No OP2 file specified, searching...")
op2_path = find_op2_file()
if op2_path is None:
log("ERROR: No OP2 file found in current directory.")
log("Usage: Run after solving, or provide OP2 path as argument.")
return
log(f"Found: {op2_path}")
# Run analysis
results = analyze_zernike(op2_path)
if results:
log("")
log("=" * 70)
log("ANALYSIS COMPLETE")
log("=" * 70)
else:
log("")
log("Analysis failed. Check errors above.")
if __name__ == '__main__':
# Get arguments (works both in NX and command line)
if len(sys.argv) > 1:
main(sys.argv[1:])
else:
main([])

View File

@@ -0,0 +1,184 @@
# NX Journal: Capture Study Images for Atomizer Documentation
#
# Purpose: Capture top view and isometric view images of a part for study documentation
# Usage: run_journal.exe capture_study_images.py -args "part_file_path" "output_directory" ["prefix"]
#
# Arguments:
# part_file_path: Full path to the .prt file to capture
# output_directory: Directory where images will be saved
# prefix (optional): Prefix for image filenames (default: part name)
#
# Output:
# {prefix}_Top.png - Top view image
# {prefix}_iso.png - Isometric view image
#
# Author: Atomizer
# Created: 2025-12-18
import sys
import os
import math
import NXOpen
import NXOpen.Gateway
def capture_images(part_path: str, output_dir: str, prefix: str = None):
"""
Capture top view and isometric view images of a part.
Args:
part_path: Full path to the .prt file
output_dir: Directory to save images
prefix: Optional prefix for image filenames
"""
theSession = NXOpen.Session.GetSession()
# Open the part if not already open
try:
workPart, loadStatus = theSession.Parts.OpenDisplay(part_path, NXOpen.Part.LoadStatically)
loadStatus.Dispose()
except:
workPart = theSession.Parts.Work
if workPart is None:
print(f"ERROR: Could not open part: {part_path}")
return False
# Determine prefix from part name if not provided
if prefix is None:
prefix = os.path.splitext(os.path.basename(part_path))[0]
# Ensure output directory exists
if not os.path.exists(output_dir):
os.makedirs(output_dir)
# Hide construction geometry for cleaner images
_hide_construction_geometry(theSession, workPart)
# Capture top view
top_image_path = os.path.join(output_dir, f"{prefix}_Top.png")
_capture_top_view(theSession, workPart, top_image_path)
print(f"Saved: {top_image_path}")
# Capture isometric view
iso_image_path = os.path.join(output_dir, f"{prefix}_iso.png")
_capture_isometric_view(theSession, workPart, iso_image_path)
print(f"Saved: {iso_image_path}")
return True
def _hide_construction_geometry(theSession, workPart):
"""Hide datums, curves, and sketches for cleaner visualization."""
markId = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Hide Construction")
# Hide datums
theSession.DisplayManager.HideByType("SHOW_HIDE_TYPE_DATUMS",
NXOpen.DisplayManager.ShowHideScope.AnyInAssembly)
# Hide curves
theSession.DisplayManager.HideByType("SHOW_HIDE_TYPE_CURVES",
NXOpen.DisplayManager.ShowHideScope.AnyInAssembly)
# Hide sketches
theSession.DisplayManager.HideByType("SHOW_HIDE_TYPE_SKETCHES",
NXOpen.DisplayManager.ShowHideScope.AnyInAssembly)
theSession.UpdateManager.DoUpdate(markId)
workPart.ModelingViews.WorkView.FitAfterShowOrHide(NXOpen.View.ShowOrHideType.HideOnly)
theSession.DeleteUndoMark(markId, None)
def _capture_top_view(theSession, workPart, output_path):
"""Capture top view (looking down Z-axis)."""
# Set top view orientation (looking down -Z)
matrix = NXOpen.Matrix3x3()
matrix.Xx = 0.0
matrix.Xy = -1.0
matrix.Xz = 0.0
matrix.Yx = -1.0
matrix.Yy = 0.0
matrix.Yz = 0.0
matrix.Zx = 0.0
matrix.Zy = 0.0
matrix.Zz = -1.0
workPart.ModelingViews.WorkView.Orient(matrix)
# Fit view
workPart.ModelingViews.WorkView.Fit()
# Export image
_export_image(workPart, output_path)
def _capture_isometric_view(theSession, workPart, output_path):
"""Capture isometric view (standard ISO angle showing backface)."""
# Set isometric orientation showing backface structure
rotMatrix = NXOpen.Matrix3x3()
rotMatrix.Xx = -0.32736574141345925
rotMatrix.Xy = -0.94489752125198745
rotMatrix.Xz = -0.00058794613984273266
rotMatrix.Yx = -0.71924452681462514
rotMatrix.Yy = 0.24959027079525001
rotMatrix.Yz = -0.64837643955618585
rotMatrix.Zx = 0.61279603621108569
rotMatrix.Zy = -0.21183335680718612
rotMatrix.Zz = -0.76131967460967154
# Get current scale and set orientation
translation = NXOpen.Point3d(0, 0, 0)
workPart.ModelingViews.WorkView.SetRotationTranslationScale(rotMatrix, translation, 0.25)
# Fit view
workPart.ModelingViews.WorkView.Fit()
# Export image
_export_image(workPart, output_path)
def _export_image(workPart, output_path, width=1200, height=1000):
"""Export current view as PNG image."""
imageExportBuilder = workPart.Views.CreateImageExportBuilder()
try:
# Configure export settings
imageExportBuilder.RegionMode = False # Use entire view
imageExportBuilder.DeviceWidth = width
imageExportBuilder.DeviceHeight = height
imageExportBuilder.FileFormat = NXOpen.Gateway.ImageExportBuilder.FileFormats.Png
imageExportBuilder.FileName = output_path
imageExportBuilder.BackgroundOption = NXOpen.Gateway.ImageExportBuilder.BackgroundOptions.Original
imageExportBuilder.EnhanceEdges = False
# Commit export
imageExportBuilder.Commit()
finally:
imageExportBuilder.Destroy()
def main(args):
"""Main entry point for journal."""
if len(args) < 2:
print("Usage: capture_study_images.py -args \"part_path\" \"output_dir\" [\"prefix\"]")
print(" part_path: Full path to .prt file")
print(" output_dir: Directory for output images")
print(" prefix: Optional filename prefix (default: part name)")
return
part_path = args[0]
output_dir = args[1]
prefix = args[2] if len(args) > 2 else None
print(f"Capturing images for: {part_path}")
print(f"Output directory: {output_dir}")
success = capture_images(part_path, output_dir, prefix)
if success:
print("Image capture complete!")
else:
print("Image capture failed!")
if __name__ == '__main__':
main(sys.argv[1:])

View File

@@ -0,0 +1,111 @@
"""
NX Journal Script to Extract All Expressions from a Part
Usage:
run_journal.exe extract_expressions.py <prt_file_path> [output_dir]
Output:
_temp_expressions.json with all expressions from the part
"""
import sys
import os
import json
import NXOpen
def main(args):
if len(args) < 1:
print("ERROR: No .prt file path provided")
return False
prt_file_path = args[0]
output_dir = args[1] if len(args) > 1 else os.path.dirname(prt_file_path)
print(f"[JOURNAL] Extracting expressions from: {os.path.basename(prt_file_path)}")
results = {
'part_file': os.path.basename(prt_file_path),
'part_path': prt_file_path,
'expressions': [],
'expression_count': 0,
'user_expression_count': 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}")
# Extract all expressions
print(f"[JOURNAL] Extracting expressions...")
for expr in workPart.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',
}
# Check if it's a user expression (not internal p0, p1, etc.)
is_internal = expr.Name.startswith('p') and len(expr.Name) > 1 and expr.Name[1:].replace('.', '').replace('_', '').isdigit()
expr_data['is_internal'] = is_internal
results['expressions'].append(expr_data)
if not is_internal:
results['user_expression_count'] += 1
except Exception as e:
print(f"[JOURNAL] Warning: Could not read expression: {e}")
results['expression_count'] = len(results['expressions'])
results['success'] = True
print(f"[JOURNAL] Found {results['expression_count']} total expressions")
print(f"[JOURNAL] Found {results['user_expression_count']} user expressions")
# Print user expressions
print(f"\n[JOURNAL] USER EXPRESSIONS:")
print(f"[JOURNAL] " + "=" * 50)
for expr in results['expressions']:
if not expr['is_internal']:
units_str = f" [{expr['units']}]" if expr['units'] else ""
print(f"[JOURNAL] {expr['name']}: {expr['value']}{units_str}")
except Exception as e:
results['error'] = str(e)
results['success'] = False
print(f"[JOURNAL] ERROR: {e}")
import traceback
traceback.print_exc()
# Write results
output_file = os.path.join(output_dir, "_temp_expressions.json")
with open(output_file, 'w') as f:
json.dump(results, f, indent=2)
print(f"\n[JOURNAL] Results written to: {output_file}")
return results['success']
if __name__ == '__main__':
main(sys.argv[1:])

View File

@@ -0,0 +1,96 @@
"""
Standalone expression extractor - opens part and extracts all expressions
Run with: ugraf.exe -run extract_expressions_standalone.py
"""
import NXOpen
import os
import json
def main():
session = NXOpen.Session.GetSession()
part_path = r"C:\Users\antoi\Atomizer\studies\m1_mirror_cost_reduction\1_setup\model\M1_Blank.prt"
output_json = r"C:\Users\antoi\Atomizer\_expressions_output.json"
output_txt = r"C:\Users\antoi\Atomizer\_expressions_output.txt"
results = {'expressions': [], 'success': False, 'part': part_path}
output_lines = []
try:
# Set load options
working_dir = os.path.dirname(part_path)
session.Parts.LoadOptions.ComponentLoadMethod = NXOpen.LoadOptions.LoadMethod.FromDirectory
session.Parts.LoadOptions.SetSearchDirectories([working_dir], [True])
# Open the part
output_lines.append(f"Opening: {part_path}")
basePart, loadStatus = session.Parts.OpenActiveDisplay(
part_path,
NXOpen.DisplayPartOption.AllowAdditional
)
loadStatus.Dispose()
workPart = session.Parts.Work
output_lines.append(f"Loaded: {workPart.Name}")
output_lines.append("")
output_lines.append("=" * 60)
output_lines.append("EXPRESSIONS IN M1_Blank.prt")
output_lines.append("=" * 60)
# Extract expressions
for expr in workPart.Expressions:
try:
name = expr.Name
# Skip internal expressions (p0, p1, p123, etc.)
if name.startswith('p') and len(name) > 1:
rest = name[1:]
# Check if rest is numeric (possibly with dots for decimals)
if rest.replace('.', '').replace('_', '').isdigit():
continue
value = expr.Value
units = expr.Units.Name if expr.Units else ''
rhs = expr.RightHandSide if hasattr(expr, 'RightHandSide') else ''
results['expressions'].append({
'name': name,
'value': value,
'units': units,
'formula': rhs
})
units_str = f" [{units}]" if units else ""
output_lines.append(f"{name}: {value}{units_str}")
except Exception as e:
output_lines.append(f"Error reading expression: {e}")
results['success'] = True
results['count'] = len(results['expressions'])
output_lines.append("")
output_lines.append(f"Total user expressions: {results['count']}")
except Exception as e:
import traceback
results['error'] = str(e)
results['traceback'] = traceback.format_exc()
output_lines.append(f"ERROR: {e}")
output_lines.append(traceback.format_exc())
# Write outputs
with open(output_json, 'w') as f:
json.dump(results, f, indent=2)
with open(output_txt, 'w') as f:
f.write('\n'.join(output_lines))
# Exit NX
try:
session.Parts.Work.Close(NXOpen.BasePart.CloseWholeTree.FalseValue,
NXOpen.BasePart.CloseModified.CloseModified, None)
except:
pass
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,620 @@
"""
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:])

View File

@@ -0,0 +1,55 @@
"""Simple expression lister - writes to file regardless of print issues"""
import NXOpen
import os
import json
session = NXOpen.Session.GetSession()
output_lines = []
results = {'expressions': [], 'success': False}
try:
# Get all open parts and find M1_Blank
for part in session.Parts:
part_name = part.Name if hasattr(part, 'Name') else str(part)
if 'M1_Blank' in part_name and '_fem' not in part_name.lower() and '_i' not in part_name.lower():
output_lines.append(f"Found part: {part_name}")
for expr in part.Expressions:
try:
name = expr.Name
# Skip internal expressions (p0, p1, etc.)
if name.startswith('p') and len(name) > 1:
rest = name[1:].replace('.', '').replace('_', '')
if rest.isdigit():
continue
value = expr.Value
units = expr.Units.Name if expr.Units else ''
rhs = expr.RightHandSide if hasattr(expr, 'RightHandSide') else ''
results['expressions'].append({
'name': name,
'value': value,
'units': units,
'rhs': rhs
})
output_lines.append(f"{name}: {value} {units}")
except:
pass
results['success'] = True
break
except Exception as e:
output_lines.append(f"Error: {str(e)}")
results['error'] = str(e)
# Write to file
output_path = r"C:\Users\antoi\Atomizer\_expressions_output.json"
with open(output_path, 'w') as f:
json.dump(results, f, indent=2)
# Also write text version
text_path = r"C:\Users\antoi\Atomizer\_expressions_output.txt"
with open(text_path, 'w') as f:
f.write('\n'.join(output_lines))

11
nx_journals/test_write.py Normal file
View File

@@ -0,0 +1,11 @@
"""Simple test - just write a file"""
with open(r"C:\Users\antoi\Atomizer\_test_output.txt", 'w') as f:
f.write("Journal executed successfully!\n")
try:
import NXOpen
f.write("NXOpen imported OK\n")
session = NXOpen.Session.GetSession()
f.write(f"Session: {session}\n")
except Exception as e:
f.write(f"NXOpen error: {e}\n")

View File

@@ -0,0 +1,229 @@
# Designcenter 2512
# Journal created by antoi on Thu Dec 18 14:06:36 2025 Eastern Standard Time
#
import math
import NXOpen
import NXOpen.Gateway
def main(args) :
theSession = NXOpen.Session.GetSession() #type: NXOpen.Session
workPart = theSession.Parts.Work
displayPart = theSession.Parts.Display
# ----------------------------------------------
# Menu: Edit->Show and Hide->Show and Hide...
# ----------------------------------------------
markId1 = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Start")
theSession.SetUndoMarkName(markId1, "Show and Hide Dialog")
markId2 = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Hide Datums")
numberHidden1 = theSession.DisplayManager.HideByType("SHOW_HIDE_TYPE_DATUMS", NXOpen.DisplayManager.ShowHideScope.AnyInAssembly)
nErrs1 = theSession.UpdateManager.DoUpdate(markId2)
workPart.ModelingViews.WorkView.FitAfterShowOrHide(NXOpen.View.ShowOrHideType.HideOnly)
markId3 = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Hide Curves")
numberHidden2 = theSession.DisplayManager.HideByType("SHOW_HIDE_TYPE_CURVES", NXOpen.DisplayManager.ShowHideScope.AnyInAssembly)
nErrs2 = theSession.UpdateManager.DoUpdate(markId3)
exists1 = theSession.DoesUndoMarkExist(markId3, "Hide Curves")
theSession.DeleteUndoMark(markId3, "Hide Curves")
workPart.ModelingViews.WorkView.FitAfterShowOrHide(NXOpen.View.ShowOrHideType.HideOnly)
markId4 = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Hide Sketches")
numberHidden3 = theSession.DisplayManager.HideByType("SHOW_HIDE_TYPE_SKETCHES", NXOpen.DisplayManager.ShowHideScope.AnyInAssembly)
nErrs3 = theSession.UpdateManager.DoUpdate(markId4)
workPart.ModelingViews.WorkView.FitAfterShowOrHide(NXOpen.View.ShowOrHideType.HideOnly)
theSession.SetUndoMarkName(markId1, "Show and Hide")
theSession.DeleteUndoMark(markId1, None)
matrix1 = NXOpen.Matrix3x3()
matrix1.Xx = 0.0
matrix1.Xy = -1.0
matrix1.Xz = 0.0
matrix1.Yx = -1.0
matrix1.Yy = -0.0
matrix1.Yz = -0.0
matrix1.Zx = 0.0
matrix1.Zy = 0.0
matrix1.Zz = -1.0
workPart.ModelingViews.WorkView.Orient(matrix1)
scaleAboutPoint1 = NXOpen.Point3d(-759.81281858578541, -319.30527689743337, 0.0)
viewCenter1 = NXOpen.Point3d(759.81281858579484, 319.30527689744417, 0.0)
workPart.ModelingViews.WorkView.ZoomAboutPoint(0.80000000000000004, scaleAboutPoint1, viewCenter1)
scaleAboutPoint2 = NXOpen.Point3d(-949.76602323223278, -399.13159612179305, 0.0)
viewCenter2 = NXOpen.Point3d(949.76602323224245, 399.13159612180385, 0.0)
workPart.ModelingViews.WorkView.ZoomAboutPoint(0.80000000000000004, scaleAboutPoint2, viewCenter2)
scaleAboutPoint3 = NXOpen.Point3d(-1394.8708922057567, -214.19365760462478, 0.0)
viewCenter3 = NXOpen.Point3d(1394.870892205766, 214.19365760463569, 0.0)
workPart.ModelingViews.WorkView.ZoomAboutPoint(1.25, scaleAboutPoint3, viewCenter3)
scaleAboutPoint4 = NXOpen.Point3d(-1115.8967137646043, -171.35492608369873, 0.0)
viewCenter4 = NXOpen.Point3d(1115.8967137646139, 171.35492608370959, 0.0)
workPart.ModelingViews.WorkView.ZoomAboutPoint(0.80000000000000004, scaleAboutPoint4, viewCenter4)
# ----------------------------------------------
# Menu: File->Export->Image...
# ----------------------------------------------
markId5 = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Start")
imageExportBuilder1 = workPart.Views.CreateImageExportBuilder()
imageExportBuilder1.RegionMode = True
regiontopleftpoint1 = [None] * 2
regiontopleftpoint1[0] = 95
regiontopleftpoint1[1] = 83
imageExportBuilder1.SetRegionTopLeftPoint(regiontopleftpoint1)
imageExportBuilder1.RegionWidth = 1157
imageExportBuilder1.RegionHeight = 1056
imageExportBuilder1.DeviceWidth = 2388
imageExportBuilder1.DeviceHeight = 1172
imageExportBuilder1.FileFormat = NXOpen.Gateway.ImageExportBuilder.FileFormats.Png
imageExportBuilder1.FileName = "C:\\Users\\antoi\\Atomizer\\studies\\M1_Mirror\\m1_mirror_cost_reduction_V4\\1_setup\\M1_Blank_Top.png"
imageExportBuilder1.BackgroundOption = NXOpen.Gateway.ImageExportBuilder.BackgroundOptions.Original
imageExportBuilder1.EnhanceEdges = False
nXObject1 = imageExportBuilder1.Commit()
theSession.DeleteUndoMark(markId5, "Export Image")
imageExportBuilder1.Destroy()
markId6 = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Start")
imageExportBuilder2 = workPart.Views.CreateImageExportBuilder()
imageExportBuilder2.Destroy()
theSession.UndoToMark(markId6, None)
theSession.DeleteUndoMark(markId6, None)
rotMatrix1 = NXOpen.Matrix3x3()
rotMatrix1.Xx = -0.34262722569067999
rotMatrix1.Xy = -0.93944302509010613
rotMatrix1.Xz = 0.0073066288434778118
rotMatrix1.Yx = -0.67329035687890959
rotMatrix1.Yy = 0.24011894541756998
rotMatrix1.Yz = -0.69930178563008338
rotMatrix1.Zx = 0.65519972493078527
rotMatrix1.Zy = -0.2445193134725811
rotMatrix1.Zz = -0.71478921773451431
translation1 = NXOpen.Point3d(-691.94814615291523, -16.771832954225655, -903.92900031772103)
workPart.ModelingViews.WorkView.SetRotationTranslationScale(rotMatrix1, translation1, 0.20258147300869808)
scaleAboutPoint5 = NXOpen.Point3d(-1091.8652302284754, -297.78142642594378, 0.0)
viewCenter5 = NXOpen.Point3d(1091.8652302284847, 297.78142642595469, 0.0)
workPart.ModelingViews.WorkView.ZoomAboutPoint(1.25, scaleAboutPoint5, viewCenter5)
scaleAboutPoint6 = NXOpen.Point3d(-873.49218418277917, -238.22514114075392, 0.0)
viewCenter6 = NXOpen.Point3d(873.49218418278895, 238.2251411407648, 0.0)
workPart.ModelingViews.WorkView.ZoomAboutPoint(1.25, scaleAboutPoint6, viewCenter6)
scaleAboutPoint7 = NXOpen.Point3d(-519.08004438038643, -302.5877231331695, 0.0)
viewCenter7 = NXOpen.Point3d(519.08004438039586, 302.58772313318048, 0.0)
workPart.ModelingViews.WorkView.ZoomAboutPoint(0.80000000000000004, scaleAboutPoint7, viewCenter7)
scaleAboutPoint8 = NXOpen.Point3d(-648.85005547548417, -378.23465391646323, 0.0)
viewCenter8 = NXOpen.Point3d(648.85005547549372, 378.23465391647414, 0.0)
workPart.ModelingViews.WorkView.ZoomAboutPoint(0.80000000000000004, scaleAboutPoint8, viewCenter8)
scaleAboutPoint9 = NXOpen.Point3d(-726.16874163520447, -271.6602486692816, 0.0)
viewCenter9 = NXOpen.Point3d(726.16874163521379, 271.66024866929223, 0.0)
workPart.ModelingViews.WorkView.ZoomAboutPoint(1.25, scaleAboutPoint9, viewCenter9)
rotMatrix2 = NXOpen.Matrix3x3()
rotMatrix2.Xx = -0.35281096074613638
rotMatrix2.Xy = -0.93549939803135751
rotMatrix2.Xz = 0.019112882052533756
rotMatrix2.Yx = -0.67083068516183819
rotMatrix2.Yy = 0.23864906945399289
rotMatrix2.Yz = -0.70216295366107118
rotMatrix2.Zx = 0.65231174895343103
rotMatrix2.Zy = -0.26055229404422597
rotMatrix2.Zz = -0.71175970962509794
translation2 = NXOpen.Point3d(-445.60899304577225, -25.448049758528374, -903.92478002019129)
workPart.ModelingViews.WorkView.SetRotationTranslationScale(rotMatrix2, translation2, 0.25322684126087264)
rotMatrix3 = NXOpen.Matrix3x3()
rotMatrix3.Xx = -0.32736574141345925
rotMatrix3.Xy = -0.94489752125198745
rotMatrix3.Xz = -0.00058794613984273266
rotMatrix3.Yx = -0.71924452681462514
rotMatrix3.Yy = 0.24959027079525001
rotMatrix3.Yz = -0.64837643955618585
rotMatrix3.Zx = 0.61279603621108569
rotMatrix3.Zy = -0.21183335680718612
rotMatrix3.Zz = -0.76131967460967154
translation3 = NXOpen.Point3d(-445.6364375527848, -25.373121722553414, -903.99382020435428)
workPart.ModelingViews.WorkView.SetRotationTranslationScale(rotMatrix3, translation3, 0.25322684126087264)
# ----------------------------------------------
# Menu: File->Export->Image...
# ----------------------------------------------
markId7 = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Start")
imageExportBuilder3 = workPart.Views.CreateImageExportBuilder()
imageExportBuilder3.RegionMode = True
regiontopleftpoint2 = [None] * 2
regiontopleftpoint2[0] = 129
regiontopleftpoint2[1] = 96
imageExportBuilder3.SetRegionTopLeftPoint(regiontopleftpoint2)
imageExportBuilder3.RegionWidth = 1343
imageExportBuilder3.RegionHeight = 1045
imageExportBuilder3.DeviceWidth = 2388
imageExportBuilder3.DeviceHeight = 1172
imageExportBuilder3.FileFormat = NXOpen.Gateway.ImageExportBuilder.FileFormats.Png
imageExportBuilder3.FileName = "C:\\Users\\antoi\\Atomizer\\studies\\M1_Mirror\\m1_mirror_cost_reduction_V4\\1_setup\\M1_Blank_iso.png"
imageExportBuilder3.BackgroundOption = NXOpen.Gateway.ImageExportBuilder.BackgroundOptions.Original
imageExportBuilder3.EnhanceEdges = False
nXObject2 = imageExportBuilder3.Commit()
theSession.DeleteUndoMark(markId7, "Export Image")
imageExportBuilder3.Destroy()
# ----------------------------------------------
# Menu: Tools->Automation->Journal->Stop Recording
# ----------------------------------------------
if __name__ == '__main__':
main(sys.argv[1:])