# Assembly FEM Optimization Workflow This document describes the multi-part assembly FEM workflow used when optimizing complex assemblies with `.afm` (Assembly FEM) files. ## CRITICAL: Working Copy Requirement **NEVER run optimization directly on user's master model files.** Before any optimization run, ALL model files must be copied to the study's working directory: ``` Source (NEVER MODIFY) Working Copy (optimization runs here) ──────────────────────────────────────────────────────────────────────────── C:/Users/.../M1-Gigabit/Latest/ studies/{study}/1_setup/model/ ├── M1_Blank.prt → ├── M1_Blank.prt ├── M1_Blank_fem1.fem → ├── M1_Blank_fem1.fem ├── M1_Blank_fem1_i.prt → ├── M1_Blank_fem1_i.prt ├── M1_Vertical_Support_Skeleton.prt → ├── M1_Vertical_Support_Skeleton.prt ├── ASSY_M1_assyfem1.afm → ├── ASSY_M1_assyfem1.afm └── ASSY_M1_assyfem1_sim1.sim → └── ASSY_M1_assyfem1_sim1.sim ``` **Why**: Optimization iteratively modifies expressions, meshes, and saves files. If corruption occurs during iteration (solver crash, bad parameter combo), the working copy can be deleted and re-copied. Master files remain safe. **Files to Copy**: - `*.prt` - All part files (geometry + idealized) - `*.fem` - All FEM files - `*.afm` - Assembly FEM files - `*.sim` - Simulation files - `*.exp` - Expression files (if any) ## Overview Assembly FEMs have a more complex dependency chain than single-part simulations: ``` .prt (geometry) → _fem1.fem (component mesh) → .afm (assembly mesh) → .sim (solution) ``` Each level must be updated in sequence when design parameters change. ## When This Workflow Applies This workflow is automatically triggered when: - The working directory contains `.afm` files - Multiple `.fem` files exist (component meshes) - Multiple `.prt` files exist (component geometry) Examples: - M1 Mirror assembly (M1_Blank + M1_Vertical_Support_Skeleton) - Multi-component mechanical assemblies - Any NX assembly where components have separate FEM files ## The 4-Step Workflow ### Step 1: Update Expressions in Geometry Part (.prt) ``` Open M1_Blank.prt ├── Find and update design expressions │ ├── whiffle_min = 42.5 │ ├── whiffle_outer_to_vertical = 75.0 │ └── inner_circular_rib_dia = 550.0 ├── Rebuild geometry (DoUpdate) └── Save part ``` The `.prt` file contains the parametric CAD model with expressions that drive dimensions. These expressions are updated with new design parameter values, then the geometry is rebuilt. ### Step 1b: Update ALL Linked Geometry Parts (CRITICAL!) **⚠️ THIS STEP IS CRITICAL - SKIPPING IT CAUSES CORRUPT RESULTS ⚠️** ``` For each geometry part with linked expressions: ├── Open M1_Vertical_Support_Skeleton.prt ├── DoUpdate() - propagate linked expression changes ├── Geometry rebuilds to match M1_Blank └── Save part ``` **Why this is critical:** - M1_Vertical_Support_Skeleton has expressions linked to M1_Blank - When M1_Blank geometry changes, the support skeleton MUST also update - If not updated, FEM nodes will be at OLD positions → nodes not coincident → merge fails - Result: "billion nm" RMS values (corrupt displacement data) **Rule: YOU MUST UPDATE ALL GEOMETRY PARTS UNDER THE .sim FILE!** - If there are 5 geometry parts, update all 5 - If there are 10 geometry parts, update all 10 - Unless explicitly told otherwise in the study config ### Step 2: Update Component FEM Files (.fem) ``` For each component FEM: ├── Open M1_Blank_fem1.fem │ ├── UpdateFemodel() - regenerates mesh from updated geometry │ └── Save FEM ├── Open M1_Vertical_Support_Skeleton_fem1.fem │ ├── UpdateFemodel() │ └── Save FEM └── ... (repeat for all component FEMs) ``` Each component FEM is linked to its source geometry. `UpdateFemodel()` regenerates the mesh based on the updated geometry. ### Step 3: Update Assembly FEM (.afm) ``` Open ASSY_M1_assyfem1.afm ├── UpdateFemodel() - updates assembly mesh ├── Merge coincident nodes (at component interfaces) ├── Resolve labeling conflicts (duplicate node/element IDs) └── Save AFM ``` The assembly FEM combines component meshes. This step: - Reconnects meshes at shared interfaces - Resolves numbering conflicts between component meshes - Ensures mesh continuity for accurate analysis ### Step 4: Solve Simulation (.sim) ``` Open ASSY_M1_assyfem1_sim1.sim ├── Execute solve │ ├── Foreground mode for all solutions │ └── or Background mode for specific solution └── Save simulation ``` The simulation file references the assembly FEM and contains solution setup (loads, constraints, subcases). ## File Dependencies ``` M1 Mirror Example: M1_Blank.prt ─────────────────────> M1_Blank_fem1.fem ─────────┐ │ │ │ │ (expressions) │ (component mesh) │ ↓ ↓ │ M1_Vertical_Support_Skeleton.prt ──> M1_..._Skeleton_fem1.fem ─┤ │ ↓ ASSY_M1_assyfem1.afm ──> ASSY_M1_assyfem1_sim1.sim (assembly mesh) (solution) ``` ## API Functions Used | Step | NX API Call | Purpose | |------|-------------|---------| | 1 | `OpenBase()` | Open .prt file | | 1 | `ImportFromFile()` | Import expressions from .exp file (preferred) | | 1 | `DoUpdate()` | Rebuild geometry | | 2-3 | `UpdateFemodel()` | Regenerate mesh from geometry | | 3 | `DuplicateNodesCheckBuilder` | Merge coincident nodes at interfaces | | 3 | `MergeOccurrenceNodes = True` | Critical: enables cross-component merge | | 4 | `SolveAllSolutions()` | Execute FEA (Foreground mode recommended) ### Expression Update Method The recommended approach uses expression file import: ```python # Write expressions to .exp file with open(exp_path, 'w') as f: for name, value in expressions.items(): unit = get_unit_for_expression(name) f.write(f"[{unit}]{name}={value}\n") # Import into part modified, errors = workPart.Expressions.ImportFromFile( exp_path, NXOpen.ExpressionCollection.ImportMode.Replace ) ``` This is more reliable than `EditExpressionWithUnits()` for batch updates. ## Error Handling Common issues and solutions: ### "Update undo happened" - Geometry update failed due to constraint violations - Check expression values are within valid ranges - May need to adjust parameter bounds ### "This operation can only be done on the work part" - Work part not properly set before operation - Use `SetWork()` to make target part the work part ### Node merge warnings - Manual intervention may be needed for complex interfaces - Check mesh connectivity in NX after solve ### "Billion nm" RMS values - Indicates node merging failed - coincident nodes not properly merged - Check `MergeOccurrenceNodes = True` is set - Verify tolerance (0.01 mm recommended) - Run node merge after every FEM update, not just once ## Configuration The workflow auto-detects assembly FEMs, but you can configure behavior: ```json { "nx_settings": { "expression_part": "M1_Blank", // Override auto-detection "component_fems": [ // Explicit list of FEMs to update "M1_Blank_fem1.fem", "M1_Vertical_Support_Skeleton_fem1.fem" ], "afm_file": "ASSY_M1_assyfem1.afm" } } ``` ## Implementation Reference See `optimization_engine/solve_simulation.py` for the full implementation: - `detect_assembly_fem()` - Detects if assembly workflow needed - `update_expressions_in_part()` - Step 1 implementation - `update_fem_part()` - Step 2 implementation - `update_assembly_fem()` - Step 3 implementation - `solve_simulation_file()` - Step 4 implementation ## HEEDS-Style Iteration Folder Management (V9+) For complex assemblies, each optimization trial uses a fresh copy of the master model: ``` study_name/ ├── 1_setup/ │ └── model/ # Master model files (NEVER MODIFY) │ ├── ASSY_M1.prt │ ├── ASSY_M1_assyfem1.afm │ ├── ASSY_M1_assyfem1_sim1.sim │ ├── M1_Blank.prt │ ├── M1_Blank_fem1.fem │ └── ... ├── 2_iterations/ │ ├── iter0/ # Trial 0 working copy │ │ ├── [all model files] │ │ ├── params.exp # Expression values for this trial │ │ └── results/ # OP2, Zernike CSV, etc. │ ├── iter1/ # Trial 1 working copy │ └── ... └── 3_results/ └── study.db # Optuna database ``` ### Why Fresh Copies Per Iteration? 1. **Corruption isolation**: If mesh regeneration fails mid-trial, only that iteration is affected 2. **Reproducibility**: Can re-run any trial by using its params.exp 3. **Debugging**: All intermediate files preserved for post-mortem analysis 4. **Parallelization**: Multiple NX sessions could run different iterations (future) ### Iteration Folder Contents | File | Purpose | |------|---------| | `*.prt, *.fem, *.afm, *.sim` | Fresh copy of all NX model files | | `params.exp` | Expression file with trial parameter values | | `*-solution_1.op2` | Nastran results (after solve) | | `results/zernike_trial_N.csv` | Extracted Zernike metrics | ### 0-Based Iteration Numbering Iterations are numbered starting from 0 to match Optuna trial numbers: - `iter0` = Optuna trial 0 = Dashboard shows trial 0 - `iter1` = Optuna trial 1 = Dashboard shows trial 1 This ensures cross-referencing between dashboard, database, and file system is straightforward. ## Multi-Subcase Solutions For gravity analysis at multiple orientations, use subcases: ``` Simulation Setup in NX: ├── Subcase 1: 90 deg elevation (zenith/polishing) ├── Subcase 2: 20 deg elevation (low angle reference) ├── Subcase 3: 40 deg elevation └── Subcase 4: 60 deg elevation ``` ### Solving All Subcases Use `solution_name=None` or `solve_all_subcases=True` to ensure all subcases are solved: ```json "nx_settings": { "solution_name": "Solution 1", "solve_all_subcases": true } ``` ### Subcase ID Mapping NX subcase IDs (1, 2, 3, 4) may not match the angle labels. Always define explicit mapping: ```json "zernike_settings": { "subcases": ["1", "2", "3", "4"], "subcase_labels": { "1": "90deg", "2": "20deg", "3": "40deg", "4": "60deg" }, "reference_subcase": "2" } ``` ## Tips 1. **Start with baseline solve**: Before optimization, manually verify the full workflow completes in NX 2. **Check mesh quality**: Poor mesh quality after updates can cause solve failures 3. **Monitor memory**: Assembly FEMs with many components use significant memory 4. **Use Foreground mode**: For multi-subcase solutions, Foreground mode ensures all subcases complete 5. **Validate OP2 data**: Check for corrupt results (all zeros, unrealistic magnitudes) before processing 6. **Preserve user NX sessions**: NXSessionManager tracks PIDs to avoid closing user's NX instances