|
|
@@ -8,14 +8,22 @@ import AlgorithmStep from "../AlgorithmStep.js";
|
|
|
import {EdgeSubdivision, FeedbackSet, SiguiyamaContext} from "../siguiyama/SiguiyamaContext.js";
|
|
|
|
|
|
/**
|
|
|
- * Network simplex algorithm
|
|
|
- * TODO fix it
|
|
|
+ * Шаг алгоритма Sugiyama для присвоения слоёв вершинам графа.
|
|
|
+ * Использует longest path layering для минимизации числа слоёв и обеспечения направления рёбер вниз.
|
|
|
*/
|
|
|
export default class LayerAssignmentStep extends AlgorithmStep<SiguiyamaContext> {
|
|
|
+ /**
|
|
|
+ * Создаёт экземпляр шага LayerAssignmentStep.
|
|
|
+ */
|
|
|
public constructor() {
|
|
|
super(LayerAssignmentStep.name);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Выполняет шаг присвоения слоёв.
|
|
|
+ * @param context Контекст алгоритма Sugiyama, содержащий граф, feedback set и edge subdivisions.
|
|
|
+ * @throws {LayerAssignmentStepError} Если граф не найден, feedback set не определён или граф ацикличен.
|
|
|
+ */
|
|
|
public run(context: SiguiyamaContext): void {
|
|
|
const { graph, feedbackSet, edgeSubdivisions } = context;
|
|
|
|
|
|
@@ -26,6 +34,9 @@ export default class LayerAssignmentStep extends AlgorithmStep<SiguiyamaContext>
|
|
|
if(!edgeSubdivisions)
|
|
|
throw new LayerAssignmentStepError("Edge subdivisions information is undefined!")
|
|
|
|
|
|
+ if(graph.isAcyclic())
|
|
|
+ throw new LayerAssignmentStepError("Graph is acyclic, can not assign layers to an acyclic graph!");
|
|
|
+
|
|
|
const layering = this.longestPathAlgorithm(graph);
|
|
|
|
|
|
this.subdivideLongEdges(graph, layering, feedbackSet, context.edgeSubdivisions!);
|
|
|
@@ -33,49 +44,49 @@ export default class LayerAssignmentStep extends AlgorithmStep<SiguiyamaContext>
|
|
|
context.layering = layering;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Реализует алгоритм longest path layering для присвоения слоёв вершинам DAG.
|
|
|
+ * Каждая вершина получает слой, равный длине самого длинного пути от неё.
|
|
|
+ * @param dag Направленный ациклический граф.
|
|
|
+ * @returns Объект Layering с присвоенными слоями.
|
|
|
+ * @private
|
|
|
+ */
|
|
|
private longestPathAlgorithm(dag: Graph<Node, Edge<Node>>) : NonNullable<SiguiyamaContext["layering"]> {
|
|
|
- const nodes = dag.getNodes();
|
|
|
-
|
|
|
- const alreadyLayeredNodeIds = new Set<Node>();
|
|
|
- const underCurrentLayerNodeIds = new Set<Node>();
|
|
|
const layering = new Layering<Node, Edge<Node>>();
|
|
|
+ const cache = new Map<string, number>();
|
|
|
|
|
|
- let currentLayerIndex = 0;
|
|
|
-
|
|
|
- while(alreadyLayeredNodeIds.size !== nodes.length) {
|
|
|
- let isNodeSelected = false;
|
|
|
-
|
|
|
- for(const node of nodes) {
|
|
|
- if(alreadyLayeredNodeIds.has(node))
|
|
|
- continue;
|
|
|
-
|
|
|
- const successors = dag.getNodeOutputs(node);
|
|
|
- const isAllSuccessorsUnder = successors.every((node) => underCurrentLayerNodeIds.has(node));
|
|
|
-
|
|
|
- if(!isAllSuccessorsUnder)
|
|
|
- continue;
|
|
|
+ const getLongestPath = (node: Node): number => {
|
|
|
+ if (cache.has(node.getId()))
|
|
|
+ return cache.get(node.getId())!;
|
|
|
|
|
|
- layering.assign(node, currentLayerIndex);
|
|
|
- alreadyLayeredNodeIds.add(node);
|
|
|
- isNodeSelected = true;
|
|
|
+ const successors = dag.getNodeOutputs(node);
|
|
|
+ if (successors.length === 0) {
|
|
|
+ cache.set(node.getId(), 0);
|
|
|
+ return 0;
|
|
|
}
|
|
|
|
|
|
- if(!isNodeSelected) {
|
|
|
- currentLayerIndex++;
|
|
|
- alreadyLayeredNodeIds.forEach((id) => underCurrentLayerNodeIds.add(id));
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- const maxLayer = currentLayerIndex;
|
|
|
+ const maxSucc = Math.max(...successors.map(s => getLongestPath(s)));
|
|
|
+ const dist = 1 + maxSucc;
|
|
|
+ cache.set(node.getId(), dist);
|
|
|
+ return dist;
|
|
|
+ };
|
|
|
|
|
|
- for(const node of nodes) {
|
|
|
- const layer = layering.getLayerOf(node)!;
|
|
|
- layering.assign(node, maxLayer - layer);
|
|
|
+ for (const node of dag.getNodes()) {
|
|
|
+ const layer = getLongestPath(node);
|
|
|
+ layering.assign(node, layer);
|
|
|
}
|
|
|
|
|
|
return layering;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Разбивает длинные рёбра (пересекающие более одного слоя) на сегменты с dummy-вершинами.
|
|
|
+ * @param graph Граф, в котором разбиваются рёбра.
|
|
|
+ * @param layering Текущая структура слоёв.
|
|
|
+ * @param feedbackSet Набор рёбер обратной связи.
|
|
|
+ * @param edgeSubdivisions Массив для хранения информации о разбиениях рёбер.
|
|
|
+ * @private
|
|
|
+ */
|
|
|
private subdivideLongEdges(graph: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>, feedbackSet: FeedbackSet, edgeSubdivisions: EdgeSubdivision<Node>[]) : void {
|
|
|
const edges = graph.getEdges();
|
|
|
|
|
|
@@ -89,55 +100,74 @@ export default class LayerAssignmentStep extends AlgorithmStep<SiguiyamaContext>
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Разбивает одно длинное ребро на сегменты с dummy-вершинами.
|
|
|
+ * @param graph Граф, в котором разбивается ребро.
|
|
|
+ * @param layering Текущая структура слоёв.
|
|
|
+ * @param edge Ребро для разбиения.
|
|
|
+ * @param feedbackSet Набор рёбер обратной связи.
|
|
|
+ * @param edgeSubdivisions Массив для хранения информации о разбиениях рёбер.
|
|
|
+ * @param span Число слоёв, которые пересекает ребро.
|
|
|
+ * @private
|
|
|
+ */
|
|
|
private subdivideLongEdge(graph: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>, edge: Edge<Node>, feedbackSet: FeedbackSet, edgeSubdivisions: EdgeSubdivision<Node>[], span: number) : void {
|
|
|
if(span <= 1)
|
|
|
return;
|
|
|
|
|
|
- const edgeId = edge.getId(), edgeFrom = edge.getFrom(), edgeTo = edge.getTo();
|
|
|
+ const edgeId = edge.getId();
|
|
|
+ const edgeFrom = edge.getFrom();
|
|
|
+ const edgeTo = edge.getTo();
|
|
|
|
|
|
const edgeReversedIndex = feedbackSet.findIndex((e) => e.getId() == edgeId);
|
|
|
const isReversed = edgeReversedIndex !== -1;
|
|
|
|
|
|
+ if(isReversed)
|
|
|
+ feedbackSet.splice(edgeReversedIndex, 1);
|
|
|
+
|
|
|
+ graph.removeEdge(edgeId);
|
|
|
+
|
|
|
const fromLayer = layering.getLayerOf(edgeFrom)!;
|
|
|
const toLayer = layering.getLayerOf(edgeTo)!;
|
|
|
const direction = Math.sign(toLayer - fromLayer);
|
|
|
- const subdivision: EdgeSubdivision<Node> = {
|
|
|
- originalEdge: edge,
|
|
|
- chainNodes: [edgeFrom],
|
|
|
- chainEdges: []
|
|
|
- };
|
|
|
- let previousNode = edgeFrom;
|
|
|
+ let splitTarget = edgeTo;
|
|
|
+ let previousLeftEdge: Edge<Node> | null = null;
|
|
|
|
|
|
for(let i = 1; i < span; i++) {
|
|
|
+ if(previousLeftEdge) {
|
|
|
+ graph.removeEdge(previousLeftEdge.getId());
|
|
|
+ if(isReversed) {
|
|
|
+ const removedIndex = feedbackSet.findIndex((e) => e.getId() === previousLeftEdge!.getId());
|
|
|
+ if(removedIndex !== -1)
|
|
|
+ feedbackSet.splice(removedIndex, 1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
const dummyId = `dummy-${edgeId}-${i}`;
|
|
|
const node = new DummyNode(0, 0, edgeId, dummyId);
|
|
|
|
|
|
const dummyLayerIndex = fromLayer + direction * i;
|
|
|
-
|
|
|
graph.addNode(node);
|
|
|
- subdivision.chainNodes.push(node);
|
|
|
layering.assign(node, dummyLayerIndex);
|
|
|
|
|
|
- const newEdge = new Edge(previousNode, node, [], `${edgeId}_segment_${i}`);
|
|
|
-
|
|
|
- graph.addEdge(newEdge);
|
|
|
- subdivision.chainEdges.push(newEdge);
|
|
|
+ const leftEdge = new Edge(edgeFrom, node, [], `${edgeId}_segment_${i}_left`);
|
|
|
+ const rightEdge = new Edge(node, splitTarget, [], `${edgeId}_segment_${i}_right`);
|
|
|
|
|
|
- if(isReversed)
|
|
|
- feedbackSet.push(newEdge);
|
|
|
+ graph.addEdge(leftEdge);
|
|
|
+ graph.addEdge(rightEdge);
|
|
|
|
|
|
- previousNode = node;
|
|
|
- }
|
|
|
+ if(isReversed) {
|
|
|
+ feedbackSet.push(leftEdge);
|
|
|
+ feedbackSet.push(rightEdge);
|
|
|
+ }
|
|
|
|
|
|
- const lastEdge = new Edge(previousNode, edgeTo, [], `${edgeId}_segment_${span}`);
|
|
|
- graph.addEdge(lastEdge);
|
|
|
+ edgeSubdivisions.push({
|
|
|
+ original: edge,
|
|
|
+ left: leftEdge,
|
|
|
+ right: rightEdge
|
|
|
+ });
|
|
|
|
|
|
- subdivision.chainEdges.push(lastEdge);
|
|
|
- subdivision.chainNodes.push(edgeTo);
|
|
|
-
|
|
|
- if(isReversed)
|
|
|
- feedbackSet.push(lastEdge);
|
|
|
-
|
|
|
- edgeSubdivisions.push(subdivision);
|
|
|
+ previousLeftEdge = leftEdge;
|
|
|
+ splitTarget = node;
|
|
|
+ }
|
|
|
}
|
|
|
}
|