Skip to content

Commit be809ed

Browse files
committed
editoast: add simulation summary and simulation endpoints for paced train
Signed-off-by: Youness CHRIFI ALAOUI <[email protected]>
1 parent b7a0b08 commit be809ed

File tree

9 files changed

+283
-67
lines changed

9 files changed

+283
-67
lines changed

editoast/openapi.yaml

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5139,6 +5139,8 @@ components:
51395139
- $ref: '#/components/schemas/EditoastOperationErrorObjectNotFound'
51405140
- $ref: '#/components/schemas/EditoastPacedTrainErrorBatchPacedTrainNotFound'
51415141
- $ref: '#/components/schemas/EditoastPacedTrainErrorDatabase'
5142+
- $ref: '#/components/schemas/EditoastPacedTrainErrorInfraNotFound'
5143+
- $ref: '#/components/schemas/EditoastPacedTrainErrorNotFound'
51425144
- $ref: '#/components/schemas/EditoastPaginationErrorInvalidPage'
51435145
- $ref: '#/components/schemas/EditoastPaginationErrorInvalidPageSize'
51445146
- $ref: '#/components/schemas/EditoastPathfindingErrorInfraNotFound'
@@ -5676,6 +5678,54 @@ components:
56765678
type: string
56775679
enum:
56785680
- editoast:paced_train:Database
5681+
EditoastPacedTrainErrorInfraNotFound:
5682+
type: object
5683+
required:
5684+
- type
5685+
- status
5686+
- message
5687+
properties:
5688+
context:
5689+
type: object
5690+
required:
5691+
- infra_id
5692+
properties:
5693+
infra_id:
5694+
type: integer
5695+
message:
5696+
type: string
5697+
status:
5698+
type: integer
5699+
enum:
5700+
- 404
5701+
type:
5702+
type: string
5703+
enum:
5704+
- editoast:paced_train:InfraNotFound
5705+
EditoastPacedTrainErrorNotFound:
5706+
type: object
5707+
required:
5708+
- type
5709+
- status
5710+
- message
5711+
properties:
5712+
context:
5713+
type: object
5714+
required:
5715+
- paced_train_id
5716+
properties:
5717+
paced_train_id:
5718+
type: integer
5719+
message:
5720+
type: string
5721+
status:
5722+
type: integer
5723+
enum:
5724+
- 404
5725+
type:
5726+
type: string
5727+
enum:
5728+
- editoast:paced_train:NotFound
56795729
EditoastPaginationErrorInvalidPage:
56805730
type: object
56815731
required:
@@ -8542,7 +8592,7 @@ components:
85428592
timetable_id:
85438593
type: integer
85448594
format: int64
8545-
description: Timetable attached to the train schedule
8595+
description: Timetable attached to the paced train
85468596
nullable: true
85478597
PacedTrainResult:
85488598
allOf:

editoast/src/models/paced_train.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::models::train_schedule::TrainSchedule;
12
use chrono::DateTime;
23
use chrono::Duration as ChronoDuration;
34
use chrono::Utc;
@@ -101,3 +102,25 @@ impl From<PacedTrain> for PacedTrainBase {
101102
}
102103
}
103104
}
105+
106+
impl From<PacedTrain> for TrainSchedule {
107+
fn from(paced_train: PacedTrain) -> Self {
108+
Self {
109+
id: paced_train.id,
110+
train_name: paced_train.train_name,
111+
labels: paced_train.labels.into(),
112+
rolling_stock_name: paced_train.rolling_stock_name,
113+
timetable_id: paced_train.timetable_id,
114+
path: paced_train.path,
115+
start_time: paced_train.start_time,
116+
schedule: paced_train.schedule,
117+
margins: paced_train.margins,
118+
initial_speed: paced_train.initial_speed,
119+
comfort: paced_train.comfort,
120+
constraint_distribution: paced_train.constraint_distribution,
121+
speed_limit_tag: paced_train.speed_limit_tag,
122+
power_restrictions: paced_train.power_restrictions,
123+
options: paced_train.options,
124+
}
125+
}
126+
}

