Skip to content

Commit 12a5a59

Browse files
committed
editoast: stdcm: add arrival time parameters
1 parent 1178871 commit 12a5a59

File tree

6 files changed

+137
-18
lines changed

6 files changed

+137
-18
lines changed

core/kt-osrd-utils/src/main/kotlin/fr/sncf/osrd/utils/json/UnitJsonAdapters.kt

+5-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,11 @@ class DurationAdapter : JsonAdapter<Duration?>() {
7575
*/
7676
class DateAdapter : JsonAdapter<ZonedDateTime>() {
7777
@FromJson
78-
override fun fromJson(reader: JsonReader): ZonedDateTime {
78+
override fun fromJson(reader: JsonReader): ZonedDateTime? {
79+
if (reader.peek() == JsonReader.Token.NULL) {
80+
reader.skipValue()
81+
return null
82+
}
7983
return ZonedDateTime.parse(reader.nextString())
8084
}
8185

core/src/main/kotlin/fr/sncf/osrd/api/api_v2/stdcm/STDCMRequestV2.kt

+7
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ class STDCMRequestV2(
6060
class STDCMPathItem(
6161
val locations: List<TrackLocation>,
6262
@Json(name = "stop_duration") val stopDuration: Duration?,
63+
@Json(name = "step_timing_data") val stepTimingData: StepTimingData?,
64+
)
65+
66+
data class StepTimingData(
67+
@Json(name = "arrival_time") val arrivalTime: ZonedDateTime,
68+
@Json(name = "arrival_time_tolerance_before") val arrivalTimeToleranceBefore: Duration,
69+
@Json(name = "arrival_time_tolerance_after") val arrivalTimeToleranceAfter: Duration,
6370
)
6471

6572
class TrackOffset(val track: String, val offset: Offset<TrackSection>)

editoast/openapi.yaml

+35-4
Original file line numberDiff line numberDiff line change
@@ -3017,7 +3017,6 @@ paths:
30173017
type: object
30183018
description: An STDCM request
30193019
required:
3020-
- start_time
30213020
- steps
30223021
- rolling_stock_id
30233022
- comfort
@@ -3035,7 +3034,9 @@ paths:
30353034
maximum_departure_delay:
30363035
type: integer
30373036
format: int64
3038-
description: By how long we can shift the departure time in milliseconds
3037+
description: |-
3038+
By how long we can shift the departure time in milliseconds
3039+
Deprecated, first step data should be used instead
30393040
default: 432000
30403041
minimum: 0
30413042
maximum_run_time:
@@ -3054,6 +3055,8 @@ paths:
30543055
start_time:
30553056
type: string
30563057
format: date-time
3058+
description: Deprecated, first step arrival time should be used instead
3059+
nullable: true
30573060
steps:
30583061
type: array
30593062
items:
@@ -8549,6 +8552,10 @@ components:
85498552
minimum: 0
85508553
location:
85518554
$ref: '#/components/schemas/PathItemLocation'
8555+
timing_data:
8556+
allOf:
8557+
- $ref: '#/components/schemas/StepTimingData'
8558+
nullable: true
85528559
PathfindingOutput:
85538560
type: object
85548561
required:
@@ -10177,7 +10184,6 @@ components:
1017710184
type: object
1017810185
description: An STDCM request
1017910186
required:
10180-
- start_time
1018110187
- steps
1018210188
- rolling_stock_id
1018310189
- comfort
@@ -10195,7 +10201,9 @@ components:
1019510201
maximum_departure_delay:
1019610202
type: integer
1019710203
format: int64
10198-
description: By how long we can shift the departure time in milliseconds
10204+
description: |-
10205+
By how long we can shift the departure time in milliseconds
10206+
Deprecated, first step data should be used instead
1019910207
default: 432000
1020010208
minimum: 0
1020110209
maximum_run_time:
@@ -10214,6 +10222,8 @@ components:
1021410222
start_time:
1021510223
type: string
1021610224
format: date-time
10225+
description: Deprecated, first step arrival time should be used instead
10226+
nullable: true
1021710227
steps:
1021810228
type: array
1021910229
items:
@@ -11487,6 +11497,27 @@ components:
1148711497
type: array
1148811498
items:
1148911499
$ref: '#/components/schemas/RangeAllowance'
11500+
StepTimingData:
11501+
type: object
11502+
required:
11503+
- arrival_time
11504+
- arrival_time_tolerance_before
11505+
- arrival_time_tolerance_after
11506+
properties:
11507+
arrival_time:
11508+
type: string
11509+
format: date-time
11510+
description: Time at which the train should arrive at the location
11511+
arrival_time_tolerance_after:
11512+
type: integer
11513+
format: int64
11514+
description: The train may arrive up to this duration after the expected arrival time
11515+
minimum: 0
11516+
arrival_time_tolerance_before:
11517+
type: integer
11518+
format: int64
11519+
description: The train may arrive up to this duration before the expected arrival time
11520+
minimum: 0
1149011521
Study:
1149111522
type: object
1149211523
required:

editoast/src/core/v2/stdcm.rs

+13
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,19 @@ pub struct STDCMPathItem {
6464
pub locations: Vec<TrackOffset>,
6565
/// Stop duration in milliseconds. None if the train does not stop at this path item.
6666
pub stop_duration: Option<u64>,
67+
/// If specified, describes when the train may arrive at the location
68+
pub step_timing_data: Option<STDCMStepTimingData>,
69+
}
70+
71+
/// Contains the data of a step timing, when it is specified
72+
#[derive(Debug, Serialize)]
73+
pub struct STDCMStepTimingData {
74+
/// Time the train should arrive at this point
75+
pub arrival_time: DateTime<Utc>,
76+
/// Tolerance for the arrival time, when it arrives before the expected time, in ms
77+
pub arrival_time_tolerance_before: u64,
78+
/// Tolerance for the arrival time, when it arrives after the expected time, in ms
79+
pub arrival_time_tolerance_after: u64,
6780
}
6881

6982
/// Lighter description of a work schedule, only contains what's relevant

editoast/src/views/v2/timetable/stdcm.rs

+64-11
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use actix_web::web::Json;
44
use actix_web::web::Path;
55
use actix_web::web::Query;
66
use chrono::Utc;
7-
use chrono::{DateTime, NaiveDateTime, TimeZone};
7+
use chrono::{DateTime, Duration, NaiveDateTime, TimeZone};
88
use editoast_derive::EditoastError;
99
use editoast_schemas::train_schedule::MarginValue;
1010
use editoast_schemas::train_schedule::PathItemLocation;
@@ -19,10 +19,10 @@ use utoipa::IntoParams;
1919
use utoipa::ToSchema;
2020

2121
use crate::core::v2::simulation::SimulationResponse;
22-
use crate::core::v2::stdcm::STDCMRequest;
2322
use crate::core::v2::stdcm::STDCMResponse;
2423
use crate::core::v2::stdcm::TrainRequirement;
2524
use crate::core::v2::stdcm::{STDCMPathItem, STDCMWorkSchedule, UndirectedTrackRange};
25+
use crate::core::v2::stdcm::{STDCMRequest, STDCMStepTimingData};
2626
use crate::core::AsCoreRequest;
2727
use crate::core::CoreClient;
2828
use crate::error::Result;
@@ -48,6 +48,7 @@ crate::routes! {
4848
editoast_common::schemas! {
4949
STDCMRequestPayload,
5050
PathfindingItem,
51+
StepTimingData,
5152
}
5253

5354
#[derive(Debug, Error, EditoastError, Serialize)]
@@ -70,11 +71,13 @@ enum STDCMError {
7071
/// An STDCM request
7172
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)]
7273
pub struct STDCMRequestPayload {
73-
start_time: DateTime<Utc>,
74+
/// Deprecated, first step arrival time should be used instead
75+
start_time: Option<DateTime<Utc>>,
7476
steps: Vec<PathfindingItem>,
7577
rolling_stock_id: i64,
7678
comfort: Comfort,
7779
/// By how long we can shift the departure time in milliseconds
80+
/// Deprecated, first step data should be used instead
7881
#[serde(default = "default_maximum_departure_delay")]
7982
#[schema(default = default_maximum_departure_delay)]
8083
maximum_departure_delay: u64,
@@ -106,6 +109,18 @@ struct PathfindingItem {
106109
duration: Option<u64>,
107110
/// The associated location
108111
location: PathItemLocation,
112+
/// Time at which the train should arrive at the location, if specified
113+
timing_data: Option<StepTimingData>,
114+
}
115+
116+
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
117+
struct StepTimingData {
118+
/// Time at which the train should arrive at the location
119+
arrival_time: DateTime<Utc>,
120+
/// The train may arrive up to this duration before the expected arrival time
121+
arrival_time_tolerance_before: u64,
122+
/// The train may arrive up to this duration after the expected arrival time
123+
arrival_time_tolerance_after: u64,
109124
}
110125

111126
const TWO_HOURS_IN_MILLISECONDS: u64 = 2 * 60 * 60 * 60;
@@ -209,6 +224,8 @@ async fn stdcm(
209224
}
210225
};
211226

227+
let departure_time = get_earliest_departure_time(&data, maximum_run_time);
228+
212229
// 3. Parse stdcm path items
213230
let path_items = parse_stdcm_steps(conn, &data, &infra).await?;
214231

@@ -223,7 +240,7 @@ async fn stdcm(
223240
.clone(),
224241
comfort: data.comfort,
225242
path_items,
226-
start_time: data.start_time,
243+
start_time: departure_time,
227244
trains_requirements,
228245
maximum_departure_delay: Some(data.maximum_departure_delay),
229246
maximum_run_time,
@@ -234,7 +251,7 @@ async fn stdcm(
234251
time_step: Some(2000),
235252
work_schedules: build_work_schedules(
236253
conn,
237-
data.start_time,
254+
departure_time,
238255
data.maximum_departure_delay,
239256
maximum_run_time,
240257
)
@@ -246,6 +263,34 @@ async fn stdcm(
246263
Ok(Json(stdcm_response))
247264
}
248265

266+
/// Returns the earliest time at which the train may start
267+
fn get_earliest_departure_time(data: &STDCMRequestPayload, maximum_run_time: u64) -> DateTime<Utc> {
268+
// Prioritize: start time, or first step time, or (first specified time - max run time)
269+
data.start_time.unwrap_or(
270+
data.steps
271+
.first()
272+
.and_then(|step| step.timing_data.clone())
273+
.and_then(|data| Option::from(data.arrival_time))
274+
.unwrap_or(
275+
get_earliest_step_time(data) - Duration::milliseconds(maximum_run_time as i64),
276+
),
277+
)
278+
}
279+
280+
/// Returns the earliest time that has been set on any step
281+
fn get_earliest_step_time(data: &STDCMRequestPayload) -> DateTime<Utc> {
282+
// Get the earliest time that has been specified for any step
283+
data.start_time
284+
.or_else(|| {
285+
data.steps
286+
.iter()
287+
.flat_map(|step| step.timing_data.iter())
288+
.map(|data| data.arrival_time)
289+
.next()
290+
})
291+
.expect("No time specified for stdcm request")
292+
}
293+
249294
/// get the maximum run time, compute it if unspecified.
250295
/// returns an enum with either the result or a SimulationResponse if it failed
251296
async fn get_maximum_run_time(
@@ -263,13 +308,16 @@ async fn get_maximum_run_time(
263308
});
264309
}
265310

311+
// Doesn't matter for now, but eventually it will affect tmp speed limits
312+
let approx_start_time = get_earliest_step_time(data);
313+
266314
let train_schedule = TrainSchedule {
267315
id: 0,
268316
train_name: "".to_string(),
269317
labels: vec![],
270318
rolling_stock_name: rolling_stock.name.clone(),
271319
timetable_id,
272-
start_time: data.start_time,
320+
start_time: approx_start_time,
273321
schedule: vec![],
274322
margins: build_single_margin(data.margin),
275323
initial_speed: 0.0,
@@ -372,21 +420,26 @@ async fn parse_stdcm_steps(
372420
) -> Result<Vec<STDCMPathItem>> {
373421
let path_items = data.steps.clone();
374422
let mut locations = Vec::with_capacity(path_items.len());
375-
let mut durations = Vec::with_capacity(path_items.len());
376423
for item in path_items {
377424
locations.push(item.location);
378-
durations.push(item.duration);
379425
}
380426

381427
let track_offsets = extract_location_from_path_items(conn, infra.id, &locations).await?;
382428
let track_offsets = track_offsets.map_err::<STDCMError, _>(|err| err.into())?;
383429

384430
Ok(track_offsets
385431
.iter()
386-
.zip(durations)
387-
.map(|(track_offset, duration)| STDCMPathItem {
388-
stop_duration: duration,
432+
.zip(&data.steps)
433+
.map(|(track_offset, path_item)| STDCMPathItem {
434+
stop_duration: path_item.duration,
389435
locations: track_offset.to_vec(),
436+
step_timing_data: path_item.timing_data.as_ref().map(|timing_data| {
437+
STDCMStepTimingData {
438+
arrival_time: timing_data.arrival_time,
439+
arrival_time_tolerance_before: timing_data.arrival_time_tolerance_before,
440+
arrival_time_tolerance_after: timing_data.arrival_time_tolerance_after,
441+
}
442+
}),
390443
})
391444
.collect())
392445
}

front/src/common/api/generatedEditoastApi.ts

+13-2
Original file line numberDiff line numberDiff line change
@@ -1751,14 +1751,16 @@ export type PostV2TimetableByIdStdcmApiArg = {
17511751
comfort: Comfort;
17521752
/** Can be a percentage `X%`, a time in minutes per 100 kilometer `Xmin/100km` or `None` */
17531753
margin?: string | null;
1754-
/** By how long we can shift the departure time in milliseconds */
1754+
/** By how long we can shift the departure time in milliseconds
1755+
Deprecated, first step data should be used instead */
17551756
maximum_departure_delay?: number;
17561757
/** Specifies how long the total run time can be in milliseconds */
17571758
maximum_run_time?: number | null;
17581759
rolling_stock_id: number;
17591760
/** Train categories for speed limits */
17601761
speed_limit_tags?: string | null;
1761-
start_time: string;
1762+
/** Deprecated, first step arrival time should be used instead */
1763+
start_time?: string | null;
17621764
steps: PathfindingItem[];
17631765
/** Margin after the train passage in milliseconds
17641766
@@ -3684,10 +3686,19 @@ export type PathItemLocation =
36843686
/** The [UIC](https://en.wikipedia.org/wiki/List_of_UIC_country_codes) code of an operational point */
36853687
uic: number;
36863688
};
3689+
export type StepTimingData = {
3690+
/** Time at which the train should arrive at the location */
3691+
arrival_time: string;
3692+
/** The train may arrive up to this duration after the expected arrival time */
3693+
arrival_time_tolerance_after: number;
3694+
/** The train may arrive up to this duration before the expected arrival time */
3695+
arrival_time_tolerance_before: number;
3696+
};
36873697
export type PathfindingItem = {
36883698
/** The stop duration in milliseconds, None if the train does not stop. */
36893699
duration?: number | null;
36903700
location: PathItemLocation;
3701+
timing_data?: StepTimingData | null;
36913702
};
36923703
export type Distribution = 'STANDARD' | 'MARECO';
36933704
export type TrainScheduleBase = {

0 commit comments

Comments
 (0)