///|
priv enum NonZeroRelation {
Positive
Negative
} derive(Eq)
///|
fn explanation_error(
code : ExplanationErrorCode,
lap : Int?,
message : String,
) -> ExplanationError {
{ code, lap, message, }
}
///|
fn non_zero_relation(value_ms : Int) -> NonZeroRelation? {
if value_ms > 0 {
Some(Positive)
} else if value_ms < 0 {
Some(Negative)
} else {
None
}
}
///|
fn crossover_direction(
kind : CrossoverKind,
relation : NonZeroRelation,
) -> CrossoverDirection {
match (kind, relation) {
(Performance, Positive) => AlternativeBecomesFaster
(Performance, Negative) => ActualBecomesFaster
(Strategic, Positive) => AlternativeBecomesBetter
(Strategic, Negative) => ActualBecomesBetter
(Opponent, Positive) => TargetFallsBehind
(Opponent, Negative) => TargetMovesAhead
}
}
///|
fn detect_value_crossovers(
values : Array[(Int, Int)],
kind : CrossoverKind,
record_initial_non_zero : Bool,
) -> Array[CrossoverPoint] {
let points : Array[CrossoverPoint] = []
let mut last_relation : NonZeroRelation? = None
let mut last_value_ms = 0
for (lap, value_ms) in values {
match non_zero_relation(value_ms) {
None => ()
Some(current_relation) =>
match last_relation {
None => {
if record_initial_non_zero {
points.push({
lap,
kind,
direction: crossover_direction(kind, current_relation),
previous_value_ms: 0,
current_value_ms: value_ms,
})
}
last_relation = Some(current_relation)
last_value_ms = value_ms
}
Some(previous_relation) => {
if current_relation != previous_relation {
points.push({
lap,
kind,
direction: crossover_direction(kind, current_relation),
previous_value_ms: last_value_ms,
current_value_ms: value_ms,
})
}
last_relation = Some(current_relation)
last_value_ms = value_ms
}
}
}
}
points
}
///|
fn performance_values(
config : PaceModelConfig,
laps : Array[StrategyLapComparison],
) -> Result[Array[(Int, Int)], ExplanationError] {
let values : Array[(Int, Int)] = []
for comparison in laps {
let actual_delta = match
tyre_pace_delta_ms(
config,
comparison.actual_compound,
comparison.actual_tyre_age_laps,
comparison.weather,
) {
Ok(delta) => delta
Err(error) =>
return Err(
explanation_error(
TyreDeltaFailure,
Some(comparison.lap),
"could not calculate actual tyre pace delta: " + error.message,
),
)
}
let simulated_delta = match
tyre_pace_delta_ms(
config,
comparison.simulated_compound,
comparison.simulated_tyre_age_laps,
comparison.weather,
) {
Ok(delta) => delta
Err(error) =>
return Err(
explanation_error(
TyreDeltaFailure,
Some(comparison.lap),
"could not calculate simulated tyre pace delta: " + error.message,
),
)
}
values.push((comparison.lap, actual_delta - simulated_delta))
}
Ok(values)
}
///|
fn detect_performance_crossovers(
config : PaceModelConfig,
laps : Array[StrategyLapComparison],
) -> Result[Array[CrossoverPoint], ExplanationError] {
match performance_values(config, laps) {
Ok(values) => Ok(detect_value_crossovers(values, Performance, true))
Err(error) => Err(error)
}
}
///|
fn detect_strategic_crossovers(
laps : Array[StrategyLapComparison],
) -> Array[CrossoverPoint] {
let values : Array[(Int, Int)] = []
for comparison in laps {
values.push((comparison.lap, comparison.cumulative_time_gain_ms))
}
detect_value_crossovers(values, Strategic, true)
}
///|
fn detect_opponent_crossovers(
laps : Array[StrategyLapComparison],
) -> Array[CrossoverPoint] {
let values : Array[(Int, Int)] = []
for comparison in laps {
values.push(
(comparison.lap, comparison.simulated_signed_gap_to_opponent_ms),
)
}
detect_value_crossovers(values, Opponent, false)
}