252 lines
9.3 KiB
Markdown
252 lines
9.3 KiB
Markdown
# Story 5.2: Bounded Control Variables
|
|
|
|
Status: completed
|
|
|
|
## Story
|
|
|
|
As a control engineer,
|
|
I want Box Constraints or Step Clipping for control variables,
|
|
so that Newton steps stay physically possible and solver respects physical bounds.
|
|
|
|
## Acceptance Criteria
|
|
|
|
1. **Bounded Variable Definition**
|
|
- Given a control variable (e.g., valve position, VFD speed)
|
|
- When I define bounds [min, max]
|
|
- Then the variable is constrained to stay within bounds
|
|
- And bounds are validated: `min < max` enforced
|
|
|
|
2. **Step Clipping**
|
|
- Given Newton step Δx that would exceed bounds
|
|
- When computing the update
|
|
- Then the step is scaled/clipped to stay within bounds
|
|
- And the variable never goes outside bounds during iterations
|
|
|
|
3. **Convergence with Saturation Detection**
|
|
- Given a converged solution
|
|
- When the solution is at a bound (min or max)
|
|
- Then `ControlSaturation` status is returned (not error)
|
|
- And the status includes which variable is saturated and at which bound
|
|
|
|
4. **Infeasible Constraint Detection**
|
|
- Given a constraint that requires value outside bounds
|
|
- When solver converges to bound
|
|
- Then clear diagnostic is provided: which constraint, which bound
|
|
- And user knows constraint cannot be satisfied
|
|
|
|
5. **Integration with Constraints**
|
|
- Given bounded control variables from this story
|
|
- And constraints from Story 5.1
|
|
- When combined
|
|
- Then bounded variables can be used as control inputs for constraints
|
|
|
|
## Tasks / Subtasks
|
|
|
|
- [x] Create `BoundedVariable` type in `crates/solver/src/inverse/bounded.rs`
|
|
- [x] `BoundedVariable` struct with value, min, max bounds
|
|
- [x] `BoundedVariableId` newtype for type-safe identifiers
|
|
- [x] `BoundedVariableError` enum for validation failures
|
|
- [x] Implement step clipping logic
|
|
- [x] `clip_step(current: f64, delta: f64, min: f64, max: f64) -> f64`
|
|
- [x] Handle edge cases (NaN, Inf, equal bounds)
|
|
- [x] Implement saturation detection
|
|
- [x] `is_saturated(&self) -> Option<SaturationInfo>` method
|
|
- [x] `SaturationInfo` struct with variable_id, bound_type (Lower/Upper), bound_value
|
|
- [x] Add `ControlSaturation` to error/status types
|
|
- [x] Add `ControlSaturation` variant to `ConstraintError` (or new enum if needed)
|
|
- [x] Include saturated variable info in the error
|
|
- [x] Integrate bounded variables with System
|
|
- [x] `add_bounded_variable(&mut self, var: BoundedVariable) -> Result<(), BoundedVariableError>`
|
|
- [x] `bounded_variables: HashMap<BoundedVariableId, BoundedVariable>` storage
|
|
- [x] Validation: component_id must exist in registry (like constraints)
|
|
- [x] Write unit tests
|
|
- [x] Test step clipping at bounds
|
|
- [x] Test saturation detection
|
|
- [x] Test invalid bounds (min >= max)
|
|
- [x] Test integration with existing constraint types
|
|
- [x] Update module exports
|
|
- [x] Export from `mod.rs`
|
|
- [x] Update `lib.rs` if needed
|
|
|
|
## Dev Notes
|
|
|
|
### Architecture Context
|
|
|
|
This is **Story 5.2** in Epic 5: Inverse Control & Optimization. It builds on Story 5.1 (Constraint Definition Framework) and enables FR23.
|
|
|
|
**Key Requirements from FR23:**
|
|
> "System calculates necessary inputs (e.g., valve opening) respecting Bounded Constraints (0.0 ≤ Valve ≤ 1.0). If solution is out of bounds, solver returns 'Saturation' or 'ControlLimitReached' error"
|
|
|
|
### Previous Story Context (Story 5.1)
|
|
|
|
Story 5.1 created the constraint framework in `crates/solver/src/inverse/`:
|
|
- `Constraint`, `ConstraintId`, `ConstraintError`, `ComponentOutput` types
|
|
- `System.constraints: HashMap<ConstraintId, Constraint>` storage
|
|
- `System.component_names: HashSet<String>` registry for validation
|
|
- Constraint residual: `measured_value - target_value`
|
|
|
|
**Patterns to follow from Story 5.1:**
|
|
- Type-safe newtype identifiers (`BoundedVariableId` like `ConstraintId`)
|
|
- `thiserror` for error types
|
|
- Validation against component registry before adding
|
|
- KaTeX documentation in rustdoc
|
|
|
|
### Technical Requirements
|
|
|
|
**Module Location:**
|
|
```
|
|
crates/solver/src/inverse/
|
|
├── mod.rs (existing - update exports)
|
|
├── constraint.rs (existing - from Story 5.1)
|
|
└── bounded.rs (NEW - this story)
|
|
```
|
|
|
|
**BoundedVariable Design:**
|
|
|
|
```rust
|
|
pub struct BoundedVariable {
|
|
id: BoundedVariableId,
|
|
current_value: f64,
|
|
min: f64,
|
|
max: f64,
|
|
}
|
|
|
|
pub struct BoundedVariableId(String); // Follow ConstraintId pattern
|
|
|
|
pub enum BoundedVariableError {
|
|
InvalidBounds { min: f64, max: f64 },
|
|
DuplicateId { id: BoundedVariableId },
|
|
ValueOutOfBounds { value: f64, min: f64, max: f64 },
|
|
InvalidComponent { component_id: String },
|
|
}
|
|
|
|
pub enum SaturationType {
|
|
LowerBound,
|
|
UpperBound,
|
|
}
|
|
|
|
pub struct SaturationInfo {
|
|
variable_id: BoundedVariableId,
|
|
saturation_type: SaturationType,
|
|
bound_value: f64,
|
|
constraint_target: Option<f64>, // What we were trying to achieve
|
|
}
|
|
```
|
|
|
|
**Step Clipping Algorithm:**
|
|
|
|
```rust
|
|
pub fn clip_step(current: f64, delta: f64, min: f64, max: f64) -> f64 {
|
|
let proposed = current + delta;
|
|
proposed.clamp(min, max)
|
|
}
|
|
```
|
|
|
|
Note: Consider more sophisticated approaches later (line search, trust region) but clipping is MVP.
|
|
|
|
**ControlSaturation vs Error:**
|
|
|
|
Per FR23, saturation is NOT an error - it's a status:
|
|
- Converged at bound = success with saturation info
|
|
- Use `Result<ConvergedState, SolverError>` where `ConvergedState` includes optional `saturation_info`
|
|
- Or use a separate `ControlStatus` enum if that fits existing patterns better
|
|
|
|
### Existing Code Patterns
|
|
|
|
**From `constraint.rs` (Story 5.1):**
|
|
```rust
|
|
// Use thiserror for errors
|
|
#[derive(Error, Debug, Clone, PartialEq)]
|
|
pub enum ConstraintError { ... }
|
|
|
|
// Newtype pattern for IDs
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
pub struct ConstraintId(String);
|
|
|
|
// Builder-style methods
|
|
impl Constraint {
|
|
pub fn with_tolerance(...) -> Result<Self, ConstraintError> { ... }
|
|
}
|
|
```
|
|
|
|
**From `system.rs` (Story 5.1):**
|
|
```rust
|
|
// HashMap storage with validation
|
|
pub fn add_constraint(&mut self, constraint: Constraint) -> Result<(), ConstraintError> {
|
|
// 1. Validate component_id exists in registry
|
|
// 2. Check for duplicate id
|
|
// 3. Insert into HashMap
|
|
}
|
|
```
|
|
|
|
### Project Structure Notes
|
|
|
|
- New file: `crates/solver/src/inverse/bounded.rs`
|
|
- Modify: `crates/solver/src/inverse/mod.rs` (add exports)
|
|
- Modify: `crates/solver/src/system.rs` (add bounded_variables storage)
|
|
|
|
### Anti-Patterns to Avoid
|
|
|
|
- **DON'T** use bare `f64` for values in public API - consider newtype if consistency with Constraint pattern is desired
|
|
- **DON'T** panic on invalid bounds - return `Result` with clear error
|
|
- **DON'T** use `unwrap()` or `expect()` - follow zero-panic policy
|
|
- **DON'T** use `println!` - use `tracing` for debug output
|
|
- **DON'T** make saturation an error - it's a valid solver outcome (status)
|
|
- **DON'T** forget to validate component_id against registry (AC5 from Story 5.1 code review)
|
|
|
|
### References
|
|
|
|
- [Source: `epics.md` Story 5.2] Bounded Control Variables acceptance criteria
|
|
- [Source: `epics.md` FR23] "Bounded Constraints (0.0 ≤ Valve ≤ 1.0)"
|
|
- [Source: `architecture.md`] Inverse Control pattern at `crates/solver/src/inverse/`
|
|
- [Source: `architecture.md`] Error handling via `ThermoError` (or appropriate error type)
|
|
- [Source: Story 5.1 implementation] `constraint.rs` for patterns to follow
|
|
- [Source: Story 5.1 code review] Component registry validation requirement
|
|
|
|
### Related Stories
|
|
|
|
- **Story 5.1**: Constraint Definition Framework (DONE) - provides constraint types
|
|
- **Story 5.3**: Residual Embedding for Inverse Control - will combine bounded variables with constraints in solver
|
|
|
|
## Dev Agent Record
|
|
|
|
### Agent Model Used
|
|
|
|
Claude 3.5 Sonnet via opencode (Antigravity)
|
|
|
|
### Debug Log References
|
|
|
|
N/A
|
|
|
|
### Completion Notes List
|
|
|
|
- ✅ Created `bounded.rs` module with complete bounded variable types
|
|
- ✅ `BoundedVariableId` newtype with From traits for ergonomic construction (matches `ConstraintId` pattern)
|
|
- ✅ `BoundedVariable` struct with value, min, max bounds and optional component_id
|
|
- ✅ `BoundedVariableError` enum with thiserror for comprehensive error handling
|
|
- ✅ `SaturationType` enum (LowerBound, UpperBound) with Display impl
|
|
- ✅ `SaturationInfo` struct with variable_id, saturation_type, bound_value, constraint_target
|
|
- ✅ `clip_step()` standalone function for solver loop use
|
|
- ✅ `is_saturated()` method returns `Option<SaturationInfo>` (not error - per FR23)
|
|
- ✅ Edge case handling: NaN returns clamped current value, Inf clips to bounds
|
|
- ✅ Integrated into System struct with HashMap storage
|
|
- ✅ Component registry validation (matches Story 5.1 code review requirement)
|
|
- ✅ `saturated_variables()` method to check all variables at once
|
|
- ✅ Module-level rustdoc with KaTeX formulas for mathematical notation
|
|
- ✅ 25 unit tests in bounded.rs + 9 integration tests in system.rs = 34 new tests
|
|
- ✅ All 195 solver tests passing
|
|
|
|
### File List
|
|
|
|
- `crates/solver/src/inverse/bounded.rs` (new - bounded variable types with 25 tests)
|
|
- `crates/solver/src/inverse/mod.rs` (modified - added bounded module and exports)
|
|
- `crates/solver/src/system.rs` (modified - added bounded_variables field, 9 new tests)
|
|
|
|
## Change Log
|
|
|
|
- 2026-02-21: Implemented Story 5.2 - Bounded Control Variables
|
|
- Created bounded.rs module following Story 5.1 patterns
|
|
- Integrated bounded variables into System with component registry validation
|
|
- All 195 solver tests passing
|
|
- Framework ready for Story 5.3 (Residual Embedding for Inverse Control)
|