Forráskód Böngészése

NEW: layer assignment step v0

Pavel Zhigalov 2 hete
szülő
commit
00c32fa2e5

+ 5 - 8
src/index.ts

@@ -1,14 +1,11 @@
 import { readFileSync } from "node:fs";
 import JointJsonDeserializer from "./v1/io/deserialize/json/JointJsonDeserializer.js";
+import CycleRemoveStep from "./v1/optimizer/siguiyama/CycleRemoveStep.js";
+import LayerAssignmentStep from "./v1/optimizer/siguiyama/LayerAssignmentStep.js";
+import SiguiyamaAlgorithm from "./v1/optimizer/siguiyama/SiguiyamaAlgorithm.js";
 
 const deserializer = new JointJsonDeserializer();
 const graph = deserializer.deserialize(String(readFileSync("./data/graph.json")));
-// const graphService = new DefaultGraphService()
-// const optimizer = new SigiuyamaAlgorithm().addStep(new CycleRemoveStep(graphService)).addStep(new LayerAssignmentStep(graphService));
+const optimizer = new SiguiyamaAlgorithm().addStep(new CycleRemoveStep()).addStep(new LayerAssignmentStep());
 
-console.log(graph.isAcyclic)
-console.log(graph.isAcyclic)
-console.log(graph.isAcyclic)
-console.log(graph.isAcyclic)
-
-// optimizer.run(graph);
+optimizer.run(graph);

+ 8 - 0
src/v1/errors/graph/layering/LayeringError.ts

@@ -0,0 +1,8 @@
+import BPMNError from "../../BPMNError.js";
+
+export default class LayeringError extends BPMNError {
+	public constructor(message: string) {
+		super(message);
+		this.name = LayeringError.name;
+	}
+}

+ 8 - 0
src/v1/graph/Graph.ts

@@ -219,6 +219,14 @@ export default class Graph<TNode extends Node, TEdge extends Edge<TNode>> {
 		return outputs;
 	}
 
+	public isSourceNode(nodeId: TNode["id"]) : boolean {
+		return this.getNodeInputs(nodeId).length === 0;
+	}
+
+	public isSinkNode(nodeId: TNode["id"]) : boolean {
+		return this.getNodeOutputs(nodeId).length === 0;
+	}
+
 	private invalidateCache(): void {
 		this._cache = {};
 	}

+ 0 - 4
src/v1/graph/GraphLayering.ts

@@ -1,4 +0,0 @@
-export default class GraphLayering {
-	// TODO
-	// stub
-}

+ 34 - 0
src/v1/graph/layering/Layer.ts

@@ -0,0 +1,34 @@
+import Node from "../Node.js";
+
+export default class Layer<TNode extends Node> {
+	public readonly index: number;
+	private readonly _nodes: TNode["id"][];
+
+	public constructor(index: number) {
+		this.index = index;
+		this._nodes = [];
+	}
+
+	public addNodes(nodes: TNode["id"][]) : this {
+		this._nodes.push(...nodes);
+		return this;
+	}
+
+	public removeNodes(nodes: TNode["id"][]) : this {
+		for(const id of nodes) {
+			const index = this._nodes.indexOf(id);
+			if(index !== -1)
+				this._nodes.splice(index, 1);
+		}
+
+		return this;
+	}
+
+	public get nodes(): TNode["id"][] {
+		return [...this._nodes];
+	}
+
+	public get width(): number {
+		return this._nodes.length;
+	}
+}

+ 95 - 0
src/v1/graph/layering/Layering.ts

