///|
fn pad_three_digits(value : Int) -> String {
if value < 10 {
"00" + value.to_string()
} else if value < 100 {
"0" + value.to_string()
} else {
value.to_string()
}
}
///|
/// Format an integer-millisecond duration without a leading positive sign.
///
/// Negative input keeps a leading minus sign; non-negative input has no plus
/// sign. The stable output always uses seconds with three decimal places.
pub fn format_duration_ms(milliseconds : Int) -> String {
if milliseconds < 0 {
"-" + format_duration_ms(0 - milliseconds)
} else {
let seconds = milliseconds / 1000
let remainder_ms = milliseconds % 1000
seconds.to_string() + "." + pad_three_digits(remainder_ms) + " s"
}
}
///|
/// Format an integer-millisecond signed gain or gap with an explicit sign.
///
/// Positive values render with `+`, negative values with `-`, and zero as
/// `0.000 s`; callers supply the domain-specific gain or gap semantics.
pub fn format_signed_ms(milliseconds : Int) -> String {
if milliseconds > 0 {
"+" + format_duration_ms(milliseconds)
} else if milliseconds < 0 {
"-" + format_duration_ms(0 - milliseconds)
} else {
"0.000 s"
}
}
///|
fn validate_strategy_simulation(
simulation : StrategySimulation,
) -> Result[Unit, ExplanationError] {
if simulation.laps.length() == 0 {
return Err(
explanation_error(
EmptySimulation,
None,
"strategy simulation must contain at least one lap",
),
)
}
let summary = simulation.summary
let request = simulation.request
if summary.target_driver != request.target_driver ||
summary.opponent_driver != request.opponent_driver {
return Err(
explanation_error(
InconsistentSimulation,
None,
"strategy summary drivers do not match the simulation request",
),
)
}
if summary.lap_count != simulation.laps.length() {
return Err(
explanation_error(
InconsistentSimulation,
None,
"strategy summary lap count does not match the lap comparisons",
),
)
}
let mut actual_stop_count = 0
let mut simulated_stop_count = 0
let mut previous_actual_cumulative_time_ms = 0
let mut previous_simulated_cumulative_time_ms = 0
for index = 0; index < simulation.laps.length(); index = index + 1 {
let comparison = simulation.laps[index]
if comparison.lap != index + 1 {
return Err(
explanation_error(
InconsistentSimulation,
Some(comparison.lap),
"strategy lap comparisons must be continuous and sorted by lap",
),
)
}
if comparison.cumulative_time_gain_ms !=
comparison.actual_cumulative_time_ms -
comparison.simulated_cumulative_time_ms ||
comparison.actual_cumulative_time_ms !=
previous_actual_cumulative_time_ms + comparison.actual_lap_time_ms ||
comparison.simulated_cumulative_time_ms !=
previous_simulated_cumulative_time_ms + comparison.simulated_lap_time_ms {
return Err(
explanation_error(
InconsistentSimulation,
Some(comparison.lap),
"strategy lap comparison times and gains are inconsistent",
),
)
}
if comparison.actual_pit {
actual_stop_count = actual_stop_count + 1
}
if comparison.simulated_pit {
simulated_stop_count = simulated_stop_count + 1
}
previous_actual_cumulative_time_ms = comparison.actual_cumulative_time_ms
previous_simulated_cumulative_time_ms = comparison.simulated_cumulative_time_ms
}
let final_lap = simulation.laps[simulation.laps.length() - 1]
if summary.actual_total_time_ms != final_lap.actual_cumulative_time_ms ||
summary.simulated_total_time_ms != final_lap.simulated_cumulative_time_ms ||
summary.time_gain_ms != final_lap.cumulative_time_gain_ms ||
summary.actual_stop_count != actual_stop_count ||
summary.simulated_stop_count != simulated_stop_count ||
summary.actual_finish_position != final_lap.actual_position ||
summary.simulated_finish_position != final_lap.simulated_position ||
summary.positions_gained !=
final_lap.actual_position - final_lap.simulated_position ||
summary.actual_final_signed_gap_to_opponent_ms !=
final_lap.actual_signed_gap_to_opponent_ms ||
summary.simulated_final_signed_gap_to_opponent_ms !=
final_lap.simulated_signed_gap_to_opponent_ms ||
summary.opponent_gap_gain_ms !=
final_lap.actual_signed_gap_to_opponent_ms -
final_lap.simulated_signed_gap_to_opponent_ms {
return Err(
explanation_error(
InconsistentSimulation,
Some(final_lap.lap),
"strategy summary does not match the final lap comparison",
),
)
}
Ok(())
}
///|
fn verdict_for(summary : StrategySummary) -> StrategyVerdict {
if summary.time_gain_ms > 0 {
Improved
} else if summary.time_gain_ms < 0 {
Worsened
} else {
Unchanged
}
}
///|
fn actual_or_simulated_stop(
laps : Array[StrategyLapComparison],
actual : Bool,
) -> Array[StrategyPitStop] {
let stops : Array[StrategyPitStop] = []
for index = 0; index < laps.length(); index = index + 1 {
let comparison = laps[index]
let pit = if actual {
comparison.actual_pit
} else {
comparison.simulated_pit
}
if pit {
let next_compound = if index == laps.length() - 1 {
None
} else if actual {
Some(laps[index + 1].actual_compound)
} else {
Some(laps[index + 1].simulated_compound)
}
stops.push({ lap: comparison.lap, next_compound, })
}
}
stops
}
///|
fn track_status_name(status : TrackStatus) -> String {
match status {
Green => "GREEN"
SafetyCar => "SAFETY_CAR"
}
}
///|
fn weather_name(weather : Weather) -> String {
match weather {
Dry => "DRY"
Damp => "DAMP"
Wet => "WET"
}
}
///|
fn crossover_turning_kind(kind : CrossoverKind) -> TurningPointKind {
match kind {
Performance => PerformanceCrossover
Strategic => StrategicCrossover
Opponent => OpponentCrossover
}
}
///|
fn crossover_direction_message(direction : CrossoverDirection) -> String {
match direction {
AlternativeBecomesFaster => "Alternative becomes faster"
ActualBecomesFaster => "Actual becomes faster"
AlternativeBecomesBetter => "Alternative becomes better"
ActualBecomesBetter => "Actual becomes better"
TargetMovesAhead => "Target moves ahead of opponent"
TargetFallsBehind => "Target falls behind opponent"
}
}
///|
fn append_crossover_turning_points(
turning_points : Array[TurningPoint],
crossovers : Array[CrossoverPoint],
) -> Unit {
for point in crossovers {
turning_points.push({
lap: point.lap,
kind: crossover_turning_kind(point.kind),
impact_ms: Some(point.current_value_ms),
message: crossover_direction_message(point.direction) + ".",
})
}
}
///|
fn append_lap_state_turning_points(
turning_points : Array[TurningPoint],
config : PaceModelConfig,
laps : Array[StrategyLapComparison],
) -> Unit {
for index = 0; index < laps.length(); index = index + 1 {
let current = laps[index]
if index > 0 {
let previous = laps[index - 1]
if previous.track_status != current.track_status {
turning_points.push({
lap: current.lap,
kind: TrackStatusChange,
impact_ms: None,
message: "Track status changed from " +
track_status_name(previous.track_status) +
" to " +
track_status_name(current.track_status) +
".",
})
}
if previous.weather != current.weather {
turning_points.push({
lap: current.lap,
kind: WeatherChange,
impact_ms: None,
message: "Weather changed from " +
weather_name(previous.weather) +
" to " +
weather_name(current.weather) +
".",
})
}
}
if current.actual_pit {
turning_points.push({
lap: current.lap,
kind: ActualPitStop,
impact_ms: None,
message: "Actual strategy pit stop.",
})
}
if current.simulated_pit {
turning_points.push({
lap: current.lap,
kind: SimulatedPitStop,
impact_ms: None,
message: "Simulated strategy pit stop.",
})
if current.track_status == SafetyCar {
let savings_ms = config.pit_loss_profile.green_pit_loss_ms -
config.pit_loss_profile.safety_car_pit_loss_ms
turning_points.push({
lap: current.lap,
kind: SafetyCarPitOpportunity,
impact_ms: Some(savings_ms),
message: "Simulated pit during Safety Car; modeled pit loss is " +
format_duration_ms(savings_ms) +
" lower than GREEN.",
})
}
}
if index > 0 {
let previous = laps[index - 1]
let previous_relative = previous.actual_position -
previous.simulated_position
let current_relative = current.actual_position -
current.simulated_position
if current_relative > previous_relative {
turning_points.push({
lap: current.lap,
kind: RelativePositionGain,
impact_ms: None,
message: "Relative position improved.",
})
} else if current_relative < previous_relative {
turning_points.push({
lap: current.lap,
kind: RelativePositionLoss,
impact_ms: None,
message: "Relative position worsened.",
})
}
}
}
}
///|
fn best_and_worst_lap_impacts(
laps : Array[StrategyLapComparison],
) -> (LapImpact?, LapImpact?) {
let mut best : LapImpact? = None
let mut worst : LapImpact? = None
for comparison in laps {
let impact = {
lap: comparison.lap,
lap_time_gain_ms: comparison.actual_lap_time_ms -
comparison.simulated_lap_time_ms,
cumulative_time_gain_ms: comparison.cumulative_time_gain_ms,
}
if impact.lap_time_gain_ms > 0 {
match best {
None => best = Some(impact)
Some(previous) =>
if impact.lap_time_gain_ms > previous.lap_time_gain_ms {
best = Some(impact)
}
}
}
if impact.lap_time_gain_ms < 0 {
match worst {
None => worst = Some(impact)
Some(previous) =>
if impact.lap_time_gain_ms < previous.lap_time_gain_ms {
worst = Some(impact)
}
}
}
}
(best, worst)
}
///|
fn append_best_and_worst_turning_points(
turning_points : Array[TurningPoint],
best : LapImpact?,
worst : LapImpact?,
) -> Unit {
match best {
Some(impact) =>
turning_points.push({
lap: impact.lap,
kind: BestLapGain,
impact_ms: Some(impact.lap_time_gain_ms),
message: "Best single-lap gain.",
})
None => ()
}
match worst {
Some(impact) =>
turning_points.push({
lap: impact.lap,
kind: WorstLapLoss,
impact_ms: Some(impact.lap_time_gain_ms),
message: "Worst single-lap loss.",
})
None => ()
}
}
///|
fn turning_point_rank(kind : TurningPointKind) -> Int {
match kind {
TrackStatusChange => 1
WeatherChange => 2
ActualPitStop => 3
SimulatedPitStop => 4
SafetyCarPitOpportunity => 5
PerformanceCrossover => 6
StrategicCrossover => 7
OpponentCrossover => 8
RelativePositionGain => 9
RelativePositionLoss => 10
BestLapGain => 11
WorstLapLoss => 12
}
}
///|
fn sorted_turning_points(
config : PaceModelConfig,
laps : Array[StrategyLapComparison],
performance : Array[CrossoverPoint],
strategic : Array[CrossoverPoint],
opponent : Array[CrossoverPoint],
best : LapImpact?,
worst : LapImpact?,
) -> Array[TurningPoint] {
let turning_points : Array[TurningPoint] = []
append_lap_state_turning_points(turning_points, config, laps)
append_crossover_turning_points(turning_points, performance)
append_crossover_turning_points(turning_points, strategic)
append_crossover_turning_points(turning_points, opponent)
append_best_and_worst_turning_points(turning_points, best, worst)
turning_points.sort_by((left, right) => {
if left.lap != right.lap {
left.lap - right.lap
} else {
turning_point_rank(left.kind) - turning_point_rank(right.kind)
}
})
turning_points
}
///|
/// Explain one completed M4 simulation without replaying or mutating it.
///
/// Performance crossovers compare modeled non-pit lap pace, strategic
/// crossovers compare cumulative time gain, and opponent crossovers compare the
/// simulated signed gap to the unchanged opponent. Positive time gain means the
/// alternative improves the target result. Returns `ExplanationError` for an
/// invalid model or inconsistent simulation; model values remain illustrative,
/// not official F1 data.
pub fn explain_strategy(
config : PaceModelConfig,
simulation : StrategySimulation,
) -> Result[StrategyExplanation, ExplanationError] {
match validate_pace_model_config(config) {
Err(error) =>
return Err(
explanation_error(
InvalidModel,
None,
"invalid pace model: " + error.message,
),
)
Ok(_) => ()
}
match validate_strategy_simulation(simulation) {
Err(error) => return Err(error)
Ok(_) => ()
}
let performance_crossovers = match
detect_performance_crossovers(config, simulation.laps) {
Ok(points) => points
Err(error) => return Err(error)
}
let strategic_crossovers = detect_strategic_crossovers(simulation.laps)
let opponent_crossovers = detect_opponent_crossovers(simulation.laps)
let (best_lap_gain, worst_lap_loss) = best_and_worst_lap_impacts(
simulation.laps,
)
let turning_points = sorted_turning_points(
config,
simulation.laps,
performance_crossovers,
strategic_crossovers,
opponent_crossovers,
best_lap_gain,
worst_lap_loss,
)
Ok({
verdict: verdict_for(simulation.summary),
target_driver: simulation.request.target_driver,
opponent_driver: simulation.request.opponent_driver,
actual_stops: actual_or_simulated_stop(simulation.laps, true),
simulated_stops: actual_or_simulated_stop(simulation.laps, false),
performance_crossovers,
strategic_crossovers,
opponent_crossovers,
turning_points,
best_lap_gain,
worst_lap_loss,
summary: simulation.summary,
laps: simulation.laps,
})
}