diff --git a/docs/protocols/operations/OP_01_CREATE_STUDY.md b/docs/protocols/operations/OP_01_CREATE_STUDY.md index 56ad0658..4ee9a6c6 100644 --- a/docs/protocols/operations/OP_01_CREATE_STUDY.md +++ b/docs/protocols/operations/OP_01_CREATE_STUDY.md @@ -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 + +![Model - Top View](1_setup/model_name_Top.png) +*Top view description* + +![Model - Isometric View](1_setup/model_name_iso.png) +*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 diff --git a/docs/protocols/system/SYS_12_EXTRACTOR_LIBRARY.md b/docs/protocols/system/SYS_12_EXTRACTOR_LIBRARY.md index c846cff4..96b26e87 100644 --- a/docs/protocols/system/SYS_12_EXTRACTOR_LIBRARY.md +++ b/docs/protocols/system/SYS_12_EXTRACTOR_LIBRARY.md @@ -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) | diff --git a/optimization_engine/extractors/__init__.py b/optimization_engine/extractors/__init__.py index 79b452f2..0028e207 100644 --- a/optimization_engine/extractors/__init__.py +++ b/optimization_engine/extractors/__init__.py @@ -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', ] diff --git a/optimization_engine/extractors/introspect_part.py b/optimization_engine/extractors/introspect_part.py new file mode 100644 index 00000000..e468b362 --- /dev/null +++ b/optimization_engine/extractors/introspect_part.py @@ -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 [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) diff --git a/optimization_engine/hooks/nx_cad/expression_manager.py b/optimization_engine/hooks/nx_cad/expression_manager.py index 4fa196e1..fe253634 100644 --- a/optimization_engine/hooks/nx_cad/expression_manager.py +++ b/optimization_engine/hooks/nx_cad/expression_manager.py @@ -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 = ''' diff --git a/optimization_engine/hooks/nx_cad/feature_manager.py b/optimization_engine/hooks/nx_cad/feature_manager.py index 487ace6c..d285ff65 100644 --- a/optimization_engine/hooks/nx_cad/feature_manager.py +++ b/optimization_engine/hooks/nx_cad/feature_manager.py @@ -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 = ''' diff --git a/optimization_engine/hooks/nx_cad/geometry_query.py b/optimization_engine/hooks/nx_cad/geometry_query.py index 869b73a5..8e90e28f 100644 --- a/optimization_engine/hooks/nx_cad/geometry_query.py +++ b/optimization_engine/hooks/nx_cad/geometry_query.py @@ -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 = ''' diff --git a/optimization_engine/hooks/nx_cad/model_introspection.py b/optimization_engine/hooks/nx_cad/model_introspection.py index e1f3741e..f7a68b17 100644 --- a/optimization_engine/hooks/nx_cad/model_introspection.py +++ b/optimization_engine/hooks/nx_cad/model_introspection.py @@ -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" + ) # ============================================================================= diff --git a/optimization_engine/hooks/nx_cad/part_manager.py b/optimization_engine/hooks/nx_cad/part_manager.py index 770e9064..ee1b483a 100644 --- a/optimization_engine/hooks/nx_cad/part_manager.py +++ b/optimization_engine/hooks/nx_cad/part_manager.py @@ -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 = ''' diff --git a/optimization_engine/hooks/nx_cae/solver_manager.py b/optimization_engine/hooks/nx_cae/solver_manager.py index 4e0050d2..023c3785 100644 --- a/optimization_engine/hooks/nx_cae/solver_manager.py +++ b/optimization_engine/hooks/nx_cae/solver_manager.py @@ -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 = ''' diff --git a/optimization_engine/nx_solver.py b/optimization_engine/nx_solver.py index 2e269488..def9784d 100644 --- a/optimization_engine/nx_solver.py +++ b/optimization_engine/nx_solver.py @@ -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']}") diff --git a/optimization_engine/solve_simulation.py b/optimization_engine/solve_simulation.py index 58c931fd..bf54765f 100644 --- a/optimization_engine/solve_simulation.py +++ b/optimization_engine/solve_simulation.py @@ -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: