Pavel Zhigalov 2 недель назад
Родитель
Сommit
a2ed54b9c2

+ 7 - 7
src/index.ts

@@ -1,14 +1,14 @@
 import { readFileSync } from "node:fs";
 import JointJsonDeserializer from "./v1/io/deserialize/json/JointJsonDeserializer.js";
-import SigiuyamaAlgorithm from "./v1/optimizer/siguiyama/SiguiyamaAlgorithm.js";
-import CycleRemoveStep from "./v1/optimizer/siguiyama/CycleRemoveStep.js";
-import DefaultGraphService from "./v1/services/DefaultGraphService.js";
-import LayerAssignmentStep from "./v1/optimizer/siguiyama/LayerAssignmentStep.js";
 
 const deserializer = new JointJsonDeserializer();
 const graph = deserializer.deserialize(String(readFileSync("./data/graph.json")));
-const graphService = new DefaultGraphService()
+// const graphService = new DefaultGraphService()
+// const optimizer = new SigiuyamaAlgorithm().addStep(new CycleRemoveStep(graphService)).addStep(new LayerAssignmentStep(graphService));
 
-const optimizer = new SigiuyamaAlgorithm().addStep(new CycleRemoveStep(graphService)).addStep(new LayerAssignmentStep(graphService));
+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/GraphError.ts

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

+ 154 - 26
src/v1/graph/Graph.ts

@@ -1,26 +1,86 @@
 import { array, forward, object, partialCheck, pipe } from "valibot";
 import Edge, { EdgeSchema } from "./Edge.js";
 import Node, { NodeSchema } from "./Node.js";
