Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

editoast: explanation of why a simulation is not feasible refactor editoast #9434

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions editoast/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2620,14 +2620,6 @@ paths:
type: string
enum:
- success
- type: object
required:
- status
properties:
status:
type: string
enum:
- path_not_found
- type: object
required:
- pathfinding_result
Expand Down
11 changes: 11 additions & 0 deletions editoast/src/core/simulation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -348,3 +348,14 @@ impl AsCoreRequest<Json<SimulationResponse>> for SimulationRequest {
Some(self.infra)
}
}

impl SimulationResponse {
pub fn simulation_run_time(self) -> Result<u64, Box<Self>> {
match self {
SimulationResponse::Success { provisional, .. } => {
Ok(*provisional.times.last().expect("empty simulation result"))
}
err => Err(Box::from(err)),
}
}
}
15 changes: 5 additions & 10 deletions editoast/src/core/stdcm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,13 @@ use serde::Deserialize;
use serde::Serialize;
use utoipa::ToSchema;

use super::conflict_detection::Conflict;
use super::conflict_detection::TrainRequirements;
use super::pathfinding::PathfindingResultSuccess;
use super::pathfinding::TrackRange;
use super::simulation::PhysicsRollingStock;
use super::simulation::SimulationResponse;
use crate::core::{AsCoreRequest, Json};
use crate::views::path::pathfinding::PathfindingResult;
use crate::core::AsCoreRequest;
use crate::core::Json;

