Developers

Install, license, integrations, embedding. The Python library + CLI side. Methodology lives in /methodology; usage docs in /help.

Is it on PyPI? How do I install the library?

Not yet. Pre-1.0 the API still moves enough that a PyPI wheel would mislead about stability. Install from source:

git clone https://github.com/sam-dumont/bike-power-model
cd bike-power-model
uv sync --extra api --extra analysis
uv run bpm --help

uv is the supported path. Bare pip install -e . works but isn't covered by CI. PyPI publish lands with the 1.0 SemVer commit.

What's the license? Can I ship it in a paid product?

MIT. Fork it, modify it, ship it in a closed-source product, sublicense it. Attribution required (standard MIT notice in your dist), no copyleft, no royalty.

Per-sector surface Crr database, OTS-stamped predictions, validation cohort: same repo, same license. A heads-up at info@bikepowermodel.fit is appreciated if you ship commercially, but not required.

Does it integrate with TrainingPeaks / Wahoo / intervals.icu?

Partial.

  • intervals.icu: wired. The 90-day power curve fit feeds the per-rider PDC used as effort ceiling. Auth lives in ~/.config/bike-power-model/.env; see the README integration section.
  • TrainingPeaks / Wahoo: not yet. Today's workflow is GPX in → Garmin Power Guide FIT out. The FIT you download from the planner sideloads onto an Edge via Garmin Express; nothing pushes to a coach platform.
  • CSV export: the planner has a CSV download for the per-split plan if you want to pull it into your own pipeline.
What's the SemVer / breaking-change policy?

Pre-1.0, so technically anything-goes. In practice:

  • The public surface (plan_race, plan_ride, RiderProfile, Course, the 11 for_* constructors) is treated as stable. Breaking changes here get a CHANGELOG entry quantifying the impact.
  • Internal helpers (modifiers, splitters, calibration tables) move freely without notice.
  • Recalibrations can shift predicted times by 2-5 % even with identical inputs. That's a customer-visible drift with no input change. Treated as a minor bump, not a patch.

1.0 commits to strict SemVer with a deprecation cycle. Until then, embedders should pin to a specific commit, not a moving tag.

Can I run bulk predictions for a squad?

The web UI is one-rider, one-route at a time. The Python library handles bulk:

from bike_power_model import RiderProfile, plan_race

for athlete in roster:
    rider = RiderProfile.for_race_day(
        ftp=athlete.ftp,
        rider_mass_kg=athlete.weight_kg,
        bike_mass_kg=8,
        cda=athlete.cda or 0.30,
        height_m=athlete.height_m,
        gender=athlete.gender,
    )
    rider.effort = 1.0
    plan = plan_race(course_path, rider,
                     race_date="2026-08-15",
                     race_start_hour_local=9)
    print(athlete.name, plan.result.total_time_s)

A coach with Python familiarity can wire athlete CSVs to per-rider FIT files in an afternoon. CLI wrappers + a coach-surface UI are planned but not shipped.

What dependencies does the library pull in?

Lean. Core: fitparse, numpy, requests, pmtiles (OSM surface tiles), scipy (cKDTree for spatial sector lookup), click (CLI), and the Garmin fit-tool + garmin-fit-sdk for FIT writing.

No PyTorch, no scikit-learn at runtime (a Lasso baseline is a one-off benchmark, not a runtime dep). The API extras pull in fastapi, uvicorn, boto3, altcha, email-validator: only needed if you're running the SaaS, not the library.

Versions exact-pinned (supply-chain policy); full set in pyproject.toml + uv.lock.

What's on the CLI?

bpm plan generates a Power Guide FIT from a GPX + a rider profile. bpm calibrate back- solves CdA from a flat ride of yours. bpm power-guide reads the messages 352/353 out of an existing Garmin Power Guide FIT. bpm --help for the full list.

All commands take a --ftp, --weight, --cda, --effort, --intent flag set; the preset chips on the SPA are just shorthand for these.

bikepowermodel.fit · open-source race-time prediction · per-split surface Crr · Garmin / Wahoo / Hammerhead / Bryton