+import GraphError from "../errors/graph/GraphError.js";
+
+type GraphCache<TNode extends Node> = Partial<{
+	isAcyclic: boolean,
+	adjacencyList: Map<TNode["id"], Array<TNode["id"]>>
+}>
 
 export default class Graph<TNode extends Node, TEdge extends Edge<TNode>> {
-	private readonly _adjacencyList: Map<TNode["id"], Array<TNode["id"]>>;
+	private _cache: GraphCache<TNode> = {};
+
 	private readonly _nodeMap: Map<TNode["id"], TNode>;
 	private readonly _edgeMap: Map<TEdge["id"], TEdge>;
 
 	public constructor(nodes: TNode[], edges: TEdge[]) {
-		this._adjacencyList = new Map();
 		this._nodeMap = new Map();
 		this._edgeMap = new Map();
-
-		for(const node of nodes) {
+		
+		for(const node of nodes)
 			this._nodeMap.set(node.id, node);
-			this._adjacencyList.set(node.id, []);
-		}
 
 		for(const edge of edges)
 			this._edgeMap.set(edge.id, edge);
+	}
+
+	private get adjacencyList(): Map<TNode["id"], Array<TNode["id"]>> {
+		const cache = this._cache.adjacencyList;
+
+		if(cache)
+			return cache;
+
+		const adjacencyList = new Map<TNode["id"], Array<TNode["id"]>>();
+
+		for(const [, edge] of this._edgeMap) {
+			const { from, to } = edge;
+			const adjacencyNodes = adjacencyList.get(from);
+			if(adjacencyNodes)
+				adjacencyNodes.push(to);
+			else
+				adjacencyList.set(from, [to]);
+		}
+
+		return this._cache.adjacencyList = adjacencyList;
+	}
+
+	public get isAcyclic() : boolean {
+		const cache = this._cache.isAcyclic;
+
+		if(cache !== undefined) {
+			console.log('from cache')
+			return cache;
+		}
+
+		const queue: TNode["id"][] = [];
+		const nodeToInDegree = new Map<TNode["id"], number>();
+		const nodesCount = this._nodeMap.size;
+		let visitedNodesCount = 0;
+
+		for(const { id } of this.getNodes())
+			nodeToInDegree.set(id, this.getNodeInputs(id).length);
+
+		for(const [nodeId, inDegree] of nodeToInDegree)
+			if(inDegree === 0)
+				queue.push(nodeId);
+
+		while(queue.length > 0) {
+			const nodeId = queue.pop()!;
+			visitedNodesCount++;
 
-		this.updateAdjacencyList();
+			for(const { id: neighbourId } of this.getNodeOutputs(nodeId)) {
+				const inDegree = nodeToInDegree.get(neighbourId);
+				const newInDegree = inDegree !== undefined ? inDegree - 1 : 0;
+
+				nodeToInDegree.set(neighbourId, newInDegree);
+
+				if(newInDegree === 0)
+					queue.push(neighbourId);
+			}
+		}
+
+		return this._cache.isAcyclic = visitedNodesCount !== nodesCount;
 	}
 
 	public getNodes() : TNode[] {
@@ -31,6 +91,47 @@ export default class Graph<TNode extends Node, TEdge extends Edge<TNode>> {
 		return this._nodeMap.get(nodeId) || null;
 	}
 
+	public addNode(node: TNode) : this {
+		const { id } = node;
+
+		if(this._nodeMap.has(id))
+			throw new GraphError(`Can't add a new node to graph: node with id ${id} already exists`);
+
+		this._nodeMap.set(id, node);
+		this.invalidateCache();
+
+		return this;
+	}
+
+	public updateNode(nodeId: TNode["id"], node: TNode) : this {
+		const { id } = node;
+
+		if(!this._nodeMap.has(nodeId))
+			throw new GraphError(`Can't update node: node with id ${nodeId} doesn't exist`);
+
+		if(nodeId != id)
+			throw new GraphError(`Cant' update node: node id cannot be changed`);
+
+		this._nodeMap.set(nodeId, node);
+		this.invalidateCache();
+
+		return this;
+	}
+
+	public removeNode(nodeId: TNode["id"]) : this {
+		if(!this._nodeMap.has(nodeId))
+			throw new GraphError(`Can't delete a node from graph: node with id ${nodeId} doesn't exists`);
+
+		for(const [edgeId, edge] of this._edgeMap)
+			if(edge.from == nodeId || edge.to == nodeId)
+				this._edgeMap.delete(edgeId);
+
+		this._nodeMap.delete(nodeId);
+		this.invalidateCache();
+
+		return this;
+	}
+
 	public getEdges() : TEdge[] {
 		return Array.from(this._edgeMap.values());
 	}
@@ -38,17 +139,55 @@ export default class Graph<TNode extends Node, TEdge extends Edge<TNode>> {
 	public getEdge(edgeId: TEdge["id"]): TEdge | null {
 		return this._edgeMap.get(edgeId) || null;
 	}
-	
-	public updateEdgeById(id: TEdge["id"], edge: TEdge) : this {
+
+	public addEdge(edge: TEdge) : this {
+		const { id, from, to } = edge;
+
+		if(this._edgeMap.has(id))
+			throw new GraphError(`Can't add a new edge to graph: edge with id ${id} already exists`);
+
+		if(!this._nodeMap.has(from) || !this._nodeMap.has(to))
+			throw new GraphError(`Can't add a new edge to graph: edge references non-existing nodes`);
+
 		this._edgeMap.set(id, edge);
-		this.updateAdjacencyList();
+		this.invalidateCache();
+
+		return this;
+	}
+
+	
+	public updateEdge(id: TEdge["id"], edge: TEdge) : this {
+		const { id: edgeId, from, to } = edge;
+	
+		if(!this._edgeMap.has(id))
+			throw new Error(`Can't update edge: edge with id "${id}" does not exist`);
+
+		if(id !== edgeId)
+			throw new Error(`Can't update edge: edge id cannot be changed`);
+
+		if(!this._nodeMap.has(from) || !this._nodeMap.has(to))
+			throw new Error(`Can't update edge: edge references non-existing nodes`);
+
+		this._edgeMap.set(edgeId, edge);
+		this.invalidateCache();
+
+		return this;
+	}
+	
+	public removeEdge(edgeId: TEdge["id"]): this {
+		if(!this._edgeMap.has(edgeId))
+			return this;
+
+		this._edgeMap.delete(edgeId);
+		this.invalidateCache();
+
 		return this;
 	}
 
 	public getNodeInputs(nodeId: TNode["id"]) : TNode[] {
-		const inputs: TNode[] = []
+		const inputs: TNode[] = [];
 
-		for(const [id, outputs] of this._adjacencyList) {
+		for(const [id, outputs] of this.adjacencyList) {
 			if(!outputs.includes(nodeId))
 				continue;
 
@@ -63,7 +202,7 @@ export default class Graph<TNode extends Node, TEdge extends Edge<TNode>> {
 	}
 
 	public getNodeOutputs(nodeId: TNode["id"]) : TNode[] {
-		const outputIds = this._adjacencyList.get(nodeId);
+		const outputIds = this.adjacencyList.get(nodeId);
 		if(!outputIds)
 			return [];
 
@@ -80,19 +219,8 @@ export default class Graph<TNode extends Node, TEdge extends Edge<TNode>> {
 		return outputs;
 	}
 
-	private updateAdjacencyList() : this {
-		this._adjacencyList.clear();
-
-		for(const [, edge] of this._edgeMap) {
-			const { from, to } = edge;
-			const adjacencyNodes = this._adjacencyList.get(from);
-			if(adjacencyNodes)
-				adjacencyNodes.push(to);
-			else
-				this._adjacencyList.set(from, [to]);
-		}
-
-		return this;
+	private invalidateCache(): void {
+		this._cache = {};
 	}
 }
 

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

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

+ 1 - 1
src/v1/optimizer/siguiyama/CycleRemoveStep.ts

@@ -65,7 +65,7 @@ export default class CycleRemoveStep implements AlgorithmStep<SiguiyamaContext>
 			const toIndex = sortedNodes.findIndex((node) => node.id === to);
 
 			if(fromIndex > toIndex) {
-				graph.updateEdgeById(edge.id, { ...edge, from: edge.to, to: edge.from });
+				graph.updateEdge(edge.id, { ...edge, from: edge.to, to: edge.from });
 				feedbackSet.push(edge);
 			}
 		}