Predict your race time.
Plan a goal event from a route, your rider numbers, and the group you expect to ride in. Get a finish-time ceiling (the best you could ride at the effort you set, not a promise) plus per-split target watts for your head unit: Garmin Power Guide overlay or FIT Workout for Wahoo, Hammerhead, and Bryton.
Built for amateurs. Validated on pro rides because public power data lives there.
Pick a route
Your numbers
Physics only: your ride, your declared shelter, no race tactics.
Not set: the model assumes male physiology (W′ scaling, W/kg tiers) until you pick.
Auto: picked from the route when you plan (TT course → TT pacing, everything else → sportive).
Proof from pro races
Remco's public FTP, body mass 63 kg + his 9.5 kg TT rig, height, and his measured TT aero (the one demo family that keeps a hand CdA). Archived rainy race-day weather (Paris hour-16 hit 2.9 mm/h).
Result on PCS →Tadej's public FTP, body mass 66 kg + 8 kg WT bike, height. No hand CdA: it resolves body-aware from height and mass. Effort 1.0, going for the win in the bunch. Wet flag picked up automatically from archived weather.
Result on PCS →MvdP's race-day FTP, body mass 75 kg + 8 kg classics bike, auto-resolved CdA (drops, body-aware). The server reads the 19% pavé from the GPX surface automatically. Get the group right and it lands inside 1%.
Result on PCS →PFP's race-day FTP per chronoswatts, body mass 50 kg + 8 kg WT bike, height 1.65 m. No hand CdA: hand-set road CdA is overfitting, so it resolves body-aware like every other road demo. The small fast bias is her holding back of full ceiling late for tactical reasons.
Result on PCS →Proof from amateur races
A real amateur circuit race, 292 W for 58 minutes. Body mass 89.4 kg from the FIT's own user_profile at ride time, + 8 kg bike. No hand CdA or Crr: both resolve from the rider and the surface. I wasn't glued to the bunch, so the honest draft is 'mass event'. The draft is an input you set, not a guess.
Ride on Strava →The model tracks the pacing curve across the course, not just the finish: intermediate checkpoints land within tens of seconds of Remco's real splits. The finish time is a ceiling for the effort you set, not a promise of what you will do. Details on Methodology.
How accurate is this?
Mean finish-time error over the whole ride. On flat, gravel and cobbled roads the per-split watts land within a few watts of what the rider actually did: flat sits at +3 W for pros, +8 W for amateurs.
You give it the effort, the group you expect to ride in, and your CdA. It doesn't read your tactics from a cohort and it doesn't guess. Give it the full set and it lands at 3 to 5%. The rides where it's looser are the ones where a rider sat in a draft the model was never told about: that draft is yours to supply, and the number tightens when you do. Full breakdown by tier, goal and terrain →
Custom-course uploads are end-to-end encrypted. The AES key lives in the URL fragment (the part after #), which browsers don't send to the server. Only someone with the full link can decrypt the route. The server only ever sees ciphertext. Privacy →
Run it locally
Open source, MIT. Repo flips public with the v1.0 SemVer commit (PyPI publish lands the same day). Until then the link 404s; the install path will work the moment the repo flips.
git clone https://github.com/sam-dumont/bike-power-model-lib cd bike-power-model-lib uv sync --extra api --extra analysis uv run bpm --help
Full install + license + integration notes on Developers.
Why this exists
One thing led to another. The original goal was a single Paris-Roubaix Challenge: prep a plan, write it onto the Edge as a Garmin Power Guide, ride. Turned out the two FIT messages behind Power Guide (352 and 353) weren't documented publicly, so they had to be reverse-engineered from binary dumps before anything else.
Once the format was writable, what to write was the next problem. Garmin's native plan only sees gradient: not Arenberg cobble, not a 33 km/h Zeeland headwind, not the mud-slick descent out of La Redoute. So the next step was a Martin-1998 power balance to drive better targets. Then per-surface Crr because cobble isn't tarmac, asymmetric wind because real weather isn't uniform, declared shelter because solo, paceline, and bunch aren't the same aero problem. Heat / altitude / fatigue / sector-aware rain decay piled on after. A long thread of "OK the prediction is wrong on this stage, why?" → fix → next stage. Methodology →