About intfnd

intfnd helps cyclists pick the best road climb to ride a target interval on. You drop a pin on the map, set a search radius, and tell the app your system weight, sustainable power, and how long you want the effort to last. The app searches OpenStreetMap-derived climbs in that area and ranks them by how closely the predicted ride time matches your target interval — so a 5-minute VO₂max effort lands on a climb you can actually finish in roughly 5 minutes at your power.

Climb data is imported from OpenStreetMap once per region, elevation data from EU_DTM. Ride times are estimated with a standard three-force cycling power model (gravity, rolling resistance, and aerodynamic drag), solved numerically for steady-state speed.

Each climb also carries a penalty score that grows with traffic signals, road intersections, sharp turns, and uneven gradient — lower is better. The predicted time and this score together determine the order of the search results.

Physics model

The total resistive force on a moving cyclist is the sum of three components: gravity (positive uphill, negative downhill), rolling resistance (always opposing motion), and aerodynamic drag (quadratic in velocity).

$$ F_\text{gravity} = m \cdot g \cdot s $$

Gravity force, where \(s\) is grade as a decimal (e.g. 6 % → 0.06).

$$ F_\text{rolling} = m \cdot g \cdot C_\text{rr} $$

Rolling resistance — independent of speed in this model.

$$ F_\text{aero} = \tfrac{1}{2} \cdot \rho \cdot C_\text{d}A \cdot v^{2} $$

Aerodynamic drag — the dominant cost on flat terrain.

Mechanical power at the wheel equals the total resistive force times velocity. After subtracting drivetrain losses, this gives the steady-state power equation we solve for \(v\):

$$ P \cdot \eta \;=\; \left( m \cdot g \cdot s \;+\; m \cdot g \cdot C_\text{rr} \;+\; \tfrac{1}{2} \cdot \rho \cdot C_\text{d}A \cdot v^{2} \right) \cdot v $$

Rearranged, this is a cubic in \(v\):

$$ \tfrac{1}{2} \cdot \rho \cdot C_\text{d}A \cdot v^{3} \;+\; \left( m \cdot g \cdot s + m \cdot g \cdot C_\text{rr} \right) v \;-\; P \cdot \eta \;=\; 0 $$

The app solves this with Newton's method (up to 64 iterations, stopping when the step falls below \(10^{-9}\)), then converts to ride time:

$$ t = \frac{d}{v} $$

Constants used

\(g\) 9.81 m/s² gravitational acceleration
\(C_\text{rr}\) 0.004 rolling resistance coefficient — road tire on pavement
\(\rho\) 1.225 kg/m³ air density at sea level, 15 °C
\(C_\text{d}A\) 0.32 m² drag coefficient × frontal area — road cyclist on the hoods
\(\eta\) 0.95 drivetrain efficiency — 5 % loss in chain and bearings

These values are reasonable defaults for a fit recreational rider on a road bike. They are not tuned per user — your real ride times will vary with wind, position, tire pressure, surface, and equipment.

Result ranking

Search results are sorted by a combined rank where lower is better. It blends how far the predicted ride time is from your target interval with the climb's penalty score:

$$ \text{rank} \;=\; 0.7 \cdot \left(\tfrac{\Delta t}{T}\right)^{\!2} \;+\; 0.3 \cdot \hat{s} $$

\(\Delta t = t_\text{est} - T\) is the gap between the predicted ride time and your target interval \(T\). Squaring penalises symmetric over- and under-shoots equally and grows fast for poor matches. \(\hat{s}\) is the climb's penalty score normalised against the other climbs in the same search.

$$ \hat{s} \;=\; \frac{s - s_{\min}}{s_{\max} - s_{\min}} $$

\(s_{\min}\) and \(s_{\max}\) are the smallest and largest penalty scores among the climbs returned for this search, so the cleanest match in the area gets \(\hat{s} = 0\) and the worst gets \(\hat{s} = 1\). Because a lower penalty score means a cleaner climb, this pushes better climbs up the list.

A planned distance term (rewarding climbs closer to the pin) is reserved in the formula but currently weighted at zero. Before ranking, climbs are filtered to those whose predicted time is at most 5 % below the target interval and no more than twice it — \(0.95\,T \le t_\text{est} \le 2\,T\).

Climb penalty score

Every climb has a precomputed penalty score where lower is better. A perfectly clean climb scores 0; the score has no upper bound and grows with each thing a rider would dislike on a sustained effort. It is the plain sum of four penalties:

$$ s \;=\; p_\text{signal} \;+\; p_\text{inter} \;+\; p_\text{turn} \;+\; p_\text{spike} $$

Traffic signals — each OSM traffic_signals node on the climb adds a flat penalty, since a red light is a hard stop on a timed effort:

$$ p_\text{signal} \;=\; 50 \cdot n_\text{signals} $$

Intersections — every node on the climb that is a real junction (degree > 2 in the road network) is penalised in proportion to the importance of the largest road crossing there. A primary road crossing counts far more than a track:

$$ p_\text{inter} \;=\; 6 \cdot \sum_{i \in \text{junctions}} r_i $$

\(r_i\) is the highest highway rank among the ways meeting the climb at junction \(i\) that are not themselves part of the climb. Ranks are listed below; any highway class not in the table contributes 0.

primary, primary_link5
secondary, secondary_link4
tertiary, tertiary_link3
unclassified, residential2smaller through roads
living_street, road1
cycleway, track0no penalty

Sharp turns — at those same junctions, any turn of at least 20° adds a penalty proportional to its angle. Gentle bends below the threshold are ignored entirely:

$$ p_\text{turn} \;=\; 0.2 \cdot \sum_{\substack{i \in \text{junctions} \\ |\Delta\theta_i| \ge 20°}} |\Delta\theta_i| $$

\(\Delta\theta_i\) is the turn angle in degrees at junction \(i\). Only turns at junctions count — a smooth curve in the middle of a road does not.

Gradient spikes — the elevation profile is resampled every 10 m and its per-sample grade compared against that same grade smoothed over a 250 m window. Each sample adds however much its local grade deviates from the smoothed trend, beyond a 4 % deadband:

$$ p_\text{spike} \;=\; \sum_i \max\!\left(0,\; \left| g_i - \bar{g}_i \right| - 0.04 \right) $$

\(g_i\) is the raw grade at sample \(i\) and \(\bar{g}_i\) the smoothed grade. The 4 % deadband absorbs DEM noise: elevation comes from EU_DTM samples whose vertical error is comparable to the height change between adjacent samples on a real road, so a single noisy point can look like a ramp that isn't there. Treat this term as a weak hint, not a measurement.

Caveats

The model assumes constant power, no wind, dry pavement, and steady-state speed (no acceleration). Short, punchy efforts and very steep grades where you'd be out of the saddle will deviate the most from the prediction. Treat the estimated time as a ranking signal, not a stopwatch.

Contact

Questions, feedback, or bug reports: dfa.sro591@gmail.com