Skip to content

Latest commit

 

History

History
275 lines (193 loc) · 8.56 KB

strategy-engine.md

File metadata and controls

275 lines (193 loc) · 8.56 KB

Strategy engine

How the platform generates, costs and ranks tyre strategies — and why the optimum it reports is exact rather than the best of a sample.

Implementation: backend/app/strategy/engine.py.


1. The structural insight

In the deterministic lap-time model, total race time decomposes as:

T = Σ_laps [ base_pace + fuel_effect·fuel(lap) − evolution(lap) ]     (A)
  + Σ_stints Σ_age tyre_delta(compound, age)                          (B)
  + n_stops · pit_loss                                                (C)

Two observations make the whole problem tractable:

  1. Term (A) depends only on lap number. Every strategy runs the same number of laps, so (A) is identical for every strategy and cancels out of any comparison.

  2. Term (B) depends only on compound and age within the stint — never on which lap of the race it happens to be.

Therefore the strategy problem separates exactly:

minimise   J = Σ_i stint_cost(compound_i, length_i) + n_stops · pit_loss

subject to Σ_i length_i = race_laps
           min_stint ≤ length_i ≤ max_stint

stint_cost is precomputable as a cumulative lookup table, and the constrained partition has an exact solution.

This is why the engine reports relative_cost_s (terms B + C) as the comparison figure and expected_race_time_s (A + B + C) as the headline: the first is what actually distinguishes strategies, the second is what a user wants to read.


2. Exact stint-length optimisation

For a fixed compound sequence, the optimal split is solved by dynamic programming — a min-plus convolution over stint lengths.

f[i][n] = minimum tyre cost of covering the first n laps with the first i stints

f[0][0] = 0
f[i][n] = min over L ∈ [min_stint, max_stint] of
          f[i−1][n−L] + stint_cost(compound_i, L)

Complexity O(k · N · L). With k ≤ 3 stints, N ≤ 78 laps and L ≤ 45 that is a few thousand operations — exact and instant. The split is recovered by backtracking through the choice table.

Why this matters

The alternative — sampling candidate stop laps and picking the best — gives an answer that looks the same but carries no guarantee. The DP returns the true optimum of the deterministic model.

test_dynamic_program_finds_the_true_optimum verifies this by brute-forcing every legal two-stint split and asserting the DP matches to 1e-9.


3. Generation

Compound sequences are enumerated over the allowed set for each permitted stop count. Three filters:

  • Two-compound rule — mirrors the regulation that a dry race must use two specifications.
  • No consecutive identical compounds — legal but never optimal: the same split on one set is always cheaper by a pit stop. Skipped to keep the ranked list readable.
  • Starting compound — optionally pinned.

Each surviving sequence gets its exact split solved, then the list is sorted by relative_cost_s.

A typical Silverstone request with 1–3 stops produces 42 feasible strategies in about 50 ms.


4. Pit loss

pit_loss = circuit_pit_lane_transit + team_pit_crew_mean

The circuit contributes the time lost travelling the pit lane at the limit versus staying on track; the team contributes its mean stationary time. Behind the safety car the transit component falls to 52%, because the field is running slowly — which is the whole reason a safety car is a strategic event.


5. Risk

A 0–1 index with three contributions, each with a stated reason so the UI can explain the number rather than just display it.

Cliff exposure

max over stints of (length − cliff_onset) / cliff_onset

How far each stint runs past the compound's cliff onset — where the model is least trustworthy and a real tyre is most likely to fall away. Weighted 0.55.

Stop count

min(1, n_stops / 3) · 0.35

Every stop is an opportunity for a slow stop and an exposure to losing track position.

Safety car exposure

sc_probability · (0.45 if n_stops ≤ 1 else 0.20)

A strategy with few stops loses more when a safety car hands cheap stops to everyone else.

Each contribution that fires also appends a human-readable note, e.g.:

One-stop with a 62% safety car rate: a mid-race deployment would give rivals a cheap stop this plan cannot answer.