@@ -0,0 +1,95 @@
+import LayeringError from "../../errors/graph/layering/LayeringError.js";
+import Edge from "../Edge.js";
+import Node from "../Node.js";
+import Layer from "./Layer.js";
+
+export default class Layering<TNode extends Node, TEdge extends Edge<TNode>> {
+	private readonly _layers: Map<number, Layer<TNode>>;
+
+	public constructor() {
+		this._layers = new Map();
+	}
+
+	public assign(node: TNode, layerIndex: number) : this {
+		if(!this._layers.has(layerIndex))
+			this._layers.set(layerIndex, new Layer(layerIndex));
+
+		this._layers.get(layerIndex)!.addNodes([node.id]);
+
+		return this;
+	}
+
+	public getLayers(): Layer<TNode>[] {
+		return Array.from(this._layers.values()).sort((a, b) => a.index - b.index);
+	}
+
+	public getLayerOf(nodeId: TNode["id"]) : number | null {
+		for(const [layerIndex, layer] of this._layers)
+			if(layer.nodes.some((id) => id == nodeId))
+				return layerIndex;
+
+		return null;
+	}
+
+	public setLayerOf(nodeId: TNode["id"], newLayerIndex: number) : this {
+		const oldIndex = this.getLayerOf(nodeId);
+		if(oldIndex !== null) {
+			const oldLayer = this._layers.get(oldIndex);
+			oldLayer?.removeNodes([nodeId])
+		}
+
+		if(!this._layers.has(newLayerIndex))
+			this._layers.set(newLayerIndex, new Layer(newLayerIndex));
+		this._layers.get(newLayerIndex)!.addNodes([nodeId]);
+
+		return this;
+	}
+
+	public isEdgeTight(edge: TEdge) : boolean {
+		const { from, to } = edge;
+		const fromLayerIndex = this.getLayerOf(from), toLayerIndex = this.getLayerOf(to);
+
+		if(fromLayerIndex == null || toLayerIndex == null)
+			throw new LayeringError(`Node from edge ${edge.id} is not assigned to any layer`);
+
+		return Math.abs(fromLayerIndex - toLayerIndex) === 1;
+	}
+
+	public getEdgeSpan(edge: TEdge) : number {
+		const fromLayer = this.getLayerOf(edge.from);
+		const toLayer = this.getLayerOf(edge.to);
+
+		if(fromLayer === null || toLayer === null)
+			throw new LayeringError(`Node from edge ${edge.id} is not assigned to any layer`);
+
+		return Math.abs(fromLayer - toLayer);
+	}
+
+	public normalize() : this {
+		const minLayerIndex = Math.min(...Array.from(this._layers.keys()));
+		if(minLayerIndex === 0) 
+			return this;
+
+		const newLayers = new Map<number, Layer<TNode>>();
+		for(const [index, layer] of this._layers) {
+			const newLayerIndex = index - minLayerIndex;
+			const newLayer = new Layer<TNode>(newLayerIndex);
+			newLayer.addNodes(layer.nodes);
+			newLayers.set(newLayerIndex, newLayer);
+		}
+
+		this._layers.clear();
+		newLayers.forEach((layer, index) => this._layers.set(index, layer));
+	
+		return this;
+	}
+
+
+	public get height() : number {
+		return this._layers.size;
+	}
+
+	public get width() : number {
+		return Math.max(...Array.from(this._layers.values()).map((layer) => layer.width));
+	}
+}

+ 4 - 11
src/v1/optimizer/siguiyama/CycleRemoveStep.ts

@@ -2,7 +2,6 @@ import CycleRemoveStepError from "../../errors/optimizer/CycleRemoveStepError.js
 import Edge from "../../graph/Edge.js";
 import Graph from "../../graph/Graph.js";
 import Node from "../../graph/Node.js";
-import GraphService from "../../services/GraphService.js";
 import AlgorithmStep from "../AlgorithmStep.js";
 import { SiguiyamaContext } from "./SiguiyamaContext.js";
 
