UCLA Math 142 group project for 2022 MCM Problem A (power profile of a cyclist): given a rider's sustainable power and fatigue limits, find how to distribute power over a time-trial course to minimize finishing time.
| Path | Contents |
|---|---|
notebooks/model_v5.ipynb |
The model: time-stepping simulation + power-threshold fatigue, with scipy-optimized pacing. |
data/courses/ |
Course files (CSV, one row per segment) — the course parameters. |
data/real_results.csv |
2021 ITT finishing times used as validation targets. |
data/rider_profiles.csv |
Rider parameter sets (one row per profile) |
references/data-sources.md |
All sourced values and citations — course geometry, race results, physical/physiological parameters. |
results/ |
Optimization sweep outputs — finishing-time table, per-segment power, and comparison plots. |
docs/assignment/ |
Official assignment instructions. |
docs/proof-of-work/ |
Handwritten derivations, scratch work, meeting notes. |
archive/ |
Earlier prototypes (v1, v2), the retired CP/W′ model, and the archived LaTeX scaffold. |
notebooks/model_v5.ipynb. The rider and bike are treated as one point mass, pushed forward by the rider against air, road, and gravity, and stepped forward in time.
On a segment of slope angle
where
Target power rises with gradient (harder uphill, easier down) and is capped by a fatigue-reduced ceiling:
Fatigue grows when output exceeds a threshold and recovers below it (clamped to
Each segment also adds its turn_penalty_s to the clock for cornering. Symbols map to code as a_density, CdA, Crr, hill_factor, P_threshold, fatigue_impact, fatigue_rate, recovery_rate. Parameter values and their justification are in the team write-up.
Pacing is set by two knobs — hill_factor (extra power per unit gradient) and flat_boost (power held above P_base on flats and descents) — and scipy.optimize.minimize searches them for the time-minimizing strategy for each rider on each course. Tokyo is one lap; set laps = 2 for the men's race.
archive/cp_w_prime_model/ holds an earlier critical-power (CP/W′) model — a different fatigue framework, kept as the alternative modeling choice discussed in the paper.
A course is an ordered list of segments in a CSV under data/courses/. Columns:
| Column | Meaning |
|---|---|
segment_id |
Order index. |
name |
Segment label. |
distance_m |
Segment length, metres. |
grade_pct |
Average gradient, percent (+ uphill / − downhill). |
turn_penalty_s |
Time added for braking/cornering, seconds. |
wind_exposure |
low / medium / high, mapped to an effective headwind in the model. |
| File | Course | Distance | Climbing | Gradient source |
|---|---|---|---|---|
custom_5km_loop.csv |
Self-designed technical loop (4 sharp turns) | 5.0 km | one 6% ramp | self-designed |
tokyo_olympic_tt.csv |
Tokyo 2021 Olympic ITT (1 lap; men ride 2) | 22.1 km | ~310 m | 26-seg reconstruction, scaled to 22.1 km |
flanders_world_tt.csv |
Flanders 2021 Worlds ITT (men) | 43.3 km | ~78 m (flat) | UCI technical guide |
Distance and gradient are sourced; turn penalty and wind label are modeling assignments. Per-field provenance is in data/courses/README.md and references/data-sources.md.
Riders load the same way as courses — one row per profile in data/rider_profiles.csv, selected by rider = "..." in the notebook's Rider Parameters cell. baseline reproduces the originally submitted rider and is kept only as a calibration reference (the results comparison uses the four typed riders); the typed profiles are derived from the sourced ranges (power from W/kg figures × an assumed body mass, CdA by rider type; female values scaled and flagged — see data/README.md).
| profile_id | gender | type | mass (kg) | CdA (m²) | P_base (W) | P_threshold (W) | P_max (W) |
|---|---|---|---|---|---|---|---|
baseline |
— | generic | 75 | 0.26 | 250 | 300 | 400 |
male_tt |
M | time trial | 80 | 0.21 | 370 | 400 | 600 |
female_tt |
F | time trial | 70 | 0.20 | 260 | 280 | 420 |
male_climber |
M | climber | 70 | 0.25 | 325 | 350 | 525 |
female_climber |
F | climber | 62 | 0.23 | 230 | 250 | 375 |
Crr, v_max, and the fatigue tuning constants are shared across riders and stay in the notebook, not the profile.
The model is checked against the actual 2021 finishing times (machine-readable in data/real_results.csv, top three per race):
| Course | Distance | Winner | Time |
|---|---|---|---|
| Tokyo 2021 Olympic ITT (men) | 44.2 km | Primož Roglič | 55:04 |
| Tokyo 2021 Olympic ITT (women) | 22.1 km | Annemiek van Vleuten | 30:13 |
| Flanders 2021 Worlds ITT (men) | 43.3 km | Filippo Ganna | 47:48 |
| Flanders 2021 Worlds ITT (women) | 30.3 km | Ellen van Dijk | 36:05 |
Real ITT winners are time-trial specialists or rouleurs, so these times validate the model's TT-specialist profile. Real races don't produce a separate time per rider type (only per rider), so the climber profile is a model exploration, not a measured comparison — the split here is by gender and course, which is all the real data provides.
Sources: Wikipedia — Tokyo men's / women's, Flanders men's / women's. Full top-three times and citations in references/data-sources.md.
2022 MCM Problem A. Full statement: references/2022_PowerOfCyclist.pdf.
Riders — profiles built (data/rider_profiles.csv); comparison results in results/, paper write-up pending
- Two rider types — time-trial specialist and climber
- Both genders (male + female of each type)
Courses — course files parameterized; model results in results/
- Tokyo 2021 Olympic ITT —
data/courses/tokyo_olympic_tt.csv - Flanders 2021 Worlds ITT —
data/courses/flanders_world_tt.csv - Self-designed course (≥4 sharp turns, ≥1 grade, finish near start) —
data/courses/custom_5km_loop.csv
Analysis — complete; outputs and a re-runnable audit (results/verify_results.py) in results/
- Power distribution vs. position that minimizes time —
scipyoptimizeshill_factor+flat_boostper rider/course; per-segment profile inresults/power_by_segment.csv, plots inresults/power_*.png - Weather sensitivity — wind strength (uniform head/tail offset),
results/wind_sensitivity.*. Wind direction is not modeled (the model uses a scalar headwind) — a stated limitation. - Power-deviation sensitivity (±5/10/15% off the planned power → finishing-time spread) —
results/power_deviation.*
Extension
- Team time trial of six riders (team time set by the fourth finisher) — discussion only (in the write-up)
Write-up
- M142 structure (
docs/paper-outline.md), not the contest's 25-page format - (Optional, contest-only) two-page Directeur Sportif race guidance — likely not required for M142; confirm with the professor
The model (notebooks/model_v5.ipynb) and the three course files exist. The write-up is a shared document following docs/paper-outline.md; the old LaTeX scaffold has been archived under archive/latex_scaffold/. With the elite profiles and optimized pacing the model finishes within ~8-12% of the real winners (results/validation.*: male_tt 2-lap Tokyo 59.2 min vs Roglič 55:04 = +7.5%, female_tt Tokyo +11.6%, male_tt Flanders +11.9%) — the residual gap is parameter calibration, not structure.
git clone https://github.com/wochaotom/Cyclist-Emulator.git
cd Cyclist-Emulator
pip install numpy matplotlib jupyter
jupyter notebook notebooks/model_v5.ipynb
Then Run All. Two one-line switches choose what gets simulated:
- Rider — in the Rider Parameters cell:
rider = "baseline"(ormale_tt,female_tt,male_climber,female_climber). - Course — in the Course Inputs cell:
course_file = "tokyo_olympic_tt.csv"(orflanders_world_tt.csv,custom_5km_loop.csv).
Each run prints total time, distance, peak speed, and final fatigue, and plots speed / distance / drag / gradient / fatigue. Step through the rider × course combinations to build the comparison.
| Name | GitHub | Area |
|---|---|---|
| Carter Dandridge | @wochaotom | Course parameters |
| Ben Miller | @benmiller74 | Rider model & parameters |
| Dennis Lee | TBD | TBD |
| Prannay Veerabahu | @pveeraba28 | TBD |
Workflow: CONTRIBUTING.md. First-time git setup: START-HERE.md.