exploration.graphs
- Authors: Peter Mawhorter
- Consulted:
- Date: 2022-3-5
- Purpose: Low-level graph helpers & types.
This file defines tools on top of the networkx
package which are
lower-level than the key types used for most tasks (see core.py
for
those).
1""" 2- Authors: Peter Mawhorter 3- Consulted: 4- Date: 2022-3-5 5- Purpose: Low-level graph helpers & types. 6 7This file defines tools on top of the `networkx` package which are 8lower-level than the key types used for most tasks (see `core.py` for 9those). 10""" 11 12from typing import ( 13 Optional, Hashable, Dict, Union, Iterable, Tuple, Any, NoReturn, 14 Set, Sequence, cast, List, TypeVar, Generic, Callable 15) 16 17import networkx as nx # type: ignore[import] 18 19 20Node = TypeVar('Node', bound=Hashable) 21"Type variable for graph nodes." 22 23Edge = TypeVar('Edge', bound=Hashable) 24"Type variable for graph edges." 25 26 27class UniqueExitsGraph(nx.MultiDiGraph, Generic[Node, Edge]): 28 """ 29 A `networkx.MultiDiGraph` which has unique-per-source-node names for 30 each edge. On top of base functionality, this uses some extra memory 31 to store per-edge outgoing (but not incoming) by-edge dictionaries, 32 so that you can iterate over edges by their names rather than 33 iterating over neighbor nodes. This helps in some circumstances where 34 you know the edge name but not the name of the room it connects to. 35 36 This does NOT change the meaning of any of the built-in 37 `networkx.MultiDiGraph` methods, but instead adds new methods for 38 access to nodes or attributes by node -> edge name. 39 """ 40 def __init__(self) -> None: 41 super().__init__() 42 # A dictionary that maps nodes to edge names, storing neighbor 43 # nodes for each edge. Those neighbor nodes can be used to look 44 # up edge attributes using the normal MultiDiGraph machinery. 45 self._byEdge: Dict[Node, Dict[Edge, Node]] = {} 46 47 # Note: not hashable 48 49 def __eq__(self, other: Any) -> bool: 50 """ 51 Compares two graphs for equality. Note that various kinds of 52 graphs can be equal to a `UniqueExitsGraph` as long as the node 53 names, edge names, and data attributes are all the same. 54 """ 55 if not isinstance(other, nx.Graph): 56 return False 57 else: 58 # Compare nodes 59 myNodes = list(self) 60 otherNodes = list(self) 61 if len(myNodes) != len(otherNodes): 62 return False 63 myNodes.sort() 64 otherNodes.sort() 65 if myNodes != otherNodes: 66 return False 67 68 # Compare edges 69 myEdges = list(self.edges) 70 otherEdges = list(other.edges) 71 if len(myEdges) != len(otherEdges): 72 return False 73 if len(myEdges) > 0 and len(myEdges[0]) != len(otherEdges[0]): 74 return False 75 myEdges.sort() 76 otherEdges.sort() 77 if myEdges != otherEdges: 78 return False 79 80 # Compare node data 81 if any( 82 self.nodes[node] != other.nodes[node] 83 for node in myNodes 84 ): 85 return False 86 87 # Compare edge data 88 if any( 89 self.edges[edge] != other.edges[edge] 90 for edge in myEdges 91 ): 92 return False 93 94 # Everything checks out... 95 return True 96 97 def new_edge_key(self, u: Node, v: Node) -> NoReturn: 98 """ 99 This method would normally be used to generate new edge keys. We 100 disable it, because we want to ensure that all edges are properly 101 labeled. 102 """ 103 raise NotImplementedError( 104 "Attempted to add an edge without specifying a key!" 105 ) 106 107 # TODO: Sort out networkx type annotations? 108 def add_node(self, node: Node, **attr: Any): # type:ignore [override] 109 """ 110 See `networkx.MultiDiGraph.add_node`. 111 """ 112 super().add_node(node, **attr) 113 self._byEdge[node] = {} # type Dict[Edge, Node] 114 115 def add_nodes_from( # type:ignore [override] 116 self, 117 nodes: Union[ 118 Iterable[Node], 119 Iterable[Tuple[Node, Dict[Any, Any]]] 120 ], 121 **attr: Any 122 ): 123 """ 124 See `networkx.MultiDiGraph.add_nodes_from`. 125 """ 126 super().add_nodes_from(nodes, **attr) 127 # Reassignment during tuple unpacking is not checkable... 128 n: Any 129 for n in nodes: 130 # Test for hashability & unpack tuple if not 131 try: 132 self._byEdge.get(n) 133 except TypeError: 134 n, _ = n # mypy can't handle this properly 135 self._byEdge[n] = {} 136 137 def remove_node(self, node: Node): 138 """ 139 See `networkx.MultiDiGraph.remove_node`. 140 """ 141 # Handle deletion from inherited structures 142 super().remove_node(node) 143 144 # Ignore if not present 145 if node not in self._byEdge: 146 return 147 148 # Remove record of outgoing edges 149 del self._byEdge[node] 150 151 # Remove incoming edge records 152 for source, edgeMap in self._byEdge.items(): 153 delete = [] 154 # Find all edges which go to the deleted node 155 # (this is not terribly efficient) 156 for edgeName, dest in edgeMap.items(): 157 if dest == node: 158 delete.append(edgeName) 159 # Delete them in a separate loop, so that we don't 160 # modify-while-iterating (not efficient and maybe 161 # unnecessary?) 162 for d in delete: 163 del edgeMap[d] 164 165 def remove_nodes_from(self, nodes: Iterable[Node]): 166 """ 167 See `networkx.MultiDiGraph.remove_nodes_from`. 168 """ 169 # First use inherited method to remove from inherited structures 170 super().remove_nodes_from(nodes) 171 # remove our custom info 172 for n in nodes: 173 if n in self._byEdge: 174 del self._byEdge[n] 175 176 for source, edgeMap in self._byEdge.items(): 177 delete = [] 178 # Find all edges that go to any deleted node 179 for edgeName, dest in edgeMap.items(): 180 if dest in nodes: 181 delete.append(edgeName) 182 183 # Remove edges in separate loop to avoid 184 # modifying-while-iterating (not efficient and maybe 185 # unnecessary?) 186 for d in delete: 187 del edgeMap[d] 188 189 def add_edge( # type:ignore [override] 190 self, 191 u_of_edge: Node, 192 v_of_edge: Node, 193 key: Edge, 194 **attr: Any 195 ) -> Edge: 196 """ 197 See `networkx.MultiDiGraph.add_edge`. 198 199 For a `UniqueExitsGraph`, an edge key must be supplied 200 explicitly. A `KeyError` will be raised if an edge using the 201 given key (i.e., name) already exists starting at the source node 202 (regardless of its destination!). 203 204 Returns the key it was given, to match the base `add_edge` API. 205 """ 206 if u_of_edge in self._byEdge and key in self._byEdge[u_of_edge]: 207 raise KeyError( 208 f"Cannot add a second edge {key!r} starting at node" 209 f" {u_of_edge!r}." 210 ) 211 super().add_edge(u_of_edge, v_of_edge, key, **attr) 212 # Note: the base add_edge function does NOT call our add_node 213 # function :( 214 if u_of_edge not in self._byEdge: 215 self._byEdge[u_of_edge] = {} 216 if v_of_edge not in self._byEdge: 217 self._byEdge[v_of_edge] = {} 218 # Add the edge to our by-edge-name structure 219 self._byEdge[u_of_edge][key] = v_of_edge 220 221 return key 222 223 def add_edges_from( 224 self, 225 ebunch_to_add: Any, 226 # Type should be this, but checker won't pass it: 227 # Union[ 228 # Iterable[Tuple[Node, Node, Edge]], 229 # Iterable[Tuple[Node, Node, Edge, Dict[Any, Any]]] 230 # ], 231 **attr: Any 232 ): 233 """ 234 See `networkx.MultiDiGraph.add_edges_from`. Tuples in the ebunch 235 must be 3- or 4-tuples that include a specific key (not just 236 data). Nodes will be created as necessary. 237 238 Raises a `KeyError` if adding an edge is impossible because it 239 re-uses the same edge name at a particular source node, but if an 240 attempt is made to add an existing edge with the same 241 destination, this will just update the relevant edge attributes. 242 243 Raises a `KeyError` instead of silently updating edge properties 244 if the existing edge was also added by an earlier entry in the 245 `ebunch_to_add` (i.e., if you are trying to add two edges at 246 once that go between the same pair of nodes and use the same 247 edge key). 248 249 >>> from exploration import graphs as eg 250 >>> g = eg.UniqueExitsGraph() 251 >>> g.add_edges_from([ 252 ... ('A', 'B', 'up'), 253 ... ('A', 'B', 'up2'), 254 ... ('B', 'A', 'down'), 255 ... ('B', 'B', 'self'), 256 ... ('B', 'C', 'next'), 257 ... ('C', 'B', 'prev') 258 ... ]) 259 >>> g.nodes 260 NodeView(('A', 'B', 'C')) 261 >>> for edge in g.edges: 262 ... print(edge) 263 ('A', 'B', 'up') 264 ('A', 'B', 'up2') 265 ('B', 'A', 'down') 266 ('B', 'B', 'self') 267 ('B', 'C', 'next') 268 ('C', 'B', 'prev') 269 """ 270 etuple: Any 271 for i, etuple in enumerate(ebunch_to_add): 272 if len(etuple) < 3: 273 raise ValueError( 274 f"Edges to add must contain explicit keys for a" 275 f" UniqueExitsGraph (edge #{i} had only 2 parts)." 276 ) 277 try: 278 hash(etuple[2]) 279 except TypeError: 280 raise ValueError( 281 f"Edges to add must contain explicit keys for a" 282 f" UniqueExitsGraph (edge #{i} had an unhashable 3rd" 283 f" component)." 284 ) 285 286 # Check edge name uniqueness 287 u, v, k = etuple[:3] 288 if u in self._byEdge and self._byEdge[u].get(k) != v: 289 raise KeyError( 290 f"Cannot add or update an edge named '{k}' from node" 291 f" '{u}' to node '{v}' because an edge by that name" 292 f" already exists and goes to a different" 293 f" destination." 294 ) 295 296 # Add edges to inherited structures 297 super().add_edges_from(ebunch_to_add, **attr) 298 299 # Note base implementation calls add_edge, so we don't need to 300 # add edges to our extra structure 301 302 def remove_edge( # type:ignore [override] 303 self, 304 u_of_edge: Node, 305 v_of_edge: Node, 306 key: Edge 307 ): 308 """ 309 See `networkx.MultiDiGraph.remove_edge`. A key is required in 310 this version to specify which edge we're removing. 311 312 Raises a NetworkXError if the target edge does not exist. 313 """ 314 super().remove_edge(u_of_edge, v_of_edge, key) 315 del self._byEdge[u_of_edge][key] 316 317 def remove_edges_from( 318 self, 319 ebunch: Union[ # type:ignore [override] 320 Iterable[Tuple[Node, Node, Edge]], 321 Iterable[Tuple[Node, Node, Edge, Dict[Any, Any]]] 322 ] 323 ): 324 """ 325 See `networkx.MultiDiGraph.remove_edges_from`. Edge tuples in 326 the ebunch must be 3- or 4-tuples that include a key. 327 328 If an edge being removed is not present, it will be ignored. 329 """ 330 if any(len(etuple) not in (3, 4) for etuple in ebunch): 331 raise ValueError( 332 "Edges to remove must be u, v, k 3-tuples or u, v, k, d" 333 " 4-tuples." 334 ) 335 # TODO: Fix networkx MultiDiGraph type stubs 336 super().remove_edges_from(ebunch) # type:ignore [arg-type] 337 # This calls self.remove_edge under the hood so we don't need 338 # extra cleanup steps for _byEdge. 339 340 def clear(self) -> None: 341 """ 342 See `networkx.MultiDiGraph.clear`. 343 """ 344 super().clear() 345 self._byEdge.clear() 346 347 def clear_edges(self) -> None: 348 """ 349 See `networkx.MultiDiGraph.clear_edges`. 350 """ 351 super().clear_edges() 352 for _, edgeMap in self._byEdge.items(): 353 edgeMap.clear() 354 355 def reverse(self) -> NoReturn: # type:ignore [override] 356 """ 357 See `networkx.MultiDiGraph.reverse`. 358 """ 359 raise NotImplementedError( 360 "Reversing a UniqueExitsGraph is not supported because" 361 " reversed edge names might not be unique." 362 ) 363 364 def removeEdgeByKey(self, uOfEdge: Node, key: Edge): 365 """ 366 Removes an edge sourced at a particular node that has a 367 particular key, without knowing what the destination is. 368 369 Raises a `KeyError` if the named edge does not exist. 370 371 ## Example 372 373 >>> g = UniqueExitsGraph() 374 >>> g.add_edges_from([ 375 ... ('A', 'B', 'up'), 376 ... ('A', 'B', 'up2'), 377 ... ('B', 'A', 'down'), 378 ... ('B', 'B', 'self'), 379 ... ('B', 'C', 'next'), 380 ... ('C', 'B', 'prev') 381 ... ]) 382 >>> g.getDestination('A', 'up') 383 'B' 384 >>> g.getDestination('A', 'up2') 385 'B' 386 >>> g.getDestination('B', 'self') 387 'B' 388 >>> g.removeEdgeByKey('A', 'up2') 389 >>> g.removeEdgeByKey('B', 'self') 390 >>> g.getDestination('A', 'up2') is None 391 True 392 >>> g.getDestination('B', 'self') is None 393 True 394 """ 395 vOfEdge = self._byEdge[uOfEdge][key] 396 super().remove_edge(uOfEdge, vOfEdge, key) 397 del self._byEdge[uOfEdge][key] 398 399 def removeEdgesByKey(self, edgeIds: Iterable[Tuple[Node, Edge]]): 400 """ 401 Removes multiple edges by source node and key, without needing 402 to know destination nodes. The `edgeIds` argument must be a list 403 of tuples containing source node, edge key pairs. 404 405 Silently ignores already-nonexistent edges. 406 407 ## Example 408 409 >>> g = UniqueExitsGraph() 410 >>> g.add_edges_from([ 411 ... ('A', 'B', 'up'), 412 ... ('A', 'B', 'up2'), 413 ... ('B', 'A', 'down'), 414 ... ('B', 'B', 'self'), 415 ... ('B', 'C', 'next'), 416 ... ('C', 'B', 'prev') 417 ... ]) 418 >>> g.getDestination('A', 'up') 419 'B' 420 >>> g.getDestination('A', 'up2') 421 'B' 422 >>> g.getDestination('B', 'self') 423 'B' 424 >>> g.removeEdgesByKey([('A', 'up2'), ('B', 'self')]) 425 >>> g.getDestination('A', 'up2') is None 426 True 427 >>> g.getDestination('B', 'self') is None 428 True 429 """ 430 for source, key in edgeIds: 431 if key in self._byEdge.get(source, {}): 432 self.removeEdgeByKey(source, key) 433 # Otherwise ignore this edge... 434 435 def connections( 436 self, 437 edgeFilter: Optional[ 438 Callable[[Node, Edge, Node, 'UniqueExitsGraph'], bool] 439 ] = None 440 ) -> nx.Graph: 441 """ 442 Returns an undirected graph with the same nodes IDs as the base 443 graph but none of the node or edge attributes. Nodes which have 444 any connection between them in either direction in the original 445 graph will be connected by a single edge in the connections 446 graph. 447 448 If an `edgeFilter` function is provided, it will be given a 449 source node, an edge, a destination node, and the entire graph as 450 arguments. It should return a boolean, and for edges where it 451 returns False, these won't be included in the final graph. Note 452 that because two nodes can be connected by multiple edges in 453 either direction, filtering out a single edge may not sever the 454 connection between two nodes in the final graph. 455 """ 456 result: nx.Graph = nx.Graph() 457 result.add_nodes_from(self) 458 if edgeFilter is None: 459 result.add_edges_from(self.edges(keys=False, data=False)) 460 else: 461 useEdges = [] 462 for (src, dst, edge) in self.edges(keys=True): 463 if edgeFilter(src, edge, dst, self): 464 result.add_edge(src, dst) 465 return result 466 467 def destinationsFrom(self, source: Node) -> Dict[Edge, Node]: 468 """ 469 Given a source node, returns a dictionary mapping the keys of all 470 outgoing edges from that node to their destination nodes. Raises 471 a `KeyError` if the node is not present in the graph. 472 473 Editing the dictionary returned could cause serious problems, so 474 please don't; it will be updated live as the graph is changed. 475 476 ## Example 477 478 >>> g = UniqueExitsGraph() 479 >>> g.add_edges_from([ 480 ... ('A', 'B', 'up'), 481 ... ('A', 'B', 'up2'), 482 ... ('B', 'A', 'down'), 483 ... ('B', 'B', 'self'), 484 ... ('B', 'C', 'next'), 485 ... ('C', 'B', 'prev') 486 ... ]) 487 >>> g.destinationsFrom('A') 488 {'up': 'B', 'up2': 'B'} 489 >>> g.destinationsFrom('B') 490 {'down': 'A', 'self': 'B', 'next': 'C'} 491 >>> g.destinationsFrom('C') 492 {'prev': 'B'} 493 >>> g.destinationsFrom('D') 494 Traceback (most recent call last): 495 ... 496 KeyError... 497 """ 498 return self._byEdge[source] 499 500 def destination(self, source: Node, edge: Edge) -> Node: 501 """ 502 Given a source node and an edge key, looks up and returns the 503 destination node for that edge. Raises a `KeyError` if there is no 504 edge from the specified node with the specified name. 505 506 ## Example 507 508 >>> g = UniqueExitsGraph() 509 >>> g.add_edges_from([ 510 ... ('A', 'B', 'up'), 511 ... ('A', 'B', 'up2'), 512 ... ('B', 'A', 'down'), 513 ... ('B', 'B', 'self'), 514 ... ('B', 'C', 'next'), 515 ... ('C', 'B', 'prev') 516 ... ]) 517 >>> g.destination('A', 'up') 518 'B' 519 >>> g.destination('A', 'up2') 520 'B' 521 >>> g.destination('B', 'down') 522 'A' 523 >>> g.destination('A', 'nonexistent') 524 Traceback (most recent call last): 525 ... 526 KeyError... 527 >>> g.destination('D', 'any') 528 Traceback (most recent call last): 529 ... 530 KeyError... 531 """ 532 return self._byEdge[source][edge] 533 534 def getDestination( 535 self, 536 source: Node, 537 edge: Edge, 538 default: Any = None 539 ) -> Optional[Node]: 540 """ 541 Works like `destination`, but instead of raising a `KeyError` if 542 the node or edge is missing, it returns a default value (with a 543 default default of `None`). 544 545 ## Example 546 547 >>> g = UniqueExitsGraph() 548 >>> g.add_edges_from([ 549 ... ('A', 'B', 'up'), 550 ... ('A', 'B', 'up2'), 551 ... ('B', 'A', 'down'), 552 ... ('B', 'B', 'self'), 553 ... ('B', 'C', 'next'), 554 ... ('C', 'B', 'prev') 555 ... ]) 556 >>> g.getDestination('A', 'up') 557 'B' 558 >>> g.getDestination('A', 'up2') 559 'B' 560 >>> g.getDestination('B', 'down') 561 'A' 562 >>> g.getDestination('A', 'nonexistent') is None 563 True 564 >>> g.getDestination('A', 'nonexistent', 'default') 565 'default' 566 >>> g.getDestination('D', 'any') is None 567 True 568 """ 569 return self._byEdge.get(source, {}).get(edge, default) 570 571 def allEdgesTo( 572 self, 573 destination: Node 574 ) -> List[Tuple[Node, Edge]]: 575 """ 576 Searches the entire graph for edges whose destinations are the 577 specified destination, and returns a list of (node, edge) pairs 578 indicating the source node and edge name for each of those edges. 579 Self-edges are included in this list. 580 581 ## Example 582 583 >>> g = UniqueExitsGraph() 584 >>> g.add_edges_from([ 585 ... ('A', 'B', 'up'), 586 ... ('A', 'B', 'up2'), 587 ... ('B', 'A', 'down'), 588 ... ('B', 'B', 'self'), 589 ... ('B', 'C', 'next'), 590 ... ('C', 'B', 'prev') 591 ... ]) 592 >>> g.allEdgesTo('A') 593 [('B', 'down')] 594 >>> g.allEdgesTo('B') 595 [('A', 'up'), ('A', 'up2'), ('B', 'self'), ('C', 'prev')] 596 >>> g.allEdgesTo('C') 597 [('B', 'next')] 598 >>> g.allEdgesTo('D') 599 [] 600 """ 601 results = [] 602 for node in self: 603 fromThere = self[node] 604 toHere = fromThere.get(destination, {}) 605 for edgeKey in toHere: 606 results.append((node, edgeKey)) 607 608 return results 609 610 def allEdges(self) -> List[Tuple[Node, Node, Edge]]: 611 """ 612 Returns a list of tuples containing source node, destination 613 node, and then edge node, which includes each edge in the graph 614 once. 615 """ 616 # TODO: Fix networkx type annotations 617 return self.edges(keys=True) # type: ignore 618 619 def textMapObj( 620 self, 621 edgeSep: str = '::', 622 external: Optional[Set[Node]] = None, 623 explorationOrder: Optional[Tuple[Node, Sequence[Edge]]] = None, 624 edgeOrders: Union[ 625 Dict[Node, Sequence[Edge]], 626 Dict[Node, Dict[Edge, Any]], 627 None 628 ] = None 629 ): 630 """ 631 Returns a special object which is JSON-serializable and which 632 when serialized creates a semi-human-usable text-format map of 633 the graph. 634 635 The object consists of nested dictionaries, one per node, where 636 keys are node name + edge name strings (combined using the 637 `edgeSep` argument, default is '::'). The value for each key is 638 one of: 639 640 1. Another dictionary representing the node that edge leads 641 to, which can in turn have dictionary values... 642 2. A string naming a destination node that's already represented 643 elsewhere (or naming the current node for self-edges). 644 645 Any node present in the specified `external` set will be linked 646 to instead of listed out, even if it exists in the graph. The 647 `external` set **will be modified** by this function to include 648 all visited nodes in the graph. 649 650 If an `explorationOrder` is provided, it must be a tuple 651 specifying a start node followed by a sequence of edges that 652 indicates the path taken, and the edges will be visited 653 according to that order (this only matters in Python 3.7+ where 654 dictionaries have consistent order). A `ValueError` will be 655 raised if an invalid exploration order is provided. The path 656 list will be ignored if `edgeOrders` is provided explicitly. 657 658 TODO: What about unexplorable graphs (allow node names in place 659 of edge names in exploration order?)?!? 660 661 If `edgeOrders` is provided directly, it will override the 662 path part of the `explorationOrder` to determine the ordering of 663 edges at each node. If not and `explorationOrder` is provided, it 664 will be deduced from the `explorationOrder`. If neither is 665 present, ordering will follow whatever natural order is in the 666 graph, which in most cases should be order-of-creation. 667 668 Notes: 669 - For the format to avoid ambiguity, the `edgeSep` value must be 670 a string which does not appear in any node or edge names. 671 - Nodes and edge values will be converted to strings to build the 672 map. 673 - Node and edge properties are not represented in the resulting 674 object. 675 - For a variety of reasons, the result cannot be converted back 676 to a graph object. This is not intended for use as a JSON 677 serialization route (see the `networkx.readwrite.json_graph` 678 module for some built-in options). 679 - To get a string representation, one could do: 680 `json.dumps(graph.textMapObj())` 681 682 ## Examples 683 684 >>> from exploration import graphs as eg 685 >>> import json 686 >>> g = eg.UniqueExitsGraph() 687 >>> g.add_edges_from([ 688 ... ('A', 'B', 'up'), 689 ... ('A', 'B', 'up2'), 690 ... ('B', 'A', 'down'), 691 ... ('B', 'B', 'self'), 692 ... ('B', 'C', 'next'), 693 ... ('C', 'B', 'prev') 694 ... ]) 695 >>> print(json.dumps(g.textMapObj(), indent=2)) 696 { 697 "A::up": { 698 "B::down": "A", 699 "B::self": "B", 700 "B::next": { 701 "C::prev": "B" 702 } 703 }, 704 "A::up2": "B" 705 } 706 """ 707 # We use `external` as our visited set 708 if external is None: 709 external = set() 710 711 if explorationOrder is not None: 712 here, path = explorationOrder 713 else: 714 # Find first non-external node as our starting node 715 for here in self.nodes: 716 if here not in external: 717 break 718 719 # Path is empty 720 path = [] 721 722 # Determine edge ordering for each node from exploration order 723 # or by natural ordering if no explorationOrder is available 724 if edgeOrders is None: 725 edgeOrders = cast( 726 Dict[Node, Dict[Edge, Any]], 727 {} 728 ) 729 current = here 730 for i in range(len(path)): 731 edge = path[i] 732 # Add this edge next in the ordering for this node 733 orderHere: Dict[Edge, Any] = edgeOrders.setdefault(current, {}) 734 # Note: we use a dictionary here because dictionaries do 735 # preserve insertion ordering (3.7+) and we need to both 736 # keep things in order AND do a bunch of lookups to 737 # avoid duplicates. 738 if edge not in orderHere: 739 orderHere[edge] = True 740 741 # Move to next node 742 if edge not in self._byEdge[current]: 743 raise ValueError( 744 f"Invalid edge in exploration order path: at" 745 f" step {i} we reached node {current} and were" 746 f" supposed to take edge {edge} but that edge" 747 f" does not exist." 748 ) 749 current = self._byEdge[current][edge] 750 751 # Add any unexplored nodes and/or edges in natural order 752 for node in self.nodes: 753 orderHere = edgeOrders.setdefault(node, {}) 754 for edge in self._byEdge[node]: 755 if edge not in orderHere: 756 orderHere[edge] = True 757 758 result = {} 759 external.add(here) 760 # Now loop through keys of this node 761 for key in edgeOrders[here]: 762 combined = str(here) + edgeSep + str(key) 763 dest = self._byEdge[here][key] 764 if dest in external: 765 # links, including self-links 766 result[combined] = str(dest) 767 else: 768 # Recurse 769 result[combined] = self.textMapObj( 770 edgeSep, 771 external, 772 (dest, []), # empty path since we have edgeOrders 773 edgeOrders 774 ) 775 776 return result
Type variable for graph nodes.
Type variable for graph edges.
28class UniqueExitsGraph(nx.MultiDiGraph, Generic[Node, Edge]): 29 """ 30 A `networkx.MultiDiGraph` which has unique-per-source-node names for 31 each edge. On top of base functionality, this uses some extra memory 32 to store per-edge outgoing (but not incoming) by-edge dictionaries, 33 so that you can iterate over edges by their names rather than 34 iterating over neighbor nodes. This helps in some circumstances where 35 you know the edge name but not the name of the room it connects to. 36 37 This does NOT change the meaning of any of the built-in 38 `networkx.MultiDiGraph` methods, but instead adds new methods for 39 access to nodes or attributes by node -> edge name. 40 """ 41 def __init__(self) -> None: 42 super().__init__() 43 # A dictionary that maps nodes to edge names, storing neighbor 44 # nodes for each edge. Those neighbor nodes can be used to look 45 # up edge attributes using the normal MultiDiGraph machinery. 46 self._byEdge: Dict[Node, Dict[Edge, Node]] = {} 47 48 # Note: not hashable 49 50 def __eq__(self, other: Any) -> bool: 51 """ 52 Compares two graphs for equality. Note that various kinds of 53 graphs can be equal to a `UniqueExitsGraph` as long as the node 54 names, edge names, and data attributes are all the same. 55 """ 56 if not isinstance(other, nx.Graph): 57 return False 58 else: 59 # Compare nodes 60 myNodes = list(self) 61 otherNodes = list(self) 62 if len(myNodes) != len(otherNodes): 63 return False 64 myNodes.sort() 65 otherNodes.sort() 66 if myNodes != otherNodes: 67 return False 68 69 # Compare edges 70 myEdges = list(self.edges) 71 otherEdges = list(other.edges) 72 if len(myEdges) != len(otherEdges): 73 return False 74 if len(myEdges) > 0 and len(myEdges[0]) != len(otherEdges[0]): 75 return False 76 myEdges.sort() 77 otherEdges.sort() 78 if myEdges != otherEdges: 79 return False 80 81 # Compare node data 82 if any( 83 self.nodes[node] != other.nodes[node] 84 for node in myNodes 85 ): 86 return False 87 88 # Compare edge data 89 if any( 90 self.edges[edge] != other.edges[edge] 91 for edge in myEdges 92 ): 93 return False 94 95 # Everything checks out... 96 return True 97 98 def new_edge_key(self, u: Node, v: Node) -> NoReturn: 99 """ 100 This method would normally be used to generate new edge keys. We 101 disable it, because we want to ensure that all edges are properly 102 labeled. 103 """ 104 raise NotImplementedError( 105 "Attempted to add an edge without specifying a key!" 106 ) 107 108 # TODO: Sort out networkx type annotations? 109 def add_node(self, node: Node, **attr: Any): # type:ignore [override] 110 """ 111 See `networkx.MultiDiGraph.add_node`. 112 """ 113 super().add_node(node, **attr) 114 self._byEdge[node] = {} # type Dict[Edge, Node] 115 116 def add_nodes_from( # type:ignore [override] 117 self, 118 nodes: Union[ 119 Iterable[Node], 120 Iterable[Tuple[Node, Dict[Any, Any]]] 121 ], 122 **attr: Any 123 ): 124 """ 125 See `networkx.MultiDiGraph.add_nodes_from`. 126 """ 127 super().add_nodes_from(nodes, **attr) 128 # Reassignment during tuple unpacking is not checkable... 129 n: Any 130 for n in nodes: 131 # Test for hashability & unpack tuple if not 132 try: 133 self._byEdge.get(n) 134 except TypeError: 135 n, _ = n # mypy can't handle this properly 136 self._byEdge[n] = {} 137 138 def remove_node(self, node: Node): 139 """ 140 See `networkx.MultiDiGraph.remove_node`. 141 """ 142 # Handle deletion from inherited structures 143 super().remove_node(node) 144 145 # Ignore if not present 146 if node not in self._byEdge: 147 return 148 149 # Remove record of outgoing edges 150 del self._byEdge[node] 151 152 # Remove incoming edge records 153 for source, edgeMap in self._byEdge.items(): 154 delete = [] 155 # Find all edges which go to the deleted node 156 # (this is not terribly efficient) 157 for edgeName, dest in edgeMap.items(): 158 if dest == node: 159 delete.append(edgeName) 160 # Delete them in a separate loop, so that we don't 161 # modify-while-iterating (not efficient and maybe 162 # unnecessary?) 163 for d in delete: 164 del edgeMap[d] 165 166 def remove_nodes_from(self, nodes: Iterable[Node]): 167 """ 168 See `networkx.MultiDiGraph.remove_nodes_from`. 169 """ 170 # First use inherited method to remove from inherited structures 171 super().remove_nodes_from(nodes) 172 # remove our custom info 173 for n in nodes: 174 if n in self._byEdge: 175 del self._byEdge[n] 176 177 for source, edgeMap in self._byEdge.items(): 178 delete = [] 179 # Find all edges that go to any deleted node 180 for edgeName, dest in edgeMap.items(): 181 if dest in nodes: 182 delete.append(edgeName) 183 184 # Remove edges in separate loop to avoid 185 # modifying-while-iterating (not efficient and maybe 186 # unnecessary?) 187 for d in delete: 188 del edgeMap[d] 189 190 def add_edge( # type:ignore [override] 191 self, 192 u_of_edge: Node, 193 v_of_edge: Node, 194 key: Edge, 195 **attr: Any 196 ) -> Edge: 197 """ 198 See `networkx.MultiDiGraph.add_edge`. 199 200 For a `UniqueExitsGraph`, an edge key must be supplied 201 explicitly. A `KeyError` will be raised if an edge using the 202 given key (i.e., name) already exists starting at the source node 203 (regardless of its destination!). 204 205 Returns the key it was given, to match the base `add_edge` API. 206 """ 207 if u_of_edge in self._byEdge and key in self._byEdge[u_of_edge]: 208 raise KeyError( 209 f"Cannot add a second edge {key!r} starting at node" 210 f" {u_of_edge!r}." 211 ) 212 super().add_edge(u_of_edge, v_of_edge, key, **attr) 213 # Note: the base add_edge function does NOT call our add_node 214 # function :( 215 if u_of_edge not in self._byEdge: 216 self._byEdge[u_of_edge] = {} 217 if v_of_edge not in self._byEdge: 218 self._byEdge[v_of_edge] = {} 219 # Add the edge to our by-edge-name structure 220 self._byEdge[u_of_edge][key] = v_of_edge 221 222 return key 223 224 def add_edges_from( 225 self, 226 ebunch_to_add: Any, 227 # Type should be this, but checker won't pass it: 228 # Union[ 229 # Iterable[Tuple[Node, Node, Edge]], 230 # Iterable[Tuple[Node, Node, Edge, Dict[Any, Any]]] 231 # ], 232 **attr: Any 233 ): 234 """ 235 See `networkx.MultiDiGraph.add_edges_from`. Tuples in the ebunch 236 must be 3- or 4-tuples that include a specific key (not just 237 data). Nodes will be created as necessary. 238 239 Raises a `KeyError` if adding an edge is impossible because it 240 re-uses the same edge name at a particular source node, but if an 241 attempt is made to add an existing edge with the same 242 destination, this will just update the relevant edge attributes. 243 244 Raises a `KeyError` instead of silently updating edge properties 245 if the existing edge was also added by an earlier entry in the 246 `ebunch_to_add` (i.e., if you are trying to add two edges at 247 once that go between the same pair of nodes and use the same 248 edge key). 249 250 >>> from exploration import graphs as eg 251 >>> g = eg.UniqueExitsGraph() 252 >>> g.add_edges_from([ 253 ... ('A', 'B', 'up'), 254 ... ('A', 'B', 'up2'), 255 ... ('B', 'A', 'down'), 256 ... ('B', 'B', 'self'), 257 ... ('B', 'C', 'next'), 258 ... ('C', 'B', 'prev') 259 ... ]) 260 >>> g.nodes 261 NodeView(('A', 'B', 'C')) 262 >>> for edge in g.edges: 263 ... print(edge) 264 ('A', 'B', 'up') 265 ('A', 'B', 'up2') 266 ('B', 'A', 'down') 267 ('B', 'B', 'self') 268 ('B', 'C', 'next') 269 ('C', 'B', 'prev') 270 """ 271 etuple: Any 272 for i, etuple in enumerate(ebunch_to_add): 273 if len(etuple) < 3: 274 raise ValueError( 275 f"Edges to add must contain explicit keys for a" 276 f" UniqueExitsGraph (edge #{i} had only 2 parts)." 277 ) 278 try: 279 hash(etuple[2]) 280 except TypeError: 281 raise ValueError( 282 f"Edges to add must contain explicit keys for a" 283 f" UniqueExitsGraph (edge #{i} had an unhashable 3rd" 284 f" component)." 285 ) 286 287 # Check edge name uniqueness 288 u, v, k = etuple[:3] 289 if u in self._byEdge and self._byEdge[u].get(k) != v: 290 raise KeyError( 291 f"Cannot add or update an edge named '{k}' from node" 292 f" '{u}' to node '{v}' because an edge by that name" 293 f" already exists and goes to a different" 294 f" destination." 295 ) 296 297 # Add edges to inherited structures 298 super().add_edges_from(ebunch_to_add, **attr) 299 300 # Note base implementation calls add_edge, so we don't need to 301 # add edges to our extra structure 302 303 def remove_edge( # type:ignore [override] 304 self, 305 u_of_edge: Node, 306 v_of_edge: Node, 307 key: Edge 308 ): 309 """ 310 See `networkx.MultiDiGraph.remove_edge`. A key is required in 311 this version to specify which edge we're removing. 312 313 Raises a NetworkXError if the target edge does not exist. 314 """ 315 super().remove_edge(u_of_edge, v_of_edge, key) 316 del self._byEdge[u_of_edge][key] 317 318 def remove_edges_from( 319 self, 320 ebunch: Union[ # type:ignore [override] 321 Iterable[Tuple[Node, Node, Edge]], 322 Iterable[Tuple[Node, Node, Edge, Dict[Any, Any]]] 323 ] 324 ): 325 """ 326 See `networkx.MultiDiGraph.remove_edges_from`. Edge tuples in 327 the ebunch must be 3- or 4-tuples that include a key. 328 329 If an edge being removed is not present, it will be ignored. 330 """ 331 if any(len(etuple) not in (3, 4) for etuple in ebunch): 332 raise ValueError( 333 "Edges to remove must be u, v, k 3-tuples or u, v, k, d" 334 " 4-tuples." 335 ) 336 # TODO: Fix networkx MultiDiGraph type stubs 337 super().remove_edges_from(ebunch) # type:ignore [arg-type] 338 # This calls self.remove_edge under the hood so we don't need 339 # extra cleanup steps for _byEdge. 340 341 def clear(self) -> None: 342 """ 343 See `networkx.MultiDiGraph.clear`. 344 """ 345 super().clear() 346 self._byEdge.clear() 347 348 def clear_edges(self) -> None: 349 """ 350 See `networkx.MultiDiGraph.clear_edges`. 351 """ 352 super().clear_edges() 353 for _, edgeMap in self._byEdge.items(): 354 edgeMap.clear() 355 356 def reverse(self) -> NoReturn: # type:ignore [override] 357 """ 358 See `networkx.MultiDiGraph.reverse`. 359 """ 360 raise NotImplementedError( 361 "Reversing a UniqueExitsGraph is not supported because" 362 " reversed edge names might not be unique." 363 ) 364 365 def removeEdgeByKey(self, uOfEdge: Node, key: Edge): 366 """ 367 Removes an edge sourced at a particular node that has a 368 particular key, without knowing what the destination is. 369 370 Raises a `KeyError` if the named edge does not exist. 371 372 ## Example 373 374 >>> g = UniqueExitsGraph() 375 >>> g.add_edges_from([ 376 ... ('A', 'B', 'up'), 377 ... ('A', 'B', 'up2'), 378 ... ('B', 'A', 'down'), 379 ... ('B', 'B', 'self'), 380 ... ('B', 'C', 'next'), 381 ... ('C', 'B', 'prev') 382 ... ]) 383 >>> g.getDestination('A', 'up') 384 'B' 385 >>> g.getDestination('A', 'up2') 386 'B' 387 >>> g.getDestination('B', 'self') 388 'B' 389 >>> g.removeEdgeByKey('A', 'up2') 390 >>> g.removeEdgeByKey('B', 'self') 391 >>> g.getDestination('A', 'up2') is None 392 True 393 >>> g.getDestination('B', 'self') is None 394 True 395 """ 396 vOfEdge = self._byEdge[uOfEdge][key] 397 super().remove_edge(uOfEdge, vOfEdge, key) 398 del self._byEdge[uOfEdge][key] 399 400 def removeEdgesByKey(self, edgeIds: Iterable[Tuple[Node, Edge]]): 401 """ 402 Removes multiple edges by source node and key, without needing 403 to know destination nodes. The `edgeIds` argument must be a list 404 of tuples containing source node, edge key pairs. 405 406 Silently ignores already-nonexistent edges. 407 408 ## Example 409 410 >>> g = UniqueExitsGraph() 411 >>> g.add_edges_from([ 412 ... ('A', 'B', 'up'), 413 ... ('A', 'B', 'up2'), 414 ... ('B', 'A', 'down'), 415 ... ('B', 'B', 'self'), 416 ... ('B', 'C', 'next'), 417 ... ('C', 'B', 'prev') 418 ... ]) 419 >>> g.getDestination('A', 'up') 420 'B' 421 >>> g.getDestination('A', 'up2') 422 'B' 423 >>> g.getDestination('B', 'self') 424 'B' 425 >>> g.removeEdgesByKey([('A', 'up2'), ('B', 'self')]) 426 >>> g.getDestination('A', 'up2') is None 427 True 428 >>> g.getDestination('B', 'self') is None 429 True 430 """ 431 for source, key in edgeIds: 432 if key in self._byEdge.get(source, {}): 433 self.removeEdgeByKey(source, key) 434 # Otherwise ignore this edge... 435 436 def connections( 437 self, 438 edgeFilter: Optional[ 439 Callable[[Node, Edge, Node, 'UniqueExitsGraph'], bool] 440 ] = None 441 ) -> nx.Graph: 442 """ 443 Returns an undirected graph with the same nodes IDs as the base 444 graph but none of the node or edge attributes. Nodes which have 445 any connection between them in either direction in the original 446 graph will be connected by a single edge in the connections 447 graph. 448 449 If an `edgeFilter` function is provided, it will be given a 450 source node, an edge, a destination node, and the entire graph as 451 arguments. It should return a boolean, and for edges where it 452 returns False, these won't be included in the final graph. Note 453 that because two nodes can be connected by multiple edges in 454 either direction, filtering out a single edge may not sever the 455 connection between two nodes in the final graph. 456 """ 457 result: nx.Graph = nx.Graph() 458 result.add_nodes_from(self) 459 if edgeFilter is None: 460 result.add_edges_from(self.edges(keys=False, data=False)) 461 else: 462 useEdges = [] 463 for (src, dst, edge) in self.edges(keys=True): 464 if edgeFilter(src, edge, dst, self): 465 result.add_edge(src, dst) 466 return result 467 468 def destinationsFrom(self, source: Node) -> Dict[Edge, Node]: 469 """ 470 Given a source node, returns a dictionary mapping the keys of all 471 outgoing edges from that node to their destination nodes. Raises 472 a `KeyError` if the node is not present in the graph. 473 474 Editing the dictionary returned could cause serious problems, so 475 please don't; it will be updated live as the graph is changed. 476 477 ## Example 478 479 >>> g = UniqueExitsGraph() 480 >>> g.add_edges_from([ 481 ... ('A', 'B', 'up'), 482 ... ('A', 'B', 'up2'), 483 ... ('B', 'A', 'down'), 484 ... ('B', 'B', 'self'), 485 ... ('B', 'C', 'next'), 486 ... ('C', 'B', 'prev') 487 ... ]) 488 >>> g.destinationsFrom('A') 489 {'up': 'B', 'up2': 'B'} 490 >>> g.destinationsFrom('B') 491 {'down': 'A', 'self': 'B', 'next': 'C'} 492 >>> g.destinationsFrom('C') 493 {'prev': 'B'} 494 >>> g.destinationsFrom('D') 495 Traceback (most recent call last): 496 ... 497 KeyError... 498 """ 499 return self._byEdge[source] 500 501 def destination(self, source: Node, edge: Edge) -> Node: 502 """ 503 Given a source node and an edge key, looks up and returns the 504 destination node for that edge. Raises a `KeyError` if there is no 505 edge from the specified node with the specified name. 506 507 ## Example 508 509 >>> g = UniqueExitsGraph() 510 >>> g.add_edges_from([ 511 ... ('A', 'B', 'up'), 512 ... ('A', 'B', 'up2'), 513 ... ('B', 'A', 'down'), 514 ... ('B', 'B', 'self'), 515 ... ('B', 'C', 'next'), 516 ... ('C', 'B', 'prev') 517 ... ]) 518 >>> g.destination('A', 'up') 519 'B' 520 >>> g.destination('A', 'up2') 521 'B' 522 >>> g.destination('B', 'down') 523 'A' 524 >>> g.destination('A', 'nonexistent') 525 Traceback (most recent call last): 526 ... 527 KeyError... 528 >>> g.destination('D', 'any') 529 Traceback (most recent call last): 530 ... 531 KeyError... 532 """ 533 return self._byEdge[source][edge] 534 535 def getDestination( 536 self, 537 source: Node, 538 edge: Edge, 539 default: Any = None 540 ) -> Optional[Node]: 541 """ 542 Works like `destination`, but instead of raising a `KeyError` if 543 the node or edge is missing, it returns a default value (with a 544 default default of `None`). 545 546 ## Example 547 548 >>> g = UniqueExitsGraph() 549 >>> g.add_edges_from([ 550 ... ('A', 'B', 'up'), 551 ... ('A', 'B', 'up2'), 552 ... ('B', 'A', 'down'), 553 ... ('B', 'B', 'self'), 554 ... ('B', 'C', 'next'), 555 ... ('C', 'B', 'prev') 556 ... ]) 557 >>> g.getDestination('A', 'up') 558 'B' 559 >>> g.getDestination('A', 'up2') 560 'B' 561 >>> g.getDestination('B', 'down') 562 'A' 563 >>> g.getDestination('A', 'nonexistent') is None 564 True 565 >>> g.getDestination('A', 'nonexistent', 'default') 566 'default' 567 >>> g.getDestination('D', 'any') is None 568 True 569 """ 570 return self._byEdge.get(source, {}).get(edge, default) 571 572 def allEdgesTo( 573 self, 574 destination: Node 575 ) -> List[Tuple[Node, Edge]]: 576 """ 577 Searches the entire graph for edges whose destinations are the 578 specified destination, and returns a list of (node, edge) pairs 579 indicating the source node and edge name for each of those edges. 580 Self-edges are included in this list. 581 582 ## Example 583 584 >>> g = UniqueExitsGraph() 585 >>> g.add_edges_from([ 586 ... ('A', 'B', 'up'), 587 ... ('A', 'B', 'up2'), 588 ... ('B', 'A', 'down'), 589 ... ('B', 'B', 'self'), 590 ... ('B', 'C', 'next'), 591 ... ('C', 'B', 'prev') 592 ... ]) 593 >>> g.allEdgesTo('A') 594 [('B', 'down')] 595 >>> g.allEdgesTo('B') 596 [('A', 'up'), ('A', 'up2'), ('B', 'self'), ('C', 'prev')] 597 >>> g.allEdgesTo('C') 598 [('B', 'next')] 599 >>> g.allEdgesTo('D') 600 [] 601 """ 602 results = [] 603 for node in self: 604 fromThere = self[node] 605 toHere = fromThere.get(destination, {}) 606 for edgeKey in toHere: 607 results.append((node, edgeKey)) 608 609 return results 610 611 def allEdges(self) -> List[Tuple[Node, Node, Edge]]: 612 """ 613 Returns a list of tuples containing source node, destination 614 node, and then edge node, which includes each edge in the graph 615 once. 616 """ 617 # TODO: Fix networkx type annotations 618 return self.edges(keys=True) # type: ignore 619 620 def textMapObj( 621 self, 622 edgeSep: str = '::', 623 external: Optional[Set[Node]] = None, 624 explorationOrder: Optional[Tuple[Node, Sequence[Edge]]] = None, 625 edgeOrders: Union[ 626 Dict[Node, Sequence[Edge]], 627 Dict[Node, Dict[Edge, Any]], 628 None 629 ] = None 630 ): 631 """ 632 Returns a special object which is JSON-serializable and which 633 when serialized creates a semi-human-usable text-format map of 634 the graph. 635 636 The object consists of nested dictionaries, one per node, where 637 keys are node name + edge name strings (combined using the 638 `edgeSep` argument, default is '::'). The value for each key is 639 one of: 640 641 1. Another dictionary representing the node that edge leads 642 to, which can in turn have dictionary values... 643 2. A string naming a destination node that's already represented 644 elsewhere (or naming the current node for self-edges). 645 646 Any node present in the specified `external` set will be linked 647 to instead of listed out, even if it exists in the graph. The 648 `external` set **will be modified** by this function to include 649 all visited nodes in the graph. 650 651 If an `explorationOrder` is provided, it must be a tuple 652 specifying a start node followed by a sequence of edges that 653 indicates the path taken, and the edges will be visited 654 according to that order (this only matters in Python 3.7+ where 655 dictionaries have consistent order). A `ValueError` will be 656 raised if an invalid exploration order is provided. The path 657 list will be ignored if `edgeOrders` is provided explicitly. 658 659 TODO: What about unexplorable graphs (allow node names in place 660 of edge names in exploration order?)?!? 661 662 If `edgeOrders` is provided directly, it will override the 663 path part of the `explorationOrder` to determine the ordering of 664 edges at each node. If not and `explorationOrder` is provided, it 665 will be deduced from the `explorationOrder`. If neither is 666 present, ordering will follow whatever natural order is in the 667 graph, which in most cases should be order-of-creation. 668 669 Notes: 670 - For the format to avoid ambiguity, the `edgeSep` value must be 671 a string which does not appear in any node or edge names. 672 - Nodes and edge values will be converted to strings to build the 673 map. 674 - Node and edge properties are not represented in the resulting 675 object. 676 - For a variety of reasons, the result cannot be converted back 677 to a graph object. This is not intended for use as a JSON 678 serialization route (see the `networkx.readwrite.json_graph` 679 module for some built-in options). 680 - To get a string representation, one could do: 681 `json.dumps(graph.textMapObj())` 682 683 ## Examples 684 685 >>> from exploration import graphs as eg 686 >>> import json 687 >>> g = eg.UniqueExitsGraph() 688 >>> g.add_edges_from([ 689 ... ('A', 'B', 'up'), 690 ... ('A', 'B', 'up2'), 691 ... ('B', 'A', 'down'), 692 ... ('B', 'B', 'self'), 693 ... ('B', 'C', 'next'), 694 ... ('C', 'B', 'prev') 695 ... ]) 696 >>> print(json.dumps(g.textMapObj(), indent=2)) 697 { 698 "A::up": { 699 "B::down": "A", 700 "B::self": "B", 701 "B::next": { 702 "C::prev": "B" 703 } 704 }, 705 "A::up2": "B" 706 } 707 """ 708 # We use `external` as our visited set 709 if external is None: 710 external = set() 711 712 if explorationOrder is not None: 713 here, path = explorationOrder 714 else: 715 # Find first non-external node as our starting node 716 for here in self.nodes: 717 if here not in external: 718 break 719 720 # Path is empty 721 path = [] 722 723 # Determine edge ordering for each node from exploration order 724 # or by natural ordering if no explorationOrder is available 725 if edgeOrders is None: 726 edgeOrders = cast( 727 Dict[Node, Dict[Edge, Any]], 728 {} 729 ) 730 current = here 731 for i in range(len(path)): 732 edge = path[i] 733 # Add this edge next in the ordering for this node 734 orderHere: Dict[Edge, Any] = edgeOrders.setdefault(current, {}) 735 # Note: we use a dictionary here because dictionaries do 736 # preserve insertion ordering (3.7+) and we need to both 737 # keep things in order AND do a bunch of lookups to 738 # avoid duplicates. 739 if edge not in orderHere: 740 orderHere[edge] = True 741 742 # Move to next node 743 if edge not in self._byEdge[current]: 744 raise ValueError( 745 f"Invalid edge in exploration order path: at" 746 f" step {i} we reached node {current} and were" 747 f" supposed to take edge {edge} but that edge" 748 f" does not exist." 749 ) 750 current = self._byEdge[current][edge] 751 752 # Add any unexplored nodes and/or edges in natural order 753 for node in self.nodes: 754 orderHere = edgeOrders.setdefault(node, {}) 755 for edge in self._byEdge[node]: 756 if edge not in orderHere: 757 orderHere[edge] = True 758 759 result = {} 760 external.add(here) 761 # Now loop through keys of this node 762 for key in edgeOrders[here]: 763 combined = str(here) + edgeSep + str(key) 764 dest = self._byEdge[here][key] 765 if dest in external: 766 # links, including self-links 767 result[combined] = str(dest) 768 else: 769 # Recurse 770 result[combined] = self.textMapObj( 771 edgeSep, 772 external, 773 (dest, []), # empty path since we have edgeOrders 774 edgeOrders 775 ) 776 777 return result
A networkx.MultiDiGraph
which has unique-per-source-node names for
each edge. On top of base functionality, this uses some extra memory
to store per-edge outgoing (but not incoming) by-edge dictionaries,
so that you can iterate over edges by their names rather than
iterating over neighbor nodes. This helps in some circumstances where
you know the edge name but not the name of the room it connects to.
This does NOT change the meaning of any of the built-in
networkx.MultiDiGraph
methods, but instead adds new methods for
access to nodes or attributes by node -> edge name.
98 def new_edge_key(self, u: Node, v: Node) -> NoReturn: 99 """ 100 This method would normally be used to generate new edge keys. We 101 disable it, because we want to ensure that all edges are properly 102 labeled. 103 """ 104 raise NotImplementedError( 105 "Attempted to add an edge without specifying a key!" 106 )
This method would normally be used to generate new edge keys. We disable it, because we want to ensure that all edges are properly labeled.
109 def add_node(self, node: Node, **attr: Any): # type:ignore [override] 110 """ 111 See `networkx.MultiDiGraph.add_node`. 112 """ 113 super().add_node(node, **attr) 114 self._byEdge[node] = {} # type Dict[Edge, Node]
See networkx.MultiDiGraph.add_node
.
116 def add_nodes_from( # type:ignore [override] 117 self, 118 nodes: Union[ 119 Iterable[Node], 120 Iterable[Tuple[Node, Dict[Any, Any]]] 121 ], 122 **attr: Any 123 ): 124 """ 125 See `networkx.MultiDiGraph.add_nodes_from`. 126 """ 127 super().add_nodes_from(nodes, **attr) 128 # Reassignment during tuple unpacking is not checkable... 129 n: Any 130 for n in nodes: 131 # Test for hashability & unpack tuple if not 132 try: 133 self._byEdge.get(n) 134 except TypeError: 135 n, _ = n # mypy can't handle this properly 136 self._byEdge[n] = {}
See networkx.MultiDiGraph.add_nodes_from
.
138 def remove_node(self, node: Node): 139 """ 140 See `networkx.MultiDiGraph.remove_node`. 141 """ 142 # Handle deletion from inherited structures 143 super().remove_node(node) 144 145 # Ignore if not present 146 if node not in self._byEdge: 147 return 148 149 # Remove record of outgoing edges 150 del self._byEdge[node] 151 152 # Remove incoming edge records 153 for source, edgeMap in self._byEdge.items(): 154 delete = [] 155 # Find all edges which go to the deleted node 156 # (this is not terribly efficient) 157 for edgeName, dest in edgeMap.items(): 158 if dest == node: 159 delete.append(edgeName) 160 # Delete them in a separate loop, so that we don't 161 # modify-while-iterating (not efficient and maybe 162 # unnecessary?) 163 for d in delete: 164 del edgeMap[d]
See networkx.MultiDiGraph.remove_node
.
166 def remove_nodes_from(self, nodes: Iterable[Node]): 167 """ 168 See `networkx.MultiDiGraph.remove_nodes_from`. 169 """ 170 # First use inherited method to remove from inherited structures 171 super().remove_nodes_from(nodes) 172 # remove our custom info 173 for n in nodes: 174 if n in self._byEdge: 175 del self._byEdge[n] 176 177 for source, edgeMap in self._byEdge.items(): 178 delete = [] 179 # Find all edges that go to any deleted node 180 for edgeName, dest in edgeMap.items(): 181 if dest in nodes: 182 delete.append(edgeName) 183 184 # Remove edges in separate loop to avoid 185 # modifying-while-iterating (not efficient and maybe 186 # unnecessary?) 187 for d in delete: 188 del edgeMap[d]
See networkx.MultiDiGraph.remove_nodes_from
.
224 def add_edges_from( 225 self, 226 ebunch_to_add: Any, 227 # Type should be this, but checker won't pass it: 228 # Union[ 229 # Iterable[Tuple[Node, Node, Edge]], 230 # Iterable[Tuple[Node, Node, Edge, Dict[Any, Any]]] 231 # ], 232 **attr: Any 233 ): 234 """ 235 See `networkx.MultiDiGraph.add_edges_from`. Tuples in the ebunch 236 must be 3- or 4-tuples that include a specific key (not just 237 data). Nodes will be created as necessary. 238 239 Raises a `KeyError` if adding an edge is impossible because it 240 re-uses the same edge name at a particular source node, but if an 241 attempt is made to add an existing edge with the same 242 destination, this will just update the relevant edge attributes. 243 244 Raises a `KeyError` instead of silently updating edge properties 245 if the existing edge was also added by an earlier entry in the 246 `ebunch_to_add` (i.e., if you are trying to add two edges at 247 once that go between the same pair of nodes and use the same 248 edge key). 249 250 >>> from exploration import graphs as eg 251 >>> g = eg.UniqueExitsGraph() 252 >>> g.add_edges_from([ 253 ... ('A', 'B', 'up'), 254 ... ('A', 'B', 'up2'), 255 ... ('B', 'A', 'down'), 256 ... ('B', 'B', 'self'), 257 ... ('B', 'C', 'next'), 258 ... ('C', 'B', 'prev') 259 ... ]) 260 >>> g.nodes 261 NodeView(('A', 'B', 'C')) 262 >>> for edge in g.edges: 263 ... print(edge) 264 ('A', 'B', 'up') 265 ('A', 'B', 'up2') 266 ('B', 'A', 'down') 267 ('B', 'B', 'self') 268 ('B', 'C', 'next') 269 ('C', 'B', 'prev') 270 """ 271 etuple: Any 272 for i, etuple in enumerate(ebunch_to_add): 273 if len(etuple) < 3: 274 raise ValueError( 275 f"Edges to add must contain explicit keys for a" 276 f" UniqueExitsGraph (edge #{i} had only 2 parts)." 277 ) 278 try: 279 hash(etuple[2]) 280 except TypeError: 281 raise ValueError( 282 f"Edges to add must contain explicit keys for a" 283 f" UniqueExitsGraph (edge #{i} had an unhashable 3rd" 284 f" component)." 285 ) 286 287 # Check edge name uniqueness 288 u, v, k = etuple[:3] 289 if u in self._byEdge and self._byEdge[u].get(k) != v: 290 raise KeyError( 291 f"Cannot add or update an edge named '{k}' from node" 292 f" '{u}' to node '{v}' because an edge by that name" 293 f" already exists and goes to a different" 294 f" destination." 295 ) 296 297 # Add edges to inherited structures 298 super().add_edges_from(ebunch_to_add, **attr) 299 300 # Note base implementation calls add_edge, so we don't need to 301 # add edges to our extra structure
See networkx.MultiDiGraph.add_edges_from
. Tuples in the ebunch
must be 3- or 4-tuples that include a specific key (not just
data). Nodes will be created as necessary.
Raises a KeyError
if adding an edge is impossible because it
re-uses the same edge name at a particular source node, but if an
attempt is made to add an existing edge with the same
destination, this will just update the relevant edge attributes.
Raises a KeyError
instead of silently updating edge properties
if the existing edge was also added by an earlier entry in the
ebunch_to_add
(i.e., if you are trying to add two edges at
once that go between the same pair of nodes and use the same
edge key).
>>> from exploration import graphs as eg
>>> g = eg.UniqueExitsGraph()
>>> g.add_edges_from([
... ('A', 'B', 'up'),
... ('A', 'B', 'up2'),
... ('B', 'A', 'down'),
... ('B', 'B', 'self'),
... ('B', 'C', 'next'),
... ('C', 'B', 'prev')
... ])
>>> g.nodes
NodeView(('A', 'B', 'C'))
>>> for edge in g.edges:
... print(edge)
('A', 'B', 'up')
('A', 'B', 'up2')
('B', 'A', 'down')
('B', 'B', 'self')
('B', 'C', 'next')
('C', 'B', 'prev')
318 def remove_edges_from( 319 self, 320 ebunch: Union[ # type:ignore [override] 321 Iterable[Tuple[Node, Node, Edge]], 322 Iterable[Tuple[Node, Node, Edge, Dict[Any, Any]]] 323 ] 324 ): 325 """ 326 See `networkx.MultiDiGraph.remove_edges_from`. Edge tuples in 327 the ebunch must be 3- or 4-tuples that include a key. 328 329 If an edge being removed is not present, it will be ignored. 330 """ 331 if any(len(etuple) not in (3, 4) for etuple in ebunch): 332 raise ValueError( 333 "Edges to remove must be u, v, k 3-tuples or u, v, k, d" 334 " 4-tuples." 335 ) 336 # TODO: Fix networkx MultiDiGraph type stubs 337 super().remove_edges_from(ebunch) # type:ignore [arg-type] 338 # This calls self.remove_edge under the hood so we don't need 339 # extra cleanup steps for _byEdge.
See networkx.MultiDiGraph.remove_edges_from
. Edge tuples in
the ebunch must be 3- or 4-tuples that include a key.
If an edge being removed is not present, it will be ignored.
341 def clear(self) -> None: 342 """ 343 See `networkx.MultiDiGraph.clear`. 344 """ 345 super().clear() 346 self._byEdge.clear()
See networkx.MultiDiGraph.clear
.
348 def clear_edges(self) -> None: 349 """ 350 See `networkx.MultiDiGraph.clear_edges`. 351 """ 352 super().clear_edges() 353 for _, edgeMap in self._byEdge.items(): 354 edgeMap.clear()
See networkx.MultiDiGraph.clear_edges
.
365 def removeEdgeByKey(self, uOfEdge: Node, key: Edge): 366 """ 367 Removes an edge sourced at a particular node that has a 368 particular key, without knowing what the destination is. 369 370 Raises a `KeyError` if the named edge does not exist. 371 372 ## Example 373 374 >>> g = UniqueExitsGraph() 375 >>> g.add_edges_from([ 376 ... ('A', 'B', 'up'), 377 ... ('A', 'B', 'up2'), 378 ... ('B', 'A', 'down'), 379 ... ('B', 'B', 'self'), 380 ... ('B', 'C', 'next'), 381 ... ('C', 'B', 'prev') 382 ... ]) 383 >>> g.getDestination('A', 'up') 384 'B' 385 >>> g.getDestination('A', 'up2') 386 'B' 387 >>> g.getDestination('B', 'self') 388 'B' 389 >>> g.removeEdgeByKey('A', 'up2') 390 >>> g.removeEdgeByKey('B', 'self') 391 >>> g.getDestination('A', 'up2') is None 392 True 393 >>> g.getDestination('B', 'self') is None 394 True 395 """ 396 vOfEdge = self._byEdge[uOfEdge][key] 397 super().remove_edge(uOfEdge, vOfEdge, key) 398 del self._byEdge[uOfEdge][key]
Removes an edge sourced at a particular node that has a particular key, without knowing what the destination is.
Raises a KeyError
if the named edge does not exist.
Example
>>> g = UniqueExitsGraph()
>>> g.add_edges_from([
... ('A', 'B', 'up'),
... ('A', 'B', 'up2'),
... ('B', 'A', 'down'),
... ('B', 'B', 'self'),
... ('B', 'C', 'next'),
... ('C', 'B', 'prev')
... ])
>>> g.getDestination('A', 'up')
'B'
>>> g.getDestination('A', 'up2')
'B'
>>> g.getDestination('B', 'self')
'B'
>>> g.removeEdgeByKey('A', 'up2')
>>> g.removeEdgeByKey('B', 'self')
>>> g.getDestination('A', 'up2') is None
True
>>> g.getDestination('B', 'self') is None
True
400 def removeEdgesByKey(self, edgeIds: Iterable[Tuple[Node, Edge]]): 401 """ 402 Removes multiple edges by source node and key, without needing 403 to know destination nodes. The `edgeIds` argument must be a list 404 of tuples containing source node, edge key pairs. 405 406 Silently ignores already-nonexistent edges. 407 408 ## Example 409 410 >>> g = UniqueExitsGraph() 411 >>> g.add_edges_from([ 412 ... ('A', 'B', 'up'), 413 ... ('A', 'B', 'up2'), 414 ... ('B', 'A', 'down'), 415 ... ('B', 'B', 'self'), 416 ... ('B', 'C', 'next'), 417 ... ('C', 'B', 'prev') 418 ... ]) 419 >>> g.getDestination('A', 'up') 420 'B' 421 >>> g.getDestination('A', 'up2') 422 'B' 423 >>> g.getDestination('B', 'self') 424 'B' 425 >>> g.removeEdgesByKey([('A', 'up2'), ('B', 'self')]) 426 >>> g.getDestination('A', 'up2') is None 427 True 428 >>> g.getDestination('B', 'self') is None 429 True 430 """ 431 for source, key in edgeIds: 432 if key in self._byEdge.get(source, {}): 433 self.removeEdgeByKey(source, key) 434 # Otherwise ignore this edge...
Removes multiple edges by source node and key, without needing
to know destination nodes. The edgeIds
argument must be a list
of tuples containing source node, edge key pairs.
Silently ignores already-nonexistent edges.
Example
>>> g = UniqueExitsGraph()
>>> g.add_edges_from([
... ('A', 'B', 'up'),
... ('A', 'B', 'up2'),
... ('B', 'A', 'down'),
... ('B', 'B', 'self'),
... ('B', 'C', 'next'),
... ('C', 'B', 'prev')
... ])
>>> g.getDestination('A', 'up')
'B'
>>> g.getDestination('A', 'up2')
'B'
>>> g.getDestination('B', 'self')
'B'
>>> g.removeEdgesByKey([('A', 'up2'), ('B', 'self')])
>>> g.getDestination('A', 'up2') is None
True
>>> g.getDestination('B', 'self') is None
True
436 def connections( 437 self, 438 edgeFilter: Optional[ 439 Callable[[Node, Edge, Node, 'UniqueExitsGraph'], bool] 440 ] = None 441 ) -> nx.Graph: 442 """ 443 Returns an undirected graph with the same nodes IDs as the base 444 graph but none of the node or edge attributes. Nodes which have 445 any connection between them in either direction in the original 446 graph will be connected by a single edge in the connections 447 graph. 448 449 If an `edgeFilter` function is provided, it will be given a 450 source node, an edge, a destination node, and the entire graph as 451 arguments. It should return a boolean, and for edges where it 452 returns False, these won't be included in the final graph. Note 453 that because two nodes can be connected by multiple edges in 454 either direction, filtering out a single edge may not sever the 455 connection between two nodes in the final graph. 456 """ 457 result: nx.Graph = nx.Graph() 458 result.add_nodes_from(self) 459 if edgeFilter is None: 460 result.add_edges_from(self.edges(keys=False, data=False)) 461 else: 462 useEdges = [] 463 for (src, dst, edge) in self.edges(keys=True): 464 if edgeFilter(src, edge, dst, self): 465 result.add_edge(src, dst) 466 return result
Returns an undirected graph with the same nodes IDs as the base graph but none of the node or edge attributes. Nodes which have any connection between them in either direction in the original graph will be connected by a single edge in the connections graph.
If an edgeFilter
function is provided, it will be given a
source node, an edge, a destination node, and the entire graph as
arguments. It should return a boolean, and for edges where it
returns False, these won't be included in the final graph. Note
that because two nodes can be connected by multiple edges in
either direction, filtering out a single edge may not sever the
connection between two nodes in the final graph.
468 def destinationsFrom(self, source: Node) -> Dict[Edge, Node]: 469 """ 470 Given a source node, returns a dictionary mapping the keys of all 471 outgoing edges from that node to their destination nodes. Raises 472 a `KeyError` if the node is not present in the graph. 473 474 Editing the dictionary returned could cause serious problems, so 475 please don't; it will be updated live as the graph is changed. 476 477 ## Example 478 479 >>> g = UniqueExitsGraph() 480 >>> g.add_edges_from([ 481 ... ('A', 'B', 'up'), 482 ... ('A', 'B', 'up2'), 483 ... ('B', 'A', 'down'), 484 ... ('B', 'B', 'self'), 485 ... ('B', 'C', 'next'), 486 ... ('C', 'B', 'prev') 487 ... ]) 488 >>> g.destinationsFrom('A') 489 {'up': 'B', 'up2': 'B'} 490 >>> g.destinationsFrom('B') 491 {'down': 'A', 'self': 'B', 'next': 'C'} 492 >>> g.destinationsFrom('C') 493 {'prev': 'B'} 494 >>> g.destinationsFrom('D') 495 Traceback (most recent call last): 496 ... 497 KeyError... 498 """ 499 return self._byEdge[source]
Given a source node, returns a dictionary mapping the keys of all
outgoing edges from that node to their destination nodes. Raises
a KeyError
if the node is not present in the graph.
Editing the dictionary returned could cause serious problems, so please don't; it will be updated live as the graph is changed.
Example
>>> g = UniqueExitsGraph()
>>> g.add_edges_from([
... ('A', 'B', 'up'),
... ('A', 'B', 'up2'),
... ('B', 'A', 'down'),
... ('B', 'B', 'self'),
... ('B', 'C', 'next'),
... ('C', 'B', 'prev')
... ])
>>> g.destinationsFrom('A')
{'up': 'B', 'up2': 'B'}
>>> g.destinationsFrom('B')
{'down': 'A', 'self': 'B', 'next': 'C'}
>>> g.destinationsFrom('C')
{'prev': 'B'}
>>> g.destinationsFrom('D')
Traceback (most recent call last):
...
KeyError...
501 def destination(self, source: Node, edge: Edge) -> Node: 502 """ 503 Given a source node and an edge key, looks up and returns the 504 destination node for that edge. Raises a `KeyError` if there is no 505 edge from the specified node with the specified name. 506 507 ## Example 508 509 >>> g = UniqueExitsGraph() 510 >>> g.add_edges_from([ 511 ... ('A', 'B', 'up'), 512 ... ('A', 'B', 'up2'), 513 ... ('B', 'A', 'down'), 514 ... ('B', 'B', 'self'), 515 ... ('B', 'C', 'next'), 516 ... ('C', 'B', 'prev') 517 ... ]) 518 >>> g.destination('A', 'up') 519 'B' 520 >>> g.destination('A', 'up2') 521 'B' 522 >>> g.destination('B', 'down') 523 'A' 524 >>> g.destination('A', 'nonexistent') 525 Traceback (most recent call last): 526 ... 527 KeyError... 528 >>> g.destination('D', 'any') 529 Traceback (most recent call last): 530 ... 531 KeyError... 532 """ 533 return self._byEdge[source][edge]
Given a source node and an edge key, looks up and returns the
destination node for that edge. Raises a KeyError
if there is no
edge from the specified node with the specified name.
Example
>>> g = UniqueExitsGraph()
>>> g.add_edges_from([
... ('A', 'B', 'up'),
... ('A', 'B', 'up2'),
... ('B', 'A', 'down'),
... ('B', 'B', 'self'),
... ('B', 'C', 'next'),
... ('C', 'B', 'prev')
... ])
>>> g.destination('A', 'up')
'B'
>>> g.destination('A', 'up2')
'B'
>>> g.destination('B', 'down')
'A'
>>> g.destination('A', 'nonexistent')
Traceback (most recent call last):
...
KeyError...
>>> g.destination('D', 'any')
Traceback (most recent call last):
...
KeyError...
535 def getDestination( 536 self, 537 source: Node, 538 edge: Edge, 539 default: Any = None 540 ) -> Optional[Node]: 541 """ 542 Works like `destination`, but instead of raising a `KeyError` if 543 the node or edge is missing, it returns a default value (with a 544 default default of `None`). 545 546 ## Example 547 548 >>> g = UniqueExitsGraph() 549 >>> g.add_edges_from([ 550 ... ('A', 'B', 'up'), 551 ... ('A', 'B', 'up2'), 552 ... ('B', 'A', 'down'), 553 ... ('B', 'B', 'self'), 554 ... ('B', 'C', 'next'), 555 ... ('C', 'B', 'prev') 556 ... ]) 557 >>> g.getDestination('A', 'up') 558 'B' 559 >>> g.getDestination('A', 'up2') 560 'B' 561 >>> g.getDestination('B', 'down') 562 'A' 563 >>> g.getDestination('A', 'nonexistent') is None 564 True 565 >>> g.getDestination('A', 'nonexistent', 'default') 566 'default' 567 >>> g.getDestination('D', 'any') is None 568 True 569 """ 570 return self._byEdge.get(source, {}).get(edge, default)
Works like destination
, but instead of raising a KeyError
if
the node or edge is missing, it returns a default value (with a
default default of None
).
Example
>>> g = UniqueExitsGraph()
>>> g.add_edges_from([
... ('A', 'B', 'up'),
... ('A', 'B', 'up2'),
... ('B', 'A', 'down'),
... ('B', 'B', 'self'),
... ('B', 'C', 'next'),
... ('C', 'B', 'prev')
... ])
>>> g.getDestination('A', 'up')
'B'
>>> g.getDestination('A', 'up2')
'B'
>>> g.getDestination('B', 'down')
'A'
>>> g.getDestination('A', 'nonexistent') is None
True
>>> g.getDestination('A', 'nonexistent', 'default')
'default'
>>> g.getDestination('D', 'any') is None
True
572 def allEdgesTo( 573 self, 574 destination: Node 575 ) -> List[Tuple[Node, Edge]]: 576 """ 577 Searches the entire graph for edges whose destinations are the 578 specified destination, and returns a list of (node, edge) pairs 579 indicating the source node and edge name for each of those edges. 580 Self-edges are included in this list. 581 582 ## Example 583 584 >>> g = UniqueExitsGraph() 585 >>> g.add_edges_from([ 586 ... ('A', 'B', 'up'), 587 ... ('A', 'B', 'up2'), 588 ... ('B', 'A', 'down'), 589 ... ('B', 'B', 'self'), 590 ... ('B', 'C', 'next'), 591 ... ('C', 'B', 'prev') 592 ... ]) 593 >>> g.allEdgesTo('A') 594 [('B', 'down')] 595 >>> g.allEdgesTo('B') 596 [('A', 'up'), ('A', 'up2'), ('B', 'self'), ('C', 'prev')] 597 >>> g.allEdgesTo('C') 598 [('B', 'next')] 599 >>> g.allEdgesTo('D') 600 [] 601 """ 602 results = [] 603 for node in self: 604 fromThere = self[node] 605 toHere = fromThere.get(destination, {}) 606 for edgeKey in toHere: 607 results.append((node, edgeKey)) 608 609 return results
Searches the entire graph for edges whose destinations are the specified destination, and returns a list of (node, edge) pairs indicating the source node and edge name for each of those edges. Self-edges are included in this list.
Example
>>> g = UniqueExitsGraph()
>>> g.add_edges_from([
... ('A', 'B', 'up'),
... ('A', 'B', 'up2'),
... ('B', 'A', 'down'),
... ('B', 'B', 'self'),
... ('B', 'C', 'next'),
... ('C', 'B', 'prev')
... ])
>>> g.allEdgesTo('A')
[('B', 'down')]
>>> g.allEdgesTo('B')
[('A', 'up'), ('A', 'up2'), ('B', 'self'), ('C', 'prev')]
>>> g.allEdgesTo('C')
[('B', 'next')]
>>> g.allEdgesTo('D')
[]
611 def allEdges(self) -> List[Tuple[Node, Node, Edge]]: 612 """ 613 Returns a list of tuples containing source node, destination 614 node, and then edge node, which includes each edge in the graph 615 once. 616 """ 617 # TODO: Fix networkx type annotations 618 return self.edges(keys=True) # type: ignore
Returns a list of tuples containing source node, destination node, and then edge node, which includes each edge in the graph once.
620 def textMapObj( 621 self, 622 edgeSep: str = '::', 623 external: Optional[Set[Node]] = None, 624 explorationOrder: Optional[Tuple[Node, Sequence[Edge]]] = None, 625 edgeOrders: Union[ 626 Dict[Node, Sequence[Edge]], 627 Dict[Node, Dict[Edge, Any]], 628 None 629 ] = None 630 ): 631 """ 632 Returns a special object which is JSON-serializable and which 633 when serialized creates a semi-human-usable text-format map of 634 the graph. 635 636 The object consists of nested dictionaries, one per node, where 637 keys are node name + edge name strings (combined using the 638 `edgeSep` argument, default is '::'). The value for each key is 639 one of: 640 641 1. Another dictionary representing the node that edge leads 642 to, which can in turn have dictionary values... 643 2. A string naming a destination node that's already represented 644 elsewhere (or naming the current node for self-edges). 645 646 Any node present in the specified `external` set will be linked 647 to instead of listed out, even if it exists in the graph. The 648 `external` set **will be modified** by this function to include 649 all visited nodes in the graph. 650 651 If an `explorationOrder` is provided, it must be a tuple 652 specifying a start node followed by a sequence of edges that 653 indicates the path taken, and the edges will be visited 654 according to that order (this only matters in Python 3.7+ where 655 dictionaries have consistent order). A `ValueError` will be 656 raised if an invalid exploration order is provided. The path 657 list will be ignored if `edgeOrders` is provided explicitly. 658 659 TODO: What about unexplorable graphs (allow node names in place 660 of edge names in exploration order?)?!? 661 662 If `edgeOrders` is provided directly, it will override the 663 path part of the `explorationOrder` to determine the ordering of 664 edges at each node. If not and `explorationOrder` is provided, it 665 will be deduced from the `explorationOrder`. If neither is 666 present, ordering will follow whatever natural order is in the 667 graph, which in most cases should be order-of-creation. 668 669 Notes: 670 - For the format to avoid ambiguity, the `edgeSep` value must be 671 a string which does not appear in any node or edge names. 672 - Nodes and edge values will be converted to strings to build the 673 map. 674 - Node and edge properties are not represented in the resulting 675 object. 676 - For a variety of reasons, the result cannot be converted back 677 to a graph object. This is not intended for use as a JSON 678 serialization route (see the `networkx.readwrite.json_graph` 679 module for some built-in options). 680 - To get a string representation, one could do: 681 `json.dumps(graph.textMapObj())` 682 683 ## Examples 684 685 >>> from exploration import graphs as eg 686 >>> import json 687 >>> g = eg.UniqueExitsGraph() 688 >>> g.add_edges_from([ 689 ... ('A', 'B', 'up'), 690 ... ('A', 'B', 'up2'), 691 ... ('B', 'A', 'down'), 692 ... ('B', 'B', 'self'), 693 ... ('B', 'C', 'next'), 694 ... ('C', 'B', 'prev') 695 ... ]) 696 >>> print(json.dumps(g.textMapObj(), indent=2)) 697 { 698 "A::up": { 699 "B::down": "A", 700 "B::self": "B", 701 "B::next": { 702 "C::prev": "B" 703 } 704 }, 705 "A::up2": "B" 706 } 707 """ 708 # We use `external` as our visited set 709 if external is None: 710 external = set() 711 712 if explorationOrder is not None: 713 here, path = explorationOrder 714 else: 715 # Find first non-external node as our starting node 716 for here in self.nodes: 717 if here not in external: 718 break 719 720 # Path is empty 721 path = [] 722 723 # Determine edge ordering for each node from exploration order 724 # or by natural ordering if no explorationOrder is available 725 if edgeOrders is None: 726 edgeOrders = cast( 727 Dict[Node, Dict[Edge, Any]], 728 {} 729 ) 730 current = here 731 for i in range(len(path)): 732 edge = path[i] 733 # Add this edge next in the ordering for this node 734 orderHere: Dict[Edge, Any] = edgeOrders.setdefault(current, {}) 735 # Note: we use a dictionary here because dictionaries do 736 # preserve insertion ordering (3.7+) and we need to both 737 # keep things in order AND do a bunch of lookups to 738 # avoid duplicates. 739 if edge not in orderHere: 740 orderHere[edge] = True 741 742 # Move to next node 743 if edge not in self._byEdge[current]: 744 raise ValueError( 745 f"Invalid edge in exploration order path: at" 746 f" step {i} we reached node {current} and were" 747 f" supposed to take edge {edge} but that edge" 748 f" does not exist." 749 ) 750 current = self._byEdge[current][edge] 751 752 # Add any unexplored nodes and/or edges in natural order 753 for node in self.nodes: 754 orderHere = edgeOrders.setdefault(node, {}) 755 for edge in self._byEdge[node]: 756 if edge not in orderHere: 757 orderHere[edge] = True 758 759 result = {} 760 external.add(here) 761 # Now loop through keys of this node 762 for key in edgeOrders[here]: 763 combined = str(here) + edgeSep + str(key) 764 dest = self._byEdge[here][key] 765 if dest in external: 766 # links, including self-links 767 result[combined] = str(dest) 768 else: 769 # Recurse 770 result[combined] = self.textMapObj( 771 edgeSep, 772 external, 773 (dest, []), # empty path since we have edgeOrders 774 edgeOrders 775 ) 776 777 return result
Returns a special object which is JSON-serializable and which when serialized creates a semi-human-usable text-format map of the graph.
The object consists of nested dictionaries, one per node, where
keys are node name + edge name strings (combined using the
edgeSep
argument, default is '::'). The value for each key is
one of:
- Another dictionary representing the node that edge leads to, which can in turn have dictionary values...
- A string naming a destination node that's already represented elsewhere (or naming the current node for self-edges).
Any node present in the specified external
set will be linked
to instead of listed out, even if it exists in the graph. The
external
set will be modified by this function to include
all visited nodes in the graph.
If an explorationOrder
is provided, it must be a tuple
specifying a start node followed by a sequence of edges that
indicates the path taken, and the edges will be visited
according to that order (this only matters in Python 3.7+ where
dictionaries have consistent order). A ValueError
will be
raised if an invalid exploration order is provided. The path
list will be ignored if edgeOrders
is provided explicitly.
TODO: What about unexplorable graphs (allow node names in place of edge names in exploration order?)?!?
If edgeOrders
is provided directly, it will override the
path part of the explorationOrder
to determine the ordering of
edges at each node. If not and explorationOrder
is provided, it
will be deduced from the explorationOrder
. If neither is
present, ordering will follow whatever natural order is in the
graph, which in most cases should be order-of-creation.
Notes:
- For the format to avoid ambiguity, the
edgeSep
value must be a string which does not appear in any node or edge names. - Nodes and edge values will be converted to strings to build the map.
- Node and edge properties are not represented in the resulting object.
- For a variety of reasons, the result cannot be converted back
to a graph object. This is not intended for use as a JSON
serialization route (see the
networkx.readwrite.json_graph
module for some built-in options). - To get a string representation, one could do:
json.dumps(graph.textMapObj())
Examples
>>> from exploration import graphs as eg
>>> import json
>>> g = eg.UniqueExitsGraph()
>>> g.add_edges_from([
... ('A', 'B', 'up'),
... ('A', 'B', 'up2'),
... ('B', 'A', 'down'),
... ('B', 'B', 'self'),
... ('B', 'C', 'next'),
... ('C', 'B', 'prev')
... ])
>>> print(json.dumps(g.textMapObj(), indent=2))
{
"A::up": {
"B::down": "A",
"B::self": "B",
"B::next": {
"C::prev": "B"
}
},
"A::up2": "B"
}
Inherited Members
- networkx.classes.multidigraph.MultiDiGraph
- MultiDiGraph
- edge_key_dict_factory
- adj
- succ
- pred
- add_edge
- remove_edge
- edges
- out_edges
- in_edges
- degree
- in_degree
- out_degree
- is_multigraph
- is_directed
- to_undirected
- reverse
- networkx.classes.multigraph.MultiGraph
- to_directed_class
- to_undirected_class
- has_edge
- get_edge_data
- copy
- to_directed
- number_of_edges
- networkx.classes.digraph.DiGraph
- graph
- has_successor
- has_predecessor
- successors
- neighbors
- predecessors
- networkx.classes.graph.Graph
- node_dict_factory
- node_attr_dict_factory
- adjlist_outer_dict_factory
- adjlist_inner_dict_factory
- edge_attr_dict_factory
- graph_attr_dict_factory
- name
- nodes
- number_of_nodes
- order
- has_node
- add_weighted_edges_from
- update
- adjacency
- subgraph
- edge_subgraph
- size
- nbunch_iter