From 725989967a4f7183deff6d8598a8e6e41c80bfd6 Mon Sep 17 00:00:00 2001 From: Angelina Kuntz Date: Tue, 11 Aug 2026 09:42:28 +0200 Subject: [PATCH] editoast: adapt api to forward can_backtrack from front to editoast Signed-off-by: Angelina Kuntz --- editoast/openapi.yaml | 46 +-- editoast/src/views/path/pathfinding.rs | 306 +++++++++++------- editoast/src/views/timetable/stdcm.rs | 74 +++-- editoast/src/views/timetable/stdcm/request.rs | 20 +- .../src/applications/stdcm/hooks/useStdcm.ts | 2 +- .../stdcm/utils/formatStdcmConf.ts | 8 +- front/src/common/api/generatedEditoastApi.ts | 14 +- .../pathfinding/hooks/usePathfindingV2.ts | 2 +- front/src/modules/pathfinding/utils.ts | 5 +- osrd_schemas/osrd_schemas/models.py | 130 ++++---- tests/conftest.py | 18 +- tests/fuzzer/fuzzer.py | 11 +- tests/fuzzer/fuzzer_stdcm_single_timetable.py | 13 +- .../coasting_not_intersecting_v2.json | 55 ++-- .../stdcm_conflict_in_result.json | 56 ++-- .../stdcm_timetable_conflict.json | 44 ++- .../stdcm_zone_transition_near_start.json | 44 ++- tests/tests/test_pathfinding.py | 24 +- tests/tests/test_stdcm.py | 80 ++++- 19 files changed, 595 insertions(+), 357 deletions(-) diff --git a/editoast/openapi.yaml b/editoast/openapi.yaml index 74b276e2845..ec39ea0d7bd 100644 --- a/editoast/openapi.yaml +++ b/editoast/openapi.yaml @@ -3652,7 +3652,7 @@ paths: steps: type: array items: - $ref: '#/components/schemas/PathfindingItem' + $ref: '#/components/schemas/StdcmPathfindingItem' temporary_speed_limit_group_id: type: - integer @@ -13061,16 +13061,10 @@ components: type: string description: Set of authorized track section ids, empty means no restriction uniqueItems: true - can_backtrack_path_items: - type: array - items: - type: integer - minimum: 0 - description: Indexes, in `path_items`, of the waypoints where the train is allowed to backtrack path_items: type: array items: - $ref: '#/components/schemas/PathItemLocation' + $ref: '#/components/schemas/PathfindingItem' description: List of waypoints given to the pathfinding rolling_stock_is_thermal: type: boolean @@ -13117,22 +13111,12 @@ components: type: object required: - location + - can_backtrack properties: - duration: - type: - - integer - - 'null' - format: int64 - description: The stop duration in milliseconds, None if the train does not stop. - minimum: 0 + can_backtrack: + type: boolean location: $ref: '#/components/schemas/PathItemLocation' - description: The associated location - timing_data: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/StepTimingData' - description: Time at which the train should arrive at the location, if specified PathfindingOutput: type: object required: @@ -15541,6 +15525,26 @@ components: description: |- For calendar timetables: elapsed ms since 1970-01-01T00:00:00Z. For hourly timetables: elapsed ms since the timetable start. + StdcmPathfindingItem: + type: object + required: + - pathfinding_item + properties: + duration: + type: + - integer + - 'null' + format: int64 + description: The stop duration in milliseconds, None if the train does not stop. + minimum: 0 + pathfinding_item: + $ref: '#/components/schemas/PathfindingItem' + description: The associated location + timing_data: + oneOf: + - type: 'null' + - $ref: '#/components/schemas/StepTimingData' + description: Time at which the train should arrive at the location, if specified StdcmProgressionEvent: type: object required: diff --git a/editoast/src/views/path/pathfinding.rs b/editoast/src/views/path/pathfinding.rs index b13c9f6d4bc..ac2f125a82e 100644 --- a/editoast/src/views/path/pathfinding.rs +++ b/editoast/src/views/path/pathfinding.rs @@ -1,5 +1,6 @@ use std::collections::BTreeSet; use std::collections::HashMap; +use std::collections::HashSet; use std::collections::hash_map::DefaultHasher; use std::hash::Hash; use std::hash::Hasher; @@ -41,12 +42,9 @@ use crate::views::path::operational_point_cache::OperationalPointCache; use crate::views::timetable::PhysicsConsistParameters; use models::Infra; use models::prelude::*; -use serde_with::DefaultOnNull; -use serde_with::serde_as; /// Path input is described by some rolling stock information /// and a list of path waypoints -#[serde_as] #[derive(Deserialize, Clone, Debug, Hash, ToSchema)] #[cfg_attr(test, derive(Serialize))] pub(in crate::views) struct PathfindingInput { @@ -60,7 +58,7 @@ pub(in crate::views) struct PathfindingInput { /// List of supported signaling systems rolling_stock_supported_signaling_systems: BTreeSet, /// List of waypoints given to the pathfinding - path_items: Vec, + path_items: Vec, /// Rolling stock maximum speed #[schema(value_type = f64)] rolling_stock_maximum_speed: OrderedFloat, @@ -74,10 +72,12 @@ pub(in crate::views) struct PathfindingInput { /// Set of authorized track section ids, empty means no restriction #[serde(default)] allowed_track_sections: BTreeSet, - /// Indexes, in `path_items`, of the waypoints where the train is allowed to backtrack - #[serde(default)] - #[serde_as(as = "DefaultOnNull")] - pub can_backtrack_path_items: Vec, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema, Hash)] +pub(crate) struct PathfindingItem { + pub(crate) location: PathItemLocation, + pub(crate) can_backtrack: bool, } impl PathfindingInput { @@ -85,6 +85,12 @@ impl PathfindingInput { consist: &PhysicsConsistParameters, train_schedule: &impl TrainScheduleLike, ) -> Self { + let can_backtracks: HashSet<_> = train_schedule + .schedule() + .iter() + .filter(|item| item.can_backtrack) + .map(|item| &item.at) + .collect(); Self { rolling_stock_loading_gauge: consist.compute_loading_gauge(), rolling_stock_is_thermal: consist.traction_engine.effort_curves.has_thermal_curves(), @@ -99,16 +105,17 @@ impl PathfindingInput { consist.compute_max_speed(), )), rolling_stock_length: units::millimeter::from(consist.compute_length()).round() as u64, - path_items: train_schedule.locations(), + path_items: train_schedule + .path() + .iter() + .map(|path_item| PathfindingItem { + location: path_item.location.clone(), + can_backtrack: can_backtracks.contains(&path_item.id), + }) + .collect(), speed_limit_tag: train_schedule.speed_limit_tag().cloned(), stops_at_end_of_block: Some(train_schedule.options().stops_at_end_of_block()), allowed_track_sections: BTreeSet::new(), - can_backtrack_path_items: train_schedule - .schedule() - .iter() - .enumerate() - .filter_map(|(idx, item)| if item.can_backtrack { Some(idx) } else { None }) - .collect(), } } @@ -130,6 +137,13 @@ impl PathfindingInput { let hash_path_input = hasher.finish(); format!("pathfinding_{osrd_version}.{infra}.{infra_version}.{hash_path_input}") } + + fn path_item_locations(&self) -> Vec<&PathItemLocation> { + self.path_items + .iter() + .map(|item| &item.location) + .collect_vec() + } } impl From<&PathfindingInput> for core_task::PathfindingConsist { @@ -267,7 +281,8 @@ pub(in crate::views) async fn post( .await?; let op_cache = - OperationalPointCache::load_path_items(conn, infra.id, &path_input.path_items).await?; + OperationalPointCache::load_path_items(conn, infra.id, &path_input.path_item_locations()) + .await?; let pathfinding_request = match build_pathfinding_request(&path_input, &infra, &op_cache) { Ok(pathfinding_request) => pathfinding_request, Err(result) => return Ok(Json(*result)), @@ -331,6 +346,7 @@ async fn pathfinding_blocks_batch( .iter() .filter(|(_, res)| res.is_none()) .flat_map(|(hash, _)| &path_request_map[*hash].path_items) + .map(|item| &item.location) .collect_vec(); let op_cache = OperationalPointCache::load_path_items(conn, infra.id, &path_items).await?; @@ -403,7 +419,7 @@ fn build_pathfinding_request( ))); } let track_offsets = op_cache - .extract_location_from_path_items(&pathfinding_input.path_items) + .extract_location_from_path_items(&pathfinding_input.path_item_locations()) .map_err(PathfindingResult::Failure)?; // Create the pathfinding request @@ -415,7 +431,11 @@ fn build_pathfinding_request( .enumerate() .map(|(index, offsets)| core_client::pathfinding::PathItem { locations: offsets, - can_backtrack: pathfinding_input.can_backtrack_path_items.contains(&index), + can_backtrack: pathfinding_input + .path_items + .get(index) + .map(|item| item.can_backtrack) + .unwrap_or(false), }) .collect(), rolling_stock_loading_gauge: pathfinding_input.rolling_stock_loading_gauge, @@ -500,10 +520,11 @@ pub mod tests { use crate::fixtures::create_small_infra; use crate::views::path::pathfinding::PathfindingFailure; use crate::views::path::pathfinding::PathfindingInput; + use crate::views::path::pathfinding::PathfindingItem; use crate::views::path::pathfinding::PathfindingResult; use crate::views::test_app; - fn pathfinding_input(path_items: Vec) -> PathfindingInput { + fn pathfinding_input(path_items: Vec) -> PathfindingInput { PathfindingInput { rolling_stock_loading_gauge: LoadingGaugeType::G1, rolling_stock_is_thermal: true, @@ -518,7 +539,6 @@ pub mod tests { stops_at_end_of_block: None, allowed_track_sections: BTreeSet::new(), path_items, - can_backtrack_path_items: Default::default(), } } @@ -546,22 +566,32 @@ pub mod tests { let db_pool = app.db_pool(); let small_infra = create_small_infra(&mut db_pool.get_ok()).await; let path_items = vec![ - PathItemLocation::OperationalPointPartReference(OperationalPointPartReference { - operational_point: OperationalPointReference::Domestic { - country_code: "FR".into(), - main_code: "WS".into(), - secondary_code: Some("BV".into()), - }, - local_track_name: None, - }), - PathItemLocation::OperationalPointPartReference(OperationalPointPartReference { - operational_point: OperationalPointReference::Domestic { - country_code: "FR".into(), - main_code: "WS".into(), - secondary_code: Some("BV".into()), - }, - local_track_name: None, - }), + PathfindingItem { + location: PathItemLocation::OperationalPointPartReference( + OperationalPointPartReference { + operational_point: OperationalPointReference::Domestic { + country_code: "FR".into(), + main_code: "WS".into(), + secondary_code: Some("BV".into()), + }, + local_track_name: None, + }, + ), + can_backtrack: false, + }, + PathfindingItem { + location: PathItemLocation::OperationalPointPartReference( + OperationalPointPartReference { + operational_point: OperationalPointReference::Domestic { + country_code: "FR".into(), + main_code: "WS".into(), + secondary_code: Some("BV".into()), + }, + local_track_name: None, + }, + ), + can_backtrack: false, + }, ]; let pathfinding_result: PathfindingResult = app @@ -584,46 +614,71 @@ pub mod tests { let db_pool = app.db_pool(); let small_infra = create_small_infra(&mut db_pool.get_ok()).await; let path_items = vec![ - PathItemLocation::OperationalPointPartReference(OperationalPointPartReference { - operational_point: OperationalPointReference::Domestic { - country_code: "FR".into(), - main_code: "WS".into(), - secondary_code: Some("BV".into()), - }, - local_track_name: None, - }), - PathItemLocation::OperationalPointPartReference(OperationalPointPartReference { - operational_point: OperationalPointReference::Domestic { - country_code: "FR".into(), - main_code: "NO_MAIN_CODE".into(), - secondary_code: None, - }, - local_track_name: None, - }), - PathItemLocation::OperationalPointPartReference(OperationalPointPartReference { - operational_point: OperationalPointReference::Domestic { - country_code: "FR".into(), - main_code: "SWS".into(), - secondary_code: Some("BV".into()), - }, - local_track_name: None, - }), - PathItemLocation::OperationalPointPartReference(OperationalPointPartReference { - operational_point: OperationalPointReference::Domestic { - country_code: "NO_COUNTRY_CODE".into(), - main_code: "WS".into(), - secondary_code: Some("BV".into()), - }, - local_track_name: None, - }), - PathItemLocation::OperationalPointPartReference(OperationalPointPartReference { - operational_point: OperationalPointReference::Domestic { - country_code: "FR".into(), - main_code: "WS".into(), - secondary_code: Some("NO_SECONDARY_CODE".into()), - }, - local_track_name: None, - }), + PathfindingItem { + location: PathItemLocation::OperationalPointPartReference( + OperationalPointPartReference { + operational_point: OperationalPointReference::Domestic { + country_code: "FR".into(), + main_code: "WS".into(), + secondary_code: Some("BV".into()), + }, + local_track_name: None, + }, + ), + can_backtrack: false, + }, + PathfindingItem { + location: PathItemLocation::OperationalPointPartReference( + OperationalPointPartReference { + operational_point: OperationalPointReference::Domestic { + country_code: "FR".into(), + main_code: "NO_MAIN_CODE".into(), + secondary_code: None, + }, + local_track_name: None, + }, + ), + can_backtrack: false, + }, + PathfindingItem { + location: PathItemLocation::OperationalPointPartReference( + OperationalPointPartReference { + operational_point: OperationalPointReference::Domestic { + country_code: "FR".into(), + main_code: "SWS".into(), + secondary_code: Some("BV".into()), + }, + local_track_name: None, + }, + ), + can_backtrack: false, + }, + PathfindingItem { + location: PathItemLocation::OperationalPointPartReference( + OperationalPointPartReference { + operational_point: OperationalPointReference::Domestic { + country_code: "NO_COUNTRY_CODE".into(), + main_code: "WS".into(), + secondary_code: Some("BV".into()), + }, + local_track_name: None, + }, + ), + can_backtrack: false, + }, + PathfindingItem { + location: PathItemLocation::OperationalPointPartReference( + OperationalPointPartReference { + operational_point: OperationalPointReference::Domestic { + country_code: "FR".into(), + main_code: "WS".into(), + secondary_code: Some("NO_SECONDARY_CODE".into()), + }, + local_track_name: None, + }, + ), + can_backtrack: false, + }, ]; let pathfinding_result: PathfindingResult = app @@ -688,27 +743,42 @@ pub mod tests { let db_pool = app.db_pool(); let small_infra = create_small_infra(&mut db_pool.get_ok()).await; let path_items = vec![ - PathItemLocation::OperationalPointPartReference(OperationalPointPartReference { - operational_point: OperationalPointReference::Uic { - uic: 8733, - secondary_code: Some("BV".into()), - }, - local_track_name: Some("V2".into()), - }), - PathItemLocation::OperationalPointPartReference(OperationalPointPartReference { - operational_point: OperationalPointReference::Uic { - uic: 8788, - secondary_code: Some("BV".into()), - }, - local_track_name: Some("V_INVALID".into()), - }), - PathItemLocation::OperationalPointPartReference(OperationalPointPartReference { - operational_point: OperationalPointReference::Uic { - uic: 8733, - secondary_code: Some("NO_SECONDARY_CODE".into()), - }, - local_track_name: Some("V2".into()), - }), + PathfindingItem { + location: PathItemLocation::OperationalPointPartReference( + OperationalPointPartReference { + operational_point: OperationalPointReference::Uic { + uic: 8733, + secondary_code: Some("BV".into()), + }, + local_track_name: Some("V2".into()), + }, + ), + can_backtrack: false, + }, + PathfindingItem { + location: PathItemLocation::OperationalPointPartReference( + OperationalPointPartReference { + operational_point: OperationalPointReference::Uic { + uic: 8788, + secondary_code: Some("BV".into()), + }, + local_track_name: Some("V_INVALID".into()), + }, + ), + can_backtrack: false, + }, + PathfindingItem { + location: PathItemLocation::OperationalPointPartReference( + OperationalPointPartReference { + operational_point: OperationalPointReference::Uic { + uic: 8733, + secondary_code: Some("NO_SECONDARY_CODE".into()), + }, + local_track_name: Some("V2".into()), + }, + ), + can_backtrack: false, + }, ]; let pathfinding_result: PathfindingResult = app @@ -763,22 +833,32 @@ pub mod tests { let db_pool = app.db_pool(); let small_infra = create_small_infra(&mut db_pool.get_ok()).await; let path_items = vec![ - PathItemLocation::OperationalPointPartReference(OperationalPointPartReference { - operational_point: OperationalPointReference::Domestic { - country_code: "FR".into(), - main_code: "WS".into(), - secondary_code: Some("BV".into()), - }, - local_track_name: None, - }), - PathItemLocation::OperationalPointPartReference(OperationalPointPartReference { - operational_point: OperationalPointReference::Domestic { - country_code: "FR".into(), - main_code: "SWS".into(), - secondary_code: Some("BV".into()), - }, - local_track_name: None, - }), + PathfindingItem { + location: PathItemLocation::OperationalPointPartReference( + OperationalPointPartReference { + operational_point: OperationalPointReference::Domestic { + country_code: "FR".into(), + main_code: "WS".into(), + secondary_code: Some("BV".into()), + }, + local_track_name: None, + }, + ), + can_backtrack: false, + }, + PathfindingItem { + location: PathItemLocation::OperationalPointPartReference( + OperationalPointPartReference { + operational_point: OperationalPointReference::Domestic { + country_code: "FR".into(), + main_code: "SWS".into(), + secondary_code: Some("BV".into()), + }, + local_track_name: None, + }, + ), + can_backtrack: false, + }, ]; let pathfinding_res: PathfindingResult = app diff --git a/editoast/src/views/timetable/stdcm.rs b/editoast/src/views/timetable/stdcm.rs index fd2bd590ca3..4d90240956d 100644 --- a/editoast/src/views/timetable/stdcm.rs +++ b/editoast/src/views/timetable/stdcm.rs @@ -624,12 +624,13 @@ mod tests { use crate::fixtures::create_small_infra; use crate::fixtures::create_timetable; use crate::fixtures::create_towed_rolling_stock; + use crate::views::path::pathfinding::PathfindingItem; use crate::views::path::pathfinding::PathfindingResult; use crate::views::test_app::TestResponseExt as _; use crate::views::test_app::test_app; use crate::views::timetable::stdcm::Request; use crate::views::timetable::stdcm::request::ConsistSchedule; - use crate::views::timetable::stdcm::request::PathfindingItem; + use crate::views::timetable::stdcm::request::StdcmPathfindingItem; use crate::views::timetable::stdcm::request::StepTimingData; use super::*; @@ -643,18 +644,21 @@ mod tests { DateTime::from_str("2024-01-01T10:00:00Z").expect("Failed to parse datetime"), ), steps: vec![ - PathfindingItem { + StdcmPathfindingItem { duration: Some(0), - location: PathItemLocation::OperationalPointPartReference( - OperationalPointPartReference { - operational_point: OperationalPointReference::Domestic { - country_code: "FR".into(), - main_code: "WS".into(), - secondary_code: Some("BV".into()), + pathfinding_item: PathfindingItem { + location: PathItemLocation::OperationalPointPartReference( + OperationalPointPartReference { + operational_point: OperationalPointReference::Domestic { + country_code: "FR".into(), + main_code: "WS".into(), + secondary_code: Some("BV".into()), + }, + local_track_name: None, }, - local_track_name: None, - }, - ), + ), + can_backtrack: false, + }, timing_data: Some(StepTimingData { arrival_time: DateTime::from_str("2024-01-01T14:00:00Z") .expect("Failed to parse datetime"), @@ -662,18 +666,21 @@ mod tests { arrival_time_tolerance_after: 0, }), }, - PathfindingItem { + StdcmPathfindingItem { duration: Some(0), - location: PathItemLocation::OperationalPointPartReference( - OperationalPointPartReference { - operational_point: OperationalPointReference::Domestic { - country_code: "FR".into(), - main_code: "MWS".into(), - secondary_code: Some("BV".into()), + pathfinding_item: PathfindingItem { + location: PathItemLocation::OperationalPointPartReference( + OperationalPointPartReference { + operational_point: OperationalPointReference::Domestic { + country_code: "FR".into(), + main_code: "MWS".into(), + secondary_code: Some("BV".into()), + }, + local_track_name: None, }, - local_track_name: None, - }, - ), + ), + can_backtrack: false, + }, timing_data: None, }, ], @@ -717,19 +724,22 @@ mod tests { } } - fn build_step(main_code: &str) -> PathfindingItem { - PathfindingItem { + fn build_step(main_code: &str) -> StdcmPathfindingItem { + StdcmPathfindingItem { duration: Some(0), - location: PathItemLocation::OperationalPointPartReference( - OperationalPointPartReference { - operational_point: OperationalPointReference::Domestic { - country_code: "FR".into(), - main_code: main_code.into(), - secondary_code: Some("BV".into()), + pathfinding_item: PathfindingItem { + location: PathItemLocation::OperationalPointPartReference( + OperationalPointPartReference { + operational_point: OperationalPointReference::Domestic { + country_code: "FR".into(), + main_code: main_code.into(), + secondary_code: Some("BV".into()), + }, + local_track_name: None, }, - local_track_name: None, - }, - ), + ), + can_backtrack: false, + }, timing_data: None, } } diff --git a/editoast/src/views/timetable/stdcm/request.rs b/editoast/src/views/timetable/stdcm/request.rs index f801abdcfe5..4a473114c30 100644 --- a/editoast/src/views/timetable/stdcm/request.rs +++ b/editoast/src/views/timetable/stdcm/request.rs @@ -13,7 +13,6 @@ use schemas::rolling_stock::RollingResistance; use schemas::train_schedule::Comfort; use schemas::train_schedule::MarginValue; use schemas::train_schedule::PathItem; -use schemas::train_schedule::PathItemLocation; use serde::Deserialize; use serde::Deserializer; use serde::Serialize; @@ -27,6 +26,7 @@ use utoipa::ToSchema; use crate::error::Result; use crate::views::path::operational_point_cache::OperationalPointCache; use crate::views::path::pathfinding::PathfindingFailure; +use crate::views::path::pathfinding::PathfindingItem; use models::TemporarySpeedLimit; use models::TowedRollingStock; use models::WorkSchedule; @@ -35,11 +35,11 @@ use models::prelude::*; use super::StdcmError; #[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)] -pub(crate) struct PathfindingItem { +pub(crate) struct StdcmPathfindingItem { /// The stop duration in milliseconds, None if the train does not stop. pub(crate) duration: Option, /// The associated location - pub(crate) location: PathItemLocation, + pub(crate) pathfinding_item: PathfindingItem, /// Time at which the train should arrive at the location, if specified pub(crate) timing_data: Option, } @@ -73,12 +73,12 @@ pub(crate) struct ConsistSchedule { } /// Convert the list of pathfinding items into a list of path item -pub(super) fn convert_steps(steps: &[PathfindingItem]) -> Vec { +pub(super) fn convert_steps(steps: &[StdcmPathfindingItem]) -> Vec { steps .iter() .map(|step| PathItem { id: Default::default(), - location: step.location.clone(), + location: step.pathfinding_item.location.clone(), }) .collect() } @@ -100,7 +100,7 @@ pub(crate) struct StepTimingData { pub(crate) struct Request { /// Deprecated, first step arrival time should be used instead pub(crate) start_time: Option>, - pub(crate) steps: Vec, + pub(crate) steps: Vec, pub(crate) electrical_profile_set_id: Option, pub(crate) work_schedule_group_id: Option, pub(crate) temporary_speed_limit_group_id: Option, @@ -166,7 +166,7 @@ impl Request { fn get_total_stop_time(&self) -> u64 { self.steps .iter() - .map(|step: &PathfindingItem| step.duration.unwrap_or_default()) + .map(|step: &StdcmPathfindingItem| step.duration.unwrap_or_default()) .sum() } @@ -252,7 +252,11 @@ impl Request { conn: DbConnection, infra_id: i64, ) -> Result> { - let locations: Vec<_> = self.steps.iter().map(|item| &item.location).collect(); + let locations: Vec<_> = self + .steps + .iter() + .map(|item| &item.pathfinding_item.location) + .collect(); let op_cache = OperationalPointCache::load_path_items(conn, infra_id, &locations).await?; let track_offsets = op_cache diff --git a/front/src/applications/stdcm/hooks/useStdcm.ts b/front/src/applications/stdcm/hooks/useStdcm.ts index 6ba0a40ab65..7244f5c5a53 100644 --- a/front/src/applications/stdcm/hooks/useStdcm.ts +++ b/front/src/applications/stdcm/hooks/useStdcm.ts @@ -134,7 +134,7 @@ const useStdcm = ({ comfort: payload.body.comfort, constraint_distribution: 'MARECO', path: payload.body.steps.map((step) => ({ - location: step.location, + location: step.pathfinding_item.location, id: uuidV4(), })), rolling_stock_name: stdcmRollingStock!.name, diff --git a/front/src/applications/stdcm/utils/formatStdcmConf.ts b/front/src/applications/stdcm/utils/formatStdcmConf.ts index 9a7bc57285a..162448d9e58 100644 --- a/front/src/applications/stdcm/utils/formatStdcmConf.ts +++ b/front/src/applications/stdcm/utils/formatStdcmConf.ts @@ -4,7 +4,7 @@ import type { Dispatch } from 'redux'; import type { ConsistSchedule, - PathfindingItem, + StdcmPathfindingItem, PostTimetableByIdStdcmApiArg, } from 'common/api/osrdEditoastApi'; import { setFailure } from 'reducers/main'; @@ -19,7 +19,7 @@ import createMargin from './createMargin'; type ValidStdcmConfig = { timetableId: number; infraId: number; - path: PathfindingItem[]; + path: StdcmPathfindingItem[]; margin?: StandardAllowance; gridMarginBefore?: Duration; gridMarginAfter?: Duration; @@ -157,7 +157,7 @@ export const checkStdcmConf = ( const path = compact(osrdconf.stdcmPathSteps).map((step) => { const formattedLocation = stdcmPathStepToPathItemLocation(step.operationalPoint); - let timingData: PathfindingItem['timing_data'] | undefined; + let timingData: StdcmPathfindingItem['timing_data'] | undefined; let duration: number | undefined; if (step.isVia) { const { stopFor } = step; @@ -181,7 +181,7 @@ export const checkStdcmConf = ( return { duration, - location: formattedLocation, + pathfinding_item: { location: formattedLocation, can_backtrack: false }, timing_data: timingData, }; }); diff --git a/front/src/common/api/generatedEditoastApi.ts b/front/src/common/api/generatedEditoastApi.ts index f10d7862d26..353d89eae75 100644 --- a/front/src/common/api/generatedEditoastApi.ts +++ b/front/src/common/api/generatedEditoastApi.ts @@ -2571,7 +2571,7 @@ export type PostTimetableByIdStdcmApiArg = { maximum_run_time?: number | null; /** Deprecated, first step arrival time should be used instead */ start_time?: string | null; - steps: PathfindingItem[]; + steps: StdcmPathfindingItem[]; temporary_speed_limit_group_id?: number | null; /** Margin after the train passage in milliseconds @@ -3876,13 +3876,15 @@ export type PathfindingResult = | (PathfindingFailure & { status: 'failure'; }); +export type PathfindingItem = { + can_backtrack: boolean; + location: PathItemLocation; +}; export type PathfindingInput = { /** Set of authorized track section ids, empty means no restriction */ allowed_track_sections?: string[]; - /** Indexes, in `path_items`, of the waypoints where the train is allowed to backtrack */ - can_backtrack_path_items?: number[]; /** List of waypoints given to the pathfinding */ - path_items: PathItemLocation[]; + path_items: PathfindingItem[]; /** Can the rolling stock run on non-electrified tracks */ rolling_stock_is_thermal: boolean; /** Rolling stock length in millimeters */ @@ -4998,11 +5000,11 @@ export type StepTimingData = { /** The train may arrive up to this duration before the expected arrival time */ arrival_time_tolerance_before: number; }; -export type PathfindingItem = { +export type StdcmPathfindingItem = { /** The stop duration in milliseconds, None if the train does not stop. */ duration?: number | null; /** The associated location */ - location: PathItemLocation; + pathfinding_item: PathfindingItem; timing_data?: null | StepTimingData; }; export type Distribution = 'STANDARD' | 'MARECO'; diff --git a/front/src/modules/pathfinding/hooks/usePathfindingV2.ts b/front/src/modules/pathfinding/hooks/usePathfindingV2.ts index 0913637aa68..4aa319227df 100644 --- a/front/src/modules/pathfinding/hooks/usePathfindingV2.ts +++ b/front/src/modules/pathfinding/hooks/usePathfindingV2.ts @@ -59,7 +59,7 @@ const usePathfindingV2 = () => { const pathFindingPayload: PostInfraByInfraIdPathfindingBlocksApiArg = { infraId, pathfindingInput: { - path_items: pathSteps, + path_items: pathSteps.map((location) => ({ location, can_backtrack: false })), rolling_stock_is_thermal: isThermal, rolling_stock_loading_gauge: rollingStock.loading_gauge, rolling_stock_supported_electrifications: supportedElectrirications, diff --git a/front/src/modules/pathfinding/utils.ts b/front/src/modules/pathfinding/utils.ts index 530ab0a8d30..61e84831809 100644 --- a/front/src/modules/pathfinding/utils.ts +++ b/front/src/modules/pathfinding/utils.ts @@ -92,7 +92,10 @@ export const getPathfindingQuery = ({ const destination = pathSteps.at(-1); if (infraId && rollingStock && origin && destination) { // Only origin and destination can be null so we can compact and we want to remove any via that would be null - const pathItems: PathfindingInput['path_items'] = compact(pathSteps); + const pathItems: PathfindingInput['path_items'] = compact(pathSteps).map((location) => ({ + location, + can_backtrack: false, + })); return { infraId, diff --git a/osrd_schemas/osrd_schemas/models.py b/osrd_schemas/osrd_schemas/models.py index 5a174de36bb..006d8753a98 100644 --- a/osrd_schemas/osrd_schemas/models.py +++ b/osrd_schemas/osrd_schemas/models.py @@ -3148,10 +3148,6 @@ class PathfindingFailurePathfindingNotFound4( pass -class CanBacktrackPathItem(RootModel[int]): - root: Annotated[int, Field(ge=0)] - - SwitchesDirectionsAdditionalProperty = TypeAliasType( "SwitchesDirectionsAdditionalProperty", Annotated[str, Field(max_length=255, min_length=1)], @@ -5392,74 +5388,14 @@ class PathfindingFailurePathfindingNotFound3( pass -class PathfindingInput(BaseModel): - """ - Path input is described by some rolling stock information - and a list of path waypoints - """ - - allowed_track_sections: list[str] | None = None - """ - Set of authorized track section ids, empty means no restriction - """ - can_backtrack_path_items: list[CanBacktrackPathItem] | None = None - """ - Indexes, in `path_items`, of the waypoints where the train is allowed to backtrack - """ - path_items: list[ - PathItemLocationTrackOffset | PathItemLocationOperationalPointPartReference - ] - """ - List of waypoints given to the pathfinding - """ - rolling_stock_is_thermal: bool - """ - Can the rolling stock run on non-electrified tracks - """ - rolling_stock_length: Annotated[int, Field(ge=0)] - """ - Rolling stock length in millimeters - """ - rolling_stock_loading_gauge: LoadingGaugeType - """ - The loading gauge of the rolling stock - """ - rolling_stock_maximum_speed: float - """ - Rolling stock maximum speed - """ - rolling_stock_supported_electrifications: list[str] - """ - List of supported electrification modes. - Empty if does not support any electrification - """ - rolling_stock_supported_signaling_systems: list[str] - """ - List of supported signaling systems - """ - speed_limit_tag: str | None = None - """ - Speed limit tag, used to estimate the travel time - """ - stops_at_end_of_block: bool | None = None - """ - Stop the train at the next block-delimiting signal, - staying in the same block and keeping the tail on the initial position - """ - - class PathfindingItem(BaseModel): - duration: Annotated[int | None, Field(ge=0)] = None - """ - The stop duration in milliseconds, None if the train does not stop. - """ + can_backtrack: bool location: ( PathItemLocationTrackOffset | PathItemLocationOperationalPointPartReference ) """ - The associated location + The location of a path waypoint """ - timing_data: StepTimingData | None = None class PathfindingInputErrorInvalidPathItems2(BaseModel): @@ -5829,6 +5765,18 @@ class SpeedSectionExtensions(BaseModel): psl_sncf: SpeedSectionPslSncfExtension | None = None +class StdcmPathfindingItem(BaseModel): + duration: Annotated[int | None, Field(ge=0)] = None + """ + The stop duration in milliseconds, None if the train does not stop. + """ + pathfinding_item: PathfindingItem + """ + The associated location + """ + timing_data: StepTimingData | None = None + + class StdcmProgressionEvent(BaseModel): best_travel_time: Annotated[int, Field(ge=0)] point: GeoJsonPoint @@ -6461,6 +6409,56 @@ class PathfindingFailurePathfindingNotFound5( pass +class PathfindingInput(BaseModel): + """ + Path input is described by some rolling stock information + and a list of path waypoints + """ + + allowed_track_sections: list[str] | None = None + """ + Set of authorized track section ids, empty means no restriction + """ + path_items: list[PathfindingItem] + """ + List of waypoints given to the pathfinding + """ + rolling_stock_is_thermal: bool + """ + Can the rolling stock run on non-electrified tracks + """ + rolling_stock_length: Annotated[int, Field(ge=0)] + """ + Rolling stock length in millimeters + """ + rolling_stock_loading_gauge: LoadingGaugeType + """ + The loading gauge of the rolling stock + """ + rolling_stock_maximum_speed: float + """ + Rolling stock maximum speed + """ + rolling_stock_supported_electrifications: list[str] + """ + List of supported electrification modes. + Empty if does not support any electrification + """ + rolling_stock_supported_signaling_systems: list[str] + """ + List of supported signaling systems + """ + speed_limit_tag: str | None = None + """ + Speed limit tag, used to estimate the travel time + """ + stops_at_end_of_block: bool | None = None + """ + Stop the train at the next block-delimiting signal, + staying in the same block and keeping the tail on the initial position + """ + + class PathfindingResultSuccess(CorePathfindingResultSuccess): status: Literal["success"] diff --git a/tests/conftest.py b/tests/conftest.py index c222a54f4e0..d2362abf5c5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -256,8 +256,22 @@ def west_to_south_east_path( f"{EDITOAST_URL}infra/{small_infra.id}/pathfinding/blocks", json={ "path_items": [ - {"type": "track_offset", "offset": 837034, "track": "TA2"}, - {"type": "track_offset", "offset": 4386000, "track": "TH1"}, + { + "location": { + "type": "track_offset", + "offset": 837034, + "track": "TA2", + }, + "can_backtrack": False, + }, + { + "location": { + "type": "track_offset", + "offset": 4386000, + "track": "TH1", + }, + "can_backtrack": False, + }, ], "rolling_stock_is_thermal": True, "rolling_stock_loading_gauge": "G1", diff --git a/tests/fuzzer/fuzzer.py b/tests/fuzzer/fuzzer.py index c70fb758dc1..d2d9b33d95e 100644 --- a/tests/fuzzer/fuzzer.py +++ b/tests/fuzzer/fuzzer.py @@ -509,10 +509,13 @@ def _convert_stop_stdcm(stop: tuple[str, float]) -> dict: duration = None if random.randint(0, 1) == 0 else _to_ms(random.random() * 1_000) return { "duration": duration, - "location": { - "type": "track_offset", - "track": track_section, - "offset": _to_mm(offset), + "pathfinding_item": { + "location": { + "type": "track_offset", + "track": track_section, + "offset": _to_mm(offset), + }, + "can_backtrack": False, }, } diff --git a/tests/fuzzer/fuzzer_stdcm_single_timetable.py b/tests/fuzzer/fuzzer_stdcm_single_timetable.py index 11cb8042b55..7186a98b0cf 100644 --- a/tests/fuzzer/fuzzer_stdcm_single_timetable.py +++ b/tests/fuzzer/fuzzer_stdcm_single_timetable.py @@ -241,12 +241,15 @@ def _make_steps(op_list: list[str], timetable_range: TimetableTimeRange) -> list for _ in range(n_steps): steps.append( { - "location": { - "type": "operational_point_part_reference", - "operational_point": { - "type": "id", - "operational_point": _random_set_element(op_list), + "pathfinding_item": { + "location": { + "type": "operational_point_part_reference", + "operational_point": { + "type": "id", + "operational_point": _random_set_element(op_list), + }, }, + "can_backtrack": False, } } ) diff --git a/tests/tests/regression_tests_data/coasting_not_intersecting_v2.json b/tests/tests/regression_tests_data/coasting_not_intersecting_v2.json index 675328f4160..c3bda584399 100644 --- a/tests/tests/regression_tests_data/coasting_not_intersecting_v2.json +++ b/tests/tests/regression_tests_data/coasting_not_intersecting_v2.json @@ -12,42 +12,57 @@ "steps": [ { "duration": 507316, - "location": { - "type": "track_offset", - "track": "TE3", - "offset": 20743 + "pathfinding_item": { + "location": { + "type": "track_offset", + "track": "TE3", + "offset": 20743 + }, + "can_backtrack": false } }, { "duration": 631216, - "location": { - "type": "track_offset", - "track": "TE3", - "offset": 393924 + "pathfinding_item": { + "location": { + "type": "track_offset", + "track": "TE3", + "offset": 393924 + }, + "can_backtrack": false } }, { "duration": null, - "location": { - "type": "track_offset", - "track": "TE2", - "offset": 1996809 + "pathfinding_item": { + "location": { + "type": "track_offset", + "track": "TE2", + "offset": 1996809 + }, + "can_backtrack": false } }, { "duration": null, - "location": { - "type": "track_offset", - "track": "TF0", - "offset": 978 + "pathfinding_item": { + "location": { + "type": "track_offset", + "track": "TF0", + "offset": 978 + }, + "can_backtrack": false } }, { "duration": 1, - "location": { - "type": "track_offset", - "track": "TF1", - "offset": 1203798 + "pathfinding_item": { + "location": { + "type": "track_offset", + "track": "TF1", + "offset": 1203798 + }, + "can_backtrack": false } } ], diff --git a/tests/tests/regression_tests_data/stdcm_conflict_in_result.json b/tests/tests/regression_tests_data/stdcm_conflict_in_result.json index 8ec209a515a..7480f426083 100644 --- a/tests/tests/regression_tests_data/stdcm_conflict_in_result.json +++ b/tests/tests/regression_tests_data/stdcm_conflict_in_result.json @@ -12,42 +12,58 @@ "steps": [ { "duration": 960840, - "location": { - "type": "track_offset", - "track": "TE1", - "offset": 1767599 + "pathfinding_item": { + "location": { + "type": "track_offset", + "track": "TE1", + "offset": 1767599 + }, + "can_backtrack": false } }, { "duration": null, - "location": { - "type": "track_offset", - "track": "TE1", - "offset": 264224 + "pathfinding_item": { + "location": { + "type": "track_offset", + "track": "TE1", + "offset": 264224 + }, + "can_backtrack": false } }, { "duration": 575287, - "location": { - "type": "track_offset", - "track": "TE3", - "offset": 528823 + "pathfinding_item": { + "location": { + "type": "track_offset", + "track": "TE3", + "offset": 528823 + }, + "can_backtrack": false } }, { "duration": 746487, - "location": { - "type": "track_offset", - "track": "TC1", - "offset": 698736 + "pathfinding_item": { + "location": { + "type": "track_offset", + "track": "TC1", + "offset": 698736 + }, + "can_backtrack": false } + }, { "duration": 1, - "location": { - "type": "track_offset", - "track": "TA6", - "offset": 9146278 + "pathfinding_item": { + "location": { + "type": "track_offset", + "track": "TA6", + "offset": 9146278 + }, + "can_backtrack": false } } ], diff --git a/tests/tests/regression_tests_data/stdcm_timetable_conflict.json b/tests/tests/regression_tests_data/stdcm_timetable_conflict.json index 41000afc23c..e1f0000d4fa 100644 --- a/tests/tests/regression_tests_data/stdcm_timetable_conflict.json +++ b/tests/tests/regression_tests_data/stdcm_timetable_conflict.json @@ -12,34 +12,46 @@ "steps": [ { "duration": 180125, - "location": { - "type": "track_offset", - "track": "TE0", - "offset": 581924 + "pathfinding_item": { + "location": { + "type": "track_offset", + "track": "TE0", + "offset": 581924 + }, + "can_backtrack": false } }, { "duration": 905261, - "location": { - "type": "track_offset", - "track": "TE0", - "offset": 338804 + "pathfinding_item": { + "location": { + "type": "track_offset", + "track": "TE0", + "offset": 338804 + }, + "can_backtrack": false } }, { "duration": null, - "location": { - "type": "track_offset", - "track": "TE3", - "offset": 1469324 + "pathfinding_item": { + "location": { + "type": "track_offset", + "track": "TE3", + "offset": 1469324 + }, + "can_backtrack": false } }, { "duration": 1, - "location": { - "type": "track_offset", - "track": "TD2", - "offset": 1694161 + "pathfinding_item": { + "location": { + "type": "track_offset", + "track": "TD2", + "offset": 1694161 + }, + "can_backtrack": false } } ], diff --git a/tests/tests/regression_tests_data/stdcm_zone_transition_near_start.json b/tests/tests/regression_tests_data/stdcm_zone_transition_near_start.json index 6d79f60823f..13b8e57a68f 100644 --- a/tests/tests/regression_tests_data/stdcm_zone_transition_near_start.json +++ b/tests/tests/regression_tests_data/stdcm_zone_transition_near_start.json @@ -12,34 +12,46 @@ "steps": [ { "duration": 216306, - "location": { - "type": "track_offset", - "track": "TA3", - "offset": 20806 + "pathfinding_item": { + "location": { + "type": "track_offset", + "track": "TA3", + "offset": 20806 + }, + "can_backtrack": false } }, { "duration": 234593, - "location": { - "type": "track_offset", - "track": "TA3", - "offset": 45018 + "pathfinding_item": { + "location": { + "type": "track_offset", + "track": "TA3", + "offset": 45018 + }, + "can_backtrack": false } }, { "duration": 644645, - "location": { - "type": "track_offset", - "track": "TC0", - "offset": 934429 + "pathfinding_item": { + "location": { + "type": "track_offset", + "track": "TC0", + "offset": 934429 + }, + "can_backtrack": false } }, { "duration": 1, - "location": { - "type": "track_offset", - "track": "TD0", - "offset": 10766163 + "pathfinding_item": { + "location": { + "type": "track_offset", + "track": "TD0", + "offset": 10766163 + }, + "can_backtrack": false } } ], diff --git a/tests/tests/test_pathfinding.py b/tests/tests/test_pathfinding.py index 7b9e12b637f..9408d2c0e7b 100644 --- a/tests/tests/test_pathfinding.py +++ b/tests/tests/test_pathfinding.py @@ -176,15 +176,25 @@ def test_start_ws_v1_path(session: Session, small_infra: Infra): json={ "path_items": [ { - "type": "operational_point_part_reference", - "operational_point": { - "uic": 8722, - "secondary_code": "BV", - "type": "uic", + "location": { + "type": "operational_point_part_reference", + "operational_point": { + "uic": 8722, + "secondary_code": "BV", + "type": "uic", + }, + "local_track_name": "V1", }, - "local_track_name": "V1", + "can_backtrack": False, + }, + { + "location": { + "type": "track_offset", + "offset": 1000000, + "track": "TA0", + }, + "can_backtrack": False, }, - {"type": "track_offset", "offset": 1000000, "track": "TA0"}, ], "rolling_stock_is_thermal": True, "rolling_stock_loading_gauge": "G1", diff --git a/tests/tests/test_stdcm.py b/tests/tests/test_stdcm.py index 6c9838a8a88..a435eca88c8 100644 --- a/tests/tests/test_stdcm.py +++ b/tests/tests/test_stdcm.py @@ -75,8 +75,14 @@ def test_empty_timetable( "margin": "0%", "start_time": "2024-08-13T21:26:05.793Z", "steps": [ - {"duration": 100, "location": _START}, - {"duration": 100, "location": _STOP}, + { + "duration": 100, + "pathfinding_item": {"location": _START, "can_backtrack": False}, + }, + { + "duration": 100, + "pathfinding_item": {"location": _STOP, "can_backtrack": False}, + }, ], "comfort": "STANDARD", "maximum_departure_delay": 7200000, @@ -111,9 +117,18 @@ def test_empty_timetable_with_stop( "margin": "0%", "start_time": "2024-08-13T21:26:05.793Z", "steps": [ - {"duration": 100, "location": _START}, - {"duration": 42000, "location": _MIDDLE}, - {"duration": 100, "location": _STOP}, + { + "duration": 100, + "pathfinding_item": {"location": _START, "can_backtrack": False}, + }, + { + "duration": 42000, + "pathfinding_item": {"location": _MIDDLE, "can_backtrack": False}, + }, + { + "duration": 100, + "pathfinding_item": {"location": _STOP, "can_backtrack": False}, + }, ], "comfort": "STANDARD", "maximum_departure_delay": 7200000, @@ -154,9 +169,18 @@ def test_between_trains( "margin": "0%", "start_time": "2024-08-13T21:26:05.793Z", "steps": [ - {"duration": 100, "location": _START}, - {"duration": 42000, "location": _MIDDLE}, - {"duration": 100, "location": _STOP}, + { + "duration": 100, + "pathfinding_item": {"location": _START, "can_backtrack": False}, + }, + { + "duration": 42000, + "pathfinding_item": {"location": _MIDDLE, "can_backtrack": False}, + }, + { + "duration": 100, + "pathfinding_item": {"location": _STOP, "can_backtrack": False}, + }, ], "comfort": "STANDARD", "maximum_departure_delay": 7200000, @@ -213,8 +237,14 @@ def test_work_schedules( "time_gap_before": 0, "time_gap_after": 0, "steps": [ - {"duration": None, "location": _START}, - {"duration": 1, "location": _STOP}, + { + "duration": None, + "pathfinding_item": {"location": _START, "can_backtrack": False}, + }, + { + "duration": 1, + "pathfinding_item": {"location": _STOP, "can_backtrack": False}, + }, ], "comfort": "STANDARD", "margin": "0%", @@ -257,10 +287,26 @@ def test_mrsp_sources( "maximum_departure_delay": 86400000, "maximum_run_time": 86400000, "steps": [ - {"location": {"type": "track_offset", "track": "TH0", "offset": 820000}}, + { + "pathfinding_item": { + "location": { + "type": "track_offset", + "track": "TH0", + "offset": 820000, + }, + "can_backtrack": False, + } + }, { "duration": 1, - "location": {"type": "track_offset", "track": "TH1", "offset": 5000000}, + "pathfinding_item": { + "location": { + "type": "track_offset", + "track": "TH1", + "offset": 5000000, + }, + "can_backtrack": False, + }, }, ], "time_gap_before": 3600000, @@ -368,8 +414,14 @@ def test_max_running_time( "time_gap_before": 0, "time_gap_after": 0, "steps": [ - {"duration": None, "location": _START}, - {"duration": 1, "location": _STOP}, + { + "duration": None, + "pathfinding_item": {"location": _START, "can_backtrack": False}, + }, + { + "duration": 1, + "pathfinding_item": {"location": _STOP, "can_backtrack": False}, + }, ], "comfort": "STANDARD", "margin": "0%",