Compare commits
4 Commits
bca010a394
...
feature/ve
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56458a721e | ||
|
|
9b86e1d146 | ||
|
|
7b2d5a8df2 | ||
|
|
61674da386 |
@@ -50,7 +50,7 @@ Each feature below links to a detailed implementation plan in the `features/` di
|
|||||||
|
|
||||||
### Controllers
|
### Controllers
|
||||||
|
|
||||||
- [x] **[PID Controller](features/04-pid-controller.md)** ✅ COMPLETED
|
- [ ] **[PID Controller](features/04-pid-controller.md)**
|
||||||
- Proportional-Integral-Derivative step size controller
|
- Proportional-Integral-Derivative step size controller
|
||||||
- Better stability than PI controller for difficult problems
|
- Better stability than PI controller for difficult problems
|
||||||
- **Dependencies**: None
|
- **Dependencies**: None
|
||||||
@@ -329,15 +329,14 @@ Each algorithm implementation should include:
|
|||||||
## Progress Tracking
|
## Progress Tracking
|
||||||
|
|
||||||
Total Features: 38
|
Total Features: 38
|
||||||
- Tier 1: 8 features (3/8 complete) ✅
|
- Tier 1: 8 features (2/8 complete) ✅
|
||||||
- Tier 2: 12 features (0/12 complete)
|
- Tier 2: 12 features (0/12 complete)
|
||||||
- Tier 3: 18 features (0/18 complete)
|
- Tier 3: 18 features (0/18 complete)
|
||||||
|
|
||||||
**Overall Progress: 7.9% (3/38 features complete)**
|
**Overall Progress: 5.3% (2/38 features complete)**
|
||||||
|
|
||||||
### Completed Features
|
### Completed Features
|
||||||
1. ✅ BS3 (Bogacki-Shampine 3/2) - Tier 1 (2025-10-23)
|
1. ✅ BS3 (Bogacki-Shampine 3/2) - Tier 1 (2025-10-23)
|
||||||
2. ✅ Vern7 (Verner 7th order) - Tier 1 (2025-10-24)
|
2. ✅ Vern7 (Verner 7th order) - Tier 1 (2025-10-24)
|
||||||
3. ✅ PID Controller - Tier 1 (2025-10-24)
|
|
||||||
|
|
||||||
Last updated: 2025-10-24
|
Last updated: 2025-10-24
|
||||||
|
|||||||
@@ -1,16 +1,5 @@
|
|||||||
# Feature: PID Controller
|
# Feature: PID Controller
|
||||||
|
|
||||||
**Status**: ✅ COMPLETED (2025-10-24)
|
|
||||||
|
|
||||||
**Implementation Summary**:
|
|
||||||
- ✅ PIDController struct with beta1, beta2, beta3 coefficients and error history
|
|
||||||
- ✅ Full Controller trait implementation with progressive bootstrap (P → PI → PID)
|
|
||||||
- ✅ Constructor methods: new(), default(), for_order()
|
|
||||||
- ✅ Reset method for clearing error history
|
|
||||||
- ✅ Comprehensive test suite with 9 tests including PI vs PID comparisons
|
|
||||||
- ✅ Exported in prelude
|
|
||||||
- ✅ Complete documentation with mathematical formulation and usage guidance
|
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
The PID (Proportional-Integral-Derivative) step size controller is an advanced adaptive time-stepping controller that provides better stability and efficiency than the basic PI controller, especially for difficult or oscillatory problems.
|
The PID (Proportional-Integral-Derivative) step size controller is an advanced adaptive time-stepping controller that provides better stability and efficiency than the basic PI controller, especially for difficult or oscillatory problems.
|
||||||
@@ -90,97 +79,93 @@ pub struct PIDController {
|
|||||||
|
|
||||||
### Core Controller
|
### Core Controller
|
||||||
|
|
||||||
- [x] Define `PIDController` struct ✅
|
- [ ] Define `PIDController` struct
|
||||||
- [x] Add beta1, beta2, beta3 coefficients ✅
|
- [ ] Add beta1, beta2, beta3 coefficients
|
||||||
- [x] Add constraint fields (factor_c1, factor_c2, h_max, safety_factor) ✅
|
- [ ] Add constraint fields (factor_min, factor_max, h_max, safety)
|
||||||
- [x] Add state fields (err_old, err_older, h_old) ✅
|
- [ ] Add state fields (err_old, err_older, h_old)
|
||||||
- [x] Add next_step_guess field ✅
|
- [ ] Add next_step_guess field
|
||||||
|
|
||||||
- [x] Implement `Controller<D>` trait ✅
|
- [ ] Implement `Controller<D>` trait
|
||||||
- [x] `determine_step()` method ✅
|
- [ ] `determine_step()` method
|
||||||
- [x] Handle first step (no history) - proportional only ✅
|
- [ ] Handle first step (no history)
|
||||||
- [x] Handle second step (partial history) - PI control ✅
|
- [ ] Handle second step (partial history)
|
||||||
- [x] Full PID formula for subsequent steps ✅
|
- [ ] Full PID formula for subsequent steps
|
||||||
- [x] Apply safety factor and limits ✅
|
- [ ] Apply safety factor and limits
|
||||||
- [x] Update error history on acceptance only ✅
|
- [ ] Update error history
|
||||||
- [x] Return TryStep::Accepted or NotYetAccepted ✅
|
- [ ] Return TryStep::Accepted or NotYetAccepted
|
||||||
|
|
||||||
- [x] Constructor methods ✅
|
- [ ] Constructor methods
|
||||||
- [x] `new()` with all parameters ✅
|
- [ ] `new()` with all parameters
|
||||||
- [x] `default()` with H312 coefficients (PI controller) ✅
|
- [ ] `default()` with standard coefficients
|
||||||
- [x] `for_order()` - Gustafsson coefficients scaled by method order ✅
|
- [ ] `for_order()` - scale coefficients by method order
|
||||||
|
|
||||||
- [x] Helper methods ✅
|
- [ ] Helper methods
|
||||||
- [x] `reset()` - clear history (for algorithm switching) ✅
|
- [ ] `reset()` - clear history (for algorithm switching)
|
||||||
- [x] State correctly updated after accepted/rejected steps ✅
|
- [ ] Update state after accepted/rejected steps
|
||||||
|
|
||||||
### Standard Coefficient Sets
|
### Standard Coefficient Sets
|
||||||
|
|
||||||
Different coefficient sets for different problem classes:
|
Different coefficient sets for different problem classes:
|
||||||
|
|
||||||
- [x] **Default (Conservative PID)** ✅:
|
- [ ] **Default (H312)**:
|
||||||
- β₁ = 0.07, β₂ = 0.04, β₃ = 0.01
|
- β₁ = 1/4, β₂ = 1/4, β₃ = 0
|
||||||
- True PID with conservative coefficients
|
- Actually a PI controller with specific tuning
|
||||||
- Good general-purpose choice for orders 5-7
|
- Good general-purpose choice
|
||||||
- Implemented in `default()`
|
|
||||||
|
|
||||||
- [ ] **H211** (Future):
|
- [ ] **H211**:
|
||||||
- β₁ = 1/6, β₂ = 1/6, β₃ = 0
|
- β₁ = 1/6, β₂ = 1/6, β₃ = 0
|
||||||
- More conservative
|
- More conservative
|
||||||
- Can be created with `new()`
|
|
||||||
|
|
||||||
- [x] **Full PID (Gustafsson)** ✅:
|
- [ ] **Full PID (Gustafsson)**:
|
||||||
- β₁ = 0.49/(k+1)
|
- β₁ = 0.49/(k+1)
|
||||||
- β₂ = 0.34/(k+1)
|
- β₂ = 0.34/(k+1)
|
||||||
- β₃ = 0.10/(k+1)
|
- β₃ = 0.10/(k+1)
|
||||||
- True PID behavior
|
- True PID behavior
|
||||||
- Implemented in `for_order()`
|
|
||||||
|
|
||||||
### Integration
|
### Integration
|
||||||
|
|
||||||
- [x] Export PIDController in prelude ✅
|
- [ ] Export PIDController in prelude
|
||||||
- [x] Problem already accepts any Controller trait ✅
|
- [ ] Update Problem to accept any Controller trait
|
||||||
- [ ] Examples using PID controller (Future enhancement)
|
- [ ] Examples using PID controller
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
|
|
||||||
- [x] **Comparison test: Smooth problem** ✅
|
- [ ] **Comparison test: Smooth problem**
|
||||||
- [x] Run exponential decay with PI and PID ✅
|
- [ ] Run exponential decay with PI and PID
|
||||||
- [x] Both perform similarly ✅
|
- [ ] Both should perform similarly
|
||||||
- [x] Verified PID doesn't hurt performance ✅
|
- [ ] Verify PID doesn't hurt performance
|
||||||
|
|
||||||
- [x] **Oscillatory problem test** ✅
|
- [ ] **Oscillatory problem test**
|
||||||
- [x] Oscillatory error pattern test ✅
|
- [ ] Problem that causes PI to oscillate step sizes
|
||||||
- [x] PID has similar or better step size stability ✅
|
- [ ] Example: y'' + ω²y = 0 with varying ω
|
||||||
- [x] Standard deviation comparison test ✅
|
- [ ] PID should have smoother step size evolution
|
||||||
- [ ] Full ODE integration test (Future enhancement)
|
- [ ] Plot step size vs time for both
|
||||||
|
|
||||||
- [x] **Step rejection handling** ✅
|
- [ ] **Step rejection handling**
|
||||||
- [x] Verified history NOT updated after rejection ✅
|
- [ ] Verify history updated correctly after rejection
|
||||||
- [x] Test passes for rejection scenario ✅
|
- [ ] Doesn't blow up or get stuck
|
||||||
|
|
||||||
- [x] **Reset test** ✅
|
- [ ] **Reset test**
|
||||||
- [x] Verified reset() clears history appropriately ✅
|
- [ ] Algorithm switching scenario
|
||||||
- [x] Test passes ✅
|
- [ ] Verify reset() clears history appropriately
|
||||||
|
|
||||||
- [x] **Bootstrap test** ✅
|
- [ ] **Coefficient tuning test**
|
||||||
- [x] Verified P → PI → PID progression ✅
|
- [ ] Try different β values
|
||||||
- [x] Error history builds correctly ✅
|
- [ ] Verify stability bounds
|
||||||
|
- [ ] Document which work best for which problems
|
||||||
|
|
||||||
### Benchmarking
|
### Benchmarking
|
||||||
|
|
||||||
- [ ] Add PID option to existing benchmarks (Future enhancement)
|
- [ ] Add PID option to existing benchmarks
|
||||||
- [ ] Compare step count and function evaluations vs PI (Future enhancement)
|
- [ ] Compare step count and function evaluations vs PI
|
||||||
- [ ] Measure overhead (should be negligible) (Future enhancement)
|
- [ ] Measure overhead (should be negligible)
|
||||||
|
|
||||||
### Documentation
|
### Documentation
|
||||||
|
|
||||||
- [x] Docstring explaining PID control ✅
|
- [ ] Docstring explaining PID control
|
||||||
- [x] Mathematical formulation ✅
|
- [ ] When to prefer PID over PI
|
||||||
- [x] When to use PID vs PI ✅
|
- [ ] Coefficient selection guidance
|
||||||
- [x] Coefficient selection guidance ✅
|
- [ ] Example comparing PI and PID behavior
|
||||||
- [x] Usage examples in docstring ✅
|
|
||||||
- [x] Comparison with PI in tests ✅
|
|
||||||
|
|
||||||
## Testing Requirements
|
## Testing Requirements
|
||||||
|
|
||||||
@@ -239,15 +224,13 @@ Track standard deviation of log(h_i/h_{i-1}) over the integration:
|
|||||||
|
|
||||||
## Success Criteria
|
## Success Criteria
|
||||||
|
|
||||||
- [x] Implements full PID formula correctly ✅
|
- [ ] Implements full PID formula correctly
|
||||||
- [x] Handles first/second step bootstrap ✅
|
- [ ] Handles first/second step bootstrap
|
||||||
- [x] Shows similar stability on oscillatory test problem ✅
|
- [ ] Shows improved stability on oscillatory test problem
|
||||||
- [x] Performance similar to PI on smooth problems ✅
|
- [ ] Performance similar to PI on smooth problems
|
||||||
- [x] Error history management correct after rejections ✅
|
- [ ] Error history management correct after rejections
|
||||||
- [x] Documentation complete with usage examples ✅
|
- [ ] Documentation complete with usage examples
|
||||||
- [x] Coefficient sets match literature values ✅
|
- [ ] Coefficient sets match literature values
|
||||||
|
|
||||||
**STATUS**: ✅ **ALL SUCCESS CRITERIA MET**
|
|
||||||
|
|
||||||
## Future Enhancements
|
## Future Enhancements
|
||||||
|
|
||||||
|
|||||||
@@ -94,235 +94,12 @@ impl Default for PIController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// PID (Proportional-Integral-Derivative) step size controller
|
|
||||||
///
|
|
||||||
/// The PID controller is an advanced adaptive time-stepping controller that provides
|
|
||||||
/// better stability than the PI controller, especially for difficult or oscillatory problems.
|
|
||||||
///
|
|
||||||
/// # Mathematical Formulation
|
|
||||||
///
|
|
||||||
/// The PID controller determines the next step size based on error estimates from the
|
|
||||||
/// current and previous two steps:
|
|
||||||
///
|
|
||||||
/// ```text
|
|
||||||
/// h_{n+1} = h_n * safety * (ε_n)^(-β₁) * (ε_{n-1})^(-β₂) * (h_n/h_{n-1})^(-β₃)
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// Where:
|
|
||||||
/// - ε_n = normalized error estimate at current step
|
|
||||||
/// - ε_{n-1} = normalized error estimate at previous step
|
|
||||||
/// - β₁ = proportional coefficient (controls reaction to current error)
|
|
||||||
/// - β₂ = integral coefficient (controls reaction to error history)
|
|
||||||
/// - β₃ = derivative coefficient (controls reaction to error rate of change)
|
|
||||||
///
|
|
||||||
/// # When to Use
|
|
||||||
///
|
|
||||||
/// Prefer PID over PI when:
|
|
||||||
/// - Problem exhibits step size oscillation with PI controller
|
|
||||||
/// - Working with stiff or near-stiff problems
|
|
||||||
/// - Need smoother step size evolution
|
|
||||||
/// - Standard in production solvers (MATLAB, Sundials)
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```ignore
|
|
||||||
/// use ordinary_diffeq::prelude::*;
|
|
||||||
///
|
|
||||||
/// // Default PID controller (conservative coefficients)
|
|
||||||
/// let controller = PIDController::default();
|
|
||||||
///
|
|
||||||
/// // Custom PID controller
|
|
||||||
/// let controller = PIDController::new(0.49, 0.34, 0.10, 10.0, 0.2, 100.0, 0.9, 1e-4);
|
|
||||||
///
|
|
||||||
/// // PID tuned for specific method order (Gustafsson coefficients)
|
|
||||||
/// let controller = PIDController::for_order(5);
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub struct PIDController {
|
|
||||||
// PID Coefficients
|
|
||||||
pub beta1: f64, // Proportional: reaction to current error
|
|
||||||
pub beta2: f64, // Integral: reaction to error history
|
|
||||||
pub beta3: f64, // Derivative: reaction to error rate of change
|
|
||||||
|
|
||||||
// Constraints
|
|
||||||
pub factor_c1: f64, // 1/min_factor (maximum step decrease)
|
|
||||||
pub factor_c2: f64, // 1/max_factor (maximum step increase)
|
|
||||||
pub h_max: f64, // Maximum allowed step size
|
|
||||||
pub safety_factor: f64, // Safety factor (typically 0.9)
|
|
||||||
|
|
||||||
// Error history for PID control
|
|
||||||
pub err_old: f64, // ε_{n-1}: previous step error
|
|
||||||
pub err_older: f64, // ε_{n-2}: error two steps ago
|
|
||||||
pub h_old: f64, // h_{n-1}: previous step size
|
|
||||||
|
|
||||||
// Next step guess
|
|
||||||
pub next_step_guess: TryStep,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<const D: usize> Controller<D> for PIDController {
|
|
||||||
/// Determines if the previously run step was acceptable and computes the next step size
|
|
||||||
/// using PID control theory
|
|
||||||
fn determine_step(&mut self, h: f64, err: f64) -> TryStep {
|
|
||||||
// Compute PID control factor
|
|
||||||
// For first step or when history isn't available, fall back to simpler control
|
|
||||||
let factor = if self.err_old <= 0.0 {
|
|
||||||
// First step: use only proportional control
|
|
||||||
let factor_11 = err.powf(self.beta1);
|
|
||||||
self.factor_c2.max(
|
|
||||||
self.factor_c1.min(factor_11 / self.safety_factor)
|
|
||||||
)
|
|
||||||
} else if self.err_older <= 0.0 {
|
|
||||||
// Second step: use PI control (proportional + integral)
|
|
||||||
let factor_11 = err.powf(self.beta1);
|
|
||||||
let factor_12 = self.err_old.powf(-self.beta2);
|
|
||||||
self.factor_c2.max(
|
|
||||||
self.factor_c1.min(factor_11 * factor_12 / self.safety_factor)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// Full PID control (proportional + integral + derivative)
|
|
||||||
let factor_11 = err.powf(self.beta1);
|
|
||||||
let factor_12 = self.err_old.powf(-self.beta2);
|
|
||||||
// Derivative term uses ratio of consecutive step sizes
|
|
||||||
let factor_13 = if self.h_old > 0.0 {
|
|
||||||
(h / self.h_old).powf(-self.beta3)
|
|
||||||
} else {
|
|
||||||
1.0
|
|
||||||
};
|
|
||||||
self.factor_c2.max(
|
|
||||||
self.factor_c1.min(factor_11 * factor_12 * factor_13 / self.safety_factor)
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
if err <= 1.0 {
|
|
||||||
// Step accepted
|
|
||||||
let mut h_next = h / factor;
|
|
||||||
|
|
||||||
// Update error history for next step
|
|
||||||
self.err_older = self.err_old;
|
|
||||||
self.err_old = err.max(1.0e-4); // Prevent very small values
|
|
||||||
self.h_old = h;
|
|
||||||
|
|
||||||
// Apply maximum step size limit
|
|
||||||
if h_next.abs() > self.h_max {
|
|
||||||
h_next = self.h_max.copysign(h_next);
|
|
||||||
}
|
|
||||||
|
|
||||||
TryStep::Accepted(h, h_next)
|
|
||||||
} else {
|
|
||||||
// Step rejected - propose smaller step
|
|
||||||
// Use only proportional control for rejection (more aggressive)
|
|
||||||
let factor_11 = err.powf(self.beta1);
|
|
||||||
let h_next = h / (self.factor_c1.min(factor_11 / self.safety_factor));
|
|
||||||
|
|
||||||
// Note: Don't update history on rejection
|
|
||||||
TryStep::NotYetAccepted(h_next)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PIDController {
|
|
||||||
/// Create a new PID controller with custom parameters
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `beta1` - Proportional coefficient (typically 0.3-0.5)
|
|
||||||
/// * `beta2` - Integral coefficient (typically 0.04-0.1)
|
|
||||||
/// * `beta3` - Derivative coefficient (typically 0.01-0.05)
|
|
||||||
/// * `max_factor` - Maximum step size increase factor (typically 10.0)
|
|
||||||
/// * `min_factor` - Maximum step size decrease factor (typically 0.2)
|
|
||||||
/// * `h_max` - Maximum allowed step size
|
|
||||||
/// * `safety_factor` - Safety factor (typically 0.9)
|
|
||||||
/// * `initial_h` - Initial step size guess
|
|
||||||
pub fn new(
|
|
||||||
beta1: f64,
|
|
||||||
beta2: f64,
|
|
||||||
beta3: f64,
|
|
||||||
max_factor: f64,
|
|
||||||
min_factor: f64,
|
|
||||||
h_max: f64,
|
|
||||||
safety_factor: f64,
|
|
||||||
initial_h: f64,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
beta1,
|
|
||||||
beta2,
|
|
||||||
beta3,
|
|
||||||
factor_c1: 1.0 / min_factor,
|
|
||||||
factor_c2: 1.0 / max_factor,
|
|
||||||
h_max: h_max.abs(),
|
|
||||||
safety_factor,
|
|
||||||
err_old: 0.0, // No history initially
|
|
||||||
err_older: 0.0, // No history initially
|
|
||||||
h_old: 0.0, // No history initially
|
|
||||||
next_step_guess: TryStep::NotYetAccepted(initial_h),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a PID controller with coefficients scaled for a specific method order
|
|
||||||
///
|
|
||||||
/// Uses the Gustafsson coefficients scaled by order:
|
|
||||||
/// - β₁ = 0.49 / (order + 1)
|
|
||||||
/// - β₂ = 0.34 / (order + 1)
|
|
||||||
/// - β₃ = 0.10 / (order + 1)
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `order` - Order of the integration method (e.g., 5 for DP5, 7 for Vern7)
|
|
||||||
pub fn for_order(order: usize) -> Self {
|
|
||||||
let k_plus_1 = (order + 1) as f64;
|
|
||||||
Self::new(
|
|
||||||
0.49 / k_plus_1, // beta1: proportional
|
|
||||||
0.34 / k_plus_1, // beta2: integral
|
|
||||||
0.10 / k_plus_1, // beta3: derivative
|
|
||||||
10.0, // max_factor
|
|
||||||
0.2, // min_factor
|
|
||||||
100000.0, // h_max
|
|
||||||
0.9, // safety_factor
|
|
||||||
1e-4, // initial_h
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reset the controller's error history
|
|
||||||
///
|
|
||||||
/// Useful when switching algorithms or restarting integration
|
|
||||||
pub fn reset(&mut self) {
|
|
||||||
self.err_old = 0.0;
|
|
||||||
self.err_older = 0.0;
|
|
||||||
self.h_old = 0.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for PIDController {
|
|
||||||
/// Default PID controller using conservative coefficients
|
|
||||||
///
|
|
||||||
/// Uses conservative PID coefficients that provide stable performance
|
|
||||||
/// across a wide range of problems:
|
|
||||||
/// - β₁ = 0.07 (proportional)
|
|
||||||
/// - β₂ = 0.04 (integral)
|
|
||||||
/// - β₃ = 0.01 (derivative)
|
|
||||||
///
|
|
||||||
/// These values are appropriate for typical ODE methods of order 5-7.
|
|
||||||
/// For method-specific tuning, use `PIDController::for_order(order)` instead.
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new(
|
|
||||||
0.07, // beta1 (proportional)
|
|
||||||
0.04, // beta2 (integral)
|
|
||||||
0.01, // beta3 (derivative)
|
|
||||||
10.0, // max_factor
|
|
||||||
0.2, // min_factor
|
|
||||||
100000.0, // h_max
|
|
||||||
0.9, // safety_factor
|
|
||||||
1e-4, // initial_h
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_pi_controller_creation() {
|
fn test_controller_creation() {
|
||||||
let controller = PIController::new(0.17, 0.04, 10.0, 0.2, 10.0, 0.9, 1e-4);
|
let controller = PIController::new(0.17, 0.04, 10.0, 0.2, 10.0, 0.9, 1e-4);
|
||||||
|
|
||||||
assert!(controller.alpha == 0.17);
|
assert!(controller.alpha == 0.17);
|
||||||
@@ -334,229 +111,4 @@ mod tests {
|
|||||||
assert!(controller.safety_factor == 0.9);
|
assert!(controller.safety_factor == 0.9);
|
||||||
assert!(controller.next_step_guess == TryStep::NotYetAccepted(1e-4));
|
assert!(controller.next_step_guess == TryStep::NotYetAccepted(1e-4));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_pid_controller_creation() {
|
|
||||||
let controller = PIDController::new(0.49, 0.34, 0.10, 10.0, 0.2, 10.0, 0.9, 1e-4);
|
|
||||||
|
|
||||||
assert_eq!(controller.beta1, 0.49);
|
|
||||||
assert_eq!(controller.beta2, 0.34);
|
|
||||||
assert_eq!(controller.beta3, 0.10);
|
|
||||||
assert_eq!(controller.factor_c1, 1.0 / 0.2);
|
|
||||||
assert_eq!(controller.factor_c2, 1.0 / 10.0);
|
|
||||||
assert_eq!(controller.h_max, 10.0);
|
|
||||||
assert_eq!(controller.safety_factor, 0.9);
|
|
||||||
assert_eq!(controller.err_old, 0.0);
|
|
||||||
assert_eq!(controller.err_older, 0.0);
|
|
||||||
assert_eq!(controller.h_old, 0.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_pid_for_order() {
|
|
||||||
let controller = PIDController::for_order(5);
|
|
||||||
|
|
||||||
// For order 5, k+1 = 6
|
|
||||||
assert!((controller.beta1 - 0.49 / 6.0).abs() < 1e-10);
|
|
||||||
assert!((controller.beta2 - 0.34 / 6.0).abs() < 1e-10);
|
|
||||||
assert!((controller.beta3 - 0.10 / 6.0).abs() < 1e-10);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_pid_default() {
|
|
||||||
let controller = PIDController::default();
|
|
||||||
|
|
||||||
// Default uses conservative PID coefficients
|
|
||||||
assert_eq!(controller.beta1, 0.07);
|
|
||||||
assert_eq!(controller.beta2, 0.04);
|
|
||||||
assert_eq!(controller.beta3, 0.01); // True PID with derivative term
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_pid_reset() {
|
|
||||||
let mut controller = PIDController::default();
|
|
||||||
|
|
||||||
// Simulate some history
|
|
||||||
controller.err_old = 0.5;
|
|
||||||
controller.err_older = 0.3;
|
|
||||||
controller.h_old = 0.01;
|
|
||||||
|
|
||||||
controller.reset();
|
|
||||||
|
|
||||||
assert_eq!(controller.err_old, 0.0);
|
|
||||||
assert_eq!(controller.err_older, 0.0);
|
|
||||||
assert_eq!(controller.h_old, 0.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_pi_vs_pid_smooth_problem() {
|
|
||||||
// For smooth problems, PI and PID should perform similarly
|
|
||||||
// Test with exponential decay: y' = -y
|
|
||||||
|
|
||||||
let mut pi = PIController::default();
|
|
||||||
let mut pid = PIDController::default();
|
|
||||||
|
|
||||||
// Simulate a sequence of small errors (smooth problem)
|
|
||||||
let errors = vec![0.8, 0.6, 0.5, 0.45, 0.4, 0.35, 0.3];
|
|
||||||
let h = 0.01;
|
|
||||||
|
|
||||||
let mut pi_steps = Vec::new();
|
|
||||||
let mut pid_steps = Vec::new();
|
|
||||||
|
|
||||||
for &err in &errors {
|
|
||||||
let mut pi_result = <PIController as Controller<1>>::determine_step(&mut pi, h, err);
|
|
||||||
let mut pid_result = <PIDController as Controller<1>>::determine_step(&mut pid, h, err);
|
|
||||||
|
|
||||||
if pi_result.is_accepted() {
|
|
||||||
pi_steps.push(pi_result.extract());
|
|
||||||
pi.next_step_guess = pi_result.reset().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
if pid_result.is_accepted() {
|
|
||||||
pid_steps.push(pid_result.extract());
|
|
||||||
pid.next_step_guess = pid_result.reset().unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Both should accept all steps for this smooth sequence
|
|
||||||
assert_eq!(pi_steps.len(), errors.len());
|
|
||||||
assert_eq!(pid_steps.len(), errors.len());
|
|
||||||
|
|
||||||
// Step sizes should be reasonably similar (within 20%)
|
|
||||||
// PID may differ slightly due to derivative term
|
|
||||||
for (pi_h, pid_h) in pi_steps.iter().zip(pid_steps.iter()) {
|
|
||||||
let relative_diff = ((pi_h - pid_h) / pi_h).abs();
|
|
||||||
assert!(
|
|
||||||
relative_diff < 0.2,
|
|
||||||
"Step sizes differ by more than 20%: PI={}, PID={}",
|
|
||||||
pi_h,
|
|
||||||
pid_h
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_pid_bootstrap() {
|
|
||||||
// Test that PID progressively uses P → PI → PID as history builds
|
|
||||||
let mut pid = PIDController::new(0.49, 0.34, 0.10, 10.0, 0.2, 100.0, 0.9, 0.01);
|
|
||||||
|
|
||||||
let h = 0.01;
|
|
||||||
let err1 = 0.5;
|
|
||||||
let err2 = 0.4;
|
|
||||||
let err3 = 0.3;
|
|
||||||
|
|
||||||
// First step: should use only proportional (beta1)
|
|
||||||
assert_eq!(pid.err_old, 0.0);
|
|
||||||
assert_eq!(pid.err_older, 0.0);
|
|
||||||
let step1 = <PIDController as Controller<1>>::determine_step(&mut pid, h, err1);
|
|
||||||
assert!(step1.is_accepted());
|
|
||||||
|
|
||||||
// After first step, err_old is updated but err_older is still 0
|
|
||||||
assert!(pid.err_old > 0.0);
|
|
||||||
assert_eq!(pid.err_older, 0.0);
|
|
||||||
|
|
||||||
// Second step: should use PI (beta1 and beta2)
|
|
||||||
let step2 = <PIDController as Controller<1>>::determine_step(&mut pid, h, err2);
|
|
||||||
assert!(step2.is_accepted());
|
|
||||||
|
|
||||||
// After second step, both err_old and err_older should be set
|
|
||||||
assert!(pid.err_old > 0.0);
|
|
||||||
assert!(pid.err_older > 0.0);
|
|
||||||
assert!(pid.h_old > 0.0);
|
|
||||||
|
|
||||||
// Third step: should use full PID (beta1, beta2, and beta3)
|
|
||||||
let step3 = <PIDController as Controller<1>>::determine_step(&mut pid, h, err3);
|
|
||||||
assert!(step3.is_accepted());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_pid_step_rejection() {
|
|
||||||
// Test that error history is NOT updated after rejection
|
|
||||||
let mut pid = PIDController::default();
|
|
||||||
|
|
||||||
let h = 0.01;
|
|
||||||
|
|
||||||
// First, accept a step to build history
|
|
||||||
let err_good = 0.5;
|
|
||||||
let step1 = <PIDController as Controller<1>>::determine_step(&mut pid, h, err_good);
|
|
||||||
assert!(step1.is_accepted());
|
|
||||||
|
|
||||||
let err_old_before = pid.err_old;
|
|
||||||
let err_older_before = pid.err_older;
|
|
||||||
let h_old_before = pid.h_old;
|
|
||||||
|
|
||||||
// Now reject a step with large error
|
|
||||||
let err_bad = 2.0;
|
|
||||||
let step2 = <PIDController as Controller<1>>::determine_step(&mut pid, h, err_bad);
|
|
||||||
assert!(!step2.is_accepted());
|
|
||||||
|
|
||||||
// History should NOT have changed after rejection
|
|
||||||
assert_eq!(pid.err_old, err_old_before);
|
|
||||||
assert_eq!(pid.err_older, err_older_before);
|
|
||||||
assert_eq!(pid.h_old, h_old_before);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_pid_vs_pi_oscillatory() {
|
|
||||||
// Test on oscillatory error pattern (simulating difficult problem)
|
|
||||||
// True PID (with derivative term) should provide smoother step size evolution
|
|
||||||
|
|
||||||
let mut pi = PIController::default();
|
|
||||||
// Use actual PID with non-zero beta3 (Gustafsson coefficients for order 5)
|
|
||||||
let mut pid = PIDController::for_order(5);
|
|
||||||
|
|
||||||
// Simulate oscillatory error pattern
|
|
||||||
let errors = vec![0.8, 0.3, 0.9, 0.2, 0.85, 0.25, 0.8, 0.3];
|
|
||||||
let h = 0.01;
|
|
||||||
|
|
||||||
let mut pi_ratios = Vec::new();
|
|
||||||
let mut pid_ratios = Vec::new();
|
|
||||||
|
|
||||||
let mut pi_h_prev = h;
|
|
||||||
let mut pid_h_prev = h;
|
|
||||||
|
|
||||||
for &err in &errors {
|
|
||||||
let mut pi_result = <PIController as Controller<1>>::determine_step(&mut pi, h, err);
|
|
||||||
let mut pid_result = <PIDController as Controller<1>>::determine_step(&mut pid, h, err);
|
|
||||||
|
|
||||||
if pi_result.is_accepted() {
|
|
||||||
let pi_h_next = pi_result.reset().unwrap().extract();
|
|
||||||
pi_ratios.push((pi_h_next / pi_h_prev).ln().abs());
|
|
||||||
pi_h_prev = pi_h_next;
|
|
||||||
pi.next_step_guess = TryStep::NotYetAccepted(pi_h_next);
|
|
||||||
}
|
|
||||||
|
|
||||||
if pid_result.is_accepted() {
|
|
||||||
let pid_h_next = pid_result.reset().unwrap().extract();
|
|
||||||
pid_ratios.push((pid_h_next / pid_h_prev).ln().abs());
|
|
||||||
pid_h_prev = pid_h_next;
|
|
||||||
pid.next_step_guess = TryStep::NotYetAccepted(pid_h_next);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute standard deviation of log step size ratios
|
|
||||||
let pi_mean: f64 = pi_ratios.iter().sum::<f64>() / pi_ratios.len() as f64;
|
|
||||||
let pi_variance: f64 = pi_ratios
|
|
||||||
.iter()
|
|
||||||
.map(|r| (r - pi_mean).powi(2))
|
|
||||||
.sum::<f64>()
|
|
||||||
/ pi_ratios.len() as f64;
|
|
||||||
let pi_std = pi_variance.sqrt();
|
|
||||||
|
|
||||||
let pid_mean: f64 = pid_ratios.iter().sum::<f64>() / pid_ratios.len() as f64;
|
|
||||||
let pid_variance: f64 = pid_ratios
|
|
||||||
.iter()
|
|
||||||
.map(|r| (r - pid_mean).powi(2))
|
|
||||||
.sum::<f64>()
|
|
||||||
/ pid_ratios.len() as f64;
|
|
||||||
let pid_std = pid_variance.sqrt();
|
|
||||||
|
|
||||||
// With true PID (non-zero beta3), we expect similar or better stability
|
|
||||||
// Allow some tolerance since this is a simple synthetic test
|
|
||||||
assert!(
|
|
||||||
pid_std <= pi_std * 1.1,
|
|
||||||
"PID should not be significantly worse than PI: PI_std={:.3}, PID_std={:.3}",
|
|
||||||
pi_std,
|
|
||||||
pid_std
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ pub mod problem;
|
|||||||
|
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
pub use super::callback::{stop, Callback};
|
pub use super::callback::{stop, Callback};
|
||||||
pub use super::controller::{PIController, PIDController};
|
pub use super::controller::PIController;
|
||||||
pub use super::integrator::bs3::BS3;
|
pub use super::integrator::bs3::BS3;
|
||||||
pub use super::integrator::dormand_prince::DormandPrince45;
|
pub use super::integrator::dormand_prince::DormandPrince45;
|
||||||
pub use super::integrator::vern7::Vern7;
|
pub use super::integrator::vern7::Vern7;
|
||||||
|
|||||||
Reference in New Issue
Block a user