# Story 5.5: Swappable Calibration Variables (Inverse Calibration One-Shot)

Status: done

<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->

## Story

As a R&D engineer calibrating a machine model against test bench data,
I want to swap calibration coefficients (f_m, f_ua, f_power, etc.) into unknowns and measured values (Tsat, capacity, power) into constraints,
so that the solver directly computes the calibration coefficients in one shot without an external optimizer.

## Acceptance Criteria

1. **Condenser Calibration**
   - Given a Condenser with `f_ua` as a calibration factor
   - When I enable calibration mode and fix `Tsat_cond` to a measured value
   - Then `f_ua` becomes an unknown in the solver state vector
   - And a residual is added: `Tsat_cond_computed - Tsat_cond_measured = 0`
   - And the solver computes `f_ua` directly

2. **Evaporator Calibration**
   - Given an Evaporator with `f_ua` as a calibration factor
   - When I enable calibration mode and fix `Tsat_evap` to a measured value
   - Then same swap mechanism: `f_ua` → unknown, `Tsat_evap` → constraint

3. **Compressor Power Calibration**
   - Given a Compressor with `f_power` as a calibration factor
   - When I enable calibration mode and fix Power to a measured value
   - Then `f_power` becomes an unknown
   - And residual: `Power_computed - Power_measured = 0`

4. **Compressor Mass Flow Calibration**
   - Given a Compressor with `f_m` as a calibration factor
   - When I enable calibration mode and fix mass flow `ṁ` to a measured value
   - Then `f_m` becomes an unknown
   - And residual: `ṁ_computed - ṁ_measured = 0`

5. **System Specific Modes**
   - Given a machine in cooling mode calibration, when I impose evaporator cooling capacity `Q_evap_measured`, then `Q_evap` becomes a constraint (`Q_evap_computed - Q_evap_measured = 0`) and corresponding `f_` (typically `f_ua` on evaporator) becomes an unknown.
   - Given a machine in heating mode calibration, when I impose condenser heating capacity `Q_cond_measured`, then `Q_cond` becomes a constraint and corresponding `f_` (typically `f_ua` on condenser) becomes an unknown.

## Tasks / Subtasks

- [x] Extend `Component` implementations to respect `f_` calibration variables
  - [x] Update `Compressor` to apply `f_m` (mass flow) and `f_power` (power) factors if provided.
  - [x] Update Heat Exchangers (`Condenser`, `Evaporator`) to apply `f_ua` (heat transfer) factor if provided.
  - [x] Update Pipes/Valves for `f_m` or `f_dp` where applicable.
- [x] Connect `BoundedVariable` for `f_` factors
  - [x] Use `BoundedVariable` to represent `f_m`, `f_ua`, etc. in the `System` state as control variables with min/max bounds (e.g., 0.5 to 2.0).
  - [x] Allow components to extract their current calibration factors from the `SystemState` during `compute_residuals`.
- [x] Inverse Control Constraint Setup
  - [x] Add ability to cleanly formulate targets like `Target Capacity`, `Target Power`, `Target Tsat`.
  - [x] Test the solver for MIMO (Multi-Input Multi-Output) with these new variables mapping to these targets.
- [x] Write integration test for Swappable Calibration
  - [x] Create a test with fixed component geometries, imposing a known capacity and power.
  - [x] Check if the solver successfully converges to the required `f_ua` and `f_power` to match those targets.

## Dev Notes

### Architecture Context

This is **Story 5.5** in Epic 5: Inverse Control & Optimization. It leverages the MIMO capability built in **Story 5.4**. Instead of traditional controls like Valve Opening targeting Superheat, this story treats the actual physical calibration parameters (like UA multiplier, mass flow multiplier) as the "Control Variables" and the test bench measurements (Capacity, Power) as the "Constraints".

**Critical Numerical Challenge:**
The components must read from the solver's expanded `SystemState` (control vector section) to obtain their `f_` multipliers instead of using hardcoded internal struct fields. If the multiplier is NOT part of the inverse control problem (regular forward simulation), it should default to `1.0`.

**Technical Stack Requirements:**
- Rust (edition 2021) with `#![deny(warnings)]`
- `nalgebra` for linear algebra
- `thiserror` for error handling
- `tracing` for structured logging

**Component → Calibration Factor Mapping:**
| Component | f_ Factors | Measurable Values (Constraints) |
|-----------|------------|------------------------------|
| Condenser | f_ua, f_dp | Tsat_cond, Q_cond (capacity), ΔP_cond |
| Evaporator | f_ua, f_dp | Tsat_evap, Q_evap (capacity), ΔP_evap |
| Compressor | f_m, f_power, f_etav | ṁ, Power, η_v |
| Expansion Valve | f_m | ṁ |
| Pipe | f_dp | ΔP |

### Previous Story Intelligence

**From Story 5.4 (Multi-Variable Control):**
- MIMO Jacobian calculation using cross-derivatives is fully working and tested.
- `extract_constraint_values_with_controls()` accurately maps primary and secondary effects via component IDs using `BoundedVariable::with_component()`.
- Use `BoundedVariable::with_component()` to properly associate the calibration factor with the component so that the Jacobian builder can calculate perturbations accurately.

### Technical Requirements

**Critical Implementation Details:**
1. **State Extraction in Components:**
   Components like `Condenser` must query the `SystemState` for their specific `BoundedVariable` (like `f_ua`) during `compute_residuals`. If present, multiply the nominal UA by `f_ua`. If not, assume `1.0`.
2. **Bounds definition:**
   Calibration factors should be bounded to physically meaningful ranges (e.g. `f_ua` between 0.1 and 10.0) to prevent the solver from taking unphysical steps or diverging.
3. **MIMO Stability:**
   Because swapping `f_m` and `f_power` simultaneously affects both mass flow and power, the Newton-Raphson solver will rely entirely on the cross-derivatives implemented in Story 5.4. Ensure that the component IDs strictly match between the constraint output extraction and the bounded variable.

**Anti-Patterns to Avoid:**
- ❌ DON'T force components to store stateful `f_` values locally that drift out of sync with the solver. Always compute dynamically from the solver state.
- ❌ DON'T use exact `1.0` equality checks. Since these are floats, if not using a control variable, pass `None`.
- ❌ DON'T use `unwrap()` or `expect()`.

### Project Structure Notes
- Changes isolated to `crates/components/src/` (modifying components to respect `f_` factors)
- May need minor additions to `crates/solver/src/system.rs` to allow components to extract their mapped controls.
- Integration tests go in `crates/solver/tests/inverse_calibration.rs`.

### References
- [Source: `epics.md` Story 5.5] Swappable Calibration Variables acceptance criteria.
- [Source: `5-4-multi-variable-control.md`] Multi-Variable MIMO Jacobian and component-id linking logic.
- [Source: `architecture.md#Inverse-Control`] Architecture decisions for one-shot inverse solving.

## Dev Agent Record

### Agent Model Used
z-ai/glm-5:free (Antigravity proxy)

### Debug Log References

### Completion Notes List

### File List
- `crates/components/src/expansion_valve.rs`
- `crates/components/src/compressor.rs`
- `crates/components/src/heat_exchanger/exchanger.rs`
- `crates/solver/tests/inverse_calibration.rs`
