Преглед на файлове

improved cycle remover step and added layer assignment step, some bugfixes

Pavel Zhigalov преди 3 дни
родител
ревизия
980a232a72

+ 14 - 2
src/graphs/abstract/Graph.ts

@@ -37,7 +37,19 @@ export default abstract class Graph<V extends Vertex, E extends Edge<V>> {
 		return this._store.getAdjacentVertices(vertex.id);
 	}
 
-	public getIncidentEdges(vertex: V): E[] {
-		return this._store.getIncidentEdges(vertex.id);
+	public getIncidentEdges(vertexId: V["id"]): E[] {
+		return this._store.getIncidentEdges(vertexId);
+	}
+
+	public getVertexDegree(vertexId: V["id"]): number {
+		return this._store.getVertexDegree(vertexId);
+	}
+
+	public getInputs(vertexId: V["id"]): V[] {
+		return this._store.getInputs(vertexId);
+	}
+
+	public getOutputs(vertexId: V["id"]): V[] {
+		return this._store.getOutputs(vertexId);
 	}
 }

+ 18 - 0
src/graphs/abstract/GraphStore.ts

@@ -33,9 +33,27 @@ export default interface GraphStore<V extends Vertex, E extends Edge<V>> {
      */
     getAdjacentVertices(id: V["id"]): V[];
 
+    /**
+     * Получить список вершин графа, из которых можно попасть в данную вершину
+     * @param id Уникальный идентификатор вершины, для которой нужно посчитать количество "входящих" вершин
+     */
+    getInputs(id: V["id"]): V[];
+
+    /**
+     * Получить список вершин графа, в которые можно попасть из данной веришны
+     * @param id Уникальный идентификатор вершины, для которой нужно найти количество "исходящих" вершин
+     */
+    getOutputs(id: V["id"]): V[];
+
     /**
      * Получить ребра, инцидентные вершине
      * @param id Уникальный идентификатор вершины, для которой нужно получить инцидентные ребра
      */
     getIncidentEdges(id: V["id"]): E[];
+
+    /**
+     * Получение степени вершины в графе
+     * @param id Уникальный идентификатор вершины, степень которой необходимо найти
+     */
+    getVertexDegree(id: V["id"]): number;
 }

+ 22 - 6
src/graphs/types/SimpleGraphStore.ts

@@ -1,6 +1,7 @@
 import GraphStore from "../abstract/GraphStore.js";
 import XVertex from "./XVertex.js";
 import XDirectedEdge from "./XDirectedEdge.js";
+import {inspect} from "node:util";
 
 export default class SimpleGraphStore implements GraphStore<XVertex, XDirectedEdge> {
 	private readonly _vertices: XVertex[];
@@ -28,20 +29,35 @@ export default class SimpleGraphStore implements GraphStore<XVertex, XDirectedEd
 	}
 
 	getAdjacentVertices(id: number): XVertex[] {
-		const v = this.getVertex(id);
+		return this.getIncidentEdges(id).map((edge) => edge.target);
+	}
 
+	getIncidentEdges(id: XVertex["id"]): XDirectedEdge[] {
+		const v = this.getVertex(id);
 		if(!v)
-		// TODO Replace by throwing error
-			return []
+			return [];
 
-		return this._edges.filter((edge) => edge.start.id == v.id).map((edge) => edge.target);
+		return this._edges.filter((edge) => edge.start.id == v.id || edge.target.id == v.id);
 	}
 
-	getIncidentEdges(id: XVertex["id"]): XDirectedEdge[] {
+	getVertexDegree(id: XVertex["id"]): number {
+		const incidentEdges = this.getIncidentEdges(id);
+		return incidentEdges.length;
+	}
+
+	getInputs(id: number): XVertex[] {
+		const v = this.getVertex(id);
+		if(!v)
+			return [];
+        
+		return this._edges.filter((e) => e.target.id == v.id).map((e) => e.start);
+	}
+
+	getOutputs(id: number): XVertex[] {
 		const v = this.getVertex(id);
 		if(!v)
 			return [];
 
-		return this._edges.filter((edge) => edge.start.id == v.id);
+		return this._edges.filter((e) => e.start.id == v.id).map((e) => e.target);
 	}
 }

+ 6 - 3
src/index.ts

@@ -5,13 +5,16 @@ import SimpleGraphStore from "./graphs/types/SimpleGraphStore.js";
 import SiguyamaOptimizer from "./optimizer/types/sugiyama-based/SiguyamaOptimizer.js";
 import CycleRemoverStep from "./optimizer/types/sugiyama-based/CycleRemoverStep.js";
 import LinearOptimizationStep from "./optimizer/types/sugiyama-based/LinearOptimizationStep.js";
