Skip to content

Commit

Permalink
feat: optimize originDestination graph (#316)
Browse files Browse the repository at this point in the history
  • Loading branch information
shenriotpro authored Oct 21, 2024
1 parent ce3f90d commit 83895a1
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 60 deletions.
2 changes: 1 addition & 1 deletion documentation/ORIGIN_DESTINATION_MATRIX.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ The Origin Destination Matrix will only use visible trainruns to compute paths.
### Caveats

Split trainruns are not supported at the moment: https://github.com/SchweizerischeBundesbahnen/netzgrafik-editor-frontend/issues/285.
As a simplification, we currently consider trains run at their frequency for a fixed schedule duration (10 hours).
As a simplification, we currently consider trains run at their frequency for a fixed schedule duration (16 hours).
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,9 @@ export class EditorToolsViewComponent {
private convertToOriginDestinationCSV(): string {
// Duration of the schedule to consider (in minutes).
// TODO: ideally this would be 24 hours, but performance is a concern.
const timeLimit = 10*60;
// One idea to optimize would be to consider the minimum time window before the schedule repeats (LCM).
// Draft here: https://colab.research.google.com/drive/1Z1r2uU2pgffWxCbG_wt2zoLStZKzWleE#scrollTo=F6vOevK6znee
const timeLimit = 16*60;

const headers: string[] = [];
headers.push($localize`:@@app.view.editor-side-view.editor-tools-view-component.origin:Origin`);
Expand Down
155 changes: 97 additions & 58 deletions src/app/view/util/origin-destination-graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,13 @@ export class Edge {
){}
}

export const buildEdges = (nodes: Node[], odNodes: Node[], trainruns: Trainrun[], connectionPenalty: number, trainrunService: TrainrunService,
timeLimit: number
export const buildEdges = (nodes: Node[], odNodes: Node[], trainruns: Trainrun[], connectionPenalty: number,
trainrunService: TrainrunService, timeLimit: number
): Edge[] => {
let edges = buildSectionEdges(trainruns, trainrunService, timeLimit);

// TODO: organize by trainrun and sort
const verticesDepartureByNode = new Map<number, Vertex[]>();
const verticesArrivalByNode = new Map<number, Vertex[]>();
const verticesDepartureByTrainrunByNode = new Map<number, Map<number, Vertex[]>>();
const verticesArrivalByTrainrunByNode = new Map<number, Map<number, Vertex[]>>();
edges.forEach((edge) => {
const src = edge.v1;
const tgt = edge.v2;
Expand All @@ -44,23 +43,47 @@ export const buildEdges = (nodes: Node[], odNodes: Node[], trainruns: Trainrun[]
if (tgt.isDeparture !== false) {
console.log("tgt is not an arrival: ", tgt);
}
const departures = verticesDepartureByNode.get(src.nodeId);
if (departures === undefined) {
verticesDepartureByNode.set(src.nodeId, [src]);
const departuresByTrainrun = verticesDepartureByTrainrunByNode.get(src.nodeId);
if (departuresByTrainrun === undefined) {
verticesDepartureByTrainrunByNode.set(src.nodeId, new Map<number, Vertex[]>([[src.trainrunId, [src]]]));
} else {
departures.push(src);
const departures = departuresByTrainrun.get(src.trainrunId);
if (departures === undefined) {
departuresByTrainrun.set(src.trainrunId, [src]);
} else {
departures.push(src);
}
}
const arrivals = verticesArrivalByNode.get(tgt.nodeId);
if (arrivals === undefined) {
verticesArrivalByNode.set(tgt.nodeId, [tgt]);
const arrivalsByTrainrun = verticesArrivalByTrainrunByNode.get(tgt.nodeId);
if (arrivalsByTrainrun === undefined) {
verticesArrivalByTrainrunByNode.set(tgt.nodeId, new Map<number, Vertex[]>([[tgt.trainrunId, [tgt]]]));
} else {
arrivals.push(tgt);
const arrivals = arrivalsByTrainrun.get(tgt.trainrunId);
if (arrivals === undefined) {
arrivalsByTrainrun.set(tgt.trainrunId, [tgt]);
} else {
arrivals.push(tgt);
}
}
});

// Sorting is useful to find relevant connections later.
verticesDepartureByTrainrunByNode.forEach((verticesDepartureByTrainrun) => {
verticesDepartureByTrainrun.forEach((departures, trainrunId) => {
departures.sort((a, b) => a.time - b.time);
});
});
verticesArrivalByTrainrunByNode.forEach((verticesArrivalByTrainrun) => {
verticesArrivalByTrainrun.forEach((arrivals, trainrunId) => {
arrivals.sort((a, b) => a.time - b.time);
});
});

// Note: pushing too many elements at once does not work well.
edges = [...edges, ...buildConvenienceEdges(odNodes, verticesDepartureByNode, verticesArrivalByNode)];
edges = [...edges, ...buildConnectionEdges(nodes, verticesDepartureByNode, verticesArrivalByNode, connectionPenalty)];
edges = [...edges, ...buildConvenienceEdges(odNodes, verticesDepartureByTrainrunByNode, verticesArrivalByTrainrunByNode)];
edges = [...edges,
...buildConnectionEdges(nodes, verticesDepartureByTrainrunByNode, verticesArrivalByTrainrunByNode, connectionPenalty)
];

return edges;
};
Expand Down Expand Up @@ -102,6 +125,7 @@ Map<number, [number, number]> => {
let started = false;
vertices.forEach((vertex) => {
const key = JSON.stringify(vertex);
// First, look for our start node.
if (!started) {
if (from === vertex.nodeId && vertex.isDeparture === true && vertex.time === undefined) {
started = true;
Expand All @@ -110,6 +134,7 @@ Map<number, [number, number]> => {
return;
}
}
// We found an end node.
if (vertex.isDeparture === false && vertex.time === undefined && dist.get(key) !== undefined
&& vertex.nodeId !== from) {
res.set(vertex.nodeId, dist.get(key));
Expand All @@ -118,12 +143,15 @@ Map<number, [number, number]> => {
if (neighs === undefined || dist.get(key) === undefined) {
return;
}
// The shortest path from the start node to this vertex is a shortest path from the start node to a neighbor
// plus the weight of the edge connecting the neighbor to this vertex.
neighs.forEach(([neighbor, weight]) => {
const alt = dist.get(key)[0] + weight;
const neighborKey = JSON.stringify(neighbor);
if (dist.get(neighborKey) === undefined || alt < dist.get(neighborKey)[0]) {
let connection = 0;
if (vertex.trainrunId !== undefined && neighbor.trainrunId !== undefined && vertex.trainrunId !== neighbor.trainrunId) {
if (vertex.trainrunId !== undefined && neighbor.trainrunId !== undefined
&& vertex.trainrunId !== neighbor.trainrunId) {
connection = 1;
}
dist.set(neighborKey, [alt, dist.get(key)[1] + connection]);
Expand All @@ -144,6 +172,7 @@ const buildSectionEdges = (trainruns: Trainrun[], trainrunService: TrainrunServi
}
tsIterators.forEach((tsIterator) => {
edges.push(...buildSectionEdgesFromIterator(tsIterator, false, timeLimit));
// Don't forget the reverse direction.
const ts = tsIterator.current().trainrunSection;
const nextIterator = trainrunService.getIterator(ts.getTargetNode(), ts);
edges.push(...buildSectionEdgesFromIterator(nextIterator, true, timeLimit));
Expand All @@ -162,6 +191,7 @@ const buildSectionEdgesFromIterator = (tsIterator: TrainrunIterator, reverse: bo
const trainrunId = reverse ? -ts.getTrainrunId() : ts.getTrainrunId();
const v1Time = reverse ? ts.getTargetDepartureDto().consecutiveTime : ts.getSourceDepartureDto().consecutiveTime;
const v1Node = reverse ? ts.getTargetNodeId() : ts.getSourceNodeId();
// If we don't stop here, we need to remember where we started.
if (reverse ? ts.getSourceNode().isNonStop(ts) : ts.getTargetNode().isNonStop(ts)) {
if (nonStopV1Time === -1) {
nonStopV1Time = v1Time;
Expand Down Expand Up @@ -190,58 +220,67 @@ const buildSectionEdgesFromIterator = (tsIterator: TrainrunIterator, reverse: bo
return edges;
};

const buildConvenienceEdges = (nodes: Node[], verticesDepartureByNode: Map<number, Vertex[]>,
verticesArrivalByNode: Map<number, Vertex[]>): Edge[] => {
const buildConvenienceEdges = (nodes: Node[], verticesDepartureByTrainrunByNode: Map<number, Map<number, Vertex[]>>,
verticesArrivalByTrainrunByNode: Map<number, Map<number, Vertex[]>>): Edge[] => {
const edges = [];
nodes.forEach((node) => {
const v1 = new Vertex(node.getId(), true);
const v2 = new Vertex(node.getId(), false);
const edge = new Edge(v1, v2, 0);
edges.push(edge);
const departures = verticesDepartureByNode.get(node.getId());
if (departures !== undefined) {
const srcVertex = new Vertex(node.getId(), true);
departures.forEach((departure) => {
const edge = new Edge(srcVertex, departure, 0);
edges.push(edge);
});
}
const arrivals = verticesArrivalByNode.get(node.getId());
if (arrivals !== undefined) {
const tgtVertex = new Vertex(node.getId(), false);
arrivals.forEach((arrival) => {
const edge = new Edge(arrival, tgtVertex, 0);
edges.push(edge);
});
const nodeId = node.getId();
// We add a single start and end vertex for each node, so we can compute shortest paths more easily.
const srcVertex = new Vertex(nodeId, true);
const tgtVertex = new Vertex(nodeId, false);
// Going from one node to itself is free.
const edge = new Edge(srcVertex, tgtVertex, 0);
edges.push(edge);
const departuresByTrainrun = verticesDepartureByTrainrunByNode.get(nodeId);
if (departuresByTrainrun !== undefined) {
departuresByTrainrun.forEach((departures, trainrunId) => {
departures.forEach((departure) => {
const edge = new Edge(srcVertex, departure, 0);
edges.push(edge);
});
});
}
const arrivalsByTrainrun = verticesArrivalByTrainrunByNode.get(nodeId);
if (arrivalsByTrainrun !== undefined) {
arrivalsByTrainrun.forEach((arrivals, trainrunId) => {
arrivals.forEach((arrival) => {
const edge = new Edge(arrival, tgtVertex, 0);
edges.push(edge);
});
});
}
});
return edges;
};

const buildConnectionEdges = (nodes: Node[], verticesDepartureByNode: Map<number, Vertex[]>,
verticesArrivalByNode: Map<number, Vertex[]>, connectionPenalty: number): Edge[] => {
const buildConnectionEdges = (nodes: Node[], verticesDepartureByTrainrunByNode: Map<number, Map<number, Vertex[]>>,
verticesArrivalByTrainrunByNode: Map<number, Map<number, Vertex[]>>, connectionPenalty: number): Edge[] => {
const edges = [];
nodes.forEach((node) => {
const departures = verticesDepartureByNode.get(node.getId());
const arrivals = verticesArrivalByNode.get(node.getId());
if (departures !== undefined && arrivals !== undefined) {
departures.forEach((departure) => {
arrivals.forEach((arrival) => {
if (departure.trainrunId === arrival.trainrunId) {
if (departure.time < arrival.time) {
return;
}
const edge = new Edge(arrival, departure, departure.time - arrival.time);
edges.push(edge);
return;
}
if (departure.time < arrival.time + node.getConnectionTime()) {
return;
}
const edge = new Edge(arrival, departure, departure.time - arrival.time + connectionPenalty);
edges.push(edge);
});
const departuresByTrainrun = verticesDepartureByTrainrunByNode.get(node.getId());
const arrivalsByTrainrun = verticesArrivalByTrainrunByNode.get(node.getId());
if (departuresByTrainrun !== undefined && arrivalsByTrainrun !== undefined) {
arrivalsByTrainrun.forEach((arrivals, arrivalTrainrunId) => {
arrivals.forEach((arrival) => {
departuresByTrainrun.forEach((departures, departureTrainrunId) => {
let minDepartureTime = arrival.time;
if (arrivalTrainrunId !== departureTrainrunId) {
minDepartureTime += node.getConnectionTime();
}
// For each arrival and for each trainrun available, we only want to consider the first departure.
// This could be a binary search but it does not seem to be worth it.
const departure = departures.find((departure) => {return departure.time >= minDepartureTime;});
if (departure !== undefined) {
let cost = departure.time - arrival.time;
if (arrivalTrainrunId !== departureTrainrunId) {
cost += connectionPenalty;
}
const edge = new Edge(arrival, departure, cost);
edges.push(edge);
}
});
});
});
}
});
return edges;
Expand Down
4 changes: 4 additions & 0 deletions src/integration-testing/origin.destination.csv.test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ describe("Origin Destination CSV Test", () => {
const connectionPenalty = 5;
const timeLimit = 60*10;

const start = new Date().getTime();
const edges = buildEdges(nodes, nodes, trainruns, connectionPenalty, trainrunService, timeLimit);

const neighbors = computeNeighbors(edges);
Expand All @@ -91,6 +92,7 @@ describe("Origin Destination CSV Test", () => {
res.set([origin.getId(), key].join(","), value);
});
});
const end = new Date().getTime();

// Note: there may be some other equivalent solutions, depending on connections.
// See https://github.com/SchweizerischeBundesbahnen/netzgrafik-editor-frontend/issues/199
Expand All @@ -99,6 +101,8 @@ describe("Origin Destination CSV Test", () => {
["12,11", [4, 0]], ["13,14", [29, 1]], ["13,11", [22, 1]], ["13,12", [2, 0]], ["14,13", [29, 1]],
["14,11", [6, 0]], ["14,12", [2, 0]]
]));
// This should be reasonably fast, likely less than 10ms.
expect(end - start).toBeLessThan(100);
});

it("integration test with selected nodes", () => {
Expand Down

0 comments on commit 83895a1

Please sign in to comment.