diff --git a/editoast/editoast_schemas/src/rolling_stock/effort_curves.rs b/editoast/editoast_schemas/src/rolling_stock/effort_curves.rs index 04c7478a7cf..066c5636550 100644 --- a/editoast/editoast_schemas/src/rolling_stock/effort_curves.rs +++ b/editoast/editoast_schemas/src/rolling_stock/effort_curves.rs @@ -27,9 +27,21 @@ impl EffortCurves { self.modes.values().any(|mode| mode.is_electric) } + pub fn has_thermal_curves(&self) -> bool { + self.modes.values().any(|mode| !mode.is_electric) + } + pub fn is_electric(&self) -> bool { self.has_electric_curves() } + + pub fn supported_electrification(&self) -> Vec { + self.modes + .iter() + .filter(|(_, mode)| mode.is_electric) + .map(|(key, _)| key.clone()) + .collect() + } } #[derive(Clone, Debug, PartialEq, Deserialize, Serialize, ToSchema, Hash)] diff --git a/editoast/openapi.yaml b/editoast/openapi.yaml index df21a9065db..d054ebf70fc 100644 --- a/editoast/openapi.yaml +++ b/editoast/openapi.yaml @@ -4741,6 +4741,7 @@ components: - $ref: '#/components/schemas/EditoastStdcmErrorRollingStockNotFound' - $ref: '#/components/schemas/EditoastStdcmErrorTimetableNotFound' - $ref: '#/components/schemas/EditoastStdcmErrorTowedRollingStockNotFound' + - $ref: '#/components/schemas/EditoastStdcmErrorTrainSimulationFail' - $ref: '#/components/schemas/EditoastStudyErrorNotFound' - $ref: '#/components/schemas/EditoastStudyErrorStartDateAfterEndDate' - $ref: '#/components/schemas/EditoastTemporarySpeedLimitErrorNameAlreadyUsed' @@ -5960,6 +5961,25 @@ components: type: string enum: - editoast:stdcm_v2:TowedRollingStockNotFound + EditoastStdcmErrorTrainSimulationFail: + type: object + required: + - type + - status + - message + properties: + context: + type: object + message: + type: string + status: + type: integer + enum: + - 400 + type: + type: string + enum: + - editoast:stdcm_v2:TrainSimulationFail EditoastStudyErrorNotFound: type: object required: diff --git a/editoast/src/core/simulation.rs b/editoast/src/core/simulation.rs index 7fb5c084914..187858d1f5a 100644 --- a/editoast/src/core/simulation.rs +++ b/editoast/src/core/simulation.rs @@ -1,6 +1,9 @@ use std::collections::BTreeMap; use std::collections::HashMap; +use std::hash::Hash; +use derivative::Derivative; +use editoast_schemas::primitives::Identifier; use editoast_schemas::rolling_stock::EffortCurves; use editoast_schemas::rolling_stock::RollingResistance; use editoast_schemas::rolling_stock::RollingStock; @@ -18,9 +21,6 @@ use super::pathfinding::TrackRange; use crate::core::{AsCoreRequest, Json}; use crate::error::InternalError; use crate::views::path::pathfinding::PathfindingFailure; -use derivative::Derivative; -use editoast_schemas::primitives::Identifier; -use std::hash::Hash; editoast_common::schemas! { CompleteReportTrain, @@ -84,7 +84,7 @@ pub struct PhysicsConsistParameters { } impl PhysicsConsistParameters { - pub fn with_traction_engine(traction_engine: RollingStock) -> Self { + pub fn from_traction_engine(traction_engine: RollingStock) -> Self { PhysicsConsistParameters { max_speed: None, total_length: None, diff --git a/editoast/src/models/rolling_stock_model.rs b/editoast/src/models/rolling_stock_model.rs index 43fcda323d6..1a2767a5b15 100644 --- a/editoast/src/models/rolling_stock_model.rs +++ b/editoast/src/models/rolling_stock_model.rs @@ -72,24 +72,6 @@ pub struct RollingStockModel { pub supported_signaling_systems: RollingStockSupportedSignalingSystems, } -impl RollingStockModel { - pub fn has_thermal_curves(&self) -> bool { - self.effort_curves - .modes - .values() - .any(|mode| !mode.is_electric) - } - - pub fn supported_electrification(&self) -> Vec { - self.effort_curves - .modes - .iter() - .filter(|(_, mode)| mode.is_electric) - .map(|(key, _)| key.clone()) - .collect() - } -} - impl RollingStockModelChangeset { pub fn validate_imported_rolling_stock(&self) -> std::result::Result<(), ValidationErrors> { self.validate()?; diff --git a/editoast/src/views/path/pathfinding.rs b/editoast/src/views/path/pathfinding.rs index ddef64d7edc..9109e5604ae 100644 --- a/editoast/src/views/path/pathfinding.rs +++ b/editoast/src/views/path/pathfinding.rs @@ -10,7 +10,9 @@ use axum::extract::State; use axum::Extension; use editoast_authz::BuiltinRole; use editoast_schemas::rolling_stock::LoadingGaugeType; +use editoast_schemas::rolling_stock::RollingStock; use editoast_schemas::train_schedule::PathItemLocation; +use itertools::Itertools; use ordered_float::OrderedFloat; use serde::Deserialize; use serde::Serialize; @@ -336,24 +338,19 @@ pub async fn pathfinding_from_train( infra: &Infra, train_schedule: TrainSchedule, ) -> Result { - let rolling_stocks = + let rolling_stock: Vec = RollingStockModel::retrieve(conn, train_schedule.rolling_stock_name.clone()) .await? .into_iter() - .map(|rs| (rs.name.clone(), rs)) + .map_into() .collect(); - Ok(pathfinding_from_train_batch( - conn, - valkey, - core, - infra, - &[train_schedule], - &rolling_stocks, + Ok( + pathfinding_from_train_batch(conn, valkey, core, infra, &[train_schedule], &rolling_stock) + .await? + .pop() + .unwrap(), ) - .await? - .pop() - .unwrap()) } /// Compute a path given a batch of trainschedule and an infrastructure. @@ -363,7 +360,7 @@ pub async fn pathfinding_from_train_batch( core: Arc, infra: &Infra, train_schedules: &[TrainSchedule], - rolling_stocks: &HashMap, + rolling_stocks: &[RollingStock], ) -> Result> { let mut results = vec![ PathfindingResult::Failure(PathfindingFailure::PathfindingInputError( @@ -371,12 +368,15 @@ pub async fn pathfinding_from_train_batch( )); train_schedules.len() ]; + + let rolling_stocks: HashMap<_, _> = rolling_stocks.iter().map(|rs| (&rs.name, rs)).collect(); + let mut to_compute = vec![]; let mut to_compute_index = vec![]; for (index, train_schedule) in train_schedules.iter().enumerate() { // Retrieve rolling stock let rolling_stock_name = &train_schedule.rolling_stock_name; - let Some(rolling_stock) = rolling_stocks.get(rolling_stock_name).cloned() else { + let Some(rolling_stock) = rolling_stocks.get(rolling_stock_name) else { let rolling_stock_name = rolling_stock_name.clone(); results[index] = PathfindingResult::Failure(PathfindingFailure::PathfindingInputError( PathfindingInputError::RollingStockNotFound { rolling_stock_name }, @@ -387,9 +387,14 @@ pub async fn pathfinding_from_train_batch( // Create the path input let path_input = PathfindingInput { rolling_stock_loading_gauge: rolling_stock.loading_gauge, - rolling_stock_is_thermal: rolling_stock.has_thermal_curves(), - rolling_stock_supported_electrifications: rolling_stock.supported_electrification(), - rolling_stock_supported_signaling_systems: rolling_stock.supported_signaling_systems.0, + rolling_stock_is_thermal: rolling_stock.effort_curves.has_thermal_curves(), + rolling_stock_supported_electrifications: rolling_stock + .effort_curves + .supported_electrification(), + rolling_stock_supported_signaling_systems: rolling_stock + .supported_signaling_systems + .0 + .clone(), rolling_stock_maximum_speed: OrderedFloat(rolling_stock.max_speed), rolling_stock_length: OrderedFloat(rolling_stock.length), path_items: train_schedule diff --git a/editoast/src/views/timetable/stdcm.rs b/editoast/src/views/timetable/stdcm.rs index f63488d4229..cf67774c481 100644 --- a/editoast/src/views/timetable/stdcm.rs +++ b/editoast/src/views/timetable/stdcm.rs @@ -45,7 +45,7 @@ use crate::models::train_schedule::TrainSchedule; use crate::models::Infra; use crate::models::RollingStockModel; use crate::views::path::pathfinding::PathfindingResult; -use crate::views::train_schedule::train_simulation; +use crate::views::train_schedule::consist_train_simulation_batch; use crate::views::train_schedule::train_simulation_batch; use crate::views::AuthenticationExt; use crate::views::AuthorizationError; @@ -92,6 +92,8 @@ enum StdcmError { RollingStockNotFound { rolling_stock_id: i64 }, #[error("Towed rolling stock {towed_rolling_stock_id} does not exist")] TowedRollingStockNotFound { towed_rolling_stock_id: i64 }, + #[error("Train simulation fail")] + TrainSimulationFail, #[error("Path items are invalid")] InvalidPathItems { items: Vec }, } @@ -167,7 +169,19 @@ async fn stdcm( rolling_stock_id: stdcm_request.rolling_stock_id, } }) - .await?; + .await? + .into(); + + let physics_consist_parameters = PhysicsConsistParameters { + max_speed: stdcm_request.max_speed, + total_length: stdcm_request.total_length, + total_mass: stdcm_request.total_mass, + towed_rolling_stock: stdcm_request + .get_towed_rolling_stock(&mut conn) + .await? + .map(From::from), + traction_engine: rolling_stock, + }; // 2. Compute the earliest start time and maximum departure delay let virtual_train_run = VirtualTrainRun::simulate( @@ -176,7 +190,7 @@ async fn stdcm( core_client.clone(), &stdcm_request, &infra, - &rolling_stock, + &physics_consist_parameters, timetable_id, ) .await?; @@ -219,21 +233,12 @@ async fn stdcm( let stdcm_request = crate::core::stdcm::Request { infra: infra.id, expected_version: infra.version.clone(), - rolling_stock_loading_gauge: rolling_stock.loading_gauge, - rolling_stock_supported_signaling_systems: rolling_stock + rolling_stock_loading_gauge: physics_consist_parameters.traction_engine.loading_gauge, + rolling_stock_supported_signaling_systems: physics_consist_parameters + .traction_engine .supported_signaling_systems .clone(), - rolling_stock: PhysicsConsistParameters { - max_speed: stdcm_request.max_speed, - total_length: stdcm_request.total_length, - total_mass: stdcm_request.total_mass, - towed_rolling_stock: stdcm_request - .get_towed_rolling_stock(&mut conn) - .await? - .map(From::from), - traction_engine: rolling_stock.into(), - } - .into(), + rolling_stock: physics_consist_parameters.into(), temporary_speed_limits: stdcm_request .get_temporary_speed_limits(&mut conn, simulation_run_time) .await?, @@ -407,7 +412,7 @@ impl VirtualTrainRun { core_client: Arc, stdcm_request: &Request, infra: &Infra, - rolling_stock: &RollingStockModel, + consist_parameters: &PhysicsConsistParameters, timetable_id: i64, ) -> Result { // Doesn't matter for now, but eventually it will affect tmp speed limits @@ -420,7 +425,7 @@ impl VirtualTrainRun { id: 0, train_name: "".to_string(), labels: vec![], - rolling_stock_name: rolling_stock.name.clone(), + rolling_stock_name: consist_parameters.traction_engine.name.clone(), timetable_id, start_time: approx_start_time, schedule: vec![ScheduleItem { @@ -441,15 +446,19 @@ impl VirtualTrainRun { options: Default::default(), }; - let (simulation, pathfinding) = train_simulation( + // Compute simulation of a train schedule + let (simulation, pathfinding) = consist_train_simulation_batch( &mut db_pool.get().await?, valkey_client, core_client, - train_schedule.clone(), infra, + &[train_schedule.clone()], + &[consist_parameters.clone()], None, ) - .await?; + .await? + .pop() + .ok_or(StdcmError::TrainSimulationFail)?; Ok(Self { train_schedule, @@ -569,7 +578,7 @@ mod tests { #[test] fn simulation_without_parameters() { let rolling_stock = create_simple_rolling_stock(); - let simulation_parameters = PhysicsConsistParameters::with_traction_engine(rolling_stock); + let simulation_parameters = PhysicsConsistParameters::from_traction_engine(rolling_stock); let physics_consist: PhysicsConsist = simulation_parameters.into(); diff --git a/editoast/src/views/train_schedule.rs b/editoast/src/views/train_schedule.rs index 3f21fb16a71..d86b609820a 100644 --- a/editoast/src/views/train_schedule.rs +++ b/editoast/src/views/train_schedule.rs @@ -14,6 +14,10 @@ use axum::extract::State; use axum::response::IntoResponse; use axum::Extension; use editoast_authz::BuiltinRole; +use editoast_derive::EditoastError; +use editoast_models::DbConnection; +use editoast_models::DbConnectionPoolV2; +use editoast_schemas::train_schedule::TrainScheduleBase; use itertools::Itertools; use serde::Deserialize; use serde::Serialize; @@ -27,6 +31,7 @@ use crate::core::pathfinding::PathfindingInputError; use crate::core::pathfinding::PathfindingNotFound; use crate::core::pathfinding::PathfindingResultSuccess; use crate::core::simulation::CompleteReportTrain; +use crate::core::simulation::PhysicsConsist; use crate::core::simulation::PhysicsConsistParameters; use crate::core::simulation::ReportTrain; use crate::core::simulation::SignalCriticalPosition; @@ -55,10 +60,6 @@ use crate::views::AuthorizationError; use crate::AppState; use crate::RollingStockModel; use crate::ValkeyClient; -use editoast_derive::EditoastError; -use editoast_models::DbConnection; -use editoast_models::DbConnectionPoolV2; -use editoast_schemas::train_schedule::TrainScheduleBase; crate::routes! { "/train_schedule" => { @@ -354,40 +355,20 @@ async fn simulation( }) .await?; - Ok(Json( - train_simulation( - &mut db_pool.get().await?, - valkey_client, - core_client, - train_schedule, - &infra, - electrical_profile_set_id, - ) - .await? - .0, - )) -} - -/// Compute simulation of a train schedule -pub async fn train_simulation( - conn: &mut DbConnection, - valkey_client: Arc, - core: Arc, - train_schedule: TrainSchedule, - infra: &Infra, - electrical_profile_set_id: Option, -) -> Result<(SimulationResponse, PathfindingResult)> { - Ok(train_simulation_batch( - conn, + // Compute simulation of a train schedule + let (simulation, _) = train_simulation_batch( + &mut db_pool.get().await?, valkey_client, - core, + core_client, &[train_schedule], - infra, + &infra, electrical_profile_set_id, ) .await? .pop() - .unwrap()) + .unwrap(); + + Ok(Json(simulation)) } /// Compute in batch the simulation of a list of train schedule @@ -401,29 +382,60 @@ pub async fn train_simulation_batch( infra: &Infra, electrical_profile_set_id: Option, ) -> Result> { - let mut valkey_conn = valkey_client.get_connection().await?; // Compute path - let (rolling_stocks, _): (Vec<_>, _) = RollingStockModel::retrieve_batch( - conn, - train_schedules - .iter() - .map::(|t| t.rolling_stock_name.clone()), - ) - .await?; - let rolling_stocks: HashMap<_, _> = rolling_stocks + let rolling_stocks_ids = train_schedules + .iter() + .map::(|t| t.rolling_stock_name.clone()); + + let (rolling_stocks, _): (Vec<_>, HashSet) = + RollingStockModel::retrieve_batch(conn, rolling_stocks_ids).await?; + + let consists: Vec = rolling_stocks .into_iter() - .map(|rs| (rs.name.clone(), rs)) + .map(|rs| PhysicsConsistParameters::from_traction_engine(rs.into())) .collect(); + + consist_train_simulation_batch( + conn, + valkey_client, + core.clone(), + infra, + train_schedules, + &consists, + electrical_profile_set_id, + ) + .await +} + +pub async fn consist_train_simulation_batch( + conn: &mut DbConnection, + valkey_client: Arc, + core: Arc, + infra: &Infra, + train_schedules: &[TrainSchedule], + consists: &[PhysicsConsistParameters], + electrical_profile_set_id: Option, +) -> Result> { + let mut valkey_conn = valkey_client.get_connection().await?; + let pathfinding_results = pathfinding_from_train_batch( conn, &mut valkey_conn, core.clone(), infra, train_schedules, - &rolling_stocks, + &consists + .iter() + .map(|consist| consist.traction_engine.clone()) + .collect::>(), ) .await?; + let consists: HashMap<_, _> = consists + .iter() + .map(|consist| (&consist.traction_engine.name, consist)) + .collect(); + let mut simulation_results = vec![SimulationResponse::default(); train_schedules.len()]; let mut to_sim = Vec::with_capacity(train_schedules.len()); for (index, (pathfinding, train_schedule)) in @@ -454,14 +466,15 @@ pub async fn train_simulation_batch( }; // Build simulation request - let rolling_stock = rolling_stocks[&train_schedule.rolling_stock_name].clone(); + let physics_consist_parameters = consists[&train_schedule.rolling_stock_name].clone(); + let simulation_request = build_simulation_request( infra, train_schedule, path_item_positions, path, - rolling_stock, electrical_profile_set_id, + physics_consist_parameters.into(), ); // Compute unique hash of the simulation input @@ -525,8 +538,8 @@ fn build_simulation_request( train_schedule: &TrainSchedule, path_item_positions: &[u64], path: SimulationPath, - rolling_stock: RollingStockModel, electrical_profile_set_id: Option, + physics_consist: PhysicsConsist, ) -> SimulationRequest { assert_eq!(path_item_positions.len(), train_schedule.path.len()); // Project path items to path offset @@ -586,7 +599,7 @@ fn build_simulation_request( speed_limit_tag: train_schedule.speed_limit_tag.clone(), power_restrictions, options: train_schedule.options.clone(), - rolling_stock: PhysicsConsistParameters::with_traction_engine(rolling_stock.into()).into(), + rolling_stock: physics_consist, electrical_profile_set_id, } } diff --git a/front/public/locales/en/errors.json b/front/public/locales/en/errors.json index 8b6dd34e806..a985172b2d7 100644 --- a/front/public/locales/en/errors.json +++ b/front/public/locales/en/errors.json @@ -191,6 +191,7 @@ "InvalidPathItems": "Invalid waypoint(s) {{items}}", "RollingStockNotFound": "Rolling stock '{{rolling_stock_id}}' does not exist", "TowedRollingStockNotFound": "Towed rolling stock {towed_rolling_stock_id} does not exist", + "TrainSimulationFail": "Train simulation failed", "TimetableNotFound": "Timetable '{{timetable_id}}' does not exist" }, "study": { diff --git a/front/public/locales/fr/errors.json b/front/public/locales/fr/errors.json index 9f41174a6aa..988881c0d9a 100644 --- a/front/public/locales/fr/errors.json +++ b/front/public/locales/fr/errors.json @@ -193,6 +193,7 @@ "InvalidPathItems": "Point(s) de passage {{items}} invalide(s)", "RollingStockNotFound": "Matériel roulant '{{rolling_stock_id}}' non trouvé", "TowedRollingStockNotFound": "Matériel roulant remorqué {towed_rolling_stock_id} non trouvé", + "TrainSimulationFail": "Échec de la simulation du train", "TimetableNotFound": "Grille horaire '{{timetable_id}}' non trouvée" }, "study": {