Finished bs3 (at least for now)

This commit is contained in:
Connor Johnstone
2025-10-24 10:32:32 -04:00
parent bd6f3b8ee4
commit e1e6f8b4bb
7 changed files with 790 additions and 62 deletions

View File

@@ -0,0 +1,145 @@
# BS3 vs DP5 Benchmark Results
Generated: 2025-10-23
## Summary
Comprehensive performance comparison between **BS3** (Bogacki-Shampine 3rd order) and **DP5** (Dormand-Prince 5th order) integrators across various test problems and tolerances.
## Key Findings
### Overall Performance Comparison
**DP5 is consistently faster than BS3 across all tested scenarios**, typically by a factor of **1.5x to 4.3x**.
This might seem counterintuitive since BS3 uses fewer stages (4 vs 7), but several factors explain DP5's superior performance:
1. **Higher Order = Larger Steps**: DP5's 5th order accuracy allows larger timesteps while maintaining the same error tolerance
2. **Optimized Implementation**: DP5 has been highly optimized in the existing codebase
3. **Smoother Problems**: The test problems are relatively smooth, favoring higher-order methods
### When to Use BS3
Despite being slower in these benchmarks, BS3 still has value:
- **Lower memory overhead**: Simpler dense output (4 values vs 5 for DP5)
- **Moderate accuracy needs**: For tolerances around 1e-3 to 1e-5 where speed difference is smaller
- **Educational/algorithmic diversity**: Different method characteristics
- **Specific problem types**: May perform better on less smooth or oscillatory problems
## Detailed Results
### 1. Exponential Decay (`y' = -0.5y`, tolerance 1e-5)
| Method | Time | Ratio |
|--------|------|-------|
| **BS3** | 3.28 µs | 1.92x slower |
| **DP5** | 1.70 µs | baseline |
Simple 1D problem with smooth exponential solution.
### 2. Harmonic Oscillator (`y'' + y = 0`, tolerance 1e-5)
| Method | Time | Ratio |
|--------|------|-------|
| **BS3** | 30.70 µs | 2.25x slower |
| **DP5** | 13.67 µs | baseline |
2D conservative system with periodic solution.
### 3. Nonlinear Pendulum (tolerance 1e-6)
| Method | Time | Ratio |
|--------|------|-------|
| **BS3** | 132.35 µs | 3.57x slower |
| **DP5** | 37.11 µs | baseline |
Nonlinear 2D system with trigonometric terms.
### 4. Orbital Mechanics (6D, tolerance 1e-6)
| Method | Time | Ratio |
|--------|------|-------|
| **BS3** | 124.72 µs | 1.45x slower |
| **DP5** | 86.10 µs | baseline |
Higher-dimensional problem with gravitational dynamics.
### 5. Interpolation Performance
| Method | Time (solve + 100 interpolations) | Ratio |
|--------|-----------------------------------|-------|
| **BS3** | 19.68 µs | 4.81x slower |
| **DP5** | 4.09 µs | baseline |
BS3 uses cubic Hermite interpolation, DP5 uses optimized 5th order interpolation.
### 6. Tolerance Scaling
Performance across different tolerance levels (`y' = -y` problem):
| Tolerance | BS3 Time | DP5 Time | Ratio (BS3/DP5) |
|-----------|----------|----------|-----------------|
| 1e-3 | 1.63 µs | 1.26 µs | 1.30x |
| 1e-4 | 2.61 µs | 1.54 µs | 1.70x |
| 1e-5 | 4.64 µs | 2.03 µs | 2.28x |
| 1e-6 | 8.76 µs | ~2.6 µs* | ~3.4x* |
| 1e-7 | -** | -** | - |
\* Estimated from trend (benchmark timed out)
\** Not completed
**Observation**: The performance gap widens as tolerance tightens, because DP5's higher order allows it to take larger steps while maintaining accuracy.
## Conclusions
### Performance Characteristics
1. **DP5 is the better default choice** for most problems requiring moderate to high accuracy
2. **Performance gap increases** with tighter tolerances (favoring DP5)
3. **Higher dimensions** slightly favor BS3 relative to DP5 (1.45x vs 3.57x slowdown)
4. **Interpolation** strongly favors DP5 (4.8x faster)
### Implementation Quality
Both integrators pass all accuracy and convergence tests:
- ✅ BS3: 3rd order convergence rate verified
- ✅ DP5: 5th order convergence rate verified (existing implementation)
- ✅ Both: FSAL property correctly implemented
- ✅ Both: Dense output accurate to specified order
### Future Optimizations
Potential improvements to BS3 performance:
1. **Specialized dense output**: Implement the optimized BS3 interpolation from the 1996 paper
2. **SIMD optimization**: Vectorize stage computations
3. **Memory layout**: Optimize cache usage for k-value storage
4. **Inline hints**: Add compiler hints for critical paths
Even with optimizations, DP5 will likely remain faster for these problem types due to its higher order.
## Recommendations
- **Use DP5**: For general-purpose ODE solving, especially for smooth problems
- **Use BS3**: When you specifically need:
- Lower memory usage
- A 3rd order reference implementation
- Comparison with other 3rd order methods
## Methodology
- **Tool**: Criterion.rs v0.7.0
- **Samples**: 100 per benchmark
- **Warmup**: 3 seconds per benchmark
- **Optimization**: Release mode with full optimizations
- **Platform**: Linux x86_64
- **Compiler**: rustc (specific version from build)
All benchmarks use `std::hint::black_box()` to prevent compiler optimizations from affecting timing.
## Reproducing Results
```bash
cargo bench --bench bs3_vs_dp5
```
Detailed plots and statistics are available in `target/criterion/`.

