From dde86e056a994117704364ad9bbad267bf4300a1 Mon Sep 17 00:00:00 2001 From: Mike Molinari Date: Wed, 17 Aug 2022 00:54:52 +0000 Subject: [PATCH] Added start of wire harness workbench. Shortest path algorithm. --- modules/workbenches/registry.ts | 7 +- .../features/autoRoute/autoRoute.operation.ts | 109 +++++++++ .../features/autoRoute/docs/index.md | 0 .../autoRoute/pathFinderLogic/demo.css | 15 ++ .../autoRoute/pathFinderLogic/demo.js | 130 ++++++++++ .../pathFinderLogic/js/digraphs/digraph.js | 86 +++++++ .../js/digraphs/digraph_simple_builder.js | 22 ++ .../pathFinderLogic/js/digraphs/spt.js | 229 ++++++++++++++++++ .../pathFinderLogic/js/digraphs/spt_html.js | 37 +++ .../pathFinderLogic/js/digraphs/spt_old.js | 118 +++++++++ .../pathFinderLogic/js/heap/min_heap.js | 125 ++++++++++ .../js/heap/min_heap_original.js | 83 +++++++ .../pathFinderLogic/js/heap/test_heap.html | 75 ++++++ .../js/sided_ab_graphs/digraph_ab_builder.js | 98 ++++++++ .../js/sided_ab_graphs/digraph_ab_elements.js | 48 ++++ .../digraph_ab_pairs_analyser.js | 84 +++++++ .../digraph_ab_shortest_path_finder.js | 76 ++++++ .../js/test_digraph_builders.js | 78 ++++++ .../pathFinderLogic/sided_ab_demo.html | 77 ++++++ .../pathFinderLogic/sided_ab_pairs.html | 53 ++++ .../pathFinderLogic/sided_ab_pairs_grid.html | 81 +++++++ .../pathFinderLogic/simple_demo.html | 118 +++++++++ .../workbenches/routingElectrical/index.ts | 27 +++ 23 files changed, 1774 insertions(+), 2 deletions(-) create mode 100644 modules/workbenches/routingElectrical/features/autoRoute/autoRoute.operation.ts create mode 100644 modules/workbenches/routingElectrical/features/autoRoute/docs/index.md create mode 100644 modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/demo.css create mode 100644 modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/demo.js create mode 100644 modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/digraphs/digraph.js create mode 100644 modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/digraphs/digraph_simple_builder.js create mode 100644 modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/digraphs/spt.js create mode 100644 modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/digraphs/spt_html.js create mode 100644 modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/digraphs/spt_old.js create mode 100644 modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/heap/min_heap.js create mode 100644 modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/heap/min_heap_original.js create mode 100644 modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/heap/test_heap.html create mode 100644 modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/sided_ab_graphs/digraph_ab_builder.js create mode 100644 modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/sided_ab_graphs/digraph_ab_elements.js create mode 100644 modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/sided_ab_graphs/digraph_ab_pairs_analyser.js create mode 100644 modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/sided_ab_graphs/digraph_ab_shortest_path_finder.js create mode 100644 modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/test_digraph_builders.js create mode 100644 modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/sided_ab_demo.html create mode 100644 modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/sided_ab_pairs.html create mode 100644 modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/sided_ab_pairs_grid.html create mode 100644 modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/simple_demo.html create mode 100644 modules/workbenches/routingElectrical/index.ts diff --git a/modules/workbenches/registry.ts b/modules/workbenches/registry.ts index 995da623..c80e7ff9 100644 --- a/modules/workbenches/registry.ts +++ b/modules/workbenches/registry.ts @@ -1,8 +1,11 @@ import {ModelerWorkspace} from "workbenches/modeler"; -import { SheetMetalWorkspace } from "workbenches/sheetMetal"; +import {SheetMetalWorkspace} from "workbenches/sheetMetal"; +// import { RoutingElectricalWorkspace } from "./routingElectrical"; import {WorkbenchConfig} from "cad/workbench/workbenchService"; export const WorkbenchRegistry: WorkbenchConfig[] = [ - ModelerWorkspace, SheetMetalWorkspace + ModelerWorkspace, + SheetMetalWorkspace, + //RoutingElectricalWorkspace, ] \ No newline at end of file diff --git a/modules/workbenches/routingElectrical/features/autoRoute/autoRoute.operation.ts b/modules/workbenches/routingElectrical/features/autoRoute/autoRoute.operation.ts new file mode 100644 index 00000000..9dcbd626 --- /dev/null +++ b/modules/workbenches/routingElectrical/features/autoRoute/autoRoute.operation.ts @@ -0,0 +1,109 @@ +import {roundValueForPresentation as r} from 'cad/craft/operationHelper'; +import {ApplicationContext} from "cad/context"; +import {OperationDescriptor} from "cad/craft/operationBundle"; +import {abSegmentsToDigraph} from "./pathFinderLogic/js/sided_ab_graphs/digraph_ab_builder" +import { + findShortestPathForAllPairsAsync, + simpleArrayToPointPairs +} from "./pathFinderLogic/js/sided_ab_graphs/digraph_ab_pairs_analyser" + + +interface autoRouteParams { + thickness: number; +} + + +export const AutoRouteOperation: OperationDescriptor = { + id: 'RE_AUTO_ROUTE', + label: 'AUTO ROUTE', + icon: 'img/cad/autoRoute', + info: 'Auto Routing tool', + path: __dirname, + paramsInfo: ({ thickness, }) => `(${r(thickness)} )`, + run: async (params: autoRouteParams, ctx: ApplicationContext) => { + + const occ = ctx.occService; + const oci = occ.commandInterface; + + + //place code here to retrieve list of roughing splines from the model. + const segments = [ + { "id": "seg1", "firstPoint": "p1", "firstSide": "A", "secondPoint": "p2", "secondSide": "A", "weight": 8 }, + { "id": "seg2", "firstPoint": "p1", "firstSide": "B", "secondPoint": "p3", "secondSide": "A", "weight": 2 }, + { "id": "seg3", "firstPoint": "p1", "firstSide": "B", "secondPoint": "p2", "secondSide": "A", "weight": 3 }, + { "id": "seg11", "firstPoint": "p1", "firstSide": "A", "secondPoint": "p5", "secondSide": "A", "weight": 10.1 }, + { "id": "seg12", "firstPoint": "p1", "firstSide": "A", "secondPoint": "p5", "secondSide": "B", "weight": 10.1 }, + { "id": "seg21", "firstPoint": "p2", "firstSide": "B", "secondPoint": "p3", "secondSide": "B", "weight": 15 }, + { "id": "otherSeg1", "firstPoint": "other1", "firstSide": "A", "secondPoint": "other2", "secondSide": "B", "weight": 115 } + ]; + + + const connections = [ + { id: "WIRE1", startPoint: "p1", endPoint: "p2", wireInfo: { diameter: 2 } }, + { id: "WIRE2", startPoint: "p2", endPoint: "p3", wireInfo: { diameter: 5 } }, + { id: "WIRE4", startPoint: "p2", endPoint: "p3", wireInfo: { diameter: 5 } }, + { id: "WIRE6", startPoint: "p2", endPoint: "p3", wireInfo: { diameter: 5 } }, + ]; + + + const report = await sidedAdPairsResolve(segments, connections, false, "error", "timing", ""); + + + const segmentResults = []; + + for (const wire of report) { + // ...use `element`... + //console.log(wire); + + for (const segment of wire.route.segments) { + // ...use `element`... + //console.log(segment); + if (segmentResults[segment] == undefined) segmentResults[segment] = []; + segmentResults[segment].push(wire); + } + } + + console.log(report, segmentResults); + //alert(JSON.stringify(report, null, 2)); + + return { + created: [], + consumed: [] + }; + + }, + + + form: [ + { + type: 'number', + label: 'Thickness', + name: 'thickness', + defaultValue: 1, + }, + ], +} + + + +async function sidedAdPairsResolve(segmentsJson, connections, brief, errorElementId, timingElementId, progressBarId) { + const t0 = performance.now(); + const digraph = abSegmentsToDigraph(segmentsJson); + let pointPairs = simpleArrayToPointPairs(connections); + console.log(pointPairs); + const t1 = performance.now(); + await findShortestPathForAllPairsAsync(digraph, pointPairs, progressBarId == null ? null : part => { + //document.getElementById(progressBarId).value = Math.round(part * 100) + }); + // - note: without "await" we will have now non-processed pointPairs! + const t2 = performance.now(); + const timing = "Executing: " + (t2 - t0) + " ms = " + + (t1 - t0) + " + " + (t2 - t1) + " for " + + pointPairs.length + " pairs from " + digraph.getNumberOfNodes() + " nodes, " + + ((t2 - t1) / pointPairs.length) + " ms/pair
 "; + //document.getElementById(timingElementId).innerHTML = timing; + if (brief) { + pointPairs = pointPairs.map(pair => pair.briefClone()) + } + return pointPairs; +} \ No newline at end of file diff --git a/modules/workbenches/routingElectrical/features/autoRoute/docs/index.md b/modules/workbenches/routingElectrical/features/autoRoute/docs/index.md new file mode 100644 index 00000000..e69de29b diff --git a/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/demo.css b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/demo.css new file mode 100644 index 00000000..d9a6c2e1 --- /dev/null +++ b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/demo.css @@ -0,0 +1,15 @@ +body { + background-color: white; + color: black; + font-family: sans-serif; +} + +.error { + color: red; + font-weight: bold; + font-size: larger; +} + +.comment { + color: #888; +} diff --git a/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/demo.js b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/demo.js new file mode 100644 index 00000000..60e16863 --- /dev/null +++ b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/demo.js @@ -0,0 +1,130 @@ +import {ShortestPathTree} from "./js/digraphs/spt.js" +import {allPathsToHTML, pathInformationToHTML, singlePathToHTML} from "./js/digraphs/spt_html.js" +import {simpleObjectToDigraph} from "./js/digraphs/digraph_simple_builder.js" +import {abSegmentsToDigraph} from "./js/sided_ab_graphs/digraph_ab_builder.js" +import {DigraphABShortestPathFinder} from "./js/sided_ab_graphs/digraph_ab_shortest_path_finder.js" +import { + findShortestPathForAllPairsAsync, + simpleArrayToPointPairs +} from "./js/sided_ab_graphs/digraph_ab_pairs_analyser.js" + +export {makeTestGridGraph, makeTestGridAbGraph, makeTestGridAbPointPairs} from "./js/test_digraph_builders.js" + +export function simpleResolve(graphJson, startVertex, endVertex, errorElementId) { + document.getElementById(errorElementId).innerHTML = ""; + let tree = null; + let timing = ""; + try { + const t0 = performance.now(); + // tree = new ShortestPathTree(graphJson); // old-style + const digraph = simpleObjectToDigraph(graphJson); + const t1 = performance.now(); + tree = new ShortestPathTree(digraph); + tree.build(startVertex); + const t2 = performance.now(); + timing = "Timing: " + (t2 - t0) + " ms = " + + (t1 - t0) + " + " + (t2 - t1) + " for " + tree.numberOfNodes + " nodes, " + + ((t2 - t1) * 1e3 / tree.numberOfNodes) + " mcs/vertex
 "; + } catch (e) { + document.getElementById(errorElementId).innerHTML = e; + throw e; + } + const result = timing + "

