Procházet zdrojové kódy

NEW: update cycle remove step
NEW: serializers v0
FIX: deserializers

Pavel Zhigalov před 3 týdny
rodič
revize
98847981d2

+ 9 - 11
src/index.ts

@@ -1,16 +1,14 @@
 import { readFileSync } from "node:fs";
-import Edge from "./v1/graph/Edge.js";
-import Graph from "./v1/graph/Graph.js";
-import Node from "./v1/graph/Node.js";
 import JointJsonDeserializer from "./v1/io/deserialize/json/JointJsonDeserializer.js";
-import { JsonDeserializer } from "./v1/io/deserialize/json/JsonDeserializer.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 SigiuyamaAlgorithm from "./v1/optimizer/sugiyama/SugiyamaAlgorithm.js";
-import CycleRemoveStep from "./v1/optimizer/sugiyama/CycleRemoveStep.js";
+import LayerAssignmentStep from "./v1/optimizer/siguiyama/LayerAssignmentStep.js";
 
-const deserializer: JsonDeserializer<Graph<Node, Edge<Node>>> = new JointJsonDeserializer();
-const graph = deserializer.deserialize(readFileSync("./data/graph.json", "utf-8"));
-const service = new DefaultGraphService()
-console.log(service.getNodeDegree(graph, "3"))
+const deserializer = new JointJsonDeserializer();
+const graph = deserializer.deserialize(String(readFileSync("./data/graph.json")));
+const graphService = new DefaultGraphService()
 
-new SigiuyamaAlgorithm().addStep(new CycleRemoveStep())
+const optimizer = new SigiuyamaAlgorithm().addStep(new CycleRemoveStep(graphService)).addStep(new LayerAssignmentStep());
+
+optimizer.run(graph);

+ 11 - 0
src/v1/errors/optimizer/CycleRemoveStepError.ts

@@ -0,0 +1,11 @@
+import OptimizerError from "./OptimizerError.js";
+
+/**
+ * Class for all exceptions in cycle removing step of Siguiyama algorithm
+ */
+export default class CycleRemoveStepError extends OptimizerError {
+	public constructor(message: string) {
+		super(message);
+		this.name = CycleRemoveStepError.name;
+	}
+}

+ 11 - 0
src/v1/errors/optimizer/LayerAssignmentStepError.ts

@@ -0,0 +1,11 @@
+import OptimizerError from "./OptimizerError.js";
+
+/**
+ * Class for all exceptions and error in layer assignment step of Siguiyama algorithm
+ */
+export default class LayerAssignmentStepError extends OptimizerError {
+	public constructor(message: string) {
+		super(message);
+		this.name = LayerAssignmentStepError.name;
+	}
+}

+ 11 - 0
src/v1/errors/optimizer/OptimizerError.ts

@@ -0,0 +1,11 @@
+import BPMNError from "../BPMNError.js";
+
+/**
+ * Common class for all errors end exceptions in optimizer methods and functions
+ */
+export default class OptimizerError extends BPMNError {
+	public constructor(message: string) {
+		super(message);
+		this.name = OptimizerError.name;
+	}
+}

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

@@ -1,61 +1,98 @@
-import { pipe, object, array, forward, partialCheck } from "valibot";
+import { array, forward, object, partialCheck, pipe } from "valibot";
 import Edge, { EdgeSchema } from "./Edge.js";
 import Node, { NodeSchema } from "./Node.js";
 
 export default class Graph<TNode extends Node, TEdge extends Edge<TNode>> {
-	private readonly _nodes: TNode[];
-	private readonly _edges: TEdge[];
 	private readonly _adjacencyList: Map<TNode["id"], Array<TNode["id"]>>;
+	private readonly _nodeMap: Map<TNode["id"], TNode>;
+	private readonly _edgeMap: Map<TEdge["id"], TEdge>;
 
 	public constructor(nodes: TNode[], edges: TEdge[]) {
-		this._nodes = nodes;
-		this._edges = edges;
 		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 { from, to } of edges) {
-			const adjacencyNodes = this._adjacencyList.get(from);
-			if(adjacencyNodes)
-				adjacencyNodes.push(to)
-			else
-				this._adjacencyList.set(from, [to])
 		}
+
+		for(const edge of edges)
+			this._edgeMap.set(edge.id, edge);
+
+		this.updateAdjacencyList();
 	}
 
 	public getNodes() : TNode[] {
-		return this._nodes;
+		return Array.from(this._nodeMap.values());
 	}
 
 	public getNode(nodeId: TNode["id"]): TNode | null {
-		return this._nodes.find((node) => node.id == nodeId) || null;
+		return this._nodeMap.get(nodeId) || null;
 	}
 
 	public getEdges() : TEdge[] {
-		return this._edges;
+		return Array.from(this._edgeMap.values());
 	}
 
 	public getEdge(edgeId: TEdge["id"]): TEdge | null {
-		return this._edges.find((edge) => edge.id == edgeId) || null;
+		return this._edgeMap.get(edgeId) || null;
 	}
