refactor: Engine updates and NX hooks improvements
optimization_engine: - Updated nx_solver.py with improvements - Enhanced solve_simulation.py - Updated extractors/__init__.py - Improved NX CAD hooks (expression_manager, feature_manager, geometry_query, model_introspection, part_manager) - Enhanced NX CAE solver_manager hook Documentation: - Updated OP_01_CREATE_STUDY.md protocol - Updated SYS_12_EXTRACTOR_LIBRARY.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -31,26 +31,44 @@ This protocol guides you through creating a complete Atomizer optimization study
|
||||
|
||||
## Quick Reference
|
||||
|
||||
**Required Outputs**:
|
||||
| File | Purpose | Location |
|
||||
|------|---------|----------|
|
||||
| `optimization_config.json` | Design vars, objectives, constraints | `1_setup/` |
|
||||
| `run_optimization.py` | Execution script | Study root |
|
||||
| `README.md` | Engineering documentation | Study root |
|
||||
| `STUDY_REPORT.md` | Results template | Study root |
|
||||
**Required Outputs** (ALL MANDATORY - study is INCOMPLETE without these):
|
||||
| File | Purpose | Location | Priority |
|
||||
|------|---------|----------|----------|
|
||||
| `optimization_config.json` | Design vars, objectives, constraints | `1_setup/` | 1 |
|
||||
| `run_optimization.py` | Execution script | Study root | 2 |
|
||||
| **`README.md`** | Engineering documentation | Study root | **3 - NEVER SKIP** |
|
||||
| `STUDY_REPORT.md` | Results template | Study root | 4 |
|
||||
|
||||
**CRITICAL**: README.md is MANDATORY for every study. A study without README.md is INCOMPLETE.
|
||||
|
||||
**Study Structure**:
|
||||
```
|
||||
studies/{study_name}/
|
||||
studies/{geometry_type}/{study_name}/
|
||||
├── 1_setup/
|
||||
│ ├── model/ # NX files (.prt, .sim, .fem)
|
||||
│ └── optimization_config.json
|
||||
├── 2_results/ # Created during run
|
||||
├── 2_iterations/ # FEA trial folders (iter1, iter2, ...)
|
||||
├── 3_results/ # Optimization outputs (study.db, logs)
|
||||
├── README.md # MANDATORY
|
||||
├── STUDY_REPORT.md # MANDATORY
|
||||
└── run_optimization.py
|
||||
```
|
||||
|
||||
**IMPORTANT: Studies are organized by geometry type**:
|
||||
| Geometry Type | Folder | Examples |
|
||||
|---------------|--------|----------|
|
||||
| M1 Mirror | `studies/M1_Mirror/` | m1_mirror_adaptive_V14, m1_mirror_cost_reduction_V3 |
|
||||
| Simple Bracket | `studies/Simple_Bracket/` | bracket_stiffness_optimization |
|
||||
| UAV Arm | `studies/UAV_Arm/` | uav_arm_optimization |
|
||||
| Drone Gimbal | `studies/Drone_Gimbal/` | drone_gimbal_arm_optimization |
|
||||
| Simple Beam | `studies/Simple_Beam/` | simple_beam_optimization |
|
||||
| Other/Test | `studies/_Other/` | training_data_export_test |
|
||||
|
||||
When creating a new study:
|
||||
1. Identify the geometry type (mirror, bracket, beam, etc.)
|
||||
2. Place study under the appropriate `studies/{geometry_type}/` folder
|
||||
3. For new geometry types, create a new folder with descriptive name
|
||||
|
||||
---
|
||||
|
||||
## Detailed Steps
|
||||
@@ -357,6 +375,34 @@ _To be filled after run_
|
||||
_To be filled after analysis_
|
||||
```
|
||||
|
||||
### Step 7b: Capture Baseline Geometry Images (Recommended)
|
||||
|
||||
For better documentation, capture images of the starting geometry using the NX journal:
|
||||
|
||||
```bash
|
||||
# Capture baseline images for study documentation
|
||||
"C:\Program Files\Siemens\DesigncenterNX2512\NXBIN\run_journal.exe" ^
|
||||
"C:\Users\antoi\Atomizer\nx_journals\capture_study_images.py" ^
|
||||
-args "path/to/model.prt" "1_setup/" "model_name"
|
||||
```
|
||||
|
||||
This generates:
|
||||
- `1_setup/{model_name}_Top.png` - Top view
|
||||
- `1_setup/{model_name}_iso.png` - Isometric view
|
||||
|
||||
**Include in README.md**:
|
||||
```markdown
|
||||
## Baseline Geometry
|
||||
|
||||

|
||||
*Top view description*
|
||||
|
||||

