From 252aa8dccc481e613bf550a6d3da5eb2b9b1708f Mon Sep 17 00:00:00 2001 From: hamz2a Date: Tue, 8 Oct 2024 17:02:59 +0200 Subject: [PATCH 1/6] editoast: return conflicts list when STDCM request fails Signed-off-by: hamz2a --- editoast/src/core/stdcm.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/editoast/src/core/stdcm.rs b/editoast/src/core/stdcm.rs index 47f65310be7..869aba5de56 100644 --- a/editoast/src/core/stdcm.rs +++ b/editoast/src/core/stdcm.rs @@ -13,6 +13,7 @@ use utoipa::ToSchema; use super::conflict_detection::Conflict; use super::conflict_detection::TrainRequirements; +use super::pathfinding::PathfindingResult; use super::pathfinding::PathfindingResultSuccess; use super::pathfinding::TrackRange; use super::simulation::PhysicsRollingStock; From 32700a05f2d77fbaedba79f8a9d3bcd09b761f01 Mon Sep 17 00:00:00 2001 From: hamz2a Date: Tue, 22 Oct 2024 13:01:12 +0200 Subject: [PATCH 2/6] editoast: move simulation_run_time to SimulationResponse Signed-off-by: hamz2a --- editoast/src/core/simulation.rs | 11 +++++ editoast/src/views/timetable/stdcm.rs | 62 ++++++++------------------- 2 files changed, 28 insertions(+), 45 deletions(-) diff --git a/editoast/src/core/simulation.rs b/editoast/src/core/simulation.rs index d34f64bfe86..d441c915ac0 100644 --- a/editoast/src/core/simulation.rs +++ b/editoast/src/core/simulation.rs @@ -348,3 +348,14 @@ impl AsCoreRequest> for SimulationRequest { Some(self.infra) } } + +impl SimulationResponse { + pub fn simulation_run_time(self) -> Result> { + match self { + SimulationResponse::Success { provisional, .. } => { + Ok(*provisional.times.last().expect("empty simulation result")) + } + err => Err(Box::from(err)), + } + } +} diff --git a/editoast/src/views/timetable/stdcm.rs b/editoast/src/views/timetable/stdcm.rs index 4c0b770e15e..68c08551617 100644 --- a/editoast/src/views/timetable/stdcm.rs +++ b/editoast/src/views/timetable/stdcm.rs @@ -238,24 +238,21 @@ async fn stdcm( // 2. Compute the earliest start time, maximum running time and maximum departure delay // Simulation time without stop duration - let ( - simulation_run_time, - virtual_train_schedule, - virtual_train_sim_result, - virtual_train_pathfinding_result, - ) = simulate_train_run( - db_pool.clone(), - valkey_client.clone(), - core_client.clone(), - &stdcm_request, - &infra, - &rolling_stock, - timetable_id, - ) - .await?; - let simulation_run_time = match simulation_run_time { - SimulationTimeResult::SimulationTime { value } => value, - SimulationTimeResult::Error { error } => { + let (virtual_train_schedule, virtual_train_sim_result, virtual_train_pathfinding_result) = + simulate_train_run( + db_pool.clone(), + valkey_client.clone(), + core_client.clone(), + &stdcm_request, + &infra, + &rolling_stock, + timetable_id, + ) + .await?; + + let simulation_run_time = match virtual_train_sim_result.clone().simulation_run_time() { + Ok(value) => value, + Err(error) => { return Ok(Json(STDCMResponse::PreprocessingSimulationError { error: *error, })) @@ -582,7 +579,6 @@ fn get_earliest_step_tolerance_window(data: &STDCMRequestPayload) -> u64 { } /// Returns a `Result` containing: -/// * `SimulationTimeResult` - The result of the simulation time calculation. /// * `TrainSchedule` - The generated train schedule based on the provided data. /// * `SimulationResponse` - Simulation response. /// * `PathfindingResult` - Pathfinding result. @@ -594,12 +590,7 @@ async fn simulate_train_run( infra: &Infra, rolling_stock: &RollingStockModel, timetable_id: i64, -) -> Result<( - SimulationTimeResult, - TrainSchedule, - SimulationResponse, - PathfindingResult, -)> { +) -> Result<(TrainSchedule, SimulationResponse, PathfindingResult)> { // Doesn't matter for now, but eventually it will affect tmp speed limits let approx_start_time = get_earliest_step_time(data); @@ -641,20 +632,7 @@ async fn simulate_train_run( ) .await?; - let simulation_run_time = match sim_result.clone() { - SimulationResponse::Success { provisional, .. } => SimulationTimeResult::SimulationTime { - value: *provisional.times.last().expect("empty simulation result"), - }, - err => SimulationTimeResult::Error { - error: Box::from(err), - }, - }; - Ok(( - simulation_run_time, - train_schedule, - sim_result, - pathfinding_result, - )) + Ok((train_schedule, sim_result, pathfinding_result)) } /// Returns the request's total stop time @@ -810,12 +788,6 @@ async fn parse_stdcm_steps( .collect()) } -#[derive(Debug)] -enum SimulationTimeResult { - SimulationTime { value: u64 }, - Error { error: Box }, -} - #[cfg(test)] mod tests { use axum::http::StatusCode; From 6cd21b4aad52fea920f4c350eb1016caa2823cd4 Mon Sep 17 00:00:00 2001 From: hamz2a Date: Tue, 22 Oct 2024 14:38:25 +0200 Subject: [PATCH 3/6] editoast: refactor STDCMRequestPayload methods for encapsulation - Moved utility functions `get_earliest_step_time`, `get_earliest_step_tolerance_window`, `get_total_stop_time`, `get_maximum_departure_delay`, `get_earliest_departure_time` and `get_latest_simulation_end` into the `STDCMRequestPayload` struct as methods. - Updated the `stdcm` function to utilize these new methods for better readability and maintainability. - Removed redundant function definitions and improved code organization. Signed-off-by: hamz2a --- editoast/src/views/timetable.rs | 3 +- editoast/src/views/timetable/stdcm.rs | 210 ++---------------- .../views/timetable/stdcm_request_payload.rs | 181 +++++++++++++++ 3 files changed, 205 insertions(+), 189 deletions(-) create mode 100644 editoast/src/views/timetable/stdcm_request_payload.rs diff --git a/editoast/src/views/timetable.rs b/editoast/src/views/timetable.rs index 49e49c54622..a7d20596dc2 100644 --- a/editoast/src/views/timetable.rs +++ b/editoast/src/views/timetable.rs @@ -1,4 +1,5 @@ pub mod stdcm; +pub mod stdcm_request_payload; use std::collections::HashMap; @@ -57,7 +58,7 @@ crate::routes! { editoast_common::schemas! { TimetableResult, TimetableDetailedResult, - stdcm::schemas(), + stdcm_request_payload::schemas(), } #[derive(Debug, Error, EditoastError)] diff --git a/editoast/src/views/timetable/stdcm.rs b/editoast/src/views/timetable/stdcm.rs index 68c08551617..16af32e754e 100644 --- a/editoast/src/views/timetable/stdcm.rs +++ b/editoast/src/views/timetable/stdcm.rs @@ -10,10 +10,10 @@ use editoast_derive::EditoastError; use editoast_models::DbConnection; use editoast_models::DbConnectionPoolV2; use editoast_schemas::primitives::PositiveDuration; -use editoast_schemas::train_schedule::PathItemLocation; +use editoast_schemas::train_schedule::MarginValue; +use editoast_schemas::train_schedule::Margins; use editoast_schemas::train_schedule::ReceptionSignal; -use editoast_schemas::train_schedule::{Comfort, Margins, PathItem}; -use editoast_schemas::train_schedule::{MarginValue, ScheduleItem}; +use editoast_schemas::train_schedule::ScheduleItem; use itertools::Itertools; use serde::Deserialize; use serde::Serialize; @@ -24,6 +24,8 @@ use thiserror::Error; use utoipa::IntoParams; use utoipa::ToSchema; +use super::stdcm_request_payload::convert_steps; +use super::stdcm_request_payload::STDCMRequestPayload; use super::SelectionSettings; use crate::core::conflict_detection::ConflictDetectionRequest; use crate::core::conflict_detection::TrainRequirements; @@ -31,7 +33,6 @@ use crate::core::conflict_detection::WorkSchedulesRequest; use crate::core::pathfinding::InvalidPathItem; use crate::core::pathfinding::PathfindingInputError; use crate::core::simulation::PhysicsRollingStock; -use crate::core::simulation::SimulationParameters; use crate::core::simulation::{RoutingRequirement, SimulationResponse, SpacingRequirement}; use crate::core::stdcm::STDCMPathItem; use crate::core::stdcm::STDCMRequest; @@ -63,12 +64,6 @@ crate::routes! { "/stdcm" => stdcm, } -editoast_common::schemas! { - STDCMRequestPayload, - PathfindingItem, - StepTimingData, -} - #[derive(Debug, Error, EditoastError, Serialize)] #[editoast_error(base_id = "stdcm_v2")] enum STDCMError { @@ -83,80 +78,6 @@ enum STDCMError { InvalidPathItems { items: Vec }, } -/// An STDCM request -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)] -pub struct STDCMRequestPayload { - /// Deprecated, first step arrival time should be used instead - start_time: Option>, - steps: Vec, - rolling_stock_id: i64, - electrical_profile_set_id: Option, - work_schedule_group_id: Option, - temporary_speed_limit_group_id: Option, - comfort: Comfort, - /// By how long we can shift the departure time in milliseconds - /// Deprecated, first step data should be used instead - maximum_departure_delay: Option, - /// Specifies how long the total run time can be in milliseconds - /// Deprecated, first step data should be used instead - maximum_run_time: Option, - /// Train categories for speed limits - // TODO: rename the field and its description - speed_limit_tags: Option, - /// Margin before the train passage in seconds - /// - /// Enforces that the path used by the train should be free and - /// available at least that many milliseconds before its passage. - #[serde(default)] - time_gap_before: u64, - /// Margin after the train passage in milliseconds - /// - /// Enforces that the path used by the train should be free and - /// available at least that many milliseconds after its passage. - #[serde(default)] - time_gap_after: u64, - /// Can be a percentage `X%`, a time in minutes per 100 kilometer `Xmin/100km` - #[serde(default)] - #[schema(value_type = Option, example = json!(["5%", "2min/100km"]))] - margin: Option, - /// Total mass of the consist in kg - total_mass: Option, - /// Total length of the consist in meters - total_length: Option, - /// Maximum speed of the consist in km/h - max_speed: Option, -} - -impl STDCMRequestPayload { - pub fn simulation_parameters(&self) -> SimulationParameters { - SimulationParameters { - total_mass: self.total_mass, - total_length: self.total_length, - max_speed: self.max_speed, - } - } -} - -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)] -struct PathfindingItem { - /// The stop duration in milliseconds, None if the train does not stop. - duration: Option, - /// The associated location - location: PathItemLocation, - /// Time at which the train should arrive at the location, if specified - timing_data: Option, -} - -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)] -struct StepTimingData { - /// Time at which the train should arrive at the location - arrival_time: DateTime, - /// The train may arrive up to this duration before the expected arrival time - arrival_time_tolerance_before: u64, - /// The train may arrive up to this duration after the expected arrival time - arrival_time_tolerance_after: u64, -} - #[derive(Debug, Default, Clone, Serialize, Deserialize, IntoParams, ToSchema)] struct InfraIdQueryParam { infra: i64, @@ -259,20 +180,9 @@ async fn stdcm( } }; - let earliest_step_tolerance_window = get_earliest_step_tolerance_window(&stdcm_request); - let maximum_departure_delay = get_maximum_departure_delay( - &stdcm_request, - simulation_run_time, - earliest_step_tolerance_window, - ); - // Maximum duration between train departure and arrival, including all stops - let maximum_run_time = stdcm_request - .maximum_run_time - .unwrap_or(2 * simulation_run_time + get_total_stop_time(&stdcm_request)); - - let earliest_departure_time = get_earliest_departure_time(&stdcm_request, maximum_run_time); - let latest_simulation_end = earliest_departure_time - + Duration::milliseconds((maximum_run_time + earliest_step_tolerance_window) as i64); + let earliest_departure_time = stdcm_request.get_earliest_departure_time(simulation_run_time); + let maximum_run_time = stdcm_request.get_maximum_run_time(simulation_run_time); + let latest_simulation_end = stdcm_request.get_latest_simulation_end(simulation_run_time); // 3. Get scheduled train requirements let trains_requirements = build_train_requirements( @@ -321,7 +231,7 @@ async fn stdcm( path_items, start_time: earliest_departure_time, trains_requirements: trains_requirements.clone(), - maximum_departure_delay, + maximum_departure_delay: stdcm_request.get_maximum_departure_delay(simulation_run_time), maximum_run_time, speed_limit_tag: stdcm_request.speed_limit_tags, time_gap_before: stdcm_request.time_gap_before, @@ -342,17 +252,17 @@ async fn stdcm( // 8. Handle PathNotFound response of STDCM if let STDCMResponse::PathNotFound = stdcm_response { let stdcm_response = handle_path_not_found( - virtual_train_schedule, + core_client, train_schedules, simulations, + &work_schedules, + virtual_train_schedule, virtual_train_sim_result, virtual_train_pathfinding_result, earliest_departure_time, latest_simulation_end, - &work_schedules, infra_id, infra.version, - core_client, ) .await?; @@ -364,17 +274,17 @@ async fn stdcm( #[allow(clippy::too_many_arguments)] async fn handle_path_not_found( - virtual_train_schedule: TrainSchedule, + core_client: Arc, train_schedules: Vec, simulations: Vec<(SimulationResponse, PathfindingResult)>, + work_schedules: &[WorkSchedule], + virtual_train_schedule: TrainSchedule, virtual_train_sim_result: SimulationResponse, virtual_train_pathfinding_result: PathfindingResult, earliest_departure_time: DateTime, latest_simulation_end: DateTime, - work_schedules: &[WorkSchedule], infra_id: i64, infra_version: String, - core_client: Arc, ) -> Result { let virtual_train_id = virtual_train_schedule.id; @@ -521,63 +431,6 @@ fn is_resource_in_range( abs_resource_start_time <= latest_sim_time && abs_resource_end_time >= earliest_sim_time } -// Returns the maximum departure delay for the train. -fn get_maximum_departure_delay( - data: &STDCMRequestPayload, - simulation_run_time: u64, - earliest_step_tolerance_window: u64, -) -> u64 { - data.maximum_departure_delay - .unwrap_or(simulation_run_time + earliest_step_tolerance_window) -} - -/// Returns the earliest time at which the train may start -fn get_earliest_departure_time(data: &STDCMRequestPayload, maximum_run_time: u64) -> DateTime { - // Prioritize: start time, or first step time, or (first specified time - max run time) - data.start_time.unwrap_or( - data.steps - .first() - .and_then(|step| step.timing_data.clone()) - .and_then(|data| { - Option::from( - data.arrival_time - - Duration::milliseconds(data.arrival_time_tolerance_before as i64), - ) - }) - .unwrap_or( - get_earliest_step_time(data) - Duration::milliseconds(maximum_run_time as i64), - ), - ) -} - -/// Returns the earliest time that has been set on any step -fn get_earliest_step_time(data: &STDCMRequestPayload) -> DateTime { - // Get the earliest time that has been specified for any step - data.start_time - .or_else(|| { - data.steps - .iter() - .flat_map(|step| step.timing_data.iter()) - .map(|data| { - data.arrival_time - - Duration::milliseconds(data.arrival_time_tolerance_before as i64) - }) - .next() - }) - .expect("No time specified for stdcm request") -} - -/// Returns the earliest tolerance window that has been set on any step -fn get_earliest_step_tolerance_window(data: &STDCMRequestPayload) -> u64 { - // Get the earliest time window that has been specified for any step, if maximum_run_time is not none - data.steps - .iter() - .flat_map(|step| step.timing_data.iter()) - .map(|data| data.arrival_time_tolerance_before + data.arrival_time_tolerance_after) - .next() - .unwrap_or(0) -} - /// Returns a `Result` containing: /// * `TrainSchedule` - The generated train schedule based on the provided data. /// * `SimulationResponse` - Simulation response. @@ -586,15 +439,15 @@ async fn simulate_train_run( db_pool: Arc, valkey_client: Arc, core_client: Arc, - data: &STDCMRequestPayload, + stdcm_request: &STDCMRequestPayload, infra: &Infra, rolling_stock: &RollingStockModel, timetable_id: i64, ) -> Result<(TrainSchedule, SimulationResponse, PathfindingResult)> { // Doesn't matter for now, but eventually it will affect tmp speed limits - let approx_start_time = get_earliest_step_time(data); + let approx_start_time = stdcm_request.get_earliest_step_time(); - let path = convert_steps(&data.steps); + let path = convert_steps(&stdcm_request.steps); let last_step = path.last().expect("empty step list"); let train_schedule = TrainSchedule { @@ -612,12 +465,12 @@ async fn simulate_train_run( reception_signal: ReceptionSignal::Open, locked: false, }], - margins: build_single_margin(data.margin), + margins: build_single_margin(stdcm_request.margin), initial_speed: 0.0, - comfort: data.comfort, + comfort: stdcm_request.comfort, path, constraint_distribution: Default::default(), - speed_limit_tag: data.speed_limit_tags.clone(), + speed_limit_tag: stdcm_request.speed_limit_tags.clone(), power_restrictions: vec![], options: Default::default(), }; @@ -635,26 +488,6 @@ async fn simulate_train_run( Ok((train_schedule, sim_result, pathfinding_result)) } -/// Returns the request's total stop time -fn get_total_stop_time(data: &STDCMRequestPayload) -> u64 { - data.steps - .iter() - .map(|step: &PathfindingItem| step.duration.unwrap_or_default()) - .sum() -} - -/// Convert the list of pathfinding items into a list of path item -fn convert_steps(steps: &[PathfindingItem]) -> Vec { - steps - .iter() - .map(|step| PathItem { - id: Default::default(), - deleted: false, - location: step.location.clone(), - }) - .collect() -} - /// Build a margins object with one margin value covering the entire range fn build_single_margin(margin: Option) -> Margins { match margin { @@ -806,6 +639,7 @@ mod tests { use crate::core::simulation::CompleteReportTrain; use crate::core::simulation::ElectricalProfiles; use crate::core::simulation::ReportTrain; + use crate::core::simulation::SimulationParameters; use crate::core::simulation::SimulationResponse; use crate::core::simulation::SpeedLimitProperties; use crate::core::stdcm::STDCMResponse; diff --git a/editoast/src/views/timetable/stdcm_request_payload.rs b/editoast/src/views/timetable/stdcm_request_payload.rs new file mode 100644 index 00000000000..a66fe1f1e6b --- /dev/null +++ b/editoast/src/views/timetable/stdcm_request_payload.rs @@ -0,0 +1,181 @@ +use chrono::DateTime; +use chrono::Duration; +use chrono::Utc; +use editoast_schemas::train_schedule::Comfort; +use editoast_schemas::train_schedule::MarginValue; +use editoast_schemas::train_schedule::PathItem; +use editoast_schemas::train_schedule::PathItemLocation; +use serde::Deserialize; +use serde::Serialize; +use utoipa::ToSchema; + +use crate::core::simulation::SimulationParameters; + +editoast_common::schemas! { + STDCMRequestPayload, + PathfindingItem, + StepTimingData, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)] +pub struct PathfindingItem { + /// The stop duration in milliseconds, None if the train does not stop. + pub duration: Option, + /// The associated location + pub location: PathItemLocation, + /// Time at which the train should arrive at the location, if specified + pub timing_data: Option, +} + +/// Convert the list of pathfinding items into a list of path item +pub fn convert_steps(steps: &[PathfindingItem]) -> Vec { + steps + .iter() + .map(|step| PathItem { + id: Default::default(), + deleted: false, + location: step.location.clone(), + }) + .collect() +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)] +pub struct StepTimingData { + /// Time at which the train should arrive at the location + pub arrival_time: DateTime, + /// The train may arrive up to this duration before the expected arrival time + pub arrival_time_tolerance_before: u64, + /// The train may arrive up to this duration after the expected arrival time + pub arrival_time_tolerance_after: u64, +} + +/// An STDCM request +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)] +pub struct STDCMRequestPayload { + /// Deprecated, first step arrival time should be used instead + pub start_time: Option>, + pub steps: Vec, + pub rolling_stock_id: i64, + pub electrical_profile_set_id: Option, + pub work_schedule_group_id: Option, + pub temporary_speed_limit_group_id: Option, + pub comfort: Comfort, + /// By how long we can shift the departure time in milliseconds + /// Deprecated, first step data should be used instead + pub maximum_departure_delay: Option, + /// Specifies how long the total run time can be in milliseconds + /// Deprecated, first step data should be used instead + pub maximum_run_time: Option, + /// Train categories for speed limits + // TODO: rename the field and its description + pub speed_limit_tags: Option, + /// Margin before the train passage in seconds + /// + /// Enforces that the path used by the train should be free and + /// available at least that many milliseconds before its passage. + #[serde(default)] + pub time_gap_before: u64, + /// Margin after the train passage in milliseconds + /// + /// Enforces that the path used by the train should be free and + /// available at least that many milliseconds after its passage. + #[serde(default)] + pub time_gap_after: u64, + /// Can be a percentage `X%`, a time in minutes per 100 kilometer `Xmin/100km` + #[serde(default)] + #[schema(value_type = Option, example = json!(["5%", "2min/100km"]))] + pub margin: Option, + /// Total mass of the consist in kg + pub total_mass: Option, + /// Total length of the consist in meters + pub total_length: Option, + /// Maximum speed of the consist in km/h + pub max_speed: Option, +} + +impl STDCMRequestPayload { + pub fn simulation_parameters(&self) -> SimulationParameters { + SimulationParameters { + total_mass: self.total_mass, + total_length: self.total_length, + max_speed: self.max_speed, + } + } + + /// Returns the earliest time that has been set on any step + pub fn get_earliest_step_time(&self) -> DateTime { + // Get the earliest time that has been specified for any step + self.start_time + .or_else(|| { + self.steps + .iter() + .flat_map(|step| step.timing_data.iter()) + .map(|data| { + data.arrival_time + - Duration::milliseconds(data.arrival_time_tolerance_before as i64) + }) + .next() + }) + .expect("No time specified for stdcm request") + } + + /// Returns the earliest tolerance window that has been set on any step + fn get_earliest_step_tolerance_window(&self) -> u64 { + // Get the earliest time window that has been specified for any step, if maximum_run_time is not none + self.steps + .iter() + .flat_map(|step| step.timing_data.iter()) + .map(|data| data.arrival_time_tolerance_before + data.arrival_time_tolerance_after) + .next() + .unwrap_or(0) + } + + /// Returns the request's total stop time + fn get_total_stop_time(&self) -> u64 { + self.steps + .iter() + .map(|step: &PathfindingItem| step.duration.unwrap_or_default()) + .sum() + } + + // Returns the maximum departure delay for the train. + pub fn get_maximum_departure_delay(&self, simulation_run_time: u64) -> u64 { + self.maximum_departure_delay + .unwrap_or(simulation_run_time + self.get_earliest_step_tolerance_window()) + } + // Maximum duration between train departure and arrival, including all stops + pub fn get_maximum_run_time(&self, simulation_run_time: u64) -> u64 { + self.maximum_run_time + .unwrap_or(2 * simulation_run_time + self.get_total_stop_time()) + } + + /// Returns the earliest time at which the train may start + pub fn get_earliest_departure_time(&self, simulation_run_time: u64) -> DateTime { + // Prioritize: start time, or first step time, or (first specified time - max run time) + self.start_time.unwrap_or( + self.steps + .first() + .and_then(|step| step.timing_data.clone()) + .and_then(|data| { + Option::from( + data.arrival_time + - Duration::milliseconds(data.arrival_time_tolerance_before as i64), + ) + }) + .unwrap_or( + self.get_earliest_step_time() + - Duration::milliseconds( + self.get_maximum_run_time(simulation_run_time) as i64 + ), + ), + ) + } + + pub fn get_latest_simulation_end(&self, simulation_run_time: u64) -> DateTime { + self.get_earliest_departure_time(simulation_run_time) + + Duration::milliseconds( + (self.get_maximum_run_time(simulation_run_time) + + self.get_earliest_step_tolerance_window()) as i64, + ) + } +} From 06565d3097c2b71d751a0423695014564de8e3ff Mon Sep 17 00:00:00 2001 From: hamz2a Date: Wed, 23 Oct 2024 13:17:42 +0200 Subject: [PATCH 4/6] editoast: refactor path not found handling Signed-off-by: hamz2a --- editoast/src/core/stdcm.rs | 4 +- editoast/src/views/timetable.rs | 31 ++++ .../views/timetable/path_not_found_handler.rs | 119 +++++++++++++++ editoast/src/views/timetable/stdcm.rs | 143 ++---------------- 4 files changed, 163 insertions(+), 134 deletions(-) create mode 100644 editoast/src/views/timetable/path_not_found_handler.rs diff --git a/editoast/src/core/stdcm.rs b/editoast/src/core/stdcm.rs index 869aba5de56..d8b33158e64 100644 --- a/editoast/src/core/stdcm.rs +++ b/editoast/src/core/stdcm.rs @@ -13,12 +13,12 @@ use utoipa::ToSchema; use super::conflict_detection::Conflict; use super::conflict_detection::TrainRequirements; -use super::pathfinding::PathfindingResult; use super::pathfinding::PathfindingResultSuccess; use super::pathfinding::TrackRange; use super::simulation::PhysicsRollingStock; use super::simulation::SimulationResponse; -use crate::core::{AsCoreRequest, Json}; +use crate::core::AsCoreRequest; +use crate::core::Json; use crate::views::path::pathfinding::PathfindingResult; #[derive(Debug, Serialize)] diff --git a/editoast/src/views/timetable.rs b/editoast/src/views/timetable.rs index a7d20596dc2..14c9d53ac2e 100644 --- a/editoast/src/views/timetable.rs +++ b/editoast/src/views/timetable.rs @@ -1,6 +1,8 @@ +pub mod path_not_found_handler; pub mod stdcm; pub mod stdcm_request_payload; +use std::cmp::max; use std::collections::HashMap; use axum::extract::Json; @@ -10,6 +12,10 @@ use axum::extract::State; use axum::http::StatusCode; use axum::response::IntoResponse; use axum::Extension; +use chrono::DateTime; +use chrono::NaiveDateTime; +use chrono::TimeZone; +use chrono::Utc; use derivative::Derivative; use editoast_authz::BuiltinRole; use editoast_derive::EditoastError; @@ -25,6 +31,7 @@ use crate::core::conflict_detection::Conflict; use crate::core::conflict_detection::ConflictDetectionRequest; use crate::core::conflict_detection::TrainRequirements; use crate::core::simulation::SimulationResponse; +use crate::core::stdcm::UndirectedTrackRange; use crate::core::AsCoreRequest; use crate::error::Result; use crate::models::prelude::*; @@ -32,6 +39,7 @@ use crate::models::timetable::Timetable; use crate::models::timetable::TimetableWithTrains; use crate::models::train_schedule::TrainSchedule; use crate::models::train_schedule::TrainScheduleChangeset; +use crate::models::work_schedules::WorkSchedule; use crate::models::Infra; use crate::views::train_schedule::train_simulation_batch; use crate::views::train_schedule::TrainScheduleForm; @@ -351,6 +359,29 @@ async fn conflicts( Ok(Json(conflict_detection_response.conflicts)) } +pub fn map_to_core_work_schedule( + ws: &WorkSchedule, + start_time: DateTime, +) -> crate::core::stdcm::WorkSchedule { + crate::core::stdcm::WorkSchedule { + start_time: elapsed_since_time_ms(&ws.start_date_time, &start_time), + end_time: elapsed_since_time_ms(&ws.end_date_time, &start_time), + track_ranges: ws + .track_ranges + .iter() + .map(|track| UndirectedTrackRange { + track_section: track.track.to_string(), + begin: (track.begin * 1000.0) as u64, + end: (track.end * 1000.0) as u64, + }) + .collect(), + } +} + +fn elapsed_since_time_ms(time: &NaiveDateTime, zero: &DateTime) -> u64 { + max(0, (Utc.from_utc_datetime(time) - zero).num_milliseconds()) as u64 +} + #[cfg(test)] mod tests { use axum::http::StatusCode; diff --git a/editoast/src/views/timetable/path_not_found_handler.rs b/editoast/src/views/timetable/path_not_found_handler.rs new file mode 100644 index 00000000000..e762db64446 --- /dev/null +++ b/editoast/src/views/timetable/path_not_found_handler.rs @@ -0,0 +1,119 @@ +use std::sync::Arc; + +use chrono::DateTime; +use chrono::Utc; + +use crate::core::conflict_detection::ConflictDetectionRequest; +use crate::core::conflict_detection::WorkSchedulesRequest; +use crate::core::simulation::SimulationResponse; +use crate::core::stdcm::STDCMResponse; +use crate::core::AsCoreRequest; +use crate::core::CoreClient; +use crate::error::Result; +use crate::models::train_schedule::TrainSchedule; +use crate::models::work_schedules::WorkSchedule; +use crate::views::path::pathfinding::PathfindingResult; + +use super::map_to_core_work_schedule; +use super::stdcm::build_train_requirements; + +pub struct PathNotFoundHandler { + pub core_client: Arc, + pub infra_id: i64, + pub infra_version: String, + pub train_schedules: Vec, + pub simulations: Vec<(SimulationResponse, PathfindingResult)>, + pub work_schedules: Vec, + pub virtual_train_schedule: TrainSchedule, + pub virtual_train_sim_result: SimulationResponse, + pub virtual_train_pathfinding_result: PathfindingResult, + pub earliest_departure_time: DateTime, + pub latest_simulation_end: DateTime, +} + +impl PathNotFoundHandler { + pub async fn handle(self) -> Result { + let virtual_train_id = self.virtual_train_schedule.id; + + // Combine the original train schedules with the virtual train schedule. + let train_schedules = [self.train_schedules, vec![self.virtual_train_schedule]].concat(); + + // Combine the original simulations with the virtual train's simulation results. + let simulations = [ + self.simulations, + vec![( + self.virtual_train_sim_result, + self.virtual_train_pathfinding_result.clone(), + )], + ] + .concat(); + + // Build train requirements based on the combined train schedules and simulations + // This prepares the data structure required for conflict detection. + let trains_requirements = build_train_requirements( + train_schedules, + simulations, + self.earliest_departure_time, + self.latest_simulation_end, + ); + + // Filter the provided work schedules to find those that conflict with the given parameters + // This identifies any work schedules that may overlap with the earliest departure time and maximum run time. + let conflict_work_schedules = make_work_schedules_request( + &self.work_schedules, + self.earliest_departure_time, + self.latest_simulation_end, + ); + + // Prepare the conflict detection request. + let conflict_detection_request = ConflictDetectionRequest { + infra: self.infra_id, + expected_version: self.infra_version, + trains_requirements, + work_schedules: conflict_work_schedules, + }; + + // Send the conflict detection request and await the response. + let conflict_detection_response = + conflict_detection_request.fetch(&self.core_client).await?; + + // Filter the conflicts to find those specifically related to the virtual train. + let conflicts: Vec<_> = conflict_detection_response + .conflicts + .into_iter() + .filter(|conflict| conflict.train_ids.contains(&virtual_train_id)) + .map(|mut conflict| { + conflict.train_ids.retain(|id| id != &virtual_train_id); + conflict + }) + .collect(); + + // Return the conflicts found along with the pathfinding result for the virtual train. + Ok(STDCMResponse::Conflicts { + pathfinding_result: self.virtual_train_pathfinding_result, + conflicts, + }) + } +} + +fn make_work_schedules_request( + work_schedules: &[WorkSchedule], + start_time: DateTime, + latest_simulation_end: DateTime, +) -> Option { + if work_schedules.is_empty() { + return None; + } + let search_window_duration = (latest_simulation_end - start_time).num_milliseconds() as u64; + + let work_schedule_requirements = work_schedules + .iter() + .map(|ws| (ws.id, map_to_core_work_schedule(ws, start_time))) + .filter(|(_, ws)| ws.end_time > 0 && ws.start_time < search_window_duration) + .collect(); + + Some(WorkSchedulesRequest { + start_time, + work_schedule_requirements, + }) +} diff --git a/editoast/src/views/timetable/stdcm.rs b/editoast/src/views/timetable/stdcm.rs index 16af32e754e..4542ff93e7c 100644 --- a/editoast/src/views/timetable/stdcm.rs +++ b/editoast/src/views/timetable/stdcm.rs @@ -4,7 +4,7 @@ use axum::extract::Query; use axum::extract::State; use axum::Extension; use chrono::Utc; -use chrono::{DateTime, Duration, NaiveDateTime, TimeZone}; +use chrono::{DateTime, Duration}; use editoast_authz::BuiltinRole; use editoast_derive::EditoastError; use editoast_models::DbConnection; @@ -17,19 +17,18 @@ use editoast_schemas::train_schedule::ScheduleItem; use itertools::Itertools; use serde::Deserialize; use serde::Serialize; -use std::cmp::max; use std::collections::HashMap; use std::sync::Arc; use thiserror::Error; use utoipa::IntoParams; use utoipa::ToSchema; +use super::map_to_core_work_schedule; +use super::path_not_found_handler::PathNotFoundHandler; use super::stdcm_request_payload::convert_steps; use super::stdcm_request_payload::STDCMRequestPayload; use super::SelectionSettings; -use crate::core::conflict_detection::ConflictDetectionRequest; use crate::core::conflict_detection::TrainRequirements; -use crate::core::conflict_detection::WorkSchedulesRequest; use crate::core::pathfinding::InvalidPathItem; use crate::core::pathfinding::PathfindingInputError; use crate::core::simulation::PhysicsRollingStock; @@ -38,7 +37,6 @@ use crate::core::stdcm::STDCMPathItem; use crate::core::stdcm::STDCMRequest; use crate::core::stdcm::STDCMResponse; use crate::core::stdcm::STDCMStepTimingData; -use crate::core::stdcm::UndirectedTrackRange; use crate::core::AsCoreRequest; use crate::core::CoreClient; use crate::error::Result; @@ -251,20 +249,20 @@ async fn stdcm( // 8. Handle PathNotFound response of STDCM if let STDCMResponse::PathNotFound = stdcm_response { - let stdcm_response = handle_path_not_found( + let path_not_found = PathNotFoundHandler { core_client, + infra_id, + infra_version: infra.version, train_schedules, simulations, - &work_schedules, + work_schedules, virtual_train_schedule, virtual_train_sim_result, virtual_train_pathfinding_result, earliest_departure_time, latest_simulation_end, - infra_id, - infra.version, - ) - .await?; + }; + let stdcm_response = path_not_found.handle().await?; return Ok(Json(stdcm_response)); } @@ -272,84 +270,9 @@ async fn stdcm( Ok(Json(stdcm_response)) } -#[allow(clippy::too_many_arguments)] -async fn handle_path_not_found( - core_client: Arc, - train_schedules: Vec, - simulations: Vec<(SimulationResponse, PathfindingResult)>, - work_schedules: &[WorkSchedule], - virtual_train_schedule: TrainSchedule, - virtual_train_sim_result: SimulationResponse, - virtual_train_pathfinding_result: PathfindingResult, - earliest_departure_time: DateTime, - latest_simulation_end: DateTime, - infra_id: i64, - infra_version: String, -) -> Result { - let virtual_train_id = virtual_train_schedule.id; - - // Combine the original train schedules with the virtual train schedule. - let train_schedules = [train_schedules, vec![virtual_train_schedule]].concat(); - - // Combine the original simulations with the virtual train's simulation results. - let simulations = [ - simulations, - vec![( - virtual_train_sim_result, - virtual_train_pathfinding_result.clone(), - )], - ] - .concat(); - - // Build train requirements based on the combined train schedules and simulations - // This prepares the data structure required for conflict detection. - let trains_requirements = build_train_requirements( - train_schedules, - simulations, - earliest_departure_time, - latest_simulation_end, - ); - - // Filter the provided work schedules to find those that conflict with the given parameters - // This identifies any work schedules that may overlap with the earliest departure time and maximum run time. - let conflict_work_schedules = make_work_schedules_request( - work_schedules, - earliest_departure_time, - latest_simulation_end, - ); - - // Prepare the conflict detection request. - let conflict_detection_request = ConflictDetectionRequest { - infra: infra_id, - expected_version: infra_version, - trains_requirements, - work_schedules: conflict_work_schedules, - }; - - // Send the conflict detection request and await the response. - let conflict_detection_response = conflict_detection_request.fetch(&core_client).await?; - - // Filter the conflicts to find those specifically related to the virtual train. - let conflicts: Vec<_> = conflict_detection_response - .conflicts - .into_iter() - .filter(|conflict| conflict.train_ids.contains(&virtual_train_id)) - .map(|mut conflict| { - conflict.train_ids.retain(|id| id != &virtual_train_id); - conflict - }) - .collect(); - - // Return the conflicts found along with the pathfinding result for the virtual train. - Ok(STDCMResponse::Conflicts { - pathfinding_result: virtual_train_pathfinding_result, - conflicts, - }) -} - /// Build the list of scheduled train requirements, only including requirements /// that overlap with the possible simulation times. -fn build_train_requirements( +pub fn build_train_requirements( train_schedules: Vec, simulations: Vec<(SimulationResponse, PathfindingResult)>, departure_time: DateTime, @@ -502,25 +425,6 @@ fn build_single_margin(margin: Option) -> Margins { } } -fn map_to_core_work_schedule( - ws: &WorkSchedule, - start_time: DateTime, -) -> crate::core::stdcm::WorkSchedule { - crate::core::stdcm::WorkSchedule { - start_time: elapsed_since_time_ms(&ws.start_date_time, &start_time), - end_time: elapsed_since_time_ms(&ws.end_date_time, &start_time), - track_ranges: ws - .track_ranges - .iter() - .map(|track| UndirectedTrackRange { - track_section: track.track.to_string(), - begin: (track.begin * 1000.0) as u64, - end: (track.end * 1000.0) as u64, - }) - .collect(), - } -} - fn filter_stdcm_work_schedules( work_schedules: &[WorkSchedule], start_time: DateTime, @@ -534,28 +438,6 @@ fn filter_stdcm_work_schedules( .collect() } -fn make_work_schedules_request( - work_schedules: &[WorkSchedule], - start_time: DateTime, - latest_simulation_end: DateTime, -) -> Option { - if work_schedules.is_empty() { - return None; - } - let search_window_duration = (latest_simulation_end - start_time).num_milliseconds() as u64; - - let work_schedule_requirements = work_schedules - .iter() - .map(|ws| (ws.id, map_to_core_work_schedule(ws, start_time))) - .filter(|(_, ws)| ws.end_time > 0 && ws.start_time < search_window_duration) - .collect(); - - Some(WorkSchedulesRequest { - start_time, - work_schedule_requirements, - }) -} - /// Return the list of speed limits that are active at any point in a given time range async fn build_temporary_speed_limits( conn: &mut DbConnection, @@ -582,10 +464,6 @@ async fn build_temporary_speed_limits( Ok(applicable_speed_limits) } -fn elapsed_since_time_ms(time: &NaiveDateTime, zero: &DateTime) -> u64 { - max(0, (Utc.from_utc_datetime(time) - zero).num_milliseconds()) as u64 -} - /// Create steps from track_map and waypoints async fn parse_stdcm_steps( conn: &mut DbConnection, @@ -625,6 +503,7 @@ async fn parse_stdcm_steps( mod tests { use axum::http::StatusCode; use chrono::DateTime; + use chrono::NaiveDateTime; use editoast_models::DbConnectionPoolV2; use pretty_assertions::assert_eq; use rstest::rstest; From 257f22c516d76bcf08cfdcd84b4d0a5d01124a57 Mon Sep 17 00:00:00 2001 From: hamz2a Date: Fri, 25 Oct 2024 10:05:17 +0200 Subject: [PATCH 5/6] editoast: add STDCMCoreResponse Signed-off-by: hamz2a --- editoast/openapi.yaml | 8 -- editoast/src/core/simulation.rs | 2 +- editoast/src/core/stdcm.rs | 12 +-- .../views/timetable/path_not_found_handler.rs | 2 +- editoast/src/views/timetable/stdcm.rs | 76 +++++++++++++------ front/src/common/api/generatedEditoastApi.ts | 3 - 6 files changed, 59 insertions(+), 44 deletions(-) diff --git a/editoast/openapi.yaml b/editoast/openapi.yaml index 26fd401fe76..caedcdad4d6 100644 --- a/editoast/openapi.yaml +++ b/editoast/openapi.yaml @@ -2620,14 +2620,6 @@ paths: type: string enum: - success - - type: object - required: - - status - properties: - status: - type: string - enum: - - path_not_found - type: object required: - pathfinding_result diff --git a/editoast/src/core/simulation.rs b/editoast/src/core/simulation.rs index d441c915ac0..91b1999eefb 100644 --- a/editoast/src/core/simulation.rs +++ b/editoast/src/core/simulation.rs @@ -350,7 +350,7 @@ impl AsCoreRequest> for SimulationRequest { } impl SimulationResponse { - pub fn simulation_run_time(self) -> Result> { + pub fn simulation_run_time(self) -> Result> { match self { SimulationResponse::Success { provisional, .. } => { Ok(*provisional.times.last().expect("empty simulation result")) diff --git a/editoast/src/core/stdcm.rs b/editoast/src/core/stdcm.rs index d8b33158e64..7ab65e4fed5 100644 --- a/editoast/src/core/stdcm.rs +++ b/editoast/src/core/stdcm.rs @@ -11,7 +11,6 @@ use serde::Deserialize; use serde::Serialize; use utoipa::ToSchema; -use super::conflict_detection::Conflict; use super::conflict_detection::TrainRequirements; use super::pathfinding::PathfindingResultSuccess; use super::pathfinding::TrackRange; @@ -19,7 +18,6 @@ use super::simulation::PhysicsRollingStock; use super::simulation::SimulationResponse; use crate::core::AsCoreRequest; use crate::core::Json; -use crate::views::path::pathfinding::PathfindingResult; #[derive(Debug, Serialize)] pub struct STDCMRequest { @@ -116,28 +114,24 @@ pub struct UndirectedTrackRange { pub end: u64, } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, ToSchema)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] #[serde(tag = "status", rename_all = "snake_case")] // We accepted the difference of memory size taken by variants // Since there is only on success and others are error cases #[allow(clippy::large_enum_variant)] -pub enum STDCMResponse { +pub enum STDCMCoreResponse { Success { simulation: SimulationResponse, path: PathfindingResultSuccess, departure_time: DateTime, }, PathNotFound, - Conflicts { - pathfinding_result: PathfindingResult, - conflicts: Vec, - }, PreprocessingSimulationError { error: SimulationResponse, }, } -impl AsCoreRequest> for STDCMRequest { +impl AsCoreRequest> for STDCMRequest { const METHOD: reqwest::Method = reqwest::Method::POST; const URL_PATH: &'static str = "/v2/stdcm"; diff --git a/editoast/src/views/timetable/path_not_found_handler.rs b/editoast/src/views/timetable/path_not_found_handler.rs index e762db64446..bbf22b4983f 100644 --- a/editoast/src/views/timetable/path_not_found_handler.rs +++ b/editoast/src/views/timetable/path_not_found_handler.rs @@ -6,13 +6,13 @@ use chrono::Utc; use crate::core::conflict_detection::ConflictDetectionRequest; use crate::core::conflict_detection::WorkSchedulesRequest; use crate::core::simulation::SimulationResponse; -use crate::core::stdcm::STDCMResponse; use crate::core::AsCoreRequest; use crate::core::CoreClient; use crate::error::Result; use crate::models::train_schedule::TrainSchedule; use crate::models::work_schedules::WorkSchedule; use crate::views::path::pathfinding::PathfindingResult; +use crate::views::timetable::stdcm::STDCMResponse; use super::map_to_core_work_schedule; use super::stdcm::build_train_requirements; diff --git a/editoast/src/views/timetable/stdcm.rs b/editoast/src/views/timetable/stdcm.rs index 4542ff93e7c..4b574b41c93 100644 --- a/editoast/src/views/timetable/stdcm.rs +++ b/editoast/src/views/timetable/stdcm.rs @@ -28,14 +28,16 @@ use super::path_not_found_handler::PathNotFoundHandler; use super::stdcm_request_payload::convert_steps; use super::stdcm_request_payload::STDCMRequestPayload; use super::SelectionSettings; +use crate::core::conflict_detection::Conflict; use crate::core::conflict_detection::TrainRequirements; use crate::core::pathfinding::InvalidPathItem; use crate::core::pathfinding::PathfindingInputError; +use crate::core::pathfinding::PathfindingResultSuccess; use crate::core::simulation::PhysicsRollingStock; use crate::core::simulation::{RoutingRequirement, SimulationResponse, SpacingRequirement}; +use crate::core::stdcm::STDCMCoreResponse; use crate::core::stdcm::STDCMPathItem; use crate::core::stdcm::STDCMRequest; -use crate::core::stdcm::STDCMResponse; use crate::core::stdcm::STDCMStepTimingData; use crate::core::AsCoreRequest; use crate::core::CoreClient; @@ -62,6 +64,26 @@ crate::routes! { "/stdcm" => stdcm, } +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, ToSchema)] +#[serde(tag = "status", rename_all = "snake_case")] +// We accepted the difference of memory size taken by variants +// Since there is only on success and others are error cases +#[allow(clippy::large_enum_variant)] +pub enum STDCMResponse { + Success { + simulation: SimulationResponse, + path: PathfindingResultSuccess, + departure_time: DateTime, + }, + Conflicts { + pathfinding_result: PathfindingResult, + conflicts: Vec, + }, + PreprocessingSimulationError { + error: SimulationResponse, + }, +} + #[derive(Debug, Error, EditoastError, Serialize)] #[editoast_error(base_id = "stdcm_v2")] enum STDCMError { @@ -247,27 +269,38 @@ async fn stdcm( let stdcm_response = stdcm_request.fetch(core_client.as_ref()).await?; - // 8. Handle PathNotFound response of STDCM - if let STDCMResponse::PathNotFound = stdcm_response { - let path_not_found = PathNotFoundHandler { - core_client, - infra_id, - infra_version: infra.version, - train_schedules, - simulations, - work_schedules, - virtual_train_schedule, - virtual_train_sim_result, - virtual_train_pathfinding_result, - earliest_departure_time, - latest_simulation_end, - }; - let stdcm_response = path_not_found.handle().await?; - - return Ok(Json(stdcm_response)); + // 8. Handle STDCM Core Response + match stdcm_response { + STDCMCoreResponse::Success { + simulation, + path, + departure_time, + } => Ok(Json(STDCMResponse::Success { + simulation, + path, + departure_time, + })), + STDCMCoreResponse::PreprocessingSimulationError { error } => { + Ok(Json(STDCMResponse::PreprocessingSimulationError { error })) + } + STDCMCoreResponse::PathNotFound => { + let path_not_found = PathNotFoundHandler { + core_client, + infra_id, + infra_version: infra.version, + train_schedules, + simulations, + work_schedules, + virtual_train_schedule, + virtual_train_sim_result, + virtual_train_pathfinding_result, + earliest_departure_time, + latest_simulation_end, + }; + let stdcm_response = path_not_found.handle().await?; + Ok(Json(stdcm_response)) + } } - - Ok(Json(stdcm_response)) } /// Build the list of scheduled train requirements, only including requirements @@ -521,7 +554,6 @@ mod tests { use crate::core::simulation::SimulationParameters; use crate::core::simulation::SimulationResponse; use crate::core::simulation::SpeedLimitProperties; - use crate::core::stdcm::STDCMResponse; use crate::models::fixtures::create_fast_rolling_stock; use crate::models::fixtures::create_small_infra; use crate::models::fixtures::create_timetable; diff --git a/front/src/common/api/generatedEditoastApi.ts b/front/src/common/api/generatedEditoastApi.ts index 3cd4cd0ec64..caa5ca622ca 100644 --- a/front/src/common/api/generatedEditoastApi.ts +++ b/front/src/common/api/generatedEditoastApi.ts @@ -1514,9 +1514,6 @@ export type PostTimetableByIdStdcmApiResponse = /** status 201 The simulation re simulation: SimulationResponse; status: 'success'; } - | { - status: 'path_not_found'; - } | { conflicts: Conflict[]; pathfinding_result: PathfindingResult; From 0f81762c78c219d9fd8ec2fc972d8a4304076de6 Mon Sep 17 00:00:00 2001 From: hamz2a Date: Tue, 29 Oct 2024 11:25:26 +0100 Subject: [PATCH 6/6] editoast: move map_to_core_work_schedule to WorkSchedule Signed-off-by: hamz2a --- editoast/src/models/work_schedules.rs | 49 +++++++++++++++++++ editoast/src/views/timetable.rs | 30 ------------ .../views/timetable/path_not_found_handler.rs | 3 +- editoast/src/views/timetable/stdcm.rs | 31 ++++-------- 4 files changed, 60 insertions(+), 53 deletions(-) diff --git a/editoast/src/models/work_schedules.rs b/editoast/src/models/work_schedules.rs index 894912dd037..31d3fbb96be 100644 --- a/editoast/src/models/work_schedules.rs +++ b/editoast/src/models/work_schedules.rs @@ -1,4 +1,9 @@ +use std::cmp::max; + +use chrono::DateTime; use chrono::NaiveDateTime; +use chrono::TimeZone; +use chrono::Utc; use editoast_derive::Model; use editoast_schemas::infra::TrackRange; use strum::FromRepr; @@ -7,6 +12,8 @@ use serde::Deserialize; use serde::Serialize; use utoipa::ToSchema; +use crate::core::stdcm::UndirectedTrackRange; + #[derive(Debug, Clone, Model)] #[model(table = editoast_models::tables::work_schedule_group)] #[model(gen(ops = crd, batch_ops = c, list))] @@ -38,3 +45,45 @@ pub struct WorkSchedule { pub work_schedule_type: WorkScheduleType, pub work_schedule_group_id: i64, } + +impl WorkSchedule { + pub fn map_to_core_work_schedule( + &self, + start_time: DateTime, + ) -> crate::core::stdcm::WorkSchedule { + crate::core::stdcm::WorkSchedule { + start_time: elapsed_since_time_ms(&self.start_date_time, &start_time), + end_time: elapsed_since_time_ms(&self.end_date_time, &start_time), + track_ranges: self + .track_ranges + .iter() + .map(|track| UndirectedTrackRange { + track_section: track.track.to_string(), + begin: (track.begin * 1000.0) as u64, + end: (track.end * 1000.0) as u64, + }) + .collect(), + } + } + + pub fn make_stdcm_work_schedule( + &self, + start_time: DateTime, + latest_simulation_end: DateTime, + ) -> Option { + let search_window_duration = (latest_simulation_end - start_time).num_milliseconds() as u64; + + let ws = self.map_to_core_work_schedule(start_time); + if ws.end_time > 0 && ws.start_time < search_window_duration { + return Some(ws); + } + None + } +} + +fn elapsed_since_time_ms(time: &NaiveDateTime, start_time: &DateTime) -> u64 { + max( + 0, + (Utc.from_utc_datetime(time) - start_time).num_milliseconds(), + ) as u64 +} diff --git a/editoast/src/views/timetable.rs b/editoast/src/views/timetable.rs index 14c9d53ac2e..4755bee8227 100644 --- a/editoast/src/views/timetable.rs +++ b/editoast/src/views/timetable.rs @@ -2,7 +2,6 @@ pub mod path_not_found_handler; pub mod stdcm; pub mod stdcm_request_payload; -use std::cmp::max; use std::collections::HashMap; use axum::extract::Json; @@ -12,10 +11,6 @@ use axum::extract::State; use axum::http::StatusCode; use axum::response::IntoResponse; use axum::Extension; -use chrono::DateTime; -use chrono::NaiveDateTime; -use chrono::TimeZone; -use chrono::Utc; use derivative::Derivative; use editoast_authz::BuiltinRole; use editoast_derive::EditoastError; @@ -31,7 +26,6 @@ use crate::core::conflict_detection::Conflict; use crate::core::conflict_detection::ConflictDetectionRequest; use crate::core::conflict_detection::TrainRequirements; use crate::core::simulation::SimulationResponse; -use crate::core::stdcm::UndirectedTrackRange; use crate::core::AsCoreRequest; use crate::error::Result; use crate::models::prelude::*; @@ -39,7 +33,6 @@ use crate::models::timetable::Timetable; use crate::models::timetable::TimetableWithTrains; use crate::models::train_schedule::TrainSchedule; use crate::models::train_schedule::TrainScheduleChangeset; -use crate::models::work_schedules::WorkSchedule; use crate::models::Infra; use crate::views::train_schedule::train_simulation_batch; use crate::views::train_schedule::TrainScheduleForm; @@ -359,29 +352,6 @@ async fn conflicts( Ok(Json(conflict_detection_response.conflicts)) } -pub fn map_to_core_work_schedule( - ws: &WorkSchedule, - start_time: DateTime, -) -> crate::core::stdcm::WorkSchedule { - crate::core::stdcm::WorkSchedule { - start_time: elapsed_since_time_ms(&ws.start_date_time, &start_time), - end_time: elapsed_since_time_ms(&ws.end_date_time, &start_time), - track_ranges: ws - .track_ranges - .iter() - .map(|track| UndirectedTrackRange { - track_section: track.track.to_string(), - begin: (track.begin * 1000.0) as u64, - end: (track.end * 1000.0) as u64, - }) - .collect(), - } -} - -fn elapsed_since_time_ms(time: &NaiveDateTime, zero: &DateTime) -> u64 { - max(0, (Utc.from_utc_datetime(time) - zero).num_milliseconds()) as u64 -} - #[cfg(test)] mod tests { use axum::http::StatusCode; diff --git a/editoast/src/views/timetable/path_not_found_handler.rs b/editoast/src/views/timetable/path_not_found_handler.rs index bbf22b4983f..1db3b59e427 100644 --- a/editoast/src/views/timetable/path_not_found_handler.rs +++ b/editoast/src/views/timetable/path_not_found_handler.rs @@ -14,7 +14,6 @@ use crate::models::work_schedules::WorkSchedule; use crate::views::path::pathfinding::PathfindingResult; use crate::views::timetable::stdcm::STDCMResponse; -use super::map_to_core_work_schedule; use super::stdcm::build_train_requirements; pub struct PathNotFoundHandler { @@ -108,7 +107,7 @@ fn make_work_schedules_request( let work_schedule_requirements = work_schedules .iter() - .map(|ws| (ws.id, map_to_core_work_schedule(ws, start_time))) + .map(|ws| (ws.id, ws.map_to_core_work_schedule(start_time))) .filter(|(_, ws)| ws.end_time > 0 && ws.start_time < search_window_duration) .collect(); diff --git a/editoast/src/views/timetable/stdcm.rs b/editoast/src/views/timetable/stdcm.rs index 4b574b41c93..c8436ee26c5 100644 --- a/editoast/src/views/timetable/stdcm.rs +++ b/editoast/src/views/timetable/stdcm.rs @@ -23,7 +23,6 @@ use thiserror::Error; use utoipa::IntoParams; use utoipa::ToSchema; -use super::map_to_core_work_schedule; use super::path_not_found_handler::PathNotFoundHandler; use super::stdcm_request_payload::convert_steps; use super::stdcm_request_payload::STDCMRequestPayload; @@ -258,11 +257,12 @@ async fn stdcm( time_gap_after: stdcm_request.time_gap_after, margin: stdcm_request.margin, time_step: Some(2000), - work_schedules: filter_stdcm_work_schedules( - &work_schedules, - earliest_departure_time, - latest_simulation_end, - ), + work_schedules: work_schedules + .iter() + .filter_map(|ws| { + ws.make_stdcm_work_schedule(earliest_departure_time, latest_simulation_end) + }) + .collect(), temporary_speed_limits, rolling_stock: PhysicsRollingStock::new(rolling_stock.into(), simulation_parameters), }; @@ -458,19 +458,6 @@ fn build_single_margin(margin: Option) -> Margins { } } -fn filter_stdcm_work_schedules( - work_schedules: &[WorkSchedule], - start_time: DateTime, - latest_simulation_end: DateTime, -) -> Vec { - let search_window_duration = (latest_simulation_end - start_time).num_milliseconds() as u64; - work_schedules - .iter() - .map(|ws| map_to_core_work_schedule(ws, start_time)) - .filter(|ws| ws.end_time > 0 && ws.start_time < search_window_duration) - .collect() -} - /// Return the list of speed limits that are active at any point in a given time range async fn build_temporary_speed_limits( conn: &mut DbConnection, @@ -863,8 +850,10 @@ mod tests { .to_utc(); // WHEN - let filtered = - filter_stdcm_work_schedules(&work_schedules, start_time, latest_simulation_end); + let filtered: Vec<_> = work_schedules + .iter() + .filter_map(|ws| ws.make_stdcm_work_schedule(start_time, latest_simulation_end)) + .collect(); // THEN assert!(filtered.is_empty() == filtered_out);