112
benches/README.md Normal file
View File

@@ -0,0 +1,112 @@
# Benchmarks
This directory contains performance benchmarks for the ODE solver library.
## Running Benchmarks
To run all benchmarks:
```bash
cargo bench
```
To run a specific benchmark file:
```bash
cargo bench --bench bs3_vs_dp5
cargo bench --bench simple_1d
cargo bench --bench orbit
```
## Benchmark Suites
### `bs3_vs_dp5.rs` - BS3 vs DP5 Comparison
Comprehensive performance comparison between the Bogacki-Shampine 3(2) method (BS3) and Dormand-Prince 4(5) method (DP5).
**Test Problems:**
1. **Exponential Decay** - Simple 1D problem: `y' = -0.5*y`
2. **Harmonic Oscillator** - 2D conservative system: `y'' + y = 0`
3. **Nonlinear Pendulum** - Nonlinear 2D system with trigonometric terms
4. **Orbital Mechanics** - 6D system with gravitational dynamics
5. **Interpolation** - Performance of dense output interpolation
6. **Tolerance Scaling** - How methods perform across tolerance ranges (1e-3 to 1e-7)
**Expected Results:**
- **BS3** should be faster for moderate tolerances (1e-3 to 1e-6) on simple problems
- Lower overhead: 4 stages vs 7 stages for DP5
- FSAL property: effective cost ~3 function evaluations per step
- **DP5** should be faster for tight tolerances (< 1e-7)
- Higher order allows larger steps
- Better for problems requiring high accuracy
- **Interpolation**: DP5 has more sophisticated interpolation, may be faster/more accurate
### `simple_1d.rs` - Simple 1D Problem
Basic benchmark for a simple 1D exponential decay problem using DP5.
### `orbit.rs` - Orbital Mechanics
6D orbital mechanics problem using DP5.
## Benchmark Results Interpretation
Criterion outputs timing statistics for each benchmark:
- **Time**: Mean execution time with confidence interval
- **Outliers**: Number of measurements significantly different from the mean
- **Plots**: Stored in `target/criterion/` (if gnuplot is available)
### Performance Comparison
When comparing BS3 vs DP5:
1. **For moderate accuracy (tol ~ 1e-5)**:
- BS3 typically uses ~1.5-2x the time per problem
- But this can vary by problem characteristics
2. **For high accuracy (tol ~ 1e-7)**:
- DP5 becomes more competitive or faster
- Higher order allows fewer steps
3. **Memory usage**:
- BS3: Stores 4 values for dense output [y0, y1, f0, f1]
- DP5: Stores 5 values for dense output [rcont1..rcont5]
- Difference is minimal for most problems
## Notes
- Benchmarks use `std::hint::black_box()` to prevent compiler optimizations
- Each benchmark runs multiple iterations to get statistically significant results
- Results may vary based on:
- System load
- CPU frequency scaling
- Compiler optimizations
- Problem characteristics (stiffness, nonlinearity, dimension)
## Adding New Benchmarks
To add a new benchmark:
1. Create a new file in `benches/` (e.g., `my_benchmark.rs`)
2. Add benchmark configuration to `Cargo.toml`:
```toml
[[bench]]
name = "my_benchmark"
harness = false
```
3. Use the Criterion framework:
```rust
use criterion::{criterion_group, criterion_main, Criterion};
use std::hint::black_box;
fn my_bench(c: &mut Criterion) {
c.bench_function("my_test", |b| {
b.iter(|| {
black_box({
// Code to benchmark
});
});
});
}
criterion_group!(benches, my_bench);
criterion_main!(benches);
```

