The mean-maximal power (MMP) curve plots the best average power you've ever sustained for each duration — 5 seconds, 1 minute, 5 minutes, 20 minutes, 60 minutes. It's one of the most informative views of a cyclist's fitness, and it's paywalled on Strava Premium and TrainingPeaks. pacelore computes it for free.
What the curve shows
The x-axis is duration (logarithmic scale makes it easier to read). The y-axis is average power in watts. Each point on the curve is your highest average power over that duration, taken from across all your rides.
The shape of the curve reveals your physiology:
- Steep left side (high short-duration power relative to long-duration power): sprinter or track rider profile. Strong fast-twitch capacity.
- Flat curve (similar power from 5min to 60min): diesel engine. High aerobic capacity, less top-end power.
- Drop-off around 3-5 minutes: typical transition between anaerobic and aerobic contributions.
As fitness improves, the curve shifts upward. Six weeks of threshold training typically shows the most improvement at the 20–60 minute durations. A sprint training block lifts the left end. The curve is a fingerprint of how you've been training.
How it's computed
For each activity with power data, compute the best average power over a set of standard durations. The standard durations pacelore uses: 5s, 10s, 30s, 1min, 2min, 5min, 10min, 20min, 30min, 60min, 90min.
For a single duration d over a power array of length n, the naive approach is O(n × d): slide a window of length d seconds, compute the average at each position, take the max. That's too slow for 60-minute windows over a 4-hour ride.
The efficient approach uses a prefix sum array:
function bestAvgPower(watts: number[], durationSec: number): number {
const n = watts.length;
if (n < durationSec) return 0;
// prefix sum
const prefix = new Float64Array(n + 1);
for (let i = 0; i < n; i++) {
prefix[i + 1] = prefix[i] + watts[i];
}
let best = 0;
for (let i = durationSec; i <= n; i++) {
const avg = (prefix[i] - prefix[i - durationSec]) / durationSec;
if (avg > best) best = avg;
}
return best;
} With the prefix sum precomputed once, each window average is O(1). The full scan over all positions is O(n). Total: O(n) per duration, O(n × k) for k durations. For a 4-hour ride at 1Hz (14,400 samples) and 11 durations, that's around 158,000 operations — runs in under a millisecond.
Building the curve across all rides
The per-ride best powers are stored in the activity_mmp table with one row per (activity, duration) pair. The athlete-level MMP curve is the column-wise maximum across all activities:
SELECT duration_sec, MAX(avg_watts) as peak_watts
FROM activity_mmp
WHERE athlete_id = ?
GROUP BY duration_sec
ORDER BY duration_sec;
This means adding a new activity only requires inserting new rows into activity_mmp, then the query automatically picks up the new maximums. No recomputation across all activities needed.
FTP estimation from the 20-minute point
The most common field-test FTP estimation: 95% of your best 20-minute average power. The 5% discount accounts for the fact that the 20-minute test effort isn't exactly equivalent to a full-hour effort.
const ftp_estimate = mmp20min * 0.95; This is an estimate, not a gold standard. Athletes who are better at 20-minute efforts relative to 60-minute efforts will overestimate their FTP this way. Athletes with a diesel engine profile will underestimate it. The MMP curve makes this bias visible — if your 60-minute MMP is higher than 0.95× your 20-minute MMP (rare but possible after specific training), use the 60-minute value directly.
Comparing across time
pacelore shows the MMP curve with a date range selector. You can compare this month's curve against last season's, or overlay the curve from a specific training block. The comparison makes fitness changes unambiguous: either the curve shifted up or it didn't.
Strava charges for this view because it requires storing per-activity power arrays (R2 storage cost) and running the prefix sum computation on ingest (Workers CPU time). At Cloudflare pricing, the marginal cost per athlete per month for MMP storage and computation is small enough to round to zero. The paywall exists because the platform can charge for it, not because it's expensive to provide.