From 73b3197389ec70a7b392e08e799db1f8fb7357ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristram=20Gr=C3=A4bener?= Date: Wed, 29 Nov 2023 09:22:09 +0100 Subject: [PATCH 1/5] editoast: pathfinding: start the search in both directions, ignoring the initial direction --- editoast/src/views/infra/pathfinding.rs | 90 ++++++++++++++++++++----- 1 file changed, 74 insertions(+), 16 deletions(-) diff --git a/editoast/src/views/infra/pathfinding.rs b/editoast/src/views/infra/pathfinding.rs index 3c6ab4d5d8a..150a4bd603c 100644 --- a/editoast/src/views/infra/pathfinding.rs +++ b/editoast/src/views/infra/pathfinding.rs @@ -121,6 +121,7 @@ struct PathfindingStep { direction: Direction, switch_direction: Option<(Identifier, Identifier)>, found: bool, + starting_step: bool, #[derivative(Hash = "ignore", PartialEq = "ignore")] previous: Option>, } @@ -133,6 +134,7 @@ impl PathfindingStep { direction, switch_direction: None, found: false, + starting_step: true, previous: None, } } @@ -151,6 +153,7 @@ impl PathfindingStep { direction, switch_direction, found, + starting_step: false, previous: Some(Box::new(previous)), } } @@ -185,6 +188,34 @@ fn compute_path( let get_length = |track: &String| track_sections[track].unwrap_track_section().length; let success = |step: &PathfindingStep| step.found; let successors = |step: &PathfindingStep| { + // We initially don’t know in which direction start searching the path + // So the first step as two successors, at the same track-position, but in opposite directions + if step.starting_step { + return vec![ + ( + PathfindingStep::new( + step.track.clone(), + step.position, + Direction::StartToStop, + None, + false, + step.clone(), + ), + 0, + ), + ( + PathfindingStep::new( + step.track.clone(), + step.position, + Direction::StopToStart, + None, + false, + step.clone(), + ), + 0, + ), + ]; + } // The successor is our on ending track if step.track == input.ending.track.0 { // If we aren't in the good direction to reach the ending position, it's a dead end @@ -264,7 +295,8 @@ fn compute_path( fn build_path_output(path: &Vec, infra_cache: &InfraCache) -> PathfindingOutput { // Fill track ranges let mut track_ranges = Vec::new(); - (0..(path.len() - 2)).for_each(|i| { + // We ignore the first element of path, as it is a virtual step to handle going in both directions + (1..(path.len() - 2)).for_each(|i| { let end = if path[i].direction == Direction::StartToStop { infra_cache.track_sections()[&path[i].track] .unwrap_track_section() @@ -326,11 +358,26 @@ mod tests { use super::compute_path; use crate::infra_cache::tests::create_small_infra_cache; use crate::infra_cache::Graph; + use crate::schema::utils::Identifier; use crate::schema::{Direction, DirectionalTrackRange}; use crate::views::infra::pathfinding::{ PathfindingInput, PathfindingTrackLocationDirInput, PathfindingTrackLocationInput, }; + fn expected_path() -> Vec { + vec![ + DirectionalTrackRange::new("A", 30., 500., Direction::StartToStop), + DirectionalTrackRange::new("B", 0., 500., Direction::StartToStop), + DirectionalTrackRange::new("C", 0., 470., Direction::StartToStop), + ] + } + fn expected_switches() -> HashMap { + HashMap::from([ + ("link".into(), "LINK".into()), + ("switch".into(), "A_B1".into()), + ]) + } + #[test] fn test_compute_path() { let infra_cache = create_small_infra_cache(); @@ -350,21 +397,32 @@ mod tests { assert_eq!(paths.len(), 1); let path = paths.pop().unwrap(); - assert_eq!( - path.track_ranges, - vec![ - DirectionalTrackRange::new("A", 30., 500., Direction::StartToStop), - DirectionalTrackRange::new("B", 0., 500., Direction::StartToStop), - DirectionalTrackRange::new("C", 0., 470., Direction::StartToStop), - ] - ); + assert_eq!(path.track_ranges, expected_path()); + assert_eq!(path.detectors, vec!["D1".into()]); + assert_eq!(path.switches_directions, expected_switches()); + } + + #[test] + fn test_compute_path_opposite_direction() { + let infra_cache = create_small_infra_cache(); + let graph = Graph::load(&infra_cache); + let input = PathfindingInput { + starting: PathfindingTrackLocationDirInput { + track: "A".into(), + position: 30.0, + direction: Direction::StopToStart, + }, + ending: PathfindingTrackLocationInput { + track: "C".into(), + position: 470.0, + }, + }; + let mut paths = compute_path(&input, &infra_cache, &graph, 1); + + assert_eq!(paths.len(), 1); + let path = paths.pop().unwrap(); + assert_eq!(path.track_ranges, expected_path()); assert_eq!(path.detectors, vec!["D1".into()]); - assert_eq!( - path.switches_directions, - HashMap::from([ - ("link".into(), "LINK".into()), - ("switch".into(), "A_B1".into()) - ]) - ); + assert_eq!(path.switches_directions, expected_switches()); } } From d50de5a066434eab1e5d28df50b48142b7212fb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristram=20Gr=C3=A4bener?= Date: Wed, 29 Nov 2023 10:03:21 +0100 Subject: [PATCH 2/5] editoast: pathfinding: add an upper bound compared to best solution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit editoast computes k-shortest path. When we are along a simple line, the next shortest path can be significant longer than the initial solution. This commit limits any subsequent path to be at most twice as long as the best solution. From an implementation point of view, we need to track the total length at each step. This is a bit cumbersome, but we can’t access with the pathfinding crate to cost data. --- editoast/src/views/infra/pathfinding.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/editoast/src/views/infra/pathfinding.rs b/editoast/src/views/infra/pathfinding.rs index 150a4bd603c..c70779c56c2 100644 --- a/editoast/src/views/infra/pathfinding.rs +++ b/editoast/src/views/infra/pathfinding.rs @@ -124,6 +124,7 @@ struct PathfindingStep { starting_step: bool, #[derivative(Hash = "ignore", PartialEq = "ignore")] previous: Option>, + total_length: u64, } impl PathfindingStep { @@ -136,6 +137,7 @@ impl PathfindingStep { found: false, starting_step: true, previous: None, + total_length: 0, } } @@ -146,7 +148,9 @@ impl PathfindingStep { switch_direction: Option<(Identifier, Identifier)>, found: bool, previous: PathfindingStep, + length: u64, ) -> Self { + let total_length = previous.total_length + length; Self { track, position, @@ -155,6 +159,7 @@ impl PathfindingStep { found, starting_step: false, previous: Some(Box::new(previous)), + total_length, } } @@ -187,6 +192,7 @@ fn compute_path( let into_cost = |length: f64| (length * 100.).round() as u64; let get_length = |track: &String| track_sections[track].unwrap_track_section().length; let success = |step: &PathfindingStep| step.found; + let mut best_distance = u64::MAX; let successors = |step: &PathfindingStep| { // We initially don’t know in which direction start searching the path // So the first step as two successors, at the same track-position, but in opposite directions @@ -200,6 +206,7 @@ fn compute_path( None, false, step.clone(), + 0, ), 0, ), @@ -211,6 +218,7 @@ fn compute_path( None, false, step.clone(), + 0, ), 0, ), @@ -224,6 +232,8 @@ fn compute_path( { return vec![]; } + let cost = into_cost((step.position - input.ending.position).abs()); + best_distance = best_distance.min(step.total_length + cost); return vec![( PathfindingStep::new( step.track.clone(), @@ -232,8 +242,9 @@ fn compute_path( None, true, step.clone(), + cost, ), - into_cost((step.position - input.ending.position).abs()), + cost, )]; } @@ -244,6 +255,11 @@ fn compute_path( } else { into_cost(step.position) }; + // We search for k-shortest path. However, we want to prune routes that are too long compared to the shortest + // We can’t do best_distance * 3, as initially it is u64::MAX + if (step.total_length + cost) / 3 > best_distance { + return vec![]; + } // Find neighbours let mut successors = vec![]; @@ -275,6 +291,7 @@ fn compute_path( switch.map(|s| (s.obj_id.clone().into(), neighbour_group.clone())), false, step.clone(), + cost, ), cost, )); From b8f339781ac8b4576df96b4d435b6a19a186330a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristram=20Gr=C3=A4bener?= Date: Wed, 29 Nov 2023 10:41:21 +0100 Subject: [PATCH 3/5] front+editoast: remove route starting direction parameter --- editoast/openapi.yaml | 7 +--- editoast/openapi_legacy.yaml | 7 +--- editoast/src/views/infra/pathfinding.rs | 26 ++++---------- .../routeEdition/components/EditRoutePath.tsx | 16 +++------ .../routeEdition/components/Endpoints.tsx | 36 ++----------------- .../editor/tools/routeEdition/utils.ts | 3 -- front/src/common/api/osrdEditoastApi.ts | 4 +-- 7 files changed, 17 insertions(+), 82 deletions(-) diff --git a/editoast/openapi.yaml b/editoast/openapi.yaml index 46bffc39676..bccedab71c2 100644 --- a/editoast/openapi.yaml +++ b/editoast/openapi.yaml @@ -4144,12 +4144,7 @@ paths: ending: $ref: '#/components/schemas/TrackLocation' starting: - allOf: - - $ref: '#/components/schemas/TrackLocation' - - properties: - direction: - $ref: '#/components/schemas/Direction' - type: object + $ref: '#/components/schemas/TrackLocation' type: object description: Starting and ending track location responses: diff --git a/editoast/openapi_legacy.yaml b/editoast/openapi_legacy.yaml index 9e6344ef425..acc8b23cf84 100644 --- a/editoast/openapi_legacy.yaml +++ b/editoast/openapi_legacy.yaml @@ -642,12 +642,7 @@ paths: type: object properties: starting: - allOf: - - $ref: "#/components/schemas/TrackLocation" - - type: object - properties: - direction: - $ref: "#/components/schemas/Direction" + $ref: "#/components/schemas/TrackLocation" ending: $ref: "#/components/schemas/TrackLocation" responses: diff --git a/editoast/src/views/infra/pathfinding.rs b/editoast/src/views/infra/pathfinding.rs index c70779c56c2..77537df419f 100644 --- a/editoast/src/views/infra/pathfinding.rs +++ b/editoast/src/views/infra/pathfinding.rs @@ -40,14 +40,6 @@ enum PathfindingViewErrors { InvalidNumberOfPaths(u8), } -#[derive(Debug, Clone, Deserialize)] -#[serde(deny_unknown_fields)] -struct PathfindingTrackLocationDirInput { - track: Identifier, - position: f64, - direction: Direction, -} - #[derive(Debug, Clone, Deserialize)] #[serde(deny_unknown_fields)] struct PathfindingTrackLocationInput { @@ -57,7 +49,7 @@ struct PathfindingTrackLocationInput { #[derive(Debug, Clone, Deserialize)] struct PathfindingInput { - starting: PathfindingTrackLocationDirInput, + starting: PathfindingTrackLocationInput, ending: PathfindingTrackLocationInput, } @@ -128,11 +120,11 @@ struct PathfindingStep { } impl PathfindingStep { - fn new_init(track: String, position: f64, direction: Direction) -> Self { + fn new_init(track: String, position: f64) -> Self { Self { track, position, - direction, + direction: Direction::StartToStop, // Ignored for initial node switch_direction: None, found: false, starting_step: true, @@ -185,7 +177,7 @@ fn compute_path( k: u8, ) -> Vec { let start = &input.starting; - let start = PathfindingStep::new_init(start.track.0.clone(), start.position, start.direction); + let start = PathfindingStep::new_init(start.track.0.clone(), start.position); let track_sections = infra_cache.track_sections(); // Transform a length (in m) into a cost (in mm). This provide the Ord implementation for our cost using u64. @@ -377,9 +369,7 @@ mod tests { use crate::infra_cache::Graph; use crate::schema::utils::Identifier; use crate::schema::{Direction, DirectionalTrackRange}; - use crate::views::infra::pathfinding::{ - PathfindingInput, PathfindingTrackLocationDirInput, PathfindingTrackLocationInput, - }; + use crate::views::infra::pathfinding::{PathfindingInput, PathfindingTrackLocationInput}; fn expected_path() -> Vec { vec![ @@ -400,10 +390,9 @@ mod tests { let infra_cache = create_small_infra_cache(); let graph = Graph::load(&infra_cache); let input = PathfindingInput { - starting: PathfindingTrackLocationDirInput { + starting: PathfindingTrackLocationInput { track: "A".into(), position: 30.0, - direction: Direction::StartToStop, }, ending: PathfindingTrackLocationInput { track: "C".into(), @@ -424,10 +413,9 @@ mod tests { let infra_cache = create_small_infra_cache(); let graph = Graph::load(&infra_cache); let input = PathfindingInput { - starting: PathfindingTrackLocationDirInput { + starting: PathfindingTrackLocationInput { track: "A".into(), position: 30.0, - direction: Direction::StopToStart, }, ending: PathfindingTrackLocationInput { track: "C".into(), diff --git a/front/src/applications/editor/tools/routeEdition/components/EditRoutePath.tsx b/front/src/applications/editor/tools/routeEdition/components/EditRoutePath.tsx index 6e0fc43ac4b..fb073a632aa 100644 --- a/front/src/applications/editor/tools/routeEdition/components/EditRoutePath.tsx +++ b/front/src/applications/editor/tools/routeEdition/components/EditRoutePath.tsx @@ -42,13 +42,12 @@ export const EditRoutePathLeftPanel: FC<{ state: EditRoutePathState }> = ({ stat const infraID = useSelector(getInfraID); const [isSaving, setIsSaving] = useState(false); const [includeReleaseDetectors, setIncludeReleaseDetectors] = useState(true); - const { entryPoint, exitPoint, entryPointDirection } = state.routeState; + const { entryPoint, exitPoint } = state.routeState; const [postPathfinding] = osrdEditoastApi.endpoints.postInfraByIdPathfinding.useMutation(); const searchCandidates = useCallback(async () => { - if (!entryPoint || !exitPoint || !entryPointDirection || state.optionsState.type === 'loading') - return; + if (!entryPoint || !exitPoint || state.optionsState.type === 'loading') return; setState({ optionsState: { type: 'loading' }, @@ -57,7 +56,6 @@ export const EditRoutePathLeftPanel: FC<{ state: EditRoutePathState }> = ({ stat const payload = await getCompatibleRoutesPayload( infraID as number, entryPoint, - entryPointDirection, exitPoint, dispatch ); @@ -87,7 +85,7 @@ export const EditRoutePathLeftPanel: FC<{ state: EditRoutePathState }> = ({ stat })), }, }); - }, [entryPoint, entryPointDirection, exitPoint, infraID, setState, state.optionsState.type]); + }, [entryPoint, exitPoint, infraID, setState, state.optionsState.type]); const focusedOptionIndex = state.optionsState.type === 'options' ? state.optionsState.focusedOptionIndex : null; @@ -108,12 +106,7 @@ export const EditRoutePathLeftPanel: FC<{ state: EditRoutePathState }> = ({ stat @@ -222,7 +215,6 @@ export const EditRoutePathLeftPanel: FC<{ state: EditRoutePathState }> = ({ stat id: '', // will be replaced by entityToCreateOperation entry_point: omit(entryPoint, 'position'), exit_point: omit(exitPoint, 'position'), - entry_point_direction: entryPointDirection, switches_directions: candidate.data.switches_directions, release_detectors: includeReleaseDetectors ? candidate.data.detectors diff --git a/front/src/applications/editor/tools/routeEdition/components/Endpoints.tsx b/front/src/applications/editor/tools/routeEdition/components/Endpoints.tsx index 758b0ffffe9..57543dc442a 100644 --- a/front/src/applications/editor/tools/routeEdition/components/Endpoints.tsx +++ b/front/src/applications/editor/tools/routeEdition/components/Endpoints.tsx @@ -1,5 +1,4 @@ -import Select from 'react-select'; -import React, { FC, useContext, useMemo } from 'react'; +import React, { FC, useContext } from 'react'; import { BsArrowBarRight, BsBoxArrowInRight } from 'react-icons/bs'; import { HiSwitchVertical } from 'react-icons/hi'; import { FaFlagCheckered } from 'react-icons/fa'; @@ -7,7 +6,7 @@ import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import { getInfraID } from 'reducers/osrdconf/selectors'; -import { BufferStopEntity, DetectorEntity, DIRECTIONS, WayPoint, WayPointEntity } from 'types'; +import { BufferStopEntity, DetectorEntity, WayPoint, WayPointEntity } from 'types'; import EditorContext from 'applications/editor/context'; import { getEntity } from 'applications/editor/data/api'; import EntitySumUp from 'applications/editor/components/EntitySumUp'; @@ -20,20 +19,7 @@ export const EditEndpoints: FC<{ state: RouteState; onChange: (newState: RouteSt onChange, }) => { const { t } = useTranslation(); - const { entryPoint, exitPoint, entryPointDirection } = state; - - const options = useMemo( - () => - DIRECTIONS.map((s) => ({ - value: s, - label: t(`Editor.tools.routes-edition.directions.${s}`), - })), - [t] - ); - const option = useMemo( - () => options.find((o) => o.value === entryPointDirection) || options[0], - [options, entryPointDirection] - ); + const { entryPoint, exitPoint } = state; return ( <> @@ -45,22 +31,6 @@ export const EditEndpoints: FC<{ state: RouteState; onChange: (newState: RouteSt wayPoint={entryPoint} onChange={(wayPoint) => onChange({ ...state, entryPoint: wayPoint })} /> - {entryPoint && ( -
- {t('Editor.tools.routes-edition.start_direction')} -