+import LayerAssignmentStep from "./optimizer/types/sugiyama-based/LayerAssignmentStep.js";
 
 const v = [new XVertex(1), new XVertex(2), new XVertex(3), new XVertex(4)]
-const e = [new XDirectedEdge(1, v[0]!, v[1]!), new XDirectedEdge(2, v[1]!, v[2]!), new XDirectedEdge(3, v[2]!, v[0]!), new XDirectedEdge(4, v[0]!, v[3]!)]
+const e = [new XDirectedEdge(1, v[0]!, v[1]!), new XDirectedEdge(2, v[1]!, v[2]!), new XDirectedEdge(3, v[0]!, v[2]!), new XDirectedEdge(4, v[0]!, v[3]!)]
 const g = new XDirectedGraph(new SimpleGraphStore(v, e));
 
-const opt = new SiguyamaOptimizer(new CycleRemoverStep().setNext(new LinearOptimizationStep()));
+const cycleRemover = new CycleRemoverStep();
+cycleRemover.setNext(new LayerAssignmentStep()).setNext(new LinearOptimizationStep());
 
-opt.optimize(g)
+new SiguyamaOptimizer(cycleRemover)
+	.optimize(g)
 	.then((g) => console.log(`Result: ${JSON.stringify(g)}`))
 	.catch((err) => console.error(err));

+ 2 - 2
src/optimizer/abstract/OptimizerStep.ts

