Chapter 20: Sports Science Algorithms & Intelligence
This chapter explores how Pierre implements sports science algorithms for training load management, fitness tracking, and performance estimation. You’ll learn about TSS calculation, CTL/ATL/TSB (Performance Manager Chart), VO2 max estimation, FTP detection, and algorithm configuration patterns.
Algorithm Configuration Pattern
Pierre uses enums to select between multiple algorithm implementations:
User selects algorithm → Enum variant → Implementation strategy → Result
Pattern benefits:
- Flexibility: Easy to add new algorithms
- Testability: Compare algorithm outputs for validation
- User choice: Power users can optimize for their data/use case
- Backwards compatibility: Default impl when new algorithms added
Training Stress Score TSS
TSS quantifies training load from a single workout.
Source: src/intelligence/algorithms/tss.rs:10-52
#![allow(unused)]
fn main() {
/// TSS calculation algorithm selection
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[derive(Default)]
pub enum TssAlgorithm {
/// Average power based TSS (current default)
///
/// Formula: `duration_hours x (avg_power/FTP)² x 100`
///
/// Pros: O(1) computation, works without power stream
/// Cons: Underestimates variable efforts by 15-30%
#[default]
AvgPower,
/// Normalized Power based TSS (industry standard)
///
/// Formula: `duration_hours x (NP/FTP)² x 100`
///
/// `NP = ⁴√(mean(mean_per_30s_window(power⁴)))`
///
/// Pros: Physiologically accurate (R²=0.92 vs glycogen depletion)
/// Cons: Requires ≥30s power stream data
NormalizedPower {
/// Rolling window size in seconds (standard: 30)
window_seconds: u32,
},
/// Hybrid approach: Try NP, fallback to `avg_power` if stream unavailable
///
/// Best of both worlds for defensive programming
Hybrid,
}
}
TSS interpretation:
- < 150: Easy recovery ride/run
- 150-300: Moderate workout
- 300-450: Hard training session
- > 450: Very hard/race effort
Average Power TSS (simple):
Source: src/intelligence/algorithms/tss.rs:111-124
#![allow(unused)]
fn main() {
fn calculate_avg_power_tss(
activity: &Activity,
ftp: f64,
duration_hours: f64,
) -> Result<f64, AppError> {
let avg_power = f64::from(
activity
.average_power
.ok_or_else(|| AppError::not_found("average power data".to_owned()))?,
);
let intensity_factor = avg_power / ftp;
Ok((duration_hours * intensity_factor * intensity_factor * TSS_BASE_MULTIPLIER).round())
}
}
Normalized Power TSS (accurate):
Source: src/intelligence/algorithms/tss.rs:129-139
#![allow(unused)]
fn main() {
fn calculate_np_tss(
activity: &Activity,
ftp: f64,
duration_hours: f64,
window_seconds: u32,
) -> Result<f64, AppError> {
// Calculate TSS using normalized power from activity power stream data
let np = Self::calculate_normalized_power(activity, window_seconds)?;
let intensity_factor = np / ftp;
Ok((duration_hours * intensity_factor * intensity_factor * TSS_BASE_MULTIPLIER).round())
}
}
Normalized Power formula:
NP = ⁴√(mean(mean_per_30s_window(power⁴)))
Why 4th power: Matches physiological stress curve (glycogen depletion, lactate accumulation).
CTL/ATL/TSB (performance Manager Chart)
CTL/ATL/TSB track fitness, fatigue, and form over time.
Definitions:
- CTL (Chronic Training Load): 42-day exponential moving average of TSS (fitness)
- ATL (Acute Training Load): 7-day exponential moving average of TSS (fatigue)
- TSB (Training Stress Balance): CTL - ATL (form/freshness)
Source: src/intelligence/algorithms/training_load.rs:8-85
#![allow(unused)]
fn main() {
/// Training load calculation algorithm selection
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum TrainingLoadAlgorithm {
/// Exponential Moving Average (EMA)
///
/// Formula: `α = 2/(N+1)`, `EMA_t = α x TSS_t + (1-α) x EMA_{t-1}`
///
/// Standard method used by TrainingPeaks Performance Manager Chart.
/// Recent days weighted more heavily with exponential decay.
Ema {
/// CTL window in days (default 42 for fitness)
ctl_days: i64,
/// ATL window in days (default 7 for fatigue)
atl_days: i64,
},
/// Simple Moving Average (SMA)
///
/// Formula: `SMA = Σ(TSS_i) / N` for i in [t-N+1, t]
///
/// All days in window weighted equally.
Sma {
ctl_days: i64,
atl_days: i64,
},
/// Weighted Moving Average (WMA)
///
/// Formula: `WMA = Σ(w_i x TSS_i) / Σ(w_i)` where `w_i = i` (linear weights)
///
/// Recent days weighted linearly more than older days.
Wma {
ctl_days: i64,
atl_days: i64,
},
/// Kalman Filter
///
/// State-space model with process and measurement noise.
/// Optimal estimation when data is noisy or has gaps.
KalmanFilter {
/// Process noise (training load variability)
process_noise: f64,
/// Measurement noise (TSS measurement error)
measurement_noise: f64,
},
}
}
EMA calculation:
Source: src/intelligence/algorithms/training_load.rs:122-136
#![allow(unused)]
fn main() {
pub fn calculate_ctl(&self, tss_data: &[TssDataPoint]) -> Result<f64, AppError> {
if tss_data.is_empty() {
return Ok(0.0);
}
match self {
Self::Ema { ctl_days, .. } => Self::calculate_ema(tss_data, *ctl_days),
Self::Sma { ctl_days, .. } => Self::calculate_sma(tss_data, *ctl_days),
Self::Wma { ctl_days, .. } => Self::calculate_wma(tss_data, *ctl_days),
Self::KalmanFilter {
process_noise,
measurement_noise,
} => Self::calculate_kalman(tss_data, *process_noise, *measurement_noise),
}
}
}
TSB interpretation:
- TSB > +25: Well-rested, ready for peak performance
- TSB +10 to +25: Fresh, good for races
- TSB -10 to +10: Balanced, sustainable training
- TSB -10 to -30: Fatigued, productive overload
- TSB < -30: High risk of overtraining
EMA formula:
α = 2 / (N + 1)
CTL_today = α × TSS_today + (1 - α) × CTL_yesterday
For CTL (N=42): α = 2/43 ≈ 0.0465 (slow adaptation)
For ATL (N=7): α = 2/8 = 0.25 (fast response)
VO2 Max Estimation
VO2 max represents maximal aerobic capacity (ml/kg/min).
Source: src/intelligence/algorithms/vo2max.rs:18-100
#![allow(unused)]
fn main() {
/// VO2max estimation algorithm selection
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum Vo2maxAlgorithm {
/// From Jack Daniels' VDOT
///
/// Formula: `VO2max = VDOT x 3.5`
///
/// VDOT is Jack Daniels' running economy-adjusted VO2max measure.
FromVdot {
/// VDOT value (30-85 for recreational to elite)
vdot: f64,
},
/// Cooper 12-Minute Run Test
///
/// Formula: `VO2max = (distance_meters - 504.9) / 44.73`
///
/// Run as far as possible in 12 minutes on a flat track.
CooperTest {
/// Distance covered in 12 minutes (meters)
distance_meters: f64,
},
/// Rockport 1-Mile Walk Test
///
/// Formula: `VO2max = 132.853 - 0.0769×weight - 0.3877×age + 6.315×gender - 3.2649×time - 0.1565×HR`
///
/// Walk 1 mile as fast as possible, measure time and heart rate at finish.
/// Gender: 0 = female, 1 = male
RockportWalk {
weight_kg: f64,
age: u8,
gender: u8,
time_seconds: f64,
heart_rate: f64,
},
/// Åstrand-Ryhming Cycle Ergometer Test
///
/// Submaximal cycle test at steady-state heart rate (120-170 bpm).
AstrandRyhming {
gender: u8,
heart_rate: f64,
power_watts: f64,
},
/// Speed-based VO2max estimation from race performance
///
/// Formula: `VO2max = 15.3 × (MaxSpeed / RecoverySpeed)`
///
/// Uses maximum sustainable speed vs recovery speed ratio.
FromPace {
/// Maximum speed in m/s during test
max_speed_ms: f64,
/// Recovery speed in m/s
recovery_speed_ms: f64,
},
/// Hybrid: Auto-select best method based on available data
///
/// Attempts to use the most appropriate algorithm given available
/// test parameters. Returns error if insufficient data provided.
Hybrid,
}
}
VO2 max ranges (ml/kg/min):
- Untrained: 30-40 (recreational)
- Trained: 40-50 (club runner)
- Well-trained: 50-60 (competitive)
- Elite: 60-70 (national level)
- World-class: 70-85 (Olympic/professional)
Cooper Test example:
Distance: 3000 meters in 12 minutes
VO2max = (3000 - 504.9) / 44.73 ≈ 55.8 ml/kg/min (well-trained)
Algorithm Selection Pattern
All algorithms follow the same pattern:
#![allow(unused)]
fn main() {
enum Algorithm {
Method1 { params },
Method2 { params },
Method3,
}
impl Algorithm {
fn calculate(&self, data: &Data) -> Result<f64> {
match self {
Self::Method1 { params } => /* implementation */,
Self::Method2 { params } => /* implementation */,
Self::Method3 => /* implementation */,
}
}
}
}
Benefits:
- Type safety: Compiler ensures all enum variants handled
- Documentation: Variant doc comments explain algorithms
- Flexibility: Easy to add new algorithms
- Configuration: Serialize/deserialize for user preferences
- Testing: Compare algorithms for validation
Scientific Validation
Pierre includes scientific references for algorithms:
TSS references:
- Coggan, A. & Allen, H. (2010). “Training and Racing with a Power Meter.” VeloPress.
- Sanders, D. & Heijboer, M. (2018). “The anaerobic power reserve.” J Sports Sci, 36(6), 621-629.
CTL/ATL/TSB references:
- Coggan, A. (2003). “Training and Racing Using a Power Meter.” Peaksware LLC.
- Banister, E.W. (1991). “Modeling elite athletic performance.” Physiological Testing of Elite Athletes.
VO2 max references:
- Daniels, J. (2013). “Daniels’ Running Formula” (3rd ed.). Human Kinetics.
- Cooper, K.H. (1968). “A means of assessing maximal oxygen intake.” JAMA, 203(3), 201-204.
Validation approach:
- Literature-based: All formulas from peer-reviewed research
- Industry standards: TrainingPeaks, Strava algorithms as benchmarks
- Correlation studies: Verify against physiological measurements
Key Takeaways
-
Enum-based selection: All algorithms use enum pattern for flexibility and type safety.
-
TSS calculation: Average power (fast, less accurate) vs Normalized Power (slow, more accurate).
-
CTL/ATL/TSB: 42-day fitness, 7-day fatigue, balance indicates form/readiness.
-
EMA standard: Exponential moving average matches TrainingPeaks industry standard.
-
VO2 max estimation: Multiple test protocols (Cooper, Rockport, Åstrand-Ryhming).
-
Scientific references: All algorithms cite peer-reviewed research for validation.
-
Multiple strategies: Users can choose algorithms based on data availability and preferences.
-
Hybrid fallback: Defensive programming with primary + fallback implementations.
-
Documentation in code: Variant doc comments explain formulas, pros/cons, scientific basis.
-
Serde support: All algorithms serialize/deserialize for configuration persistence.
Detailed Methodology Reference
For comprehensive technical documentation of all sports science algorithms, see the Intelligence Methodology Document in the Pierre source repository:
docs/intelligence-methodology.md
This 100+ section reference document covers:
| Topic | Description |
|---|---|
| Architecture Overview | Foundation modules, core modules, 47 intelligence tools |
| Data Sources | Primary data, user profiles, provider normalization |
| Personalization Engine | Age-based MaxHR, HR zones, power zones |
| Core Metrics | Pace/speed conversion, TSS variants, Normalized Power |
| Training Load | CTL/ATL/TSB mathematical formulation with EMA |
| Overtraining Detection | Risk indicators and warning thresholds |
| VDOT Performance | Daniels tables, race prediction, accuracy verification |
| Pattern Recognition | Weekly schedules, hard/easy alternation, volume progression |
| Sleep & Recovery | Sleep quality scoring, recovery score calculation |
| Validation & Safety | Parameter bounds, confidence levels, edge cases |
| Configuration Strategies | Conservative, default, aggressive profiles |
| Debugging Guide | Metric-specific troubleshooting, platform issues |
For implementers: The methodology document includes complete formulas, code examples, and scientific references for every algorithm in the Pierre intelligence engine.
Next Chapter: Chapter 21: Training Load, Recovery & Sleep Analysis - Learn how Pierre analyzes recovery metrics, sleep quality, HRV, and suggests optimal training intensity based on recovery status.