# NX Multi-Solution Solve Protocol

## Critical Finding: SolveAllSolutions API Required for Multi-Solution Models

**Date**: November 23, 2025
**Last Updated**: November 23, 2025
**Protocol**: Multi-Solution Nastran Solve
**Affected Models**: Any NX simulation with multiple solutions (e.g., static + modal, thermal + structural)

---

## Problem Statement

When an NX simulation contains multiple solutions (e.g., Solution 1 = Static Analysis, Solution 2 = Modal Analysis), using `SolveChainOfSolutions()` with Background mode **does not wait for all solutions to complete** before returning control to Python. This causes:

1. **Missing OP2 Files**: Only the first solution's OP2 file is generated
2. **Stale Data**: Subsequent trials read old OP2 files from previous runs
3. **Identical Results**: All trials show the same values for results from missing solutions
4. **Silent Failures**: No error is raised - the solve completes but files are not written

### Example Scenario

**Drone Gimbal Arm Optimization**:
- Solution 1: Static analysis (stress, displacement)
- Solution 2: Modal analysis (frequency)

**Symptoms**:
- All 100 trials showed **identical frequency** (27.476 Hz)
- Only `beam_sim1-solution_1.op2` was created
- `beam_sim1-solution_2.op2` was never regenerated after Trial 0
- Both `.dat` files were written correctly, but solve didn't wait for completion

---

## Root Cause

```python
# WRONG APPROACH (doesn't wait for completion)
psolutions1 = []
solution_idx = 1
while True:
    solution_obj_name = f"Solution[Solution {solution_idx}]"
    simSolution = simSimulation1.FindObject(solution_obj_name)
    if simSolution:
        psolutions1.append(simSolution)
        solution_idx += 1
    else:
        break

theCAESimSolveManager.SolveChainOfSolutions(
    psolutions1,
    NXOpen.CAE.SimSolution.SolveOption.Solve,
    NXOpen.CAE.SimSolution.SetupCheckOption.CompleteDeepCheckAndOutputErrors,
    NXOpen.CAE.SimSolution.SolveMode.Background  # ❌ Returns immediately!
)
```

**Issue**: Background mode runs asynchronously and returns control to Python before all solutions finish solving.

---

## Correct Solution

### For Solving All Solutions

Use `SolveAllSolutions()` API with **Foreground mode**:

```python
# CORRECT APPROACH (waits for completion)
if solution_name:
    # Solve specific solution in background mode
    solution_obj_name = f"Solution[{solution_name}]"
    simSolution1 = simSimulation1.FindObject(solution_obj_name)
    psolutions1 = [simSolution1]

    numsolutionssolved1, numsolutionsfailed1, numsolutionsskipped1 = theCAESimSolveManager.SolveChainOfSolutions(
        psolutions1,
        NXOpen.CAE.SimSolution.SolveOption.Solve,
        NXOpen.CAE.SimSolution.SetupCheckOption.CompleteDeepCheckAndOutputErrors,
        NXOpen.CAE.SimSolution.SolveMode.Background
    )
else:
    # Solve ALL solutions using SolveAllSolutions API (Foreground mode)
    # This ensures all solutions (static + modal, etc.) complete before returning
    print(f"[JOURNAL] Solving all solutions using SolveAllSolutions API (Foreground mode)...")

    numsolutionssolved1, numsolutionsfailed1, numsolutionsskipped1 = theCAESimSolveManager.SolveAllSolutions(
        NXOpen.CAE.SimSolution.SolveOption.Solve,
        NXOpen.CAE.SimSolution.SetupCheckOption.CompleteCheckAndOutputErrors,
        NXOpen.CAE.SimSolution.SolveMode.Foreground,  # ✅ Blocks until complete
        False
    )
```

### Key Differences

| Aspect | SolveChainOfSolutions | SolveAllSolutions |
|--------|----------------------|-------------------|
| **Manual enumeration** | Required (loop through solutions) | Automatic (handles all solutions) |
| **Background mode behavior** | Returns immediately, async | N/A (Foreground recommended) |
| **Foreground mode behavior** | Blocks until complete | Blocks until complete ✅ |
| **Use case** | Specific solution selection | Solve all solutions |

---

## Implementation Location

**File**: `optimization_engine/solve_simulation.py`
**Lines**: 271-295

**When to use this protocol**:
- When `solution_name=None` is passed to `NXSolver.run_simulation()`
- Any simulation with multiple solutions that must all complete
- Multi-objective optimization requiring results from different analysis types