@@ -3,9 +3,9 @@ import IOptimizerStep from "../interfaces/IOptimizerStep.js";
 export default abstract class OptimizerStep<Context> implements IOptimizerStep<Context> {
 	private _nextStep: IOptimizerStep<Context> | undefined;
 
-	public setNext(step: IOptimizerStep<Context>): OptimizerStep<Context> {
+	public setNext(step: OptimizerStep<Context>): OptimizerStep<Context> {
 		this._nextStep = step;
-		return this;
+		return step;
 	}
 
 	public process(context: Context): Context {

+ 3 - 1
src/optimizer/abstract/sugiyama-based/SugiyamaOptimizerStep.ts

@@ -1,11 +1,13 @@
 import XDirectedGraph from "../../../graphs/types/XDirectedGraph.js";
 import OptimizerStep from "../OptimizerStep.js";
 import XDirectedEdge from "../../../graphs/types/XDirectedEdge.js";
+import XVertex from "../../../graphs/types/XVertex.js";
 
 export type Context = {
     graph: XDirectedGraph,
     acyclicGraph?: XDirectedGraph,
-    reversedEdges?: XDirectedEdge[]
+    reversedEdges?: XDirectedEdge[],
+    layers?: XVertex[][]
 }
 
 export default abstract class SugiyamaOptimizerStep extends OptimizerStep<Context> {}

+ 10 - 0
src/optimizer/errors/OptimizerError.ts

@@ -0,0 +1,10 @@
+import BPMNError from "../../core/errors/BPMNError.js";
+
+/**
+ * Ошибка, возникшая в результате работы процессов оптимизации
+ */
+export default class OptimizerError extends BPMNError {
+	public constructor(message: string) {
+		super(`Error during optimization processes: ${message}`);
+	}
+}

+ 57 - 2
src/optimizer/types/sugiyama-based/CycleRemoverStep.ts

@@ -1,8 +1,63 @@
-import SugiyamaOptimizerStep, { Context } from "../../abstract/sugiyama-based/SugiyamaOptimizerStep.js";
+import SugiyamaOptimizerStep, {Context} from "../../abstract/sugiyama-based/SugiyamaOptimizerStep.js";
+import XDirectedGraph from "../../../graphs/types/XDirectedGraph.js";
+import XDirectedEdge from "../../../graphs/types/XDirectedEdge.js";
+import XVertex from "../../../graphs/types/XVertex.js";
+import SimpleGraphStore from "../../../graphs/types/SimpleGraphStore.js";
+
+type CycleRemoverStepOutput = {
+    reversedEdges: NonNullable<Context["reversedEdges"]>,
+    acyclicGraph: NonNullable<Context["acyclicGraph"]>
+};
 
 export default class CycleRemoverStep extends SugiyamaOptimizerStep {
 	public process(context: Context): Context {
-		//TODO
+		const { graph } = context;
+        
+		const { reversedEdges, acyclicGraph } = this.cycleRemover(graph);
+
+		context.acyclicGraph = acyclicGraph;
+		context.reversedEdges = reversedEdges;
+
 		return super.process(context);
 	}
+
+	protected cycleRemover(graph: XDirectedGraph) : CycleRemoverStepOutput {
+		return this.dfs(graph);
+	}
+
+	private dfs(graph: XDirectedGraph) : CycleRemoverStepOutput {
+		const stack: { incidentEdge?: XDirectedEdge, vertex: XVertex }[] = [];
+		const visited = new Set<XVertex>();
+		const reversedEdges = new Set<XDirectedEdge>();
+
+		const startVertex = graph.vertices[Math.floor(Math.random() * graph.vertices.length)]!;
+
+		stack.push({ vertex: startVertex });
+
+		while(stack.length > 0) {
+			const current = stack.pop();
+
+			if(!current)
+				continue;
+
+			const currVertex = current.vertex;
+
+			if(current.incidentEdge && visited.has(currVertex)) {
+				reversedEdges.add(current.incidentEdge)
+				continue;
+			}
+
+			visited.add(currVertex);
+
+			for(const edge of graph.edges.filter((edge) => edge.start.id == currVertex.id))
+				stack.push({ incidentEdge: edge, vertex: edge.target });
+		}
+
+		const acyclicGraph = new XDirectedGraph(new SimpleGraphStore(
+			graph.vertices,
+			graph.edges.map((edge) => reversedEdges.has(edge) ? new XDirectedEdge(edge.id, edge.target, edge.start) : edge)
+		));
+
+		return { acyclicGraph: acyclicGraph, reversedEdges: Array.from(reversedEdges) }
+	}
 }

+ 79 - 0
src/optimizer/types/sugiyama-based/LayerAssignmentStep.ts

@@ -0,0 +1,79 @@
+import SugiyamaOptimizerStep, {Context} from "../../abstract/sugiyama-based/SugiyamaOptimizerStep.js";
+import OptimizerError from "../../errors/OptimizerError.js";
+import XDirectedGraph from "../../../graphs/types/XDirectedGraph.js";
+import XVertex from "../../../graphs/types/XVertex.js";
+
+export default class LayerAssignmentStep extends SugiyamaOptimizerStep {
+	process(context: Context): Context {
+		const { acyclicGraph } = context;
+
+		if(!acyclicGraph)
+			throw new OptimizerError("Directed acyclic graph is null or undefined");
+
+		context.layers = this.assignLayers(acyclicGraph);
+
+		console.log(context.layers)
+
+		return super.process(context);
+	}
+
+	protected assignLayers(graph: XDirectedGraph): XVertex[][] {
+		const topologicalOrder = this.topologicalSort(graph);
+
+		const layers = new Map<XVertex, number>()
+
+		for(const vertex of topologicalOrder) {
+			const inputs = graph.getInputs(vertex.id);
+
+			if(inputs.length == 0) {
+				layers.set(vertex, 0);
+			} else {
+				const maxInputsLayer = Math.max(
+					...inputs.map((input) => layers.get(input) ?? 0)
+				);
+				layers.set(vertex, maxInputsLayer + 1);
+			}
+		}
+
+		const maxLayer = Math.max(...layers.values());
+		const result: XVertex[][] = Array.from({ length: maxLayer + 1 }, () => []);
+
+		for(const [vertex, layer] of layers)
+			result[layer]!.push(vertex);
+
+		return result;
+	}
+
+	private topologicalSort(graph: XDirectedGraph): XVertex[] {
+		const degreeMap = new Map<XVertex, number>();
+
+		for(const vertex of graph.vertices)
+			degreeMap.set(vertex, graph.getInputs(vertex.id).length);
+
+		const stack: XVertex[] = [];
+
+		for(const [vertex, degree] of degreeMap)
+			if(degree == 0)
+				stack.push(vertex);
+
+		const result: XVertex[] = [];
+
+		while(stack.length > 0) {
+			const curr = stack.shift()!;
+			result.push(curr);
+
+			for(const neighbour of graph.getOutputs(curr.id)) {
+				const newDegree = (degreeMap.get(neighbour) ?? 0) - 1;
+				degreeMap.set(neighbour, newDegree);
+
+				if(newDegree == 0)
+					stack.push(neighbour);
+			}
+		}
+
+		if(result.length != graph.vertices.length)
+			throw new OptimizerError("Cycle is detected..")
+
+		return result;
+	}
+}

+ 1 - 1
src/optimizer/types/sugiyama-based/LinearOptimizationStep.ts

@@ -1,4 +1,4 @@
-import SugiyamaOptimizerStep, { Context } from "../../abstract/sugiyama-based/SugiyamaOptimizerStep.js";
+import SugiyamaOptimizerStep, {Context} from "../../abstract/sugiyama-based/SugiyamaOptimizerStep.js";
 
 export default class LinearOptimizationStep extends SugiyamaOptimizerStep {
 	public process(context: Context): Context {