Best and worst case

Pit stop variance and lap-time consistency are independent, so they combine in quadrature:

σ_total = √( n_stops·σ_pit² + race_laps·σ_lap² )

best_case  = expected − 1.96·σ_total
worst_case = expected + 1.96·σ_total + risk·12

6. What the deterministic optimum ignores

This is the important caveat, and the reason the Monte Carlo engine exists.

The decomposition in §1 holds only for the deterministic model. Traffic, safety cars, pit stop variance and reliability all break it, because they make lap time depend on race state rather than only on lap number and tyre age.

Specifically, the deterministic optimum ignores:

  • Track position. A strategy that is theoretically faster but emerges into traffic may lose in practice. The optimiser cannot see this at all.
  • The undercut and overcut. These are interactions between two cars' strategies; they emerge only in the stochastic simulation.
  • Safety car timing. The risk index accounts for exposure, but not for what actually happens.

The observable consequence

At Monaco, the deterministic optimiser prefers a two- or three-stop plan, because degradation is low and pit loss is only 17.2 s. In reality Monaco is a one-stop race, because overtaking difficulty is ~96% and track position dominates everything.

That divergence is not a bug — it is the deterministic model correctly answering the question it was asked, and it is exactly why the workflow is:

rank exactly, then re-rank under uncertainty.

The Monte Carlo engine sees traffic, dirty air and overtaking difficulty, and its ranking reflects them.


7. What-if analysis

Each scenario rebuilds the model with its perturbations applied, re-optimises from scratch, and re-simulates an ensemble. Comparing against the baseline shows why the optimal plan moves, not just that it did.

Available perturbations:

Parameter Effect
tyre_degradation_scale Multiplies wear and cliff rates
pit_loss_delta_s Shifts pit loss
safety_car_probability_delta Shifts the deployment rate
track_temp_delta_c Moves the tyre away from its optimal window
driver_pace_delta_s Shifts base lap time

Isolation

Degradation scaling mutates the model's tyre cache, never the global compound table. The compound parameters are module-level singletons, so a what-if scenario that modified them would leak into every subsequent request. The scaling wraps RaceModel.tyre_model for that model instance only.

Rival strategies

Every driver runs their own optimal plan under each scenario, not a frozen default. Comparing the focus driver against opponents who are also racing sensibly is the only comparison that means anything.

The UI flags when a scenario changes the optimal plan:

Safety car +15% → plan changes from S → M to S → M → S, podium probability +7.2%


8. Tyre projection

A separate endpoint (POST /api/strategies/tyre-projection) that projects a tyre forward and returns every term of the degradation law separately so the projection can be audited:

{
  "current_grip_fraction": 0.9641,
  "projected_grip_fraction": 0.9518,
  "degradation_rate_s_per_lap": 0.0662,
  "additional_loss_s": 0.42,
  "usable_life_laps": 53.2,
  "condition": "WORKING",
  "confidence": 0.92,
  "breakdown": {
    "warmup_deficit": 0.00000,
    "linear_wear": 0.02718,
    "cliff": 0.00000,
    "temperature": 0.00089,
    "compound_offset": 0.01350,
    "severity_multiplier": 1.1876,
    "effective_wear_rate_per_lap": 0.001510,
    "grip_sensitivity_s_per_grip": 33.798
  }
}

The race view displays this breakdown inline. Nothing about the projection is hidden.


9. Tests

backend/tests/test_simulation.py covers:

  • Stint cost tables are cumulative and strictly increasing
  • Generated strategies cover the full race distance exactly
  • Ranking is by cost
  • The two-compound rule and stint bounds are enforced
  • The DP matches an exhaustive search (the exactness claim)
  • More stops reduce tyre cost but add pit cost — the core trade
  • Higher degradation does not reduce the optimal stop count
  • Infeasible constraints return an empty list rather than crashing
  • Pinned stop laps are honoured
  • Risk index is bounded, and best/worst case bracket the expected time