" + singleHTML(tree, endVertex) + + "

" + + allPathsToHTML(tree, 250) + + "
" + + "Processed directed graph:
" + + tree.digraph.toJsonString(50).replace(/\},/g, "},
") + + "
"; + return result; +} + +export function sidedAdResolve(segmentsJson, startPoint, endPoint, errorElementId) { + document.getElementById(errorElementId).innerHTML = ""; + let finder = null; + let timing = ""; + try { + const t0 = performance.now(); + // tree = new ShortestPathTree(graphJson); // old-style + const digraph = abSegmentsToDigraph(segmentsJson); + const t1 = performance.now(); + finder = new DigraphABShortestPathFinder(digraph); + finder.build(startPoint); + const t2 = performance.now(); + timing = "Timing: " + (t2 - t0) + " ms = " + + (t1 - t0) + " + " + (t2 - t1) + ", " + + ((t2 - t1) * 1e3 / digraph.getNumberOfNodes()) + " mcs/vertex
 "; + } catch (e) { + document.getElementById(errorElementId).innerHTML = e; + throw e; + } + const result = timing + "

" + singleHTML(finder, endPoint) + + "
" + + forBothHTML(finder, endPoint) + + "

" + + allPathsToHTML(finder.treeA, 150) + + "
" + + allPathsToHTML(finder.treeB, 150) + + "
" + + "Processed directed graph:
" + + finder.treeA.digraph.toJsonString(50).replace(/\},/g, "},
") + + "
"; + return result; +} + +export async function sidedAdPairsResolve(segmentsJson, pairsJson, brief, errorElementId, timingElementId, progressBarId) { + //document.getElementById(errorElementId).innerHTML = ""; + //document.getElementById(timingElementId).innerHTML = "Executing..."; + let pointPairs = null; + try { + const t0 = performance.now(); + const digraph = abSegmentsToDigraph(segmentsJson); + pointPairs = simpleArrayToPointPairs(pairsJson); + const t1 = performance.now(); + await findShortestPathForAllPairsAsync(digraph, pointPairs, progressBarId == null ? null : part => { + //document.getElementById(progressBarId).value = Math.round(part * 100) + }); + // - note: without "await" we will have now non-processed pointPairs! + const t2 = performance.now(); + const timing = "Executing: " + (t2 - t0) + " ms = " + + (t1 - t0) + " + " + (t2 - t1) + " for " + + pointPairs.length + " pairs from " + digraph.getNumberOfNodes() + " nodes, " + + ((t2 - t1) / pointPairs.length) + " ms/pair
 "; + //document.getElementById(timingElementId).innerHTML = timing; + } catch (e) { + document.getElementById(errorElementId).innerHTML = e; + throw e; + } + if (brief) { + pointPairs = pointPairs.map(pair => pair.briefClone()) + } + return JSON.stringify(pointPairs, null, 2); +} + + +function singleHTML(treeOrFinder, endVertex) { + try { + return singlePathToHTML(treeOrFinder.getShortestPathInformation(endVertex, true)); + } catch (e) { + return String(e); + } +} + +function forBothHTML(finder, endPoint) { + try { + const forBoth = finder.getShortestPathInformationForBothSides(endPoint); + return forBoth.length + " checked paths:
" + + (forBoth.length == 0 ? "" : pathInformationToHTML(forBoth).replace(/\},/g, "},
")); + } catch (e) { + return String(e); + } +} + +export function parseJsonAndPrintError(jsonText, errorElementId) { + try { + return JSON.parse(jsonText); + } catch (e) { + document.getElementById(errorElementId).innerHTML = "JSON problem: " + e; + throw e; + } +} diff --git a/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/digraphs/digraph.js b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/digraphs/digraph.js new file mode 100644 index 00000000..10e6a74b --- /dev/null +++ b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/digraphs/digraph.js @@ -0,0 +1,86 @@ +export class GraphNode { + constructor(id) { + if (id == null) { + throw "Null id"; + } + this.id = id; + this.weights = []; + this.neighbourIds = []; + this.edgeIds = []; + // Note: edgeIds is an optional additional information about edges, not important for processing + this.numberOfEdges = 0; + } + + addEdge(weight, otherNodeId, edgeId) { + this.weights.push(weight); + this.neighbourIds.push(otherNodeId); + this.edgeIds.push(edgeId ?? this.id + "->" + otherNodeId); + this.numberOfEdges++; + } + + getWeight(k) { + return this.weights[k]; + } + + getNeighbourId(k) { + return this.neighbourIds[k]; + } + + getEdgeId(k) { + return this.edgeIds[k]; + } +} + +export class Digraph { + constructor() { + this.nodeMap = new Map(); + } + + addNode(node) { + const id = node.id; + if (id == null) { + throw "Null id in " + node + ": probably it is not a graph node"; + } + this.nodeMap.set(id, node); + } + + addNodes(nodes) { + for (const node of nodes) { + this.addNode(node); + } + } + + getNode(id) { + return this.nodeMap.get(id); + } + + getAllNodeIds() { + return this.nodeMap.keys(); + } + + getNumberOfNodes() { + return this.nodeMap.size; + } + + toJson(limitOfNodes) { + const json = {}; + let count = 0; + for (const [id, node] of this.nodeMap) { + const nodeJson = {}; + for (let k = 0; k < node.numberOfEdges; k++) { + nodeJson[node.getNeighbourId(k)] = node.getWeight(k) + } + json[id] = nodeJson; + ++count; + if (limitOfNodes && count > limitOfNodes) { + json["other_nodes"] = "cannot be shown (more than " + limitOfNodes + " nodes)"; + break; + } + } + return json; + } + + toJsonString(limitOfNodes, space) { + return JSON.stringify(this.toJson(limitOfNodes), null, space); + } +} diff --git a/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/digraphs/digraph_simple_builder.js b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/digraphs/digraph_simple_builder.js new file mode 100644 index 00000000..e8ced9fa --- /dev/null +++ b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/digraphs/digraph_simple_builder.js @@ -0,0 +1,22 @@ +import {Digraph, GraphNode} from "./digraph.js" + +// For example: +// id= "A" +// object= { "B": 5.3, "C": 2.1 }, +// object is a map of other nodes with their weights +export function simpleObjectToGraphNode(id, object) { + const result = new GraphNode(id); + for (const propertyName of Object.keys(object)) { + result.addEdge(object[propertyName], propertyName); + } + return result; +} + +export function simpleObjectToDigraph(object) { + const result = new Digraph(); + for (const id of Object.keys(object)) { + const node = simpleObjectToGraphNode(id, object[id]); + result.addNode(node); + } + return result; +} \ No newline at end of file diff --git a/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/digraphs/spt.js b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/digraphs/spt.js new file mode 100644 index 00000000..55242c57 --- /dev/null +++ b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/digraphs/spt.js @@ -0,0 +1,229 @@ +import {MinHeap} from "../heap/min_heap.js" + +class NodeAndDistance { + // counter allows to provide stable behaviour order, identical to SimpleDistanceHeap + constructor(nodeId, distance, globalCounter) { + this.nodeId = nodeId; + this.distance = distance; + this.counter = globalCounter ?? 0; + } +} + +let _USE_MIN_HEAP = true; + +// For debugging needs; usually should be true +export function _setUseMinHeap(value) { + _USE_MIN_HEAP = value; +} + +function nodeAndDistanceLess(a, b) { + const result = a.distance < b.distance || (a.distance == b.distance && a.counter < b.counter); +// console.log(JSON.stringify(a) + " and " + JSON.stringify(b) + ": " + result); + return result; +} + +class DistanceHeap { + constructor() { + this.heap = new MinHeap(nodeAndDistanceLess); + } + + isEmpty() { + return this.heap.isEmpty(); + } + + removeShortestDistanceNode() { + return this.heap.remove(); + } + + addNode(nodeAndDistance) { + this.heap.insert(nodeAndDistance); + } + + checkIntegrity() { + this.heap.checkIntegrity(); + } + + toString() { + return JSON.stringify(this.heap.heap); + } +} + +// Preserving this class for comparison/debugging +class SimpleDistanceHeap { + constructor() { + this.heap = new Map(); + } + + isEmpty() { + return this.heap.size == 0; + } + + removeShortestDistanceNode() { + let shortestId = null; + let shortestDistance = Number.POSITIVE_INFINITY; + for (const [id, distance] of this.heap) { + // actually this.heap.keys is a set of "interesting" vertices + if (shortestId === null || distance < shortestDistance) { + shortestId = id; + shortestDistance = distance; + } + } + if (shortestId == null) { + return null; + } + this.heap.delete(shortestId); + return new NodeAndDistance(shortestId, shortestDistance); + } + + addNode(nodeAndDistance) { + if (this.heap.has(nodeAndDistance.nodeId)) { + throw "Assertion! " + nodeAndDistance.nodeId + " already exists!"; + } + this.heap.set(nodeAndDistance.nodeId, nodeAndDistance.distance); + } + + checkIntegrity() { + } + + toString() { + const result = []; + for (const [id, distance] of this.heap) { + result.push(new NodeAndDistance(id, distance)); + } + return JSON.stringify(result); + } +} + +// ShortestPathTree is created on the base of an instance of the class Digraph; see digraph.js +export class ShortestPathTree { + constructor(digraph) { + if (!digraph) + throw "No digraph passed: " + digraph; + if (!digraph.getNode) { + throw "No getNode method in digraph " + digraph + ": probably it is not a Digraph instance"; + } + this.digraph = digraph; + this.startNode = null; + this.parentNodeIds = new Map(); + this.parentEdgeIds = new Map(); + // Note: parentEdgeIds is a map nodeId => parentEdgeID, not edgeId => parentEdgeId! + // We have no any guarantee that edge IDs are different, they are additional information + // and do not affect the algorithm. + this.distances = new Map(); + this.numberOfNodes = digraph.getNumberOfNodes(); + } + + build(startNodeId) { + const startNode = this.digraph.getNode(startNodeId); + if (!startNode) { + throw "No start node '" + startNodeId + "'"; + } + this.startNode = startNode; + this.distances = new Map(); + // - unlike distancesHeap, this is both a temporary memory AND the result of algorithm; + // in other language like C/Java in would be an array, not map + this.parentNodeIds = new Map(); + this.parentEdgeIds = new Map(); + const distancesHeap = _USE_MIN_HEAP ? new DistanceHeap() : new SimpleDistanceHeap(); + const visited = new Set(); + let counter = 0; + distancesHeap.addNode(new NodeAndDistance(startNodeId, 0, counter++)); + + while (!distancesHeap.isEmpty()) { + const nodeAndDistance = distancesHeap.removeShortestDistanceNode(); +// console.log("!!! --> " + nodeAndDistance.nodeId); +// distancesHeap.checkIntegrity(); // - uncomment this to test/debug MinHeap + const nodeId = nodeAndDistance.nodeId; + const distance = nodeAndDistance.distance; + const nodeWithChildren = this.digraph.getNode(nodeId); + for (let k = 0; k < nodeWithChildren.numberOfEdges; k++) { + const childId = nodeWithChildren.getNeighbourId(k); + if (visited.has(childId)) { + continue; + } + const edgeId = nodeWithChildren.getEdgeId(k); + const childWeight = nodeWithChildren.getWeight(k); + const childDistance = this.distances.get(childId); + const newDistance = distance + childWeight; + if (childDistance == null || childDistance > newDistance) { +// console.log("!!! " + childId + " := " + newDistance + ", from " + nodeId + ", " + counter); + this.distances.set(childId, newDistance); + distancesHeap.addNode(new NodeAndDistance(childId, newDistance, counter++)); + this.parentNodeIds.set(childId, nodeId); + this.parentEdgeIds.set(childId, edgeId); + } + } + this.distances.set(nodeId, distance); + visited.add(nodeId); + } + } + + getDistance(endNodeId) { + return this.distances.get(endNodeId); + } + + getShortestPath(endNodeId, resultSegmentsArray, resultNodesArray) { + const endNode = this.digraph.getNode(endNodeId); + resultSegmentsArray.length = 0; + resultNodesArray.length = 0; + if (!endNode) { + return false; + // - it is more simple in usage than throwing exception + } + let parentNodeId = this.parentNodeIds.get(endNodeId); + let parentEdgeId = this.parentEdgeIds.get(endNodeId); + if (parentNodeId == null) { + return false; + } + resultNodesArray.push(endNodeId); + while (parentNodeId != null) { + resultSegmentsArray.push(parentEdgeId); + resultNodesArray.push(parentNodeId); + parentEdgeId = this.parentEdgeIds.get(parentNodeId); + // - warning: must be BEFORE the next operator (changing parentNodeId) + parentNodeId = this.parentNodeIds.get(parentNodeId); + } + resultSegmentsArray.reverse(); + resultNodesArray.reverse(); + return true; + } + + getShortestPathInformation(endNodeId) { + const pathSegments = []; + const pathNodes = []; + const feasible = this.getShortestPath(endNodeId, pathSegments, pathNodes); + return feasible ? + new PathInformation(this.startNode.id, endNodeId, pathSegments, pathNodes, this.getDistance(endNodeId)) : + new PathInformation(this.startNode.id, endNodeId); + } + + getAllShortestPathMap() { + const results = new Map(); + for (const endNodeId of this.digraph.getAllNodeIds()) { + const pathInformation = this.getShortestPathInformation(endNodeId); + if (pathInformation.feasible) { + results.set(endNodeId, pathInformation); + } + } + return results; + } +} + +export class PathInformation { + constructor(startId, endId, pathSegments, pathNodes, distance) { + this.distance = distance; + // - we assign it first to make this attribute the first in JSON + this.startId = startId; + this.endId = endId; + if (pathSegments != null) { + this.segments = pathSegments; + this.nodes = pathNodes; + } + this.feasible = pathSegments != null; + } + + briefClone() { + return new PathInformation(this.startId, this.endId, null, null, this.distance); + } +} + diff --git a/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/digraphs/spt_html.js b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/digraphs/spt_html.js new file mode 100644 index 00000000..2782d3eb --- /dev/null +++ b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/digraphs/spt_html.js @@ -0,0 +1,37 @@ +export function singlePathToHTML(pathInformation) { + return pathInformation.feasible ? + "Shortest path '" + pathInformation.startId + "' ➔ '" + pathInformation.endId + "': " + + pathInformationToHTML(pathInformation) : + "No path '" + pathInformation.startId + "' ➔ '" + pathInformation.endId + "'"; +} + +export function allPathsToHTML(tree, limitOfNodes) { + try { + const pathMap = tree.getAllShortestPathMap(); + let report = "All " + pathMap.size + " shortest paths from '" + tree.startNode.id + "':
\n"; + let count = 0; + for (const [key, path] of pathMap) { + report += "    [#" + (count + 1) + "] ...➔ '" + + key + "': " + pathInformationToHTML(path) + "
\n"; + ++count; + if (limitOfNodes && count > limitOfNodes) { + report += "...
\n" + break; + } + } + return report; + } catch (e) { + return String(e); + } +} + +export function pathInformationToHTML(path) { + let s = JSON.stringify(path); + s = toHtmlEntities(s); + s = s.replace(/->/g, "→"); + return s; +} + +function toHtmlEntities(s) { + return s.replace(/&/g, "&").replace(/"/g, """).replace(//g, ">"); +} \ No newline at end of file diff --git a/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/digraphs/spt_old.js b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/digraphs/spt_old.js new file mode 100644 index 00000000..fb3c2f5c --- /dev/null +++ b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/digraphs/spt_old.js @@ -0,0 +1,118 @@ +// Created on the base of the project: +// https://github.com/noamsauerutley/shortest-path + +// MIT License + +// Copyright 2020 © Noam Sauer-Utley. https://www.noamsauerutley.com/ + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + + +class ShortestPathTree { + constructor(graph) { + if (!graph) + throw "No graph passed: " + graph; + this.graph = graph; + this.startNode = null; + this.parents = {}; + this.distances = {}; + this.numberOfNodes = Object.entries(graph).length; + } + + build(startNode) { + if (!this.graph[startNode]) + throw "No start node '" + startNode + "'"; + this.startNode = startNode; + this.distances = {}; + this.distances = Object.assign(this.distances, this.graph[startNode]); + + this.parents = {}; + // track paths + for (const child in this.graph[startNode]) { + this.parents[child] = startNode; + } + + // track nodes that have already been visited + const visited = []; + + // find the nearest node + let node = this.shortestDistanceNode(visited); + + // for that node + while (node) { + // find its distance from the start node & its child nodes + const distance = this.distances[node]; + const children = this.graph[node]; + // for each of those child nodes + for (const child in children) { + // make sure each child node is not the start node + if (String(child) === String(startNode)) { + continue; + } + // save the distance from the start node to the child node + const newdistance = distance + children[child]; + // if there's no recorded distance from the start node to the child node in the distances object + // or if the recorded distance is shorter than the previously stored distance from the start node to the child node + // save the distance to the object + // record the path + if (!this.distances[child] || this.distances[child] > newdistance) { + this.distances[child] = newdistance; + this.parents[child] = node; + } + } + // move the node to the visited set + visited.push(node); + // move to the nearest neighbor node + node = this.shortestDistanceNode(visited); + } + } + + getShortestPath(endNode) { + if (!this.graph[endNode]) + throw "No end node '" + endNode + "'"; + let parent = this.parents[endNode]; + if (parent == null) { + return null; + } + const shortestPath = [endNode]; + while (parent) { + shortestPath.push(parent); + parent = this.parents[parent]; + } + shortestPath.reverse(); + return shortestPath; + } + + // private + shortestDistanceNode(visited) { + let shortest = null; + + for (const node in this.distances) { + const currentIsShortest = + shortest === null || this.distances[node] < this.distances[shortest]; + if (currentIsShortest && !visited.includes(node)) { + shortest = node; + } + } + return shortest; + } +} + + + diff --git a/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/heap/min_heap.js b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/heap/min_heap.js new file mode 100644 index 00000000..74a683fd --- /dev/null +++ b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/heap/min_heap.js @@ -0,0 +1,125 @@ +export class MinHeap { + + constructor(nodeLessFunction) { + // Initialing the array heap and adding a dummy element at index 0 + this.heap = [null]; + this.nodeLessFunction = nodeLessFunction ?? function (a, b) { + return a < b + }; + } + + size() { + return this.heap.length - 1; + } + + isEmpty() { + return this.size() <= 0; + } + + getMin() { + return this.heap[1]; + } + + checkIntegrity() { + for (let k = 2; k < this.heap.length; k++) { + // Start from k = 2: element #1 has no parent + if (this.nodeLessFunction(this.heap[k], this.heap[k >> 1])) { + throw "Heap damaged: element #" + k + " < #" + (k >> 1); + } + } + } + + insert(node) { + this.heap.push(node); + + if (this.heap.length > 1) { + let current = this.heap.length - 1; + + let parent = current >> 1; + while (current > 1 && this.nodeLessFunction(this.heap[current], this.heap[parent])) { + this.swap(parent, current); + current = parent; + parent = current >> 1; + } + } + } + + remove() { + const heapSize = this.size(); + if (heapSize < 0) { + throw "Assertion: negative heap size " + heapSize; + } + if (this.isEmpty()) { + return null; + } + // Smallest element is at the index 1 in the heap array + const smallest = this.heap[1]; +// console.log("smallest: " + smallest); + + // When there are more than two elements in the array, we put the right most element at the first position + // and start comparing nodes with the child nodes + if (this.heap.length > 2) { + this.heap[1] = this.heap[this.heap.length - 1]; + // - 1st element should be removing; replace it with the last + this.heap.splice(heapSize); + // - remove the last element (duplicate) + + if (this.heap.length == 3) { + if (this.nodeLessFunction(this.heap[2], this.heap[1])) { + this.swap(1, 2); + } + return smallest; + } + + let current = 1; + let leftChildIndex = current * 2; + let rightChildIndex = leftChildIndex + 1; + + // Note: in original version there was a bug: it checked + // this.heap[...ChildIndex] + // (will be "false" for number element 0!) instead of correct + // ...ChildIndex] <= heapSize + // Moreover, it is possible that only left index is inside heapSide! + // Note: < , not <= ! heapSize is the size BEFORE removing the last element + for (; ;) { + let newCurrent = -1; + if (this.existsAndLess(rightChildIndex, current)) { + // So, also leftChildIndex < this.heap.length! + // No sense to compare leftChildIndex and current: + // in any case, we will swap with smallest from leftChildIndex and rightChildIndex + newCurrent = this.nodeLessFunction(this.heap[leftChildIndex], this.heap[rightChildIndex]) ? + leftChildIndex : + rightChildIndex; + } else { + if (this.existsAndLess(leftChildIndex, current)) { + newCurrent = leftChildIndex; + } else { + // Heap is restored + break; + } + } + this.swap(current, newCurrent); + current = newCurrent; + leftChildIndex = current * 2; + rightChildIndex = leftChildIndex + 1; + } + } else { + // If there are only two elements in the array (in other words, only 1 element in the heap), + // we directly splice out the first element + this.heap.splice(1, 1); + } + return smallest; + } + + // private + existsAndLess(checked, other) { + return checked < this.heap.length && this.nodeLessFunction(this.heap[checked], this.heap[other]); + } + + // private + swap(i, j) { + // Swapping the two nodes by using the ES6 destructuring syntax: + // works little faster than usual exchange via "temp" variable + [this.heap[i], this.heap[j]] = [this.heap[j], this.heap[i]] + } +} \ No newline at end of file diff --git a/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/heap/min_heap_original.js b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/heap/min_heap_original.js new file mode 100644 index 00000000..1ba1f083 --- /dev/null +++ b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/heap/min_heap_original.js @@ -0,0 +1,83 @@ +// See https://blog.bitsrc.io/implementing-heaps-in-javascript-c3fbf1cb2e65 +class MinHeap { + + constructor() { + /* Initialing the array heap and adding a dummy element at index 0 */ + this.heap = [null] + } + + getMin() { + /* Accessing the min element at index 1 in the heap array */ + return this.heap[1] + } + + insert(node) { + + /* Inserting the new node at the end of the heap array */ + this.heap.push(node) + + /* Finding the correct position for the new node */ + + if (this.heap.length > 1) { + let current = this.heap.length - 1 + + /* Traversing up the parent node until the current node (current) is greater than the parent (current/2)*/ + while (current > 1 && this.heap[Math.floor(current / 2)] > this.heap[current]) { + + /* Swapping the two nodes by using the ES6 destructuring syntax*/ + [this.heap[Math.floor(current / 2)], this.heap[current]] = [this.heap[current], this.heap[Math.floor(current / 2)]] + current = Math.floor(current / 2) + } + } + } + + remove() { + /* Smallest element is at the index 1 in the heap array */ + const smallest = this.heap[1] + + /* When there are more than two elements in the array, we put the right most element at the first position + and start comparing nodes with the child nodes + */ + if (this.heap.length > 2) { + this.heap[1] = this.heap[this.heap.length - 1] + this.heap.splice(this.heap.length - 1) + + if (this.heap.length === 3) { + if (this.heap[1] > this.heap[2]) { + [this.heap[1], this.heap[2]] = [this.heap[2], this.heap[1]] + } + return smallest + } + + let current = 1 + let leftChildIndex = current * 2 + let rightChildIndex = current * 2 + 1 + + while (this.heap[leftChildIndex] && + this.heap[rightChildIndex] && + (this.heap[current] > this.heap[leftChildIndex] || + this.heap[current] > this.heap[rightChildIndex])) { + if (this.heap[leftChildIndex] < this.heap[rightChildIndex]) { + [this.heap[current], this.heap[leftChildIndex]] = [this.heap[leftChildIndex], this.heap[current]] + current = leftChildIndex + } else { + [this.heap[current], this.heap[rightChildIndex]] = [this.heap[rightChildIndex], this.heap[current]] + current = rightChildIndex + } + + leftChildIndex = current * 2 + rightChildIndex = current * 2 + 1 + } + } + + /* If there are only two elements in the array, we directly splice out the first element */ + + else if (this.heap.length === 2) { + this.heap.splice(1, 1) + } else { + return null + } + + return smallest + } +} \ No newline at end of file diff --git a/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/heap/test_heap.html b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/heap/test_heap.html new file mode 100644 index 00000000..8a7c2dd6 --- /dev/null +++ b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/heap/test_heap.html @@ -0,0 +1,75 @@ + + + + Simple Heap Test + + + + + +
+

+ +

+
+ + diff --git a/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/sided_ab_graphs/digraph_ab_builder.js b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/sided_ab_graphs/digraph_ab_builder.js new file mode 100644 index 00000000..9bde3b0d --- /dev/null +++ b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/sided_ab_graphs/digraph_ab_builder.js @@ -0,0 +1,98 @@ +import {Digraph, GraphNode} from "../digraphs/digraph.js" +import {checkSegment} from "./digraph_ab_elements.js" + +// Example of segments argument (probably JSON array): +// [ +// { +// "id": "segment1", +// "weight": 8, +// "firstPoint": "p1", +// "firstSide": "A", +// "secondPoint": "p2", +// "secondSide": "B" +// }, +// { +// "id": "segment2", +// "weight": 3, +// "firstPoint": "p2", +// "firstSide": "A", +// "secondPoint": "p3", +// "secondSide": "B" +// } +// ... +// ] +// Every point side (A or B) of a point P is a vertex of undirected graph (P/A or P/B), +// BUT path cannot enter to side A and exit from the same side A. +// So, every segment, connecting side X of point P with side Y of point Q, +// is transformed to a pair of digraph edges: P/X -> Q/inv(Y), Q/Y -> P/inv(X), +// where inv(A)=B, inv(B)=A + +export function abSegmentsToDigraph(segments) { + const allPointIds = new Set(); + for (const segment of segments) { + checkSegment(segment); + addPointPair(allPointIds, segment); + } + const nodesA = toDigraphNodes(allPointIds, "A"); + const nodesB = toDigraphNodes(allPointIds, "B"); + for (const segment of segments) { + addFirstToSecondEdge(nodesA, nodesB, segment); + addSecondToFirstEdge(nodesA, nodesB, segment); + } + const result = new Digraph(); + result.addNodes(nodesA.values()); + result.addNodes(nodesB.values()); + return result; +} + +export function pointToNode(point, side) { + return point + "/" + side; +} + +export function nodeToPoint(node) { + if (node.endsWith("/A") || node.endsWith("/B")) { + return node.substring(0, node.length - 2); + } else { + throw "'" + node + "' is not a correct node name in AB-digraph"; + } +} + +// private +function addPointPair(pointSet, segment) { + pointSet.add(String(segment.firstPoint)); + pointSet.add(String(segment.secondPoint)); +} + +// private +function toDigraphNodes(pointSet, side) { + const result = new Map(); + for (const point of pointSet) { + result.set(point, new GraphNode(pointToNode(point, side))); + } + return result; +} + +// private +function addFirstToSecondEdge(nodesA, nodesB, segment) { + const firstNodes = segment.firstSide == "A" ? nodesB : nodesA; + // - inversion! + const secondNodes = segment.secondSide == "A" ? nodesA : nodesB; + const firstNode = firstNodes.get(segment.firstPoint); + const secondNode = secondNodes.get(segment.secondPoint); + firstNode.addEdge(segment.weight, secondNode.id, segment.id); +} + +// private +function addSecondToFirstEdge(nodesA, nodesB, segment) { + const firstNodes = segment.firstSide == "A" ? nodesA : nodesB; + const secondNodes = segment.secondSide == "A" ? nodesB : nodesA; + // - inversion! + const firstNode = firstNodes.get(segment.firstPoint); + const secondNode = secondNodes.get(segment.secondPoint); + secondNode.addEdge(segment.weight, firstNode.id, segment.id); +} + +// private +function otherSide(side) { + return side == "A" ? "B" : "A"; +} \ No newline at end of file diff --git a/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/sided_ab_graphs/digraph_ab_elements.js b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/sided_ab_graphs/digraph_ab_elements.js new file mode 100644 index 00000000..aeb4f425 --- /dev/null +++ b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/sided_ab_graphs/digraph_ab_elements.js @@ -0,0 +1,48 @@ +export class DigraphABSegment { + constructor(id, firstPoint, firstSide, secondPoint, secondSide, weight) { + this.id = id; + this.firstPoint = firstPoint; + this.firstSide = firstSide; + this.secondPoint = secondPoint; + this.secondSide = secondSide; + this.weight = weight; + checkSegment(this); + } +} + +export class DigraphABPointPair { + constructor(startPoint, endPoint, id, wireInfo = null) { + this.id = id; // - optional + this.startPoint = startPoint; + this.endPoint = endPoint; + this.route = null; + this.wireInfo = wireInfo; + } + + briefClone() { + const result = new DigraphABPointPair(this.startPoint, this.endPoint, this.id, this.wireInfo); + if (this.route != null) { + result.route = this.route.briefClone(); + } + return result; + } +} + +export function checkSegment(segment) { + if (!segment.id) + throw "No id attribute in segment " + JSON.stringify(segment); + if (!segment.weight) + throw "No weight attribute in segment " + JSON.stringify(segment); + if (!segment.firstPoint) + throw "No firstPoint attribute in segment " + JSON.stringify(segment); + if (!segment.firstSide) + throw "No firstSide attribute in segment " + JSON.stringify(segment); + if (segment.firstSide != "A" && segment.firstSide != "B") + throw "Invalid firstSide attribute '" + segment.firstSide + + "' (not 'A' or 'B') in segment " + JSON.stringify(segment); + if (!segment.secondPoint) throw "No secondPoint attribute in segment " + JSON.stringify(segment); + if (!segment.secondSide) throw "No secondSide attribute in segment " + JSON.stringify(segment); + if (segment.secondSide != "A" && segment.secondSide != "B") + throw "Invalid secondSide attribute '" + segment.secondSide + + "' (not 'A' or 'B') in segment " + JSON.stringify(segment); +} diff --git a/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/sided_ab_graphs/digraph_ab_pairs_analyser.js b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/sided_ab_graphs/digraph_ab_pairs_analyser.js new file mode 100644 index 00000000..7a575aa8 --- /dev/null +++ b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/sided_ab_graphs/digraph_ab_pairs_analyser.js @@ -0,0 +1,84 @@ +import {DigraphABPointPair} from "./digraph_ab_elements.js" +import {DigraphABShortestPathFinder} from "./digraph_ab_shortest_path_finder.js" + +export function simpleArrayToPointPairs(array) { + const result = []; + for (const pair of array) { + if (pair.startPoint == null) + throw "No startPoint in element " + JSON.stringify(pair); + if (pair.endPoint == null) + throw "No endPoint in element " + JSON.stringify(pair); + result.push(new DigraphABPointPair(pair.startPoint, pair.endPoint, pair.id, pair.wireInfo)); + } + return result; +} + +export function findShortestPathForAllPairs(digraph, pointPairsArray) { + sortPointPairArray(pointPairsArray); + const finder = new DigraphABShortestPathFinder(digraph); + for (const pair of pointPairsArray) { + finder.processPair(pair); + } + return pointPairsArray; +} + +// Example: +// function showProgressBar(part) { +// // part is a number from 0.0 to 1.0 +// document.getElementById("progressBar").value = Math.round(part * 100) +// } +// IMPORTANT: this function SHOULD be called with "await" operator +// (if you want to get correct pointPairsArray after its execution) +export async function findShortestPathForAllPairsAsync(digraph, pointPairsArray, showProgressBar) { + let lastTime = performance.now(); + sortPointPairArray(pointPairsArray); + const finder = new DigraphABShortestPathFinder(digraph); + for (let i = 0, n = pointPairsArray.length; i < n; i++) { + lastTime = await updateProgress(i, n, showProgressBar, lastTime); + finder.processPair(pointPairsArray[i]); + } + return pointPairsArray; +} + +export function sortPointPairArray(pairArray) { + pairArray.sort(function (a, b) { + if (a.startPoint > b.startPoint) { + return 1; + } + if (a.startPoint < b.startPoint) { + return -1; + } + return 0; + }); +} + +// private +async function updateProgress(i, n, showProgressBar, lastTime) { + if (n <= 1) { + return lastTime; + } + if (i != n - 1) { + // n-1 is shown always + const t = performance.now(); +// console.log(lastTime +"; " + (t - lastTime)); + if (lastTime != null && t - lastTime < 200) { + // less than 200 ms from last showing + return lastTime; + } + lastTime = t; + } + if (showProgressBar != null) { + showProgressBar(i / (n - 1)); + } + if (i != n - 1) { + await sleep(1); + } + return lastTime; +} + +// private +async function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + + diff --git a/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/sided_ab_graphs/digraph_ab_shortest_path_finder.js b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/sided_ab_graphs/digraph_ab_shortest_path_finder.js new file mode 100644 index 00000000..f48b5c1e --- /dev/null +++ b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/sided_ab_graphs/digraph_ab_shortest_path_finder.js @@ -0,0 +1,76 @@ +import {nodeToPoint, pointToNode} from "./digraph_ab_builder.js" +import {PathInformation, ShortestPathTree} from "../digraphs/spt.js" + +export class DigraphABShortestPathFinder { + constructor(digraph) { + this.treeA = new ShortestPathTree(digraph); + this.treeB = new ShortestPathTree(digraph); + this.startPointId = null; + } + + processPair(pointPair) { + this.rebuild(pointPair.startPoint); + pointPair.route = this.getShortestPathInformation(pointPair.endPoint); + } + + rebuild(startPointId) { + if (startPointId != this.startPointId) { + this.build(startPointId); + } + } + + build(startPointId) { + this.treeA.build(pointToNode(startPointId, "A")); + this.treeB.build(pointToNode(startPointId, "B")); + this.startPointId = startPointId; + } + + // Rarely used + getShortestPathInformationForBothSides(endPointId) { + const result = []; + addIfExists(result, this.treeA.getShortestPathInformation(pointToNode(endPointId, "A"))); + addIfExists(result, this.treeA.getShortestPathInformation(pointToNode(endPointId, "B"))); + addIfExists(result, this.treeB.getShortestPathInformation(pointToNode(endPointId, "A"))); + addIfExists(result, this.treeB.getShortestPathInformation(pointToNode(endPointId, "B"))); + return result; + } + + getShortestPathInformation(endPointId, includePointsWhenNotFound) { + const forBothSides = this.getShortestPathInformationForBothSides(endPointId); + if (forBothSides.length == 0) { + return includePointsWhenNotFound ? + new PathInformation(this.startPointId, endPointId) : + new PathInformation(); + } + let result = forBothSides[0]; + for (let k = 1; k < forBothSides.length; k++) { + if (forBothSides[k].distance < result.distance) { + result = forBothSides[k]; + } + } + if (containsDuplicates(result.nodes)) { + result.containsDuplicates = true; + } + return result; + } +} + +export function containsDuplicates(nodesArray) { + const points = new Set(); + for (const node of nodesArray) { + const p = nodeToPoint(node); + if (points.has(p)) { + return true; + } + points.add(p); + } + return false; +} + +// private +function addIfExists(array, pathInformation) { + if (pathInformation.feasible) { + array.push(pathInformation); + } +} + diff --git a/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/test_digraph_builders.js b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/test_digraph_builders.js new file mode 100644 index 00000000..eabe6af7 --- /dev/null +++ b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/js/test_digraph_builders.js @@ -0,0 +1,78 @@ +import {DigraphABPointPair, DigraphABSegment} from "./sided_ab_graphs/digraph_ab_elements.js" + +export function makeTestGridGraph(width, height, straightWeight, diagonalWeight, includeDiagonals) { + straightWeight = parseFloat(straightWeight); + diagonalWeight = parseFloat(diagonalWeight); + const g = {}; + for (let i = 0; i < height; i++) { + for (let j = 0; j < width; j++) { + const node = {}; + if (i < height - 1) { + node[vertex(i + 1, j)] = straightWeight; + } + if (j < width - 1) { + node[vertex(i, j + 1)] = straightWeight; + } + if (i < height - 1 && j < width - 1 && includeDiagonals) { + node[vertex(i + 1, j + 1)] = diagonalWeight; + } + g[vertex(i, j)] = node; + } + } + return g; +} + +export function makeTestGridAbGraph(width, height, straightWeight, diagonalWeight, includeDiagonals) { + straightWeight = parseFloat(straightWeight); + diagonalWeight = parseFloat(diagonalWeight); + const g = []; + for (let i = 0; i < height; i++) { + for (let j = 0; j < width; j++) { + const v = vertex(i, j); + if (i < height - 1) { + g.push(new DigraphABSegment("Down-" + i + "-" + j, v, "B", vertex(i + 1, j), "A", straightWeight)); + } + if (j < width - 1) { + g.push(new DigraphABSegment("Right-" + i + "-" + j, v, "B", vertex(i, j + 1), "A", straightWeight)); + } + if (i < height - 1 && j < width - 1 && includeDiagonals) { + g.push(new DigraphABSegment("Diag-" + i + "-" + j, v, "B", vertex(i + 1, j + 1), "A", diagonalWeight)); + } + } + } + return g; +} + +export function makeTestGridAbPointPairs(width, height, numberOfPairs, seed) { + const pairs = []; + const rnd = mulberryRandom32(seed ?? 157); + pairs.push(new DigraphABPointPair(vertex(0, 0), vertex(height - 1, width - 1))); + for (let k = 1; k < numberOfPairs; k++) { + const i1 = randomInt(0, height, rnd); + const i2 = randomInt(0, height, rnd); + const j1 = randomInt(0, width, rnd); + const j2 = randomInt(0, width, rnd); + pairs.push(new DigraphABPointPair(vertex(i1, j1), vertex(i2, j2))); + } + return pairs; +} + +function vertex(y, x) { + return "V" + x + "|" + y; +} + +function randomInt(min, max, rnd) { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(rnd() * (max - min)) + min; +} + +// Allows to specify start seed, unlike Math.random +function mulberryRandom32(seed) { + return function () { + let t = seed += 0x6D2B79F5; + t = Math.imul(t ^ t >>> 15, t | 1); + t ^= t + Math.imul(t ^ t >>> 7, t | 61); + return ((t ^ t >>> 14) >>> 0) / 4294967296; + } +} diff --git a/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/sided_ab_demo.html b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/sided_ab_demo.html new file mode 100644 index 00000000..064037aa --- /dev/null +++ b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/sided_ab_demo.html @@ -0,0 +1,77 @@ + + + + Sided-A/B Path Demo + + + + + +
+

A/B sided graphs simple test

+

+

Enter JSON array of segments of some A/B-graph:

+ +

+ Start point:
+ End point: +

+

+ +

+

Click button about to see results here +

+ + +
+ + diff --git a/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/sided_ab_pairs.html b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/sided_ab_pairs.html new file mode 100644 index 00000000..40727c4f --- /dev/null +++ b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/sided_ab_pairs.html @@ -0,0 +1,53 @@ + + + + Sided-A/B Path Demo + + + + + +
+

A/B sided graphs main demo

+

+

Enter JSON array of segments of some A/B-graph:

+ + +

+ +

+

Click button about to see results below

+ + +
+ + diff --git a/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/sided_ab_pairs_grid.html b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/sided_ab_pairs_grid.html new file mode 100644 index 00000000..49c55b9b --- /dev/null +++ b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/sided_ab_pairs_grid.html @@ -0,0 +1,81 @@ + + + + Sided-A/B Path Demo + + + + + +
+

Grid A/B sided graphs speed test

+

+

+ Grid width:
+ Grid height:
+ Straigh weight:
+ Diagonal weight:
+ Include diagonals:
+ Number of random pairs:
+ Random seed:
+ Brief results:
+ Use MinHeap structure (should be true):
+

+

+

+
Click button to see results below
+
+ +

+ +

+ +
Results:
+ + +

 

+
Actually processed segments:
+ + +
Actually processed pairs:
+ +
+ + diff --git a/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/simple_demo.html b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/simple_demo.html new file mode 100644 index 00000000..118e23c1 --- /dev/null +++ b/modules/workbenches/routingElectrical/features/autoRoute/pathFinderLogic/simple_demo.html @@ -0,0 +1,118 @@ + + + + Simple Shortest-Path Demo + + + + + + + +
+

Simple digraph SPT test

+

+

Enter graph:

+ + + +

+ Start vertex:
+ End vertex: +

+

+ +

+

Click button about to see results here
+ For default graph example above, expected results are the following:

+ Shortest path '0' ➔ '7': {"distance":60,"startId":"0","endId":"7","segments":["0→2","2→7"],"nodes":["0","2","7"],"feasible":true}

All 7 shortest paths from '0':
+    [#1] ...➔ '1': {"distance":105,"startId":"0","endId":"1","segments":["0→4","4→5","5→1"],"nodes":["0","4","5","1"],"feasible":true}
+    [#2] ...➔ '2': {"distance":26,"startId":"0","endId":"2","segments":["0→2"],"nodes":["0","2"],"feasible":true}
+    [#3] ...➔ '3': {"distance":99,"startId":"0","endId":"3","segments":["0→2","2→7","7→3"],"nodes":["0","2","7","3"],"feasible":true}
+    [#4] ...➔ '4': {"distance":38,"startId":"0","endId":"4","segments":["0→4"],"nodes":["0","4"],"feasible":true}
+    [#5] ...➔ '5': {"distance":73,"startId":"0","endId":"5","segments":["0→4","4→5"],"nodes":["0","4","5"],"feasible":true}
+    [#6] ...➔ '6': {"distance":151,"startId":"0","endId":"6","segments":["0→2","2→7","7→3","3→6"],"nodes":["0","2","7","3","6"],"feasible":true}
+    [#7] ...➔ '7': {"distance":60,"startId":"0","endId":"7","segments":["0→2","2→7"],"nodes":["0","2","7"],"feasible":true}
+
+

+ +

Grid graphs

+

+

+ Start vertex:
+ End vertex: (common syntax: 'Vxx|yy')
+ Grid width:
+ Grid height:
+ Straigh weight:
+ Diagonal weight:
+ Include diagonals: +

+

+ +

+

Click button about to see results here

+
+ + diff --git a/modules/workbenches/routingElectrical/index.ts b/modules/workbenches/routingElectrical/index.ts new file mode 100644 index 00000000..9c12dee5 --- /dev/null +++ b/modules/workbenches/routingElectrical/index.ts @@ -0,0 +1,27 @@ +import {WorkbenchConfig} from "cad/workbench/workbenchService"; + +//imports of feature history type commands +import {AutoRouteOperation} from "./features/autoRoute/autoRoute.operation"; +import {GiFoldedPaper} from "react-icons/gi"; + + +//imports of action type commands + + + +export const RoutingElectricalWorkspace: WorkbenchConfig = { + + workbenchId: 'RoutingElectrical', + features: [ + AutoRouteOperation, + + ], + actions: [], + ui: { + toolbar: [ + 'DATUM_CREATE', 'PLANE', 'EditFace', '-', + 'RE_AUTO_ROUTE', + ] + }, + icon: GiFoldedPaper +} \ No newline at end of file