#[derive(Debug, Serialize)]
pub struct STDCMRequest {
Expand Down Expand Up @@ -115,28 +114,24 @@ pub struct UndirectedTrackRange {
pub end: u64,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, ToSchema)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(tag = "status", rename_all = "snake_case")]
// We accepted the difference of memory size taken by variants
// Since there is only on success and others are error cases
#[allow(clippy::large_enum_variant)]
pub enum STDCMResponse {
pub enum STDCMCoreResponse {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that I understand the renaming. This struct is defined inside a module name core so we already know it's relative to core's logic.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I renamed it to avoid conflicts and as, since we have another STDCMResponse in views, and both are used in the same file.

Success {
simulation: SimulationResponse,
path: PathfindingResultSuccess,
departure_time: DateTime<Utc>,
},
PathNotFound,
Conflicts {
pathfinding_result: PathfindingResult,
conflicts: Vec<Conflict>,
},
PreprocessingSimulationError {
error: SimulationResponse,
},
}

impl AsCoreRequest<Json<STDCMResponse>> for STDCMRequest {
impl AsCoreRequest<Json<STDCMCoreResponse>> for STDCMRequest {
const METHOD: reqwest::Method = reqwest::Method::POST;
const URL_PATH: &'static str = "/v2/stdcm";

Expand Down
49 changes: 49 additions & 0 deletions editoast/src/models/work_schedules.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
use std::cmp::max;

use chrono::DateTime;
use chrono::NaiveDateTime;
use chrono::TimeZone;
use chrono::Utc;
use editoast_derive::Model;
use editoast_schemas::infra::TrackRange;
use strum::FromRepr;
Expand All @@ -7,6 +12,8 @@ use serde::Deserialize;
use serde::Serialize;
use utoipa::ToSchema;

use crate::core::stdcm::UndirectedTrackRange;

#[derive(Debug, Clone, Model)]
#[model(table = editoast_models::tables::work_schedule_group)]
#[model(gen(ops = crd, batch_ops = c, list))]
Expand Down Expand Up @@ -38,3 +45,45 @@ pub struct WorkSchedule {
pub work_schedule_type: WorkScheduleType,
pub work_schedule_group_id: i64,
}

impl WorkSchedule {
pub fn map_to_core_work_schedule(
&self,
start_time: DateTime<Utc>,
) -> crate::core::stdcm::WorkSchedule {
crate::core::stdcm::WorkSchedule {
start_time: elapsed_since_time_ms(&self.start_date_time, &start_time),
end_time: elapsed_since_time_ms(&self.end_date_time, &start_time),
track_ranges: self
.track_ranges
.iter()
.map(|track| UndirectedTrackRange {
track_section: track.track.to_string(),
begin: (track.begin * 1000.0) as u64,
end: (track.end * 1000.0) as u64,
})
.collect(),
}
}

pub fn make_stdcm_work_schedule(
&self,
start_time: DateTime<Utc>,
latest_simulation_end: DateTime<Utc>,
) -> Option<crate::core::stdcm::WorkSchedule> {
let search_window_duration = (latest_simulation_end - start_time).num_milliseconds() as u64;

let ws = self.map_to_core_work_schedule(start_time);
if ws.end_time > 0 && ws.start_time < search_window_duration {
return Some(ws);
}
None
}
}

fn elapsed_since_time_ms(time: &NaiveDateTime, start_time: &DateTime<Utc>) -> u64 {
max(
0,
(Utc.from_utc_datetime(time) - start_time).num_milliseconds(),
) as u64
}
4 changes: 3 additions & 1 deletion editoast/src/views/timetable.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
pub mod path_not_found_handler;
pub mod stdcm;
pub mod stdcm_request_payload;

use std::collections::HashMap;

Expand Down Expand Up @@ -57,7 +59,7 @@ crate::routes! {
editoast_common::schemas! {
TimetableResult,
TimetableDetailedResult,
stdcm::schemas(),
stdcm_request_payload::schemas(),
}

#[derive(Debug, Error, EditoastError)]
Expand Down
118 changes: 118 additions & 0 deletions editoast/src/views/timetable/path_not_found_handler.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
use std::sync::Arc;

use chrono::DateTime;
use chrono::Utc;

use crate::core::conflict_detection::ConflictDetectionRequest;
use crate::core::conflict_detection::WorkSchedulesRequest;
use crate::core::simulation::SimulationResponse;
use crate::core::AsCoreRequest;
use crate::core::CoreClient;
use crate::error::Result;
use crate::models::train_schedule::TrainSchedule;
use crate::models::work_schedules::WorkSchedule;
use crate::views::path::pathfinding::PathfindingResult;
use crate::views::timetable::stdcm::STDCMResponse;

use super::stdcm::build_train_requirements;

pub struct PathNotFoundHandler {
pub core_client: Arc<CoreClient>,
pub infra_id: i64,
pub infra_version: String,
pub train_schedules: Vec<TrainSchedule>,
pub simulations: Vec<(SimulationResponse, PathfindingResult)>,
pub work_schedules: Vec<WorkSchedule>,
pub virtual_train_schedule: TrainSchedule,
pub virtual_train_sim_result: SimulationResponse,
pub virtual_train_pathfinding_result: PathfindingResult,
pub earliest_departure_time: DateTime<Utc>,
pub latest_simulation_end: DateTime<Utc>,
}

impl PathNotFoundHandler {
pub async fn handle(self) -> Result<STDCMResponse> {
let virtual_train_id = self.virtual_train_schedule.id;

// Combine the original train schedules with the virtual train schedule.
let train_schedules = [self.train_schedules, vec![self.virtual_train_schedule]].concat();

// Combine the original simulations with the virtual train's simulation results.
let simulations = [
self.simulations,
vec![(
self.virtual_train_sim_result,
self.virtual_train_pathfinding_result.clone(),
)],
]
.concat();

// Build train requirements based on the combined train schedules and simulations
// This prepares the data structure required for conflict detection.
let trains_requirements = build_train_requirements(
train_schedules,
simulations,
self.earliest_departure_time,
self.latest_simulation_end,
);

// Filter the provided work schedules to find those that conflict with the given parameters
// This identifies any work schedules that may overlap with the earliest departure time and maximum run time.
let conflict_work_schedules = make_work_schedules_request(
&self.work_schedules,
self.earliest_departure_time,
self.latest_simulation_end,
);

// Prepare the conflict detection request.
let conflict_detection_request = ConflictDetectionRequest {
infra: self.infra_id,
expected_version: self.infra_version,
trains_requirements,
work_schedules: conflict_work_schedules,
};

// Send the conflict detection request and await the response.
let conflict_detection_response =
conflict_detection_request.fetch(&self.core_client).await?;

// Filter the conflicts to find those specifically related to the virtual train.
let conflicts: Vec<_> = conflict_detection_response
.conflicts
.into_iter()
.filter(|conflict| conflict.train_ids.contains(&virtual_train_id))
.map(|mut conflict| {
conflict.train_ids.retain(|id| id != &virtual_train_id);
conflict
})
.collect();

// Return the conflicts found along with the pathfinding result for the virtual train.
Ok(STDCMResponse::Conflicts {
pathfinding_result: self.virtual_train_pathfinding_result,
conflicts,
})
}
}

fn make_work_schedules_request(
work_schedules: &[WorkSchedule],
start_time: DateTime<Utc>,
latest_simulation_end: DateTime<Utc>,
) -> Option<WorkSchedulesRequest> {
if work_schedules.is_empty() {
return None;
}
let search_window_duration = (latest_simulation_end - start_time).num_milliseconds() as u64;

let work_schedule_requirements = work_schedules
.iter()
.map(|ws| (ws.id, ws.map_to_core_work_schedule(start_time)))
.filter(|(_, ws)| ws.end_time > 0 && ws.start_time < search_window_duration)
.collect();

Some(WorkSchedulesRequest {
start_time,
work_schedule_requirements,
})
}
Loading
Loading