Skip to content

Commit

Permalink
core, editoast: return conflict requirements
Browse files Browse the repository at this point in the history
Closes: #8680
Signed-off-by: Simon Ser <[email protected]>
  • Loading branch information
emersion committed Sep 17, 2024
1 parent f1665a2 commit c1b736f
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,31 @@ public enum ConflictType {
@Json(name = "conflict_type")
public final ConflictType conflictType;

public Conflict(Collection<Long> trainIds, double startTime, double endTime, ConflictType conflictType) {
public final transient Collection<ConflictRequirement> requirements;

public Conflict(
Collection<Long> trainIds,
double startTime,
double endTime,
ConflictType conflictType,
Collection<ConflictRequirement> requirements) {
this.trainIds = trainIds;
this.startTime = startTime;
this.endTime = endTime;
this.conflictType = conflictType;
this.requirements = requirements;
}
}

public static class ConflictRequirement {
public final String zone;
public final double startTime;
public final double endTime;

public ConflictRequirement(String zone, double startTime, double endTime) {
this.zone = zone;
this.startTime = startTime;
this.endTime = endTime;
}
}

Expand Down
67 changes: 48 additions & 19 deletions core/src/main/java/fr/sncf/osrd/conflicts/Conflicts.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.carrotsearch.hppc.IntArrayList
import com.squareup.moshi.Json
import fr.sncf.osrd.api.ConflictDetectionEndpoint.ConflictDetectionResult.Conflict
import fr.sncf.osrd.api.ConflictDetectionEndpoint.ConflictDetectionResult.Conflict.ConflictType
import fr.sncf.osrd.api.ConflictDetectionEndpoint.ConflictDetectionResult.ConflictRequirement
import fr.sncf.osrd.standalone_sim.result.ResultTrain.RoutingRequirement
import fr.sncf.osrd.standalone_sim.result.ResultTrain.SpacingRequirement
import kotlin.math.max
Expand Down Expand Up @@ -171,12 +172,15 @@ class IncrementalConflictDetectorImpl(trainRequirements: List<TrainRequirements>
// look for requirement times overlaps.
// as spacing requirements are exclusive, any overlap is a conflict
val res = mutableListOf<Conflict>()
for (requirements in spacingZoneRequirements.values) {
for (conflictGroup in detectRequirementConflicts(requirements) { _, _ -> true }) {
for (entry in spacingZoneRequirements) {
for (conflictGroup in detectRequirementConflicts(entry.value) { _, _ -> true }) {
val trains = conflictGroup.map { it.trainId }
val beginTime = conflictGroup.minBy { it.beginTime }.beginTime
val endTime = conflictGroup.maxBy { it.endTime }.endTime
res.add(Conflict(trains, beginTime, endTime, ConflictType.SPACING))
val conflictReq = ConflictRequirement(entry.key, beginTime, endTime)
res.add(
Conflict(trains, beginTime, endTime, ConflictType.SPACING, listOf(conflictReq))
)
}
}
return res
Expand All @@ -185,13 +189,16 @@ class IncrementalConflictDetectorImpl(trainRequirements: List<TrainRequirements>
private fun detectRoutingConflicts(): List<Conflict> {
// for each zone, check compatibility of overlapping requirements
val res = mutableListOf<Conflict>()
for (requirements in routingZoneRequirements.values) {
for (entry in routingZoneRequirements) {
for (conflictGroup in
detectRequirementConflicts(requirements) { a, b -> a.config != b.config }) {
detectRequirementConflicts(entry.value) { a, b -> a.config != b.config }) {
val trains = conflictGroup.map { it.trainId }
val beginTime = conflictGroup.minBy { it.beginTime }.beginTime
val endTime = conflictGroup.maxBy { it.endTime }.endTime
res.add(Conflict(trains, beginTime, endTime, ConflictType.ROUTING))
val conflictReq = ConflictRequirement(entry.key, beginTime, endTime)
res.add(
Conflict(trains, beginTime, endTime, ConflictType.ROUTING, listOf(conflictReq))
)
}
}
return res
Expand All @@ -218,9 +225,16 @@ class IncrementalConflictDetectorImpl(trainRequirements: List<TrainRequirements>
for (otherReq in requirements) {
val beginTime = max(req.beginTime, otherReq.beginTime)
val endTime = min(req.endTime, otherReq.endTime)
val conflictReq = ConflictRequirement(req.zone, beginTime, endTime)
if (beginTime < endTime)
res.add(
Conflict(listOf(otherReq.trainId), beginTime, endTime, ConflictType.SPACING)
Conflict(
listOf(otherReq.trainId),
beginTime,
endTime,
ConflictType.SPACING,
listOf(conflictReq)
)
)
}

Expand All @@ -238,9 +252,16 @@ class IncrementalConflictDetectorImpl(trainRequirements: List<TrainRequirements>
if (otherReq.config == zoneReqConfig) continue
val beginTime = max(req.beginTime, otherReq.beginTime)
val endTime = min(zoneReq.endTime, otherReq.endTime)
val conflictReq = ConflictRequirement(zoneReq.zone, beginTime, endTime)
if (beginTime < endTime)
res.add(
Conflict(listOf(otherReq.trainId), beginTime, endTime, ConflictType.ROUTING)
Conflict(
listOf(otherReq.trainId),
beginTime,
endTime,
ConflictType.ROUTING,
listOf(conflictReq)
)
)
}
}
Expand Down Expand Up @@ -421,7 +442,11 @@ enum class EventType {
END
}

class Event(val eventType: EventType, val time: Double) : Comparable<Event> {
class Event(
val eventType: EventType,
val time: Double,
val requirements: Collection<ConflictRequirement>
) : Comparable<Event> {
override fun compareTo(other: Event): Int {
val timeDelta = this.time.compareTo(other.time)
if (timeDelta != 0) return timeDelta
Expand All @@ -443,28 +468,32 @@ fun mergeMap(
// create an event list and sort it
val events = mutableListOf<Event>()
for (conflict in conflicts) {
events.add(Event(EventType.BEGIN, conflict.startTime))
events.add(Event(EventType.END, conflict.endTime))
events.add(Event(EventType.BEGIN, conflict.startTime, conflict.requirements))
events.add(Event(EventType.END, conflict.endTime, conflict.requirements))
}

events.sort()
var eventCount = 0
var eventBeginning = 0.0
var conflictReqs = mutableListOf<ConflictRequirement>()
for (event in events) {
when (event.eventType) {
EventType.BEGIN -> {
if (++eventCount == 1) eventBeginning = event.time
conflictReqs.addAll(event.requirements)
}
EventType.END -> {
if (--eventCount == 0)
newConflicts.add(
Conflict(
trainIds.toMutableList(),
eventBeginning,
event.time,
conflictType
)
if (--eventCount > 0) continue
newConflicts.add(
Conflict(
trainIds.toMutableList(),
eventBeginning,
event.time,
conflictType,
conflictReqs
)
)
conflictReqs = mutableListOf()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,14 @@ private fun makeConflictDetectionResponse(
it.trainIds,
startTime.plus(Duration.ofMillis((it.startTime * 1000).toLong())),
startTime.plus(Duration.ofMillis((it.endTime * 1000).toLong())),
it.conflictType
it.conflictType,
it.requirements.map {
ConflictRequirement(
it.zone,
startTime.plus(Duration.ofMillis((it.startTime * 1000).toLong())),
startTime.plus(Duration.ofMillis((it.endTime * 1000).toLong())),
)
}
)
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ class Conflict(
@Json(name = "end_time") val endTime: ZonedDateTime,
@Json(name = "conflict_type")
val conflictType: ConflictDetectionEndpoint.ConflictDetectionResult.Conflict.ConflictType,
@Json(name = "requirements") val requirements: Collection<ConflictRequirement>,
)

class ConflictRequirement(
@Json(name = "zone") val zone: String,
@Json(name = "start_time") val startTime: ZonedDateTime,
@Json(name = "end_time") val endTime: ZonedDateTime,
)

val conflictResponseAdapter: JsonAdapter<ConflictDetectionResponse> =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@
import static fr.sncf.osrd.utils.Helpers.fullInfraFromRJS;
import static fr.sncf.osrd.utils.units.Distance.fromMeters;
import static fr.sncf.osrd.utils.units.Distance.toMeters;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertIterableEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import fr.sncf.osrd.api.ConflictDetectionEndpoint.ConflictDetectionResult.Conflict;
import fr.sncf.osrd.api.ConflictDetectionEndpoint.ConflictDetectionResult.ConflictRequirement;
import fr.sncf.osrd.api.FullInfra;
import fr.sncf.osrd.conflicts.ConflictsKt;
import fr.sncf.osrd.conflicts.TrainRequirements;
Expand All @@ -31,6 +34,7 @@
import fr.sncf.osrd.utils.Helpers;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.LongStream;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
Expand Down Expand Up @@ -438,6 +442,38 @@ public void conflictDetectionForOvertakeInStation(
assert (hasSpacingConflict == conflicts.stream().anyMatch((conflict) -> conflict.conflictType == SPACING));
}

/*
Test that merging conflicts groups entries by time as expected. The first two
conflicts overlap (zones "A" and "B"), the last one is isolated (zone "C").
*/
@Test
public void testConflictMerge() {
var reqs = List.of(
new TrainRequirements(0, List.of(new ResultTrain.SpacingRequirement("A", 10, 20, true)), List.of()),
new TrainRequirements(0, List.of(new ResultTrain.SpacingRequirement("B", 20, 30, true)), List.of()),
new TrainRequirements(0, List.of(new ResultTrain.SpacingRequirement("C", 40, 50, true)), List.of()),
new TrainRequirements(1, List.of(new ResultTrain.SpacingRequirement("A", 15, 25, true)), List.of()),
new TrainRequirements(1, List.of(new ResultTrain.SpacingRequirement("B", 25, 35, true)), List.of()),
new TrainRequirements(1, List.of(new ResultTrain.SpacingRequirement("C", 45, 55, true)), List.of()));

var conflicts = ConflictsKt.detectConflicts(reqs);

var expectedConflicts = List.of(
new Conflict(
LongStream.of(0, 1).boxed().toList(),
10,
35,
SPACING,
List.of(new ConflictRequirement("A", 10, 25), new ConflictRequirement("B", 20, 35))),
new Conflict(
LongStream.of(0, 1).boxed().toList(),
40,
55,
SPACING,
List.of(new ConflictRequirement("C", 40, 55))));
assertThat(conflicts).usingRecursiveComparison().isEqualTo(expectedConflicts);
}

private static TrainRequirements convertRequirements(long trainId, double offset, ResultTrain train) {
var spacingRequirements = new ArrayList<ResultTrain.SpacingRequirement>();
for (var req : train.spacingRequirements)
Expand Down
32 changes: 32 additions & 0 deletions editoast/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2953,6 +2953,7 @@ components:
- start_time
- end_time
- conflict_type
- requirements
properties:
conflict_type:
type: string
Expand All @@ -2963,6 +2964,11 @@ components:
type: string
format: date-time
description: Datetime of the end of the conflict
requirements:
type: array
items:
$ref: '#/components/schemas/ConflictRequirement'
description: List of requirements causing the conflict
start_time:
type: string
format: date-time
Expand All @@ -2987,6 +2993,7 @@ components:
- start_time
- end_time
- conflict_type
- requirements
properties:
conflict_type:
type: string
Expand All @@ -2997,6 +3004,11 @@ components:
type: string
format: date-time
description: Datetime of the end of the conflict
requirements:
type: array
items:
$ref: '#/components/schemas/ConflictRequirement'
description: List of requirements causing the conflict
start_time:
type: string
format: date-time
Expand All @@ -3008,6 +3020,26 @@ components:
format: int64
description: List of train ids involved in the conflict
description: List of conflicts detected
ConflictRequirement:
type: object
description: |-
Unmet requirement causing a conflict.
The start and end time describe the conflicting time span (not the full
requirement's time span).
required:
- zone
- start_time
- end_time
properties:
end_time:
type: string
format: date-time
start_time:
type: string
format: date-time
zone:
type: string
CopyOperation:
type: object
description: JSON Patch 'copy' operation representation
Expand Down
14 changes: 14 additions & 0 deletions editoast/src/core/conflict_detection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use super::simulation::SpacingRequirement;
editoast_common::schemas! {
ConflictDetectionResponse,
Conflict,
ConflictRequirement,
}

#[derive(Debug, Serialize)]
Expand Down Expand Up @@ -47,6 +48,19 @@ pub struct Conflict {
/// Type of the conflict
#[schema(inline)]
pub conflict_type: ConflictType,
/// List of requirements causing the conflict
pub requirements: Vec<ConflictRequirement>,
}

/// Unmet requirement causing a conflict.
///
/// The start and end time describe the conflicting time span (not the full
/// requirement's time span).
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
pub struct ConflictRequirement {
pub zone: String,
pub start_time: DateTime<Utc>,
pub end_time: DateTime<Utc>,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema)]
Expand Down
7 changes: 7 additions & 0 deletions front/src/common/api/generatedEditoastApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2744,10 +2744,17 @@ export type TimetableDetailedResult = {
timetable_id: number;
train_ids: number[];
};
export type ConflictRequirement = {
end_time: string;
start_time: string;
zone: string;
};
export type Conflict = {
conflict_type: 'Spacing' | 'Routing';
/** Datetime of the end of the conflict */
end_time: string;
/** List of requirements causing the conflict */
requirements: ConflictRequirement[];
/** Datetime of the start of the conflict */
start_time: string;
/** List of train ids involved in the conflict */
Expand Down

0 comments on commit c1b736f

Please sign in to comment.