@@ -10,12 +9,6 @@ import { SiguiyamaContext } from "./SiguiyamaContext.js";
  * Greedy Cycle Removal Algoirthm
  */
 export default class CycleRemoveStep implements AlgorithmStep<SiguiyamaContext> {
-	private readonly _graphService: GraphService;
-
-	public constructor(graphService: GraphService) {
-		this._graphService = graphService;
-	}
-
 	run(context: SiguiyamaContext): void {
 		const { graph } = context;
 		if(!graph)
@@ -34,13 +27,13 @@ export default class CycleRemoveStep implements AlgorithmStep<SiguiyamaContext>
 		const feedbackSet: NonNullable<SiguiyamaContext["feedbackSet"]> = [];
 
 		for(const node of nodes) {
-			const isSource = this._graphService.isSourceNode(graph, node.id);
+			const isSource = graph.isSourceNode(node.id);
 			if(isSource) {
 				l.push(node);
 				continue;
 			}
 
-			const isSink = this._graphService.isSinkNode(graph, node.id);
+			const isSink = graph.isSinkNode(node.id);
 			if(isSink) {
 				r.unshift(node);
 				continue;
@@ -74,8 +67,8 @@ export default class CycleRemoveStep implements AlgorithmStep<SiguiyamaContext>
 	}
 
 	private calculateScore(graph: Graph<Node, Edge<Node>>, node: Node) : number {
-		const outDegree = this._graphService.getNodeOutDegree(graph, node.id);
-		const inDegree = this._graphService.getNodeInDegree(graph, node.id);
+		const outDegree = graph.getNodeOutputs(node.id).length;
+		const inDegree = graph.getNodeInputs(node.id).length;
 
 		return outDegree - inDegree;
 	}

+ 240 - 49
src/v1/optimizer/siguiyama/LayerAssignmentStep.ts

@@ -1,19 +1,12 @@
 import LayerAssignmentStepError from "../../errors/optimizer/LayerAssignmentStepError.js";
 import Edge from "../../graph/Edge.js";
 import Graph from "../../graph/Graph.js";
+import Layering from "../../graph/layering/Layering.js";
 import Node from "../../graph/Node.js";
-import GraphService from "../../services/GraphService.js";
 import AlgorithmStep from "../AlgorithmStep.js";
-import { GraphLayering, SiguiyamaContext } from "./SiguiyamaContext.js";
+import { SiguiyamaContext } from "./SiguiyamaContext.js";
 
-// TODO
 export default class LayerAssignmentStep implements AlgorithmStep<SiguiyamaContext> {
-	private readonly _graphService: GraphService;
-
-	public constructor(graphService: GraphService) {
-		this._graphService = graphService;
-	}
-	
 	run(context: SiguiyamaContext): void {
 		const { graph, feedbackSet } = context;
 
@@ -22,40 +15,12 @@ export default class LayerAssignmentStep implements AlgorithmStep<SiguiyamaConte
 		if(!feedbackSet)
 			throw new LayerAssignmentStepError("Feedback set is undefined!");
 
-		const initialLayering = this.longestPathAlgorithm(graph);
-	}
-
-	private getSpanningTree(layering: NonNullable<SiguiyamaContext["layering"]>, dag: Graph<Node, Edge<Node>>) : Graph<Node, Edge<Node>> {
-		throw new Error("Not implemented!")
-	}
-
-	private getNodeRank(layering: GraphLayering, nodeId: Node["id"]) : number | null {
-		for(let i = 0; i < layering.length; i++) {
-			const layer = layering[i]!;
-			if(layer.includes(nodeId))
-				return i;
-		}
+		const layering = this.longestPathAlgorithm(graph);
+		this.networkSimplexAlgorithm(graph, layering);
 
-		return null;
-	}
-
-	private getEdgeSpan(layering: GraphLayering, edge: Edge<Node>) : number | null {
-		const { from, to } = edge;
-		const fromRank = this.getNodeRank(layering, from), toRank = this.getNodeRank(layering, to);
-		
-		if(fromRank == null || toRank == null)
-			return null;
-
-		return fromRank - toRank;
-	}
+		console.log(JSON.stringify(layering, null, 2))
 
-	private isTightEdge(layering: GraphLayering, edge: Edge<Node>) : boolean {
-		return this.getEdgeSpan(layering, edge) === 1;
-	}
-
-	private isLongEdge(layering: GraphLayering, edge: Edge<Node>) : boolean {
-		const span = this.getEdgeSpan(layering, edge);
-		return span ? span > 1 : false;
+		context.layering = layering;
 	}
 
 	private longestPathAlgorithm(dag: Graph<Node, Edge<Node>>) : NonNullable<SiguiyamaContext["layering"]> {
@@ -63,7 +28,7 @@ export default class LayerAssignmentStep implements AlgorithmStep<SiguiyamaConte
 
 		const alreadyLayeredNodeIds = new Set<Node["id"]>();
 		const underCurrentLayerNodeIds = new Set<Node["id"]>();
-		const layering: NonNullable<SiguiyamaContext["layering"]> = [];
+		const layering = new Layering<Node, Edge<Node>>();
 
 		let currentLayerIndex = 0;
 
@@ -80,13 +45,7 @@ export default class LayerAssignmentStep implements AlgorithmStep<SiguiyamaConte
 				if(!isAllSuccessorsUnder)
 					continue;
 
-				const currentLayer = layering[currentLayerIndex];
-
-				if(!currentLayer)
-					layering[currentLayerIndex] = [node.id];
-				else
-					currentLayer.push(node.id);
-
+				layering.assign(node, currentLayerIndex);
 				alreadyLayeredNodeIds.add(node.id);
 				isNodeSelected = true;
 			}
@@ -99,4 +58,236 @@ export default class LayerAssignmentStep implements AlgorithmStep<SiguiyamaConte
 
 		return layering;
 	}
+
+	private networkSimplexAlgorithm(dag: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>) : void {
+		const spanningTree = this.getFeasibleTree(dag, layering);
+
+		let leaveEdge = this.leaveEdge(dag, layering, spanningTree);
+		while(leaveEdge !== null) {
+			const enterEdge = this.enterEdge(dag, layering,	spanningTree, leaveEdge);
+			this.exchange(dag, layering, spanningTree, leaveEdge, enterEdge);
+			leaveEdge = this.leaveEdge(dag, layering, spanningTree);
+		}
+
+		layering.normalize();
+		this.balance(dag, layering);
+	}
+
+	private getFeasibleTree(dag: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>) : Set<Edge<Node>["id"]> {
+		const spanningTree = new Set<Edge<Node>["id"]>();
+		const visitedNodes = new Set<Node["id"]>();
+		const nodes = dag.getNodes(), edges = dag.getEdges();
+
+		if(nodes.length === 0)
+			return spanningTree;
+
+		const queue: Node["id"][] = [nodes[0]!.id];
+		visitedNodes.add(nodes[0]!.id);
+
+		while(queue.length > 0) {
+			const nodeId = queue.shift()!;
+
+			for(const edge of edges) {
+				if(!layering.isEdgeTight(edge))
+					continue;
+
+				const { id: edgeId, from, to } = edge;
+
+				const isFromCurrent = from === nodeId && !visitedNodes.has(to);
+				const isToCurrent = to === nodeId && !visitedNodes.has(from);
+
+				if(!isFromCurrent && !isToCurrent)
+					continue;
+
+				const neighbourId = isFromCurrent ? edge.to : edge.from;
+				spanningTree.add(edgeId);
+				visitedNodes.add(neighbourId);
+				queue.push(neighbourId);
+			}
+		}
+
+		return spanningTree;
+	}
+
+	private leaveEdge(dag: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>, spanningTree: Set<Edge<Node>["id"]>) : Edge<Node> | null {
+		const edges = dag.getEdges();
+
+		for(const edge of edges) {
+			if(!spanningTree.has(edge.id))
+				continue;
+
+			const cutValue = this.getCutValue(dag, layering, spanningTree, edge);
+			if(cutValue < 0)
+				return edge;
+		}
+
+		return null;
+	}
+
+	private getCutValue(dag: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>, spanningTree: Set<Edge<Node>["id"]>, edge: Edge<Node>) : number {
+		const { head, tail } = this.splitTree(dag, spanningTree, edge);
+
+		let cutValue = 0;
+
+		for(const edge of dag.getEdges()) {
+			const { from, to } = edge;
+			const fromInTail = tail.has(from);
+			const toInTail = tail.has(to);
+			const fromInHead = head.has(from);
+			const toInHead = head.has(to);
+
+			if(fromInTail && toInHead) cutValue++; // tail -> head
+			if(fromInHead && toInTail) cutValue--; // head -> tail
+		}
+
+		return cutValue;
+	}
+
+	private splitTree(dag: Graph<Node, Edge<Node>>, spanningTree: Set<Edge<Node>["id"]>, removedEdge: Edge<Node>) : { head: Set<Node["id"]>, tail: Set<Node["id"]> } {
+		const { from } = removedEdge;
+		
+		const treeEdges = dag.getEdges().filter((edge) => {
+			return spanningTree.has(edge.id) && edge.id !== removedEdge.id;
+		})
+
+		const tail = new Set<Node["id"]>();
+		const queue = [removedEdge.from];
+		tail.add(from);
+
+		while(queue.length > 0) {
+			const nodeId = queue.shift()!;
+			for(const edge of treeEdges) {
+				const { from, to } = edge;
+				if(from == nodeId && !tail.has(to)) {
+					tail.add(to);
+					queue.push(to);
+				}
+				if(to == nodeId && !tail.has(from)) {
+					tail.add(from);
+					queue.push(from);
+				}
+			}
+		}
+
+		const head = new Set<Node["id"]>(
+			dag.getNodes().map((node) => node.id).filter((id) => !tail.has(id))
+		);
+
+		return { head, tail };
+	}
+
+	private enterEdge(dag: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>, spanningTree: Set<Edge<Node>["id"]>, leaveEdge: Edge<Node>) : Edge<Node> {
+		const { head, tail } = this.splitTree(dag, spanningTree, leaveEdge);
+
+		let minSpan = Infinity;
+		let bestEdge: Edge<Node> | null = null;
+
+		for(const edge of dag.getEdges()) {
+			if(spanningTree.has(edge.id))
+				continue;
+
+			const fromInHead = head.has(edge.from);
+			const toInTail = tail.has(edge.to);
+			const fromInTail = tail.has(edge.from);
+			const toInHead = head.has(edge.to);
+
+			// edges between components (any direction)
+			if((!fromInHead || !toInTail) && (!fromInTail || !toInHead))
+				continue;
+
+			const span = layering.getEdgeSpan(edge);
+			if(span >= minSpan)
+				continue;
+
+			minSpan = span;
+			bestEdge = edge;
+		}
+
+		if(!bestEdge)
+			throw new LayerAssignmentStepError("No enter edge found");
+
+		return bestEdge;
+	}
+
+	private exchange(dag: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>, spanningTree: Set<Edge<Node>["id"]>, leaveEdge: Edge<Node>, enterEdge: Edge<Node>) : void {
+		spanningTree.delete(leaveEdge.id);
+		spanningTree.add(enterEdge.id);
+	}
+
+	private recomputeLayers(dag: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>, spanningTree: Set<Edge<Node>["id"]>) : void {
+		const nodes = dag.getNodes();
+		const treeEdges = dag.getEdges().filter((edge) => spanningTree.has(edge.id));
+
+		const inDegree = new Map<Node["id"], number>();
+		nodes.forEach((node) => inDegree.set(node.id, 0));
+		treeEdges.forEach((edge) => inDegree.set(edge.to, (inDegree.get(edge.to) ?? 0) + 1));
+
+		const source = nodes.find((node) => inDegree.get(node.id) === 0);
+		if(!source)
+			return;
+
+		layering.setLayerOf(source.id, 0);
+
+		const queue = [source.id];
+		const visited = new Set<Node["id"]>([source.id]);
+
+		while(queue.length > 0) {
+			const nodeId = queue.shift()!;
+			const currentLayer = layering.getLayerOf(nodeId) ?? 0;
+		
+			for(const edge of treeEdges) {
+				const { from, to } = edge;
+				if(from === nodeId && !visited.has(to)) {
+					layering.setLayerOf(to, currentLayer + 1);
+					visited.add(to);
+					queue.push(to);
+				}
+				if(to === nodeId && !visited.has(from)) {
+					layering.setLayerOf(from, currentLayer - 1);
+					visited.add(from);
+					queue.push(from);
+				}
+			}
+		}
+	}
+
+	private balance(dag: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>) : void {
+		for(const { id } of dag.getNodes()) {
+			const inDegree = dag.getNodeInputs(id).length;
+			const outDegree = dag.getNodeOutputs(id).length;
+
+			if(inDegree !== outDegree)
+				continue;
+
+			const currentLayer = layering.getLayerOf(id);
+			if(currentLayer == null)
+				continue;
+
+			const predecessorLayers = dag.getNodeInputs(id).map((node) => layering.getLayerOf(node.id)).filter((layer) => layer !== null);
+			if(predecessorLayers.length === 0)
+				continue;
+
+			const successorLayers = dag.getNodeOutputs(id).map((node) => layering.getLayerOf(node.id)).filter((layer) => layer !== null);
+			if(successorLayers.length === 0)
+				continue;
+
+			const minFeasible = Math.max(...predecessorLayers) + 1;
+			const maxFeasible = Math.min(...successorLayers) - 1;
+
+			let bestLayer = currentLayer;
+			let minWidth = layering.getLayers().find((layer) => layer.index === currentLayer)?.width ?? Infinity;
+
+			for(let i = minFeasible; i <= maxFeasible; i++) {
+				const width = layering.getLayers().find((layer) => layer.index === currentLayer)?.width ?? Infinity;
+				if(width >= minWidth)
+					continue;
+
+				minWidth = width;
+				bestLayer = i;
+			}
+
+			if(bestLayer !== currentLayer)
+				layering.setLayerOf(id, bestLayer);
+		}
+	}
 }

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

@@ -1,12 +1,11 @@
 import Edge from "../../graph/Edge.js"
+import Layering from "../../graph/layering/Layering.js";
 import Node from "../../graph/Node.js"
 import { AlgorithmContext } from "../AlgorithmContext.js"
 
 export type FeedbackSet = Edge<Node>[];
 
-export type GraphLayering = Node["id"][][];
-
 export type SiguiyamaContext = AlgorithmContext & Partial<{
 	feedbackSet: FeedbackSet,
-	layering: GraphLayering;
+	layering: Layering<Node, Edge<Node>>;
 }>

+ 0 - 30
src/v1/services/DefaultGraphService.ts

@@ -1,30 +0,0 @@
-import Edge from "../graph/Edge.js";
-import Graph from "../graph/Graph.js";
-import Node from "../graph/Node.js";
-import GraphService from "./GraphService.js";
-
-export default class DefaultGraphService implements GraphService<Node, Edge<Node>, Graph<Node, Edge<Node>>> {
-	getNodeInputs(graph: Graph<Node, Edge<Node>>, nodeId: string): Node[] {
-		return graph.getNodeInputs(nodeId);
-	}
-
-	getNodeInDegree(graph: Graph<Node, Edge<Node>>, nodeId: string): number {
-		return this.getNodeInputs(graph, nodeId).length;
-	}
-
-	getNodeOutputs(graph: Graph<Node, Edge<Node>>, nodeId: string): Node[] {
-		return graph.getNodeOutputs(nodeId);
-	}
-
-	getNodeOutDegree(graph: Graph<Node, Edge<Node>>, nodeId: string): number {
-		return this.getNodeOutputs(graph, nodeId).length;
-	}
-
-	isSourceNode(graph: Graph<Node, Edge<Node>>, nodeId: string): boolean {
-		return this.getNodeInputs(graph, nodeId).length === 0;
-	}
-
-	isSinkNode(graph: Graph<Node, Edge<Node>>, nodeId: string): boolean {
-		return this.getNodeOutputs(graph, nodeId).length === 0;
-	}
-}

+ 0 - 12
src/v1/services/GraphService.ts

@@ -1,12 +0,0 @@
-import Edge from "../graph/Edge.js";
-import Graph from "../graph/Graph.js";
-import Node from "../graph/Node.js";
-
-export default interface GraphService<TNode extends Node = Node, TEdge extends Edge<TNode> = Edge<TNode>, TGraph extends Graph<TNode, TEdge> = Graph<TNode, TEdge>> {
-	getNodeInputs(graph: TGraph, nodeId: TNode["id"]) : TNode[];
-	getNodeInDegree(graph: TGraph, nodeId: TNode["id"]) : number;
-	getNodeOutputs(graph: TGraph, nodeId: TNode["id"]) : TNode[];
-	getNodeOutDegree(graph: TGraph, nodeId: TNode["id"]) : number;
-	isSourceNode(graph: TGraph, nodeId: TNode["id"]) : boolean;
-	isSinkNode(graph: TGraph, nodeId: TNode["id"]) : boolean;
-}