Target Time Mode Documentation
Overview
RaceCraft's Target Time Mode uses a neutral route cost algorithm that calculates segment pacing purely from target time, distance, elevation, and terrain difficulty. This mode guarantees the total moving time exactly equals the target time by solving the inverse problem: "Given time T, what pace must each segment have so the weighted sum equals T?"
This approach eliminates circular dependencies, produces deterministic results, and is impossible for users to "game" by adjusting athlete-specific parameters.
Core Principles
- Target Time is a Hard Constraint: The total calculated time MUST always equal the target time exactly
- Athlete-Independent: Base pace, fitness level, fatigue, and technical ability have ZERO effect on calculations
- Route-Relative: Pacing is determined by the route characteristics (terrain, elevation) relative to the target time
- Neutral Cost Foundation: Terrain difficulty establishes a neutral cost before elevation effects are applied
- Realistic Constraints: Pace limits (2:50-15:00 min/km) ensure achievable speeds while maintaining target time
The Algorithm: 5 Steps
Step 0: Build Neutral Route Cost
Purpose: Calculate terrain-weighted distance without any elevation effects
Formula:
neutral_cost = Σ(distance_km × terrain_factor)
What It Ignores: - Slope direction (uphill vs downhill) - Elevation gain penalties - Descent bonuses - All vertical movement
What It Includes: - Horizontal distance - Terrain difficulty multipliers
Terrain Factors:
| Terrain Type | Factor | Description |
|---|---|---|
| Road | 0.90 | 10% faster than baseline |
| Smooth Trail | 1.0 | Baseline reference |
| Dirt Road | 1.10 | 10% slower |
| Rocky Runnable | 1.35 | 35% slower |
| Technical | 1.75 | 75% slower |
| Very Technical | 2.25 | 125% slower |
| Scrambling | 3.0 | 200% slower |
Why This Works: - Answers the question: "If this route were run at uniform difficulty, what flat pace would produce the target time?" - A flat road race and a technical mountain race with the same target time will produce different internal neutral paces - Provides a deterministic anchor for all subsequent calculations
Example:
Route: 30 km total
- 10 km smooth trail → 10 × 1.0 = 10.0
- 10 km technical → 10 × 1.75 = 17.5
- 10 km road → 10 × 0.90 = 9.0
neutral_cost = 10.0 + 17.5 + 9.0 = 36.5 km-equivalent
Step 1: Compute Neutral Reference Pace
Purpose: Derive the internal pacing anchor from target time and neutral cost
Formula:
neutral_reference_pace = target_time_minutes / neutral_cost
Properties: - Internal only: Never shown in UI - Route-relative: Changes automatically with route + target time - Non-athlete-specific: No VO₂max, fitness, skill, or training assumptions - Deterministic: Same inputs always produce same result - Impossible to game: Users cannot manipulate this value
Example (continuing from Step 0):
target_time = 240 minutes (4 hours)
neutral_cost = 36.5 km-equivalent
neutral_reference_pace = 240 / 36.5 = 6.58 min/km
Also Calculated (for UI display only):
simple_flat_pace = target_time / total_distance
simple_flat_pace = 240 / 30 = 8.00 min/km
The simple_flat_pace is shown to users as "Target Average Moving Pace (distance-only)" for reference, but neutral_reference_pace is used internally for calculations.
Step 2: Apply Elevation Modifiers
Purpose: Layer elevation effects on top of the neutral cost foundation
Base Weight:
base_weight = distance_km × terrain_factor
This is the same terrain-weighted distance from Step 0.
Elevation Factor Calculation:
elev_factor = 1.0 # Start neutral
# Climbing penalty (if elevation gain > 0)
if elev_gain > 0:
vertical_speed = 600 m/h = 0.01 km/min
climb_time_penalty = (elev_gain / 1000.0) / vertical_speed
base_segment_time = base_weight × neutral_reference_pace
elev_factor += climb_time_penalty / base_segment_time
# Descent bonus (if elevation loss > 0)
if elev_loss > 0:
gradient = (elev_gain - elev_loss) / (distance_km × 1000)
descent_bonus = min(0.20, abs(gradient) × 2.0) # Max 20% speedup
elev_factor -= descent_bonus
Elevation-Adaptive Terrain Adjustment:
Technical terrain impacts pace differently based on elevation profile:
Descents (gradient < -3%):
terrain_amplification = 1.3 # 30% more impact
adjusted_terrain_factor = 1.0 + (base_terrain_factor - 1.0) × 1.3
- Technical terrain forces significant slowdown for safety and footing
- Example: Technical descent is 11% slower, Scrambling is 29% slower
Climbs (gradient > 3%):
terrain_reduction = 0.6 # 40% less impact
adjusted_terrain_factor = 1.0 + (base_terrain_factor - 1.0) × 0.6
- Already slow on climbs, technical terrain adds less penalty
- Example: Technical climb is 3.7% slower, Scrambling is 5.6% slower
Flat (-3% to +3%):
adjusted_terrain_factor = base_terrain_factor # No adjustment
- Moderate terrain impact
- Example: Technical flat is 20% slower, Scrambling is 54% slower
Final Weight:
segment_weight = base_weight × elev_factor
Example:
Segment: 10 km technical trail, 500m climb, gradient = 5%
Base weight = 10 × 1.75 = 17.5 km-equivalent
Terrain adjustment (climb):
adjusted_terrain = 1.0 + (1.75 - 1.0) × 0.6 = 1.45
base_weight = 10 × 1.45 = 14.5 km-equivalent
Elevation factor:
climb_time = 0.5 km / 0.01 km/min = 50 min
base_time = 14.5 × 6.58 = 95.4 min
elev_factor = 1.0 + 50 / 95.4 = 1.52
Final weight = 14.5 × 1.52 = 22.04 km-equivalent
Step 3: Distribute Time Proportionally
Purpose: Allocate target time across segments based on their weights
Formula:
total_weight = Σ(segment_weights)
for each segment:
segment_time = target_time × (segment_weight / total_weight)
segment_pace = segment_time / segment_distance
Property: At this stage, before pace limits are applied:
Σ(segment_times) = target_time (exactly)
Example (3 segments with weights 22.0, 17.8, 10.5):
total_weight = 22.0 + 17.8 + 10.5 = 50.3 km-equivalent
target_time = 240 minutes
Segment 1: 240 × (22.0 / 50.3) = 104.97 min
Segment 2: 240 × (17.8 / 50.3) = 84.89 min
Segment 3: 240 × (10.5 / 50.3) = 50.14 min
Total: 104.97 + 84.89 + 50.14 = 240.00 min ✓
Step 4: Apply Pace Limits with Redistribution
Purpose: Enforce realistic pace constraints while preserving target time
Pace Limits: - MIN_PACE = 2.85 min/km (sub-2hr marathon pace, ~2:51 min/km) - MAX_PACE = 15.0 min/km (average walking pace)
Iterative Clamping Algorithm:
MAX_ITERATIONS = 10
for iteration in range(MAX_ITERATIONS):
clamped_segments = []
excess_time = 0.0 # From too-fast segments
deficit_time = 0.0 # For too-slow segments
adjustable_weight = 0.0
for each segment:
pace = segment_time / segment_distance
if pace < MIN_PACE:
# Too fast - clamp and free up time
clamped_time = segment_distance × MIN_PACE
excess_time += segment_time - clamped_time
segment_time = clamped_time
mark as clamped
elif pace > MAX_PACE:
# Too slow - clamp and need more time
clamped_time = segment_distance × MAX_PACE
deficit_time += clamped_time - segment_time
segment_time = clamped_time
mark as clamped
else:
# Within limits - can be adjusted
adjustable_weight += segment_weight
# Redistribute net excess/deficit
net_excess = excess_time - deficit_time
if adjustable_weight > 0 and abs(net_excess) > 0.01:
for each unclamped segment:
adjustment = net_excess × (segment_weight / adjustable_weight)
segment_time += adjustment
else:
break # Converged
Final Adjustment:
actual_total = Σ(segment_times)
time_diff = target_time - actual_total
# Add difference to last segment to guarantee exact match
segment_times[-1] += time_diff
Properties: - All segment paces remain within [MIN_PACE, MAX_PACE] - Total time still equals target exactly - Time redistributed proportionally among unclamped segments - Converges in typically 2-5 iterations
Example:
Initial (before limits):
Seg 1: 1.5 min/km (TOO FAST) → Clamp to 2.85 min/km, free up time
Seg 2: 18.0 min/km (TOO SLOW) → Clamp to 15.0 min/km, need more time
Seg 3: 5.0 min/km (OK) → Absorb excess time from Seg 1
After redistribution:
Seg 1: 2.85 min/km ✓
Seg 2: 15.00 min/km ✓
Seg 3: 6.20 min/km ✓ (adjusted to maintain target time)
Step 5: Assign Effort Labels
Purpose: Communicate segment difficulty independent of pace
Calculation:
# Gradient difficulty score (0-3)
gradient = (elev_gain - elev_loss) / (distance_km × 1000)
if gradient > 0.12: gradient_score = 3
elif gradient > 0.08: gradient_score = 2
elif gradient > 0.04: gradient_score = 1
else: gradient_score = 0
# Terrain difficulty score (0-5)
terrain_scores = {
'road': 1,
'smooth_trail': 1,
'dirt_road': 2,
'rocky_runnable': 3,
'technical': 4,
'very_technical': 5,
'scrambling': 6
}
terrain_score = terrain_scores[terrain_type]
# Combined difficulty
difficulty_score = gradient_score + (terrain_score - 1)
# Map to effort labels
if difficulty_score <= 0: effort = 'easy'
elif difficulty_score <= 2: effort = 'medium'
elif difficulty_score <= 4: effort = 'hard'
else: effort = 'very_hard'
Effort Labels & Colors:
| Label | Color | Typical Conditions |
|---|---|---|
| Easy | Green | Flat/gentle road or smooth trail |
| Medium | Blue | Moderate climb or technical terrain |
| Hard | Orange | Steep climb or very technical |
| Very Hard | Red | Very steep + technical, or scrambling |
Key Principle: Effort labels communicate difficulty of the terrain, not speed requirements. A slow pace on very steep/technical terrain is labeled "Very Hard" even though the pace itself is slow.
Mental Model
Old Approach (Base Pace Mode)
"Runner can run X min/km, therefore time is Y"
- Start with athlete abilities
- Apply modifiers (fatigue, terrain, elevation)
- Calculate resulting time
- Problem: Circular dependencies, unpredictable results
New Approach (Target Time Mode)
"Given time T, what pace must each segment have so the weighted sum equals T?"
- Start with target time (hard constraint)
- Calculate neutral route cost (terrain only)
- Derive neutral reference pace (anchor)
- Layer elevation effects
- Distribute time to meet target
- Apply realistic limits
- Advantage: Deterministic, no circular logic, impossible to game
Comparison: Base Pace vs Target Time Mode
| Aspect | Base Pace Mode | Target Time Mode |
|---|---|---|
| Input | Base pace, fitness, fatigue | Target time only |
| Output | Estimated finish time | Guaranteed target time |
| Approach | Forward calculation | Inverse calculation |
| Athlete Settings | Required and influential | Disabled (no effect) |
| Predictability | Varies with abilities | Always meets target |
| Use Case | "How fast can I finish?" | "Can I finish in X hours?" |
| Effort Labels | Push/Steady/Protect | Easy/Medium/Hard/Very Hard |
| Pace Variation | From athlete + fatigue + terrain | From terrain + elevation only |
UI Display
Disabled Inputs
When Target Time mode is active, the following inputs are greyed out and excluded from calculations: - Base pace - Climbing ability - Fitness level - Fatigue penalty checkbox - Technical skill
Effort Guidance Panel
Replaces the old "Effort Thresholds" display with:
⚡ Estimated GAP (Grade Adjusted Pace): Shows the route-level flat-terrain equivalent pace - Derived from the final modelled segment pacing - Mode-agnostic: if Base Pace and Target Time modes produce the same total moving time, they show the same GAP - Useful for comparing against flat-road pace references (e.g., Strava)
Model Anchor Pace (terrain-adjusted): Shows neutral_reference_pace = target_time / neutral_cost
- Informational representation of the internal solver anchor
- Route-relative and terrain-adjusted
- Used internally before elevation modifiers are applied
- Always displayed; when it matches the distance-only average on a simple/neutral route, a note is shown beneath the pace value instead of a duplicate row
Effort Label Legend: - Easy (Green): Flat/gentle terrain - Medium (Blue): Moderate difficulty - Hard (Orange): Steep or technical - Very Hard (Red): Very steep + technical
Course Difficulty Column
New table column visible only in Target Time mode: - Shows effort label badge for each segment - Separate from pace (which shows speed) - Communicates terrain/elevation challenge independent of pace
Pace Color Scale
Final Pace values use red-blue-green gradient: - Green (#16a34a): Fast pace (<85% of flat pace baseline) - Blue (#2563eb): Normal pace (85-115% of flat pace) - Red (#dc2626): Slow pace (>115% of flat pace)
Colors provide visual indication of relative speed variation across terrain.
Examples
Example 1: Mixed Terrain Route
Route: 30 km total - 10 km smooth trail, 500m gain - 10 km technical trail, 500m loss - 10 km flat road
Target Time: 240 minutes (4:00:00)
Step 0: Neutral Cost
Segment 1: 10 × 1.0 = 10.0
Segment 2: 10 × 1.75 × 1.3 (descent amplification) = 22.75
Segment 3: 10 × 0.9 = 9.0
Total neutral cost = 41.75 km-equivalent
Step 1: Neutral Reference Pace
neutral_reference_pace = 240 / 41.75 = 5.75 min/km
simple_flat_pace = 240 / 30 = 8.00 min/km (UI display)
Step 2-3: Apply Elevation & Distribute
Segment 1 (climb): weight ≈ 17.6 → time = 101.2 min, pace = 10.12 min/km
Segment 2 (tech descent): weight ≈ 18.8 → time = 108.0 min, pace = 10.80 min/km
Segment 3 (flat road): weight ≈ 9.0 → time = 30.8 min, pace = 3.08 min/km
Step 4: Apply Limits
Segment 1: 10.12 min/km ✓ (within limits)
Segment 2: 10.80 min/km ✓ (within limits)
Segment 3: 3.08 min/km ✓ (within limits, >2.85)
Total: 240.00 min ✓ (exact)
Step 5: Effort Labels
Segment 1: Medium (moderate climb, smooth trail)
Segment 2: Hard (descent + technical terrain)
Segment 3: Easy (flat road)
Example 2: Extreme Elevation
Route: 20 km - 10 km, 1000m gain (very steep) - 10 km, 1000m loss (very steep)
Target Time: 180 minutes (3:00:00)
Without Pace Limits (unrealistic): - Climb: 25 min/km (too slow) - Descent: 1.5 min/km (too fast)
With Pace Limits (realistic): - Climb: 15.00 min/km (clamped to max) - Descent: 2.85 min/km (clamped to min) - Time redistributed to maintain 180 min total
Implementation Notes
Code Location
- Function:
calculate_independent_target_pacing()inapp.py(lines ~1170-1460) - Frontend:
handlePacingModeChange()instatic/js/app.jsfor input state management - Display:
displayResults()instatic/js/app.jsfor effort guidance and course difficulty
Logging
Detailed logging includes: - Neutral route cost calculation - Neutral reference pace - Segment weight calculations - Pace clamping iterations - Final time verification
Testing
Test with: - Routes with varied terrain (road, trail, technical) - Extreme elevation changes (steep climbs, descents) - Very short or very long target times - Verify: total time always equals target, all paces within limits
Why This Approach Works
Eliminates Circular Logic
Old problem: Base pace affects time, time affects fatigue, fatigue affects pace New solution: Target time is fixed, pace is derived to match
Route-Relative, Not Athlete-Relative
Old assumption: "I can run X min/km on flat ground" New assumption: "This route + target time requires Y pace distribution"
Deterministic & Predictable
Same inputs (route + target time) always produce same outputs (segment paces) No hidden variables, no fitness assumptions, no surprises
Impossible to Game
Users cannot manipulate athlete settings to force unrealistic paces The neutral cost anchor is purely mathematical and route-dependent
Respects Physical Reality
Pace limits ensure segments remain achievable Terrain and elevation naturally create variation Steep/technical sections get slower paces, flats get faster paces
Future Enhancements
Potential improvements (not currently implemented):
- User-Adjustable Pace Limits: Allow athletes to set their own min/max based on abilities
- Terrain-Specific Limits: Different pace ranges for different terrain types
- Segment-Level Overrides: Manual pace adjustment for specific segments
- Optimization Mode: Suggest target times that minimize extreme pace variations
- Confidence Intervals: Show uncertainty ranges for segment times
Version History
v1.7.0 (Feb 2026): Refactored to neutral route cost algorithm
- Eliminated circular dependencies
- Introduced neutral_reference_pace as internal anchor
- Clear separation: terrain cost → elevation modifiers → distribution
v1.6.2 (Feb 2026): Added elevation-adaptive terrain difficulty
- Descents amplify terrain impact (1.3x)
- Climbs reduce terrain impact (0.6x)
v1.6.1 (Feb 2026): Increased terrain difficulty factors
- Technical: 1.33 → 1.75
- Very technical: 1.65 → 2.25
- Scrambling: 2.0 → 3.0
v1.6.0 (Feb 2026): Initial Target Time mode implementation
- Independent time solving
- Pace limits (2.85-15.00 min/km)
- Course difficulty column
- Pace color scale
References
- Naismith's Rule: 600m/h vertical speed for climbing (used in elevation penalty)
- Sub-2hr Marathon: ~2:51 min/km (basis for MIN_PACE)
- Average Walking Pace: 15 min/km (basis for MAX_PACE)
- Terrain Factors: Based on typical speed reductions observed in trail running research
Last Updated: February 7, 2026
Maintained by: @lennon101 and GitHub Copilot