From d02039073990dcde36e8d268a11d6826685952e8 Mon Sep 17 00:00:00 2001 From: Youness Chrifi Alaoui Date: Wed, 24 Apr 2024 22:57:48 +0200 Subject: [PATCH] editoast: add path projection endpoint --- .../src/infra/track_offset.rs | 12 +- editoast/openapi.yaml | 215 ++++++- editoast/src/views/v2/train_schedule.rs | 356 +++-------- .../src/views/v2/train_schedule/projection.rs | 600 ++++++++++++++++++ front/src/common/api/osrdEditoastApi.ts | 88 ++- 5 files changed, 971 insertions(+), 300 deletions(-) create mode 100644 editoast/src/views/v2/train_schedule/projection.rs diff --git a/editoast/editoast_schemas/src/infra/track_offset.rs b/editoast/editoast_schemas/src/infra/track_offset.rs index 236acc0582d..9a21d38293e 100644 --- a/editoast/editoast_schemas/src/infra/track_offset.rs +++ b/editoast/editoast_schemas/src/infra/track_offset.rs @@ -7,7 +7,7 @@ editoast_common::schemas! { TrackOffset, } -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Hash)] +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, ToSchema, Hash)] pub struct TrackOffset { /// Track section identifier #[schema(inline)] @@ -15,3 +15,13 @@ pub struct TrackOffset { /// Offset in mm pub offset: u64, } + +impl TrackOffset { + /// Create a new track location. + pub fn new>(track: T, offset: u64) -> Self { + Self { + track: track.as_ref().into(), + offset, + } + } +} diff --git a/editoast/openapi.yaml b/editoast/openapi.yaml index 52268a6e43a..185424cd746 100644 --- a/editoast/openapi.yaml +++ b/editoast/openapi.yaml @@ -2403,10 +2403,10 @@ components: properties: context: properties: - number: + missing: type: integer required: - - number + - missing type: object message: type: string @@ -4958,29 +4958,123 @@ components: - $ref: '#/components/schemas/Tags' nullable: true type: object - ProjectPathResult: - description: Project path output is described by time-space points and blocks + ProjectPathInput: + description: Project path input is described by a list of routes and a list of track range properties: - blocks: - items: - $ref: '#/components/schemas/SignalUpdate' - type: array - positions: + routes: + description: List of route ids items: - format: int64 - minimum: 0 - type: integer + maxLength: 255 + minLength: 1 + type: string + minItems: 1 type: array - times: + track_section_ranges: + description: List of track ranges items: - format: double - type: number + $ref: '#/components/schemas/TrackRange' + minItems: 1 type: array required: - - positions - - times - - blocks + - routes + - track_section_ranges type: object + ProjectPathTrainResult: + allOf: + - description: Project path output is described by time-space points and blocks + properties: + signal_updates: + description: List of signal updates along the path + items: + properties: + aspect_label: + description: The labels of the new aspect + type: string + blinking: + description: Whether the signal is blinking + type: boolean + color: + description: |- + The color of the aspect + (Bits 24-31 are alpha, 16-23 are red, 8-15 are green, 0-7 are blue) + format: int32 + type: integer + position_end: + description: The route ends at this position in mm on the train path + format: int64 + minimum: 0 + type: integer + position_start: + description: The route starts at this position in mm on the train path + format: int64 + minimum: 0 + type: integer + signal_id: + description: The id of the updated signal + type: string + time_end: + description: The aspects stop being displayed at this time (number of seconds since `departure_time`) + format: int64 + minimum: 0 + type: integer + time_start: + description: The aspects start being displayed at this time (number of mseconds since `departure_time`) + format: int64 + minimum: 0 + type: integer + required: + - signal_id + - time_start + - time_end + - position_start + - position_end + - color + - blinking + - aspect_label + type: object + type: array + space_time_curves: + description: List of space-time curves sections along the path + items: + properties: + positions: + items: + format: int64 + minimum: 0 + type: integer + minItems: 2 + type: array + times: + items: + format: int64 + minimum: 0 + type: integer + minItems: 2 + type: array + required: + - positions + - times + type: object + type: array + required: + - space_time_curves + - signal_updates + type: object + - properties: + departure_time: + description: Departure time of the train + format: date-time + type: string + rolling_stock_length: + description: Rolling stock length in mm + format: int64 + minimum: 0 + type: integer + required: + - departure_time + - rolling_stock_length + type: object + description: Project path output is described by time-space points and blocks ProjectWithStudies: allOf: - $ref: '#/components/schemas/Project' @@ -6568,9 +6662,36 @@ components: base: $ref: '#/components/schemas/ReportTrainV2' final_output: - $ref: '#/components/schemas/CompleteReportTrain' + allOf: + - $ref: '#/components/schemas/ReportTrainV2' + - properties: + routing_requirements: + items: + $ref: '#/components/schemas/RoutingRequirement' + type: array + signal_sightings: + items: + $ref: '#/components/schemas/SignalSighting' + type: array + spacing_requirements: + items: + $ref: '#/components/schemas/SpacingRequirement' + type: array + zone_updates: + items: + $ref: '#/components/schemas/ZoneUpdate' + type: array + required: + - signal_sightings + - zone_updates + - spacing_requirements + - routing_requirements + type: object mrsp: - $ref: '#/components/schemas/Mrsp' + description: A MRSP computation result (Most Restrictive Speed Profile) + items: + $ref: '#/components/schemas/MrspPoint' + type: array power_restrictions: items: properties: @@ -7225,14 +7346,19 @@ components: - offset type: object TrackRange: + description: |- + An oriented range on a track section. + `begin` is always less than `end`. properties: begin: + description: The beginning of the range in mm. format: int64 minimum: 0 type: integer direction: $ref: '#/components/schemas/Direction' end: + description: The end of the range in mm. format: int64 minimum: 0 type: integer @@ -10856,14 +10982,42 @@ paths: post: description: |- Projects the space time curves and paths of a number of train schedules onto a given path - Params are the infra_id and a list of train_ids + + - Returns 404 if the infra or any of the train schedules are not found + - Returns 200 with a hashmap of train_id to ProjectPathTrainResult + + Train schedules that are invalid (pathfinding or simulation failed) are not included in the result + parameters: + - description: The infra id + in: query + name: infra + required: true + schema: + format: int64 + type: integer + - description: Ids of train schedule + in: query + name: ids + required: true + schema: + items: + format: int64 + type: integer + type: array + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectPathInput' + description: '' + required: true responses: '200': content: application/json: schema: additionalProperties: - $ref: '#/components/schemas/ProjectPathResult' + $ref: '#/components/schemas/ProjectPathTrainResult' type: object description: Project Path Output summary: Projects the space time curves and paths of a number of train schedules onto a given path @@ -10874,6 +11028,23 @@ paths: description: |- Retrieve simulation information for a given train list. Useful for finding out whether pathfinding/simulation was successful. + parameters: + - description: The infra id + in: query + name: infra + required: true + schema: + format: int64 + type: integer + - description: Ids of train schedule + in: query + name: ids + required: true + schema: + items: + format: int64 + type: integer + type: array responses: '200': content: diff --git a/editoast/src/views/v2/train_schedule.rs b/editoast/src/views/v2/train_schedule.rs index a4d141bb858..ec689d87c5e 100644 --- a/editoast/src/views/v2/train_schedule.rs +++ b/editoast/src/views/v2/train_schedule.rs @@ -1,27 +1,42 @@ +mod projection; + use std::collections::hash_map::DefaultHasher; use std::collections::HashMap; use std::collections::HashSet; use std::hash::Hash; use std::hash::Hasher; +use std::sync::Arc; +use actix_web::web::{Data, Json, Path, Query}; +use actix_web::{delete, get, post, put, HttpResponse}; use diesel_async::AsyncPgConnection as PgConnection; use editoast_derive::EditoastError; use editoast_schemas::train_schedule::TrainScheduleBase; use itertools::Itertools; +use serde::Deserialize; +use serde::Serialize; use serde_qs::actix::QsQuery; use thiserror::Error; +use tracing::info; +use utoipa::IntoParams; +use utoipa::ToSchema; +use crate::client::get_app_version; +use crate::core::v2::pathfinding::PathfindingResult; +use crate::core::v2::simulation::CompleteReportTrain; +use crate::core::v2::simulation::Mrsp; +use crate::core::v2::simulation::ReportTrain; +use crate::core::v2::simulation::SignalSighting; use crate::core::v2::simulation::SimulationMargins; use crate::core::v2::simulation::SimulationPath; use crate::core::v2::simulation::SimulationPowerRestrictionItem; +use crate::core::v2::simulation::SimulationPowerRestrictionRange; use crate::core::v2::simulation::SimulationRequest; use crate::core::v2::simulation::SimulationScheduleItem; +use crate::core::v2::simulation::ZoneUpdate; +use crate::core::AsCoreRequest; use crate::core::CoreClient; use crate::error::Result; - -use crate::client::get_app_version; -use crate::core::v2::pathfinding::PathfindingResult; -use crate::core::v2::pathfinding::TrackRange; use crate::modelsv2::infra::Infra; use crate::modelsv2::timetable::Timetable; use crate::modelsv2::train_schedule::TrainSchedule; @@ -34,22 +49,15 @@ use crate::views::v2::path::PathfindingError; use crate::DbPool; use crate::RedisClient; use crate::RollingStockModel; -use editoast_schemas::primitives::Identifier; - -use actix_web::web::{Data, Json, Path, Query}; -use actix_web::{delete, get, post, put, HttpResponse}; -use serde::{Deserialize, Serialize}; -use tracing::info; -use utoipa::{IntoParams, ToSchema}; -const CACHE_SIMULATION_EXPIRATION: u64 = 604800; +const CACHE_SIMULATION_EXPIRATION: u64 = 604800; // 1 week crate::routes! { "/v2/train_schedule" => { post, delete, simulation_summary, - project_path, + projection::routes(), "/{id}" => { get, put, @@ -68,11 +76,10 @@ editoast_common::schemas! { TrainScheduleResult, BatchDeletionRequest, SimulationResult, - ProjectPathResult, - SimulationSummaryResultResponse, + SimulationSummaryResult, CompleteReportTrain, - ReportTrain, InfraIdQueryParam, + projection::schemas(), } #[derive(Debug, Error, EditoastError)] @@ -82,9 +89,9 @@ pub enum TrainScheduleError { #[error("Train Schedule '{train_schedule_id}', could not be found")] #[editoast_error(status = 404)] NotFound { train_schedule_id: i64 }, - #[error("{number} train schedule(s) could not be found")] + #[error("{missing} train schedule(s) could not be found")] #[editoast_error(status = 404)] - BatchTrainScheduleNotFound { number: usize }, + BatchTrainScheduleNotFound { missing: usize }, #[error("Infra '{infra_id}', could not be found")] #[editoast_error(status = 404)] InfraNotFound { infra_id: i64 }, @@ -227,8 +234,8 @@ async fn delete(db_pool: Data, data: Json) -> Resu let conn = &mut db_pool.get().await?; let train_ids = data.into_inner().ids; - TrainSchedule::delete_batch_or_fail(conn, train_ids, |number| { - TrainScheduleError::BatchTrainScheduleNotFound { number } + TrainSchedule::delete_batch_or_fail(conn, train_ids, |missing| { + TrainScheduleError::BatchTrainScheduleNotFound { missing } }) .await?; @@ -277,7 +284,9 @@ enum SimulationResult { base: ReportTrain, #[schema(value_type = ReportTrainV2)] provisional: ReportTrain, + #[schema(inline)] final_output: CompleteReportTrain, + #[schema(inline)] mrsp: Mrsp, #[schema(inline)] power_restrictions: Vec, @@ -285,107 +294,7 @@ enum SimulationResult { PathfindingFailed { pathfinding_result: PathfindingResult, }, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)] -pub struct SignalSighting { - pub signal: String, - // Time in ms - pub time: u64, - pub position: u64, - pub state: String, -} - -#[derive(Deserialize, Serialize, Clone, Debug, ToSchema)] -#[schema(as = ReportTrainV2)] -pub struct ReportTrain { - // List of positions of a train - // Both positions (in mm) and times (in ms) must have the same length - positions: Vec, - times: Vec, - // List of speeds associated to a position - speeds: Vec, - energy_consumption: f64, -} - -#[derive(Deserialize, Serialize, Clone, Debug, ToSchema)] -pub struct CompleteReportTrain { - #[serde(flatten)] - #[schema(value_type = ReportTrainV2)] - report_train: ReportTrain, - signal_sightings: Vec, - zone_updates: Vec, - spacing_requirements: Vec, - routing_requirements: Vec, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)] -pub struct ZoneUpdate { - pub zone: String, - // Time in ms - pub time: u64, - pub position: u64, - // TODO: see /~https://github.com/DGEXSolutions/osrd/issues/4294 - #[serde(rename = "isEntry")] - pub is_entry: bool, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)] -pub struct SpacingRequirement { - pub zone: String, - // Time in ms - pub begin_time: u64, - // Time in ms - pub end_time: u64, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)] -pub struct RoutingRequirement { - pub route: String, - /// Time in ms - pub begin_time: u64, - pub zones: Vec, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)] -pub struct RoutingZoneRequirement { - pub zone: String, - pub entry_detector: String, - pub exit_detector: String, - pub switches: HashMap, - /// Time in ms - pub end_time: u64, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)] -pub struct SimulationPowerRestrictionRange { - /// Start position in the path in mm - begin: u64, - /// End position in the path in mm - end: u64, - code: String, - /// Is power restriction handled during simulation - handled: bool, -} - -/// A MRSP computation result (Most Restrictive Speed Profile) -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, ToSchema)] -pub struct Mrsp(pub Vec); - -/// An MRSP point -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)] -pub struct MrspPoint { - /// Relative position of the point on its path (in milimeters) - pub position: u64, - /// Speed limit at this point (in m/s) - pub speed: f64, -} - -#[derive(Debug, Clone, Hash)] -struct SimulationInput { - blocks: Vec, - routes: Vec, - track_section_ranges: Vec, + SimulationFailed, } /// Retrieve the space, speed and time curve of a given train @@ -407,6 +316,9 @@ pub async fn simulation( let infra_id = query.into_inner().infra_id; let train_schedule_id = train_schedule_id.into_inner().id; let conn = &mut db_pool.get().await?; + let db_pool = db_pool.into_inner(); + let redis_client = redis_client.into_inner(); + let core_client = core_client.into_inner(); // Retrieve infra or fail let infra = Infra::retrieve_or_fail(conn, infra_id, || TrainScheduleError::InfraNotFound { @@ -427,17 +339,23 @@ pub async fn simulation( /// Compute the simulation of a given train schedule async fn train_simulation( - db_pool: Data, - redis_client: Data, - core: Data, + db_pool: Arc, + redis_client: Arc, + core: Arc, train_schedule: &TrainSchedule, infra: &Infra, ) -> Result { let mut redis_conn = redis_client.get_connection().await?; let conn = &mut db_pool.get().await?; // Compute path - let pathfinding_result = - pathfinding_from_train(conn, &mut redis_conn, core, infra, train_schedule.clone()).await?; + let pathfinding_result = pathfinding_from_train( + conn, + &mut redis_conn, + core.clone(), + infra, + train_schedule.clone(), + ) + .await?; let (path, path_items_positions) = match pathfinding_result { PathfindingResult::Success { @@ -461,9 +379,9 @@ async fn train_simulation( // Build simulation request let simulation_request = - build_simulation_request(conn, train_schedule, &path_items_positions, path).await?; + build_simulation_request(conn, infra, train_schedule, &path_items_positions, path).await?; - // Compute unique hash of SimulationInput + // Compute unique hash of the simulation input let hash = train_simulation_input_hash(infra.id, &infra.version, &simulation_request); let result: Option = redis_conn @@ -475,66 +393,31 @@ async fn train_simulation( } // Compute simulation from core - // TODO: Implement the simulation call - - let res = SimulationResult::Success { - base: ReportTrain { - speeds: vec![10.0, 27.0], - positions: vec![20, 50], - times: vec![27500, 35500], - energy_consumption: 100.0, - }, - provisional: ReportTrain { - speeds: vec![10.0, 27.0], - positions: vec![20, 50], - times: vec![27500, 35500], - energy_consumption: 100.0, - }, - final_output: CompleteReportTrain { - report_train: ReportTrain { - speeds: vec![10.0, 27.0], - positions: vec![2000, 5000], - times: vec![27500, 35500], - energy_consumption: 100.0, - }, - signal_sightings: vec![SignalSighting { - signal: "signal.0".into(), - time: 28000, - position: 3000, - state: "VL".into(), - }], - zone_updates: vec![ZoneUpdate { - zone: "zone.0".into(), - time: 28000, - position: 3000, - is_entry: true, - }], - spacing_requirements: vec![SpacingRequirement { - zone: "zone.0".into(), - begin_time: 30000, - end_time: 35500, - }], - routing_requirements: vec![RoutingRequirement { - route: "route.0".into(), - begin_time: 32000, - zones: vec![], - }], + let result = match simulation_request.fetch(core.as_ref()).await { + Ok(response) => SimulationResult::Success { + base: response.base, + provisional: response.provisional, + final_output: response.final_output, + mrsp: response.mrsp, + power_restrictions: response.power_restrictions, }, - mrsp: Mrsp(vec![]), - power_restrictions: vec![], + // TODO: Handle simulation failure + Err(err) if err.status.as_u16() / 100 == 5 => SimulationResult::SimulationFailed, + Err(err) => return Err(err), }; // Cache the simulation response redis_conn - .json_set_ex(&hash, &res, CACHE_SIMULATION_EXPIRATION) + .json_set_ex(&hash, &result, CACHE_SIMULATION_EXPIRATION) .await?; // Return the response - Ok(res) + Ok(result) } async fn build_simulation_request( conn: &mut PgConnection, + infa: &Infra, train_schedule: &TrainSchedule, path_items_position: &[u64], path: SimulationPath, @@ -597,8 +480,9 @@ async fn build_simulation_request( .collect(); Ok(SimulationRequest { + infra: infa.id, + expected_version: infa.version.clone(), path, - start_time: train_schedule.start_time, schedule, margins, initial_speed: train_schedule.initial_speed, @@ -632,63 +516,8 @@ struct SimulationBatchParams { ids: Vec, } -/// Project path output is described by time-space points and blocks -#[derive(Debug, Deserialize, Serialize, ToSchema)] -pub struct ProjectPathResult { - // List of positions of a train - // Both positions and times must have the same length - positions: Vec, - // List of times associated to a position - times: Vec, - // List of blocks that are in the path - blocks: Vec, -} - -#[derive(Debug, Serialize, Deserialize, PartialEq, ToSchema)] -pub struct SignalUpdate { - /// The id of the updated signal - pub signal_id: String, - /// The aspects start being displayed at this time (number of seconds since 1970-01-01T00:00:00) - pub time_start: f64, - /// The aspects stop being displayed at this time (number of seconds since 1970-01-01T00:00:00) - #[schema(required)] - pub time_end: Option, - /// The route starts at this position on the train path - pub position_start: u64, - /// The route ends at this position on the train path - #[schema(required)] - pub position_end: Option, - /// The color of the aspect - /// (Bits 24-31 are alpha, 16-23 are red, 8-15 are green, 0-7 are blue) - pub color: i32, - /// Whether the signal is blinking - pub blinking: bool, - /// The labels of the new aspect - pub aspect_label: String, -} - -/// Projects the space time curves and paths of a number of train schedules onto a given path -/// Params are the infra_id and a list of train_ids -#[utoipa::path( - tag = "train_schedulev2", - responses( - (status = 200, description = "Project Path Output", body = HashMap), - ), -)] -#[post("/project_path")] -pub async fn project_path( - _db_pool: Data, - _redis_client: Data, - _params: QsQuery, - _core_client: Data, -) -> Result>> { - // TO DO - // issue: /~https://github.com/OpenRailAssociation/osrd/issues/6858 - Ok(Json(HashMap::new())) -} - #[derive(Debug, Serialize, ToSchema)] -enum SimulationSummaryResultResponse { +enum SimulationSummaryResult { // Minimal information on a simulation's result Success { // Length of a path in mm @@ -698,16 +527,20 @@ enum SimulationSummaryResultResponse { // Total energy consumption of a train in kWh energy_consumption: f64, }, - // Pathfinding fails for a train id + // Pathfinding failed PathfindingFailed, - // Running time fails or a train id - RunningTimeFailed, + // Simulation failed + SimulationFailed, } /// Retrieve simulation information for a given train list. /// Useful for finding out whether pathfinding/simulation was successful. #[utoipa::path( tag = "train_schedulev2", + params( + ("infra" = i64, Query, description = "The infra id"), + ("ids" = Vec, Query, description = "Ids of train schedule"), + ), responses( (status = 200, description = "Project Path Output", body = HashMap), ), @@ -718,10 +551,13 @@ pub async fn simulation_summary( redis_client: Data, params: QsQuery, core: Data, -) -> Result>> { +) -> Result>> { let query_props = params.into_inner(); let infra_id = query_props.infra; let conn = &mut db_pool.clone().get().await?; + let db_pool = db_pool.into_inner(); + let redis_client = redis_client.into_inner(); + let core_client = core_client.into_inner(); let infra = Infra::retrieve_or_fail(conn, infra_id, || TrainScheduleError::InfraNotFound { infra_id, @@ -731,27 +567,28 @@ pub async fn simulation_summary( let train_schedule_batch: Vec = TrainSchedule::retrieve_batch_or_fail(conn, train_ids, |missing| { TrainScheduleError::BatchTrainScheduleNotFound { - number: missing.len(), + missing: missing.len(), } }) .await?; + + // Build a HashMap with train_ids as keys and the corresponding simulation response as values Ok(Json( build_simulation_summary_map(db_pool, redis_client, core, &train_schedule_batch, &infra) - .await, + .await?, )) } /// Associate each train id with its simulation summary response /// If the simulation fails, it associates the reason: pathfinding failed or running time failed async fn build_simulation_summary_map( - db_pool: Data, - redis_client: Data, - core_client: Data, + db_pool: Arc, + redis_client: Arc, + core_client: Arc, train_schedule_batch: &[TrainSchedule], infra: &Infra, -) -> HashMap { - let mut simulation_summary_hashmap: HashMap = - HashMap::new(); +) -> Result> { + let mut simulation_summary_hashmap = HashMap::new(); for train_schedule in train_schedule_batch.iter() { let simulation_result = train_simulation( db_pool.clone(), @@ -760,26 +597,24 @@ async fn build_simulation_summary_map( train_schedule, infra, ) - .await; - let simulation_summary_result = if let Ok(train_simulation) = simulation_result { - match train_simulation { - SimulationResult::Success { final_output, .. } => { - SimulationSummaryResultResponse::Success { - length: *final_output.report_train.positions.last().unwrap(), - time: *final_output.report_train.times.last().unwrap(), - energy_consumption: final_output.report_train.energy_consumption, - } - } - SimulationResult::PathfindingFailed { .. } => { - SimulationSummaryResultResponse::PathfindingFailed + .await?; + let simulation_summary_result = match simulation_result { + SimulationResult::Success { final_output, .. } => { + let report = final_output.report_train; + SimulationSummaryResult::Success { + length: *report.positions.last().unwrap(), + time: *report.times.last().unwrap(), + energy_consumption: report.energy_consumption, } } - } else { - SimulationSummaryResultResponse::RunningTimeFailed + SimulationResult::PathfindingFailed { .. } => { + SimulationSummaryResult::PathfindingFailed + } + SimulationResult::SimulationFailed => SimulationSummaryResult::SimulationFailed, }; simulation_summary_hashmap.insert(train_schedule.id, simulation_summary_result); } - simulation_summary_hashmap + Ok(simulation_summary_hashmap) } #[derive(Debug, Default, Clone, Serialize, Deserialize, IntoParams, ToSchema)] @@ -806,6 +641,7 @@ async fn get_path( ) -> Result> { let conn = &mut db_pool.get().await?; let mut redis_conn = redis_client.get_connection().await?; + let core = core.into_inner(); let inner_query = query.into_inner(); let infra_id = inner_query.infra_id; diff --git a/editoast/src/views/v2/train_schedule/projection.rs b/editoast/src/views/v2/train_schedule/projection.rs new file mode 100644 index 00000000000..598b3994956 --- /dev/null +++ b/editoast/src/views/v2/train_schedule/projection.rs @@ -0,0 +1,600 @@ +use actix_web::post; +use actix_web::web::Data; +use actix_web::web::Json; +use chrono::DateTime; +use chrono::Utc; +use editoast_schemas::primitives::Identifier; +use futures::join; +use serde::Deserialize; +use serde::Serialize; +use serde_qs::actix::QsQuery; +use std::collections::hash_map::DefaultHasher; +use std::collections::HashMap; +use std::collections::HashSet; +use std::hash::Hash; +use std::hash::Hasher; +use std::sync::Arc; +use tracing::info; +use utoipa::ToSchema; + +use super::train_simulation; +use super::SimulationBatchParams; +use super::SimulationResult; +use super::TrainScheduleError; +use crate::client::get_app_version; +use crate::core::v2::pathfinding::PathfindingResult; +use crate::core::v2::pathfinding::TrackRange; +use crate::core::v2::signal_updates::SignalUpdate; +use crate::core::CoreClient; +use crate::error::Result; +use crate::modelsv2::infra::Infra; +use crate::modelsv2::train_schedule::TrainSchedule; +use crate::modelsv2::Retrieve; +use crate::modelsv2::RetrieveBatch; +use crate::views::v2::path::pathfinding_from_train; +use crate::views::v2::path::projection::PathProjection; +use crate::views::v2::path::projection::TrackLocationFromPath; +use crate::views::v2::train_schedule::CompleteReportTrain; +use crate::views::v2::train_schedule::ReportTrain; +use crate::views::v2::train_schedule::SignalSighting; +use crate::views::v2::train_schedule::ZoneUpdate; + +use crate::DbPool; +use crate::RedisClient; +use crate::RollingStockModel; + +const CACHE_PROJECTION_EXPIRATION: u64 = 604800; // 1 week + +editoast_common::schemas! { + ProjectPathTrainResult, + ProjectPathInput, +} +crate::routes! { + project_path, +} + +/// Project path input is described by a list of routes and a list of track range +#[derive(Debug, Deserialize, Serialize, ToSchema)] +struct ProjectPathInput { + /// List of route ids + #[schema(inline, min_items = 1)] + routes: Vec, + /// List of track ranges + #[schema(min_items = 1)] + track_section_ranges: Vec, +} + +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +struct SpaceTimeCurve { + // List of positions of a train in mm + // Both positions and times must have the same length + #[schema(min_items = 2)] + positions: Vec, + // List of times in ms since `departure_time` associated to a position + #[schema(min_items = 2)] + times: Vec, +} + +/// Project path output is described by time-space points and blocks +#[derive(Debug, Deserialize, Serialize, ToSchema)] +struct ProjectPathTrainResult { + /// Departure time of the train + departure_time: DateTime, + /// Rolling stock length in mm + rolling_stock_length: u64, + #[serde(flatten)] + #[schema(inline)] + cached: CachedProjectPathTrainResult, +} + +/// Project path output is described by time-space points and blocks +#[derive(Debug, Deserialize, Serialize, ToSchema)] +struct CachedProjectPathTrainResult { + /// List of space-time curves sections along the path + #[schema(inline)] + space_time_curves: Vec, + /// List of signal updates along the path + #[schema(inline)] + signal_updates: Vec, +} + +/// Projects the space time curves and paths of a number of train schedules onto a given path +/// +/// - Returns 404 if the infra or any of the train schedules are not found +/// - Returns 200 with a hashmap of train_id to ProjectPathTrainResult +/// +/// Train schedules that are invalid (pathfinding or simulation failed) are not included in the result +#[utoipa::path( + tag = "train_schedulev2", + params( + ("infra" = i64, Query, description = "The infra id"), + ("ids" = Vec, Query, description = "Ids of train schedule"), + ), + responses( + (status = 200, description = "Project Path Output", body = HashMap), + ), +)] +#[post("/project_path")] +async fn project_path( + db_pool: Data, + redis_client: Data, + core_client: Data, + params: QsQuery, + data: Json, +) -> Result>> { + let ProjectPathInput { + track_section_ranges: path_track_ranges, + .. + } = data.into_inner(); + let path_projection = PathProjection::new(&path_track_ranges); + let query_props = params.into_inner(); + let train_ids = query_props.ids; + let infra_id = query_props.infra; + let db_pool = db_pool.into_inner(); + let redis_client = redis_client.into_inner(); + let conn = &mut db_pool.clone().get().await?; + let mut redis_conn = redis_client.get_connection().await?; + let core = core_client.into_inner(); + + let infra = Infra::retrieve_or_fail(conn, infra_id, || TrainScheduleError::InfraNotFound { + infra_id, + }) + .await?; + + let train_schedule_batch: Vec = + TrainSchedule::retrieve_batch_or_fail(conn, train_ids, |missing| { + TrainScheduleError::BatchTrainScheduleNotFound { + missing: missing.len(), + } + }) + .await?; + let train_map: HashMap = train_schedule_batch + .into_iter() + .map(|ts| (ts.id, ts)) + .collect(); + + // 1. Retrieve cached projection + let mut hit_cache: HashMap = HashMap::new(); + let mut miss_cache = HashMap::new(); + + for (train_id, train) in &train_map { + let pathfinding_result = + pathfinding_from_train(conn, &mut redis_conn, core.clone(), &infra, train.clone()) + .await?; + + let track_ranges = match pathfinding_result { + PathfindingResult::Success { + track_section_ranges, + .. + } => track_section_ranges, + _ => continue, + }; + + let simulation_result = train_simulation( + db_pool.clone(), + redis_client.clone(), + core.clone(), + train, + &infra, + ) + .await?; + let CompleteReportTrain { + report_train, + signal_sightings, + zone_updates, + .. + } = match simulation_result { + SimulationResult::Success { final_output, .. } => final_output, + _ => continue, + }; + let ReportTrain { + times, positions, .. + } = report_train; + + let train_details = TrainSimulationDetails { + positions, + times, + signal_sightings, + zone_updates, + train_path: track_ranges, + }; + + let hash = train_projection_input_hash( + infra.id, + &infra.version, + &train_details, + &path_track_ranges, + ); + let projection: Option = redis_conn + .json_get_ex(&hash, CACHE_PROJECTION_EXPIRATION) + .await?; + if let Some(cached) = projection { + hit_cache.insert(*train_id, cached); + } else { + miss_cache.insert(*train_id, train_details); + } + } + info!( + "{}/{} hit cache", + hit_cache.len(), + hit_cache.len() + miss_cache.len() + ); + + // 2 Compute space time curves and signal updates for all miss cache + let (space_time_curves, signal_updates) = join!( + compute_batch_space_time_curves(&miss_cache, &path_projection), + compute_batch_signal_updates(core.clone(), &miss_cache) + ); + + // 3. Store the projection in the cache + for (id, train_details) in miss_cache { + let hash = train_projection_input_hash( + infra.id, + &infra.version, + &train_details, + &path_track_ranges, + ); + let cached = CachedProjectPathTrainResult { + space_time_curves: space_time_curves + .get(&id) + .expect("Space time curves not availabe for train") + .clone(), + signal_updates: signal_updates + .get(&id) + .expect("Signal update not availabe for train") + .clone(), + }; + redis_conn + .json_set_ex(&hash, &cached, CACHE_PROJECTION_EXPIRATION) + .await?; + hit_cache.insert(id, cached); + } + + // 4.1 Fetch rolling stock length + let mut project_path_result = HashMap::new(); + let rolling_stock_names: HashSet<_> = hit_cache + .keys() + .map(|id| train_map.get(id).unwrap().rolling_stock_name.clone()) + .collect(); + let (rs, missing): (Vec<_>, _) = + RollingStockModel::retrieve_batch(conn, rolling_stock_names).await?; + assert!(missing.is_empty(), "Missing rolling stock models"); + let rolling_stock_length: HashMap<_, _> = + rs.into_iter().map(|rs| (rs.name, rs.length)).collect(); + + // 4.2 Build the projection response + for (id, cached) in hit_cache { + let train = train_map.get(&id).expect("Train not found"); + let length = rolling_stock_length + .get(&train.rolling_stock_name) + .expect("Rolling stock length not found"); + + project_path_result.insert( + id, + ProjectPathTrainResult { + departure_time: train.start_time, + rolling_stock_length: (length * 1000.).round() as u64, + cached, + }, + ); + } + + Ok(Json(project_path_result)) +} + +/// Input for the projection of a train schedule on a path +#[derive(Debug, Clone, Hash)] +struct TrainSimulationDetails { + positions: Vec, + times: Vec, + train_path: Vec, + signal_sightings: Vec, + zone_updates: Vec, +} + +/// Compute the signal updates of a list of train schedules +async fn compute_batch_signal_updates( + _core_client: Arc, + trains_details: &HashMap, +) -> HashMap> { + // TODO: Wait for core implementation + trains_details.keys().map(|id| (*id, vec![])).collect() +} + +/// Compute space time curves of a list of train schedules +async fn compute_batch_space_time_curves<'a>( + trains_details: &HashMap, + path_projection: &PathProjection<'a>, +) -> HashMap> { + let mut space_time_curves = HashMap::new(); + + for (train_id, train_detail) in trains_details { + space_time_curves.insert( + *train_id, + compute_space_time_curves(train_detail, path_projection), + ); + } + space_time_curves +} + +/// Compute the space time curves of a train schedule on a path +fn compute_space_time_curves( + project_path_input: &TrainSimulationDetails, + path_projection: &PathProjection, +) -> Vec { + let train_path = PathProjection::new(&project_path_input.train_path); + let intersections = path_projection.get_intersections(&project_path_input.train_path); + let positions = &project_path_input.positions; + let times = &project_path_input.times; + + assert_eq!(positions[0], 0); + assert_eq!(positions[positions.len() - 1], train_path.len()); + assert_eq!(positions.len(), times.len()); + + let mut space_time_curves = vec![]; + for intersection in intersections { + let (start, end) = intersection; + let start_index = find_index_upper(positions, start); + let end_index = find_index_upper(positions, end); + + // Each segment contains the start, end and all positions between them + // We must interpolate the start and end positions if they are not part of the positions + let mut segment_positions = Vec::with_capacity(end_index - start_index + 2); + let mut segment_times = Vec::with_capacity(end_index - start_index + 2); + if positions[start_index] > start { + // Interpolate the first point of the segment + segment_positions.push(project_pos(start, &train_path, path_projection)); + segment_times.push(interpolate( + positions[start_index - 1], + positions[start_index], + times[start_index - 1], + times[start_index], + start, + )); + } + + // Project all the points in the segment + for index in start_index..end_index { + segment_positions.push(project_pos(positions[index], &train_path, path_projection)); + segment_times.push(times[index]); + } + + // Interpolate the last point of the segment + segment_positions.push(project_pos(end, &train_path, path_projection)); + segment_times.push(interpolate( + positions[end_index - 1], + positions[end_index], + times[end_index - 1], + times[end_index], + end, + )); + space_time_curves.push(SpaceTimeCurve { + positions: segment_positions, + times: segment_times, + }); + } + space_time_curves +} + +/// Find the index of the first element greater to a value +/// +/// **Values must be sorted in ascending order** +/// +/// ## Panics +/// +/// - If value is greater than the last element of values. +/// - If values is empty +fn find_index_upper(values: &[u64], value: u64) -> usize { + assert!(!values.is_empty(), "Values can't be empty"); + assert!( + value <= values[values.len() - 1], + "Value can't be greater than the last element" + ); + // Binary search that retrieve the smallest index of the first element greater than value + let mut left = 0; + let mut right = values.len(); + while left < right { + let mid = (left + right) / 2; + if values[mid] > value { + right = mid; + } else { + left = mid + 1; + } + } + if values[right - 1] == value { + right - 1 + } else { + right + } +} + +/// Project a position on a train path to a position on a projection path +/// +/// ## Panics +/// +/// Panics if the position is not part of **both** paths +fn project_pos( + train_pos: u64, + train_path: &PathProjection, + path_projection: &PathProjection, +) -> u64 { + match train_path.get_location(train_pos) { + TrackLocationFromPath::One(loc) => path_projection + .get_position(&loc) + .expect("Position should be in the projection path"), + TrackLocationFromPath::Two(loc_a, loc_b) => { + path_projection.get_position(&loc_a).unwrap_or_else(|| { + path_projection + .get_position(&loc_b) + .expect("Position should be in the projection path") + }) + } + } +} + +/// Interpolate a time value between two positions +fn interpolate( + start_pos: u64, + end_pos: u64, + start_time: u64, + end_time: u64, + pos_to_interpolate: u64, +) -> u64 { + if start_pos == end_pos { + start_time + } else { + start_time + + (pos_to_interpolate - start_pos) * (end_time - start_time) / (end_pos - start_pos) + } +} + +// Compute hash input of the projection of a train schedule on a path +fn train_projection_input_hash( + infra_id: i64, + infra_version: &String, + project_path_input: &TrainSimulationDetails, + path_projection_tracks: &[TrackRange], +) -> String { + let osrd_version = get_app_version().unwrap_or_default(); + let mut hasher = DefaultHasher::new(); + project_path_input.hash(&mut hasher); + path_projection_tracks.hash(&mut hasher); + let hash_simulation_input = hasher.finish(); + format!("projection_{osrd_version}.{infra_id}.{infra_version}.{hash_simulation_input}") +} + +#[cfg(test)] +mod tests { + use super::*; + use editoast_schemas::infra::Direction; + use rstest::rstest; + + #[rstest] + #[case(1, 0)] + #[case(2, 1)] + #[case(3, 1)] + #[case(4, 2)] + #[case(5, 3)] + #[case(6, 4)] + #[case(7, 4)] + #[case(8, 5)] + #[case(9, 6)] + fn test_find_index_upper(#[case] value: u64, #[case] expected: usize) { + let values = vec![1, 3, 4, 5, 7, 8, 9]; + assert_eq!(find_index_upper(&values, value), expected); + } + + #[rstest] + fn test_compute_space_time_curves_case_1() { + let positions: Vec = vec![0, 100, 200, 300, 400, 600, 730, 1000]; + let times: Vec = vec![0, 10, 20, 30, 40, 50, 70, 90]; + let path = vec![ + TrackRange::new("A", 0, 100, Direction::StartToStop), + TrackRange::new("B", 0, 200, Direction::StopToStart), + TrackRange::new("C", 0, 300, Direction::StartToStop), + TrackRange::new("D", 120, 250, Direction::StopToStart), + ]; + let path_projection = PathProjection::new(&path); + + let train_path = vec![ + TrackRange::new("A", 0, 100, Direction::StartToStop), + TrackRange::new("B", 0, 200, Direction::StopToStart), + TrackRange::new("C", 0, 300, Direction::StartToStop), + TrackRange::new("D", 0, 250, Direction::StopToStart), + TrackRange::new("E", 0, 150, Direction::StartToStop), + ]; + + let project_path_input = TrainSimulationDetails { + positions, + times, + train_path, + signal_sightings: vec![], + zone_updates: vec![], + }; + + let space_time_curves = compute_space_time_curves(&project_path_input, &path_projection); + assert_eq!(space_time_curves.clone().len(), 1); + let curve = &space_time_curves[0]; + assert_eq!(curve.times.len(), curve.positions.len()); + assert_eq!(curve.positions, vec![0, 100, 200, 300, 400, 600, 730]); + } + + #[rstest] + fn test_compute_space_time_curves_case_2() { + let positions: Vec = vec![0, 100, 200, 300, 400, 730]; + let times: Vec = vec![0, 10, 20, 30, 40, 70]; + let path = vec![ + TrackRange::new("A", 0, 100, Direction::StartToStop), + TrackRange::new("B", 0, 200, Direction::StopToStart), + TrackRange::new("C", 0, 300, Direction::StartToStop), + TrackRange::new("D", 120, 250, Direction::StopToStart), + ]; + let path_projection = PathProjection::new(&path); + + let train_path = vec![ + TrackRange::new("A", 0, 100, Direction::StartToStop), + TrackRange::new("B", 0, 200, Direction::StopToStart), + TrackRange::new("C", 0, 300, Direction::StartToStop), + TrackRange::new("D", 120, 250, Direction::StopToStart), + ]; + + let project_path_input = TrainSimulationDetails { + positions: positions.clone(), + times: times.clone(), + train_path, + signal_sightings: vec![], + zone_updates: vec![], + }; + + let space_time_curves = compute_space_time_curves(&project_path_input, &path_projection); + assert_eq!(space_time_curves.clone().len(), 1); + let curve = &space_time_curves[0]; + assert_eq!(curve.positions, positions); + assert_eq!(curve.times, times); + } + + #[rstest] + fn test_compute_space_time_curves_case_3() { + let positions: Vec = vec![0, 100, 200, 300, 400, 450, 500, 600, 720]; + let times: Vec = vec![0, 10, 20, 30, 40, 50, 60, 70, 80]; + let train_path = vec![ + TrackRange::new("A", 50, 100, Direction::StartToStop), + TrackRange::new("B", 0, 200, Direction::StartToStop), + TrackRange::new("X", 0, 100, Direction::StartToStop), + TrackRange::new("C", 0, 200, Direction::StopToStart), + TrackRange::new("Z", 0, 100, Direction::StartToStop), + TrackRange::new("E", 30, 100, Direction::StartToStop), + ]; + + let path = vec![ + TrackRange::new("A", 0, 100, Direction::StartToStop), + TrackRange::new("B", 0, 200, Direction::StartToStop), + TrackRange::new("C", 0, 300, Direction::StartToStop), + TrackRange::new("D", 0, 250, Direction::StopToStart), + TrackRange::new("E", 25, 100, Direction::StopToStart), + ]; + let path_projection = PathProjection::new(&path); + + let project_path_input = TrainSimulationDetails { + positions, + times, + train_path, + signal_sightings: vec![], + zone_updates: vec![], + }; + + let space_time_curves = compute_space_time_curves(&project_path_input, &path_projection); + assert_eq!(space_time_curves.clone().len(), 3); + let curve = &space_time_curves[0]; + assert_eq!(curve.positions, vec![50, 150, 250, 300]); + assert_eq!(curve.times, vec![0, 10, 20, 25]); + + let curve = &space_time_curves[1]; + assert_eq!(curve.positions, vec![500, 450, 400, 350, 300]); + assert_eq!(curve.times, vec![35, 40, 50, 60, 65]); + + let curve = &space_time_curves[2]; + assert_eq!(curve.positions, vec![920, 850]); + assert_eq!(curve.times, vec![74, 80]); + } +} diff --git a/front/src/common/api/osrdEditoastApi.ts b/front/src/common/api/osrdEditoastApi.ts index 7c2fa337d0e..dd5e11258d7 100644 --- a/front/src/common/api/osrdEditoastApi.ts +++ b/front/src/common/api/osrdEditoastApi.ts @@ -885,14 +885,22 @@ const injectedRtkApi = api PostV2TrainScheduleProjectPathApiResponse, PostV2TrainScheduleProjectPathApiArg >({ - query: () => ({ url: `/v2/train_schedule/project_path/`, method: 'POST' }), + query: (queryArg) => ({ + url: `/v2/train_schedule/project_path/`, + method: 'POST', + body: queryArg.projectPathInput, + params: { infra: queryArg.infra, ids: queryArg.ids }, + }), invalidatesTags: ['train_schedulev2'], }), getV2TrainScheduleSimulationSummary: build.query< GetV2TrainScheduleSimulationSummaryApiResponse, GetV2TrainScheduleSimulationSummaryApiArg >({ - query: () => ({ url: `/v2/train_schedule/simulation_summary/` }), + query: (queryArg) => ({ + url: `/v2/train_schedule/simulation_summary/`, + params: { infra: queryArg.infra, ids: queryArg.ids }, + }), providesTags: ['train_schedulev2'], }), getV2TrainScheduleById: build.query< @@ -1693,13 +1701,24 @@ export type PostV2TrainScheduleApiArg = { body: TrainScheduleForm[]; }; export type PostV2TrainScheduleProjectPathApiResponse = /** status 200 Project Path Output */ { - [key: string]: ProjectPathResult; + [key: string]: ProjectPathTrainResult; +}; +export type PostV2TrainScheduleProjectPathApiArg = { + /** The infra id */ + infra: number; + /** Ids of train schedule */ + ids: number[]; + projectPathInput: ProjectPathInput; }; -export type PostV2TrainScheduleProjectPathApiArg = void; export type GetV2TrainScheduleSimulationSummaryApiResponse = /** status 200 Project Path Output */ { [key: string]: SimulationSummaryResultResponse; }; -export type GetV2TrainScheduleSimulationSummaryApiArg = void; +export type GetV2TrainScheduleSimulationSummaryApiArg = { + /** The infra id */ + infra: number; + /** Ids of train schedule */ + ids: number[]; +}; export type GetV2TrainScheduleByIdApiResponse = /** status 200 The train schedule */ TrainScheduleResult; export type GetV2TrainScheduleByIdApiArg = { @@ -1753,8 +1772,10 @@ export type LightElectricalProfileSet = { export type LevelValues = string[]; export type Direction = 'START_TO_STOP' | 'STOP_TO_START'; export type TrackRange = { + /** The beginning of the range in mm. */ begin: number; direction: Direction; + /** The end of the range in mm. */ end: number; track_section: string; }; @@ -3237,10 +3258,43 @@ export type TrainScheduleResult = TrainScheduleBase & { export type TrainScheduleForm = TrainScheduleBase & { timetable_id: number; }; -export type ProjectPathResult = { - blocks: SignalUpdate[]; - positions: number[]; - times: number[]; +export type ProjectPathTrainResult = { + /** List of signal updates along the path */ + signal_updates: { + /** The labels of the new aspect */ + aspect_label: string; + /** Whether the signal is blinking */ + blinking: boolean; + /** The color of the aspect + (Bits 24-31 are alpha, 16-23 are red, 8-15 are green, 0-7 are blue) */ + color: number; + /** The route ends at this position in mm on the train path */ + position_end: number; + /** The route starts at this position in mm on the train path */ + position_start: number; + /** The id of the updated signal */ + signal_id: string; + /** The aspects stop being displayed at this time (number of seconds since `departure_time`) */ + time_end: number; + /** The aspects start being displayed at this time (number of mseconds since `departure_time`) */ + time_start: number; + }[]; + /** List of space-time curves sections along the path */ + space_time_curves: { + positions: number[]; + times: number[]; + }[]; +} & { + /** Departure time of the train */ + departure_time: string; + /** Rolling stock length in mm */ + rolling_stock_length: number; +}; +export type ProjectPathInput = { + /** List of route ids */ + routes: string[]; + /** List of track ranges */ + track_section_ranges: TrackRange[]; }; export type SimulationSummaryResultResponse = | { @@ -3258,17 +3312,17 @@ export type ReportTrainV2 = { speeds: number[]; times: number[]; }; -export type CompleteReportTrain = ReportTrainV2 & { - routing_requirements: RoutingRequirement[]; - signal_sightings: SignalSighting[]; - spacing_requirements: SpacingRequirement[]; - zone_updates: ZoneUpdate[]; -}; export type SimulationResult = | { base: ReportTrainV2; - final_output: CompleteReportTrain; - mrsp: Mrsp; + final_output: ReportTrainV2 & { + routing_requirements: RoutingRequirement[]; + signal_sightings: SignalSighting[]; + spacing_requirements: SpacingRequirement[]; + zone_updates: ZoneUpdate[]; + }; + /** A MRSP computation result (Most Restrictive Speed Profile) */ + mrsp: MrspPoint[]; power_restrictions: { /** Start position in the path in mm */ begin: number;