editoast/src/views/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ pub struct InfraIdQueryParam {
172172

173173
#[derive(Debug, Serialize, ToSchema)]
174174
#[serde(tag = "status", rename_all = "snake_case")]
175-
enum SimulationSummaryResult {
175+
pub enum SimulationSummaryResult {
176176
/// Minimal information on a simulation's result
177177
Success {
178178
/// Length of a path in mm

editoast/src/views/paced_train.rs

Lines changed: 156 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ use std::collections::HashSet;
44
use crate::core::simulation::SimulationResponse;
55
use crate::error::Result;
66
use crate::models::prelude::*;
7+
use crate::models::train_schedule::TrainSchedule;
8+
use crate::models::Infra;
79
use crate::views::projection::ProjectPathTrainResult;
810
use crate::views::ListId;
911
use axum::extract::Json;
@@ -15,13 +17,16 @@ use editoast_authz::BuiltinRole;
1517
use editoast_derive::EditoastError;
1618
use editoast_models::DbConnectionPoolV2;
1719
use editoast_schemas::paced_train::PacedTrainBase;
20+
use itertools::Itertools;
1821
use serde::{Deserialize, Serialize};
1922
use thiserror::Error;
2023
use utoipa::IntoParams;
2124
use utoipa::ToSchema;
2225

2326
use super::path::pathfinding::PathfindingResult;
2427
use super::projection::ProjectPathForm;
28+
use super::train_schedule::simulation_response;
29+
use super::train_schedule::train_simulation_batch;
2530
use super::AppState;
2631
use super::AuthenticationExt;
2732
use super::InfraIdQueryParam;
@@ -54,14 +59,20 @@ enum PacedTrainError {
5459
#[error("{count} paced train(s) could not be found")]
5560
#[editoast_error(status = 404)]
5661
BatchPacedTrainNotFound { count: usize },
62+
#[error("Paced train '{paced_train_id}', could not be found")]
63+
#[editoast_error(status = 404)]
64+
NotFound { paced_train_id: i64 },
65+
#[error("Infra '{infra_id}', could not be found")]
66+
#[editoast_error(status = 404)]
67+
InfraNotFound { infra_id: i64 },
5768
#[error(transparent)]
5869
#[editoast_error(status = 500)]
5970
Database(#[from] editoast_models::model::Error),
6071
}
6172

6273
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
6374
pub struct PacedTrainForm {
64-
/// Timetable attached to the train schedule
75+
/// Timetable attached to the paced train
6576
pub timetable_id: Option<i64>,
6677
#[serde(flatten)]
6778
pub paced_train_base: PacedTrainBase,
@@ -181,19 +192,61 @@ struct SimulationBatchForm {
181192
)]
182193
async fn simulation_summary(
183194
State(AppState {
184-
db_pool: _db_pool,
185-
valkey: _valkey_client,
186-
core_client: _core,
195+
db_pool,
196+
valkey: valkey_client,
197+
core_client: core,
187198
..
188199
}): State<AppState>,
189-
Extension(_auth): AuthenticationExt,
200+
Extension(auth): AuthenticationExt,
190201
Json(SimulationBatchForm {
191-
infra_id: _infra_id,
192-
electrical_profile_set_id: _electrical_profile_set_id,
193-
ids: _paced_train_ids,
202+
infra_id,
203+
electrical_profile_set_id,
204+
ids: paced_train_ids,
194205
}): Json<SimulationBatchForm>,
195206
) -> Result<Json<HashMap<i64, SimulationSummaryResult>>> {
196-
todo!();
207+
let authorized = auth
208+
.check_roles([BuiltinRole::InfraRead, BuiltinRole::TimetableRead].into())
209+
.await
210+
.map_err(AuthorizationError::AuthError)?;
211+
if !authorized {
212+
return Err(AuthorizationError::Forbidden.into());
213+
}
214+
215+
let conn = &mut db_pool.get().await?;
216+
217+
let infra = Infra::retrieve_or_fail(conn, infra_id, || PacedTrainError::InfraNotFound {
218+
infra_id,
219+
})
220+
.await?;
221+
222+
let paced_trains: Vec<PacedTrain> =
223+
PacedTrain::retrieve_batch_or_fail(conn, paced_train_ids, |missing| {
224+
PacedTrainError::BatchPacedTrainNotFound {
225+
count: missing.len(),
226+
}
227+
})
228+
.await?;
229+
let paced_trains_to_ts: Vec<TrainSchedule> = paced_trains.clone().into_iter().map_into().collect();
230+
231+
let simulations = train_simulation_batch(
232+
conn,
233+
valkey_client,
234+
core,
235+
&paced_trains_to_ts,
236+
&infra,
237+
electrical_profile_set_id,
238+
)
239+
.await?;
240+
241+
// Transform simulations to simulation summary
242+
let mut simulation_summaries = HashMap::new();
243+
for (paced_train, sim) in paced_trains.into_iter().zip(simulations) {
244+
let (sim, _) = sim;
245+
let simulation_summary_result = simulation_response(sim);
246+
simulation_summaries.insert(paced_train.id, simulation_summary_result);
247+
}
248+
249+
Ok(Json(simulation_summaries))
197250
}
198251

199252
/// Get a path from a paced train given an infrastructure id and a paced train id
@@ -241,23 +294,53 @@ pub struct ElectricalProfileSetIdQueryParam {
241294
)]
242295
async fn simulation(
243296
State(AppState {
244-
valkey: _valkey_client,
245-
core_client: _core_client,
246-
db_pool: _db_pool,
297+
valkey: valkey_client,
298+
core_client,
299+
db_pool,
247300
..
248301
}): State<AppState>,
249-
Extension(_auth): AuthenticationExt,
250-
Path(PacedTrainIdParam {
251-
id: _paced_train_id,
252-
}): Path<PacedTrainIdParam>,
253-
Query(InfraIdQueryParam {
254-
infra_id: _infra_id,
255-
}): Query<InfraIdQueryParam>,
302+
Extension(auth): AuthenticationExt,
303+
Path(PacedTrainIdParam { id: paced_train_id }): Path<PacedTrainIdParam>,
304+
Query(InfraIdQueryParam { infra_id }): Query<InfraIdQueryParam>,
256305
Query(ElectricalProfileSetIdQueryParam {
257-
electrical_profile_set_id: _electrical_profile_set_id,
306+
electrical_profile_set_id,
258307
}): Query<ElectricalProfileSetIdQueryParam>,
259308
) -> Result<Json<SimulationResponse>> {
260-
todo!();
309+
let authorized = auth
310+
.check_roles([BuiltinRole::InfraRead, BuiltinRole::TimetableRead].into())
311+
.await
312+
.map_err(AuthorizationError::AuthError)?;
313+
if !authorized {
314+
return Err(AuthorizationError::Forbidden.into());
315+
}
316+
317+
// Retrieve infra or fail
318+
let infra = Infra::retrieve_or_fail(&mut db_pool.get().await?, infra_id, || {
319+
PacedTrainError::InfraNotFound { infra_id }
320+
})
321+
.await?;
322+
323+
// Retrieve paced_train or fail
324+
let paced_train =
325+
PacedTrain::retrieve_or_fail(&mut db_pool.get().await?, paced_train_id, || {
326+
PacedTrainError::NotFound { paced_train_id }
327+
})
328+
.await?;
329+
330+
// Compute simulation of a paced_train
331+
let (simulation, _) = train_simulation_batch(
332+
&mut db_pool.get().await?,
333+
valkey_client,
334+
core_client,
335+
&[paced_train.into()],
336+
&infra,
337+
electrical_profile_set_id,
338+
)
339+
.await?
340+
.pop()
341+
.unwrap();
342+
343+
Ok(Json(simulation))
261344
}
262345

263346
/// Projects the space time curves and paths of a number of paced trains onto a given path
@@ -294,17 +377,26 @@ async fn project_path(
294377

295378
#[cfg(test)]
296379
mod tests {
380+
use chrono::Duration;
381+
use editoast_models::DbConnectionPoolV2;
382+
use editoast_schemas::paced_train::{Paced, PacedTrainBase};
383+
use editoast_schemas::train_schedule::TrainScheduleBase;
297384
use rstest::rstest;
298385
use serde_json::json;
299386

387+
use crate::models::paced_train::PacedTrainChangeset;
300388
use crate::models::prelude::*;
389+
use crate::views::test_app::TestApp;
390+
use crate::views::train_schedule::tests::mocked_core_pathfinding_sim_and_proj;
301391
use crate::{
302392
models::{
303393
fixtures::{create_simple_paced_train, create_timetable, simple_paced_train_base},
304394
paced_train::PacedTrain,
305395
},
306396
views::{paced_train::PacedTrainResult, test_app::TestAppBuilder},
307397
};
398+
use crate::models::fixtures::create_small_infra;
399+
use crate::models::fixtures::create_fast_rolling_stock;
308400
use axum::http::StatusCode;
309401

310402
#[rstest]
@@ -344,4 +436,47 @@ mod tests {
344436

345437
assert!(!exists);
346438
}
439+
440+
async fn app_infra_id_paced_train_id_for_simulation_tests() -> (TestApp, i64, i64) {
441+
let db_pool = DbConnectionPoolV2::for_tests();
442+
let small_infra = create_small_infra(&mut db_pool.get_ok()).await;
443+
let rolling_stock =
444+
create_fast_rolling_stock(&mut db_pool.get_ok(), "simulation_rolling_stock").await;
445+
let paced_train_base: PacedTrainBase = PacedTrainBase {
446+
train_schedule_base: TrainScheduleBase {
447+
rolling_stock_name: rolling_stock.name.clone(),
448+
..serde_json::from_str(include_str!("../tests/train_schedules/simple.json"))
449+
.expect("Unable to parse")
450+
},
451+
paced: Paced {
452+
duration: Duration::hours(1).try_into().unwrap(),
453+
step: Duration::minutes(15).try_into().unwrap(),
454+
},
455+
};
456+
let paced_train: PacedTrainChangeset = paced_train_base.into();
457+
let paced_train = paced_train
458+
.create(&mut db_pool.get_ok())
459+
.await
460+
.expect("Failed to create paced train");
461+
let core = mocked_core_pathfinding_sim_and_proj(paced_train.id);
462+
let app = TestAppBuilder::new()
463+
.db_pool(db_pool.clone())
464+
.core_client(core.into())
465+
.build();
466+
(app, small_infra.id, paced_train.id)
467+
}
468+
469+
#[rstest]
470+
async fn paced_train_simulation() {
471+
let (app, infra_id, train_schedule_id) =
472+
app_infra_id_paced_train_id_for_simulation_tests().await;
473+
let request = app.get(
474+
format!(
475+
"/paced_train/{}/simulation/?infra_id={}",
476+
train_schedule_id, infra_id
477+
)
478+
.as_str(),
479+
);
480+
app.fetch(request).assert_status(StatusCode::OK);
481+
}
347482
}

editoast/src/views/timetable.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ async fn paced_train(
310310
let changesets = paced_trains
311311
.into_iter()
312312
.map(PacedTrainChangeset::from)
313-
.map(|cs| cs.timetable_id(timetable_id))
313+
.map(|cs: PacedTrainChangeset| cs.timetable_id(timetable_id))
314314
.collect::<Vec<_>>();
315315

316316
// Create a batch of paced trains

0 commit comments

Comments
 (0)