+	
+	public updateEdgeById(id: TEdge["id"], edge: TEdge) : this {
+		this._edgeMap.set(id, edge);
+		this.updateAdjacencyList();
+		return this;
+	}
+
+	public getNodeInputs(nodeId: TNode["id"]) : TNode[] {
+		const inputs: TNode[] = []
+
+		for(const [id, outputs] of this._adjacencyList) {
+			if(!outputs.includes(nodeId))
+				continue;
 
-	public getNodeNeighbours(nodeId: TNode["id"]): TNode[] {
-		const neighbours: TNode[] = [];
-		const neighbourIds = this._adjacencyList.get(nodeId);
+			const input = this._nodeMap.get(id);
+			if(!input)
+				continue;
+
+			inputs.push(input);
+		}
 
-		if(!neighbourIds)
+		return inputs;
+	}
+
+	public getNodeOutputs(nodeId: TNode["id"]) : TNode[] {
+		const outputIds = this._adjacencyList.get(nodeId);
+		if(!outputIds)
 			return [];
 
-		neighbourIds.forEach((id) => {
-			const node = this._nodes.find((n) => n.id == id);
+		const outputs: TNode[] = [];
+
+		outputIds.forEach((id) => {
+			const node = this._nodeMap.get(id);
 			if(!node)
 				return;
 
-			neighbours.push(node);
+			outputs.push(node);
 		})
 
-		return neighbours;
+		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;
 	}
 }
 
@@ -64,6 +101,42 @@ export const GraphSchema = pipe(
 		nodes: array(NodeSchema),
 		edges: array(EdgeSchema)
 	}),
+	forward(
+		partialCheck(
+			[['nodes']],
+			(input) => {
+				const {nodes} = input;
+				for(let i = 0; i < nodes.length; i++) {
+					const currentNodeId = nodes[i]!.id;
+					const idIndex = nodes.findIndex((n) => n.id === currentNodeId);
+					if(idIndex !== i)
+						return false;
+				}
+
+				return true;
+			},
+			`Every node in "nodes" array must have an unique identifier!`
+		),
+		["nodes"]
+	),
+	forward(
+		partialCheck(
+			[['edges']],
+			(input) => {
+				const { edges } = input;
+				for(let i = 0; i < edges.length; i++) {
+					const currentNodeId = edges[i]!.id;
+					const idIndex = edges.findIndex((n) => n.id === currentNodeId);
+					if(idIndex !== i)
+						return false;
+				}
+
+				return true;
+			},
+			`Every edge in "edges" array must have an unique identifier!`
+		),
+		['edges']
+	),
 	forward(
 		partialCheck(
 			[['nodes'], ['edges']],
@@ -81,4 +154,4 @@ export const GraphSchema = pipe(
 		),
 		['edges']
 	)
-)
+)

+ 3 - 0
src/v1/io/serialize/Serializable.ts

@@ -0,0 +1,3 @@
+export default interface Serializable<TOut> {
+	serialize() : TOut;
+}

+ 3 - 0
src/v1/io/serialize/Serializer.ts

@@ -0,0 +1,3 @@
+export default interface Serializer<TIn, TOut> {
+	serialize(data: TIn) : TOut;
+}

+ 21 - 0
src/v1/io/serialize/json/GraphJsonSerializer.ts

@@ -0,0 +1,21 @@
+import Edge from "../../../graph/Edge.js";
+import Graph from "../../../graph/Graph.js";
+import Node from "../../../graph/Node.js";
+import { JsonSerializer } from "./JsonSerializer.js";
+
+type NodeJson = { id: string, x: number, y: number };
+type EdgeJson = { id: string, from: NodeJson["id"], to: NodeJson["id"] };
+type GraphJson = { nodes: NodeJson[], edges: EdgeJson[] };
+
+export default class GraphJsonSerializer implements JsonSerializer<Graph<Node, Edge<Node>>> {
+	serialize(data: Graph<Node, Edge<Node>>): string {
+		const graphJson: GraphJson = {
+			nodes: data.getNodes().map((node) => { return { id: node.id, x: node.x, y: node.y } }),
+			edges: data.getEdges().map((edge) => {
+				return { id: edge.id, from: edge.from, to: edge.to };
+			})
+		}
+
+		return JSON.stringify(graphJson);
+	}
+}

+ 3 - 0
src/v1/io/serialize/json/JsonSerializer.ts

@@ -0,0 +1,3 @@
+import Serializer from "../Serializer.js";
+
+export type JsonSerializer<TIn> = Serializer<TIn, string>;

+ 79 - 0
src/v1/optimizer/siguiyama/CycleRemoveStep.ts

