Skip to content

Commit 018d1fa

Browse files
committed
core, editoast: return conflict requirements
Closes: #8680 Signed-off-by: Simon Ser <[email protected]>
1 parent a53ca8a commit 018d1fa

File tree

8 files changed

+173
-21
lines changed

8 files changed

+173
-21
lines changed

core/src/main/java/fr/sncf/osrd/api/ConflictDetectionEndpoint.java

+21-1
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,31 @@ public enum ConflictType {
9090
@Json(name = "conflict_type")
9191
public final ConflictType conflictType;
9292

93-
public Conflict(Collection<Long> trainIds, double startTime, double endTime, ConflictType conflictType) {
93+
public final transient Collection<ConflictRequirement> requirements;
94+
95+
public Conflict(
96+
Collection<Long> trainIds,
97+
double startTime,
98+
double endTime,
99+
ConflictType conflictType,
100+
Collection<ConflictRequirement> requirements) {
94101
this.trainIds = trainIds;
95102
this.startTime = startTime;
96103
this.endTime = endTime;
97104
this.conflictType = conflictType;
105+
this.requirements = requirements;
106+
}
107+
}
108+
109+
public static class ConflictRequirement {
110+
public final String zone;
111+
public final double startTime;
112+
public final double endTime;
113+
114+
public ConflictRequirement(String zone, double startTime, double endTime) {
115+
this.zone = zone;
116+
this.startTime = startTime;
117+
this.endTime = endTime;
98118
}
99119
}
100120

core/src/main/java/fr/sncf/osrd/conflicts/Conflicts.kt

+48-19
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.carrotsearch.hppc.IntArrayList
44
import com.squareup.moshi.Json
55
import fr.sncf.osrd.api.ConflictDetectionEndpoint.ConflictDetectionResult.Conflict
66
import fr.sncf.osrd.api.ConflictDetectionEndpoint.ConflictDetectionResult.Conflict.ConflictType
7+
import fr.sncf.osrd.api.ConflictDetectionEndpoint.ConflictDetectionResult.ConflictRequirement
78
import fr.sncf.osrd.standalone_sim.result.ResultTrain.RoutingRequirement
89
import fr.sncf.osrd.standalone_sim.result.ResultTrain.SpacingRequirement
910
import kotlin.math.max
@@ -171,12 +172,15 @@ class IncrementalConflictDetectorImpl(trainRequirements: List<TrainRequirements>
171172
// look for requirement times overlaps.
172173
// as spacing requirements are exclusive, any overlap is a conflict
173174
val res = mutableListOf<Conflict>()
174-
for (requirements in spacingZoneRequirements.values) {
175-
for (conflictGroup in detectRequirementConflicts(requirements) { _, _ -> true }) {
175+
for (entry in spacingZoneRequirements) {
176+
for (conflictGroup in detectRequirementConflicts(entry.value) { _, _ -> true }) {
176177
val trains = conflictGroup.map { it.trainId }
177178
val beginTime = conflictGroup.minBy { it.beginTime }.beginTime
178179
val endTime = conflictGroup.maxBy { it.endTime }.endTime
179-
res.add(Conflict(trains, beginTime, endTime, ConflictType.SPACING))
180+
val conflictReq = ConflictRequirement(entry.key, beginTime, endTime)
181+
res.add(
182+
Conflict(trains, beginTime, endTime, ConflictType.SPACING, listOf(conflictReq))
183+
)
180184
}
181185
}
182186
return res
@@ -185,13 +189,16 @@ class IncrementalConflictDetectorImpl(trainRequirements: List<TrainRequirements>
185189
private fun detectRoutingConflicts(): List<Conflict> {
186190
// for each zone, check compatibility of overlapping requirements
187191
val res = mutableListOf<Conflict>()
188-
for (requirements in routingZoneRequirements.values) {
192+
for (entry in routingZoneRequirements) {
189193
for (conflictGroup in
190-
detectRequirementConflicts(requirements) { a, b -> a.config != b.config }) {
194+
detectRequirementConflicts(entry.value) { a, b -> a.config != b.config }) {
191195
val trains = conflictGroup.map { it.trainId }
192196
val beginTime = conflictGroup.minBy { it.beginTime }.beginTime
193197
val endTime = conflictGroup.maxBy { it.endTime }.endTime
194-
res.add(Conflict(trains, beginTime, endTime, ConflictType.ROUTING))
198+
val conflictReq = ConflictRequirement(entry.key, beginTime, endTime)
199+
res.add(
200+
Conflict(trains, beginTime, endTime, ConflictType.ROUTING, listOf(conflictReq))
201+
)
195202
}
196203
}
197204
return res
@@ -218,9 +225,16 @@ class IncrementalConflictDetectorImpl(trainRequirements: List<TrainRequirements>
218225
for (otherReq in requirements) {
219226
val beginTime = max(req.beginTime, otherReq.beginTime)
220227
val endTime = min(req.endTime, otherReq.endTime)
228+
val conflictReq = ConflictRequirement(req.zone, beginTime, endTime)
221229
if (beginTime < endTime)
222230
res.add(
223-
Conflict(listOf(otherReq.trainId), beginTime, endTime, ConflictType.SPACING)
231+
Conflict(
232+
listOf(otherReq.trainId),
233+
beginTime,
234+
endTime,
235+
ConflictType.SPACING,
236+
listOf(conflictReq)
237+
)
224238
)
225239
}
226240

@@ -238,9 +252,16 @@ class IncrementalConflictDetectorImpl(trainRequirements: List<TrainRequirements>
238252
if (otherReq.config == zoneReqConfig) continue
239253
val beginTime = max(req.beginTime, otherReq.beginTime)
240254
val endTime = min(zoneReq.endTime, otherReq.endTime)
255+
val conflictReq = ConflictRequirement(zoneReq.zone, beginTime, endTime)
241256
if (beginTime < endTime)
242257
res.add(
243-
Conflict(listOf(otherReq.trainId), beginTime, endTime, ConflictType.ROUTING)
258+
Conflict(
259+
listOf(otherReq.trainId),
260+
beginTime,
261+
endTime,
262+
ConflictType.ROUTING,
263+
listOf(conflictReq)
264+
)
244265
)
245266
}
246267
}
@@ -421,7 +442,11 @@ enum class EventType {
421442
END
422443
}
423444

424-
class Event(val eventType: EventType, val time: Double) : Comparable<Event> {
445+
class Event(
446+
val eventType: EventType,
447+
val time: Double,
448+
val requirements: Collection<ConflictRequirement>
449+
) : Comparable<Event> {
425450
override fun compareTo(other: Event): Int {
426451
val timeDelta = this.time.compareTo(other.time)
427452
if (timeDelta != 0) return timeDelta
@@ -443,28 +468,32 @@ fun mergeMap(
443468
// create an event list and sort it
444469
val events = mutableListOf<Event>()
445470
for (conflict in conflicts) {
446-
events.add(Event(EventType.BEGIN, conflict.startTime))
447-
events.add(Event(EventType.END, conflict.endTime))
471+
events.add(Event(EventType.BEGIN, conflict.startTime, conflict.requirements))
472+
events.add(Event(EventType.END, conflict.endTime, conflict.requirements))
448473
}
449474

450475
events.sort()
451476
var eventCount = 0
452477
var eventBeginning = 0.0
478+
var conflictReqs = mutableListOf<ConflictRequirement>()
453479
for (event in events) {
454480
when (event.eventType) {
455481
EventType.BEGIN -> {
456482
if (++eventCount == 1) eventBeginning = event.time
483+
conflictReqs.addAll(event.requirements)
457484
}
458485
EventType.END -> {
459-
if (--eventCount == 0)
460-
newConflicts.add(
461-
Conflict(
462-
trainIds.toMutableList(),
463-
eventBeginning,
464-
event.time,
465-
conflictType
466-
)
486+
if (--eventCount > 0) continue
487+
newConflicts.add(
488+
Conflict(
489+
trainIds.toMutableList(),
490+
eventBeginning,
491+
event.time,
492+
conflictType,
493+
conflictReqs
467494
)
495+
)
496+
conflictReqs = mutableListOf()
468497
}
469498
}
470499
}

core/src/main/kotlin/fr/sncf/osrd/api/api_v2/conflicts/ConflictDetectionEndpointV2.kt

+8-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,14 @@ private fun makeConflictDetectionResponse(
5252
it.trainIds,
5353
startTime.plus(Duration.ofMillis((it.startTime * 1000).toLong())),
5454
startTime.plus(Duration.ofMillis((it.endTime * 1000).toLong())),
55-
it.conflictType
55+
it.conflictType,
56+
it.requirements.map {
57+
ConflictRequirement(
58+
it.zone,
59+
startTime.plus(Duration.ofMillis((it.startTime * 1000).toLong())),
60+
startTime.plus(Duration.ofMillis((it.endTime * 1000).toLong())),
61+
)
62+
}
5663
)
5764
}
5865
)

core/src/main/kotlin/fr/sncf/osrd/api/api_v2/conflicts/ConflictDetectionResponse.kt

+7
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ class Conflict(
1818
@Json(name = "end_time") val endTime: ZonedDateTime,
1919
@Json(name = "conflict_type")
2020
val conflictType: ConflictDetectionEndpoint.ConflictDetectionResult.Conflict.ConflictType,
21+
@Json(name = "requirements") val requirements: Collection<ConflictRequirement>,
22+
)
23+
24+
class ConflictRequirement(
25+
@Json(name = "zone") val zone: String,
26+
@Json(name = "start_time") val startTime: ZonedDateTime,
27+
@Json(name = "end_time") val endTime: ZonedDateTime,
2128
)
2229

2330
val conflictResponseAdapter: JsonAdapter<ConflictDetectionResponse> =

core/src/test/java/fr/sncf/osrd/standalone_sim/ConflictDetectionTest.java

+36
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@
1010
import static fr.sncf.osrd.utils.Helpers.fullInfraFromRJS;
1111
import static fr.sncf.osrd.utils.units.Distance.fromMeters;
1212
import static fr.sncf.osrd.utils.units.Distance.toMeters;
13+
import static org.assertj.core.api.Assertions.assertThat;
1314
import static org.junit.jupiter.api.Assertions.assertFalse;
1415
import static org.junit.jupiter.api.Assertions.assertIterableEquals;
1516
import static org.junit.jupiter.api.Assertions.assertTrue;
1617

18+
import fr.sncf.osrd.api.ConflictDetectionEndpoint.ConflictDetectionResult.Conflict;
19+
import fr.sncf.osrd.api.ConflictDetectionEndpoint.ConflictDetectionResult.ConflictRequirement;
1720
import fr.sncf.osrd.api.FullInfra;
1821
import fr.sncf.osrd.conflicts.ConflictsKt;
1922
import fr.sncf.osrd.conflicts.TrainRequirements;
@@ -31,6 +34,7 @@
3134
import fr.sncf.osrd.utils.Helpers;
3235
import java.util.ArrayList;
3336
import java.util.List;
37+
import java.util.stream.LongStream;
3438
import org.junit.jupiter.api.Assertions;
3539
import org.junit.jupiter.api.Test;
3640
import org.junit.jupiter.params.ParameterizedTest;
@@ -438,6 +442,38 @@ public void conflictDetectionForOvertakeInStation(
438442
assert (hasSpacingConflict == conflicts.stream().anyMatch((conflict) -> conflict.conflictType == SPACING));
439443
}
440444

445+
/*
446+
Test that merging conflicts groups entries by time as expected. The first
447+
two requirements overlap, the last requirement is isolated.
448+
*/
449+
@Test
450+
public void testConflictMerge() {
451+
var reqs = List.of(
452+
new TrainRequirements(0, List.of(new ResultTrain.SpacingRequirement("A", 10, 20, true)), List.of()),
453+
new TrainRequirements(0, List.of(new ResultTrain.SpacingRequirement("B", 20, 30, true)), List.of()),
454+
new TrainRequirements(0, List.of(new ResultTrain.SpacingRequirement("C", 40, 50, true)), List.of()),
455+
new TrainRequirements(1, List.of(new ResultTrain.SpacingRequirement("A", 15, 25, true)), List.of()),
456+
new TrainRequirements(1, List.of(new ResultTrain.SpacingRequirement("B", 25, 35, true)), List.of()),
457+
new TrainRequirements(1, List.of(new ResultTrain.SpacingRequirement("C", 45, 55, true)), List.of()));
458+
459+
var conflicts = ConflictsKt.detectConflicts(reqs);
460+
461+
var expectedConflicts = List.of(
462+
new Conflict(
463+
LongStream.of(0, 1).boxed().toList(),
464+
10,
465+
35,
466+
SPACING,
467+
List.of(new ConflictRequirement("A", 10, 25), new ConflictRequirement("B", 20, 35))),
468+
new Conflict(
469+
LongStream.of(0, 1).boxed().toList(),
470+
40,
471+
55,
472+
SPACING,
473+
List.of(new ConflictRequirement("C", 40, 55))));
474+
assertThat(conflicts).usingRecursiveComparison().isEqualTo(expectedConflicts);
475+
}
476+
441477
private static TrainRequirements convertRequirements(long trainId, double offset, ResultTrain train) {
442478
var spacingRequirements = new ArrayList<ResultTrain.SpacingRequirement>();
443479
for (var req : train.spacingRequirements)

editoast/openapi.yaml

+32
Original file line numberDiff line numberDiff line change
@@ -2953,6 +2953,7 @@ components:
29532953
- start_time
29542954
- end_time
29552955
- conflict_type
2956+
- requirements
29562957
properties:
29572958
conflict_type:
29582959
type: string
@@ -2963,6 +2964,11 @@ components:
29632964
type: string
29642965
format: date-time
29652966
description: Datetime of the end of the conflict
2967+
requirements:
2968+
type: array
2969+
items:
2970+
$ref: '#/components/schemas/ConflictRequirement'
2971+
description: List of requirements causing the conflict
29662972
start_time:
29672973
type: string
29682974
format: date-time
@@ -2987,6 +2993,7 @@ components:
29872993
- start_time
29882994
- end_time
29892995
- conflict_type
2996+
- requirements
29902997
properties:
29912998
conflict_type:
29922999
type: string
@@ -2997,6 +3004,11 @@ components:
29973004
type: string
29983005
format: date-time
29993006
description: Datetime of the end of the conflict
3007+
requirements:
3008+
type: array
3009+
items:
3010+
$ref: '#/components/schemas/ConflictRequirement'
3011+
description: List of requirements causing the conflict
30003012
start_time:
30013013
type: string
30023014
format: date-time
@@ -3008,6 +3020,26 @@ components:
30083020
format: int64
30093021
description: List of train ids involved in the conflict
30103022
description: List of conflicts detected
3023+
ConflictRequirement:
3024+
type: object
3025+
description: |-
3026+
Unmet requirement causing a conflict.
3027+
3028+
The start and end time describe the conflicting time span (not the full
3029+
requirement's time span).
3030+
required:
3031+
- zone
3032+
- start_time
3033+
- end_time
3034+
properties:
3035+
end_time:
3036+
type: string
3037+
format: date-time
3038+
start_time:
3039+
type: string
3040+
format: date-time
3041+
zone:
3042+
type: string
30113043
CopyOperation:
30123044
type: object
30133045
description: JSON Patch 'copy' operation representation

editoast/src/core/conflict_detection.rs

+14
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use super::simulation::SpacingRequirement;
1313
editoast_common::schemas! {
1414
ConflictDetectionResponse,
1515
Conflict,
16+
ConflictRequirement,
1617
}
1718

1819
#[derive(Debug, Serialize)]
@@ -47,6 +48,19 @@ pub struct Conflict {
4748
/// Type of the conflict
4849
#[schema(inline)]
4950
pub conflict_type: ConflictType,
51+
/// List of requirements causing the conflict
52+
pub requirements: Vec<ConflictRequirement>,
53+
}
54+
55+
/// Unmet requirement causing a conflict.
56+
///
57+
/// The start and end time describe the conflicting time span (not the full
58+
/// requirement's time span).
59+
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
60+
pub struct ConflictRequirement {
61+
pub zone: String,
62+
pub start_time: DateTime<Utc>,
63+
pub end_time: DateTime<Utc>,
5064
}
5165

5266
#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema)]

front/src/common/api/generatedEditoastApi.ts

+7
Original file line numberDiff line numberDiff line change
@@ -2744,10 +2744,17 @@ export type TimetableDetailedResult = {
27442744
timetable_id: number;
27452745
train_ids: number[];
27462746
};
2747+
export type ConflictRequirement = {
2748+
end_time: string;
2749+
start_time: string;
2750+
zone: string;
2751+
};
27472752
export type Conflict = {
27482753
conflict_type: 'Spacing' | 'Routing';
27492754
/** Datetime of the end of the conflict */
27502755
end_time: string;
2756+
/** List of requirements causing the conflict */
2757+
requirements: ConflictRequirement[];
27512758
/** Datetime of the start of the conflict */
27522759
start_time: string;
27532760
/** List of train ids involved in the conflict */

0 commit comments

Comments
 (0)