///|
priv struct SimulatedLap {
tyre_state : SimulatedTyreState
lap_time_ms : Int
cumulative_time_ms : Int
}
///|
fn lap_state_or_error(
analysis : RaceAnalysis,
driver : String,
lap : Int,
) -> Result[DriverLapState, StrategyError] {
match find_lap_state(analysis, driver, lap) {
Some(state) => Ok(state)
None =>
Err(
strategy_error(
MissingLapState,
Some(lap),
"race analysis is missing a required driver lap state",
),
)
}
}
///|
fn stop_for_lap(stops : Array[PlannedPitStop], lap : Int) -> PlannedPitStop? {
for stop in stops {
if stop.pit_lap == lap {
return Some(stop)
}
}
None
}
///|
fn derive_neutral_anchors(
analysis : RaceAnalysis,
config : PaceModelConfig,
target_driver : String,
) -> Result[Array[Int], StrategyError] {
let anchors : Array[Int] = []
for lap = 1; lap <= analysis.lap_count; lap = lap + 1 {
let actual = match lap_state_or_error(analysis, target_driver, lap) {
Ok(state) => state
Err(error) => return Err(error)
}
match
derive_neutral_lap_time_ms(
config,
actual.lap_time_ms,
actual.compound,
actual.tyre_age_laps,
actual.weather,
actual.track_status,
actual.pit,
) {
Ok(anchor) => anchors.push(anchor)
Err(error) =>
return Err(
strategy_error(
NeutralLapFailure,
Some(lap),
"could not derive neutral lap time: " + error.message,
),
)
}
}
Ok(anchors)
}
///|
fn build_simulated_laps(
analysis : RaceAnalysis,
config : PaceModelConfig,
request : StrategyRequest,
anchors : Array[Int],
) -> Result[Array[SimulatedLap], StrategyError] {
let simulated : Array[SimulatedLap] = []
let first_actual = match
lap_state_or_error(analysis, request.target_driver, 1) {
Ok(state) => state
Err(error) => return Err(error)
}
let mut compound = first_actual.compound
let mut tyre_age_laps = first_actual.tyre_age_laps
let mut cumulative_time_ms = 0
for lap = 1; lap <= analysis.lap_count; lap = lap + 1 {
let actual = match
lap_state_or_error(analysis, request.target_driver, lap) {
Ok(state) => state
Err(error) => return Err(error)
}
let planned_stop = stop_for_lap(request.stops, lap)
let pit = planned_stop is Some(_)
let lap_time_ms = match
estimate_lap_time_ms(
config,
anchors[lap - 1],
compound,
tyre_age_laps,
actual.weather,
actual.track_status,
pit,
) {
Ok(time) => time
Err(error) =>
return Err(
strategy_error(
EstimatedLapFailure,
Some(lap),
"could not estimate simulated lap time: " + error.message,
),
)
}
cumulative_time_ms = cumulative_time_ms + lap_time_ms
simulated.push({
tyre_state: { compound, tyre_age_laps, pit, },
lap_time_ms,
cumulative_time_ms,
})
match planned_stop {
Some(stop) => {
compound = stop.next_compound
tyre_age_laps = 1
}
None => tyre_age_laps = tyre_age_laps + 1
}
}
Ok(simulated)
}
///|
fn simulated_position(
analysis : RaceAnalysis,
target_driver : String,
lap : Int,
target_cumulative_time_ms : Int,
) -> Result[Int, StrategyError] {
let ranking : Array[(String, Int)] = []
for driver in analysis.drivers {
if driver == target_driver {
ranking.push((driver, target_cumulative_time_ms))
} else {
let actual = match lap_state_or_error(analysis, driver, lap) {
Ok(state) => state
Err(error) => return Err(error)
}
ranking.push((driver, actual.cumulative_time_ms))
}
}
ranking.sort_by((left, right) => {
if left.1 != right.1 {
left.1 - right.1
} else {
left.0.compare(right.0)
}
})
for index = 0; index < ranking.length(); index = index + 1 {
if ranking[index].0 == target_driver {
return Ok(index + 1)
}
}
Err(
strategy_error(
MissingLapState,
Some(lap),
"target driver is missing from simulated ranking",
),
)
}
///|
fn build_comparisons(
analysis : RaceAnalysis,
request : StrategyRequest,
anchors : Array[Int],
simulated : Array[SimulatedLap],
) -> Result[Array[StrategyLapComparison], StrategyError] {
let comparisons : Array[StrategyLapComparison] = []
for lap = 1; lap <= analysis.lap_count; lap = lap + 1 {
let actual = match
lap_state_or_error(analysis, request.target_driver, lap) {
Ok(state) => state
Err(error) => return Err(error)
}
let opponent = match
lap_state_or_error(analysis, request.opponent_driver, lap) {
Ok(state) => state
Err(error) => return Err(error)
}
let simulated_lap = simulated[lap - 1]
let position = match
simulated_position(
analysis,
request.target_driver,
lap,
simulated_lap.cumulative_time_ms,
) {
Ok(position) => position
Err(error) => return Err(error)
}
comparisons.push({
lap,
neutral_lap_time_ms: anchors[lap - 1],
weather: actual.weather,
track_status: actual.track_status,
actual_lap_time_ms: actual.lap_time_ms,
actual_cumulative_time_ms: actual.cumulative_time_ms,
actual_compound: actual.compound,
actual_tyre_age_laps: actual.tyre_age_laps,
actual_pit: actual.pit,
actual_position: actual.position,
actual_signed_gap_to_opponent_ms: actual.cumulative_time_ms -
opponent.cumulative_time_ms,
simulated_lap_time_ms: simulated_lap.lap_time_ms,
simulated_cumulative_time_ms: simulated_lap.cumulative_time_ms,
simulated_compound: simulated_lap.tyre_state.compound,
simulated_tyre_age_laps: simulated_lap.tyre_state.tyre_age_laps,
simulated_pit: simulated_lap.tyre_state.pit,
simulated_position: position,
simulated_signed_gap_to_opponent_ms: simulated_lap.cumulative_time_ms -
opponent.cumulative_time_ms,
cumulative_time_gain_ms: actual.cumulative_time_ms -
simulated_lap.cumulative_time_ms,
})
}
Ok(comparisons)
}
///|
fn count_actual_stops(
analysis : RaceAnalysis,
target_driver : String,
) -> Result[Int, StrategyError] {
let mut count = 0
for lap = 1; lap <= analysis.lap_count; lap = lap + 1 {
let state = match lap_state_or_error(analysis, target_driver, lap) {
Ok(state) => state
Err(error) => return Err(error)
}
if state.pit {
count = count + 1
}
}
Ok(count)
}
///|
fn build_summary(
analysis : RaceAnalysis,
request : StrategyRequest,
comparisons : Array[StrategyLapComparison],
) -> Result[StrategySummary, StrategyError] {
let final_comparison = comparisons[comparisons.length() - 1]
let actual_stop_count = match
count_actual_stops(analysis, request.target_driver) {
Ok(count) => count
Err(error) => return Err(error)
}
let actual_final_gap = final_comparison.actual_signed_gap_to_opponent_ms
let simulated_final_gap = final_comparison.simulated_signed_gap_to_opponent_ms
Ok({
target_driver: request.target_driver,
opponent_driver: request.opponent_driver,
lap_count: analysis.lap_count,
actual_stop_count,
simulated_stop_count: request.stops.length(),
actual_total_time_ms: final_comparison.actual_cumulative_time_ms,
simulated_total_time_ms: final_comparison.simulated_cumulative_time_ms,
time_gain_ms: final_comparison.cumulative_time_gain_ms,
actual_finish_position: final_comparison.actual_position,
simulated_finish_position: final_comparison.simulated_position,
positions_gained: final_comparison.actual_position -
final_comparison.simulated_position,
actual_final_signed_gap_to_opponent_ms: actual_final_gap,
simulated_final_signed_gap_to_opponent_ms: simulated_final_gap,
opponent_gap_gain_ms: actual_final_gap - simulated_final_gap,
})
}
///|
/// Replay a complete replacement pit plan for one target driver against real rivals.
///
/// `request.stops` replaces, rather than augments, the target driver's observed
/// stops; other drivers retain their observed timeline. The returned comparisons
/// use milliseconds where positive `cumulative_time_gain_ms` means the simulated
/// alternative is faster. Returns a structured `StrategyError` for invalid
/// model configuration, unknown drivers, illegal stops, or unreconstructable laps.
pub fn simulate_strategy(
data : RaceData,
config : PaceModelConfig,
request : StrategyRequest,
) -> Result[StrategySimulation, StrategyError] {
match validate_pace_model_config(config) {
Err(error) =>
return Err(
strategy_error(
InvalidModel,
None,
"invalid pace model: " + error.message,
),
)
Ok(_) => ()
}
let analysis = analyze_race(data)
let normalized_request = match
validate_and_normalize_request(analysis, request) {
Ok(request) => request
Err(error) => return Err(error)
}
let anchors = match
derive_neutral_anchors(analysis, config, normalized_request.target_driver) {
Ok(anchors) => anchors
Err(error) => return Err(error)
}
let simulated = match
build_simulated_laps(analysis, config, normalized_request, anchors) {
Ok(laps) => laps
Err(error) => return Err(error)
}
let laps = match
build_comparisons(analysis, normalized_request, anchors, simulated) {
Ok(laps) => laps
Err(error) => return Err(error)
}
let summary = match build_summary(analysis, normalized_request, laps) {
Ok(summary) => summary
Err(error) => return Err(error)
}
Ok({ request: normalized_request, laps, summary, })
}