---

## Verification Steps

After implementing the fix, verify:

1. **Both .dat files are written** (one per solution)
   ```
   beam_sim1-solution_1.dat  # Static analysis
   beam_sim1-solution_2.dat  # Modal analysis
   ```

2. **Both .op2 files are created** with updated timestamps
   ```
   beam_sim1-solution_1.op2  # Contains stress, displacement
   beam_sim1-solution_2.op2  # Contains eigenvalues, mode shapes
   ```

3. **Results are unique per trial** - check that frequency values vary across trials

4. **Journal log shows**:
   ```
   [JOURNAL] Solving all solutions using SolveAllSolutions API (Foreground mode)...
   [JOURNAL] Solve completed!
   [JOURNAL]   Solutions solved: 2
   ```

---

## Solution Monitor Window Control (November 24, 2025)

### Problem: Monitor Window Pile-Up

When running optimization studies with multiple trials, NX opens solution monitor windows for each trial. These windows:
- Superpose on top of each other
- Cannot be easily closed programmatically
- Cause usability issues during long optimization runs
- Slow down the optimization process

### Solution: Automatic Monitor Disabling

The solution monitor is now automatically disabled when solving multiple solutions (when `solution_name=None`).

**Implementation**: `optimization_engine/solve_simulation.py` lines 271-295

```python
# CRITICAL: Disable solution monitor when solving multiple solutions
# This prevents NX from opening multiple monitor windows which superpose and cause usability issues
if not solution_name:
    print("[JOURNAL] Disabling solution monitor for all solutions to prevent window pile-up...")
    try:
        # Get all solutions in the simulation
        solutions_disabled = 0
        solution_num = 1
        while True:
            try:
                solution_obj_name = f"Solution[Solution {solution_num}]"
                simSolution = simSimulation1.FindObject(solution_obj_name)
                if simSolution:
                    propertyTable = simSolution.SolverOptionsPropertyTable
                    propertyTable.SetBooleanPropertyValue("solution monitor", False)
                    solutions_disabled += 1
                    solution_num += 1
                else:
                    break
            except:
                break  # No more solutions
        print(f"[JOURNAL]   Solution monitor disabled for {solutions_disabled} solution(s)")
    except Exception as e:
        print(f"[JOURNAL]   WARNING: Could not disable solution monitor: {e}")
        print(f"[JOURNAL]   Continuing with solve anyway...")
```

**When this activates**:
- Automatically when `solution_name=None` (solve all solutions mode)
- For any study with multiple trials (typical optimization scenario)
- No user configuration required

**User-recorded journal**: `nx_journals/user_generated_journals/journal_monitor_window_off.py`

---

## Related Issues Fixed

1. **All trials showing identical frequency**: Fixed by ensuring modal solution runs
2. **Only one data point in dashboard**: Fixed by all trials succeeding
3. **Parallel coordinates with NaN**: Fixed by having complete data from all solutions
4. **Solution monitor windows piling up**: Fixed by automatically disabling monitor for multi-solution runs

---

## References

- **User's Example**: `nx_journals/user_generated_journals/journal_solve_all_solution.py` (line 27)
- **NX Open Documentation**: SimSolveManager.SolveAllSolutions() method
- **Implementation**: `optimization_engine/solve_simulation.py`

---

## Best Practices

1. **Always use Foreground mode** when solving all solutions
2. **Verify OP2 timestamp changes** to ensure fresh solves
3. **Check solve counts** in journal output to confirm both solutions ran
4. **Test with 5 trials** before running large optimizations
5. **Monitor unique frequency values** as a smoke test for multi-solution models

---

## Example Use Cases

### ✅ Correct Usage

```python
# Multi-objective optimization with static + modal
result = nx_solver.run_simulation(
    sim_file=sim_file,
    working_dir=model_dir,
    expression_updates=design_vars,
    solution_name=None  # Solve ALL solutions
)
```

### ❌ Incorrect Usage (Don't Do This)

```python
# Running modal separately - inefficient and error-prone
result1 = nx_solver.run_simulation(..., solution_name="Solution 1")  # Static
result2 = nx_solver.run_simulation(..., solution_name="Solution 2")  # Modal
# This doubles the solve time and requires managing two result objects
```

---

**Status**: ✅ Implemented and Verified
**Impact**: Critical for all multi-solution optimization workflows