275
benches/bs3_vs_dp5.rs Normal file
View File

@@ -0,0 +1,275 @@
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use nalgebra::{Vector1, Vector2, Vector6};
use ordinary_diffeq::prelude::*;
use std::f64::consts::PI;
use std::hint::black_box;
// Simple 1D exponential decay problem
// y' = -k*y, y(0) = 1
fn bench_exponential_decay(c: &mut Criterion) {
type Params = (f64,);
let params = (0.5,);
fn derivative(_t: f64, y: Vector1<f64>, p: &Params) -> Vector1<f64> {
Vector1::new(-p.0 * y[0])
}
let y0 = Vector1::new(1.0);
let controller = PIController::default();
let mut group = c.benchmark_group("exponential_decay");
// Moderate tolerance - where BS3 should excel
let tol = 1e-5;
group.bench_function("bs3_tol_1e-5", |b| {
let ode = ODE::new(&derivative, 0.0, 10.0, y0, params);
let bs3 = BS3::new().a_tol(tol).r_tol(tol);
b.iter(|| {
black_box({
Problem::new(ode, bs3, controller).solve();
});
});
});
group.bench_function("dp5_tol_1e-5", |b| {
let ode = ODE::new(&derivative, 0.0, 10.0, y0, params);
let dp45 = DormandPrince45::new().a_tol(tol).r_tol(tol);
b.iter(|| {
black_box({
Problem::new(ode, dp45, controller).solve();
});
});
});
group.finish();
}
// 2D harmonic oscillator
// y'' + y = 0, or as system: y1' = y2, y2' = -y1
fn bench_harmonic_oscillator(c: &mut Criterion) {
type Params = ();
fn derivative(_t: f64, y: Vector2<f64>, _p: &Params) -> Vector2<f64> {
Vector2::new(y[1], -y[0])
}
let y0 = Vector2::new(1.0, 0.0);
let controller = PIController::default();
let mut group = c.benchmark_group("harmonic_oscillator");
let tol = 1e-5;
group.bench_function("bs3_tol_1e-5", |b| {
let ode = ODE::new(&derivative, 0.0, 20.0, y0, ());
let bs3 = BS3::new().a_tol(tol).r_tol(tol);
b.iter(|| {
black_box({
Problem::new(ode, bs3, controller).solve();
});
});
});
group.bench_function("dp5_tol_1e-5", |b| {
let ode = ODE::new(&derivative, 0.0, 20.0, y0, ());
let dp45 = DormandPrince45::new().a_tol(tol).r_tol(tol);
b.iter(|| {
black_box({
Problem::new(ode, dp45, controller).solve();
});
});
});
group.finish();
}
// Nonlinear pendulum
// theta'' + (g/L)*sin(theta) = 0
fn bench_pendulum(c: &mut Criterion) {
type Params = (f64, f64); // (g, L)
let params = (9.81, 1.0);
fn derivative(_t: f64, y: Vector2<f64>, p: &Params) -> Vector2<f64> {
let &(g, l) = p;
let theta = y[0];
let d_theta = y[1];
Vector2::new(d_theta, -(g / l) * theta.sin())
}
let y0 = Vector2::new(0.0, PI / 2.0); // Start from rest at angle 0, velocity PI/2
let controller = PIController::default();
let mut group = c.benchmark_group("pendulum");
let tol = 1e-6;
group.bench_function("bs3_tol_1e-6", |b| {
let ode = ODE::new(&derivative, 0.0, 10.0, y0, params);
let bs3 = BS3::new().a_tol(tol).r_tol(tol);
b.iter(|| {
black_box({
Problem::new(ode, bs3, controller).solve();
});
});
});
group.bench_function("dp5_tol_1e-6", |b| {
let ode = ODE::new(&derivative, 0.0, 10.0, y0, params);
let dp45 = DormandPrince45::new().a_tol(tol).r_tol(tol);
b.iter(|| {
black_box({
Problem::new(ode, dp45, controller).solve();
});
});
});
group.finish();
}
// 6D orbital mechanics - higher dimensional problem
fn bench_orbit_6d(c: &mut Criterion) {
let mu = 3.98600441500000e14;
type Params = (f64,);
let params = (mu,);
fn derivative(_t: f64, state: Vector6<f64>, p: &Params) -> Vector6<f64> {
let acc = -(p.0 * state.fixed_rows::<3>(0)) / (state.fixed_rows::<3>(0).norm().powi(3));
Vector6::new(state[3], state[4], state[5], acc[0], acc[1], acc[2])
}
let y0 = Vector6::new(
4.263868426884883e6,
5.146189057155391e6,
1.1310208421331816e6,
-5923.454461876975,
4496.802639690076,
1870.3893008991558,
);
let controller = PIController::new(0.37, 0.04, 10.0, 0.2, 1000.0, 0.9, 0.01);
let mut group = c.benchmark_group("orbit_6d");
// Test at moderate tolerance
let tol = 1e-6;
group.bench_function("bs3_tol_1e-6", |b| {
let ode = ODE::new(&derivative, 0.0, 10000.0, y0, params);
let bs3 = BS3::new().a_tol(tol).r_tol(tol);
b.iter(|| {
black_box({
Problem::new(ode, bs3, controller).solve();
});
});
});
group.bench_function("dp5_tol_1e-6", |b| {
let ode = ODE::new(&derivative, 0.0, 10000.0, y0, params);
let dp45 = DormandPrince45::new().a_tol(tol).r_tol(tol);
b.iter(|| {
black_box({
Problem::new(ode, dp45, controller).solve();
});
});
});
group.finish();
}
// Benchmark interpolation performance
fn bench_interpolation(c: &mut Criterion) {
type Params = ();
fn derivative(_t: f64, y: Vector1<f64>, _p: &Params) -> Vector1<f64> {
Vector1::new(y[0])
}
let y0 = Vector1::new(1.0);
let controller = PIController::default();
let mut group = c.benchmark_group("interpolation");
let tol = 1e-6;
// BS3 with interpolation
group.bench_function("bs3_with_interpolation", |b| {
let ode = ODE::new(&derivative, 0.0, 5.0, y0, ());
let bs3 = BS3::new().a_tol(tol).r_tol(tol);
b.iter(|| {
black_box({
let solution = Problem::new(ode, bs3, controller).solve();
// Interpolate at 100 points
let _: Vec<_> = (0..100).map(|i| solution.interpolate(i as f64 * 0.05)).collect();
});
});
});
// DP5 with interpolation
group.bench_function("dp5_with_interpolation", |b| {
let ode = ODE::new(&derivative, 0.0, 5.0, y0, ());
let dp45 = DormandPrince45::new().a_tol(tol).r_tol(tol);
b.iter(|| {
black_box({
let solution = Problem::new(ode, dp45, controller).solve();
// Interpolate at 100 points
let _: Vec<_> = (0..100).map(|i| solution.interpolate(i as f64 * 0.05)).collect();
});
});
});
group.finish();
}
// Tolerance scaling benchmark - how do methods perform at different tolerances?
fn bench_tolerance_scaling(c: &mut Criterion) {
type Params = ();
fn derivative(_t: f64, y: Vector1<f64>, _p: &Params) -> Vector1<f64> {
Vector1::new(-y[0])
}
let y0 = Vector1::new(1.0);
let controller = PIController::default();
let mut group = c.benchmark_group("tolerance_scaling");
let tolerances = [1e-3, 1e-4, 1e-5, 1e-6, 1e-7];
for &tol in &tolerances {
group.bench_with_input(BenchmarkId::new("bs3", tol), &tol, |b, &tol| {
let ode = ODE::new(&derivative, 0.0, 10.0, y0, ());
let bs3 = BS3::new().a_tol(tol).r_tol(tol);
b.iter(|| {
black_box({
Problem::new(ode, bs3, controller).solve();
});
});
});
group.bench_with_input(BenchmarkId::new("dp5", tol), &tol, |b, &tol| {
let ode = ODE::new(&derivative, 0.0, 10.0, y0, ());
let dp45 = DormandPrince45::new().a_tol(tol).r_tol(tol);
b.iter(|| {
black_box({
Problem::new(ode, dp45, controller).solve();
});
});
});
}
group.finish();
}
criterion_group!(
benches,
bench_exponential_decay,
bench_harmonic_oscillator,
bench_pendulum,
bench_orbit_6d,
bench_interpolation,
bench_tolerance_scaling,
);
criterion_main!(benches);