Skip to content

Commit 0677bce

Browse files
committed
editoast: stdcm: add arrival time parameters
1 parent f39a86d commit 0677bce

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
@@ -5210,6 +5210,10 @@ components:
52105210
type: integer
52115211
location:
52125212
$ref: '#/components/schemas/PathItemLocation'
5213+
timing_data:
5214+
allOf:
5215+
- $ref: '#/components/schemas/StepTimingData'
5216+
nullable: true
52135217
required:
52145218
- location
52155219
type: object
@@ -6876,7 +6880,9 @@ components:
68766880
type: string
68776881
maximum_departure_delay:
68786882
default: 432000
6879-
description: By how long we can shift the departure time in milliseconds
6883+
description: |-
6884+
By how long we can shift the departure time in milliseconds
6885+
Deprecated, first step data should be used instead
68806886
format: int64
68816887
minimum: 0
68826888
type: integer
@@ -6894,7 +6900,9 @@ components:
68946900
nullable: true
68956901
type: string
68966902
start_time:
6903+
description: Deprecated, first step arrival time should be used instead
68976904
format: date-time
6905+
nullable: true
68986906
type: string
68996907
steps:
69006908
items:
@@ -6919,7 +6927,6 @@ components:
69196927
minimum: 0
69206928
type: integer
69216929
required:
6922-
- start_time
69236930
- steps
69246931
- rolling_stock_id
69256932
- comfort
@@ -8175,6 +8182,27 @@ components:
81758182
- ranges
81768183
- distribution
81778184
type: object
8185+
StepTimingData:
8186+
properties:
8187+
arrival_time:
8188+
description: Time at which the train should arrive at the location
8189+
format: date-time
8190+
type: string
8191+
arrival_time_tolerance_after:
8192+
description: The train may arrive up to this duration after the expected arrival time
8193+
format: int64
8194+
minimum: 0
8195+
type: integer
8196+
arrival_time_tolerance_before:
8197+
description: The train may arrive up to this duration before the expected arrival time
8198+
format: int64
8199+
minimum: 0
8200+
type: integer
8201+
required:
8202+
- arrival_time
8203+
- arrival_time_tolerance_before
8204+
- arrival_time_tolerance_after
8205+
type: object
81788206
Study:
81798207
properties:
81808208
actual_end_date:
@@ -12433,7 +12461,9 @@ paths:
1243312461
type: string
1243412462
maximum_departure_delay:
1243512463
default: 432000
12436-
description: By how long we can shift the departure time in milliseconds
12464+
description: |-
12465+
By how long we can shift the departure time in milliseconds
12466+
Deprecated, first step data should be used instead
1243712467
format: int64
1243812468
minimum: 0
1243912469
type: integer
@@ -12451,7 +12481,9 @@ paths:
1245112481
nullable: true
1245212482
type: string
1245312483
start_time:
12484+
description: Deprecated, first step arrival time should be used instead
1245412485
format: date-time
12486+
nullable: true
1245512487
type: string
1245612488
steps:
1245712489
items:
@@ -12476,7 +12508,6 @@ paths:
1247612508
minimum: 0
1247712509
type: integer
1247812510
required:
12479-
- start_time
1248012511
- steps
1248112512
- rolling_stock_id
1248212513
- comfort

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
@@ -1753,14 +1753,16 @@ export type PostV2TimetableByIdStdcmApiArg = {
17531753
comfort: Comfort;
17541754
/** Can be a percentage `X%`, a time in minutes per 100 kilometer `Xmin/100km` or `None` */
17551755
margin?: string | null;
1756-
/** By how long we can shift the departure time in milliseconds */
1756+
/** By how long we can shift the departure time in milliseconds
1757+
Deprecated, first step data should be used instead */
17571758
maximum_departure_delay?: number;
17581759
/** Specifies how long the total run time can be in milliseconds */
17591760
maximum_run_time?: number | null;
17601761
rolling_stock_id: number;
17611762
/** Train categories for speed limits */
17621763
speed_limit_tags?: string | null;
1763-
start_time: string;
1764+
/** Deprecated, first step arrival time should be used instead */
1765+
start_time?: string | null;
17641766
steps: PathfindingItem[];
17651767
/** Margin after the train passage in milliseconds
17661768
@@ -3708,10 +3710,19 @@ export type PathItemLocation =
37083710
/** The [UIC](https://en.wikipedia.org/wiki/List_of_UIC_country_codes) code of an operational point */
37093711
uic: number;
37103712
};
3713+
export type StepTimingData = {
3714+
/** Time at which the train should arrive at the location */
3715+
arrival_time: string;
3716+
/** The train may arrive up to this duration after the expected arrival time */
3717+
arrival_time_tolerance_after: number;
3718+
/** The train may arrive up to this duration before the expected arrival time */
3719+
arrival_time_tolerance_before: number;
3720+
};
37113721
export type PathfindingItem = {
37123722
/** The stop duration in milliseconds, None if the train does not stop. */
37133723
duration?: number | null;
37143724
location: PathItemLocation;
3725+
timing_data?: StepTimingData | null;
37153726
};
37163727
export type Distribution = 'STANDARD' | 'MARECO';
37173728
export type TrainScheduleBase = {

0 commit comments

Comments
 (0)