Source code for tracklib.core.Network

"""This module contains the class to manage Network"""

# For type annotation
from __future__ import annotations   
from typing import Literal, Union, Dict, Tuple   

import random
#from typing import Union
import progressbar
import numpy as np
import matplotlib.pyplot as plt

import tracklib.algo.Simplification as Simplification
from tracklib.core.Bbox import Bbox
from tracklib.core.ObsCoords import ECEFCoords, ENUCoords, GeoCoords
from tracklib.core.Obs import Obs
from tracklib.core.Track import Track
from tracklib.core.Utils import priority_dict
from tracklib.core.TrackCollection import TrackCollection
import tracklib.plot.IPlotVisitor as ivisitor


[docs]class Node: """Node / vertice of a network"""
[docs] def __init__(self, id: int, coord: Union[ECEFCoords, ENUCoords, GeoCoords]): """:class:`Node` constructor :param id: unique id of Node :param coord: A coordinate object """ self.id = id self.coord = coord
[docs] def __str__(self) -> str: """Node to string""" return "Node object: " + str(self.id)
def __lt__(self, other: Node) -> bool: """Check if a node is bigger than the current node :param other: Node to test :return: Result of the test """ if isinstance(other, Node): return self.id < other.id return False def __eq__(self, other: Node) -> bool: """Check if a node is equal with the current node :param other: Node to test :return: Result of the test """ if isinstance(other, Node): return self.id == other.id return False def __hash__(self) -> int: """Return the hash of current node""" return hash(self.id)
[docs] def distanceTo(self, node: Node) -> float: """Distance to an other node :param node: Node to compute the distance :return: 3d distance """ return self.coord.distanceTo(node.coord)
[docs] def distance2DTo(self, node: Node) -> float: """2d distance to an other node :param node: Node to compute the distance :return: 2d distance """ return self.coord.distance2DTo(node.coord)
[docs] def plot(self, sym="r+"): """Plot the node""" plt.plot(self.coord.getX(), self.coord.getY(), sym)
[docs]class Edge: """Class to define an edge / arc""" DOUBLE_SENS = 0 SENS_DIRECT = 1 SENS_INVERSE = -1
[docs] def __init__(self, id: int, track: Track): """:class:`Edge` constructor :param id: unique id of Edge :param track: A track """ self.id = id self.geom = track self.source = None self.target = None self.orientation = 0 self.weight = 0
[docs] def plot(self, sym: str = "k-"): """Plot the edge :param sym: TODO """ self.geom.plot(sym)
[docs] def __str__(self) -> str: """Print the edge""" return "Edge #" + str(self.id)
[docs]class Network: """Define a network""" # Routing modes ROUTING_ALGO_DIJKSTRA = 0 ROUTING_ALGO_ASTAR = 1 # Analytical features AF_LINK = "#link" AF_WEIGHT = "#weight"
[docs] def __init__(self): """:class:`Network` constructor""" self.NODES = dict() self.EDGES = dict() self.__idx_nodes = [] self.__idx_edges = [] self.NEXT_EDGES = dict() self.PREV_EDGES = dict() self.NBGR_EDGES = dict() self.NEXT_NODES = dict() self.PREV_NODES = dict() self.NBGR_NODES = dict() self.DISTANCES = None self.routing_mode = Network.ROUTING_ALGO_DIJKSTRA self.astar_wgt = 1 self.spatial_index = None
[docs] def addNode(self, node: Node): """Add a :class:`Node` to the current :class:`Network` :param Node: Node to add """ if node.id not in self.NODES: self.NODES[node.id] = node self.__idx_nodes.append(node.id) self.NEXT_EDGES[node.id] = [] self.PREV_EDGES[node.id] = [] self.NBGR_EDGES[node.id] = [] self.NEXT_NODES[node.id] = [] self.PREV_NODES[node.id] = [] self.NBGR_NODES[node.id] = []
[docs] def addEdge(self, edge: Edge, source: Node, target: Node): """Add a :class:`Edge` to the current :class:`Network` :param edge: Edge to add :param source: Source Node :param target: Target Node """ self.addNode(source) self.addNode(target) if target.id not in self.NODES: self.NODES[target.id] = target self.__idx_nodes.append(target.id) edge.source = self.NODES[source.id] edge.target = self.NODES[target.id] self.EDGES[edge.id] = edge self.__idx_edges.append(edge.id) if edge.orientation >= 0: self.NEXT_EDGES[source.id].append(edge.id) self.PREV_EDGES[target.id].append(edge.id) self.NEXT_NODES[source.id].append(target.id) self.PREV_NODES[target.id].append(source.id) if edge.orientation <= 0: self.NEXT_EDGES[target.id].append(edge.id) self.PREV_EDGES[source.id].append(edge.id) self.NEXT_NODES[target.id].append(source.id) self.PREV_NODES[source.id].append(target.id) self.NBGR_EDGES[source.id].append(edge.id) self.NBGR_EDGES[target.id].append(edge.id) self.NBGR_NODES[source.id].append(target.id) self.NBGR_NODES[target.id].append(source.id) if not self.spatial_index is None: self.spatial_index.addFeature(edge.geom, self.getNumberOfEdges() - 1)
def __iter__(self): """Define an iterator on Network""" yield from [l[1] for l in list(self.EDGES.items())]
[docs] def size(self) -> int: """Return the number of edges :return: Number of edges """ return len(self.EDGES)
[docs] def setRoutingMethod(self, method: int): """Define the routing algorithm :param method: Algorithm of routing. Two values are possible : 1. ROUTING_ALGO_DIJKSTRA [default]. 2. ROUTING_ALGO_ASTAR. **NB:** A* is an efficient approximate version of Dijkstra. It returns exact solution under theoretical assumptions on the metrics used to set weights on graph edges. """ self.routing_mode = method
[docs] def setAStarWeight(self, weight: float): """Define the A* weight""" self.astar_wgt = weight
# ------------------------------------------------------------ # Topometric methods # ------------------------------------------------------------
[docs] def toGeoCoords(self, base: Union[ECEFCoords, ENUCoords]): """Convert network to :class:`core.Coords.GeoCoords` :param base: Base coordinate for conversion """ for id in self.__idx_nodes: self.NODES[id].coord = self.NODES[id].coord.toGeoCoords(base) for id in self.__idx_edges: self.EDGES[id].geom.toGeoCoords(base)
[docs] def toENUCoords(self, base: Union[GeoCoords, ECEFCoords]): """Convert network to :class:`core.Coords.ENUCoords` :param base: Base coordinate for conversion """ for id in self.__idx_nodes: self.NODES[id].coord = self.NODES[id].coord.toENUCoords(base) for id in self.__idx_edges: self.EDGES[id].geom.toENUCoords(base)
[docs] def getSRID(self) -> int: """Return the SRID of network :return: SRID of current network """ return self.EDGES[self.__idx_edges[0]].geom.getSRID()
[docs] def totalLength(self) -> int: """Count the edges of current network :return: Number of edges """ count = 0 for id in self.__idx_edges: count += self.EDGES[id].geom.length() return count
[docs] def simplify(self, tolerance, mode: int = 1): """Simplification of current network :param tolerance: TODO :param mode: Mode of simplification. The possibles values are: 1. DOUGLAS_PEUCKER 2. VISVALINGAM 3. MINIMIZE_LARGEST_DEVIATION 4. MINIMIZE_ELONGATION_RATIO 5. PRECLUDE_LARGE_DEVIATION 6. FREE 7. FREE_MAXIMIZE See :func:`algo.Simplification.simplify` documentation for more infos """ for id in self.__idx_edges: self.EDGES[id].geom = Simplification.simplify( self.EDGES[id].geom, tolerance, mode )
# ------------------------------------------------------------ # Spatial index creation, export and import functions # ------------------------------------------------------------
[docs] def createSpatialIndex ( self, resolution=None, margin: float = 0.05, verbose: bool = True ): """Create a spatial index :param resolution: TODO :param margin: TODO :param verbose: Verbose creation """ from tracklib.core.SpatialIndex import SpatialIndex self.spatial_index = SpatialIndex(self, resolution, margin, verbose)
[docs] def exportSpatialIndex(self, filename: str): """Export the spatial index to a file :param filename: File to export """ self.spatial_index.save(filename)
[docs] def importSpatialIndex(self, filename: str): """Import a spatial index from a file :parma filename: File to import """ from tracklib.core.SpatialIndex import SpatialIndex self.spatial_index = SpatialIndex.load(filename)
# ------------------------------------------------------------ # Topologic methods # ------------------------------------------------------------
[docs] def getNextNodes(self, node_id: int): """Give the XXX of a node :param node_id: id of a node """ return self.NEXT_NODES[node_id]
[docs] def getNextEdges(self, node_id: int): """Give the XXX of a node :param node_id: id of a node """ return self.NEXT_EDGES[node_id]
[docs] def getPrevNodes(self, node_id: int): """Give the XXX of a node :param node_id: id of a node """ return self.PREV_NODES[node_id]
[docs] def getPrevEdges(self, node_id: int): """Give the XXX of a node :param node_id: id of a node """ return self.PREV_EDGES[node_id]
[docs] def getAdjacentNodes(self, node_id: int): """Give the XXX of a node :param node_id: id of a node """ return self.NBGR_NODES[node_id]
[docs] def getIncidentEdges(self, node_id: int): """Give the XXX of a node :param node_id: id of a node """ return self.NBGR_EDGES[node_id]
[docs] def degree(self, node_id: int) -> int: """Compute the degree of a node :param node_id: id of a node :return: in degree """ return self.getAdjacentNodes(node_id).size()
[docs] def degreeIn(self, node_id: int) -> int: """Compute the in degree of a node :param node_id: id of a node :return: in degree """ return self.getPrevNodes(node_id).size()
[docs] def degreeOut(self, node_id: int) -> int: """Compute the out degree of a node :param node_id: id of a node :return: out degree """ return self.getNextNodes(node_id).size()
# ------------------------------------------------------------ # Acces to data # ------------------------------------------------------------ def __getitem__(self, n: int) -> Edge: """Return an edge item from its id :param n: Edge id :return: An edge """ return self.EDGES[self.__idx_edges[n]] def __setitem__(self, n: int, edge: Edge): """Set an Edge for a give id value :param id: The id :param edge: The edge to set """ self.EDGES[self.__idx_edges[id]] = edge
[docs] def getIndexNodes(self) -> list[int]: """Return the list of seted nodes :return: A list of nodes id """ return self.__idx_nodes
[docs] def getNumberOfNodes(self) -> int: """Number of nodes in the network :return: Number of nodes """ return len(self.NODES)
[docs] def getNumberOfEdges(self) -> int: """Number of edges in the network :return: Number of edges """ return len(self.EDGES)
[docs] def getNumberOfVertices(self) -> int: """Number of vertices in the network :return: Number of vertices """ count = 0 for id in self.__idx_edges: count += self.EDGES[id].geom.size() return count
[docs] def getNodeId(self, n: int) -> int: """The return the id of the n-est Node :param n: Position of the node :return: Id of the node """ return self.__idx_nodes[n]
[docs] def getEdgeId(self, n: int) -> int: """The return the id of the n-est Edge :param n: Position of the Edge :return: Id of the Edge """ return self.__idx_edges[n]
[docs] def getNodesId(self) -> list[int]: """Return a list of all nodes Id :return: A list of id """ return [l[0] for l in list(self.NODES.items())]
[docs] def getEdgesId(self) -> list[int]: """Return a list of all edges Id :return: A list of id """ return [l[0] for l in list(self.EDGES.items())]
[docs] def getRandomNode(self) -> Node: """Return a random node of network :return: A node randomly choosed """ return self.getNode( self.getNodesId()[random.randint(0, self.getNumberOfNodes() - 1)] )
[docs] def hasNode(self, id: int) -> bool: """Check if a node with a given id exist :param id: node id to test """ return id in self.NODES
[docs] def hasEdge(self, id: int) -> bool: """Check if a node with a given id exist :param id: edge id to test """ return id in self.EDGES
[docs] def getNode(self, id: int) -> Node: """Return the node with a give id :param id: node id to get """ return self.NODES[id]
[docs] def getEdge(self, id: int) -> Edge: """Return the edge with a give id :param id: edge id to get """ return self.EDGES[id]
[docs] def getAllEdgeGeoms(self) -> TrackCollection: """Return a TrackCollection of all edges :return: All edges of :class:`Network` """ tracks = TrackCollection() for id in self.__idx_edges: tracks.addTrack(self.EDGES[id].geom) return tracks
[docs] def bbox(self) -> Bbox: """Return the :class:`Bbox` of network""" return self.getAllEdgeGeoms().bbox()
# ------------------------------------------------------------ # Graphics # ------------------------------------------------------------
[docs] def plot(self, edges: str = "k-", nodes: str = "", direct: str = "k--", indirect: str = "k--", size: float = 0.5, append=plt, v:ivisitor.IPlotVisitor=None): if v == None: import tracklib.plot.MatplotlibVisitor as visitor v = visitor.MatplotlibVisitor() v.plotNetwork(self, edges, nodes, direct, indirect, size, append)
# ------------------------------------------------------------ # Routing methods # ------------------------------------------------------------ # ------------------------------------------------------------ # Routing algorithms to compute shortest_path(s). Routine is # stopped when either target node is reached or distance to # source node is greater than 'cut' value. The result of # run_dijstra may be used as shortest distance between pair of # node, or as shortest distances between 1 node and all other # nodes of the graph model. Source and target must be provided # as nodes or node ids. As a default strategy, all computations # are done with the popular and efficient Dijkstra's algorithm # If needed, settings enable to use A* heuristics optimization # ------------------------------------------------------------ # Available functions: # - run_routing_forward: executes forward pass of dijkstra # - run_routing_backward: backtracking recursive search for # the shortest path joining source and target nodes. # - sub_network: extracts sub-network from forward search # - shortest_distance: finds shortest distance between source # node and either a target node or a list of target nodes. # - all_shortest_distances: computes all shortest distances # between pairs of nodes in an efficient way. # - shortest_path: compute shortest path between 2 nodes. # - prepare: computes all distances between pairs of nodes # - save_prep: saves the result computed by 'prepare' # - load_prep: imports the result saved by 'save_prep' # - prepared_shortest_distance: reads shortest distance from # a precomputed dictionnary structure. May be called only # after prepare or load_prep functions. # ------------------------------------------------------------ # Most of the above search functions may be interrupted before # end by specifying a target node and/or a cut distance. # Output results may be compiled in an output dictionnary. # ------------------------------------------------------------ # Important: to compute the shortest distances between all # pairs of nodes, 'all_shortest_distances' function is more # efficient than successive calls of shortest_distance(n1, n2) # for all pairs of nodes n1 and n2, since the former sums up # to an n x O(n.log(n)) = O(n^2.log(n)) time complexity for # a typical planar road network, versus n^2 x O(n.log(n)) = # O(n^3.log(n)) for the latter. For a general network (not # necessarily planar), 'all_shortest_distances' function takes # O(n^3.log(n)) against O(n^4.log(n)) for 'shortest_distance'. # As a consequence, in every situation, all_shortest_distances # is n times faster than a (bad) strategy based on successive # calls of pairwise shortest_distance function. # ------------------------------------------------------------ def __correctInputNode(self, node): """TODO""" if isinstance(node, Node): return node.id return node
[docs] def sub_network(self, source: Union[int, Node, Union[GeoCoords, ENUCoords, ECEFCoords]], cut: float, mode: Literal["TOPOLOGIC", "GEOMETRIC"] = "TOPOLOGIC", verbose: bool = True) -> Network: """Extracts sub-network from routing forward search An edge is added to the output network if bot its ends have been visited during the search process (TOPOGRAPHIC mode) or if one of its ends are located in a circle centered on source with radius = cut :param source: a source node (id or node object) or a coord :param cut: a maximal distance for search :param mode: "TOPOLOGIC" (default) or "GEOMETRIC" :param verbose: Verbose creation :result: a network containing only visited nodes reachable from source with a cost lower than cut distance. """ if mode == "TOPOLOGIC": self.run_routing_forward(source, cut=cut) return self.__sub_network_routing(verbose) if mode == "GEOMETRIC": return self.__sub_network_geometric(source, cut, verbose) print("Error: unknown network extraction mode: ", mode) exit(1)
def __sub_network_routing(self, verbose: bool) -> Network: """Sub network generation :param verbose: Verbose creation :return: A sub part of the current network """ if verbose: print("Sub network extraction...") sub_net = Network() to_run = self.getEdgesId() if verbose: to_run = progressbar.progressbar(to_run) for eid in to_run: e = self.EDGES[eid] if (not e.source.visite) or (not e.target.visite): continue sub_net.addEdge(e, e.source, e.target) return sub_net def __sub_network_geometric(self, source: Union[Node, str], cut: float, verbose: bool) -> Network: """Sub network generation, based on geometry :param source: Source for the sub-network :param cut: TODO :param verbose: Verbose generation :return: A sub part of the current network """ if verbose: print("Sub network extraction...") if isinstance(source, Node) or isinstance(source, str): source = self.__correctInputNode(source).coord sub_net = Network() if self.spatial_index is None: to_run = self.getEdgesId() else: unit = self.spatial_index.groundDistanceToUnits(cut) to_run = [e[1] for e in self.spatial_index.neighborhood(source, unit=unit)] if verbose: to_run = progressbar.progressbar(to_run) for eid in to_run: e = self.EDGES[eid] if ( min( source.distance2DTo(e.source.coord), source.distance2DTo(e.target.coord), ) > cut ): continue sub_net.addEdge(e, e.source, e.target) return sub_net def __resetFlags(self): """Internal function to call before dijkstra forward step""" for elem in self.NODES.items(): elem[1].poids = -1 elem[1].visite = False elem[1].antecedent = "" elem[1].antecedent_edge = ""
[docs] def run_routing_forward(self, source: Union[Node, int], target: Union[Node, int] = None, cut: float = 1e300, output_dict: dict = None): """Executes forward pass of routing algorithm to find shortest distance between source node and all other nodes in graph. Execution may be stopped prematurely by specifying target node and/or cut distance. For each nodes reached (with a distance d < cut) during the search process, an entry {key=(source, n), value=d} is added to an output dictionnary (if specified as input) :param source: a source node :param target: a target node :param cut: a maximal distance for search :param output_dict: output structure for retrieved distance """ # Input format source = self.__correctInputNode(source) target = self.__correctInputNode(target) heuristic = 0 # Node initialization self.__resetFlags() self.NODES[source].poids = 0 pere = self.NODES[source] # Priority heap initialization fil = priority_dict({self.NODES[source]: self.NODES[source].poids}) # Routing while len(fil) != 0: pere = fil.pop_smallest() # Stop conditions if (pere.poids > cut) or (pere.id == target): break if not output_dict is None: output_dict[(source, pere.id)] = pere.poids pere.visite = True nextEdges = self.getNextEdges(pere.id) # Loop on connected edges for edge_id in nextEdges: e = self.EDGES[edge_id] fils = e.target if fils == pere: fils = e.source if fils.visite: continue if (fils.poids == -1) or (pere.poids + e.weight < fils.poids): if (self.routing_mode == 1) and not (target is None): heuristic = self.astar_wgt * fils.distanceTo(self.NODES[target]) fils.poids = pere.poids + e.weight + heuristic fils.antecedent = pere fils.antecedent_edge = e.id fil.__setitem__(fils, fils.poids)
[docs] def shortest_distance( self, source: Union[int, Node], target: Union[int, Node] = None, cut: float = 1e300, output_dict: dict = None) -> Union[float, list[float]]: """Finds shortest distance between source node and either a target node or a list of target nodes. In every case, time complexity is :math:`O((n+m) \cdot \log(n))` with :math:`n` the number of node and :math:`m` the number of edges. That amounts roughly to :math:`O(n \cdot \log(n))` for a typical planar road network, and to :math:`O(n^2 \cdot \log(n))` for a general network :param source: A source node (id or node object) :param target: A target node (id or node object, optional) :param cut: A maximal distance for search (optional) :param output_dict: output structure for retrieved distance :return: The distance between source and target nodes If target node is not specified, returns a list of distances between source node and all other nodes in the network (non-reachable nodes are associated to 1e300 distance). """ # Input format source = self.__correctInputNode(source) target = self.__correctInputNode(target) self.run_routing_forward(source, target, cut=cut, output_dict=output_dict) if not target is None: return self.NODES[target].poids else: return [(n[1].poids < 0) * 1e300 + n[1].poids for n in self.NODES.items()]
[docs] def all_shortest_distances(self, cut: float = 1e300, output_dict: dict = None) -> Dict[Tuple[int, int], float]: """Computes all shortest distances between pairs of nodes The results are saved in a dictionnary `{key=(source, n), value=d}`. If output dictionnary structure is provided as input, it is directly incremented during the process, otherwise, it is created before begining to search for shortest distances. Time complexity is :math:`O(n \cdot (n+m) \cdot \log(n))` with :math:`n` the number of nodes and :math:`n` the number of edges. That amounts roughly to :math:`O(n^2.log(n))` for a typical planar road network, and to :math:`O(n^3.log(n))` for a general network. :param cut: a maximal distance for search (optional) :param output_dict: output structure for retrieved distances :return: The distances between all pairs of nodes. Nodes separated by a distance > `cut` are not registered in the output dictionnary. If dictionnary is provided as input it is incremented during the process, hence it is possble tocall 'all_shortest_distances' successively with the same output dictionnary structure. """ print("Computing all pairs shortest distances...") if output_dict is None: output_dict = dict() for id_source in progressbar.progressbar(self.getNodesId()): self.run_routing_forward(id_source, cut=cut, output_dict=output_dict) return output_dict
[docs] def run_routing_backward(self, target: Union[int, Node]) -> Union[Track, None]: """Computes shortest path between the source node used in forward step :func:`run_routing_forward` and any target node. If target node has not been reached during forward search, a None object is returned by the function. :param target: A target node :return: A track between the source node specified in :func:`run_routing_forward` and a target node. The track contains topologic and non-topologic vertices. If the node target has not been reached during forward step, None object is output """ target = self.__correctInputNode(target) node = self.NODES[target] track = Track() track.addObs(Obs(node.coord)) if node.antecedent == "": return None while (node.poids != 0) and (node.antecedent != ""): e = self.EDGES[node.antecedent_edge] edge_geom = e.geom.copy() if e.source != node: edge_geom = edge_geom.reverse() track = track + (edge_geom > 1) node = node.antecedent return track
[docs] def shortest_path( self, source: Union[int, Node], target: Union[int, Node], cut: float = 1e300, output_dict: dict = None) -> Union[Track, None]: """Computes shortest path between source and target nodes :param source: A source node :param target: a target node :param cut: a maximal distance for search :param output_dict: output structure for retrieved distances :return: A track between source and target node. If target is not reachable during forward step, None object is output """ self.run_routing_forward(source, target, cut=cut, output_dict=output_dict) return self.run_routing_backward(target)
[docs] def distanceBtwPts( self, edge1: int, abs_curv_1: float, edge2: int, abs_curv_2: float) -> float: """Distance between 2 points Each points being described by: - Edge id number - Curvilinear abscissa on edge geometry from source :param edge1: edge 1 id number :param abs_curv_1: curvilinear abscissa on edge 1 geometry from source :param edge2: edge 2 id number :param abs_curv_2: curvilinear abscissa on edge 2 geometry from source :return: Distance between points """ e1 = self.EDGES[self.getEdgeId(edge1)] e2 = self.EDGES[self.getEdgeId(edge2)] if e1 == e2: return abs(abs_curv_1 - abs_curv_2) d1s = abs_curv_1 ds2 = abs_curv_2 d1t = e1.geom.length() - abs_curv_1 dt2 = e2.geom.length() - abs_curv_2 dss = self.prepared_shortest_distance(e1.source.id, e2.source.id) dtt = self.prepared_shortest_distance(e1.target.id, e2.target.id) dst = self.prepared_shortest_distance(e1.source.id, e2.target.id) dts = self.prepared_shortest_distance(e1.target.id, e2.source.id) d1 = d1s + dss + ds2 d2 = d1t + dtt + dt2 d3 = d1s + dst + dt2 d4 = d1t + dts + ds2 return min(min(d1, d2), min(d3, d4))
[docs] def prepare(self, cut: Union[float, None] = 1e300): """Precomputes shortest distances between all pairs of nodes and saves the result in :attr:`DISTANCES` attribute. :param cut: A maximal distance for search """ if self.DISTANCES is None: self.DISTANCES = dict() self.all_shortest_distances(cut=cut, output_dict=self.DISTANCES)
[docs] def has_prepared_shortest_distance(self, source: Union[int, Node], target: Union[int, Node]) -> bool: """Tests if a shortest distance has been precomputed :param source: A source node :param target: A target node :return: True if a shortest distance between `source` and `target` has already been computed. """ if self.DISTANCES is None: print( "Error: prepare function must be called before attempting to use preparation" ) if isinstance(source, Node): source = source.id if isinstance(target, Node): target = target.id key = (source, target) return key in self.DISTANCES
[docs] def prepared_shortest_distance(self, source: Union[int, Node], target: Union[int, Node]) -> float: """Finds shortest distance from the precomputation. May be called only after :func:`prepare` or :func:`load_prep.` :param source: A source node :param target: A target node :return: Shortest distance between source and targer. Returns 1e300 if shortest distance has not been precomputed. """ if self.DISTANCES is None: print( "Error: prepare function must be called before attempting to use preparation" ) if isinstance(source, Node): source = source.id if isinstance(target, Node): target = target.id key = (source, target) if key in self.DISTANCES: return self.DISTANCES[key] else: return 1e300
[docs] def save_prep(self, filename: str): """Saves DISTANCES attribute in a npy file for further use :param filename: Path to save precomputed structure """ if self.DISTANCES is None: print( "Error: prepare function must be called before attempting to save preparation" ) exit(1) np.save(filename, self.DISTANCES)
[docs] def load_prep(self, filename: str): """Imports DISTANCES attribute from an npy file :param filename: Path where precomputed structure is saved """ if len(filename) < 4: filename = filename + ".npy" if filename[:-4] != ".npy": filename = filename + ".npy" self.DISTANCES = read_dictionary = np.load(filename, allow_pickle="TRUE").item()