@@ -0,0 +1,79 @@
+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";
+
+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)
+			throw new CycleRemoveStepError("Graph not found!");
+
+		const feedbackSet = this.removeCycles(graph);
+		console.log(feedbackSet);
+
+		context.feedbackSet = feedbackSet;
+	}
+
+	protected removeCycles(graph: Graph<Node, Edge<Node>>) : NonNullable<SiguiyamaContext["feedbackSet"]> {
+		const nodes = graph.getNodes(), edges = graph.getEdges();
+
+		const middleNodes: Node[] = [], l: Node[] = [], r: Node[] = [];
+		const feedbackSet: NonNullable<SiguiyamaContext["feedbackSet"]> = [];
+
+		for(const node of nodes) {
+			const isSource = this._graphService.isSourceNode(graph, node.id);
+			if(isSource) {
+				l.push(node);
+				continue;
+			}
+
+			const isSink = this._graphService.isSinkNode(graph, node.id);
+			if(isSink) {
+				r.unshift(node);
+				continue;
+			}
+
+			middleNodes.push(node);
+		}
+
+		middleNodes.sort((a, b) => this.calculateScore(graph, b) - this.calculateScore(graph, a));
+
+		while(middleNodes.length > 0) {
+			const node = middleNodes[0]!;
+			l.push(node);
+			middleNodes.splice(0, 1);
+		}
+
+		const sortedNodes = [...l, ...r.reverse()];
+
+		for(const edge of edges) {
+			const { from, to } = edge;
+			const fromIndex = sortedNodes.findIndex((node) => node.id === from);
+			const toIndex = sortedNodes.findIndex((node) => node.id === to);
+
+			if(fromIndex > toIndex) {
+				graph.updateEdgeById(edge.id, { ...edge, from: edge.to, to: edge.from });
+				feedbackSet.push(edge);
+			}
+		}
+
+		return feedbackSet;
+	}
+
+	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);
+
+		return outDegree - inDegree;
+	}
+}

+ 16 - 0
src/v1/optimizer/siguiyama/LayerAssignmentStep.ts

@@ -0,0 +1,16 @@
+import LayerAssignmentStepError from "../../errors/optimizer/LayerAssignmentStepError.js";
+import AlgorithmStep from "../AlgorithmStep.js";
+import { SiguiyamaContext } from "./SiguiyamaContext.js";
+
+export default class LayerAssignmentStep implements AlgorithmStep<SiguiyamaContext> {
+	run(context: SiguiyamaContext): void {
+		const { graph, feedbackSet } = context;
+
+		if(!graph)
+			throw new LayerAssignmentStepError("Graph was not found!");
+		if(!feedbackSet)
+			throw new LayerAssignmentStepError("Feedback set is undefined!");
+	
+
+	}
+}

+ 2 - 2
src/v1/optimizer/sugiyama/SugiyamaAlgorithm.ts → src/v1/optimizer/siguiyama/SiguiyamaAlgorithm.ts

@@ -2,9 +2,9 @@ import Edge from "../../graph/Edge.js";
 import Graph from "../../graph/Graph.js";
 import Node from "../../graph/Node.js";
 import Step from "../AlgorithmStep.js";
-import { SiguiyamaContext } from "./SugiyamaContext.js";
+import { SiguiyamaContext } from "./SiguiyamaContext.js";
 
-export default class SigiuyamaAlgorithm {
+export default class SiguiyamaAlgorithm {
 	private readonly _steps: Step<SiguiyamaContext>[] = [];
 
 	public addStep(step: Step<SiguiyamaContext>): this {

+ 4 - 3
src/v1/optimizer/sugiyama/SugiyamaContext.ts → src/v1/optimizer/siguiyama/SiguiyamaContext.ts

@@ -2,6 +2,7 @@ import Edge from "../../graph/Edge.js"
 import Node from "../../graph/Node.js"
 import { AlgorithmContext } from "../AlgorithmContext.js"
 
-export type SiguiyamaContext = AlgorithmContext & {
-	reversedEdges?: Edge<Node>[]
-}
+export type SiguiyamaContext = AlgorithmContext & Partial<{
+	feedbackSet: Edge<Node>[],
+	layers: Node[][]
+}>

+ 0 - 8
src/v1/optimizer/sugiyama/CycleRemoveStep.ts

@@ -1,8 +0,0 @@
-import Step from "../AlgorithmStep.js";
-import { SiguiyamaContext } from "./SugiyamaContext.js";
-
-export default class CycleRemoveStep implements Step<SiguiyamaContext> {
-	run(context: SiguiyamaContext): void {
-		throw new Error("Method not implemented.");
-	}
-}

+ 22 - 2
src/v1/services/DefaultGraphService.ts

@@ -4,7 +4,27 @@ import Node from "../graph/Node.js";
 import GraphService from "./GraphService.js";
 
 export default class DefaultGraphService implements GraphService<Node, Edge<Node>, Graph<Node, Edge<Node>>> {
-	getNodeDegree(graph: Graph<Node, Edge<Node>>, nodeId: string): number {
-		return graph.getNodeNeighbours(nodeId).length;
+	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;
 	}
 }

+ 6 - 1
src/v1/services/GraphService.ts

@@ -3,5 +3,10 @@ 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>> {
-	getNodeDegree(graph: TGraph, nodeId: TNode["id"]) : number;
+	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;
 }