Parcourir la source

layer assignment step update + new way to store edge subdivisions (BT)

icestormikk il y a 1 mois
Parent
commit
73499dcc0b

+ 12 - 3
src/v1/optimizer/siguiyama/SiguiyamaContext.ts

@@ -4,14 +4,23 @@ import Node from "../../graph/node/Node.js"
 import { AlgorithmContext } from "../AlgorithmContext.js"
 import Grid from "./grid/Grid.js";
 
+/**
+ * Набор рёбер обратной связи - рёбра, которые были инвертированы на этапе удаления циклов и должны быть восстановлены на этапе назначения координат.
+ */
 export type FeedbackSet = Edge<Node>[];
 
+/**
+ * Информация о разбиении длинного ребра на сегменты с dummy-вершинами. Содержит ссылку на исходное ребро и, при наличии, ссылки на новые рёбра, образованные в результате разбиения.
+ */
 export type EdgeSubdivision<TNode extends Node> = {
-    originalEdge: Edge<TNode>,
-    chainNodes: TNode[],
-    chainEdges: Edge<TNode>[]
+    original: Edge<TNode>,
+    left?: Edge<TNode>,
+    right?: Edge<TNode>,
 }
 
+/**
+ * Контекст, передаваемый между шагами алгоритма Siguiyama. Содержит всю необходимую информацию о графе, его разбиении на слои, сетке и рёбрах обратной связи, которая может быть использована и модифицирована на каждом этапе оптимизации.
+ */
 export type SiguiyamaContext = AlgorithmContext & Partial<{
 	feedbackSet: FeedbackSet,
 	layering: Layering<Node, Edge<Node>>,

+ 37 - 11
src/v1/optimizer/steps/CleanupStep.ts

@@ -20,26 +20,52 @@ export default class CleanupStep extends AlgorithmStep<SiguiyamaContext> {
 
 		this.restoreEdges(graph, edgeSubdivisions);
 		this.removeSubdividedEdges(graph, edgeSubdivisions);
+		this.removeDummyNodes(graph, edgeSubdivisions);
 	}
 
 	private restoreEdges(graph: Graph<Node, Edge<Node>>, edgeSubdivisions: EdgeSubdivision<Node>[]) : void {
-		const subdivisionsMap = new Map(
-			edgeSubdivisions.map((e) => [e.originalEdge.getId(), e])
-		);
+		const grouped = new Map<string, EdgeSubdivision<Node>[]>();
 
-		for(const edge of graph.getEdges()) {
-			const edgeId = edge.getId();
+		for(const subdivision of edgeSubdivisions) {
+			const edgeId = subdivision.original.getId();
+			const group = grouped.get(edgeId);
+			if(group)
+				group.push(subdivision);
+			else
+				grouped.set(edgeId, [subdivision]);
+		}
+
+		for(const subdivisions of grouped.values()) {
+			const originalEdge = subdivisions[0]!.original;
+			const waypoints = subdivisions
+				.slice()
+				.sort((a, b) => {
+					const getDummyIndex = (subdivision: EdgeSubdivision<Node>) => {
+						const dummy = subdivision.left?.getTo() ?? subdivision.right?.getFrom();
+						return Number(dummy!.getId().split("-").pop() ?? "0");
+					};
 
-			const subdivision = subdivisionsMap.get(edgeId);
-			if(!subdivision)
-				continue;
+					return getDummyIndex(a) - getDummyIndex(b);
+				})
+				.map((subdivision) => {
+					const dummy = subdivision.left?.getTo() ?? subdivision.right?.getFrom();
+					return { x: dummy!.getX(), y: dummy!.getY() };
+				});
 
-			edge.setWaypoints(subdivision.chainNodes.slice(1, -1).map((n) => ({ x: n.getX(), y: n.getY() })));
+			originalEdge.setWaypoints(waypoints);
+			graph.addEdge(originalEdge);
 		}
 	}
 
 	private removeSubdividedEdges(graph:Graph<Node, Edge<Node>>, edgeSubdivisions: EdgeSubdivision<Node>[]) : void {
-		for(const subdivision of edgeSubdivisions)
-			subdivision.chainEdges.forEach((ce) => graph.removeEdge(ce.getId()));
+		for(const subdivision of edgeSubdivisions) {
+			if(subdivision.left)
+				graph.removeEdge(subdivision.left.getId());
+			if(subdivision.right)
+				graph.removeEdge(subdivision.right.getId());
+		}
+	}
+
+	private removeDummyNodes(graph: Graph<Node, Edge<Node>>, edgeSubdivisions: EdgeSubdivision<Node>[]) : void {
 	}
 }

+ 5 - 5
src/v1/optimizer/steps/CoordinateAssignmentStep.ts

@@ -26,6 +26,8 @@ export default class CoordinateAssignmentStep extends AlgorithmStep<SiguiyamaCon
 		if(!feedbackSet)
 			throw new Error("Feedback set was not found!");
 
+
+
 		this.reverseEdgesInFeedback(graph, feedbackSet);
 
 		const grid = this.buildGrid(layering);
@@ -49,7 +51,7 @@ export default class CoordinateAssignmentStep extends AlgorithmStep<SiguiyamaCon
 		const grid = new Grid<Node>();
 		const layers = layering.getLayers();
 
-		for(let col = 0; col < layers.length; col++) {
+		for(let col = layers.length - 1; col >= 0; col--) {
 			const layer = layers[col]!;
 
 			for(let row = 0; row < layer.nodes.length; row++)
@@ -61,14 +63,12 @@ export default class CoordinateAssignmentStep extends AlgorithmStep<SiguiyamaCon
 
 	private assignCoordinatesFromGrid(graph: Graph<Node, Edge<Node>>, grid: Grid<Node>): void {
 		const PADDING = 60;
-		const H_GAP = 80;  
-		const V_GAP = 50; 
 
 		const colWidths = this.computeColWidths(grid);
 		const rowHeights = this.computeRowHeights(grid);
 
-		const colX = this.computeOffsets(colWidths, H_GAP, PADDING);
-		const rowY = this.computeOffsets(rowHeights, V_GAP, PADDING);
+		const colX = this.computeOffsets(colWidths, this._nodeGap, PADDING);
+		const rowY = this.computeOffsets(rowHeights, this._layerGap, PADDING);
 
 		for(let col = 0; col < grid.cols; col++) {
 			for(let row = 0; row < grid.rows; row++) {

+ 89 - 59
src/v1/optimizer/steps/LayerAssignmentStep.ts

@@ -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;
+		}
 	}
 }