|
||||
*Isometric view description*
|
||||
```
|
||||
|
||||
**Journal location**: `nx_journals/capture_study_images.py`
|
||||
|
||||
### Step 8: Validate NX Model File Chain
|
||||
|
||||
**CRITICAL**: NX simulation files have parent-child dependencies. ALL linked files must be copied to the study folder.
|
||||
@@ -394,16 +440,26 @@ _To be filled after analysis_
|
||||
|
||||
### Step 9: Final Validation Checklist
|
||||
|
||||
Before running:
|
||||
**CRITICAL**: Study is NOT complete until ALL items are checked:
|
||||
|
||||
- [ ] NX files exist in `1_setup/model/`
|
||||
- [ ] **ALL child parts copied** (especially `*_i.prt`)
|
||||
- [ ] Expression names match model
|
||||
- [ ] Config validates (JSON schema)
|
||||
- [ ] `run_optimization.py` has no syntax errors
|
||||
- [ ] README.md has all 11 sections
|
||||
- [ ] **README.md exists** (MANDATORY - study is incomplete without it!)
|
||||
- [ ] README.md contains: Overview, Objectives, Constraints, Design Variables, Settings, Usage, Structure
|
||||
- [ ] STUDY_REPORT.md template exists
|
||||
|
||||
**README.md Minimum Content**:
|
||||
1. Overview/Purpose
|
||||
2. Objectives with weights
|
||||
3. Constraints (if any)
|
||||
4. Design variables with ranges
|
||||
5. Optimization settings
|
||||
6. Usage commands
|
||||
7. Directory structure
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -55,6 +55,8 @@ The Extractor Library provides centralized, reusable functions for extracting ph
|
||||
| E16 | Thermal Gradient | `extract_temperature_gradient()` | .op2 | K/mm |
|
||||
| E17 | Heat Flux | `extract_heat_flux()` | .op2 | W/mm² |
|
||||
| E18 | Modal Mass | `extract_modal_mass()` | .f06 | kg |
|
||||
| **Phase 4 (2025-12-19)** | | | | |
|
||||
| E19 | Part Introspection | `introspect_part()` | .prt | dict |
|
||||
|
||||
---
|
||||
|
||||
@@ -581,6 +583,109 @@ ratio = get_modal_mass_ratio(f06_file, direction='z', n_modes=10) # 0-1
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 Extractors (2025-12-19)
|
||||
|
||||
### E19: Part Introspection (Comprehensive)
|
||||
|
||||
**Module**: `optimization_engine.extractors.introspect_part`
|
||||
|
||||
Comprehensive introspection of NX .prt files. Extracts everything available from a part in a single call.
|
||||
|
||||
**Prerequisites**: Uses PowerShell with proper license server setup (see LAC workaround).
|
||||
|
||||
```python
|
||||
from optimization_engine.extractors import (
|
||||
introspect_part,
|
||||
get_expressions_dict,
|
||||
get_expression_value,
|
||||
print_introspection_summary
|
||||
)
|
||||
|
||||
# Full introspection
|
||||
result = introspect_part("path/to/model.prt")
|
||||
# Returns: {
|
||||
# 'success': bool,
|
||||
# 'part_file': str,
|
||||
# 'expressions': {
|
||||
# 'user': [{'name', 'value', 'rhs', 'units', 'type'}, ...],
|
||||
# 'internal': [...],
|
||||
# 'user_count': int,
|
||||
# 'total_count': int
|
||||
# },
|
||||
# 'mass_properties': {
|
||||
# 'mass_kg': float,
|
||||
# 'mass_g': float,
|
||||
# 'volume_mm3': float,
|
||||
# 'surface_area_mm2': float,
|
||||
# 'center_of_gravity_mm': [x, y, z]
|
||||
# },
|
||||
# 'materials': {
|
||||
# 'assigned': [{'name', 'body', 'properties': {...}}],
|
||||
# 'available': [...]
|
||||
# },
|
||||
# 'bodies': {
|
||||
# 'solid_bodies': [{'name', 'is_solid', 'attributes': [...]}],
|
||||
# 'sheet_bodies': [...],
|
||||
# 'counts': {'solid', 'sheet', 'total'}
|
||||
# },
|
||||
# 'attributes': [{'title', 'type', 'value'}, ...],
|
||||
# 'groups': [{'name', 'member_count', 'members': [...]}],
|
||||
# 'features': {
|
||||
# 'total_count': int,
|
||||
# 'by_type': {'Extrude': 5, 'Revolve': 2, ...}
|
||||
# },
|
||||
# 'datums': {
|
||||
# 'planes': [...],
|
||||
# 'csys': [...],
|
||||
# 'axes': [...]
|
||||
# },
|
||||
# 'units': {
|
||||
# 'base_units': {'Length': 'MilliMeter', ...},
|
||||
# 'system': 'Metric (mm)'
|
||||
# },
|
||||
# 'linked_parts': {
|
||||
# 'loaded_parts': [...],
|
||||
# 'fem_parts': [...],
|
||||
# 'sim_parts': [...],
|
||||
# 'idealized_parts': [...]
|
||||
# }
|
||||
# }
|
||||
|
||||
# Convenience functions
|
||||
expr_dict = get_expressions_dict(result) # {'name': value, ...}
|
||||
pocket_radius = get_expression_value(result, 'Pocket_Radius') # float
|
||||
|
||||
# Print formatted summary
|
||||
print_introspection_summary(result)
|
||||
```
|
||||
|
||||
**What It Extracts**:
|
||||
- **Expressions**: All user and internal expressions with values, RHS formulas, units
|
||||
- **Mass Properties**: Mass, volume, surface area, center of gravity
|
||||
- **Materials**: Material names and properties (density, Young's modulus, etc.)
|
||||
- **Bodies**: Solid and sheet bodies with their attributes
|
||||
- **Part Attributes**: All NX_* system attributes plus user attributes
|
||||
- **Groups**: Named groups and their members
|
||||
- **Features**: Feature tree summary by type
|
||||
- **Datums**: Datum planes, coordinate systems, axes
|
||||
- **Units**: Base units and unit system
|
||||
- **Linked Parts**: FEM, SIM, idealized parts loaded in session
|
||||
|
||||
**Use Cases**:
|
||||
- Study setup: Extract actual expression values for baseline
|
||||
- Debugging: Verify model state before optimization
|
||||
- Documentation: Generate part specifications
|
||||
- Validation: Compare expected vs actual parameter values
|
||||
|
||||
**NX Journal Execution** (LAC Workaround):
|
||||
```python
|
||||
# CRITICAL: Use PowerShell with [Environment]::SetEnvironmentVariable()
|
||||
# NOT cmd /c SET or $env: syntax (these fail)
|
||||
powershell -Command "[Environment]::SetEnvironmentVariable('SPLM_LICENSE_SERVER', '28000@server', 'Process'); & 'run_journal.exe' 'introspect_part.py' -args 'model.prt' 'output_dir'"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Files
|
||||
|
||||
```
|
||||
@@ -603,11 +708,13 @@ optimization_engine/extractors/
|
||||
├── extract_spc_forces.py # E14 (Phase 2)
|
||||
├── extract_temperature.py # E15, E16, E17 (Phase 3)
|
||||
├── extract_modal_mass.py # E18 (Phase 3)
|
||||
├── introspect_part.py # E19 (Phase 4)
|
||||
├── test_phase2_extractors.py # Phase 2 tests
|
||||
└── test_phase3_extractors.py # Phase 3 tests
|
||||
|
||||
nx_journals/
|
||||
└── extract_part_mass_material.py # E11 NX journal (prereq)
|
||||
├── extract_part_mass_material.py # E11 NX journal (prereq)
|
||||
└── introspect_part.py # E19 NX journal (comprehensive introspection)
|
||||
```
|
||||
|
||||
---
|
||||
@@ -620,3 +727,4 @@ nx_journals/
|
||||
| 1.1 | 2025-12-06 | Added Phase 2: E12 (principal stress), E13 (strain energy), E14 (SPC forces) |
|
||||
| 1.2 | 2025-12-06 | Added Phase 3: E15-E17 (thermal), E18 (modal mass) |
|
||||
| 1.3 | 2025-12-07 | Added Element Type Selection Guide; documented shell vs solid stress columns |
|
||||
| 1.4 | 2025-12-19 | Added Phase 4: E19 (comprehensive part introspection) |
|
||||
|
||||
@@ -10,6 +10,7 @@ Available extractors:
|
||||
- Strain Energy: extract_strain_energy, extract_total_strain_energy
|
||||
- SPC Forces: extract_spc_forces, extract_total_reaction_force
|
||||
- Zernike: extract_zernike_from_op2, ZernikeExtractor (telescope mirrors)
|
||||
- Part Introspection: introspect_part (comprehensive NX .prt analysis)
|
||||
|
||||
Phase 2 Extractors (2025-12-06):
|
||||
- Principal stress extraction (sigma1, sigma2, sigma3)
|
||||
@@ -21,6 +22,9 @@ Phase 3 Extractors (2025-12-06):
|
||||
- Thermal gradient extraction
|
||||
- Heat flux extraction
|
||||
- Modal mass extraction (modal effective mass from F06)
|
||||
|
||||
Phase 4 Extractors (2025-12-19):
|
||||
- Part Introspection (E12): Comprehensive .prt analysis (expressions, mass, materials, attributes, groups, features)
|
||||
"""
|
||||
|
||||
# Zernike extractor for telescope mirror optimization
|
||||
@@ -82,6 +86,14 @@ from optimization_engine.extractors.extract_modal_mass import (
|
||||
get_modal_mass_ratio,
|
||||
)
|
||||
|
||||
# Part introspection (Phase 4) - comprehensive .prt analysis
|
||||
from optimization_engine.extractors.introspect_part import (
|
||||
introspect_part,
|
||||
get_expressions_dict,
|
||||
get_expression_value,
|
||||
print_introspection_summary,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Part mass & material (from .prt)
|
||||
'extract_part_mass_material',
|
||||
@@ -117,4 +129,9 @@ __all__ = [
|
||||
'extract_frequencies',
|
||||
'get_first_frequency',
|
||||
'get_modal_mass_ratio',
|
||||
# Part introspection (Phase 4)
|
||||
'introspect_part',
|
||||
'get_expressions_dict',
|
||||
'get_expression_value',
|
||||
'print_introspection_summary',
|
||||
]
|
||||
|
||||
297
optimization_engine/extractors/introspect_part.py
Normal file
297
optimization_engine/extractors/introspect_part.py
Normal file
@@ -0,0 +1,297 @@
|
||||
"""
|
||||
Part Introspection Extractor (E12)
|
||||
===================================
|
||||
|
||||
Comprehensive introspection of NX .prt files. Extracts:
|
||||
- All expressions (user and internal)
|
||||
- Mass properties (mass, volume, surface area, CoG)
|
||||
- Material properties
|
||||
- Body information
|
||||
- Part attributes
|
||||
- Groups
|
||||
- Features summary
|
||||
- Datum planes and coordinate systems
|
||||
- Unit system
|
||||
- Linked/associated parts
|
||||
|
||||
Usage:
|
||||
from optimization_engine.extractors.introspect_part import introspect_part
|
||||
|
||||
result = introspect_part("path/to/model.prt")
|
||||
print(f"Mass: {result['mass_properties']['mass_kg']} kg")
|
||||
print(f"Expressions: {result['expressions']['user_count']}")
|
||||
|
||||
Dependencies:
|
||||
- NX installed with run_journal.exe
|
||||
- SPLM_LICENSE_SERVER environment variable
|
||||
|
||||
Author: Atomizer
|
||||
Created: 2025-12-19
|
||||
Version: 1.0
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
|
||||
# NX installation path
|
||||
NX_INSTALL_PATH = r"C:\Program Files\Siemens\DesigncenterNX2512"
|
||||
RUN_JOURNAL_EXE = os.path.join(NX_INSTALL_PATH, "NXBIN", "run_journal.exe")
|
||||
JOURNAL_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
|
||||
"nx_journals", "introspect_part.py")
|
||||
|
||||
# License server
|
||||
LICENSE_SERVER = "28000@dalidou;28000@100.80.199.40"
|
||||
|
||||
|
||||
def introspect_part(
|
||||
prt_file_path: str,
|
||||
output_dir: Optional[str] = None,
|
||||
timeout_seconds: int = 120,
|
||||
verbose: bool = True
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Run comprehensive introspection on an NX .prt file.
|
||||
|
||||
Args:
|
||||
prt_file_path: Path to the .prt file
|
||||
output_dir: Directory for output JSON (defaults to prt directory)
|
||||
timeout_seconds: Maximum time to wait for journal execution
|
||||
verbose: Print progress messages
|
||||
|
||||
Returns:
|
||||
Dictionary with introspection results:
|
||||
{
|
||||
'success': bool,
|
||||
'part_file': str,
|
||||
'expressions': {
|
||||
'user': [...],
|
||||
'internal': [...],
|
||||
'user_count': int,
|
||||
'total_count': int
|
||||
},
|
||||
'mass_properties': {
|
||||
'mass_kg': float,
|
||||
'mass_g': float,
|
||||
'volume_mm3': float,
|
||||
'surface_area_mm2': float,
|
||||
'center_of_gravity_mm': [x, y, z]
|
||||
},
|
||||
'materials': {
|
||||
'assigned': [...],
|
||||
'available': [...]
|
||||
},
|
||||
'bodies': {
|
||||
'solid_bodies': [...],
|
||||
'sheet_bodies': [...],
|
||||
'counts': {...}
|
||||
},
|
||||
'attributes': [...],
|
||||
'groups': [...],
|
||||
'features': {
|
||||
'total_count': int,
|
||||
'by_type': {...}
|
||||
},
|
||||
'datums': {...},
|
||||
'units': {...},
|
||||
'linked_parts': {...}
|
||||
}
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If prt file doesn't exist
|
||||
RuntimeError: If journal execution fails
|
||||
"""
|
||||
prt_path = Path(prt_file_path)
|
||||
if not prt_path.exists():
|
||||
raise FileNotFoundError(f"Part file not found: {prt_file_path}")
|
||||
|
||||
if output_dir is None:
|
||||
output_dir = str(prt_path.parent)
|
||||
|
||||
output_file = os.path.join(output_dir, "_temp_introspection.json")
|
||||
|
||||
# Remove old output file if exists
|
||||
if os.path.exists(output_file):
|
||||
os.remove(output_file)
|
||||
|
||||
if verbose:
|
||||
print(f"[INTROSPECT] Part: {prt_path.name}")
|
||||
print(f"[INTROSPECT] Output: {output_dir}")
|
||||
|
||||
# Build PowerShell command (learned workaround - see LAC)
|
||||
# Using [Environment]::SetEnvironmentVariable() for reliable license server setting
|
||||
ps_command = (
|
||||
f"[Environment]::SetEnvironmentVariable('SPLM_LICENSE_SERVER', '{LICENSE_SERVER}', 'Process'); "
|
||||
f"& '{RUN_JOURNAL_EXE}' '{JOURNAL_PATH}' -args '{prt_file_path}' '{output_dir}' 2>&1"
|
||||
)
|
||||
|
||||
cmd = ["powershell", "-Command", ps_command]
|
||||
|
||||
if verbose:
|
||||
print(f"[INTROSPECT] Executing NX journal...")
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout_seconds
|
||||
)
|
||||
|
||||
if verbose and result.stdout:
|
||||
# Print key lines from output
|
||||
for line in result.stdout.split('\n'):
|
||||
if '[INTROSPECT]' in line:
|
||||
print(line)
|
||||
|
||||
if result.returncode != 0 and verbose:
|
||||
print(f"[INTROSPECT] Warning: Non-zero return code: {result.returncode}")
|
||||
if result.stderr:
|
||||
print(f"[INTROSPECT] Stderr: {result.stderr[:500]}")
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
raise RuntimeError(f"Journal execution timed out after {timeout_seconds}s")
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Journal execution failed: {e}")
|
||||
|
||||
# Wait for output file (NX may still be writing)
|
||||
wait_start = time.time()
|
||||
while not os.path.exists(output_file) and (time.time() - wait_start) < 10:
|
||||
time.sleep(0.5)
|
||||
|
||||
if not os.path.exists(output_file):
|
||||
raise RuntimeError(f"Introspection output file not created: {output_file}")
|
||||
|
||||
# Read and return results
|
||||
with open(output_file, 'r') as f:
|
||||
results = json.load(f)
|
||||
|
||||
if verbose:
|
||||
if results.get('success'):
|
||||
print(f"[INTROSPECT] Success!")
|
||||
print(f"[INTROSPECT] Expressions: {results['expressions']['user_count']} user")
|
||||
print(f"[INTROSPECT] Mass: {results['mass_properties']['mass_kg']:.4f} kg")
|
||||
else:
|
||||
print(f"[INTROSPECT] Failed: {results.get('error', 'Unknown error')}")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def get_expressions_dict(introspection_result: Dict[str, Any]) -> Dict[str, float]:
|
||||
"""
|
||||
Convert introspection result to simple {name: value} dictionary of user expressions.
|
||||
|
||||
Args:
|
||||
introspection_result: Result from introspect_part()
|
||||
|
||||
Returns:
|
||||
Dictionary mapping expression names to values
|
||||
"""
|
||||
expressions = {}
|
||||
for expr in introspection_result.get('expressions', {}).get('user', []):
|
||||
name = expr.get('name')
|
||||
value = expr.get('value')
|
||||
if name and value is not None:
|
||||
expressions[name] = value
|
||||
return expressions
|
||||
|
||||
|
||||
def get_expression_value(introspection_result: Dict[str, Any], name: str) -> Optional[float]:
|
||||
"""
|
||||
Get a specific expression value from introspection result.
|
||||
|
||||
Args:
|
||||
introspection_result: Result from introspect_part()
|
||||
name: Expression name
|
||||
|
||||
Returns:
|
||||
Expression value or None if not found
|
||||
"""
|
||||
for expr in introspection_result.get('expressions', {}).get('user', []):
|
||||
if expr.get('name') == name:
|
||||
return expr.get('value')
|
||||
return None
|
||||
|
||||
|
||||
def print_introspection_summary(result: Dict[str, Any]) -> None:
|
||||
"""Print a formatted summary of introspection results."""
|
||||
print("\n" + "="*60)
|
||||
print(f"INTROSPECTION SUMMARY: {result.get('part_file', 'Unknown')}")
|
||||
print("="*60)
|
||||
|
||||
# Mass
|
||||
mp = result.get('mass_properties', {})
|
||||
print(f"\nMASS PROPERTIES:")
|
||||
print(f" Mass: {mp.get('mass_kg', 0):.4f} kg ({mp.get('mass_g', 0):.2f} g)")
|
||||
print(f" Volume: {mp.get('volume_mm3', 0):.2f} mm³")
|
||||
print(f" Surface Area: {mp.get('surface_area_mm2', 0):.2f} mm²")
|
||||
cog = mp.get('center_of_gravity_mm', [0, 0, 0])
|
||||
print(f" Center of Gravity: [{cog[0]:.2f}, {cog[1]:.2f}, {cog[2]:.2f}] mm")
|
||||
|
||||
# Bodies
|
||||
bodies = result.get('bodies', {})
|
||||
counts = bodies.get('counts', {})
|
||||
print(f"\nBODIES:")
|
||||
print(f" Solid: {counts.get('solid', 0)}")
|
||||
print(f" Sheet: {counts.get('sheet', 0)}")
|
||||
|
||||
# Materials
|
||||
mats = result.get('materials', {})
|
||||
print(f"\nMATERIALS:")
|
||||
for mat in mats.get('assigned', []):
|
||||
print(f" {mat.get('name', 'Unknown')}")
|
||||
props = mat.get('properties', {})
|
||||
if 'Density' in props:
|
||||
print(f" Density: {props['Density']}")
|
||||
if 'YoungModulus' in props:
|
||||
print(f" Young's Modulus: {props['YoungModulus']}")
|
||||
|
||||
# Expressions
|
||||
exprs = result.get('expressions', {})
|
||||
print(f"\nEXPRESSIONS: {exprs.get('user_count', 0)} user, {len(exprs.get('internal', []))} internal")
|
||||
user_exprs = exprs.get('user', [])
|
||||
if user_exprs:
|
||||
print(" User expressions (first 10):")
|
||||
for expr in user_exprs[:10]:
|
||||
units = f" [{expr.get('units', '')}]" if expr.get('units') else ""
|
||||
print(f" {expr.get('name')}: {expr.get('value')}{units}")
|
||||
if len(user_exprs) > 10:
|
||||
print(f" ... and {len(user_exprs) - 10} more")
|
||||
|
||||
# Features
|
||||
feats = result.get('features', {})
|
||||
print(f"\nFEATURES: {feats.get('total_count', 0)} total")
|
||||
by_type = feats.get('by_type', {})
|
||||
if by_type:
|
||||
top_types = sorted(by_type.items(), key=lambda x: x[1], reverse=True)[:5]
|
||||
for feat_type, count in top_types:
|
||||
print(f" {feat_type}: {count}")
|
||||
|
||||
# Units
|
||||
units = result.get('units', {})
|
||||
print(f"\nUNITS: {units.get('system', 'Unknown')}")
|
||||
|
||||
print("="*60 + "\n")
|
||||
|
||||
|
||||
# CLI interface
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python introspect_part.py <prt_file_path> [output_dir]")
|
||||
sys.exit(1)
|
||||
|
||||
prt_path = sys.argv[1]
|
||||
out_dir = sys.argv[2] if len(sys.argv) > 2 else None
|
||||
|
||||
try:
|
||||
result = introspect_part(prt_path, out_dir, verbose=True)
|
||||
print_introspection_summary(result)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
sys.exit(1)
|
||||
@@ -36,11 +36,15 @@ 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"
|
||||
)
|
||||
# Import NX path from centralized config
|
||||
try:
|
||||
from config import NX_BIN_DIR
|
||||
NX_BIN_PATH = str(NX_BIN_DIR)
|
||||
except ImportError:
|
||||
NX_BIN_PATH = os.environ.get(
|
||||
"NX_BIN_PATH",
|
||||
r"C:\Program Files\Siemens\DesigncenterNX2512\NXBIN"
|
||||
)
|
||||
|
||||
# Journal template for expression operations
|
||||
EXPRESSION_OPERATIONS_JOURNAL = '''
|
||||
|
||||
@@ -34,11 +34,15 @@ 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"
|
||||
)
|
||||
# Import NX path from centralized config
|
||||
try:
|
||||
from config import NX_BIN_DIR
|
||||
NX_BIN_PATH = str(NX_BIN_DIR)
|
||||
except ImportError:
|
||||
NX_BIN_PATH = os.environ.get(
|
||||
"NX_BIN_PATH",
|
||||
r"C:\Program Files\Siemens\DesigncenterNX2512\NXBIN"
|
||||
)
|
||||
|
||||
# Journal template for feature operations
|
||||
FEATURE_OPERATIONS_JOURNAL = '''
|
||||
|
||||
@@ -30,11 +30,15 @@ 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"
|
||||
)
|
||||
# Import NX path from centralized config
|
||||
try:
|
||||
from config import NX_BIN_DIR
|
||||
NX_BIN_PATH = str(NX_BIN_DIR)
|
||||
except ImportError:
|
||||
NX_BIN_PATH = os.environ.get(
|
||||
"NX_BIN_PATH",
|
||||
r"C:\Program Files\Siemens\DesigncenterNX2512\NXBIN"
|
||||
)
|
||||
|
||||
# Journal template for geometry query operations
|
||||
GEOMETRY_QUERY_JOURNAL = '''
|
||||
|
||||
@@ -32,11 +32,16 @@ import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any, List
|
||||
|
||||
# NX installation path (configurable)
|
||||
NX_BIN_PATH = os.environ.get(
|
||||
"NX_BIN_PATH",
|
||||
r"C:\Program Files\Siemens\NX2506\NXBIN"
|
||||
)
|
||||
# Import NX path from centralized config
|
||||
try:
|
||||
from config import NX_BIN_DIR
|
||||
NX_BIN_PATH = str(NX_BIN_DIR)
|
||||
except ImportError:
|
||||
# Fallback if config not available
|
||||
NX_BIN_PATH = os.environ.get(
|
||||
"NX_BIN_PATH",
|
||||
r"C:\Program Files\Siemens\DesigncenterNX2512\NXBIN"
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
||||
@@ -31,11 +31,15 @@ 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"
|
||||
)
|
||||
# Import NX path from centralized config
|
||||
try:
|
||||
from config import NX_BIN_DIR
|
||||
NX_BIN_PATH = str(NX_BIN_DIR)
|
||||
except ImportError:
|
||||
NX_BIN_PATH = os.environ.get(
|
||||
"NX_BIN_PATH",
|
||||
r"C:\Program Files\Siemens\DesigncenterNX2512\NXBIN"
|
||||
)
|
||||
|
||||
# Journal template for part operations
|
||||
PART_OPERATIONS_JOURNAL = '''
|
||||
|
||||
@@ -32,11 +32,15 @@ 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"
|
||||
)
|
||||
# Import NX path from centralized config
|
||||
try:
|
||||
from config import NX_BIN_DIR
|
||||
NX_BIN_PATH = str(NX_BIN_DIR)
|
||||
except ImportError:
|
||||
NX_BIN_PATH = os.environ.get(
|
||||
"NX_BIN_PATH",
|
||||
r"C:\Program Files\Siemens\DesigncenterNX2512\NXBIN"
|
||||
)
|
||||
|
||||
# Journal template for BDF export
|
||||
BDF_EXPORT_JOURNAL = '''
|
||||
|
||||
@@ -107,18 +107,25 @@ class NXSolver:
|
||||
|
||||
def _find_journal_runner(self) -> Path:
|
||||
"""Find the NX journal runner executable."""
|
||||
# Simcenter3D has run_journal.exe for batch execution
|
||||
# First check the provided nx_install_dir
|
||||
if self.nx_install_dir:
|
||||
direct_path = self.nx_install_dir / "NXBIN" / "run_journal.exe"
|
||||
if direct_path.exists():
|
||||
return direct_path
|
||||
|
||||
# Fallback: check common installation paths
|
||||
possible_exes = [
|
||||
Path(f"C:/Program Files/Siemens/Simcenter3D_{self.nastran_version}/NXBIN/run_journal.exe"),
|
||||
Path(f"C:/Program Files/Siemens/NX{self.nastran_version}/NXBIN/run_journal.exe"),
|
||||
Path(f"C:/Program Files/Siemens/DesigncenterNX{self.nastran_version}/NXBIN/run_journal.exe"),
|
||||
]
|
||||
|
||||
for exe in possible_exes:
|
||||
if exe.exists():
|
||||
return exe
|
||||
|
||||
# Return first guess (will error in __init__ if doesn't exist)
|
||||
return possible_exes[0]
|
||||
# Return the direct path (will error in __init__ if doesn't exist)
|
||||
return self.nx_install_dir / "NXBIN" / "run_journal.exe" if self.nx_install_dir else possible_exes[0]
|
||||
|
||||
def _find_solver_executable(self) -> Path:
|
||||
"""Find the Nastran solver executable."""
|
||||
@@ -440,9 +447,35 @@ sys.argv = ['', {argv_str}] # Set argv for the main function
|
||||
# Set up environment for Simcenter/NX
|
||||
env = os.environ.copy()
|
||||
|
||||
# Use existing SPLM_LICENSE_SERVER from environment if set
|
||||
# Only set if not already defined (respects user's license configuration)
|
||||
if 'SPLM_LICENSE_SERVER' not in env or not env['SPLM_LICENSE_SERVER']:
|
||||
# Get SPLM_LICENSE_SERVER - prefer system registry (most up-to-date) over process env
|
||||
license_server = ''
|
||||
|
||||
# First try system-level environment (Windows registry) - this is the authoritative source
|
||||
import subprocess as sp
|
||||
try:
|
||||
result = sp.run(
|
||||
['reg', 'query', 'HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment', '/v', 'SPLM_LICENSE_SERVER'],
|
||||
capture_output=True, text=True, timeout=5
|
||||
)
|
||||
if result.returncode == 0:
|
||||
# Parse: " SPLM_LICENSE_SERVER REG_SZ value"
|
||||
for line in result.stdout.splitlines():
|
||||
if 'SPLM_LICENSE_SERVER' in line:
|
||||
parts = line.split('REG_SZ')
|
||||
if len(parts) > 1:
|
||||
license_server = parts[1].strip()
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fall back to process environment if registry query failed
|
||||
if not license_server:
|
||||
license_server = env.get('SPLM_LICENSE_SERVER', '')
|
||||
|
||||
if license_server:
|
||||
env['SPLM_LICENSE_SERVER'] = license_server
|
||||
print(f"[NX SOLVER] Using license server: {license_server}")
|
||||
else:
|
||||
env['SPLM_LICENSE_SERVER'] = '29000@localhost'
|
||||
print(f"[NX SOLVER] WARNING: SPLM_LICENSE_SERVER not set, using default: {env['SPLM_LICENSE_SERVER']}")
|
||||
|
||||
|
||||
@@ -53,6 +53,111 @@ import NXOpen.Assemblies
|
||||
import NXOpen.CAE
|
||||
|
||||
|
||||
def extract_part_mass(theSession, part, output_dir):
|
||||
"""
|
||||
Extract mass from a part using NX MeasureManager.
|
||||
|
||||
Writes mass to _temp_mass.txt and _temp_part_properties.json in output_dir.
|
||||
|
||||
Args:
|
||||
theSession: NXOpen.Session
|
||||
part: NXOpen.Part to extract mass from
|
||||
output_dir: Directory to write temp files
|
||||
|
||||
Returns:
|
||||
Mass in kg (float)
|
||||
"""
|
||||
import json
|
||||
|
||||
results = {
|
||||
'part_file': part.Name,
|
||||
'mass_kg': 0.0,
|
||||
'mass_g': 0.0,
|
||||
'volume_mm3': 0.0,
|
||||
'surface_area_mm2': 0.0,
|
||||
'center_of_gravity_mm': [0.0, 0.0, 0.0],
|
||||
'num_bodies': 0,
|
||||
'success': False,
|
||||
'error': None
|
||||
}
|
||||
|
||||
try:
|
||||
# Get all solid bodies
|
||||
bodies = []
|
||||
for body in part.Bodies:
|
||||
if body.IsSolidBody:
|
||||
bodies.append(body)
|
||||
|
||||
results['num_bodies'] = len(bodies)
|
||||
|
||||
if not bodies:
|
||||
results['error'] = "No solid bodies found"
|
||||
raise ValueError("No solid bodies found in part")
|
||||
|
||||
# Get the measure manager
|
||||
measureManager = part.MeasureManager
|
||||
|
||||
# Get unit collection and build mass_units array
|
||||
# API requires: [Area, Volume, Mass, Length] base units
|
||||
uc = part.UnitCollection
|
||||
mass_units = [
|
||||
uc.GetBase("Area"),
|
||||
uc.GetBase("Volume"),
|
||||
uc.GetBase("Mass"),
|
||||
uc.GetBase("Length")
|
||||
]
|
||||
|
||||
# Create mass properties measurement
|
||||
measureBodies = measureManager.NewMassProperties(mass_units, 0.99, bodies)
|
||||
|
||||
if measureBodies:
|
||||
results['mass_kg'] = measureBodies.Mass
|
||||
results['mass_g'] = results['mass_kg'] * 1000.0
|
||||
|
||||
try:
|
||||
results['volume_mm3'] = measureBodies.Volume
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
results['surface_area_mm2'] = measureBodies.Area
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
cog = measureBodies.Centroid
|
||||
if cog:
|
||||
results['center_of_gravity_mm'] = [cog.X, cog.Y, cog.Z]
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
measureBodies.Dispose()
|
||||
except:
|
||||
pass
|
||||
|
||||
results['success'] = True
|
||||
|
||||
except Exception as e:
|
||||
results['error'] = str(e)
|
||||
results['success'] = False
|
||||
|
||||
# Write results to JSON file
|
||||
output_file = os.path.join(output_dir, "_temp_part_properties.json")
|
||||
with open(output_file, 'w') as f:
|
||||
json.dump(results, f, indent=2)
|
||||
|
||||
# Write simple mass value for backward compatibility
|
||||
mass_file = os.path.join(output_dir, "_temp_mass.txt")
|
||||
with open(mass_file, 'w') as f:
|
||||
f.write(str(results['mass_kg']))
|
||||
|
||||
if not results['success']:
|
||||
raise ValueError(results['error'])
|
||||
|
||||
return results['mass_kg']
|
||||
|
||||
|
||||
def find_or_open_part(theSession, part_path):
|
||||
"""
|
||||
Find a part if already loaded, otherwise open it.
|
||||
@@ -296,6 +401,15 @@ def solve_assembly_fem_workflow(theSession, sim_file_path, solution_name, expres
|
||||
partSaveStatus_blank.Dispose()
|
||||
print(f"[JOURNAL] M1_Blank saved")
|
||||
|
||||
# STEP 2a: EXTRACT MASS FROM M1_BLANK
|
||||
# Extract mass using MeasureManager after geometry is updated
|
||||
print(f"[JOURNAL] Extracting mass from M1_Blank...")
|
||||
try:
|
||||
mass_kg = extract_part_mass(theSession, workPart, working_dir)
|
||||
print(f"[JOURNAL] Mass extracted: {mass_kg:.6f} kg ({mass_kg * 1000:.2f} g)")
|
||||
except Exception as mass_err:
|
||||
print(f"[JOURNAL] WARNING: Mass extraction failed: {mass_err}")
|
||||
|
||||
updated_expressions = list(expression_updates.keys())
|
||||
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user