feat: Add MLP surrogate with Turbo Mode for 100x faster optimization
Neural Acceleration (MLP Surrogate): - Add run_nn_optimization.py with hybrid FEA/NN workflow - MLP architecture: 4-layer (64->128->128->64) with BatchNorm/Dropout - Three workflow modes: - --all: Sequential export->train->optimize->validate - --hybrid-loop: Iterative Train->NN->Validate->Retrain cycle - --turbo: Aggressive single-best validation (RECOMMENDED) - Turbo mode: 5000 NN trials + 50 FEA validations in ~12 minutes - Separate nn_study.db to avoid overloading dashboard Performance Results (bracket_pareto_3obj study): - NN prediction errors: mass 1-5%, stress 1-4%, stiffness 5-15% - Found minimum mass designs at boundary (angle~30deg, thick~30mm) - 100x speedup vs pure FEA exploration Protocol Operating System: - Add .claude/skills/ with Bootstrap, Cheatsheet, Context Loader - Add docs/protocols/ with operations (OP_01-06) and system (SYS_10-14) - Update SYS_14_NEURAL_ACCELERATION.md with MLP Turbo Mode docs NX Automation: - Add optimization_engine/hooks/ for NX CAD/CAE automation - Add study_wizard.py for guided study creation - Fix FEM mesh update: load idealized part before UpdateFemodel() New Study: - bracket_pareto_3obj: 3-objective Pareto (mass, stress, stiffness) - 167 FEA trials + 5000 NN trials completed - Demonstrates full hybrid workflow 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
344
optimization_engine/hooks/README.md
Normal file
344
optimization_engine/hooks/README.md
Normal file
@@ -0,0 +1,344 @@
|
||||
# Atomizer NX Open Hooks
|
||||
|
||||
Direct Python hooks for NX CAD/CAE operations via NX Open API.
|
||||
|
||||
## Overview
|
||||
|
||||
This module provides a clean Python API for manipulating NX parts programmatically. Each hook executes NX journals via `run_journal.exe` and returns structured JSON results.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
hooks/
|
||||
├── __init__.py # Main entry point
|
||||
├── README.md # This file
|
||||
├── test_hooks.py # Test script
|
||||
└── nx_cad/ # CAD manipulation hooks
|
||||
├── __init__.py
|
||||
├── part_manager.py # Open/Close/Save parts
|
||||
├── expression_manager.py # Get/Set expressions
|
||||
├── geometry_query.py # Mass properties, bodies
|
||||
└── feature_manager.py # Suppress/Unsuppress features
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- **NX Installation**: Siemens NX 2506 or compatible version
|
||||
- **Environment Variable**: `NX_BIN_PATH` (defaults to `C:\Program Files\Siemens\NX2506\NXBIN`)
|
||||
- **Python**: 3.8+ with `atomizer` conda environment
|
||||
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
from optimization_engine.hooks.nx_cad import (
|
||||
part_manager,
|
||||
expression_manager,
|
||||
geometry_query,
|
||||
feature_manager,
|
||||
)
|
||||
|
||||
# Path to your NX part
|
||||
part_path = "C:/path/to/model.prt"
|
||||
|
||||
# Get all expressions
|
||||
result = expression_manager.get_expressions(part_path)
|
||||
if result["success"]:
|
||||
for name, expr in result["data"]["expressions"].items():
|
||||
print(f"{name} = {expr['value']} {expr['units']}")
|
||||
|
||||
# Get mass properties
|
||||
result = geometry_query.get_mass_properties(part_path)
|
||||
if result["success"]:
|
||||
print(f"Mass: {result['data']['mass']:.4f} kg")
|
||||
print(f"Material: {result['data']['material']}")
|
||||
```
|
||||
|
||||
## Module Reference
|
||||
|
||||
### part_manager
|
||||
|
||||
Manage NX part files (open, close, save).
|
||||
|
||||
| Function | Description | Returns |
|
||||
|----------|-------------|---------|
|
||||
| `open_part(path)` | Open an NX part file | Part info dict |
|
||||
| `close_part(path)` | Close an open part | Success status |
|
||||
| `save_part(path)` | Save a part | Success status |
|
||||
| `save_part_as(path, new_path)` | Save with new name | Success status |
|
||||
| `get_part_info(path)` | Get part metadata | Part info dict |
|
||||
|
||||
**Example:**
|
||||
```python
|
||||
from optimization_engine.hooks.nx_cad import part_manager
|
||||
|
||||
# Open a part
|
||||
result = part_manager.open_part("C:/models/bracket.prt")
|
||||
if result["success"]:
|
||||
print(f"Opened: {result['data']['part_name']}")
|
||||
print(f"Modified: {result['data']['is_modified']}")
|
||||
|
||||
# Save the part
|
||||
result = part_manager.save_part("C:/models/bracket.prt")
|
||||
|
||||
# Save as new file
|
||||
result = part_manager.save_part_as(
|
||||
"C:/models/bracket.prt",
|
||||
"C:/models/bracket_v2.prt"
|
||||
)
|
||||
```
|
||||
|
||||
### expression_manager
|
||||
|
||||
Get and set NX expressions (design parameters).
|
||||
|
||||
| Function | Description | Returns |
|
||||
|----------|-------------|---------|
|
||||
| `get_expressions(path)` | Get all expressions | Dict of expressions |
|
||||
| `get_expression(path, name)` | Get single expression | Expression dict |
|
||||
| `set_expression(path, name, value)` | Set single expression | Success status |
|
||||
| `set_expressions(path, dict)` | Set multiple expressions | Success status |
|
||||
|
||||
**Example:**
|
||||
```python
|
||||
from optimization_engine.hooks.nx_cad import expression_manager
|
||||
|
||||
part = "C:/models/bracket.prt"
|
||||
|
||||
# Get all expressions
|
||||
result = expression_manager.get_expressions(part)
|
||||
if result["success"]:
|
||||
for name, expr in result["data"]["expressions"].items():
|
||||
print(f"{name} = {expr['value']} {expr['units']}")
|
||||
# Example output:
|
||||
# thickness = 5.0 MilliMeter
|
||||
# width = 50.0 MilliMeter
|
||||
|
||||
# Get specific expression
|
||||
result = expression_manager.get_expression(part, "thickness")
|
||||
if result["success"]:
|
||||
print(f"Thickness: {result['data']['value']} {result['data']['units']}")
|
||||
|
||||
# Set single expression
|
||||
result = expression_manager.set_expression(part, "thickness", 7.5)
|
||||
|
||||
# Set multiple expressions (batch update)
|
||||
result = expression_manager.set_expressions(part, {
|
||||
"thickness": 7.5,
|
||||
"width": 60.0,
|
||||
"height": 100.0
|
||||
})
|
||||
if result["success"]:
|
||||
print(f"Updated {result['data']['update_count']} expressions")
|
||||
```
|
||||
|
||||
### geometry_query
|
||||
|
||||
Query geometric properties (mass, volume, bodies).
|
||||
|
||||
| Function | Description | Returns |
|
||||
|----------|-------------|---------|
|
||||
| `get_mass_properties(path)` | Get mass, volume, area, centroid | Properties dict |
|
||||
| `get_bodies(path)` | Get body count and types | Bodies dict |
|
||||
| `get_volume(path)` | Get total volume | Volume float |
|
||||
| `get_surface_area(path)` | Get total surface area | Area float |
|
||||
| `get_material(path)` | Get material name | Material string |
|
||||
|
||||
**Example:**
|
||||
```python
|
||||
from optimization_engine.hooks.nx_cad import geometry_query
|
||||
|
||||
part = "C:/models/bracket.prt"
|
||||
|
||||
# Get mass properties
|
||||
result = geometry_query.get_mass_properties(part)
|
||||
if result["success"]:
|
||||
data = result["data"]
|
||||
print(f"Mass: {data['mass']:.6f} {data['mass_unit']}")
|
||||
print(f"Volume: {data['volume']:.2f} {data['volume_unit']}")
|
||||
print(f"Surface Area: {data['surface_area']:.2f} {data['area_unit']}")
|
||||
print(f"Centroid: ({data['centroid']['x']:.2f}, "
|
||||
f"{data['centroid']['y']:.2f}, {data['centroid']['z']:.2f}) mm")
|
||||
print(f"Material: {data['material']}")
|
||||
# Example output:
|
||||
# Mass: 0.109838 kg
|
||||
# Volume: 39311.99 mm^3
|
||||
# Surface Area: 10876.71 mm^2
|
||||
# Centroid: (0.00, 42.30, 39.58) mm
|
||||
# Material: Aluminum_2014
|
||||
|
||||
# Get body information
|
||||
result = geometry_query.get_bodies(part)
|
||||
if result["success"]:
|
||||
print(f"Total bodies: {result['data']['count']}")
|
||||
print(f"Solid bodies: {result['data']['solid_count']}")
|
||||
```
|
||||
|
||||
### feature_manager
|
||||
|
||||
Suppress and unsuppress features for design exploration.
|
||||
|
||||
| Function | Description | Returns |
|
||||
|----------|-------------|---------|
|
||||
| `get_features(path)` | List all features | Features list |
|
||||
| `get_feature_status(path, name)` | Check if suppressed | Boolean |
|
||||
| `suppress_feature(path, name)` | Suppress a feature | Success status |
|
||||
| `unsuppress_feature(path, name)` | Unsuppress a feature | Success status |
|
||||
| `suppress_features(path, names)` | Suppress multiple | Success status |
|
||||
| `unsuppress_features(path, names)` | Unsuppress multiple | Success status |
|
||||
|
||||
**Example:**
|
||||
```python
|
||||
from optimization_engine.hooks.nx_cad import feature_manager
|
||||
|
||||
part = "C:/models/bracket.prt"
|
||||
|
||||
# List all features
|
||||
result = feature_manager.get_features(part)
|
||||
if result["success"]:
|
||||
print(f"Found {result['data']['count']} features")
|
||||
for feat in result["data"]["features"]:
|
||||
status = "suppressed" if feat["is_suppressed"] else "active"
|
||||
print(f" {feat['name']} ({feat['type']}): {status}")
|
||||
|
||||
# Suppress a feature
|
||||
result = feature_manager.suppress_feature(part, "FILLET(3)")
|
||||
if result["success"]:
|
||||
print("Feature suppressed!")
|
||||
|
||||
# Unsuppress multiple features
|
||||
result = feature_manager.unsuppress_features(part, ["FILLET(3)", "CHAMFER(1)"])
|
||||
```
|
||||
|
||||
## Return Format
|
||||
|
||||
All hook functions return a consistent dictionary structure:
|
||||
|
||||
```python
|
||||
{
|
||||
"success": bool, # True if operation succeeded
|
||||
"error": str | None, # Error message if failed
|
||||
"data": dict # Operation-specific results
|
||||
}
|
||||
```
|
||||
|
||||
**Error Handling:**
|
||||
```python
|
||||
result = expression_manager.get_expressions(part_path)
|
||||
|
||||
if not result["success"]:
|
||||
print(f"Error: {result['error']}")
|
||||
# Handle error...
|
||||
else:
|
||||
# Process result["data"]...
|
||||
```
|
||||
|
||||
## NX Open API Reference
|
||||
|
||||
These hooks use the following NX Open APIs (verified via Siemens MCP documentation):
|
||||
|
||||
| Hook | NX Open API |
|
||||
|------|-------------|
|
||||
| Open part | `Session.Parts.OpenActiveDisplay()` |
|
||||
| Close part | `Part.Close()` |
|
||||
| Save part | `Part.Save()`, `Part.SaveAs()` |
|
||||
| Get expressions | `Part.Expressions` collection |
|
||||
| Set expression | `ExpressionCollection.Edit()` |
|
||||
| Update model | `Session.UpdateManager.DoUpdate()` |
|
||||
| Mass properties | `MeasureManager.NewMassProperties()` |
|
||||
| Get bodies | `Part.Bodies` collection |
|
||||
| Suppress feature | `Feature.Suppress()` |
|
||||
| Unsuppress feature | `Feature.Unsuppress()` |
|
||||
|
||||
## Configuration
|
||||
|
||||
### NX Path
|
||||
|
||||
Set the NX installation path via environment variable:
|
||||
|
||||
```bash
|
||||
# Windows
|
||||
set NX_BIN_PATH=C:\Program Files\Siemens\NX2506\NXBIN
|
||||
|
||||
# Or in Python before importing
|
||||
import os
|
||||
os.environ["NX_BIN_PATH"] = r"C:\Program Files\Siemens\NX2506\NXBIN"
|
||||
```
|
||||
|
||||
### Timeout
|
||||
|
||||
Journal execution has a default 2-minute timeout. For large parts, you may need to increase this in the hook source code.
|
||||
|
||||
## Integration with Atomizer
|
||||
|
||||
These hooks are designed to integrate with Atomizer's optimization workflow:
|
||||
|
||||
```python
|
||||
# In run_optimization.py or custom extractor
|
||||
|
||||
from optimization_engine.hooks.nx_cad import expression_manager, geometry_query
|
||||
|
||||
def evaluate_design(part_path: str, params: dict) -> dict:
|
||||
"""Evaluate a design point by updating NX model and extracting metrics."""
|
||||
|
||||
# 1. Update design parameters
|
||||
result = expression_manager.set_expressions(part_path, params)
|
||||
if not result["success"]:
|
||||
raise RuntimeError(f"Failed to set expressions: {result['error']}")
|
||||
|
||||
# 2. Extract mass (objective)
|
||||
result = geometry_query.get_mass_properties(part_path)
|
||||
if not result["success"]:
|
||||
raise RuntimeError(f"Failed to get mass: {result['error']}")
|
||||
|
||||
return {
|
||||
"mass_kg": result["data"]["mass"],
|
||||
"volume_mm3": result["data"]["volume"],
|
||||
"material": result["data"]["material"]
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Run the test script to verify hooks work with your NX installation:
|
||||
|
||||
```bash
|
||||
# Activate atomizer environment
|
||||
conda activate atomizer
|
||||
|
||||
# Run tests with default bracket part
|
||||
python -m optimization_engine.hooks.test_hooks
|
||||
|
||||
# Or specify a custom part
|
||||
python -m optimization_engine.hooks.test_hooks "C:/path/to/your/part.prt"
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Part file not found"
|
||||
- Verify the path exists and is accessible
|
||||
- Use forward slashes or raw strings: `r"C:\path\to\file.prt"`
|
||||
|
||||
### "Failed to open part"
|
||||
- Ensure NX license is available
|
||||
- Check `NX_BIN_PATH` environment variable
|
||||
- Verify NX version compatibility
|
||||
|
||||
### "Expression not found"
|
||||
- Expression names are case-sensitive
|
||||
- Use `get_expressions()` to list available names
|
||||
|
||||
### Journal execution timeout
|
||||
- Large parts may need longer timeout
|
||||
- Check NX is not displaying modal dialogs
|
||||
|
||||
## Version History
|
||||
|
||||
| Version | Date | Changes |
|
||||
|---------|------|---------|
|
||||
| 1.0.0 | 2025-12-06 | Initial release with CAD hooks |
|
||||
|
||||
## See Also
|
||||
|
||||
- [NX_OPEN_AUTOMATION_ROADMAP.md](../../docs/plans/NX_OPEN_AUTOMATION_ROADMAP.md) - Development roadmap
|
||||
- [SYS_12_EXTRACTOR_LIBRARY.md](../../docs/protocols/system/SYS_12_EXTRACTOR_LIBRARY.md) - Extractor catalog
|
||||
- [NXJournaling.com](https://nxjournaling.com/) - NX Open examples
|
||||
72
optimization_engine/hooks/__init__.py
Normal file
72
optimization_engine/hooks/__init__.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""
|
||||
Atomizer NX Open Hooks
|
||||
======================
|
||||
|
||||
Direct Python hooks for NX CAD/CAE operations via NX Open API.
|
||||
|
||||
This module provides a clean Python interface for manipulating NX parts
|
||||
programmatically. Each hook executes NX journals via `run_journal.exe`
|
||||
and returns structured JSON results.
|
||||
|
||||
Modules
|
||||
-------
|
||||
nx_cad : CAD manipulation hooks
|
||||
- part_manager : Open, close, save parts
|
||||
- expression_manager : Get/set design parameters
|
||||
- geometry_query : Mass properties, bodies, volumes
|
||||
- feature_manager : Suppress/unsuppress features
|
||||
|
||||
nx_cae : CAE/Simulation hooks (Phase 2)
|
||||
- solver_manager : BDF export, solve simulations
|
||||
|
||||
Quick Start
|
||||
-----------
|
||||
>>> from optimization_engine.hooks.nx_cad import expression_manager
|
||||
>>> result = expression_manager.get_expressions("C:/model.prt")
|
||||
>>> if result["success"]:
|
||||
... for name, expr in result["data"]["expressions"].items():
|
||||
... print(f"{name} = {expr['value']}")
|
||||
|
||||
>>> from optimization_engine.hooks.nx_cae import solver_manager
|
||||
>>> result = solver_manager.get_bdf_from_solution_folder("C:/model.sim")
|
||||
|
||||
Requirements
|
||||
------------
|
||||
- Siemens NX 2506+ installed
|
||||
- NX_BIN_PATH environment variable (or default path)
|
||||
- Python 3.8+ with atomizer conda environment
|
||||
|
||||
See Also
|
||||
--------
|
||||
- optimization_engine/hooks/README.md : Full documentation
|
||||
- docs/plans/NX_OPEN_AUTOMATION_ROADMAP.md : Development roadmap
|
||||
|
||||
Version
|
||||
-------
|
||||
1.1.0 (2025-12-06) - Added nx_cae module with solver_manager
|
||||
1.0.0 (2025-12-06) - Initial release with nx_cad hooks
|
||||
"""
|
||||
|
||||
from .nx_cad import (
|
||||
part_manager,
|
||||
expression_manager,
|
||||
geometry_query,
|
||||
feature_manager,
|
||||
)
|
||||
|
||||
from .nx_cae import (
|
||||
solver_manager,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# CAD hooks
|
||||
'part_manager',
|
||||
'expression_manager',
|
||||
'geometry_query',
|
||||
'feature_manager',
|
||||
# CAE hooks
|
||||
'solver_manager',
|
||||
]
|
||||
|
||||
__version__ = '1.1.0'
|
||||
__author__ = 'Atomizer'
|
||||
399
optimization_engine/hooks/examples.py
Normal file
399
optimization_engine/hooks/examples.py
Normal file
@@ -0,0 +1,399 @@
|
||||
"""
|
||||
NX Open Hooks - Usage Examples
|
||||
==============================
|
||||
|
||||
This file contains practical examples of using the NX Open hooks
|
||||
for common optimization tasks.
|
||||
|
||||
Run examples:
|
||||
python -m optimization_engine.hooks.examples
|
||||
|
||||
Or import specific examples:
|
||||
from optimization_engine.hooks.examples import design_exploration_example
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add project root to path
|
||||
project_root = Path(__file__).parent.parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from optimization_engine.hooks.nx_cad import (
|
||||
part_manager,
|
||||
expression_manager,
|
||||
geometry_query,
|
||||
feature_manager,
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Example 1: Basic Expression Query
|
||||
# =============================================================================
|
||||
|
||||
def basic_expression_query(part_path: str):
|
||||
"""
|
||||
Example: Query all expressions from an NX part.
|
||||
|
||||
This is useful for discovering available design parameters
|
||||
before setting up an optimization study.
|
||||
"""
|
||||
print("\n" + "=" * 60)
|
||||
print("Example 1: Basic Expression Query")
|
||||
print("=" * 60)
|
||||
|
||||
result = expression_manager.get_expressions(part_path)
|
||||
|
||||
if not result["success"]:
|
||||
print(f"ERROR: {result['error']}")
|
||||
return None
|
||||
|
||||
data = result["data"]
|
||||
print(f"\nFound {data['count']} expressions:\n")
|
||||
|
||||
# Print in a nice table format
|
||||
print(f"{'Name':<25} {'Value':>12} {'Units':<15} {'RHS'}")
|
||||
print("-" * 70)
|
||||
|
||||
for name, expr in data["expressions"].items():
|
||||
units = expr.get("units") or ""
|
||||
rhs = expr.get("rhs", "")
|
||||
# Truncate RHS if it's a formula reference
|
||||
if len(rhs) > 20:
|
||||
rhs = rhs[:17] + "..."
|
||||
print(f"{name:<25} {expr['value']:>12.4f} {units:<15} {rhs}")
|
||||
|
||||
return data["expressions"]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Example 2: Mass Properties Extraction
|
||||
# =============================================================================
|
||||
|
||||
def mass_properties_example(part_path: str):
|
||||
"""
|
||||
Example: Extract mass properties from an NX part.
|
||||
|
||||
This is useful for mass optimization objectives.
|
||||
"""
|
||||
print("\n" + "=" * 60)
|
||||
print("Example 2: Mass Properties Extraction")
|
||||
print("=" * 60)
|
||||
|
||||
result = geometry_query.get_mass_properties(part_path)
|
||||
|
||||
if not result["success"]:
|
||||
print(f"ERROR: {result['error']}")
|
||||
return None
|
||||
|
||||
data = result["data"]
|
||||
|
||||
print(f"\nMass Properties:")
|
||||
print("-" * 40)
|
||||
print(f" Mass: {data['mass']:.6f} {data['mass_unit']}")
|
||||
print(f" Volume: {data['volume']:.2f} {data['volume_unit']}")
|
||||
print(f" Surface Area: {data['surface_area']:.2f} {data['area_unit']}")
|
||||
print(f" Material: {data['material'] or 'Not assigned'}")
|
||||
|
||||
centroid = data["centroid"]
|
||||
print(f"\nCentroid (mm):")
|
||||
print(f" X: {centroid['x']:.4f}")
|
||||
print(f" Y: {centroid['y']:.4f}")
|
||||
print(f" Z: {centroid['z']:.4f}")
|
||||
|
||||
if data.get("principal_moments"):
|
||||
pm = data["principal_moments"]
|
||||
print(f"\nPrincipal Moments of Inertia ({pm['unit']}):")
|
||||
print(f" Ixx: {pm['Ixx']:.4f}")
|
||||
print(f" Iyy: {pm['Iyy']:.4f}")
|
||||
print(f" Izz: {pm['Izz']:.4f}")
|
||||
|
||||
return data
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Example 3: Design Parameter Update
|
||||
# =============================================================================
|
||||
|
||||
def design_update_example(part_path: str, dry_run: bool = True):
|
||||
"""
|
||||
Example: Update design parameters in an NX part.
|
||||
|
||||
This demonstrates the workflow for parametric optimization:
|
||||
1. Read current values
|
||||
2. Compute new values
|
||||
3. Update the model
|
||||
|
||||
Args:
|
||||
part_path: Path to the NX part
|
||||
dry_run: If True, only shows what would be changed (default)
|
||||
"""
|
||||
print("\n" + "=" * 60)
|
||||
print("Example 3: Design Parameter Update")
|
||||
print("=" * 60)
|
||||
|
||||
# Step 1: Get current expressions
|
||||
result = expression_manager.get_expressions(part_path)
|
||||
if not result["success"]:
|
||||
print(f"ERROR: {result['error']}")
|
||||
return None
|
||||
|
||||
expressions = result["data"]["expressions"]
|
||||
|
||||
# Step 2: Find numeric expressions (potential design variables)
|
||||
design_vars = {}
|
||||
for name, expr in expressions.items():
|
||||
# Skip linked expressions (RHS contains another expression name)
|
||||
if expr.get("rhs") and not expr["rhs"].replace(".", "").replace("-", "").isdigit():
|
||||
continue
|
||||
# Only include length/angle expressions
|
||||
if expr.get("units") in ["MilliMeter", "Degrees", None]:
|
||||
design_vars[name] = expr["value"]
|
||||
|
||||
print(f"\nIdentified {len(design_vars)} potential design variables:")
|
||||
for name, value in design_vars.items():
|
||||
print(f" {name}: {value}")
|
||||
|
||||
if dry_run:
|
||||
print("\n[DRY RUN] Would update expressions (no changes made)")
|
||||
|
||||
# Example: increase all dimensions by 10%
|
||||
new_values = {name: value * 1.1 for name, value in design_vars.items()}
|
||||
|
||||
print("\nProposed changes:")
|
||||
for name, new_val in new_values.items():
|
||||
old_val = design_vars[name]
|
||||
print(f" {name}: {old_val:.4f} -> {new_val:.4f} (+10%)")
|
||||
|
||||
return new_values
|
||||
|
||||
else:
|
||||
# Actually update the model
|
||||
new_values = {name: value * 1.1 for name, value in design_vars.items()}
|
||||
|
||||
print("\nUpdating expressions...")
|
||||
result = expression_manager.set_expressions(part_path, new_values)
|
||||
|
||||
if result["success"]:
|
||||
print(f"SUCCESS: Updated {result['data']['update_count']} expressions")
|
||||
if result["data"].get("errors"):
|
||||
print(f"Warnings: {result['data']['errors']}")
|
||||
else:
|
||||
print(f"ERROR: {result['error']}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Example 4: Feature Exploration
|
||||
# =============================================================================
|
||||
|
||||
def feature_exploration_example(part_path: str):
|
||||
"""
|
||||
Example: Explore and manipulate features.
|
||||
|
||||
This is useful for topological optimization where features
|
||||
can be suppressed/unsuppressed to explore design space.
|
||||
"""
|
||||
print("\n" + "=" * 60)
|
||||
print("Example 4: Feature Exploration")
|
||||
print("=" * 60)
|
||||
|
||||
result = feature_manager.get_features(part_path)
|
||||
|
||||
if not result["success"]:
|
||||
print(f"ERROR: {result['error']}")
|
||||
return None
|
||||
|
||||
data = result["data"]
|
||||
|
||||
print(f"\nFound {data['count']} features ({data['suppressed_count']} suppressed):\n")
|
||||
|
||||
print(f"{'Name':<30} {'Type':<20} {'Status'}")
|
||||
print("-" * 60)
|
||||
|
||||
for feat in data["features"]:
|
||||
status = "SUPPRESSED" if feat["is_suppressed"] else "Active"
|
||||
print(f"{feat['name']:<30} {feat['type']:<20} {status}")
|
||||
|
||||
# Group by type
|
||||
print("\n\nFeatures by type:")
|
||||
print("-" * 40)
|
||||
type_counts = {}
|
||||
for feat in data["features"]:
|
||||
feat_type = feat["type"]
|
||||
type_counts[feat_type] = type_counts.get(feat_type, 0) + 1
|
||||
|
||||
for feat_type, count in sorted(type_counts.items(), key=lambda x: -x[1]):
|
||||
print(f" {feat_type}: {count}")
|
||||
|
||||
return data
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Example 5: Optimization Objective Evaluation
|
||||
# =============================================================================
|
||||
|
||||
def evaluate_design_point(part_path: str, parameters: dict) -> dict:
|
||||
"""
|
||||
Example: Complete design evaluation workflow.
|
||||
|
||||
This demonstrates how hooks integrate into an optimization loop:
|
||||
1. Update parameters
|
||||
2. Extract objectives (mass, volume)
|
||||
3. Return metrics
|
||||
|
||||
Args:
|
||||
part_path: Path to the NX part
|
||||
parameters: Dict of parameter_name -> new_value
|
||||
|
||||
Returns:
|
||||
Dict with mass_kg, volume_mm3, surface_area_mm2
|
||||
"""
|
||||
print("\n" + "=" * 60)
|
||||
print("Example 5: Optimization Objective Evaluation")
|
||||
print("=" * 60)
|
||||
|
||||
print(f"\nParameters to set:")
|
||||
for name, value in parameters.items():
|
||||
print(f" {name} = {value}")
|
||||
|
||||
# Step 1: Update parameters
|
||||
print("\n[1/2] Updating design parameters...")
|
||||
result = expression_manager.set_expressions(part_path, parameters)
|
||||
|
||||
if not result["success"]:
|
||||
raise RuntimeError(f"Failed to set expressions: {result['error']}")
|
||||
|
||||
print(f" Updated {result['data']['update_count']} expressions")
|
||||
|
||||
# Step 2: Extract objectives
|
||||
print("\n[2/2] Extracting mass properties...")
|
||||
result = geometry_query.get_mass_properties(part_path)
|
||||
|
||||
if not result["success"]:
|
||||
raise RuntimeError(f"Failed to get mass properties: {result['error']}")
|
||||
|
||||
data = result["data"]
|
||||
|
||||
# Return metrics
|
||||
metrics = {
|
||||
"mass_kg": data["mass"],
|
||||
"volume_mm3": data["volume"],
|
||||
"surface_area_mm2": data["surface_area"],
|
||||
"material": data.get("material"),
|
||||
}
|
||||
|
||||
print(f"\nObjective metrics:")
|
||||
print(f" Mass: {metrics['mass_kg']:.6f} kg")
|
||||
print(f" Volume: {metrics['volume_mm3']:.2f} mm^3")
|
||||
print(f" Surface Area: {metrics['surface_area_mm2']:.2f} mm^2")
|
||||
|
||||
return metrics
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Example 6: Batch Processing Multiple Parts
|
||||
# =============================================================================
|
||||
|
||||
def batch_mass_extraction(part_paths: list) -> list:
|
||||
"""
|
||||
Example: Extract mass from multiple parts.
|
||||
|
||||
Useful for comparing variants or processing a design library.
|
||||
"""
|
||||
print("\n" + "=" * 60)
|
||||
print("Example 6: Batch Processing Multiple Parts")
|
||||
print("=" * 60)
|
||||
|
||||
results = []
|
||||
|
||||
for i, part_path in enumerate(part_paths, 1):
|
||||
print(f"\n[{i}/{len(part_paths)}] Processing: {Path(part_path).name}")
|
||||
|
||||
result = geometry_query.get_mass_properties(part_path)
|
||||
|
||||
if result["success"]:
|
||||
data = result["data"]
|
||||
results.append({
|
||||
"part": Path(part_path).name,
|
||||
"mass_kg": data["mass"],
|
||||
"volume_mm3": data["volume"],
|
||||
"material": data.get("material"),
|
||||
"success": True,
|
||||
})
|
||||
print(f" Mass: {data['mass']:.4f} kg, Material: {data.get('material')}")
|
||||
else:
|
||||
results.append({
|
||||
"part": Path(part_path).name,
|
||||
"error": result["error"],
|
||||
"success": False,
|
||||
})
|
||||
print(f" ERROR: {result['error']}")
|
||||
|
||||
# Summary
|
||||
print("\n" + "-" * 60)
|
||||
print("Summary:")
|
||||
successful = [r for r in results if r["success"]]
|
||||
print(f" Processed: {len(successful)}/{len(part_paths)} parts")
|
||||
|
||||
if successful:
|
||||
total_mass = sum(r["mass_kg"] for r in successful)
|
||||
print(f" Total mass: {total_mass:.4f} kg")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Main - Run All Examples
|
||||
# =============================================================================
|
||||
|
||||
def main():
|
||||
"""Run all examples with a test part."""
|
||||
|
||||
# Default test part
|
||||
default_part = project_root / "studies/bracket_stiffness_optimization_V3/1_setup/model/Bracket.prt"
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
part_path = sys.argv[1]
|
||||
else:
|
||||
part_path = str(default_part)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("NX OPEN HOOKS - EXAMPLES")
|
||||
print("=" * 60)
|
||||
print(f"\nUsing part: {Path(part_path).name}")
|
||||
|
||||
if not os.path.exists(part_path):
|
||||
print(f"\nERROR: Part file not found: {part_path}")
|
||||
print("\nUsage: python -m optimization_engine.hooks.examples [part_path]")
|
||||
sys.exit(1)
|
||||
|
||||
# Run examples
|
||||
try:
|
||||
# Example 1: Query expressions
|
||||
basic_expression_query(part_path)
|
||||
|
||||
# Example 2: Get mass properties
|
||||
mass_properties_example(part_path)
|
||||
|
||||
# Example 3: Design update (dry run)
|
||||
design_update_example(part_path, dry_run=True)
|
||||
|
||||
# Example 4: Feature exploration
|
||||
feature_exploration_example(part_path)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("ALL EXAMPLES COMPLETED SUCCESSFULLY!")
|
||||
print("=" * 60)
|
||||
|
||||
except Exception as e:
|
||||
print(f"\nEXAMPLE FAILED: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
83
optimization_engine/hooks/nx_cad/__init__.py
Normal file
83
optimization_engine/hooks/nx_cad/__init__.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
NX CAD Hooks
|
||||
============
|
||||
|
||||
Direct manipulation of NX CAD parts via NX Open Python API.
|
||||
|
||||
This submodule contains hooks for CAD-level operations on NX parts:
|
||||
geometry, expressions, features, and part management.
|
||||
|
||||
Modules
|
||||
-------
|
||||
part_manager
|
||||
Open, close, save, and query NX part files.
|
||||
|
||||
Functions:
|
||||
- open_part(path) -> Open an NX part file
|
||||
- close_part(path) -> Close an open part
|
||||
- save_part(path) -> Save a part
|
||||
- save_part_as(path, new_path) -> Save with new name
|
||||
- get_part_info(path) -> Get part metadata
|
||||
|
||||
expression_manager
|
||||
Get and set NX expressions (design parameters).
|
||||
|
||||
Functions:
|
||||
- get_expressions(path) -> Get all expressions
|
||||
- get_expression(path, name) -> Get single expression
|
||||
- set_expression(path, name, value) -> Set single expression
|
||||
- set_expressions(path, dict) -> Set multiple expressions
|
||||
|
||||
geometry_query
|
||||
Query geometric properties (mass, volume, area, bodies).
|
||||
|
||||
Functions:
|
||||
- get_mass_properties(path) -> Get mass, volume, area, centroid
|
||||
- get_bodies(path) -> Get body count and types
|
||||
- get_volume(path) -> Get total volume
|
||||
- get_surface_area(path) -> Get total surface area
|
||||
- get_material(path) -> Get material name
|
||||
|
||||
feature_manager
|
||||
Suppress and unsuppress features for design exploration.
|
||||
|
||||
Functions:
|
||||
- get_features(path) -> List all features
|
||||
- get_feature_status(path, name) -> Check if suppressed
|
||||
- suppress_feature(path, name) -> Suppress a feature
|
||||
- unsuppress_feature(path, name) -> Unsuppress a feature
|
||||
- suppress_features(path, names) -> Suppress multiple
|
||||
- unsuppress_features(path, names) -> Unsuppress multiple
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> from optimization_engine.hooks.nx_cad import geometry_query
|
||||
>>> result = geometry_query.get_mass_properties("C:/model.prt")
|
||||
>>> if result["success"]:
|
||||
... print(f"Mass: {result['data']['mass']:.4f} kg")
|
||||
... print(f"Material: {result['data']['material']}")
|
||||
|
||||
NX Open APIs Used
|
||||
-----------------
|
||||
- Session.Parts.OpenActiveDisplay() - Open parts
|
||||
- Part.Close(), Part.Save(), Part.SaveAs() - Part operations
|
||||
- Part.Expressions, ExpressionCollection.Edit() - Expressions
|
||||
- MeasureManager.NewMassProperties() - Mass properties
|
||||
- Part.Bodies - Body collection
|
||||
- Feature.Suppress(), Feature.Unsuppress() - Feature control
|
||||
- Session.UpdateManager.DoUpdate() - Model update
|
||||
"""
|
||||
|
||||
from . import part_manager
|
||||
from . import expression_manager
|
||||
from . import geometry_query
|
||||
from . import feature_manager
|
||||
from . import model_introspection
|
||||
|
||||
__all__ = [
|
||||
'part_manager',
|
||||
'expression_manager',
|
||||
'geometry_query',
|
||||
'feature_manager',
|
||||
'model_introspection',
|
||||
]
|
||||
566
optimization_engine/hooks/nx_cad/expression_manager.py
Normal file
566
optimization_engine/hooks/nx_cad/expression_manager.py
Normal file
@@ -0,0 +1,566 @@
|
||||
"""
|
||||
NX Expression Manager Hook
|
||||
===========================
|
||||
|
||||
Provides Python functions to get and set NX expressions (parameters).
|
||||
|
||||
API Reference (verified via Siemens MCP docs):
|
||||
- Part.Expressions() -> ExpressionCollection
|
||||
- ExpressionCollection.Edit(expression, value)
|
||||
- Expression.Name, Expression.Value, Expression.RightHandSide
|
||||
- Expression.Units.Name
|
||||
|
||||
Usage:
|
||||
from optimization_engine.hooks.nx_cad import expression_manager
|
||||
|
||||
# Get all expressions
|
||||
result = expression_manager.get_expressions("C:/path/to/part.prt")
|
||||
|
||||
# Get specific expression
|
||||
result = expression_manager.get_expression("C:/path/to/part.prt", "thickness")
|
||||
|
||||
# Set expression value
|
||||
result = expression_manager.set_expression("C:/path/to/part.prt", "thickness", 5.0)
|
||||
|
||||
# Set multiple expressions
|
||||
result = expression_manager.set_expressions("C:/path/to/part.prt", {
|
||||
"thickness": 5.0,
|
||||
"width": 10.0
|
||||
})
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any, List, Tuple, Union
|
||||
|
||||
# NX installation path (configurable)
|
||||
NX_BIN_PATH = os.environ.get(
|
||||
"NX_BIN_PATH",
|
||||
r"C:\Program Files\Siemens\NX2506\NXBIN"
|
||||
)
|
||||
|
||||
# Journal template for expression operations
|
||||
EXPRESSION_OPERATIONS_JOURNAL = '''
|
||||
# NX Open Python Journal - Expression Operations
|
||||
# Auto-generated by Atomizer hooks
|
||||
|
||||
import NXOpen
|
||||
import NXOpen.UF
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
|
||||
def main():
|
||||
"""Execute expression operation based on command arguments."""
|
||||
# Get the NX session
|
||||
session = NXOpen.Session.GetSession()
|
||||
|
||||
# Parse arguments: operation, part_path, output_json, [extra_args...]
|
||||
args = sys.argv[1:] if len(sys.argv) > 1 else []
|
||||
|
||||
if len(args) < 3:
|
||||
raise ValueError("Usage: script.py <operation> <part_path> <output_json> [args...]")
|
||||
|
||||
operation = args[0]
|
||||
part_path = args[1]
|
||||
output_json = args[2]
|
||||
extra_args = args[3:] if len(args) > 3 else []
|
||||
|
||||
result = {"success": False, "error": None, "data": {}}
|
||||
|
||||
try:
|
||||
# Ensure part is open
|
||||
part = ensure_part_open(session, part_path)
|
||||
|
||||
if part is None:
|
||||
result["error"] = f"Failed to open part: {part_path}"
|
||||
elif operation == "get_all":
|
||||
result = get_all_expressions(part)
|
||||
elif operation == "get":
|
||||
expr_name = extra_args[0] if extra_args else None
|
||||
result = get_expression(part, expr_name)
|
||||
elif operation == "set":
|
||||
expr_name = extra_args[0] if len(extra_args) > 0 else None
|
||||
expr_value = extra_args[1] if len(extra_args) > 1 else None
|
||||
result = set_expression(session, part, expr_name, expr_value)
|
||||
elif operation == "set_multiple":
|
||||
# Extra args is a JSON string with name:value pairs
|
||||
expr_dict = json.loads(extra_args[0]) if extra_args else {}
|
||||
result = set_multiple_expressions(session, part, expr_dict)
|
||||
else:
|
||||
result["error"] = f"Unknown operation: {operation}"
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
result["error"] = str(e)
|
||||
result["traceback"] = traceback.format_exc()
|
||||
|
||||
# Write result to output JSON
|
||||
with open(output_json, 'w') as f:
|
||||
json.dump(result, f, indent=2)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def ensure_part_open(session, part_path):
|
||||
"""Ensure the part is open and return it."""
|
||||
# Check if already open
|
||||
part_path_normalized = os.path.normpath(part_path).lower()
|
||||
|
||||
for part in session.Parts:
|
||||
if os.path.normpath(part.FullPath).lower() == part_path_normalized:
|
||||
return part
|
||||
|
||||
# Need to open it
|
||||
if not os.path.exists(part_path):
|
||||
return None
|
||||
|
||||
try:
|
||||
# Set load options for the working directory
|
||||
working_dir = os.path.dirname(part_path)
|
||||
session.Parts.LoadOptions.ComponentLoadMethod = NXOpen.LoadOptions.LoadMethod.FromDirectory
|
||||
session.Parts.LoadOptions.SetSearchDirectories([working_dir], [True])
|
||||
|
||||
# Use OpenActiveDisplay instead of OpenBase for better compatibility
|
||||
part, load_status = session.Parts.OpenActiveDisplay(
|
||||
part_path,
|
||||
NXOpen.DisplayPartOption.AllowAdditional
|
||||
)
|
||||
load_status.Dispose()
|
||||
return part
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
def get_all_expressions(part):
|
||||
"""Get all expressions from a part.
|
||||
|
||||
NX Open API: Part.Expressions() -> ExpressionCollection
|
||||
"""
|
||||
result = {"success": False, "error": None, "data": {}}
|
||||
|
||||
try:
|
||||
expressions = {}
|
||||
|
||||
for expr in part.Expressions:
|
||||
try:
|
||||
expr_data = {
|
||||
"name": expr.Name,
|
||||
"value": expr.Value,
|
||||
"rhs": expr.RightHandSide,
|
||||
"units": expr.Units.Name if expr.Units else None,
|
||||
"type": expr.Type.ToString() if hasattr(expr.Type, 'ToString') else str(expr.Type),
|
||||
}
|
||||
expressions[expr.Name] = expr_data
|
||||
except:
|
||||
# Skip expressions that can't be read
|
||||
pass
|
||||
|
||||
result["success"] = True
|
||||
result["data"] = {
|
||||
"count": len(expressions),
|
||||
"expressions": expressions
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
result["error"] = str(e)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_expression(part, expr_name):
|
||||
"""Get a specific expression by name.
|
||||
|
||||
NX Open API: ExpressionCollection iteration, Expression properties
|
||||
"""
|
||||
result = {"success": False, "error": None, "data": {}}
|
||||
|
||||
if not expr_name:
|
||||
result["error"] = "Expression name is required"
|
||||
return result
|
||||
|
||||
try:
|
||||
# Find the expression by name
|
||||
found_expr = None
|
||||
for expr in part.Expressions:
|
||||
if expr.Name == expr_name:
|
||||
found_expr = expr
|
||||
break
|
||||
|
||||
if found_expr is None:
|
||||
result["error"] = f"Expression not found: {expr_name}"
|
||||
return result
|
||||
|
||||
result["success"] = True
|
||||
result["data"] = {
|
||||
"name": found_expr.Name,
|
||||
"value": found_expr.Value,
|
||||
"rhs": found_expr.RightHandSide,
|
||||
"units": found_expr.Units.Name if found_expr.Units else None,
|
||||
"type": found_expr.Type.ToString() if hasattr(found_expr.Type, 'ToString') else str(found_expr.Type),
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
result["error"] = str(e)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def set_expression(session, part, expr_name, expr_value):
|
||||
"""Set an expression value.
|
||||
|
||||
NX Open API: ExpressionCollection.Edit(expression, new_rhs)
|
||||
"""
|
||||
result = {"success": False, "error": None, "data": {}}
|
||||
|
||||
if not expr_name:
|
||||
result["error"] = "Expression name is required"
|
||||
return result
|
||||
|
||||
if expr_value is None:
|
||||
result["error"] = "Expression value is required"
|
||||
return result
|
||||
|
||||
try:
|
||||
# Find the expression
|
||||
found_expr = None
|
||||
for expr in part.Expressions:
|
||||
if expr.Name == expr_name:
|
||||
found_expr = expr
|
||||
break
|
||||
|
||||
if found_expr is None:
|
||||
result["error"] = f"Expression not found: {expr_name}"
|
||||
return result
|
||||
|
||||
# Set undo mark
|
||||
mark_id = session.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Edit Expression")
|
||||
|
||||
# Edit the expression
|
||||
# The value should be a string for the RHS
|
||||
new_rhs = str(expr_value)
|
||||
part.Expressions.Edit(found_expr, new_rhs)
|
||||
|
||||
# Update the model
|
||||
session.UpdateManager.DoUpdate(mark_id)
|
||||
|
||||
result["success"] = True
|
||||
result["data"] = {
|
||||
"name": expr_name,
|
||||
"old_value": found_expr.Value, # Note: this might be the new value after edit
|
||||
"new_rhs": new_rhs,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
result["error"] = str(e)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def set_multiple_expressions(session, part, expr_dict):
|
||||
"""Set multiple expressions at once.
|
||||
|
||||
Args:
|
||||
session: NX session
|
||||
part: NX part
|
||||
expr_dict: Dict of expression name -> value
|
||||
"""
|
||||
result = {"success": False, "error": None, "data": {}}
|
||||
|
||||
if not expr_dict:
|
||||
result["error"] = "No expressions provided"
|
||||
return result
|
||||
|
||||
try:
|
||||
# Set undo mark for all changes
|
||||
mark_id = session.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Edit Multiple Expressions")
|
||||
|
||||
updated = []
|
||||
errors = []
|
||||
|
||||
for expr_name, expr_value in expr_dict.items():
|
||||
# Find the expression
|
||||
found_expr = None
|
||||
for expr in part.Expressions:
|
||||
if expr.Name == expr_name:
|
||||
found_expr = expr
|
||||
break
|
||||
|
||||
if found_expr is None:
|
||||
errors.append(f"Expression not found: {expr_name}")
|
||||
continue
|
||||
|
||||
try:
|
||||
# Edit the expression
|
||||
new_rhs = str(expr_value)
|
||||
part.Expressions.Edit(found_expr, new_rhs)
|
||||
updated.append({"name": expr_name, "value": expr_value})
|
||||
except Exception as e:
|
||||
errors.append(f"Failed to set {expr_name}: {str(e)}")
|
||||
|
||||
# Update the model
|
||||
session.UpdateManager.DoUpdate(mark_id)
|
||||
|
||||
result["success"] = len(errors) == 0
|
||||
result["data"] = {
|
||||
"updated": updated,
|
||||
"errors": errors,
|
||||
"update_count": len(updated),
|
||||
"error_count": len(errors),
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
result["error"] = str(e)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
'''
|
||||
|
||||
|
||||
def _get_run_journal_exe() -> str:
|
||||
"""Get the path to run_journal.exe."""
|
||||
return os.path.join(NX_BIN_PATH, "run_journal.exe")
|
||||
|
||||
|
||||
def _run_journal(journal_path: str, args: list) -> Tuple[bool, str]:
|
||||
"""Run an NX journal with arguments.
|
||||
|
||||
Returns:
|
||||
Tuple of (success, output_or_error)
|
||||
"""
|
||||
run_journal = _get_run_journal_exe()
|
||||
|
||||
if not os.path.exists(run_journal):
|
||||
return False, f"run_journal.exe not found at {run_journal}"
|
||||
|
||||
cmd = [run_journal, journal_path, "-args"] + args
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120 # 2 minute timeout
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
return False, f"Journal execution failed: {result.stderr}"
|
||||
|
||||
return True, result.stdout
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
return False, "Journal execution timed out"
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
|
||||
def _execute_expression_operation(
|
||||
operation: str,
|
||||
part_path: str,
|
||||
extra_args: list = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Execute an expression operation via NX journal.
|
||||
|
||||
Args:
|
||||
operation: The operation to perform (get_all, get, set, set_multiple)
|
||||
part_path: Path to the part file
|
||||
extra_args: Additional arguments for the operation
|
||||
|
||||
Returns:
|
||||
Dict with operation result
|
||||
"""
|
||||
# Create temporary journal file
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode='w',
|
||||
suffix='.py',
|
||||
delete=False
|
||||
) as journal_file:
|
||||
journal_file.write(EXPRESSION_OPERATIONS_JOURNAL)
|
||||
journal_path = journal_file.name
|
||||
|
||||
# Create temporary output file
|
||||
output_file = tempfile.NamedTemporaryFile(
|
||||
mode='w',
|
||||
suffix='.json',
|
||||
delete=False
|
||||
).name
|
||||
|
||||
try:
|
||||
# Build arguments
|
||||
args = [operation, part_path, output_file]
|
||||
if extra_args:
|
||||
args.extend(extra_args)
|
||||
|
||||
# Run the journal
|
||||
success, output = _run_journal(journal_path, args)
|
||||
|
||||
if not success:
|
||||
return {"success": False, "error": output, "data": {}}
|
||||
|
||||
# Read the result
|
||||
if os.path.exists(output_file):
|
||||
with open(output_file, 'r') as f:
|
||||
return json.load(f)
|
||||
else:
|
||||
return {"success": False, "error": "Output file not created", "data": {}}
|
||||
|
||||
finally:
|
||||
# Cleanup temporary files
|
||||
if os.path.exists(journal_path):
|
||||
os.unlink(journal_path)
|
||||
if os.path.exists(output_file):
|
||||
os.unlink(output_file)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Public API
|
||||
# =============================================================================
|
||||
|
||||
def get_expressions(part_path: str) -> Dict[str, Any]:
|
||||
"""Get all expressions from an NX part.
|
||||
|
||||
Args:
|
||||
part_path: Full path to the .prt file
|
||||
|
||||
Returns:
|
||||
Dict with keys:
|
||||
- success: bool
|
||||
- error: Optional error message
|
||||
- data: Dict with count and expressions dict
|
||||
Each expression has: name, value, rhs, units, type
|
||||
|
||||
Example:
|
||||
>>> result = get_expressions("C:/models/bracket.prt")
|
||||
>>> if result["success"]:
|
||||
... for name, expr in result["data"]["expressions"].items():
|
||||
... print(f"{name} = {expr['value']} {expr['units']}")
|
||||
"""
|
||||
part_path = os.path.abspath(part_path)
|
||||
|
||||
if not os.path.exists(part_path):
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Part file not found: {part_path}",
|
||||
"data": {}
|
||||
}
|
||||
|
||||
return _execute_expression_operation("get_all", part_path)
|
||||
|
||||
|
||||
def get_expression(part_path: str, expression_name: str) -> Dict[str, Any]:
|
||||
"""Get a specific expression from an NX part.
|
||||
|
||||
Args:
|
||||
part_path: Full path to the .prt file
|
||||
expression_name: Name of the expression
|
||||
|
||||
Returns:
|
||||
Dict with keys:
|
||||
- success: bool
|
||||
- error: Optional error message
|
||||
- data: Dict with name, value, rhs, units, type
|
||||
|
||||
Example:
|
||||
>>> result = get_expression("C:/models/bracket.prt", "thickness")
|
||||
>>> if result["success"]:
|
||||
... print(f"thickness = {result['data']['value']}")
|
||||
"""
|
||||
part_path = os.path.abspath(part_path)
|
||||
|
||||
if not os.path.exists(part_path):
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Part file not found: {part_path}",
|
||||
"data": {}
|
||||
}
|
||||
|
||||
return _execute_expression_operation("get", part_path, [expression_name])
|
||||
|
||||
|
||||
def set_expression(
|
||||
part_path: str,
|
||||
expression_name: str,
|
||||
value: Union[float, int, str]
|
||||
) -> Dict[str, Any]:
|
||||
"""Set an expression value in an NX part.
|
||||
|
||||
Args:
|
||||
part_path: Full path to the .prt file
|
||||
expression_name: Name of the expression
|
||||
value: New value (will be converted to string for RHS)
|
||||
|
||||
Returns:
|
||||
Dict with keys:
|
||||
- success: bool
|
||||
- error: Optional error message
|
||||
- data: Dict with name, old_value, new_rhs
|
||||
|
||||
Example:
|
||||
>>> result = set_expression("C:/models/bracket.prt", "thickness", 5.0)
|
||||
>>> if result["success"]:
|
||||
... print("Expression updated!")
|
||||
"""
|
||||
part_path = os.path.abspath(part_path)
|
||||
|
||||
if not os.path.exists(part_path):
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Part file not found: {part_path}",
|
||||
"data": {}
|
||||
}
|
||||
|
||||
return _execute_expression_operation(
|
||||
"set",
|
||||
part_path,
|
||||
[expression_name, str(value)]
|
||||
)
|
||||
|
||||
|
||||
def set_expressions(
|
||||
part_path: str,
|
||||
expressions: Dict[str, Union[float, int, str]]
|
||||
) -> Dict[str, Any]:
|
||||
"""Set multiple expressions in an NX part.
|
||||
|
||||
Args:
|
||||
part_path: Full path to the .prt file
|
||||
expressions: Dict mapping expression names to values
|
||||
|
||||
Returns:
|
||||
Dict with keys:
|
||||
- success: bool
|
||||
- error: Optional error message
|
||||
- data: Dict with updated list, errors list, counts
|
||||
|
||||
Example:
|
||||
>>> result = set_expressions("C:/models/bracket.prt", {
|
||||
... "thickness": 5.0,
|
||||
... "width": 10.0,
|
||||
... "height": 15.0
|
||||
... })
|
||||
>>> if result["success"]:
|
||||
... print(f"Updated {result['data']['update_count']} expressions")
|
||||
"""
|
||||
part_path = os.path.abspath(part_path)
|
||||
|
||||
if not os.path.exists(part_path):
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Part file not found: {part_path}",
|
||||
"data": {}
|
||||
}
|
||||
|
||||
# Convert expressions dict to JSON string
|
||||
expr_json = json.dumps(expressions)
|
||||
|
||||
return _execute_expression_operation(
|
||||
"set_multiple",
|
||||
part_path,
|
||||
[expr_json]
|
||||
)
|
||||
711
optimization_engine/hooks/nx_cad/feature_manager.py
Normal file
711
optimization_engine/hooks/nx_cad/feature_manager.py
Normal file
@@ -0,0 +1,711 @@
|
||||
"""
|
||||
NX Feature Manager Hook
|
||||
=======================
|
||||
|
||||
Provides Python functions to manage NX features (suppress, unsuppress, etc.).
|
||||
|
||||
API Reference (verified via Siemens MCP docs):
|
||||
- Part.Features() -> FeatureCollection
|
||||
- Feature.Suppress() -> Suppresses the feature
|
||||
- Feature.Unsuppress() -> Unsuppresses the feature
|
||||
- Feature.Name, Feature.IsSuppressed
|
||||
- Session.UpdateManager.DoUpdate() -> Update the model
|
||||
|
||||
Usage:
|
||||
from optimization_engine.hooks.nx_cad import feature_manager
|
||||
|
||||
# Get all features
|
||||
result = feature_manager.get_features("C:/path/to/part.prt")
|
||||
|
||||
# Suppress a feature
|
||||
result = feature_manager.suppress_feature("C:/path/to/part.prt", "HOLE(1)")
|
||||
|
||||
# Unsuppress a feature
|
||||
result = feature_manager.unsuppress_feature("C:/path/to/part.prt", "HOLE(1)")
|
||||
|
||||
# Get feature status
|
||||
result = feature_manager.get_feature_status("C:/path/to/part.prt", "HOLE(1)")
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any, List, Tuple
|
||||
|
||||
# NX installation path (configurable)
|
||||
NX_BIN_PATH = os.environ.get(
|
||||
"NX_BIN_PATH",
|
||||
r"C:\Program Files\Siemens\NX2506\NXBIN"
|
||||
)
|
||||
|
||||
# Journal template for feature operations
|
||||
FEATURE_OPERATIONS_JOURNAL = '''
|
||||
# NX Open Python Journal - Feature Operations
|
||||
# Auto-generated by Atomizer hooks
|
||||
#
|
||||
# Based on Siemens NX Open Python API:
|
||||
# - Part.Features()
|
||||
# - Feature.Suppress() / Feature.Unsuppress()
|
||||
# - Feature.Name, Feature.IsSuppressed
|
||||
|
||||
import NXOpen
|
||||
import NXOpen.Features
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
|
||||
def main():
|
||||
"""Execute feature operation based on command arguments."""
|
||||
# Get the NX session
|
||||
session = NXOpen.Session.GetSession()
|
||||
|
||||
# Parse arguments: operation, part_path, output_json, [extra_args...]
|
||||
args = sys.argv[1:] if len(sys.argv) > 1 else []
|
||||
|
||||
if len(args) < 3:
|
||||
raise ValueError("Usage: script.py <operation> <part_path> <output_json> [args...]")
|
||||
|
||||
operation = args[0]
|
||||
part_path = args[1]
|
||||
output_json = args[2]
|
||||
extra_args = args[3:] if len(args) > 3 else []
|
||||
|
||||
result = {"success": False, "error": None, "data": {}}
|
||||
|
||||
try:
|
||||
# Ensure part is open
|
||||
part = ensure_part_open(session, part_path)
|
||||
|
||||
if part is None:
|
||||
result["error"] = f"Failed to open part: {part_path}"
|
||||
elif operation == "get_all":
|
||||
result = get_all_features(part)
|
||||
elif operation == "get_status":
|
||||
feature_name = extra_args[0] if extra_args else None
|
||||
result = get_feature_status(part, feature_name)
|
||||
elif operation == "suppress":
|
||||
feature_name = extra_args[0] if extra_args else None
|
||||
result = suppress_feature(session, part, feature_name)
|
||||
elif operation == "unsuppress":
|
||||
feature_name = extra_args[0] if extra_args else None
|
||||
result = unsuppress_feature(session, part, feature_name)
|
||||
elif operation == "suppress_multiple":
|
||||
feature_names = json.loads(extra_args[0]) if extra_args else []
|
||||
result = suppress_multiple_features(session, part, feature_names)
|
||||
elif operation == "unsuppress_multiple":
|
||||
feature_names = json.loads(extra_args[0]) if extra_args else []
|
||||
result = unsuppress_multiple_features(session, part, feature_names)
|
||||
else:
|
||||
result["error"] = f"Unknown operation: {operation}"
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
result["error"] = str(e)
|
||||
result["traceback"] = traceback.format_exc()
|
||||
|
||||
# Write result to output JSON
|
||||
with open(output_json, 'w') as f:
|
||||
json.dump(result, f, indent=2)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def ensure_part_open(session, part_path):
|
||||
"""Ensure the part is open and return it."""
|
||||
# Check if already open
|
||||
part_path_normalized = os.path.normpath(part_path).lower()
|
||||
|
||||
for part in session.Parts:
|
||||
if os.path.normpath(part.FullPath).lower() == part_path_normalized:
|
||||
return part
|
||||
|
||||
# Need to open it
|
||||
if not os.path.exists(part_path):
|
||||
return None
|
||||
|
||||
try:
|
||||
# Set load options for the working directory
|
||||
working_dir = os.path.dirname(part_path)
|
||||
session.Parts.LoadOptions.ComponentLoadMethod = NXOpen.LoadOptions.LoadMethod.FromDirectory
|
||||
session.Parts.LoadOptions.SetSearchDirectories([working_dir], [True])
|
||||
|
||||
# Use OpenActiveDisplay instead of OpenBase for better compatibility
|
||||
part, load_status = session.Parts.OpenActiveDisplay(
|
||||
part_path,
|
||||
NXOpen.DisplayPartOption.AllowAdditional
|
||||
)
|
||||
load_status.Dispose()
|
||||
return part
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
def find_feature_by_name(part, feature_name):
|
||||
"""Find a feature by name."""
|
||||
for feature in part.Features:
|
||||
if feature.Name == feature_name:
|
||||
return feature
|
||||
return None
|
||||
|
||||
|
||||
def get_all_features(part):
|
||||
"""Get all features from a part.
|
||||
|
||||
NX Open API: Part.Features()
|
||||
"""
|
||||
result = {"success": False, "error": None, "data": {}}
|
||||
|
||||
try:
|
||||
features = []
|
||||
|
||||
for feature in part.Features:
|
||||
try:
|
||||
feature_data = {
|
||||
"name": feature.Name,
|
||||
"type": feature.FeatureType,
|
||||
"is_suppressed": feature.IsSuppressed,
|
||||
"is_internal": feature.IsInternal,
|
||||
}
|
||||
features.append(feature_data)
|
||||
except:
|
||||
# Skip features that can't be read
|
||||
pass
|
||||
|
||||
result["success"] = True
|
||||
result["data"] = {
|
||||
"count": len(features),
|
||||
"suppressed_count": sum(1 for f in features if f["is_suppressed"]),
|
||||
"features": features
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
result["error"] = str(e)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_feature_status(part, feature_name):
|
||||
"""Get status of a specific feature.
|
||||
|
||||
NX Open API: Feature properties
|
||||
"""
|
||||
result = {"success": False, "error": None, "data": {}}
|
||||
|
||||
if not feature_name:
|
||||
result["error"] = "Feature name is required"
|
||||
return result
|
||||
|
||||
try:
|
||||
feature = find_feature_by_name(part, feature_name)
|
||||
|
||||
if feature is None:
|
||||
result["error"] = f"Feature not found: {feature_name}"
|
||||
return result
|
||||
|
||||
result["success"] = True
|
||||
result["data"] = {
|
||||
"name": feature.Name,
|
||||
"type": feature.FeatureType,
|
||||
"is_suppressed": feature.IsSuppressed,
|
||||
"is_internal": feature.IsInternal,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
result["error"] = str(e)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def suppress_feature(session, part, feature_name):
|
||||
"""Suppress a feature.
|
||||
|
||||
NX Open API: Feature.Suppress()
|
||||
"""
|
||||
result = {"success": False, "error": None, "data": {}}
|
||||
|
||||
if not feature_name:
|
||||
result["error"] = "Feature name is required"
|
||||
return result
|
||||
|
||||
try:
|
||||
feature = find_feature_by_name(part, feature_name)
|
||||
|
||||
if feature is None:
|
||||
result["error"] = f"Feature not found: {feature_name}"
|
||||
return result
|
||||
|
||||
if feature.IsSuppressed:
|
||||
result["success"] = True
|
||||
result["data"] = {
|
||||
"name": feature_name,
|
||||
"action": "already_suppressed",
|
||||
"is_suppressed": True
|
||||
}
|
||||
return result
|
||||
|
||||
# Set undo mark
|
||||
mark_id = session.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Suppress Feature")
|
||||
|
||||
# Suppress the feature
|
||||
feature.Suppress()
|
||||
|
||||
# Update the model
|
||||
session.UpdateManager.DoUpdate(mark_id)
|
||||
|
||||
result["success"] = True
|
||||
result["data"] = {
|
||||
"name": feature_name,
|
||||
"action": "suppressed",
|
||||
"is_suppressed": True
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
result["error"] = str(e)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def unsuppress_feature(session, part, feature_name):
|
||||
"""Unsuppress a feature.
|
||||
|
||||
NX Open API: Feature.Unsuppress()
|
||||
"""
|
||||
result = {"success": False, "error": None, "data": {}}
|
||||
|
||||
if not feature_name:
|
||||
result["error"] = "Feature name is required"
|
||||
return result
|
||||
|
||||
try:
|
||||
feature = find_feature_by_name(part, feature_name)
|
||||
|
||||
if feature is None:
|
||||
result["error"] = f"Feature not found: {feature_name}"
|
||||
return result
|
||||
|
||||
if not feature.IsSuppressed:
|
||||
result["success"] = True
|
||||
result["data"] = {
|
||||
"name": feature_name,
|
||||
"action": "already_unsuppressed",
|
||||
"is_suppressed": False
|
||||
}
|
||||
return result
|
||||
|
||||
# Set undo mark
|
||||
mark_id = session.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Unsuppress Feature")
|
||||
|
||||
# Unsuppress the feature
|
||||
feature.Unsuppress()
|
||||
|
||||
# Update the model
|
||||
session.UpdateManager.DoUpdate(mark_id)
|
||||
|
||||
result["success"] = True
|
||||
result["data"] = {
|
||||
"name": feature_name,
|
||||
"action": "unsuppressed",
|
||||
"is_suppressed": False
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
result["error"] = str(e)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def suppress_multiple_features(session, part, feature_names):
|
||||
"""Suppress multiple features.
|
||||
|
||||
Args:
|
||||
session: NX session
|
||||
part: NX part
|
||||
feature_names: List of feature names to suppress
|
||||
"""
|
||||
result = {"success": False, "error": None, "data": {}}
|
||||
|
||||
if not feature_names:
|
||||
result["error"] = "No feature names provided"
|
||||
return result
|
||||
|
||||
try:
|
||||
# Set undo mark for all changes
|
||||
mark_id = session.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Suppress Multiple Features")
|
||||
|
||||
suppressed = []
|
||||
errors = []
|
||||
|
||||
for feature_name in feature_names:
|
||||
feature = find_feature_by_name(part, feature_name)
|
||||
|
||||
if feature is None:
|
||||
errors.append(f"Feature not found: {feature_name}")
|
||||
continue
|
||||
|
||||
try:
|
||||
if not feature.IsSuppressed:
|
||||
feature.Suppress()
|
||||
suppressed.append(feature_name)
|
||||
except Exception as e:
|
||||
errors.append(f"Failed to suppress {feature_name}: {str(e)}")
|
||||
|
||||
# Update the model
|
||||
session.UpdateManager.DoUpdate(mark_id)
|
||||
|
||||
result["success"] = len(errors) == 0
|
||||
result["data"] = {
|
||||
"suppressed": suppressed,
|
||||
"errors": errors,
|
||||
"suppressed_count": len(suppressed),
|
||||
"error_count": len(errors),
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
result["error"] = str(e)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def unsuppress_multiple_features(session, part, feature_names):
|
||||
"""Unsuppress multiple features.
|
||||
|
||||
Args:
|
||||
session: NX session
|
||||
part: NX part
|
||||
feature_names: List of feature names to unsuppress
|
||||
"""
|
||||
result = {"success": False, "error": None, "data": {}}
|
||||
|
||||
if not feature_names:
|
||||
result["error"] = "No feature names provided"
|
||||
return result
|
||||
|
||||
try:
|
||||
# Set undo mark for all changes
|
||||
mark_id = session.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Unsuppress Multiple Features")
|
||||
|
||||
unsuppressed = []
|
||||
errors = []
|
||||
|
||||
for feature_name in feature_names:
|
||||
feature = find_feature_by_name(part, feature_name)
|
||||
|
||||
if feature is None:
|
||||
errors.append(f"Feature not found: {feature_name}")
|
||||
continue
|
||||
|
||||
try:
|
||||
if feature.IsSuppressed:
|
||||
feature.Unsuppress()
|
||||
unsuppressed.append(feature_name)
|
||||
except Exception as e:
|
||||
errors.append(f"Failed to unsuppress {feature_name}: {str(e)}")
|
||||
|
||||
# Update the model
|
||||
session.UpdateManager.DoUpdate(mark_id)
|
||||
|
||||
result["success"] = len(errors) == 0
|
||||
result["data"] = {
|
||||
"unsuppressed": unsuppressed,
|
||||
"errors": errors,
|
||||
"unsuppressed_count": len(unsuppressed),
|
||||
"error_count": len(errors),
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
result["error"] = str(e)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
'''
|
||||
|
||||
|
||||
def _get_run_journal_exe() -> str:
|
||||
"""Get the path to run_journal.exe."""
|
||||
return os.path.join(NX_BIN_PATH, "run_journal.exe")
|
||||
|
||||
|
||||
def _run_journal(journal_path: str, args: list) -> Tuple[bool, str]:
|
||||
"""Run an NX journal with arguments.
|
||||
|
||||
Returns:
|
||||
Tuple of (success, output_or_error)
|
||||
"""
|
||||
run_journal = _get_run_journal_exe()
|
||||
|
||||
if not os.path.exists(run_journal):
|
||||
return False, f"run_journal.exe not found at {run_journal}"
|
||||
|
||||
cmd = [run_journal, journal_path, "-args"] + args
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120 # 2 minute timeout
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
return False, f"Journal execution failed: {result.stderr}"
|
||||
|
||||
return True, result.stdout
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
return False, "Journal execution timed out"
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
|
||||
def _execute_feature_operation(
|
||||
operation: str,
|
||||
part_path: str,
|
||||
extra_args: list = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Execute a feature operation via NX journal.
|
||||
|
||||
Args:
|
||||
operation: The operation to perform
|
||||
part_path: Path to the part file
|
||||
extra_args: Additional arguments for the operation
|
||||
|
||||
Returns:
|
||||
Dict with operation result
|
||||
"""
|
||||
# Create temporary journal file
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode='w',
|
||||
suffix='.py',
|
||||
delete=False
|
||||
) as journal_file:
|
||||
journal_file.write(FEATURE_OPERATIONS_JOURNAL)
|
||||
journal_path = journal_file.name
|
||||
|
||||
# Create temporary output file
|
||||
output_file = tempfile.NamedTemporaryFile(
|
||||
mode='w',
|
||||
suffix='.json',
|
||||
delete=False
|
||||
).name
|
||||
|
||||
try:
|
||||
# Build arguments
|
||||
args = [operation, part_path, output_file]
|
||||
if extra_args:
|
||||
args.extend(extra_args)
|
||||
|
||||
# Run the journal
|
||||
success, output = _run_journal(journal_path, args)
|
||||
|
||||
if not success:
|
||||
return {"success": False, "error": output, "data": {}}
|
||||
|
||||
# Read the result
|
||||
if os.path.exists(output_file):
|
||||
with open(output_file, 'r') as f:
|
||||
return json.load(f)
|
||||
else:
|
||||
return {"success": False, "error": "Output file not created", "data": {}}
|
||||
|
||||
finally:
|
||||
# Cleanup temporary files
|
||||
if os.path.exists(journal_path):
|
||||
os.unlink(journal_path)
|
||||
if os.path.exists(output_file):
|
||||
os.unlink(output_file)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Public API
|
||||
# =============================================================================
|
||||
|
||||
def get_features(part_path: str) -> Dict[str, Any]:
|
||||
"""Get all features from an NX part.
|
||||
|
||||
Args:
|
||||
part_path: Full path to the .prt file
|
||||
|
||||
Returns:
|
||||
Dict with keys:
|
||||
- success: bool
|
||||
- error: Optional error message
|
||||
- data: Dict with count, suppressed_count, features list
|
||||
|
||||
Example:
|
||||
>>> result = get_features("C:/models/bracket.prt")
|
||||
>>> if result["success"]:
|
||||
... for f in result["data"]["features"]:
|
||||
... status = "suppressed" if f["is_suppressed"] else "active"
|
||||
... print(f"{f['name']} ({f['type']}): {status}")
|
||||
"""
|
||||
part_path = os.path.abspath(part_path)
|
||||
|
||||
if not os.path.exists(part_path):
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Part file not found: {part_path}",
|
||||
"data": {}
|
||||
}
|
||||
|
||||
return _execute_feature_operation("get_all", part_path)
|
||||
|
||||
|
||||
def get_feature_status(part_path: str, feature_name: str) -> Dict[str, Any]:
|
||||
"""Get status of a specific feature.
|
||||
|
||||
Args:
|
||||
part_path: Full path to the .prt file
|
||||
feature_name: Name of the feature
|
||||
|
||||
Returns:
|
||||
Dict with keys:
|
||||
- success: bool
|
||||
- error: Optional error message
|
||||
- data: Dict with name, type, is_suppressed, is_internal
|
||||
|
||||
Example:
|
||||
>>> result = get_feature_status("C:/models/bracket.prt", "HOLE(1)")
|
||||
>>> if result["success"]:
|
||||
... print(f"Suppressed: {result['data']['is_suppressed']}")
|
||||
"""
|
||||
part_path = os.path.abspath(part_path)
|
||||
|
||||
if not os.path.exists(part_path):
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Part file not found: {part_path}",
|
||||
"data": {}
|
||||
}
|
||||
|
||||
return _execute_feature_operation("get_status", part_path, [feature_name])
|
||||
|
||||
|
||||
def suppress_feature(part_path: str, feature_name: str) -> Dict[str, Any]:
|
||||
"""Suppress a feature in an NX part.
|
||||
|
||||
Args:
|
||||
part_path: Full path to the .prt file
|
||||
feature_name: Name of the feature to suppress
|
||||
|
||||
Returns:
|
||||
Dict with keys:
|
||||
- success: bool
|
||||
- error: Optional error message
|
||||
- data: Dict with name, action, is_suppressed
|
||||
|
||||
Example:
|
||||
>>> result = suppress_feature("C:/models/bracket.prt", "HOLE(1)")
|
||||
>>> if result["success"]:
|
||||
... print(f"Feature {result['data']['action']}")
|
||||
"""
|
||||
part_path = os.path.abspath(part_path)
|
||||
|
||||
if not os.path.exists(part_path):
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Part file not found: {part_path}",
|
||||
"data": {}
|
||||
}
|
||||
|
||||
return _execute_feature_operation("suppress", part_path, [feature_name])
|
||||
|
||||
|
||||
def unsuppress_feature(part_path: str, feature_name: str) -> Dict[str, Any]:
|
||||
"""Unsuppress a feature in an NX part.
|
||||
|
||||
Args:
|
||||
part_path: Full path to the .prt file
|
||||
feature_name: Name of the feature to unsuppress
|
||||
|
||||
Returns:
|
||||
Dict with keys:
|
||||
- success: bool
|
||||
- error: Optional error message
|
||||
- data: Dict with name, action, is_suppressed
|
||||
|
||||
Example:
|
||||
>>> result = unsuppress_feature("C:/models/bracket.prt", "HOLE(1)")
|
||||
>>> if result["success"]:
|
||||
... print(f"Feature {result['data']['action']}")
|
||||
"""
|
||||
part_path = os.path.abspath(part_path)
|
||||
|
||||
if not os.path.exists(part_path):
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Part file not found: {part_path}",
|
||||
"data": {}
|
||||
}
|
||||
|
||||
return _execute_feature_operation("unsuppress", part_path, [feature_name])
|
||||
|
||||
|
||||
def suppress_features(part_path: str, feature_names: List[str]) -> Dict[str, Any]:
|
||||
"""Suppress multiple features in an NX part.
|
||||
|
||||
Args:
|
||||
part_path: Full path to the .prt file
|
||||
feature_names: List of feature names to suppress
|
||||
|
||||
Returns:
|
||||
Dict with keys:
|
||||
- success: bool
|
||||
- error: Optional error message
|
||||
- data: Dict with suppressed list, errors list, counts
|
||||
|
||||
Example:
|
||||
>>> result = suppress_features("C:/models/bracket.prt", ["HOLE(1)", "HOLE(2)"])
|
||||
>>> if result["success"]:
|
||||
... print(f"Suppressed {result['data']['suppressed_count']} features")
|
||||
"""
|
||||
part_path = os.path.abspath(part_path)
|
||||
|
||||
if not os.path.exists(part_path):
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Part file not found: {part_path}",
|
||||
"data": {}
|
||||
}
|
||||
|
||||
# Convert list to JSON string
|
||||
names_json = json.dumps(feature_names)
|
||||
|
||||
return _execute_feature_operation("suppress_multiple", part_path, [names_json])
|
||||
|
||||
|
||||
def unsuppress_features(part_path: str, feature_names: List[str]) -> Dict[str, Any]:
|
||||
"""Unsuppress multiple features in an NX part.
|
||||
|
||||
Args:
|
||||
part_path: Full path to the .prt file
|
||||
feature_names: List of feature names to unsuppress
|
||||
|
||||
Returns:
|
||||
Dict with keys:
|
||||
- success: bool
|
||||
- error: Optional error message
|
||||
- data: Dict with unsuppressed list, errors list, counts
|
||||
|
||||
Example:
|
||||
>>> result = unsuppress_features("C:/models/bracket.prt", ["HOLE(1)", "HOLE(2)"])
|
||||
>>> if result["success"]:
|
||||
... print(f"Unsuppressed {result['data']['unsuppressed_count']} features")
|
||||
"""
|
||||
part_path = os.path.abspath(part_path)
|
||||
|
||||
if not os.path.exists(part_path):
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Part file not found: {part_path}",
|
||||
"data": {}
|
||||
}
|
||||
|
||||
# Convert list to JSON string
|
||||
names_json = json.dumps(feature_names)
|
||||
|
||||
return _execute_feature_operation("unsuppress_multiple", part_path, [names_json])
|
||||
667
optimization_engine/hooks/nx_cad/geometry_query.py
Normal file
667
optimization_engine/hooks/nx_cad/geometry_query.py
Normal file
@@ -0,0 +1,667 @@
|
||||
"""
|
||||
NX Geometry Query Hook
|
||||
======================
|
||||
|
||||
Provides Python functions to query geometry properties from NX parts.
|
||||
|
||||
API Reference (verified via Siemens MCP docs):
|
||||
- Part.MeasureManager() -> Returns measure manager for this part
|
||||
- MeasureManager.NewMassProperties() -> Create mass properties measurement
|
||||
- Part.Bodies() -> BodyCollection (solid bodies in the part)
|
||||
- Body.GetPhysicalMaterial() -> Get material assigned to body
|
||||
|
||||
Usage:
|
||||
from optimization_engine.hooks.nx_cad import geometry_query
|
||||
|
||||
# Get mass properties
|
||||
result = geometry_query.get_mass_properties("C:/path/to/part.prt")
|
||||
|
||||
# Get body info
|
||||
result = geometry_query.get_bodies("C:/path/to/part.prt")
|
||||
|
||||
# Get volume
|
||||
result = geometry_query.get_volume("C:/path/to/part.prt")
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any, List, Tuple
|
||||
|
||||
# NX installation path (configurable)
|
||||
NX_BIN_PATH = os.environ.get(
|
||||
"NX_BIN_PATH",
|
||||
r"C:\Program Files\Siemens\NX2506\NXBIN"
|
||||
)
|
||||
|
||||
# Journal template for geometry query operations
|
||||
GEOMETRY_QUERY_JOURNAL = '''
|
||||
# NX Open Python Journal - Geometry Query Operations
|
||||
# Auto-generated by Atomizer hooks
|
||||
#
|
||||
# Based on Siemens NX Open Python API:
|
||||
# - MeasureManager.NewMassProperties()
|
||||
# - BodyCollection
|
||||
# - Body.GetPhysicalMaterial()
|
||||
|
||||
import NXOpen
|
||||
import NXOpen.UF
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
import math
|
||||
|
||||
def main():
|
||||
"""Execute geometry query operation based on command arguments."""
|
||||
# Get the NX session
|
||||
session = NXOpen.Session.GetSession()
|
||||
|
||||
# Parse arguments: operation, part_path, output_json, [extra_args...]
|
||||
args = sys.argv[1:] if len(sys.argv) > 1 else []
|
||||
|
||||
if len(args) < 3:
|
||||
raise ValueError("Usage: script.py <operation> <part_path> <output_json> [args...]")
|
||||
|
||||
operation = args[0]
|
||||
part_path = args[1]
|
||||
output_json = args[2]
|
||||
extra_args = args[3:] if len(args) > 3 else []
|
||||
|
||||
result = {"success": False, "error": None, "data": {}}
|
||||
|
||||
try:
|
||||
# Ensure part is open
|
||||
part = ensure_part_open(session, part_path)
|
||||
|
||||
if part is None:
|
||||
result["error"] = f"Failed to open part: {part_path}"
|
||||
elif operation == "mass_properties":
|
||||
result = get_mass_properties(part)
|
||||
elif operation == "bodies":
|
||||
result = get_bodies(part)
|
||||
elif operation == "volume":
|
||||
result = get_volume(part)
|
||||
elif operation == "surface_area":
|
||||
result = get_surface_area(part)
|
||||
elif operation == "material":
|
||||
result = get_material(part)
|
||||
else:
|
||||
result["error"] = f"Unknown operation: {operation}"
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
result["error"] = str(e)
|
||||
result["traceback"] = traceback.format_exc()
|
||||
|
||||
# Write result to output JSON
|
||||
with open(output_json, 'w') as f:
|
||||
json.dump(result, f, indent=2)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def ensure_part_open(session, part_path):
|
||||
"""Ensure the part is open and return it."""
|
||||
# Check if already open
|
||||
part_path_normalized = os.path.normpath(part_path).lower()
|
||||
|
||||
for part in session.Parts:
|
||||
if os.path.normpath(part.FullPath).lower() == part_path_normalized:
|
||||
return part
|
||||
|
||||
# Need to open it
|
||||
if not os.path.exists(part_path):
|
||||
return None
|
||||
|
||||
try:
|
||||
# Set load options for the working directory
|
||||
working_dir = os.path.dirname(part_path)
|
||||
session.Parts.LoadOptions.ComponentLoadMethod = NXOpen.LoadOptions.LoadMethod.FromDirectory
|
||||
session.Parts.LoadOptions.SetSearchDirectories([working_dir], [True])
|
||||
|
||||
# Use OpenActiveDisplay instead of OpenBase for better compatibility
|
||||
part, load_status = session.Parts.OpenActiveDisplay(
|
||||
part_path,
|
||||
NXOpen.DisplayPartOption.AllowAdditional
|
||||
)
|
||||
load_status.Dispose()
|
||||
return part
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
def get_solid_bodies(part):
|
||||
"""Get all solid bodies from a part."""
|
||||
solid_bodies = []
|
||||
for body in part.Bodies:
|
||||
if body.IsSolidBody:
|
||||
solid_bodies.append(body)
|
||||
return solid_bodies
|
||||
|
||||
|
||||
def get_mass_properties(part):
|
||||
"""Get mass properties from a part.
|
||||
|
||||
NX Open API: MeasureManager.NewMassProperties()
|
||||
|
||||
Returns mass, volume, surface area, centroid, and inertia properties.
|
||||
"""
|
||||
result = {"success": False, "error": None, "data": {}}
|
||||
|
||||
try:
|
||||
# Get solid bodies
|
||||
solid_bodies = get_solid_bodies(part)
|
||||
|
||||
if not solid_bodies:
|
||||
result["error"] = "No solid bodies found in part"
|
||||
return result
|
||||
|
||||
# Get measure manager
|
||||
measure_manager = part.MeasureManager
|
||||
|
||||
# Get units - use base units array like the working journal
|
||||
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)
|
||||
mass_props = measure_manager.NewMassProperties(mass_units, 0.99, solid_bodies)
|
||||
|
||||
# Get properties
|
||||
mass = mass_props.Mass
|
||||
volume = mass_props.Volume
|
||||
area = mass_props.Area
|
||||
|
||||
# Get centroid
|
||||
centroid = mass_props.Centroid
|
||||
centroid_x = centroid.X
|
||||
centroid_y = centroid.Y
|
||||
centroid_z = centroid.Z
|
||||
|
||||
# Get principal moments of inertia (may not be available)
|
||||
ixx = 0.0
|
||||
iyy = 0.0
|
||||
izz = 0.0
|
||||
try:
|
||||
principal_moments = mass_props.PrincipalMomentsOfInertia
|
||||
ixx = principal_moments[0]
|
||||
iyy = principal_moments[1]
|
||||
izz = principal_moments[2]
|
||||
except:
|
||||
pass
|
||||
|
||||
# Get material info from first body via attributes
|
||||
material_name = None
|
||||
density = None
|
||||
try:
|
||||
# Try body attributes (NX stores material as attribute)
|
||||
attrs = solid_bodies[0].GetUserAttributes()
|
||||
for attr in attrs:
|
||||
if 'material' in attr.Title.lower():
|
||||
material_name = attr.StringValue
|
||||
break
|
||||
except:
|
||||
pass
|
||||
|
||||
result["success"] = True
|
||||
result["data"] = {
|
||||
"mass": mass,
|
||||
"mass_unit": "kg",
|
||||
"volume": volume,
|
||||
"volume_unit": "mm^3",
|
||||
"surface_area": area,
|
||||
"area_unit": "mm^2",
|
||||
"centroid": {
|
||||
"x": centroid_x,
|
||||
"y": centroid_y,
|
||||
"z": centroid_z,
|
||||
"unit": "mm"
|
||||
},
|
||||
"principal_moments": {
|
||||
"Ixx": ixx,
|
||||
"Iyy": iyy,
|
||||
"Izz": izz,
|
||||
"unit": "kg*mm^2"
|
||||
},
|
||||
"material": material_name,
|
||||
"density": density,
|
||||
"body_count": len(solid_bodies)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
result["error"] = str(e)
|
||||
result["traceback"] = traceback.format_exc()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_bodies(part):
|
||||
"""Get information about all bodies in the part.
|
||||
|
||||
NX Open API: Part.Bodies()
|
||||
"""
|
||||
result = {"success": False, "error": None, "data": {}}
|
||||
|
||||
try:
|
||||
bodies_info = []
|
||||
|
||||
for body in part.Bodies:
|
||||
body_data = {
|
||||
"name": body.Name if hasattr(body, 'Name') else None,
|
||||
"is_solid": body.IsSolidBody,
|
||||
"is_sheet": body.IsSheetBody,
|
||||
}
|
||||
|
||||
# Try to get material
|
||||
try:
|
||||
phys_mat = body.GetPhysicalMaterial()
|
||||
if phys_mat:
|
||||
body_data["material"] = phys_mat.Name
|
||||
except:
|
||||
body_data["material"] = None
|
||||
|
||||
bodies_info.append(body_data)
|
||||
|
||||
result["success"] = True
|
||||
result["data"] = {
|
||||
"count": len(bodies_info),
|
||||
"solid_count": sum(1 for b in bodies_info if b["is_solid"]),
|
||||
"sheet_count": sum(1 for b in bodies_info if b["is_sheet"]),
|
||||
"bodies": bodies_info
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
result["error"] = str(e)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_volume(part):
|
||||
"""Get total volume of all solid bodies.
|
||||
|
||||
NX Open API: MeasureManager.NewMassProperties()
|
||||
"""
|
||||
result = {"success": False, "error": None, "data": {}}
|
||||
|
||||
try:
|
||||
solid_bodies = get_solid_bodies(part)
|
||||
|
||||
if not solid_bodies:
|
||||
result["error"] = "No solid bodies found in part"
|
||||
return result
|
||||
|
||||
measure_manager = part.MeasureManager
|
||||
units = part.UnitCollection
|
||||
|
||||
body_array = [NXOpen.IBody.Wrap(body) for body in solid_bodies]
|
||||
|
||||
mass_props = measure_manager.NewMassProperties(
|
||||
units.FindObject("SquareMilliMeter"),
|
||||
units.FindObject("CubicMillimeter"),
|
||||
units.FindObject("Kilogram"),
|
||||
body_array
|
||||
)
|
||||
|
||||
result["success"] = True
|
||||
result["data"] = {
|
||||
"volume": mass_props.Volume,
|
||||
"unit": "mm^3",
|
||||
"body_count": len(solid_bodies)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
result["error"] = str(e)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_surface_area(part):
|
||||
"""Get total surface area of all solid bodies.
|
||||
|
||||
NX Open API: MeasureManager.NewMassProperties()
|
||||
"""
|
||||
result = {"success": False, "error": None, "data": {}}
|
||||
|
||||
try:
|
||||
solid_bodies = get_solid_bodies(part)
|
||||
|
||||
if not solid_bodies:
|
||||
result["error"] = "No solid bodies found in part"
|
||||
return result
|
||||
|
||||
measure_manager = part.MeasureManager
|
||||
units = part.UnitCollection
|
||||
|
||||
body_array = [NXOpen.IBody.Wrap(body) for body in solid_bodies]
|
||||
|
||||
mass_props = measure_manager.NewMassProperties(
|
||||
units.FindObject("SquareMilliMeter"),
|
||||
units.FindObject("CubicMillimeter"),
|
||||
units.FindObject("Kilogram"),
|
||||
body_array
|
||||
)
|
||||
|
||||
result["success"] = True
|
||||
result["data"] = {
|
||||
"surface_area": mass_props.Area,
|
||||
"unit": "mm^2",
|
||||
"body_count": len(solid_bodies)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
result["error"] = str(e)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_material(part):
|
||||
"""Get material information from bodies in the part.
|
||||
|
||||
NX Open API: Body.GetPhysicalMaterial()
|
||||
"""
|
||||
result = {"success": False, "error": None, "data": {}}
|
||||
|
||||
try:
|
||||
solid_bodies = get_solid_bodies(part)
|
||||
|
||||
if not solid_bodies:
|
||||
result["error"] = "No solid bodies found in part"
|
||||
return result
|
||||
|
||||
materials = {}
|
||||
|
||||
for body in solid_bodies:
|
||||
try:
|
||||
phys_mat = body.GetPhysicalMaterial()
|
||||
if phys_mat:
|
||||
mat_name = phys_mat.Name
|
||||
if mat_name not in materials:
|
||||
mat_data = {"name": mat_name}
|
||||
# Try to get properties
|
||||
try:
|
||||
mat_data["density"] = phys_mat.GetRealPropertyValue("Density")
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
mat_data["youngs_modulus"] = phys_mat.GetRealPropertyValue("YoungsModulus")
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
mat_data["poissons_ratio"] = phys_mat.GetRealPropertyValue("PoissonsRatio")
|
||||
except:
|
||||
pass
|
||||
materials[mat_name] = mat_data
|
||||
except:
|
||||
pass
|
||||
|
||||
result["success"] = True
|
||||
result["data"] = {
|
||||
"material_count": len(materials),
|
||||
"materials": materials,
|
||||
"body_count": len(solid_bodies)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
result["error"] = str(e)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
'''
|
||||
|
||||
|
||||
def _get_run_journal_exe() -> str:
|
||||
"""Get the path to run_journal.exe."""
|
||||
return os.path.join(NX_BIN_PATH, "run_journal.exe")
|
||||
|
||||
|
||||
def _run_journal(journal_path: str, args: list) -> Tuple[bool, str]:
|
||||
"""Run an NX journal with arguments.
|
||||
|
||||
Returns:
|
||||
Tuple of (success, output_or_error)
|
||||
"""
|
||||
run_journal = _get_run_journal_exe()
|
||||
|
||||
if not os.path.exists(run_journal):
|
||||
return False, f"run_journal.exe not found at {run_journal}"
|
||||
|
||||
cmd = [run_journal, journal_path, "-args"] + args
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120 # 2 minute timeout
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
return False, f"Journal execution failed: {result.stderr}"
|
||||
|
||||
return True, result.stdout
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
return False, "Journal execution timed out"
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
|
||||
def _execute_geometry_operation(
|
||||
operation: str,
|
||||
part_path: str,
|
||||
extra_args: list = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Execute a geometry query operation via NX journal.
|
||||
|
||||
Args:
|
||||
operation: The operation to perform
|
||||
part_path: Path to the part file
|
||||
extra_args: Additional arguments for the operation
|
||||
|
||||
Returns:
|
||||
Dict with operation result
|
||||
"""
|
||||
# Create temporary journal file
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode='w',
|
||||
suffix='.py',
|
||||
delete=False
|
||||
) as journal_file:
|
||||
journal_file.write(GEOMETRY_QUERY_JOURNAL)
|
||||
journal_path = journal_file.name
|
||||
|
||||
# Create temporary output file
|
||||
output_file = tempfile.NamedTemporaryFile(
|
||||
mode='w',
|
||||
suffix='.json',
|
||||
delete=False
|
||||
).name
|
||||
|
||||
try:
|
||||
# Build arguments
|
||||
args = [operation, part_path, output_file]
|
||||
if extra_args:
|
||||
args.extend(extra_args)
|
||||
|
||||
# Run the journal
|
||||
success, output = _run_journal(journal_path, args)
|
||||
|
||||
if not success:
|
||||
return {"success": False, "error": output, "data": {}}
|
||||
|
||||
# Read the result
|
||||
if os.path.exists(output_file):
|
||||
with open(output_file, 'r') as f:
|
||||
return json.load(f)
|
||||
else:
|
||||
return {"success": False, "error": "Output file not created", "data": {}}
|
||||
|
||||
finally:
|
||||
# Cleanup temporary files
|
||||
if os.path.exists(journal_path):
|
||||
os.unlink(journal_path)
|
||||
if os.path.exists(output_file):
|
||||
os.unlink(output_file)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Public API
|
||||
# =============================================================================
|
||||
|
||||
def get_mass_properties(part_path: str) -> Dict[str, Any]:
|
||||
"""Get mass properties from an NX part.
|
||||
|
||||
Args:
|
||||
part_path: Full path to the .prt file
|
||||
|
||||
Returns:
|
||||
Dict with keys:
|
||||
- success: bool
|
||||
- error: Optional error message
|
||||
- data: Dict with mass, volume, surface_area, centroid,
|
||||
principal_moments, material, density, body_count
|
||||
|
||||
Example:
|
||||
>>> result = get_mass_properties("C:/models/bracket.prt")
|
||||
>>> if result["success"]:
|
||||
... print(f"Mass: {result['data']['mass']} kg")
|
||||
... print(f"Volume: {result['data']['volume']} mm^3")
|
||||
"""
|
||||
part_path = os.path.abspath(part_path)
|
||||
|
||||
if not os.path.exists(part_path):
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Part file not found: {part_path}",
|
||||
"data": {}
|
||||
}
|
||||
|
||||
return _execute_geometry_operation("mass_properties", part_path)
|
||||
|
||||
|
||||
def get_bodies(part_path: str) -> Dict[str, Any]:
|
||||
"""Get information about bodies in an NX part.
|
||||
|
||||
Args:
|
||||
part_path: Full path to the .prt file
|
||||
|
||||
Returns:
|
||||
Dict with keys:
|
||||
- success: bool
|
||||
- error: Optional error message
|
||||
- data: Dict with count, solid_count, sheet_count, bodies list
|
||||
|
||||
Example:
|
||||
>>> result = get_bodies("C:/models/bracket.prt")
|
||||
>>> if result["success"]:
|
||||
... print(f"Solid bodies: {result['data']['solid_count']}")
|
||||
"""
|
||||
part_path = os.path.abspath(part_path)
|
||||
|
||||
if not os.path.exists(part_path):
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Part file not found: {part_path}",
|
||||
"data": {}
|
||||
}
|
||||
|
||||
return _execute_geometry_operation("bodies", part_path)
|
||||
|
||||
|
||||
def get_volume(part_path: str) -> Dict[str, Any]:
|
||||
"""Get total volume of solid bodies in an NX part.
|
||||
|
||||
Args:
|
||||
part_path: Full path to the .prt file
|
||||
|
||||
Returns:
|
||||
Dict with keys:
|
||||
- success: bool
|
||||
- error: Optional error message
|
||||
- data: Dict with volume (mm^3), unit, body_count
|
||||
|
||||
Example:
|
||||
>>> result = get_volume("C:/models/bracket.prt")
|
||||
>>> if result["success"]:
|
||||
... print(f"Volume: {result['data']['volume']} mm^3")
|
||||
"""
|
||||
part_path = os.path.abspath(part_path)
|
||||
|
||||
if not os.path.exists(part_path):
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Part file not found: {part_path}",
|
||||
"data": {}
|
||||
}
|
||||
|
||||
return _execute_geometry_operation("volume", part_path)
|
||||
|
||||
|
||||
def get_surface_area(part_path: str) -> Dict[str, Any]:
|
||||
"""Get total surface area of solid bodies in an NX part.
|
||||
|
||||
Args:
|
||||
part_path: Full path to the .prt file
|
||||
|
||||
Returns:
|
||||
Dict with keys:
|
||||
- success: bool
|
||||
- error: Optional error message
|
||||
- data: Dict with surface_area (mm^2), unit, body_count
|
||||
|
||||
Example:
|
||||
>>> result = get_surface_area("C:/models/bracket.prt")
|
||||
>>> if result["success"]:
|
||||
... print(f"Area: {result['data']['surface_area']} mm^2")
|
||||
"""
|
||||
part_path = os.path.abspath(part_path)
|
||||
|
||||
if not os.path.exists(part_path):
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Part file not found: {part_path}",
|
||||
"data": {}
|
||||
}
|
||||
|
||||
return _execute_geometry_operation("surface_area", part_path)
|
||||
|
||||
|
||||
def get_material(part_path: str) -> Dict[str, Any]:
|
||||
"""Get material information from bodies in an NX part.
|
||||
|
||||
Args:
|
||||
part_path: Full path to the .prt file
|
||||
|
||||
Returns:
|
||||
Dict with keys:
|
||||
- success: bool
|
||||
- error: Optional error message
|
||||
- data: Dict with material_count, materials dict, body_count
|
||||
|
||||
Example:
|
||||
>>> result = get_material("C:/models/bracket.prt")
|
||||
>>> if result["success"]:
|
||||
... for name, mat in result["data"]["materials"].items():
|
||||
... print(f"{name}: density={mat.get('density')}")
|
||||
"""
|
||||
part_path = os.path.abspath(part_path)
|
||||
|
||||
if not os.path.exists(part_path):
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Part file not found: {part_path}",
|
||||
"data": {}
|
||||
}
|
||||
|
||||
return _execute_geometry_operation("material", part_path)
|
||||
1158
optimization_engine/hooks/nx_cad/model_introspection.py
Normal file
1158
optimization_engine/hooks/nx_cad/model_introspection.py
Normal file
File diff suppressed because it is too large
Load Diff
478
optimization_engine/hooks/nx_cad/part_manager.py
Normal file
478
optimization_engine/hooks/nx_cad/part_manager.py
Normal file
@@ -0,0 +1,478 @@
|
||||
"""
|
||||
NX Part Manager Hook
|
||||
====================
|
||||
|
||||
Provides Python functions to open, close, and save NX parts.
|
||||
|
||||
API Reference (verified via Siemens MCP docs):
|
||||
- Session.Parts() -> PartCollection
|
||||
- PartCollection.OpenBase() -> Opens a part file
|
||||
- Part.Close() -> Closes the part
|
||||
- Part.Save() -> Saves the part
|
||||
- Part.SaveAs() -> Saves the part with a new name
|
||||
|
||||
Usage:
|
||||
from optimization_engine.hooks.nx_cad import part_manager
|
||||
|
||||
# Open a part
|
||||
part = part_manager.open_part("C:/path/to/part.prt")
|
||||
|
||||
# Save the part
|
||||
part_manager.save_part(part)
|
||||
|
||||
# Close the part
|
||||
part_manager.close_part(part)
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any, Tuple
|
||||
|
||||
# NX installation path (configurable)
|
||||
NX_BIN_PATH = os.environ.get(
|
||||
"NX_BIN_PATH",
|
||||
r"C:\Program Files\Siemens\NX2506\NXBIN"
|
||||
)
|
||||
|
||||
# Journal template for part operations
|
||||
PART_OPERATIONS_JOURNAL = '''
|
||||
# NX Open Python Journal - Part Operations
|
||||
# Auto-generated by Atomizer hooks
|
||||
|
||||
import NXOpen
|
||||
import NXOpen.UF
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
|
||||
def main():
|
||||
"""Execute part operation based on command arguments."""
|
||||
# Get the NX session
|
||||
session = NXOpen.Session.GetSession()
|
||||
|
||||
# Parse arguments: operation, part_path, [output_json]
|
||||
args = sys.argv[1:] if len(sys.argv) > 1 else []
|
||||
|
||||
if len(args) < 2:
|
||||
raise ValueError("Usage: script.py <operation> <part_path> [output_json]")
|
||||
|
||||
operation = args[0]
|
||||
part_path = args[1]
|
||||
output_json = args[2] if len(args) > 2 else None
|
||||
|
||||
result = {"success": False, "error": None, "data": {}}
|
||||
|
||||
try:
|
||||
if operation == "open":
|
||||
result = open_part(session, part_path)
|
||||
elif operation == "close":
|
||||
result = close_part(session, part_path)
|
||||
elif operation == "save":
|
||||
result = save_part(session, part_path)
|
||||
elif operation == "save_as":
|
||||
new_path = args[3] if len(args) > 3 else None
|
||||
result = save_part_as(session, part_path, new_path)
|
||||
elif operation == "info":
|
||||
result = get_part_info(session, part_path)
|
||||
else:
|
||||
result["error"] = f"Unknown operation: {operation}"
|
||||
except Exception as e:
|
||||
result["error"] = str(e)
|
||||
|
||||
# Write result to output JSON if specified
|
||||
if output_json:
|
||||
with open(output_json, 'w') as f:
|
||||
json.dump(result, f, indent=2)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def open_part(session, part_path):
|
||||
"""Open a part file.
|
||||
|
||||
NX Open API: Session.Parts().OpenActiveDisplay()
|
||||
"""
|
||||
result = {"success": False, "error": None, "data": {}}
|
||||
|
||||
if not os.path.exists(part_path):
|
||||
result["error"] = f"Part file not found: {part_path}"
|
||||
return result
|
||||
|
||||
try:
|
||||
# Set load options for the working directory
|
||||
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 using OpenActiveDisplay (more compatible with batch mode)
|
||||
part, load_status = session.Parts.OpenActiveDisplay(
|
||||
part_path,
|
||||
NXOpen.DisplayPartOption.AllowAdditional
|
||||
)
|
||||
load_status.Dispose()
|
||||
|
||||
if part is None:
|
||||
result["error"] = "Failed to open part - returned None"
|
||||
return result
|
||||
|
||||
result["success"] = True
|
||||
result["data"] = {
|
||||
"part_name": part.Name,
|
||||
"full_path": part.FullPath,
|
||||
"leaf": part.Leaf,
|
||||
"is_modified": part.IsModified,
|
||||
"is_fully_loaded": part.IsFullyLoaded,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
result["error"] = str(e)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def close_part(session, part_path):
|
||||
"""Close a part.
|
||||
|
||||
NX Open API: Part.Close()
|
||||
"""
|
||||
result = {"success": False, "error": None, "data": {}}
|
||||
|
||||
try:
|
||||
# Find the part in the session
|
||||
part = find_part_by_path(session, part_path)
|
||||
|
||||
if part is None:
|
||||
result["error"] = f"Part not found in session: {part_path}"
|
||||
return result
|
||||
|
||||
# Close the part
|
||||
# Parameters: close_whole_tree, close_modified, responses
|
||||
part.Close(
|
||||
NXOpen.BasePart.CloseWholeTree.TrueValue,
|
||||
NXOpen.BasePart.CloseModified.CloseModified,
|
||||
None
|
||||
)
|
||||
|
||||
result["success"] = True
|
||||
result["data"] = {"closed": part_path}
|
||||
|
||||
except Exception as e:
|
||||
result["error"] = str(e)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def save_part(session, part_path):
|
||||
"""Save a part.
|
||||
|
||||
NX Open API: Part.Save()
|
||||
"""
|
||||
result = {"success": False, "error": None, "data": {}}
|
||||
|
||||
try:
|
||||
# Find the part in the session
|
||||
part = find_part_by_path(session, part_path)
|
||||
|
||||
if part is None:
|
||||
result["error"] = f"Part not found in session: {part_path}"
|
||||
return result
|
||||
|
||||
# Save the part
|
||||
# Parameters: save_component_parts, close_after_save
|
||||
save_status = part.Save(
|
||||
NXOpen.BasePart.SaveComponents.TrueValue,
|
||||
NXOpen.BasePart.CloseAfterSave.FalseValue
|
||||
)
|
||||
|
||||
result["success"] = True
|
||||
result["data"] = {
|
||||
"saved": part_path,
|
||||
"is_modified": part.IsModified
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
result["error"] = str(e)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def save_part_as(session, part_path, new_path):
|
||||
"""Save a part with a new name.
|
||||
|
||||
NX Open API: Part.SaveAs()
|
||||
"""
|
||||
result = {"success": False, "error": None, "data": {}}
|
||||
|
||||
if not new_path:
|
||||
result["error"] = "New path is required for SaveAs operation"
|
||||
return result
|
||||
|
||||
try:
|
||||
# Find the part in the session
|
||||
part = find_part_by_path(session, part_path)
|
||||
|
||||
if part is None:
|
||||
result["error"] = f"Part not found in session: {part_path}"
|
||||
return result
|
||||
|
||||
# Save as new file
|
||||
part.SaveAs(new_path)
|
||||
|
||||
result["success"] = True
|
||||
result["data"] = {
|
||||
"original": part_path,
|
||||
"saved_as": new_path
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
result["error"] = str(e)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_part_info(session, part_path):
|
||||
"""Get information about a part.
|
||||
|
||||
NX Open API: Part properties
|
||||
"""
|
||||
result = {"success": False, "error": None, "data": {}}
|
||||
|
||||
try:
|
||||
# Find the part in the session
|
||||
part = find_part_by_path(session, part_path)
|
||||
|
||||
if part is None:
|
||||
result["error"] = f"Part not found in session: {part_path}"
|
||||
return result
|
||||
|
||||
# Get part info
|
||||
result["success"] = True
|
||||
result["data"] = {
|
||||
"name": part.Name,
|
||||
"full_path": part.FullPath,
|
||||
"leaf": part.Leaf,
|
||||
"is_modified": part.IsModified,
|
||||
"is_fully_loaded": part.IsFullyLoaded,
|
||||
"is_read_only": part.IsReadOnly,
|
||||
"has_write_access": part.HasWriteAccess,
|
||||
"part_units": str(part.PartUnits),
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
result["error"] = str(e)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def find_part_by_path(session, part_path):
|
||||
"""Find a part in the session by its file path."""
|
||||
part_path_normalized = os.path.normpath(part_path).lower()
|
||||
|
||||
for part in session.Parts:
|
||||
if os.path.normpath(part.FullPath).lower() == part_path_normalized:
|
||||
return part
|
||||
|
||||
return None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
'''
|
||||
|
||||
|
||||
def _get_run_journal_exe() -> str:
|
||||
"""Get the path to run_journal.exe."""
|
||||
return os.path.join(NX_BIN_PATH, "run_journal.exe")
|
||||
|
||||
|
||||
def _run_journal(journal_path: str, args: list) -> Tuple[bool, str]:
|
||||
"""Run an NX journal with arguments.
|
||||
|
||||
Returns:
|
||||
Tuple of (success, output_or_error)
|
||||
"""
|
||||
run_journal = _get_run_journal_exe()
|
||||
|
||||
if not os.path.exists(run_journal):
|
||||
return False, f"run_journal.exe not found at {run_journal}"
|
||||
|
||||
cmd = [run_journal, journal_path, "-args"] + args
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120 # 2 minute timeout
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
return False, f"Journal execution failed: {result.stderr}"
|
||||
|
||||
return True, result.stdout
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
return False, "Journal execution timed out"
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
|
||||
def _execute_part_operation(
|
||||
operation: str,
|
||||
part_path: str,
|
||||
extra_args: list = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Execute a part operation via NX journal.
|
||||
|
||||
Args:
|
||||
operation: The operation to perform (open, close, save, save_as, info)
|
||||
part_path: Path to the part file
|
||||
extra_args: Additional arguments for the operation
|
||||
|
||||
Returns:
|
||||
Dict with operation result
|
||||
"""
|
||||
# Create temporary journal file
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode='w',
|
||||
suffix='.py',
|
||||
delete=False
|
||||
) as journal_file:
|
||||
journal_file.write(PART_OPERATIONS_JOURNAL)
|
||||
journal_path = journal_file.name
|
||||
|
||||
# Create temporary output file
|
||||
output_file = tempfile.NamedTemporaryFile(
|
||||
mode='w',
|
||||
suffix='.json',
|
||||
delete=False
|
||||
).name
|
||||
|
||||
try:
|
||||
# Build arguments
|
||||
args = [operation, part_path, output_file]
|
||||
if extra_args:
|
||||
args.extend(extra_args)
|
||||
|
||||
# Run the journal
|
||||
success, output = _run_journal(journal_path, args)
|
||||
|
||||
if not success:
|
||||
return {"success": False, "error": output, "data": {}}
|
||||
|
||||
# Read the result
|
||||
if os.path.exists(output_file):
|
||||
with open(output_file, 'r') as f:
|
||||
return json.load(f)
|
||||
else:
|
||||
return {"success": False, "error": "Output file not created", "data": {}}
|
||||
|
||||
finally:
|
||||
# Cleanup temporary files
|
||||
if os.path.exists(journal_path):
|
||||
os.unlink(journal_path)
|
||||
if os.path.exists(output_file):
|
||||
os.unlink(output_file)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Public API
|
||||
# =============================================================================
|
||||
|
||||
def open_part(part_path: str) -> Dict[str, Any]:
|
||||
"""Open an NX part file.
|
||||
|
||||
Args:
|
||||
part_path: Full path to the .prt file
|
||||
|
||||
Returns:
|
||||
Dict with keys:
|
||||
- success: bool
|
||||
- error: Optional error message
|
||||
- data: Dict with part_name, full_path, leaf, is_modified, is_fully_loaded
|
||||
|
||||
Example:
|
||||
>>> result = open_part("C:/models/bracket.prt")
|
||||
>>> if result["success"]:
|
||||
... print(f"Opened: {result['data']['part_name']}")
|
||||
"""
|
||||
part_path = os.path.abspath(part_path)
|
||||
|
||||
if not os.path.exists(part_path):
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Part file not found: {part_path}",
|
||||
"data": {}
|
||||
}
|
||||
|
||||
return _execute_part_operation("open", part_path)
|
||||
|
||||
|
||||
def close_part(part_path: str) -> Dict[str, Any]:
|
||||
"""Close an NX part.
|
||||
|
||||
Args:
|
||||
part_path: Full path to the .prt file
|
||||
|
||||
Returns:
|
||||
Dict with keys:
|
||||
- success: bool
|
||||
- error: Optional error message
|
||||
- data: Dict with closed path
|
||||
"""
|
||||
part_path = os.path.abspath(part_path)
|
||||
return _execute_part_operation("close", part_path)
|
||||
|
||||
|
||||
def save_part(part_path: str) -> Dict[str, Any]:
|
||||
"""Save an NX part.
|
||||
|
||||
Args:
|
||||
part_path: Full path to the .prt file
|
||||
|
||||
Returns:
|
||||
Dict with keys:
|
||||
- success: bool
|
||||
- error: Optional error message
|
||||
- data: Dict with saved path and is_modified flag
|
||||
"""
|
||||
part_path = os.path.abspath(part_path)
|
||||
return _execute_part_operation("save", part_path)
|
||||
|
||||
|
||||
def save_part_as(part_path: str, new_path: str) -> Dict[str, Any]:
|
||||
"""Save an NX part with a new name.
|
||||
|
||||
Args:
|
||||
part_path: Full path to the original .prt file
|
||||
new_path: Full path for the new file
|
||||
|
||||
Returns:
|
||||
Dict with keys:
|
||||
- success: bool
|
||||
- error: Optional error message
|
||||
- data: Dict with original and saved_as paths
|
||||
"""
|
||||
part_path = os.path.abspath(part_path)
|
||||
new_path = os.path.abspath(new_path)
|
||||
return _execute_part_operation("save_as", part_path, [new_path])
|
||||
|
||||
|
||||
def get_part_info(part_path: str) -> Dict[str, Any]:
|
||||
"""Get information about an NX part.
|
||||
|
||||
Args:
|
||||
part_path: Full path to the .prt file
|
||||
|
||||
Returns:
|
||||
Dict with keys:
|
||||
- success: bool
|
||||
- error: Optional error message
|
||||
- data: Dict with name, full_path, leaf, is_modified,
|
||||
is_fully_loaded, is_read_only, has_write_access, part_units
|
||||
"""
|
||||
part_path = os.path.abspath(part_path)
|
||||
return _execute_part_operation("info", part_path)
|
||||
18
optimization_engine/hooks/nx_cae/__init__.py
Normal file
18
optimization_engine/hooks/nx_cae/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
NX CAE Hooks
|
||||
============
|
||||
|
||||
Python hooks for NX CAE (FEM/Simulation) operations via NX Open API.
|
||||
|
||||
Modules
|
||||
-------
|
||||
solver_manager : Solution export and solve operations
|
||||
- export_bdf: Export Nastran deck without solving
|
||||
- solve_simulation: Solve a simulation solution
|
||||
|
||||
Phase 2 Task 2.1 - NX Open Automation Roadmap
|
||||
"""
|
||||
|
||||
from . import solver_manager
|
||||
|
||||
__all__ = ['solver_manager']
|
||||
472
optimization_engine/hooks/nx_cae/solver_manager.py
Normal file
472
optimization_engine/hooks/nx_cae/solver_manager.py
Normal file
@@ -0,0 +1,472 @@
|
||||
"""
|
||||
NX Solver Manager Hook
|
||||
======================
|
||||
|
||||
Provides Python functions to export BDF decks and solve simulations.
|
||||
|
||||
API Reference (NX Open):
|
||||
- SimSolution.ExportSolver() -> Export Nastran deck (.dat/.bdf)
|
||||
- SimSolution.Solve() -> Solve a single solution
|
||||
- SimSolveManager.SolveChainOfSolutions() -> Solve solution chain
|
||||
|
||||
Phase 2 Task 2.1 - NX Open Automation Roadmap
|
||||
|
||||
Usage:
|
||||
from optimization_engine.hooks.nx_cae import solver_manager
|
||||
|
||||
# Export BDF without solving
|
||||
result = solver_manager.export_bdf(
|
||||
"C:/model.sim",
|
||||
"Solution 1",
|
||||
"C:/output/model.dat"
|
||||
)
|
||||
|
||||
# Solve simulation
|
||||
result = solver_manager.solve_simulation("C:/model.sim", "Solution 1")
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
# NX installation path (configurable)
|
||||
NX_BIN_PATH = os.environ.get(
|
||||
"NX_BIN_PATH",
|
||||
r"C:\Program Files\Siemens\NX2506\NXBIN"
|
||||
)
|
||||
|
||||
# Journal template for BDF export
|
||||
BDF_EXPORT_JOURNAL = '''
|
||||
# NX Open Python Journal - BDF Export
|
||||
# Auto-generated by Atomizer hooks
|
||||
# Phase 2 Task 2.1 - NX Open Automation Roadmap
|
||||
|
||||
import NXOpen
|
||||
import NXOpen.CAE
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
def main():
|
||||
"""Export BDF/DAT file from a simulation solution."""
|
||||
args = sys.argv[1:] if len(sys.argv) > 1 else []
|
||||
|
||||
if len(args) < 3:
|
||||
raise ValueError("Usage: script.py <sim_path> <solution_name> <output_bdf> [output_json]")
|
||||
|
||||
sim_path = args[0]
|
||||
solution_name = args[1]
|
||||
output_bdf = args[2]
|
||||
output_json = args[3] if len(args) > 3 else None
|
||||
|
||||
result = {"success": False, "error": None, "data": {}}
|
||||
|
||||
try:
|
||||
session = NXOpen.Session.GetSession()
|
||||
|
||||
# Set load options
|
||||
working_dir = os.path.dirname(sim_path)
|
||||
session.Parts.LoadOptions.ComponentLoadMethod = NXOpen.LoadOptions.LoadMethod.FromDirectory
|
||||
session.Parts.LoadOptions.SetSearchDirectories([working_dir], [True])
|
||||
|
||||
# Open the simulation file
|
||||
print(f"[JOURNAL] Opening simulation: {sim_path}")
|
||||
basePart, loadStatus = session.Parts.OpenActiveDisplay(
|
||||
sim_path,
|
||||
NXOpen.DisplayPartOption.AllowAdditional
|
||||
)
|
||||
loadStatus.Dispose()
|
||||
|
||||
# Get the sim part
|
||||
simPart = session.Parts.Work
|
||||
if not isinstance(simPart, NXOpen.CAE.SimPart):
|
||||
raise ValueError(f"Part is not a SimPart: {type(simPart)}")
|
||||
|
||||
simSimulation = simPart.Simulation
|
||||
print(f"[JOURNAL] Simulation: {simSimulation.Name}")
|
||||
|
||||
# Find the solution
|
||||
solution = None
|
||||
for sol in simSimulation.Solutions:
|
||||
if sol.Name == solution_name:
|
||||
solution = sol
|
||||
break
|
||||
|
||||
if solution is None:
|
||||
# Try to find by index or use first solution
|
||||
solutions = list(simSimulation.Solutions)
|
||||
if solutions:
|
||||
solution = solutions[0]
|
||||
print(f"[JOURNAL] Solution '{solution_name}' not found, using '{solution.Name}'")
|
||||
else:
|
||||
raise ValueError(f"No solutions found in simulation")
|
||||
|
||||
print(f"[JOURNAL] Solution: {solution.Name}")
|
||||
|
||||
# Export the solver deck
|
||||
# The ExportSolver method exports the Nastran input deck
|
||||
print(f"[JOURNAL] Exporting BDF to: {output_bdf}")
|
||||
|
||||
# Create export builder
|
||||
# NX API: SimSolution has methods for exporting
|
||||
# Method 1: Try ExportSolver if available
|
||||
try:
|
||||
# Some NX versions use NastranSolverExportBuilder
|
||||
exportBuilder = solution.CreateNastranSolverExportBuilder()
|
||||
exportBuilder.NastranInputFile = output_bdf
|
||||
exportBuilder.Commit()
|
||||
exportBuilder.Destroy()
|
||||
print("[JOURNAL] Exported via NastranSolverExportBuilder")
|
||||
except AttributeError:
|
||||
# Method 2: Alternative - solve and copy output
|
||||
# When solving, NX creates the deck in SXXXXX folder
|
||||
print("[JOURNAL] NastranSolverExportBuilder not available")
|
||||
print("[JOURNAL] BDF export requires solving - use solve_simulation instead")
|
||||
raise ValueError("Direct BDF export not available in this NX version. "
|
||||
"Use solve_simulation() and find BDF in solution folder.")
|
||||
|
||||
result["success"] = True
|
||||
result["data"] = {
|
||||
"output_file": output_bdf,
|
||||
"solution_name": solution.Name,
|
||||
"simulation": simSimulation.Name,
|
||||
}
|
||||
print(f"[JOURNAL] Export completed successfully")
|
||||
|
||||
except Exception as e:
|
||||
result["error"] = str(e)
|
||||
print(f"[JOURNAL] ERROR: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# Write result
|
||||
if output_json:
|
||||
with open(output_json, 'w') as f:
|
||||
json.dump(result, f, indent=2)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
'''
|
||||
|
||||
|
||||
def _run_journal(journal_content: str, *args) -> Dict[str, Any]:
|
||||
"""Execute an NX journal script and return the result."""
|
||||
run_journal_exe = Path(NX_BIN_PATH) / "run_journal.exe"
|
||||
if not run_journal_exe.exists():
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"run_journal.exe not found at {run_journal_exe}",
|
||||
"data": {}
|
||||
}
|
||||
|
||||
# Create temporary files
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as journal_file:
|
||||
journal_file.write(journal_content)
|
||||
journal_path = journal_file.name
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as output_file:
|
||||
output_path = output_file.name
|
||||
|
||||
try:
|
||||
# Build command
|
||||
cmd = [str(run_journal_exe), journal_path, "-args"]
|
||||
cmd.extend(str(a) for a in args)
|
||||
cmd.append(output_path)
|
||||
|
||||
# Execute
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120 # 2 minute timeout
|
||||
)
|
||||
|
||||
# Read result
|
||||
if os.path.exists(output_path):
|
||||
with open(output_path, 'r') as f:
|
||||
return json.load(f)
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"No output file generated. stdout: {result.stdout}, stderr: {result.stderr}",
|
||||
"data": {}
|
||||
}
|
||||
except subprocess.TimeoutExpired:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Journal execution timed out after 120 seconds",
|
||||
"data": {}
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"data": {}
|
||||
}
|
||||
finally:
|
||||
# Cleanup
|
||||
try:
|
||||
os.unlink(journal_path)
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
os.unlink(output_path)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def export_bdf(
|
||||
sim_path: str,
|
||||
solution_name: str = "Solution 1",
|
||||
output_bdf: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Export Nastran deck (BDF/DAT) from a simulation without solving.
|
||||
|
||||
Note: This functionality depends on NX version. Some versions require
|
||||
solving to generate the BDF. Use solve_simulation() and locate the BDF
|
||||
in the solution folder (SXXXXX/*.dat) as an alternative.
|
||||
|
||||
Args:
|
||||
sim_path: Path to .sim file
|
||||
solution_name: Name of solution to export (default "Solution 1")
|
||||
output_bdf: Output path for BDF file (default: same dir as sim)
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
'success': bool,
|
||||
'error': str or None,
|
||||
'data': {
|
||||
'output_file': Path to exported BDF,
|
||||
'solution_name': Solution name used,
|
||||
'simulation': Simulation name
|
||||
}
|
||||
}
|
||||
|
||||
Example:
|
||||
>>> result = export_bdf("C:/model.sim", "Solution 1", "C:/output/model.dat")
|
||||
>>> if result["success"]:
|
||||
... print(f"BDF exported to: {result['data']['output_file']}")
|
||||
"""
|
||||
sim_path = str(Path(sim_path).resolve())
|
||||
|
||||
if not Path(sim_path).exists():
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Simulation file not found: {sim_path}",
|
||||
"data": {}
|
||||
}
|
||||
|
||||
if output_bdf is None:
|
||||
sim_dir = Path(sim_path).parent
|
||||
sim_name = Path(sim_path).stem
|
||||
output_bdf = str(sim_dir / f"{sim_name}.dat")
|
||||
|
||||
return _run_journal(BDF_EXPORT_JOURNAL, sim_path, solution_name, output_bdf)
|
||||
|
||||
|
||||
def get_bdf_from_solution_folder(
|
||||
sim_path: str,
|
||||
solution_name: str = "Solution 1"
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Locate BDF file in the solution output folder.
|
||||
|
||||
After solving, NX creates a folder structure like:
|
||||
- model_sim1_fem1_SXXXXX/
|
||||
- model_sim1_fem1.dat (BDF file)
|
||||
- model_sim1_fem1.op2 (results)
|
||||
|
||||
This function finds the BDF without running export.
|
||||
|
||||
Args:
|
||||
sim_path: Path to .sim file
|
||||
solution_name: Name of solution
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
'success': bool,
|
||||
'error': str or None,
|
||||
'data': {
|
||||
'bdf_file': Path to BDF if found,
|
||||
'solution_folders': List of found solution folders
|
||||
}
|
||||
}
|
||||
"""
|
||||
sim_path = Path(sim_path)
|
||||
if not sim_path.exists():
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Simulation file not found: {sim_path}",
|
||||
"data": {}
|
||||
}
|
||||
|
||||
sim_dir = sim_path.parent
|
||||
sim_stem = sim_path.stem
|
||||
|
||||
# Search for solution folders (pattern: *_SXXXXX)
|
||||
solution_folders = list(sim_dir.glob(f"{sim_stem}*_S[0-9]*"))
|
||||
|
||||
if not solution_folders:
|
||||
# Also try simpler patterns
|
||||
solution_folders = list(sim_dir.glob("*_S[0-9]*"))
|
||||
|
||||
bdf_files = []
|
||||
for folder in solution_folders:
|
||||
if folder.is_dir():
|
||||
# Look for .dat or .bdf files
|
||||
dat_files = list(folder.glob("*.dat"))
|
||||
bdf_files.extend(dat_files)
|
||||
|
||||
if bdf_files:
|
||||
# Return the most recent one
|
||||
bdf_files.sort(key=lambda f: f.stat().st_mtime, reverse=True)
|
||||
return {
|
||||
"success": True,
|
||||
"error": None,
|
||||
"data": {
|
||||
"bdf_file": str(bdf_files[0]),
|
||||
"all_bdf_files": [str(f) for f in bdf_files],
|
||||
"solution_folders": [str(f) for f in solution_folders]
|
||||
}
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "No BDF files found. Ensure the simulation has been solved.",
|
||||
"data": {
|
||||
"solution_folders": [str(f) for f in solution_folders]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def solve_simulation(
|
||||
sim_path: str,
|
||||
solution_name: str = "Solution 1",
|
||||
expression_updates: Optional[Dict[str, float]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Solve a simulation solution.
|
||||
|
||||
This uses the existing solve_simulation.py journal which handles both
|
||||
single-part and assembly FEM workflows.
|
||||
|
||||
Args:
|
||||
sim_path: Path to .sim file
|
||||
solution_name: Name of solution to solve (default "Solution 1")
|
||||
expression_updates: Optional dict of {expression_name: value} to update
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
'success': bool,
|
||||
'error': str or None,
|
||||
'data': {
|
||||
'solution_folder': Path to solution output folder,
|
||||
'op2_file': Path to OP2 results file,
|
||||
'bdf_file': Path to BDF input file
|
||||
}
|
||||
}
|
||||
|
||||
Note:
|
||||
For full solve functionality, use the NXSolver class in
|
||||
optimization_engine/nx_solver.py which provides more features
|
||||
like iteration folders and batch processing.
|
||||
"""
|
||||
# This is a simplified wrapper - for full functionality use NXSolver
|
||||
solve_journal = Path(__file__).parent.parent.parent / "solve_simulation.py"
|
||||
|
||||
if not solve_journal.exists():
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Solve journal not found: {solve_journal}",
|
||||
"data": {}
|
||||
}
|
||||
|
||||
run_journal_exe = Path(NX_BIN_PATH) / "run_journal.exe"
|
||||
if not run_journal_exe.exists():
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"run_journal.exe not found at {run_journal_exe}",
|
||||
"data": {}
|
||||
}
|
||||
|
||||
# Build command
|
||||
cmd = [str(run_journal_exe), str(solve_journal), "-args", sim_path, solution_name]
|
||||
|
||||
# Add expression updates
|
||||
if expression_updates:
|
||||
for name, value in expression_updates.items():
|
||||
cmd.append(f"{name}={value}")
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=600 # 10 minute timeout for solving
|
||||
)
|
||||
|
||||
# Check for success in output
|
||||
if "Solve completed successfully" in result.stdout or result.returncode == 0:
|
||||
# Find output files
|
||||
bdf_result = get_bdf_from_solution_folder(sim_path, solution_name)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"error": None,
|
||||
"data": {
|
||||
"stdout": result.stdout[-2000:], # Last 2000 chars
|
||||
"bdf_file": bdf_result["data"].get("bdf_file") if bdf_result["success"] else None,
|
||||
"solution_folders": bdf_result["data"].get("solution_folders", [])
|
||||
}
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Solve may have failed. Check output.",
|
||||
"data": {
|
||||
"stdout": result.stdout[-2000:],
|
||||
"stderr": result.stderr[-1000:]
|
||||
}
|
||||
}
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Solve timed out after 600 seconds",
|
||||
"data": {}
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"data": {}
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Example usage
|
||||
import sys
|
||||
if len(sys.argv) > 1:
|
||||
sim_path = sys.argv[1]
|
||||
solution = sys.argv[2] if len(sys.argv) > 2 else "Solution 1"
|
||||
|
||||
print(f"Looking for BDF in solution folder...")
|
||||
result = get_bdf_from_solution_folder(sim_path, solution)
|
||||
|
||||
if result["success"]:
|
||||
print(f"Found BDF: {result['data']['bdf_file']}")
|
||||
else:
|
||||
print(f"Error: {result['error']}")
|
||||
print(f"Trying to export...")
|
||||
result = export_bdf(sim_path, solution)
|
||||
print(f"Export result: {result}")
|
||||
else:
|
||||
print("Usage: python solver_manager.py <sim_path> [solution_name]")
|
||||
125
optimization_engine/hooks/test_hooks.py
Normal file
125
optimization_engine/hooks/test_hooks.py
Normal file
@@ -0,0 +1,125 @@
|
||||
"""
|
||||
Test script for NX Open hooks.
|
||||
|
||||
This script tests the hooks module with a real NX part.
|
||||
Run with: python -m optimization_engine.hooks.test_hooks
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
|
||||
# Add the project root to path
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
sys.path.insert(0, project_root)
|
||||
|
||||
from optimization_engine.hooks.nx_cad import (
|
||||
part_manager,
|
||||
expression_manager,
|
||||
geometry_query,
|
||||
feature_manager,
|
||||
)
|
||||
|
||||
|
||||
def test_hooks(part_path: str):
|
||||
"""Test all hooks with the given part."""
|
||||
print(f"\n{'='*60}")
|
||||
print(f"Testing NX Open Hooks")
|
||||
print(f"Part: {part_path}")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
if not os.path.exists(part_path):
|
||||
print(f"ERROR: Part file not found: {part_path}")
|
||||
return False
|
||||
|
||||
all_passed = True
|
||||
|
||||
# Test 1: Get expressions
|
||||
print("\n--- Test 1: Get Expressions ---")
|
||||
result = expression_manager.get_expressions(part_path)
|
||||
if result["success"]:
|
||||
print(f"SUCCESS: Found {result['data']['count']} expressions")
|
||||
# Show first 5 expressions
|
||||
for i, (name, expr) in enumerate(list(result['data']['expressions'].items())[:5]):
|
||||
print(f" {name} = {expr['value']} {expr.get('units', '')}")
|
||||
if result['data']['count'] > 5:
|
||||
print(f" ... and {result['data']['count'] - 5} more")
|
||||
else:
|
||||
print(f"FAILED: {result['error']}")
|
||||
all_passed = False
|
||||
|
||||
# Test 2: Get mass properties
|
||||
print("\n--- Test 2: Get Mass Properties ---")
|
||||
result = geometry_query.get_mass_properties(part_path)
|
||||
if result["success"]:
|
||||
data = result['data']
|
||||
print(f"SUCCESS:")
|
||||
print(f" Mass: {data['mass']:.6f} {data['mass_unit']}")
|
||||
print(f" Volume: {data['volume']:.2f} {data['volume_unit']}")
|
||||
print(f" Surface Area: {data['surface_area']:.2f} {data['area_unit']}")
|
||||
print(f" Material: {data.get('material', 'N/A')}")
|
||||
print(f" Centroid: ({data['centroid']['x']:.2f}, {data['centroid']['y']:.2f}, {data['centroid']['z']:.2f}) mm")
|
||||
else:
|
||||
print(f"FAILED: {result['error']}")
|
||||
all_passed = False
|
||||
|
||||
# Test 3: Get bodies
|
||||
print("\n--- Test 3: Get Bodies ---")
|
||||
result = geometry_query.get_bodies(part_path)
|
||||
if result["success"]:
|
||||
data = result['data']
|
||||
print(f"SUCCESS:")
|
||||
print(f" Total bodies: {data['count']}")
|
||||
print(f" Solid bodies: {data['solid_count']}")
|
||||
print(f" Sheet bodies: {data['sheet_count']}")
|
||||
else:
|
||||
print(f"FAILED: {result['error']}")
|
||||
all_passed = False
|
||||
|
||||
# Test 4: Get features
|
||||
print("\n--- Test 4: Get Features ---")
|
||||
result = feature_manager.get_features(part_path)
|
||||
if result["success"]:
|
||||
data = result['data']
|
||||
print(f"SUCCESS: Found {data['count']} features ({data['suppressed_count']} suppressed)")
|
||||
# Show first 5 features
|
||||
for i, feat in enumerate(data['features'][:5]):
|
||||
status = "suppressed" if feat['is_suppressed'] else "active"
|
||||
print(f" {feat['name']} ({feat['type']}): {status}")
|
||||
if data['count'] > 5:
|
||||
print(f" ... and {data['count'] - 5} more")
|
||||
else:
|
||||
print(f"FAILED: {result['error']}")
|
||||
all_passed = False
|
||||
|
||||
# Summary
|
||||
print(f"\n{'='*60}")
|
||||
if all_passed:
|
||||
print("ALL TESTS PASSED!")
|
||||
else:
|
||||
print("SOME TESTS FAILED")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
return all_passed
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
# Default to bracket study part
|
||||
default_part = os.path.join(
|
||||
project_root,
|
||||
"studies/bracket_stiffness_optimization_V3/1_setup/model/Bracket.prt"
|
||||
)
|
||||
|
||||
# Use command line argument if provided
|
||||
if len(sys.argv) > 1:
|
||||
part_path = sys.argv[1]
|
||||
else:
|
||||
part_path = default_part
|
||||
|
||||
success = test_hooks(part_path)
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
121
optimization_engine/hooks/test_introspection.py
Normal file
121
optimization_engine/hooks/test_introspection.py
Normal file
@@ -0,0 +1,121 @@
|
||||
"""Test model introspection module."""
|
||||
|
||||
import json
|
||||
import glob
|
||||
from pathlib import Path
|
||||
|
||||
# Add project root to path
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||
|
||||
from optimization_engine.hooks.nx_cad.model_introspection import (
|
||||
introspect_op2,
|
||||
introspect_study,
|
||||
)
|
||||
|
||||
|
||||
def test_op2_introspection():
|
||||
"""Test OP2 introspection on bracket study."""
|
||||
print("=" * 60)
|
||||
print("OP2 INTROSPECTION TEST")
|
||||
print("=" * 60)
|
||||
|
||||
# Find bracket OP2 files
|
||||
op2_files = glob.glob(
|
||||
"C:/Users/Antoine/Atomizer/studies/bracket_stiffness_optimization_V3/**/*.op2",
|
||||
recursive=True
|
||||
)
|
||||
|
||||
print(f"\nFound {len(op2_files)} OP2 files")
|
||||
for f in op2_files[:5]:
|
||||
print(f" - {Path(f).name}")
|
||||
|
||||
if not op2_files:
|
||||
print("No OP2 files found!")
|
||||
return
|
||||
|
||||
# Introspect first OP2
|
||||
print(f"\nIntrospecting: {Path(op2_files[0]).name}")
|
||||
result = introspect_op2(op2_files[0])
|
||||
|
||||
if not result["success"]:
|
||||
print(f"ERROR: {result['error']}")
|
||||
return
|
||||
|
||||
data = result["data"]
|
||||
|
||||
# Print results
|
||||
print(f"\nFile Info:")
|
||||
print(f" Size: {data['file_info']['size_mb']:.2f} MB")
|
||||
print(f" Subcases: {data['subcases']}")
|
||||
|
||||
print(f"\nAvailable Results:")
|
||||
for r_type, info in data["results"].items():
|
||||
status = "YES" if info["available"] else "no"
|
||||
extra = ""
|
||||
if info["available"]:
|
||||
if "element_types" in info and info["element_types"]:
|
||||
extra = f" ({', '.join(info['element_types'][:3])})"
|
||||
elif "subcases" in info and info["subcases"]:
|
||||
extra = f" (subcases: {info['subcases'][:3]})"
|
||||
print(f" {r_type:20s}: {status:4s} {extra}")
|
||||
|
||||
print(f"\nMesh Info:")
|
||||
print(f" Nodes: {data['mesh']['node_count']}")
|
||||
print(f" Elements: {data['mesh']['element_count']}")
|
||||
if data['mesh']['element_types']:
|
||||
print(f" Element types: {list(data['mesh']['element_types'].keys())[:5]}")
|
||||
|
||||
print(f"\nExtractable results: {data['extractable']}")
|
||||
|
||||
|
||||
def test_study_introspection():
|
||||
"""Test study directory introspection."""
|
||||
print("\n" + "=" * 60)
|
||||
print("STUDY INTROSPECTION TEST")
|
||||
print("=" * 60)
|
||||
|
||||
study_dir = "C:/Users/Antoine/Atomizer/studies/bracket_stiffness_optimization_V3"
|
||||
print(f"\nIntrospecting study: {study_dir}")
|
||||
|
||||
result = introspect_study(study_dir)
|
||||
|
||||
if not result["success"]:
|
||||
print(f"ERROR: {result['error']}")
|
||||
return
|
||||
|
||||
data = result["data"]
|
||||
|
||||
print(f"\nStudy Summary:")
|
||||
print(f" Parts (.prt): {data['summary']['part_count']}")
|
||||
print(f" Simulations (.sim): {data['summary']['simulation_count']}")
|
||||
print(f" Results (.op2): {data['summary']['results_count']}")
|
||||
print(f" Has config: {data['summary']['has_config']}")
|
||||
|
||||
print(f"\nParts found:")
|
||||
for p in data["parts"][:5]:
|
||||
print(f" - {Path(p['path']).name}")
|
||||
|
||||
print(f"\nSimulations found:")
|
||||
for s in data["simulations"][:5]:
|
||||
print(f" - {Path(s['path']).name}")
|
||||
|
||||
if data["config"]:
|
||||
print(f"\nOptimization Config:")
|
||||
config = data["config"]
|
||||
if "variables" in config:
|
||||
print(f" Variables: {len(config['variables'])}")
|
||||
for v in config["variables"][:3]:
|
||||
print(f" - {v.get('name', 'unnamed')}: [{v.get('lower')}, {v.get('upper')}]")
|
||||
if "objectives" in config:
|
||||
print(f" Objectives: {len(config['objectives'])}")
|
||||
for o in config["objectives"][:3]:
|
||||
print(f" - {o.get('name', 'unnamed')} ({o.get('direction', 'minimize')})")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_op2_introspection()
|
||||
test_study_introspection()
|
||||
print("\n" + "=" * 60)
|
||||
print("INTROSPECTION TESTS COMPLETE")
|
||||
print("=" * 60)
|
||||
@@ -676,7 +676,13 @@ def solve_assembly_fem_workflow(theSession, sim_file_path, solution_name, expres
|
||||
|
||||
def solve_simple_workflow(theSession, sim_file_path, solution_name, expression_updates, working_dir):
|
||||
"""
|
||||
Simple workflow for single-part simulations or when no expression updates needed.
|
||||
Workflow for single-part simulations with optional expression updates.
|
||||
|
||||
For single-part FEMs (Bracket.prt -> Bracket_fem1.fem -> Bracket_sim1.sim):
|
||||
1. Open the .sim file (this loads .fem and .prt)
|
||||
2. If expression_updates: find the geometry .prt, update expressions, rebuild
|
||||
3. Update the FEM mesh
|
||||
4. Solve
|
||||
"""
|
||||
print(f"[JOURNAL] Opening simulation: {sim_file_path}")
|
||||
|
||||
@@ -688,6 +694,192 @@ def solve_simple_workflow(theSession, sim_file_path, solution_name, expression_u
|
||||
partLoadStatus1.Dispose()
|
||||
|
||||
workSimPart = theSession.Parts.BaseWork
|
||||
|
||||
# =========================================================================
|
||||
# STEP 1: UPDATE EXPRESSIONS IN GEOMETRY PART (if any)
|
||||
# =========================================================================
|
||||
if expression_updates:
|
||||
print(f"[JOURNAL] STEP 1: Updating expressions in geometry part...")
|
||||
|
||||
# List all loaded parts for debugging
|
||||
print(f"[JOURNAL] Currently loaded parts:")
|
||||
for part in theSession.Parts:
|
||||
print(f"[JOURNAL] - {part.Name} (type: {type(part).__name__})")
|
||||
|
||||
# NX doesn't automatically load the geometry .prt when opening a SIM file
|
||||
# We need to find and load it explicitly from the working directory
|
||||
geom_part = None
|
||||
|
||||
# First, try to find an already loaded geometry part
|
||||
for part in theSession.Parts:
|
||||
part_name = part.Name.lower()
|
||||
part_type = type(part).__name__
|
||||
|
||||
# Skip FEM and SIM parts by type
|
||||
if 'fem' in part_type.lower() or 'sim' in part_type.lower():
|
||||
continue
|
||||
|
||||
# Skip parts with _fem or _sim in name
|
||||
if '_fem' in part_name or '_sim' in part_name:
|
||||
continue
|
||||
|
||||
geom_part = part
|
||||
print(f"[JOURNAL] Found geometry part (already loaded): {part.Name}")
|
||||
break
|
||||
|
||||
# If not found, try to load the geometry .prt file from working directory
|
||||
if geom_part is None:
|
||||
print(f"[JOURNAL] Geometry part not loaded, searching for .prt file...")
|
||||
for filename in os.listdir(working_dir):
|
||||
if filename.endswith('.prt') and '_fem' not in filename.lower() and '_sim' not in filename.lower():
|
||||
prt_path = os.path.join(working_dir, filename)
|
||||
print(f"[JOURNAL] Loading geometry part: {filename}")
|
||||
try:
|
||||
geom_part, partLoadStatus = theSession.Parts.Open(prt_path)
|
||||
partLoadStatus.Dispose()
|
||||
print(f"[JOURNAL] Geometry part loaded: {geom_part.Name}")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"[JOURNAL] WARNING: Could not load {filename}: {e}")
|
||||
|
||||
if geom_part:
|
||||
try:
|
||||
# Switch to the geometry part for expression editing
|
||||
markId_expr = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Update Expressions")
|
||||
status, partLoadStatus = theSession.Parts.SetActiveDisplay(
|
||||
geom_part,
|
||||
NXOpen.DisplayPartOption.AllowAdditional,
|
||||
NXOpen.PartDisplayPartWorkPartOption.UseLast
|
||||
)
|
||||
partLoadStatus.Dispose()
|
||||
|
||||
# Switch to modeling application for expression editing
|
||||
theSession.ApplicationSwitchImmediate("UG_APP_MODELING")
|
||||
|
||||
workPart = theSession.Parts.Work
|
||||
|
||||
# Write expressions to temp file and import
|
||||
exp_file_path = os.path.join(working_dir, "_temp_expressions.exp")
|
||||
with open(exp_file_path, 'w') as f:
|
||||
for expr_name, expr_value in expression_updates.items():
|
||||
# Determine unit based on name
|
||||
if 'angle' in expr_name.lower():
|
||||
unit_str = "Degrees"
|
||||
else:
|
||||
unit_str = "MilliMeter"
|
||||
f.write(f"[{unit_str}]{expr_name}={expr_value}\n")
|
||||
print(f"[JOURNAL] {expr_name} = {expr_value} ({unit_str})")
|
||||
|
||||
print(f"[JOURNAL] Importing expressions...")
|
||||
expModified, errorMessages = workPart.Expressions.ImportFromFile(
|
||||
exp_file_path,
|
||||
NXOpen.ExpressionCollection.ImportMode.Replace
|
||||
)
|
||||
print(f"[JOURNAL] Expressions modified: {expModified}")
|
||||
if errorMessages:
|
||||
print(f"[JOURNAL] Import messages: {errorMessages}")
|
||||
|
||||
# Update geometry
|
||||
print(f"[JOURNAL] Rebuilding geometry...")
|
||||
markId_update = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Invisible, "NX update")
|
||||
nErrs = theSession.UpdateManager.DoUpdate(markId_update)
|
||||
theSession.DeleteUndoMark(markId_update, "NX update")
|
||||
print(f"[JOURNAL] Geometry rebuilt ({nErrs} errors)")
|
||||
|
||||
# Save geometry part
|
||||
print(f"[JOURNAL] Saving geometry part...")
|
||||
partSaveStatus_geom = workPart.Save(NXOpen.BasePart.SaveComponents.TrueValue, NXOpen.BasePart.CloseAfterSave.FalseValue)
|
||||
partSaveStatus_geom.Dispose()
|
||||
|
||||
# Clean up temp file
|
||||
try:
|
||||
os.remove(exp_file_path)
|
||||
except:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
print(f"[JOURNAL] ERROR updating expressions: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
else:
|
||||
print(f"[JOURNAL] WARNING: Could not find geometry part for expression updates!")
|
||||
|
||||
# =========================================================================
|
||||
# STEP 2: UPDATE FEM MESH (if expressions were updated)
|
||||
# =========================================================================
|
||||
if expression_updates:
|
||||
print(f"[JOURNAL] STEP 2: Updating FEM mesh...")
|
||||
|
||||
# First, load the idealized part if it exists (required for mesh update chain)
|
||||
# The chain is: .prt (geometry) -> _i.prt (idealized) -> .fem (mesh)
|
||||
idealized_part = None
|
||||
for filename in os.listdir(working_dir):
|
||||
if '_i.prt' in filename.lower():
|
||||
idealized_path = os.path.join(working_dir, filename)
|
||||
print(f"[JOURNAL] Loading idealized part: {filename}")
|
||||
try:
|
||||
idealized_part, partLoadStatus = theSession.Parts.Open(idealized_path)
|
||||
partLoadStatus.Dispose()
|
||||
print(f"[JOURNAL] Idealized part loaded: {idealized_part.Name}")
|
||||
except Exception as e:
|
||||
print(f"[JOURNAL] WARNING: Could not load idealized part: {e}")
|
||||
break
|
||||
|
||||
# Find the FEM part
|
||||
fem_part = None
|
||||
for part in theSession.Parts:
|
||||
if '_fem' in part.Name.lower() or part.Name.lower().endswith('.fem'):
|
||||
fem_part = part
|
||||
print(f"[JOURNAL] Found FEM part: {part.Name}")
|
||||
break
|
||||
|
||||
if fem_part:
|
||||
try:
|
||||
# Switch to FEM part - CRITICAL: Use SameAsDisplay to make FEM the work part
|
||||
# This is required for UpdateFemodel() to properly regenerate the mesh
|
||||
# Reference: tests/journal_with_regenerate.py line 76
|
||||
print(f"[JOURNAL] Switching to FEM part: {fem_part.Name}")
|
||||
status, partLoadStatus = theSession.Parts.SetActiveDisplay(
|
||||
fem_part,
|
||||
NXOpen.DisplayPartOption.AllowAdditional,
|
||||
NXOpen.PartDisplayPartWorkPartOption.SameAsDisplay # Critical fix!
|
||||
)
|
||||
partLoadStatus.Dispose()
|
||||
|
||||
# Switch to FEM application
|
||||
theSession.ApplicationSwitchImmediate("UG_APP_SFEM")
|
||||
|
||||
# Update the FE model
|
||||
workFemPart = theSession.Parts.BaseWork
|
||||
feModel = workFemPart.FindObject("FEModel")
|
||||
|
||||
print(f"[JOURNAL] Updating FE model...")
|
||||
feModel.UpdateFemodel()
|
||||
print(f"[JOURNAL] FE model updated")
|
||||
|
||||
# Save FEM
|
||||
partSaveStatus_fem = workFemPart.Save(NXOpen.BasePart.SaveComponents.TrueValue, NXOpen.BasePart.CloseAfterSave.FalseValue)
|
||||
partSaveStatus_fem.Dispose()
|
||||
print(f"[JOURNAL] FEM saved")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[JOURNAL] ERROR updating FEM: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# =========================================================================
|
||||
# STEP 3: SWITCH BACK TO SIM AND SOLVE
|
||||
# =========================================================================
|
||||
print(f"[JOURNAL] STEP 3: Solving simulation...")
|
||||
|
||||
# Switch back to sim part
|
||||
status, partLoadStatus = theSession.Parts.SetActiveDisplay(
|
||||
workSimPart,
|
||||
NXOpen.DisplayPartOption.AllowAdditional,
|
||||
NXOpen.PartDisplayPartWorkPartOption.UseLast
|
||||
)
|
||||
partLoadStatus.Dispose()
|
||||
|
||||
theSession.ApplicationSwitchImmediate("UG_APP_SFEM")
|
||||
theSession.Post.UpdateUserGroupsFromSimPart(workSimPart)
|
||||
|
||||
@@ -710,7 +902,7 @@ def solve_simple_workflow(theSession, sim_file_path, solution_name, expression_u
|
||||
psolutions1,
|
||||
NXOpen.CAE.SimSolution.SolveOption.Solve,
|
||||
NXOpen.CAE.SimSolution.SetupCheckOption.CompleteCheckAndOutputErrors,
|
||||
NXOpen.CAE.SimSolution.SolveMode.Background
|
||||
NXOpen.CAE.SimSolution.SolveMode.Foreground # Use Foreground to wait for completion
|
||||
)
|
||||
|
||||
theSession.DeleteUndoMark(markId_solve2, None)
|
||||
@@ -718,14 +910,11 @@ def solve_simple_workflow(theSession, sim_file_path, solution_name, expression_u
|
||||
|
||||
print(f"[JOURNAL] Solve completed: {numsolved} solved, {numfailed} failed, {numskipped} skipped")
|
||||
|
||||
# Save
|
||||
# Save all
|
||||
try:
|
||||
partSaveStatus = workSimPart.Save(
|
||||
NXOpen.BasePart.SaveComponents.TrueValue,
|
||||
NXOpen.BasePart.CloseAfterSave.FalseValue
|
||||
)
|
||||
anyPartsModified, partSaveStatus = theSession.Parts.SaveAll()
|
||||
partSaveStatus.Dispose()
|
||||
print(f"[JOURNAL] Saved!")
|
||||
print(f"[JOURNAL] Saved all parts!")
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
1745
optimization_engine/study_wizard.py
Normal file
1745
optimization_engine/study_wizard.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user