From 04ea7199ac2dcedb55017ddd3abba9c301d235e9 Mon Sep 17 00:00:00 2001 From: HeWeMel <69761336+HeWeMel@users.noreply.github.com> Date: Thu, 25 Jul 2024 15:24:39 +0200 Subject: [PATCH] v3.4.0 --- .../workflows/continuous-integration-pip.yml | 30 +- README.rst | 24 +- docs/source/api.rst | 22 + docs/source/bidirectional_search.rst | 4 +- docs/source/changelog.rst | 38 + docs/source/concept_and_examples.rst | 438 ++- docs/source/conf.py | 6 +- docs/source/gears.rst | 14 +- docs/source/graphs_and_adaptation.rst | 102 +- docs/source/index.rst | 26 +- docs/source/reduction_of_other_problems.rst | 511 ++- docs/source/search_aware_graphs.rst | 352 ++ docs/source/traversals.rst | 206 +- docs/source/vertex_identity.rst | 10 +- pyproject.toml | 50 +- requirements_ci.txt | 7 +- requirements_dev.txt | 7 +- setup.cfg | 7 + setup.py | 50 - src/nographs/__init__.py | 38 +- src/nographs/_extra_edge_gadgets.py | 2 +- src/nographs/_extra_matrix_gadgets.py | 2 +- src/nographs/_extra_tsp.py | 7 +- src/nographs/_gears.py | 94 +- src/nographs/_paths.py | 50 +- src/nographs/_strategies/__init__.py | 100 + .../bidirectional_search/__init__.py | 0 .../_strategies/bidirectional_search/base.py | 95 + .../bidirectional_search/breadth_first.py | 399 ++ .../bidirectional_search/shortest_path.py} | 514 +-- src/nographs/_strategies/strategy.py | 56 + .../_strategies/traversals/__init__.py | 0 .../_strategies/traversals/traversal.py | 329 ++ .../traversals/with_weights/__init__.py | 1 + .../traversals/with_weights/a_star.py | 489 +++ .../with_weights/extra_infinite_branching.py} | 42 +- .../with_weights/minimum_spanning_tree.py | 411 ++ .../traversals/with_weights/shortest_paths.py | 428 ++ .../with_weights/traversal_with_weights.py | 175 + .../traversals/without_weights/__init__.py | 1 + .../without_weights/breadth_first.py | 443 +++ .../traversals/without_weights/depth_first.py | 1500 +++++++ .../without_weights/neighbors_then_depth.py | 404 ++ .../without_weights/topological_sort.py | 706 ++++ .../traversal_without_weights.py | 163 + src/nographs/_strategies/type_aliases.py | 107 + .../{_strategies.py => _strategies/utils.py} | 211 +- src/nographs/_traversals.py | 3498 ----------------- tests/extra_test_tsp_performance.py | 34 +- tests/test_expanded_template.py | 62 + tests/test_extra_tsp.py | 8 +- tests/test_gears.py | 10 +- tests/test_nographs.py | 58 +- tests/test_paths.py | 2 +- tests/test_strategies.py | 11 +- tests/test_traversals_and_searches.py | 1936 +++++++-- tests/test_unit_typed.py | 48 +- tests/utils.py | 52 + tpl/!LICENSE | 27 + tpl/base_lib.py | 23 + tpl/make_insert_look_defined.py | 20 + .../nographs/_strategies/MCalculationLimit.py | 45 + tpl/src/nographs/_strategies/MStrategy.py | 32 + .../nographs/_strategies/MVertexMapping.py | 98 + .../_strategies/MVertexMappingExpectNone.py | 104 + tpl/src/nographs/_strategies/MVertexSet.py | 466 +++ .../traversals/with_weights/!a_star.py | 324 ++ .../with_weights/!minimum_spanning_tree.py | 196 + .../with_weights/!shortest_paths.py | 257 ++ .../with_weights/MTraversalWithWeights.py | 141 + .../with_weights/cls_traversal/doc_input.rst | 2 + .../with_weights/cls_traversal/doc_start.rst | 12 + .../code_init_distances_and_zero.py | 11 + .../method_start_from/code_start.py | 17 + .../method_start_from/doc_std.rst | 11 + .../method_start_from/signature_standard.py | 5 + .../method_traverse/code_start.py | 11 + .../without_weights/!breadth_first.py | 209 + .../without_weights/!depth_first.py | 1034 +++++ .../without_weights/!neighbors_then_depth.py | 168 + .../without_weights/!topological_sort.py | 323 ++ .../MTraversalWithoutWeights.py | 164 + .../cls_traversal/doc_input.rst | 2 + .../cls_traversal/doc_start.rst | 20 + .../method_start_from/code_start.py | 9 + .../doc_already_visited_compatible.txt | 14 + .../doc_already_visited_std.txt | 7 + .../method_start_from/doc_compute_depth.txt | 4 + .../method_start_from/doc_end.rst | 2 + .../method_start_from/doc_start.rst | 14 + .../method_start_from/signature.py | 6 + .../method_traverse/code_edges_loop.py | 0 .../code_prepare_edges_loop.py | 15 + .../method_traverse/code_start.py | 21 + .../code_start_with_tree_and_visited.py | 10 + 95 files changed, 13289 insertions(+), 4925 deletions(-) create mode 100644 docs/source/search_aware_graphs.rst create mode 100644 setup.cfg delete mode 100644 setup.py create mode 100644 src/nographs/_strategies/__init__.py create mode 100644 src/nographs/_strategies/bidirectional_search/__init__.py create mode 100644 src/nographs/_strategies/bidirectional_search/base.py create mode 100644 src/nographs/_strategies/bidirectional_search/breadth_first.py rename src/nographs/{_bidir_search.py => _strategies/bidirectional_search/shortest_path.py} (55%) create mode 100644 src/nographs/_strategies/strategy.py create mode 100644 src/nographs/_strategies/traversals/__init__.py create mode 100644 src/nographs/_strategies/traversals/traversal.py create mode 100644 src/nographs/_strategies/traversals/with_weights/__init__.py create mode 100644 src/nographs/_strategies/traversals/with_weights/a_star.py rename src/nographs/{_extra_infinite_branching.py => _strategies/traversals/with_weights/extra_infinite_branching.py} (96%) create mode 100644 src/nographs/_strategies/traversals/with_weights/minimum_spanning_tree.py create mode 100644 src/nographs/_strategies/traversals/with_weights/shortest_paths.py create mode 100644 src/nographs/_strategies/traversals/with_weights/traversal_with_weights.py create mode 100644 src/nographs/_strategies/traversals/without_weights/__init__.py create mode 100644 src/nographs/_strategies/traversals/without_weights/breadth_first.py create mode 100644 src/nographs/_strategies/traversals/without_weights/depth_first.py create mode 100644 src/nographs/_strategies/traversals/without_weights/neighbors_then_depth.py create mode 100644 src/nographs/_strategies/traversals/without_weights/topological_sort.py create mode 100644 src/nographs/_strategies/traversals/without_weights/traversal_without_weights.py create mode 100644 src/nographs/_strategies/type_aliases.py rename src/nographs/{_strategies.py => _strategies/utils.py} (56%) delete mode 100644 src/nographs/_traversals.py create mode 100644 tests/test_expanded_template.py create mode 100644 tests/utils.py create mode 100644 tpl/!LICENSE create mode 100644 tpl/base_lib.py create mode 100644 tpl/make_insert_look_defined.py create mode 100644 tpl/src/nographs/_strategies/MCalculationLimit.py create mode 100644 tpl/src/nographs/_strategies/MStrategy.py create mode 100644 tpl/src/nographs/_strategies/MVertexMapping.py create mode 100644 tpl/src/nographs/_strategies/MVertexMappingExpectNone.py create mode 100644 tpl/src/nographs/_strategies/MVertexSet.py create mode 100644 tpl/src/nographs/_strategies/traversals/with_weights/!a_star.py create mode 100644 tpl/src/nographs/_strategies/traversals/with_weights/!minimum_spanning_tree.py create mode 100644 tpl/src/nographs/_strategies/traversals/with_weights/!shortest_paths.py create mode 100644 tpl/src/nographs/_strategies/traversals/with_weights/MTraversalWithWeights.py create mode 100644 tpl/src/nographs/_strategies/traversals/with_weights/cls_traversal/doc_input.rst create mode 100644 tpl/src/nographs/_strategies/traversals/with_weights/cls_traversal/doc_start.rst create mode 100644 tpl/src/nographs/_strategies/traversals/with_weights/method_start_from/code_init_distances_and_zero.py create mode 100644 tpl/src/nographs/_strategies/traversals/with_weights/method_start_from/code_start.py create mode 100644 tpl/src/nographs/_strategies/traversals/with_weights/method_start_from/doc_std.rst create mode 100644 tpl/src/nographs/_strategies/traversals/with_weights/method_start_from/signature_standard.py create mode 100644 tpl/src/nographs/_strategies/traversals/with_weights/method_traverse/code_start.py create mode 100644 tpl/src/nographs/_strategies/traversals/without_weights/!breadth_first.py create mode 100644 tpl/src/nographs/_strategies/traversals/without_weights/!depth_first.py create mode 100644 tpl/src/nographs/_strategies/traversals/without_weights/!neighbors_then_depth.py create mode 100644 tpl/src/nographs/_strategies/traversals/without_weights/!topological_sort.py create mode 100644 tpl/src/nographs/_strategies/traversals/without_weights/MTraversalWithoutWeights.py create mode 100644 tpl/src/nographs/_strategies/traversals/without_weights/cls_traversal/doc_input.rst create mode 100644 tpl/src/nographs/_strategies/traversals/without_weights/cls_traversal/doc_start.rst create mode 100644 tpl/src/nographs/_strategies/traversals/without_weights/method_start_from/code_start.py create mode 100644 tpl/src/nographs/_strategies/traversals/without_weights/method_start_from/doc_already_visited_compatible.txt create mode 100644 tpl/src/nographs/_strategies/traversals/without_weights/method_start_from/doc_already_visited_std.txt create mode 100644 tpl/src/nographs/_strategies/traversals/without_weights/method_start_from/doc_compute_depth.txt create mode 100644 tpl/src/nographs/_strategies/traversals/without_weights/method_start_from/doc_end.rst create mode 100644 tpl/src/nographs/_strategies/traversals/without_weights/method_start_from/doc_start.rst create mode 100644 tpl/src/nographs/_strategies/traversals/without_weights/method_start_from/signature.py create mode 100644 tpl/src/nographs/_strategies/traversals/without_weights/method_traverse/code_edges_loop.py create mode 100644 tpl/src/nographs/_strategies/traversals/without_weights/method_traverse/code_prepare_edges_loop.py create mode 100644 tpl/src/nographs/_strategies/traversals/without_weights/method_traverse/code_start.py create mode 100644 tpl/src/nographs/_strategies/traversals/without_weights/method_traverse/code_start_with_tree_and_visited.py diff --git a/.github/workflows/continuous-integration-pip.yml b/.github/workflows/continuous-integration-pip.yml index 71bae2c..e3c9089 100644 --- a/.github/workflows/continuous-integration-pip.yml +++ b/.github/workflows/continuous-integration-pip.yml @@ -26,18 +26,17 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 # Override language selection with: languages: python - setup-python-dependencies: false - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 test-pip: needs: skip_duplicate @@ -48,9 +47,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -59,29 +58,32 @@ jobs: python -m pip install --upgrade -r requirements_ci.txt - name: Lint all python files with flake8 run: | - # stop the build if there are flake8 errors (E203,W505 not compatible to black) - flake8 src tests --count --extend-ignore=E203,W503 --max-line-length=88 --show-source --statistics + # stop the build if there are flake8 errors + flake8 src tests --count --show-source --statistics - name: Type check package source with mypy run: | mypy src/nographs - name: Check source code formatting with Black run: | - black src/nographs --check --verbose --diff + black src/nographs tests --check --verbose --diff - name: Install from source (required for the pre-commit tests) run: python -m pip install . - name: Run all tests (including doc tests) run: python ./tests/test_nographs.py - name: Upload coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 + with: + fail_ci_if_error: true + token: ${{ secrets.CODECOV_TOKEN }} build: name: Build runs-on: ubuntu-latest steps: - name: Checkout source - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python 3.9 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.9 - name: Build package @@ -99,9 +101,9 @@ jobs: # runs-on: ubuntu-latest # steps: # - name: Checkout source -# uses: actions/checkout@v3 +# uses: actions/checkout@v4 # - name: Set up Python 3.9 -# uses: actions/setup-python@v4 +# uses: actions/setup-python@v5 # with: # python-version: 3.9 # - name: Build package diff --git a/README.rst b/README.rst index 3b61fd6..209c624 100644 --- a/README.rst +++ b/README.rst @@ -62,10 +62,14 @@ Think of it as *graph analysis - the lazy (evaluation) way*. - Unidirectional traversal algorithms: DFS, BFS, topological search, Dijkstra, A\* and MST. - Bidirectional search algorithms: BFS and Dijkstra. -- Results: Reachability, depth, distance, paths and trees. +- Results: Reachability, depth, distance, and paths. Paths can be calculated with vertices, edges, or attributed edges, and can be iterated in both directions. + Additionally, for DFS: + forest, all kinds of edge types, both entering and leaving events, + and DFS tree edges or + all paths or all walks. - Flexible graph notion: - Infinite directed multigraphs with loops and @@ -119,14 +123,26 @@ Think of it as *graph analysis - the lazy (evaluation) way*. positive / zero / negative edge weights, graph does not need to be complete) - Dijkstra shortest paths algorithm for infinitely branching graphs with locally sorted edges. -- Example for computing the - longest path - between two vertices in a weighted, acyclic graph. - Gadget functions for test purposes. They make the easy task of adapting existing explicit test graphs a no brainer, may they be stored in edge indices or edge iterables or in arrays. +**Examples with further algorithms** + +- Depth-limited search +- Iterative deepening depth-first search +- Critical path + in a weighted, acyclic graph +- Longest path + between two vertices in a weighted, acyclic graph +- Longest path + between two vertices in a weighted graph or in an unweighted graph +- Strongly connected components + of a graph +- Biconnected components of a connected undirected graph + + **Example** Our graph is directed, weighted and has infinitely many edges. These edges are diff --git a/docs/source/api.rst b/docs/source/api.rst index b00178a..72bd32d 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -433,6 +433,14 @@ the comparing examples `here `, and TraversalDepthFirst +++++++++++++++++++ +**Enumerations used in the definition of parameters:** + +.. autoclass:: DFSEvent + +.. autoclass:: DFSMode + +**About the class:** + Examples: See `example-traversal-depth-first-integers` and the comparing examples `here `. @@ -445,8 +453,22 @@ the comparing examples `here `. .. autoattribute:: visited + .. autoattribute:: event + + .. autoattribute:: trace + + .. autoattribute:: trace_labels + + .. autoattribute:: on_trace + + .. autoattribute:: index + .. automethod:: start_from + .. automethod:: __iter__ + + .. automethod:: skip_expanding_entered_vertex + .. autoclass:: TraversalDepthFirst :show-inheritance: yes diff --git a/docs/source/bidirectional_search.rst b/docs/source/bidirectional_search.rst index 86ecede..7fdd662 100644 --- a/docs/source/bidirectional_search.rst +++ b/docs/source/bidirectional_search.rst @@ -5,9 +5,7 @@ Bidirectional search algorithms Import nographs for doctests of this document. Does not go into docs. >>> import nographs as nog -.. versionchanged:: 3.1 - - Bidirectional search algorithms added. +.. versionadded:: 3.1 The analysis algorithms `presented so far ` traverse a graph in "forward" direction, i.e., they start from one or more start vertices, follow the diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 80e4111..ed0902e 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -1,6 +1,44 @@ ChangeLog --------- +**v3.4.0** (2024-07-25) + +- Method TraversalDepthsFirst.start_from: New parameters: + + - report: Instead of just ENTERING_SUCCESSOR, many + different events can be chosen to be reported. + - mode: Two new traversal modes can be chosen, ALL_PATHS and ALL_WALKS. + - compute_trace: Maintains the list of the vertices on the trace, + the current path from a start vertex to the current vertex. + - compute_on_trace: Maintains the set of the vertices on the trace. + - compute_index: Numbers vertices in DFS pre-order. + +- Class TraversalDepthsFirst: + + - Start vertices are evaluated successively. This enables a direct + computation of the DFS forest. + - Attribute __iter__ is now a generator instead of just an iterator, + and throwing a *StopIteration* + signals to the generator to skip the vertex that has just be entered + and reported. + +- Methods *start_from* of traversals: Argument for parameter *start_vertices* + is traversed at most once. Thus, it can be an Iterator or a Generator. + +- Tutorial: Further examples added: + + - depth-limited search + - iterative deepening depth-first search (IDDFS) + - longest path between two vertices in a weighted graph or in an + unweighted graph + - strongly connected components of a graph + - biconnected components of a connected undirected graph (Tarjan). + +- Code quality improved: Code structure improved. + Source code macro system used to improve code consistency. + +- pyproject.toml instead of setup.py + **v3.3.2** (2024-02-04) - Added method paths.predecessor diff --git a/docs/source/concept_and_examples.rst b/docs/source/concept_and_examples.rst index 0e9a4af..2077a14 100644 --- a/docs/source/concept_and_examples.rst +++ b/docs/source/concept_and_examples.rst @@ -164,7 +164,7 @@ number of edges. We use the *TraversalBreadthFirst* strategy of NoGraphs (see `Traversal algorithms `). -It implements the *Breadth First Search* graph algorithm in the NoGraphs style. +It implements the graph algorithm **breadth-first search** in the NoGraphs style. .. code-block:: python @@ -205,7 +205,7 @@ Breadth First Search for the Towers of Hanoi ............................................ We play *Towers of Hanoi* -(see https://en.wikipedia.org/wiki/Tower_of_Hanoi). +(see `Wikipedia `_). We model a tower as a tuple of the sizes of its "disks", sorted in ascending order. We decide that a vertex (state in the game) is a tuple of such towers. During the game, from one state to the other, we choose a tower, take its smallest disk, @@ -286,7 +286,7 @@ We choose the integers as our vertices. The (only) successor of a vertex *i* is >>> def next_vertices(i, _): ... return i+2, -We check that 20000000 (20 million) can be reached from 0. This means, that the number +We check that 20,000,000 **can be reached** from 0. This means, that the number is even. There might be easier ways to find that out... :-) We use the *TraversalDepthFirst* strategy of NoGraphs (see @@ -296,33 +296,33 @@ We use the *TraversalDepthFirst* strategy of NoGraphs (see .. code-block:: python >>> traversal = nog.TraversalDepthFirst(next_vertices, is_tree=True) - >>> traversal.start_from(0).go_to(20000000) + >>> traversal.start_from(0).go_to(20_000_000) #doctest:+SLOW_TEST 20000000 Now, we choose some odd number and try to **check that it cannot be reached**. Here are two examples for techniques we can use to to that: -In the first case, we use a *sentinel vertex*, here 20000002, together with +In the first case, we use a *sentinel vertex*, here 20,000,002, together with our goal vertex. When the sentinel vertex is reached, we know by the structure -or our graph, that our goal vertex 20000001 - a lower number - will not be +or our graph, that our goal vertex 20,000,001 - a lower number - will not be reached anymore. .. code-block:: python - >>> next(traversal.start_from(0).go_for_vertices_in( (20000001, 20000002) )) #doctest:+SKIP + >>> next(traversal.start_from(0).go_for_vertices_in( (20_000_001, 20_000_002) )) #doctest:+SLOW_TEST 20000002 In the second case, we define an *upper limit for the number of allowed calculation steps*, i.e., a maximal number of vertices to be read in from the graph. -We choose a limit, here 10000001, that is surely high enough to reach the goal +We choose a limit, here 10,000,001, that is surely high enough to reach the goal vertex, if it is reachable, but prevents an unnecessarily high run time or, like in our case, even an infinite run time, if it is not reachable. .. code-block:: python - >>> traversal.start_from(0, calculation_limit=10000001).go_to(20000001) #doctest:+SKIP + >>> traversal.start_from(0, calculation_limit=10_000_001).go_to(20_000_001) #doctest:+SKIP Traceback (most recent call last): RuntimeError: Number of visited vertices reached limit @@ -336,10 +336,333 @@ Typically, it is faster than TraversalDepthFirst and needs less memory. .. code-block:: python >>> traversal = nog.TraversalNeighborsThenDepth(next_vertices, is_tree=True) - >>> traversal.start_from(0).go_to(20000000) #doctest:+SKIP + >>> traversal.start_from(0).go_to(20_000_000) #doctest:+SKIP 20000000 +.. _dfs_forest_edges: + +DFS forest: edges, predecessor relation, and paths +.................................................. + +.. versionadded:: 3.4 + +In this example, we choose the integers 1...9 as vertices of our finite graph. +The successors *w* of a vertex *i* are *i-2* and *i+4*, if they are valid +vertices. + +.. code-block:: python + + >>> vertices = range(1, 10) + >>> def next_vertices(v, _): + ... for w in [v - 2, v + 4]: + ... if w in vertices: + ... yield w + +The edges of this graph are the following: + +.. code-block:: python + + >>> [(v, w) for v in vertices for w in next_vertices(v, None)] # doctest: +NORMALIZE_WHITESPACE + [(1, 5), (2, 6), (3, 1), (3, 7), (4, 2), (4, 8), (5, 3), (5, 9), (6, 4), + (7, 5), (8, 6), (9, 7)] + +We want to compute a *DFS forest* (set of depth-first search trees) covering all +vertices, store the forest in form of its predecessor relation, and +list the edges of the forest. + +We use the *TraversalDepthFirst* strategy of NoGraphs (see +`Traversal algorithms `) to traverse the edges of a DFS-tree of the graph. +We tell the traversal to generate paths leading from start vertices to the traversed +vertices, following the edges of the DFS-tree: Then, the +**paths container permanently stores the generated predecessor relation** +**of the DFS-forest** for us. + +.. code-block:: python + + >>> traversal = nog.TraversalDepthFirst(next_vertices) + >>> reported_vertices = list( + ... traversal.start_from(start_vertices=vertices, build_paths=True)) + +Now, for all vertices that has been reported as end vertex of an edge of the +DFS forest, we list the edge consisting of the predecessor and the vertex. + +.. code-block:: python + + >>> list((traversal.paths.predecessor(v), v) for v in reported_vertices) + [(1, 5), (5, 9), (9, 7), (5, 3), (2, 6), (6, 4), (4, 8)] + +1 and 2 have no predecessors, +because they have not occurred as end vertex of an edge of the DFS forest, as +they are the root vertices of the two DFS-trees of the DFS forest. + +.. code-block:: python + + >>> list((traversal.paths.predecessor(v), v) for v in [1, 2]) + [(None, 1), (None, 2)] + +The edges listed above really form a DFS-forest: Starting from one of the roots, +we can reach all other vertices of the graph by following the computed edges, and +there are no two edges ending at the same vertex. + +.. note:: + + While the start vertices are iterated in the order in which they were + indicated (1 before 2 - this is guaranted), + currently, + the edges computed by *next_vertices* are processed in reversed order + (e.g., from vertex 5, first the *v+4* edge to 9 is traversed, and later the + *v-2* edge to 3), as it is typical for non-recursive depth-first traversal + algorithms. + + The **order of processing the successors of a vertex is an implementation** + **detail that can change anytime without prior notice**, + as it is not part of the specification of the search strategies. + +As the predecessor relation of the DFS-trees is kept in the paths object, we can also +ask later on for the predecessor of a vertex. And we can ask for a +path that leads from a start vertex to a given vertex using the edges of the DFS-trees: + +.. code-block:: python + + >>> traversal.paths.predecessor(3) + 5 + >>> traversal.paths[3] + (1, 5, 3) + + +.. _dfs_forest: + +DFS forest: events, edge types, and successor relation +...................................................... + +.. versionadded:: 3.4 + +We use the same graph as in the example before. + +This time, we store the DFS-forest in form of a dictionary of the +**successor relation** +of the trees. For this, we demand that the traversal generates / updates +the *trace*, i.e., the path that leads from a start vertex to the current vertex +following the edges of the DFS forest. +We use the current trace to determine the predecessor of the current vertex. + +.. code-block:: python + + >>> from collections import defaultdict + >>> _ = traversal.start_from(start_vertices=vertices, compute_trace=True) + >>> successors = defaultdict(list) + >>> for v in traversal: + ... predecessor = traversal.trace[-2] + ... successors[predecessor].append(v) + >>> print(successors) + defaultdict(, {1: [5], 5: [9, 3], 9: [7], 2: [6], 6: [4], 4: [8]}) + +Next, we like to **see each step of the traversal of the DFS-forest in detail**. +The following cases (*events*, see `here `) may occur: + +- ENTERING SUCCESSOR: + An edge of the DFS-forest is followed, from a vertex to a successor, and the + successor is entered. +- LEAVING_SUCCESSOR: + The successor, that an edge of the DFS-forest leads to, is left and the edge is + traversed in the opposite direction in the sense of a backtracking. +- BACK_EDGE, CROSS_EDGE, or FORWARD_EDGE: + An edge has been found, that does not belong to the DFS-forest. The traversal + does not follow such edges and does not enter the vertex it leads to. + There are different kinds of such edges. The example shows two of them. +- ENTERING_START, LEAVING_START, SKIPPING_START: + A start vertex is entered or left. Or it is skipped, because it has already been + visited as successor of some other vertex. + +We tell TraversalDepthFirst, that we like to be informed about all these +kinds of events. When an event is reported, we print it together with up to the +last two vertices of the trace. + +.. code-block:: python + + >>> traversal = nog.TraversalDepthFirst(next_vertices) + >>> _ = traversal.start_from(start_vertices=vertices, compute_trace=True, + ... report=nog.DFSEvent.ALL) + >>> for v in traversal: + ... print(traversal.event, traversal.trace[-2:]) + DFSEvent.ENTERING_START [1] + DFSEvent.ENTERING_SUCCESSOR [1, 5] + DFSEvent.ENTERING_SUCCESSOR [5, 9] + DFSEvent.ENTERING_SUCCESSOR [9, 7] + DFSEvent.BACK_EDGE [7, 5] + DFSEvent.LEAVING_SUCCESSOR [9, 7] + DFSEvent.LEAVING_SUCCESSOR [5, 9] + DFSEvent.ENTERING_SUCCESSOR [5, 3] + DFSEvent.CROSS_EDGE [3, 7] + DFSEvent.BACK_EDGE [3, 1] + DFSEvent.LEAVING_SUCCESSOR [5, 3] + DFSEvent.LEAVING_SUCCESSOR [1, 5] + DFSEvent.LEAVING_START [1] + DFSEvent.ENTERING_START [2] + DFSEvent.ENTERING_SUCCESSOR [2, 6] + DFSEvent.ENTERING_SUCCESSOR [6, 4] + DFSEvent.ENTERING_SUCCESSOR [4, 8] + DFSEvent.BACK_EDGE [8, 6] + DFSEvent.LEAVING_SUCCESSOR [4, 8] + DFSEvent.BACK_EDGE [4, 2] + DFSEvent.LEAVING_SUCCESSOR [6, 4] + DFSEvent.LEAVING_SUCCESSOR [2, 6] + DFSEvent.LEAVING_START [2] + DFSEvent.SKIPPING_START [3] + DFSEvent.SKIPPING_START [4] + DFSEvent.SKIPPING_START [5] + DFSEvent.SKIPPING_START [6] + DFSEvent.SKIPPING_START [7] + DFSEvent.SKIPPING_START [8] + DFSEvent.SKIPPING_START [9] + +Note, that the event *ENTERING_START* gives us the *roots of the DFS-trees* in our +DFS-forest. + +.. tip:: + + *TraversalDepthFirst* and **its event-reporting feature** + **can be leveraged to implement additional DFS-based algorithms**: + *TraversalDepthFirst* deals for us with the non-recursive + implementation of the graph-traversal and the handling of some basic + bookkeeping. And our application code can + choose which events are relevant and handle these events in a + problem-specific way. + + Examples for this approach can be found in the section + `problem reduction ` of this tutorial. + They include computing the + `strongly connected components ` + of a graph and the + `biconnectec components of a connected undirected graph `. + + +.. _dfs_all_paths_and_walks: + +DFS: all paths and walks +........................ + +.. versionadded:: 3.4 + +We compute paths and walks. + +- A *directed walk* is a finite or infinite sequence of edges directed in the same + direction which joins a sequence of vertices. + +- A *directed path* is a directed walk in which all vertices are distinct. + +In the following, we always mean *directed* walks resp. *directed* paths and thus +leave out *directed*. + +.. note:: + + In the sections before, we always focussed on paths along the edges of the + DFS forest, while now, we want to regard all possible paths, or even all walks. + +We choose the strings as vertices of the following cyclic graph. It contains +a diamond-shaped sub-graph *A*, *B1*, *B2*, *C*. And additionally, there +is a vertex *B* in the middle, that is successor of *C* and is connected +with *B1* and *B2* in both directions. + +.. code-block:: python + + >>> successors = { + ... "A": ["B1", "B2"], + ... "B1": ["C", "B"], + ... "B2": ["C", "B"], + ... "B": ["B1", "B2"], + ... "C": ["B"], + ... } + >>> def next_vertices(v, _): + ... return successors.get(v, ()) + +First, we want to compute **all paths starting at vertex** *B*. +A vertex can occur in several paths, unlike the situation in a normal +DFS-tree traversal, where each vertex is visited only once. + +We use the *TraversalDepthFirst* strategy of NoGraphs (see +`Traversal algorithms `) to traverse the graph in DFS-order, +starting at *B*. And we generate all paths from the start vertex by using +option *mode = DFSMode.ALL_PATHS*. +We tell the traversal to maintain the trace, i.e., the path leading from the +start vertex to the traversed vertex. + + +.. code-block:: python + + >>> traversal = nog.TraversalDepthFirst(next_vertices) + >>> _ = traversal.start_from(start_vertices="B", mode=nog.DFSMode.ALL_PATHS, compute_trace=True) + >>> for v in traversal: + ... print(traversal.trace) + ['B', 'B2'] + ['B', 'B2', 'C'] + ['B', 'B1'] + ['B', 'B1', 'C'] + +Next, we want to compute **all paths from vertex** *A* **to vertex** *C*. +We print the current trace when *C* is reached. And we prevent the search from further +extending such a path beyond *C*. + +.. code-block:: python + + >>> def next_vertices_prune_at_c(v, _): + ... return next_vertices(v, ()) if v != "C" else [] + >>> traversal = nog.TraversalDepthFirst(next_vertices_prune_at_c) + >>> _ = traversal.start_from(start_vertices="A", mode=nog.DFSMode.ALL_PATHS, compute_trace=True) + >>> for v in traversal: + ... if v == "C": + ... print(traversal.trace) + ['A', 'B2', 'B', 'B1', 'C'] + ['A', 'B2', 'C'] + ['A', 'B1', 'B', 'B2', 'C'] + ['A', 'B1', 'C'] + + +Now, we want to compute **all walks from from the start vertex** *A* +**to the goal vertex** *C* with a length of at most 4 edges. +The function *next_vertices* shown below only returns the successors of a +node if the search depth +has not already reached *4*. This technique is explained in more detail the +tutorial section about `search-aware graphs `. + +.. code-block:: python + + >>> def next_vertices(v, traversal): + ... if traversal.depth == 4: + ... return [] + ... return successors.get(v, ()) + + >>> traversal = nog.TraversalDepthFirst(next_vertices) + >>> _ = traversal.start_from( + ... start_vertices="A", compute_depth=True, mode=nog.DFSMode.ALL_WALKS, compute_trace=True + ... ) + + >>> for v in traversal: + ... if v == "C": + ... print(traversal.trace) + ['A', 'B2', 'B', 'B2', 'C'] + ['A', 'B2', 'B', 'B1', 'C'] + ['A', 'B2', 'C'] + ['A', 'B1', 'B', 'B2', 'C'] + ['A', 'B1', 'B', 'B1', 'C'] + ['A', 'B1', 'C'] + + +.. tip:: + + *TraversalDepthFirst* and **its traversal modes** + **can be leveraged to implement DFS-based algorithms** + **that need to regard all possible paths, or even all walks**. + + Example: Section + `problem reduction ` + of this tutorial shows how *TraversalDepthFirst* + with mode *ALL_PATHS* can be used to compute the + `longest path ` + between two vertices in a weighted graph or in an unweighted graph. + + .. _example-topological_sorting_processes: Topological sorting of process steps @@ -378,11 +701,50 @@ graph algorithm in the NoGraphs style. ('stand up', 'get coffee', 'get filter', 'fill filter', 'get water', 'heat water', 'make coffee', 'drink coffee') -When calculations for a vertex depend on results of (positively) connected -other vertices, we can use the topological sorting of the vertices for ordering -the calculations in the graph. +Next, we try out what happens when there is a **cyclic dependency** between the tasks: +We add an artificial dependency that states that *get water* also depends on +*make coffee* and ask NoGraphs again to traverse the graph in topological order: + +.. code-block:: python + + >>> depends_on["get water"].append("make coffee") + >>> tuple(traversal.start_from("drink coffee")) + Traceback (most recent call last): + RuntimeError: Graph contains cycle + +As you can see, we get a *RuntimeError*, because the tasks cannot be sorted +in a topological order. NoGraphs can **demonstrate the problem** to us by +reporting a path of dependencies from a start vertex (here: our goal to drink +coffee), that leads back to a previous vertex of the same path (here: +we need to *make coffee* before we can *make coffee*): + +.. code-block:: python + + >>> traversal.cycle_from_start + ['drink coffee', 'make coffee', 'heat water', 'get water', 'make coffee'] + +Finally, we remove the additional dependency again, in order to be able +to re-use the graph in the following section: + +.. code-block:: python + + >>> _ = depends_on["get water"].pop() + + +.. tip:: + + When calculations for a vertex depend on results of (positively) connected + other vertices, we can use the topological sorting of the vertices for ordering + the calculations in the graph. This is shown in the following two section. + + +.. _example-critical-path: + +Critical path in a weighted, acyclic graph (using topological search) +..................................................................... -Example: We assign (local) runtimes to the tasks. For each task, the minimal global +We assign (local) runtimes to the tasks shown in the previous section. +For each task, the minimal global runtime till it is completed (runtime of the **critical path**) is the sum of the local runtime and the maximum of the total runtimes of the tasks the task depends on. We order the computations by using the topological sort we got from NoGraphs, so that @@ -408,28 +770,6 @@ completed. >>> runtime_till_end_of["drink coffee"] 15 -Next, we try out what happens when there is a **cyclic dependency** between the tasks: -We add an artificial dependency that states that *get water* also depends on -*make coffee* and ask NoGraphs again to traverse the graph in topological order: - -.. code-block:: python - - >>> depends_on["get water"].append("make coffee") - >>> tuple(traversal.start_from("drink coffee")) - Traceback (most recent call last): - RuntimeError: Graph contains cycle - -As you can see, we get a *RuntimeError*, because the tasks cannot be sorted -in a topological order. NoGraphs can **demonstrate the problem** to us by -reporting a path of dependencies from a start vertex (here: our goal to drink -coffee), that leads back to a previous vertex of the same path (here: -we need to *make coffee* before we can *make coffee*): - -.. code-block:: python - - >>> traversal.cycle_from_start - ['drink coffee', 'make coffee', 'heat water', 'get water', 'make coffee'] - .. _example-longest-path-acyclic-graph: @@ -518,6 +858,15 @@ neighbor with the maximal distance to the goal under all neighbors: >>> path [(1, 0), (2, 0), (2, 1), (2, 2), (3, 2), (4, 2), (4, 3), (4, 4)] +.. tip:: + + Since *TraversalTopologicalSort* does the work for us, but we defined on our + own how to compute the optimal *goal_distance*, we can vary this to solve + similar problems. + For example, to search for the longest path with an even number of edges, + we could simply fill two separate containers for optimal goal distances with + an even and with an odd number of edges. + .. _example-shortest-paths-in-maze: @@ -548,7 +897,7 @@ per place... We use the traversal strategy *TraversalShortestPaths* of NoGraphs (see `Traversal algorithms `). As already said, it implements the -*Dijkstra* algorithm in the style of NoGraphs. +**Dijkstra** algorithm in the style of NoGraphs. .. code-block:: python @@ -558,13 +907,20 @@ We use the traversal strategy *TraversalShortestPaths* of NoGraphs (12, ((0, 0), (0, 1), (0, 2), (1, 2), (2, 2), (2, 1), (3, 1), (4, 1), (4, 2))) +Later in this tutorial we will see how other types of problems, +e.g., `the traveling salesman ` problem and +the problem of finding +`shortest paths in infinitely branching graphs with sorted edges `, +can be solved using *TraversalShortestPaths* as part of a so-called +`problem reduction `. + .. _example-shortest-paths-with-heuristic: -Shortest path search with distance heuristic -............................................ +Shortest path search with distance heuristic (*A\* search*) +........................................................... -Again, vertices are tuples of x and y coordinates ("position vector"), and a +Again, vertices are tuples of *x* and *y* coordinates ("position vector"), and a coordinate is an integer. This time, we use no coordinate limits, valid moves include the diagonal moves, and all edge weights are 1. We define an obstacle represented by a set of positions, that build an "L"-form out of two "walls" in the "region" of @@ -592,7 +948,7 @@ Based on that, NoGraphs calculates a path from start to end position that avoids the obstacle. We use the traversal strategy *TraversalAStar* of NoGraphs -(see `Traversal algorithms `). It implements the *A\* search* +(see `Traversal algorithms `). It implements the **A\* search** algorithm in the style of NoGraphs. >>> traversal =nog.TraversalAStar(next_edges) diff --git a/docs/source/conf.py b/docs/source/conf.py index cc731f8..920330e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -12,13 +12,13 @@ # import os import sys +import time sys.path.insert(0, os.path.abspath('../../src')) -# sys.path.insert(0, os.path.abspath('../..')) # -- Project information ----------------------------------------------------- project = 'NoGraphs' -copyright = '2022 - 2024, Helmut Melcher' +copyright = f'2022 - {time.strftime("%Y", time.gmtime())}, Helmut Melcher' author = 'Helmut Melcher' @@ -77,7 +77,7 @@ 'WeightedUnlabeledFullEdge', 'WeightedLabeledFullEdge', 'WeightedOrLabeledFullEdge', 'AnyFullEdge', 'OutEdge', # 'VertexIdSet', 'VertexIdToDistanceMapping', - # 'VertexIdToVertexMapping', 'VertexIdToPathEdgeDataMapping', + # 'VertexIdToVertexMapping', 'VertexIdToEdgeLabelsMapping', ) autodoc_type_aliases = {alias: alias for alias in aliases} # | { diff --git a/docs/source/gears.rst b/docs/source/gears.rst index d606fd6..56b976e 100644 --- a/docs/source/gears.rst +++ b/docs/source/gears.rst @@ -167,7 +167,7 @@ NoGraphs offers the following predefined gears. **The three main classes support ... elif i % 1200000 > 5: ... yield i - 6, 1 -We use the next_edges function defined in section `overview `. +We use the next_edges function defined in section :doc:`overview `. Instead of the traversal class `TraversalShortestPaths` used there, now, we use the more flexible class `TraversalShortestPathsFlex`. We provide the @@ -187,7 +187,7 @@ with *GearDefault*: .. code-block:: python - >>> gear_test(nog.GearDefault()) + >>> gear_test(nog.GearDefault()) #doctest:+SLOW_TEST [816674, (0, 1, 2, 8, 14), (1199976, 1199982, 1199988, 1199994, 1200000)] 2. We have not changed `vertex identity `, so our vertices are @@ -198,7 +198,7 @@ with integer edge weights and float("infinity") for infinite distances .. code-block:: python - >>> gear_test(nog.GearForIntVertexIDsAndIntsMaybeFloats()) + >>> gear_test(nog.GearForIntVertexIDsAndIntsMaybeFloats()) #doctest:+SLOW_TEST [816674, (0, 1, 2, 8, 14), (1199976, 1199982, 1199988, 1199994, 1200000)] 3. Our vertices themselves, not only their vertex ids, are numbered from 0 on, and our @@ -208,7 +208,7 @@ options here): .. code-block:: python - >>> gear_test(nog.GearForIntVerticesAndIDsAndCFloats()) + >>> gear_test(nog.GearForIntVerticesAndIDsAndCFloats()) #doctest:+SLOW_TEST [816674.0, (0, 1, 2, 8, 14), (1199976, 1199982, 1199988, 1199994, 1200000)] @@ -295,7 +295,7 @@ id set for given vertices, by an implementation that returns an *intbitset*. ... nog.GearForIntVerticesAndIDsAndCFloats ... ): ... def vertex_id_set(self, vertices): - ... return intbitset(vertices) + ... return intbitset(list(vertices)) We can use the new gear just like the predefined ones: @@ -304,9 +304,9 @@ We can use the new gear just like the predefined ones: >>> our_gear = GearBitsetAndArrayForIntVerticesAndCFloats() >>> traversal = nog.TraversalBreadthFirstFlex( ... next_edges=next_edges, gear=our_gear, vertex_to_id=nog.vertex_as_id) - >>> traversal.start_from(0).go_to(1200000) + >>> traversal.start_from(0).go_to(1200000) #doctest:+SLOW_TEST 1200000 - >>> traversal.depth + >>> traversal.depth #doctest:+SLOW_TEST 200000 Section `Comparison of NoGraphs gears ` shows the diff --git a/docs/source/graphs_and_adaptation.rst b/docs/source/graphs_and_adaptation.rst index 13c74b2..26f58ab 100644 --- a/docs/source/graphs_and_adaptation.rst +++ b/docs/source/graphs_and_adaptation.rst @@ -214,7 +214,7 @@ NoGraphs supports a large range of number types as edge weights. For details, see the `API documentation `. See section `typing`, if you like to work strictly type safe with weights. -**An example:** +**Example:** In the following graph, the vertices *i* with numbers *0, 1, ...* are connected by edges *(i, i+1), ...* with weights *1/2, 1/4, 1/8, ...*. The distances @@ -280,103 +280,3 @@ NoGraphs supports graphs with multiple edges, cycles and self loops: - If a graph contains an edge with identical start and end vertex, this is called a *self loop*. - - -.. _search_aware_graphs: - -Search-aware graphs -~~~~~~~~~~~~~~~~~~~ - -A graph is a *search-aware* graph (to be exact: a graph that is defined in a -search-aware way), if existence or attributes of some **vertices or edges are defined -using partial results** that an **algorithm traversing the graph** has computed -**based on the already traversed part of the graph**. - -From a mathematical point of view, this is just an ordinary graph with a special form -of recursive definition, and the definition uses a function that calculates -properties of parts of the graph that are already known. From a practical point of -view, search-aware graphs enrich our possibilities: we can use a traversal algorithm -as such function. - -With NoGraphs, you can define search-aware graphs. In your NextEdges or NextVertices -function, you can easily use state attributes of the search, like the current search -depth or already computed paths: as shown before, -**you get a traversal object as second parameter**, -and **it provides state information to you**, that is valid in the context of the call -of your function. -This traversal object is of the same class as the traversal object that has been used -to start the traversal. -(Sometimes, it is even the same object, but in other cases, it is a separate object.) - -Note: In the examples shown so far, we have already accessed -state information when a found vertex is reported, e.g. the depth of this vertex. -But for search-aware graphs, we -**access state information when the callback function is called** -and **use it to define further parts** of the graph - and the library allows for that. - -.. _eratosthenes_with_Dijkstra: - -**Example: Sieve of Eratosthenes, reduced to Dijkstra search** - -We implement an infinite generator of primes based on the *Sieve of Eratosthenes* -(see https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes). -The special thing about it is: We implement it in the form of a search in an -infinite and search-aware graph. - -We represent the elements of a sequence of numbers -*(j, j+increment, j+2\*increment, ...)* -by tuples *(i, increment)*. For example, the value *8* in sequence *4, 6, 8, 10...* -is represented by *(8, 2)*. These tuples are our vertices. - -We start such a sequence, the *base sequence*, at *(1, 1)*. For each prime *i* that we -find, we start an additional sequence, a *multiples sequence*, -at *(i\*i, i)*. And we define edges that connect a vertex -*(i, increment)* of a multiples sequence with *(i, 1)* of the base sequence. - -We choose the weights in such a way, that **the length (sum of edge weights)** -**of a path to a number equals** -**the number itself, if it is reached by the base sequence alone,** -**and slightly less, if the path goes through a multiples sequence**. -Here, we use the distance of a vertex from the start vertex -(that means: a partial result of the search), to define elements of -the graphs that are still to be searched: The graph is a search-aware graph. - -If the shortest path from *(1, 1)* to some other vertex *(i, 1)* has a length -of *i*, we know that there is no (shorter) path using a multiples sequence, and thus, -that *i* is prime. - -.. code-block:: python - - >>> def next_edges(vertex, traversal): - ... i, increment = vertex - ... if increment == 1: # Base sequence - ... # Return edge to next number i+1, path length i+1 - ... yield (i+1, 1), (i+1) - traversal.distance - ... if traversal.distance == i: # i is prime - ... # (i is neither 1 nor reached via a multiples sequence) - ... # Then start sequence of i multiples at i*i, with - ... # distance advantage -0.5. - ... yield (i*i, i), i*i - i - 0.5 - ... else: # Multiples sequence - ... # Return edge to next multiple, with increment as weight - ... yield (i+increment, increment), increment - ... # Return edge to vertex for i of base sequence, length 0 - ... yield (i, 1), 0 - -Now, we start the search at vertex *(1, 1)*, go till number 50, and print the found -primes. - -.. code-block:: python - - >>> import itertools - >>> traversal = nog.TraversalShortestPaths(next_edges).start_from((1, 1)) - >>> list(itertools.takewhile(lambda i: i<=50, # Results up to 50 - ... (i for i, factor in traversal # Only the value of a vertex - ... if i == traversal.distance))) # Only the primes - [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47] - - -In the tutorial section about `infinitely branching graphs `, -we will see a simplified version of this graph -(function `next_edges_prime_search `), -that shows the idea of the graph more directly. \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 9f155d4..71fb6b8 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -10,6 +10,7 @@ NoGraphs: Graph analysis on the fly installation concept_and_examples graphs_and_adaptation + search_aware_graphs graph_operations traversals bidirectional_search @@ -49,10 +50,14 @@ Think of it as *graph analysis - the lazy (evaluation) way*. - `Unidirectional traversal algorithms `: DFS, BFS, topological search, Dijkstra, A\* and MST. - `Bidirectional search algorithms `: BFS and Dijkstra. -- Results: `Reachability, depth, distance, paths and trees `. +- Results: `Reachability, depth, distance, and paths `. `Paths ` can be `calculated with vertices, edges, or attributed edges `, and can be iterated in both directions. + Additionally, for DFS: + `forest, all kinds of edge types, both entering and leaving events `, + and `DFS tree edges ` or + `all paths or all walks `. - Flexible graph notion: - Infinite directed multigraphs with loops and @@ -102,21 +107,32 @@ Think of it as *graph analysis - the lazy (evaluation) way*. - Source: Available `here `__. - Licence: `MIT `__. -**Extras** (outside of the core of NoGraphs) +**Extras** (outside the core of NoGraphs) - Computation of exact solutions for (small) `traveling salesman problems ` (shortest / longest route, positive / zero / negative edge weights, graph does not need to be complete). - Dijkstra shortest paths algorithm for `infinitely branching graphs with locally sorted edges `. -- Example for computing the - `longest path ` - between two vertices in a weighted, acyclic graph. - `Gadget functions ` for test purposes. They make the easy task of adapting existing explicit test graphs a no brainer, may they be stored in `edge indices or edge iterables ` or in `arrays `. +**Examples with further algorithms** + +- `Depth-limited search ` +- `Iterative deepening depth-first search ` +- `Critical path ` + in a weighted, acyclic graph +- `Longest path ` + between two vertices in a weighted, acyclic graph +- `Longest path ` + between two vertices in a weighted graph or in an unweighted graph +- `Strongly connected components ` + of a graph +- `Biconnected components of a connected undirected graph ` + .. _overview_example: diff --git a/docs/source/reduction_of_other_problems.rst b/docs/source/reduction_of_other_problems.rst index 861d734..637d59d 100644 --- a/docs/source/reduction_of_other_problems.rst +++ b/docs/source/reduction_of_other_problems.rst @@ -34,10 +34,17 @@ When we use NoGraphs in the above computation part 2, we can try to - In part 3, we might **fetch results from part 2** only when, and **as far as, they are needed** to construct the final results. -This can help to reduce the needed runtime and memory. And it can be used to create -a kind of graph analysis that is even more "lazy" than that of NoGraphs alone. +This can help to keep runtime and memory low. And it can be used to create +a kind of graph analysis that is +`even more "lazy" than that of NoGraphs alone `. Both is demonstrated in the following sections. +.. note:: + + In these sections, no new functionality of NoGraphs is shown, + but only examples of how they can be used. + Thus, you can safely skip one or several of them if you like. + .. _traveling_salesman_example: @@ -199,9 +206,7 @@ explicit graphs as used by typical graph libraries cannot provide them. **The TSP-solving function in the extras section of NoGraphs** -.. versionchanged:: 3.3 - - traveling_salesman added. +.. versionadded:: 3.3 NoGraphs contains a function *traveling_salesman(vertices, weights)*, a more general version of the above algorithm @@ -227,8 +232,8 @@ In the following, we apply it to the `above problem .. code-block:: python - >>> length, path_iterator = nog.traveling_salesman(range(len(graph)), graph) - >>> print(length, list(path_iterator)) + >>> length, path_iterator = nog.traveling_salesman(range(len(graph)), graph) #doctest:+SLOW_TEST + >>> print(length, list(path_iterator)) #doctest:+SLOW_TEST 39 [0, 5, 6, 14, 15, 3, 4, 16, 8, 7, 12, 10, 9, 1, 13, 2, 11, 0] Of cause, it computes the same optimal TSP path length. But it returns another path of @@ -250,7 +255,7 @@ Example: Shortest paths in infinitely branching graphs with sorted edges **The Problem** -We have the following graph, defined on the positive integers as +We have a graph, defined on the positive integers as vertices. We like to start at vertex 1, iterate the shortest paths from there in ascending order, and report vertices with a distance that equals the vertex itself. We will explain the purpose later on. @@ -271,8 +276,10 @@ For vertices larger than 1, the graph has This is called an *infinitely branching graph*. NoGraphs itself cannot analyze such graphs. -But still, we can analyze the graph using NoGraphs - in combination with problem -reduction: +**The algorithm** + +Although NoGraphs cannot analyze such graphs, we can still analyze the graph +using NoGraphs - in combination with problem reduction: We search in a search graph, not directly in the given graph. The idea of the search graph is, that **instead of having infinitely many edges** starting @@ -349,6 +356,8 @@ The details of the code are not needed. ... return ((v, traversal.distance) for v, edge_no in traversal if edge_no == 0) +**Applying the algorithm** + Based on this implementation of the problem reduction, we can now solve our given problem: .. code-block:: python @@ -411,13 +420,12 @@ Please see there, if you like to know how it works. computation steps to produce the next shortest path (resp. its end vertex and distance from the start). + .. _infinite_branching_in_nographs: **Functionality for infinitely branching graphs in the extras section of NoGraphs** -.. versionchanged:: 3.3 - - TraversalShortestPathsInfBranchingSorted added. +.. versionadded:: 3.3 NoGraphs contains a class *TraversalShortestPathsInfBranchingSorted*, a more general version of the above algorithm @@ -473,3 +481,480 @@ No, we also ask for paths, and report the predecessor of each non-prime number a 16 4 18 3 20 4 + + +.. _longest_path_two_vertices: + +Example: Longest simple path between two vertices - based on DFS +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 3.4 + +Our goal is to compute the longest (simple) path from a given vertex *v* to another +given vertex *w*. + +**Problem 1: Search for longest path w.r.t. the sum of edge weights** + +First, we use a weighted graph, and we define the length of a path by the +sum of the weights of its edges. + +We re-use the graph and the function *neighbors_in_grid* from example +`shortest paths in a maze with weights `. +This time, our edge weights are the energy needed for walking +uphill (here: just the height difference) or +downhill (here: half the height difference). + +.. + >>> def neighbors_in_grid(position): + ... pos_x, pos_y = position + ... for move_x, move_y in (-1, 0), (1, 0), (0, -1), (0, 1): + ... new_x, new_y = pos_x + move_x, pos_y + move_y + ... if new_x in range(5) and new_y in range(5): + ... yield new_x, new_y + +.. code-block:: python + + >>> data = ''' + ... 02819 + ... 37211 + ... 21290 + ... 91888 + ... 55990 + ... '''.strip().splitlines() + >>> def next_edges(position, _): + ... x, y = position + ... position_height = int(data[y][x]) + ... for x, y in neighbors_in_grid(position): + ... successor_height = int(data[y][x]) + ... slope = successor_height - position_height + ... energy = slope if slope >= 0 else -slope/2 + ... yield (x, y), energy + + +**The algorithm** + +We traverse all possible paths from the start vertex using +*depth first search*. We maintain the length of the path: When we enter +a vertex, we add the length of the edge we used to the length of the path, +and when we leave a vertex, we subtract the length of the edge that we use +for backtracking. +When we reach the destination node, we prevent the path from being extended +any further. And we maintain the longest path found so far, and its length. + +.. code-block:: python + + >>> def longest_weighted_path(next_edges, start_vertex, goal_vertex): + ... traversal = nog.TraversalDepthFirst(next_labeled_edges=next_edges) + ... _ = traversal.start_from( + ... start_vertex=start_vertex, mode=nog.DFSMode.ALL_PATHS, + ... compute_trace=True, report=nog.DFSEvent.IN_OUT_SUCCESSOR, + ... ) + ... # Load loop-invariant objects + ... trace = traversal.trace + ... trace_labels = traversal.trace_labels + ... generator = traversal.__iter__() + ... dfs_event_entering_successor = nog.DFSEvent.ENTERING_SUCCESSOR + ... stop_iteration = StopIteration() + ... # Bookkeeping + ... trace_length = 0 + ... longest, length_of_longest = (), -1 + ... # Traversal of all possible paths from the start vertex, + ... # pruned when we reach the goal vertex + ... for v in generator: + ... if traversal.event == dfs_event_entering_successor: + ... trace_length += trace_labels[-1] + ... if v == goal_vertex: + ... generator.throw(stop_iteration) + ... if trace_length > length_of_longest: + ... length_of_longest = trace_length + ... longest = tuple(trace) + ... else: # nog.DFSEvent.LEAVING_SUCCESSOR + ... trace_length -= trace_labels[-1] + ... return length_of_longest, longest + +**Applying the algorithm** + +.. code-block:: python + + >>> longest_weighted_path(next_edges, (0, 0), (4, 2)) # doctest: +NORMALIZE_WHITESPACE + (85.5, + ((0, 0), (0, 1), (0, 2), (1, 2), (1, 1), (1, 0), (2, 0), (3, 0), (4, 0), + (4, 1), (3, 1), (3, 2), (2, 2), (2, 3), (1, 3), (0, 3), (0, 4), (1, 4), + (2, 4), (3, 4), (4, 4), (4, 3), (4, 2))) + +**Problem 2: Search for longest path w.r.t. the number of edges** + +Next, we define the length of a path by the number of its edges. + +For this, we just assign weight *1* to each edge that we generate in our +`NextEdges` function. + +**Applying the algorithm** + +.. code-block:: python + + >>> def next_edges_weight_1(position, _): + ... for x, y in neighbors_in_grid(position): + ... yield (x, y), 1 + >>> longest_weighted_path(next_edges_weight_1, (0, 0), (4, 2)) # doctest: +NORMALIZE_WHITESPACE + (24, ((0, 0), (0, 1), (0, 2), (0, 3), (0, 4), (1, 4), (1, 3), (1, 2), + (1, 1), (1, 0), (2, 0), (2, 1), (2, 2), (2, 3), (2, 4), (3, 4), (4, 4), + (4, 3), (3, 3), (3, 2), (3, 1), (3, 0), (4, 0), (4, 1), (4, 2))) + + +.. _strongly_connected_components: + +Example: Strongly connected components - based on DFS +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 3.4 + +**The Problem** + +Our goal is to compute the +`strongly connected components `_ +of a graph. +A *directed graphs is said to be strongly connected* if every vertex is reachable from +every other vertex. The *strongly connected components of a directed graph* form a +partition into subgraphs that are themselves strongly connected. + +Here is our graph: + +.. code-block:: python + + >>> edges = {0: (1, 2), 1: (0, 3), 2: (0, 3), 3: (5,), 4: (2, 5, 7), 5: (3, 8), + ... 6: (4,), 7: (5, 6), 8: (), 9: (8,)} + >>> vertices = edges.keys() + >>> def next_vertices(v, _): + ... return edges[v] + + +**The algorithm** + +We want to **implement a non-recursive version of the** +`algorithm of Tarjan +`_. + +We **use the non-recursive implementation of DFS in NoGraphs as basis** for this. +We turn on exactly the functionalities of this implementation that we need. + +According to the algorithm, we need to do the following: + +- We number the vertices in the so-called pre-oder, i.e., vertices get their + number when they are visited. + + NoGraphs can compute this pre-preorder index for us. + We turn on this feature of the DFS implementation: *compute_index=True*. + +- We maintain a stack and place nodes on the stack in the order in which they are + visited. And we maintain the so-called *low_link*, the smallest index of any node + on the stack known to be reachable from *v* through *v*'s DFS subtree, including *v* + itself. We implement both the stack and the *low_link*-mapping in a single *dict*. + + When we backtrack from a vertex *v*, and its *low_link* equals its index, we + pop all vertices from the stack till we reach *v* and report these vertices + as component. *v* is the root of the component. + + For these steps, we can also use NoGraphs: + + - We need to know when one of the following events occurs during the + traversal: A vertex (a start vertex or a successor) is entered, an edge of the + DFS-tree is followed, a vertex is left (backtracking), or some non-tree edge + is seen. + + We turn on the respective reporting: + *report=nog.DFSEvent.IN_OUT | nog.DFSEvent.SOME_NON_TREE_EDGE*. + + - And we need access to the predecessor of a vertex when an edge is followed or + when the traversal backtracks. + + For this, we switch on the computation of + the trace (current path from a start vertex to the current vertex): + *compute_trace=True*. + +Now, we can directly implement the steps that need to happen when the different +events occur: + +.. code-block:: python + + >>> def tarjan_strongly_connected_components(next_vertices, start_vertices): + ... traversal = nog.TraversalDepthFirst(next_vertices) + ... _ = traversal.start_from( + ... start_vertices=start_vertices, compute_trace=True, compute_index=True, + ... report=nog.DFSEvent.IN_OUT | nog.DFSEvent.SOME_NON_TREE_EDGE) + ... trace, index = traversal.trace, traversal.index + ... + ... # Stack of vertices, and the *low_link* values computed for them + ... low_link = dict() + ... # Iterate the occurring events and the respective vertices: + ... for v in traversal: + ... event = traversal.event + ... if event in nog.DFSEvent.ENTERING: + ... # Entering a new vertex via an edge of the DFS-tree: + ... # Push v on the stack, store index as low_link + ... low_link[v] = index[v] + ... continue + ... if event == nog.DFSEvent.SOME_NON_TREE_EDGE and v in low_link: + ... # A non-tree edge (predecessor, v), and v is on the stack: + ... # Update low_link of predecessor w.r.t. the index of v + ... if (v_index:=index[v]) < low_link[predecessor:=trace[-2]]: + ... low_link[predecessor] = v_index + ... continue + ... if event == nog.DFSEvent.LEAVING_SUCCESSOR: + ... # Backtracking from an edge (predecessor, v) of the DFS-tree: + ... # Update low_link of predecessor w.r.t. low_link of v + ... if (v_low_link:=low_link[v]) < low_link[predecessor:=trace[-2]]: + ... low_link[predecessor] = v_low_link + ... if event in nog.DFSEvent.LEAVING and low_link[v] == index[v]: + ... # Leaving a vertex of the DFS-tree that is root of a component + ... # (the lowest link reachable from it is itself): + ... # Pop from the stack and report till the component root is reached + ... component = [] + ... while True: + ... w, l = low_link.popitem() + ... component.append(w) + ... del index[w] # We do not need the index number of w anymore + ... if v == w: break + ... yield component + + +**Applying the algorithm** + +And here are the strongly connected components of the graph: + +.. code-block:: python + + >>> list(tarjan_strongly_connected_components(next_vertices, vertices)) + [[8], [5, 3], [1, 2, 0], [6, 7, 4], [9]] + +Like this, the **TraversalDepthFirst of NoGraphs simplifies** +**the implementation of non-recursive DFS-based algorithms**. + +Note that the implementation reports components immediately when it identifies them. +And it only traverses the part of the diagram that is necessary to identify them. The +**lazy evaluation style of TraversalDepthFirst thus carries over** to the calculation +of the components. For example, it is possible to calculate components only until a +component with certain properties has been found. + + + +.. _biconnected_components: + +Example: Biconnected components of a connected undirected graph - based on DFS +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 3.4 + +**The problem** + +Our goal is to compute the +`biconnected components `_ +(sometimes also called *2-connected component*) of a graph +. + +The *undirected graph* is represented as a symmetric directed graph, +i.e., for each edge *(v, w)* in the graph, *(w, v)* also is an edge in the graph. +The graph is *connected*, i.e., every vertex is reachable from every other vertex. + +.. code-block:: python + + >>> from collections import defaultdict + >>> successors = defaultdict(list) + >>> for v, w in ( + ... ("MY", "GYB"), ("GYB", "GS"), ("GS", "S"), + ... ("MY", "TM"), ("GYB", "B"), ("GYB", "G0"), ("GS", "G1"), + ... ("TM", "RT"), ("G0", "G2"), ("G1", "G2"), ("G1", "G3"), + ... ("RT", "R0"), ("G2", "G3"), + ... ("RT", "R1"), ("R0", "R2"), + ... ("R1", "R2"), + ... ): + ... successors[v].append(w) + ... successors[w].append(v) + +(The names of the vertices are chosen to represent the first letters of the colors +red, turquoise, yellow, blue, green, and stone grey of the example graph in the +above mentioned Wikipedia article. This makes it +easy the match the graph and the results that we will get to the example +- at least as long as the article uses the example...) + +A connected undirected graph is said to be *biconnected* +if it has more than 2 vertices and remains connected when a vertex is removed. +A *biconnected component* is a maximal biconnected subgraph. + + +**The algorithm** + +We want to implement a +**non-recursive version of the algorithm of Hopcroft and Tarjan** +(https://en.wikipedia.org/wiki/Biconnected_component). + +**We use the non-recursive implementation of DFS in NoGraphs as basis** for this. +We turn on exactly the functionalities of this implementation that we need. + +The algorithm is based on the following elements: + +- We need to store the parent of each visited vertex + + NoGraphs can do this for us - we could just use option *compute_paths*, and the + path container stores the predecessors for us. + + But although the algorithm maintains all these parents, it only uses the + parent of the current vertex or the parent of the parent. These vertices + are part of the trace (current path from a start vertex to the current vertex). + + A single trace is less data than the whole predecessor relation for all paths, + so we choose to switch on the computation of the trace: + *compute_trace=True*. + +- We need to maintain the search depths of all visited vertices + + When the DFS traversal enters a vertex, we store the DFS search depth of + the vertex in a dict. + + NoGraphs can compute the search depth of the current vertex. But we do not use + this feature, because when a vertex is entered, we already know its + search depth: it equals *len(trace) - 1*. + +- We need to compute and store the so-called *lowpoint* of a vertex *v*, when the + traversal leaves the vertex: + + This lowpoint is the minimum of the depth of v, the depth of all neighbors + of v (other than the parent of v in the depth-first-search tree) and the lowpoint of + all children of v in the depth-first-search tree. + + And if condition + *low_point(y) ≥ depth(v)* + holds, *v* is an articulation point. + + - For these steps of the algorithm, + we need to know when one of the following events occurs during the + traverse: A vertex (a start vertex or a successor) is entered, an edge of the + DFS-tree is followed, a vertex is left (backtracking along an edge of the + DFS-tree), or some non-tree edge + is seen. + + We turn on the respective reporting: + *report=nog.DFSEvent.IN_OUT | nog.DFSEvent.SOME_NON_TREE_EDGE*. + +Now, we can directly implement the steps that need to happen when the different +events occur: + +.. code-block:: python + + >>> def hopcroft_tarjan_articulation_points(next_vertices, any_vertex, lowpoint_dict=None): + ... """ Reports articulation points. The undirected and connected graph needs + ... to be given in form of directed edges in both directions. An articulation + ... point is reported each time a biconnected component is found. So, the + ... same vertex could be reported several times. + ... + ... If you also need the biconnected components, then provide an empty + ... dictionary *lowpoint_dict* from the vertices to the integers + ... (see function *hopcroft_tarjan_biconnected_components*). + ... """ + ... traversal = nog.TraversalDepthFirst(next_vertices) + ... _ = traversal.start_from( + ... start_vertex=any_vertex, compute_trace=True, + ... report=nog.DFSEvent.IN_OUT | nog.DFSEvent.SOME_NON_TREE_EDGE) + ... trace = traversal.trace + ... depths = dict() + ... low_point = dict() if lowpoint_dict is None else lowpoint_dict + ... dfs_children_of_start = 0 + ... # Iterate the occurring events and the respective vertices: + ... for v in traversal: + ... event = traversal.event + ... if event in nog.DFSEvent.ENTERING: + ... # Entering a new vertex via an edge of the DFS-tree or as start + ... # vertex: Store its search depth as depth and lowpoint + ... low_point[v] = depths[v] = depth = len(trace) - 1 + ... if depth == 1: + ... # The predecesor is the start vertex: Count v as DFS-child of it + ... dfs_children_of_start += 1 + ... elif event == nog.DFSEvent.LEAVING_SUCCESSOR: + ... # Backtracking from an edge (predecessor, v) of the DFS-tree: + ... # Update low_point of predecessor w.r.t. the low_point of v + ... if (v_low_point:=low_point[v]) < low_point[predecessor:=trace[-2]]: + ... low_point[predecessor] = v_low_point + ... # If predecessor is a non-start vertex, check the respective + ... # condition for such articulations points w.r.t DFS-child v + ... if 0 < (predecessor_depth:=len(trace)-2) <= v_low_point: + ... yield predecessor + ... elif event in nog.DFSEvent.SOME_NON_TREE_EDGE and ( + ... len(trace) < 3 or v != trace[-3]): + ... # A non-tree edge (predecessor, v), and v is not the + ... # DFS-predecessor of the predecessor (e.g., the edge, that we are + ... # seeing, is not the edge, that we just followed, in backwards + ... # direction): + ... # Update low_link of predecessor w.r.t. depth of v + ... if (v_depth := depths[v]) < low_point[predecessor:=trace[-2]]: + ... low_point[predecessor] = v_depth + ... elif event == nog.DFSEvent.LEAVING_START: + ... # Check condition whether start vertex is articulation point. + ... if dfs_children_of_start > 1: + ... yield v + +The steps, that are needed to compute the found biconnected component +when an articulation point is reported, do not need any graph traversal. So, we +implement them without using NoGraphs. It's a simple loop that extracts data +from a *dict*. + +.. code-block:: python + + >>> def hopcroft_tarjan_biconnected_components(articulation_point, lowpoint_dict): + ... """ Report the biconnected component from *data_dict*, + ... the mapping from a vertex to its *lowpoint* value, in insertion order. + ... The function can only be used + ... when *hopcroft_tarjan_articulation_points* just reported an + ... articulation point, and then only once. """ + ... component = [] + ... while True: + ... # Get all vertices of the DFS tree (in its current state) that starts + ... # at the articulation point. Use the lowpoint dictionary, and + ... # remove all vertices except for the articulation point itself, as + ... # it will be part of other components that we report later on. + ... vertex, low_point_value = lowpoint_dict.popitem() + ... component.append(vertex) + ... if vertex == articulation_point: + ... lowpoint_dict[vertex] = low_point_value + ... return component + + +**Applying the algorithm** + +Now, we can compute the articulation points of the graph: + +.. code-block:: python + + >>> def next_vertices(v, _): + ... return successors[v] + >>> start_vertex = next(iter(successors.keys())) + + >>> articulation_points = set( + ... hopcroft_tarjan_articulation_points(next_vertices, start_vertex)) + >>> list(sorted(articulation_points)) + ['GS', 'GYB', 'MY', 'RT', 'TM'] + + +Next, we repeat the computation, but this time we ask for the biconnected components +(we could also get both parts of the result in a single run by storing the +articulation points in a set): + +.. code-block:: python + + >>> lowpoint_dict = dict() + >>> for articulation_point in ( + ... hopcroft_tarjan_articulation_points(next_vertices, start_vertex, lowpoint_dict) + ... ): + ... print(hopcroft_tarjan_biconnected_components(articulation_point, lowpoint_dict)) + ['R0', 'R2', 'R1', 'RT'] + ['RT', 'TM'] + ['S', 'GS'] + ['GS', 'G1', 'G3', 'G2', 'G0', 'GYB'] + ['B', 'GYB'] + ['GYB', 'TM', 'MY'] + +Note that, like in the previous section, the implementation reports results +immediately when it identifies them, and it only traverses the part of the diagram +that is necessary to identify them. The lazy evaluation style of TraversalDepthFirst +thus carries over to the calculation of the articulation points and the components. +For example, it is possible to calculate components only until a component with +certain properties has been found. diff --git a/docs/source/search_aware_graphs.rst b/docs/source/search_aware_graphs.rst new file mode 100644 index 0000000..777a645 --- /dev/null +++ b/docs/source/search_aware_graphs.rst @@ -0,0 +1,352 @@ +Search-aware graphs and start vertices +-------------------------------------- + +.. + Import nographs for doctests of this document. Does not go into docs. + >>> import nographs as nog + +During an already running graph traversal, further vertices and edges of +the graph and further start vertices +can be computed based on already available partial results of the same +traversal. This is possible due to the lazy-evaluation style of NoGraphs. +The following sections will explain this and show examples. + +Search-aware graphs +~~~~~~~~~~~~~~~~~~~ + +A graph is a *search-aware* graph (to be exact: a graph that is defined in a +search-aware way), if existence or attributes of some **vertices or edges are defined +using partial results** that an **algorithm traversing the graph** has computed +**based on the already traversed part of the graph**. + +From a mathematical point of view, this is just an ordinary graph with a special form +of recursive definition, and the definition uses a function that calculates +properties of parts of the graph that are already known. From a practical point of +view, search-aware graphs enrich our possibilities: we can use a traversal algorithm +as such function. + +With NoGraphs, you can define search-aware graphs. In your NextEdges or NextVertices +function, you can easily use state attributes of the search, like the current search +depth or already computed paths: +as shown `before `, +**you get a traversal object as second parameter**, +and **it provides state information to you**, that is valid in the context of the call +of your function. +This traversal object is of the same class as the traversal object that has been used +to start the traversal. +(Sometimes, it is even the same object, but in other cases, it is a separate object.) + +.. Note:: + + In the examples shown so far, we have already accessed + state information when a found vertex is reported, e.g. the depth of this vertex. + + But for search-aware graphs, we + **access state information when the callback function is called** + and **use it to define further parts** of the graph - and the library allows for that. + + +.. _graph_pruning_by_search_depth: + +**Example: Pruning a graph depending on search depth and start vertex** + +We start with a very simple example: +The vertices of our graph are the integers. Successors of a vertex *v* are +*v+1* and *v+4*. +We have a set of start vertices, and for each of it, we want to compute the +vertices reachable in at most two steps along edges. + +We want to use a breadth-first search. This search strategy guarantees that +the search depth (*traversal.depth*) of a vertex equals its depth, i.e., the +minimal number of edges that are necessary to reach it from the start vertex. +So, if *traversal.depth == 2* for some vertex, we do not want to follow +further edges. + +We take this condition into the definition of the *next_vertices* function. +So, **we define a graph that, so to say,** +**automatically ends after two search steps** +**- depending of the vertex at which the search begins**. + +.. code-block:: python + + >>> def next_vertices(v, traversal): + ... if traversal.depth == 2: + ... return [] + ... return [v+1, v+4] + +Now we carry out the search for each of the start vertices. We see, that we +get different results depending on the start vertex: +**The graph adapts to the search**. + +.. code-block:: python + + >>> traversal = nog.TraversalBreadthFirst(next_vertices) + >>> for start_vertex in [10, 30, 40]: + ... print(f"{start_vertex}:", + ... list(traversal.start_from(start_vertex))) + 10: [11, 14, 12, 15, 18] + 30: [31, 34, 32, 35, 38] + 40: [41, 44, 42, 45, 48] + +.. _eratosthenes_with_Dijkstra: + +**Example: Sieve of Eratosthenes, reduced to Dijkstra search** + +.. note:: + + This example shows what is possible and why + search-aware graphs open new possibilities. + But it is a bit complicated. + Since it is only an example, and does not introduce any other + features of NoGraph you can safely skip it if you like. + +We implement an infinite generator of primes based on the +`Sieve of Eratosthenes `_. +The special thing about it is: We implement it in the form of a search in an +infinite and search-aware graph. + +We represent the elements of a sequence of numbers +*(j, j+increment, j+2\*increment, ...)* +by tuples *(i, increment)*. For example, the value *8* in sequence *4, 6, 8, 10...* +is represented by *(8, 2)*. These tuples are our vertices. + +We start such a sequence, the *base sequence*, at *(1, 1)*. For each prime *i* that we +find, we start an additional sequence, a *multiples sequence*, +at *(i\*i, i)*. And we define edges that connect a vertex +*(i, increment)* of a multiples sequence with *(i, 1)* of the base sequence. + +We choose the weights in such a way, that **the length (sum of edge weights)** +**of a path to a number equals** +**the number itself, if it is reached by the base sequence alone,** +**and slightly less, if the path goes through a multiples sequence**. +Here, we use the distance of a vertex from the start vertex +(that means: a partial result of the search), to define elements of +the graphs that are still to be searched: The graph is a search-aware graph. + +If the shortest path from *(1, 1)* to some other vertex *(i, 1)* has a length +of *i*, we know that there is no (shorter) path using a multiples sequence, and thus, +that *i* is prime. + +.. code-block:: python + + >>> def next_edges(vertex, traversal): + ... i, increment = vertex + ... if increment == 1: # Base sequence + ... # Return edge to next number i+1, path length i+1 + ... yield (i+1, 1), (i+1) - traversal.distance + ... if traversal.distance == i: # i is prime + ... # (i is neither 1 nor reached via a multiples sequence) + ... # Then start sequence of i multiples at i*i, with + ... # distance advantage -0.5. + ... yield (i*i, i), i*i - i - 0.5 + ... else: # Multiples sequence + ... # Return edge to next multiple, with increment as weight + ... yield (i+increment, increment), increment + ... # Return edge to vertex for i of base sequence, length 0 + ... yield (i, 1), 0 + +Now, we start the search at vertex *(1, 1)*, go till number 50, and print the found +primes. + +.. code-block:: python + + >>> import itertools + >>> traversal = nog.TraversalShortestPaths(next_edges).start_from((1, 1)) + >>> list(itertools.takewhile(lambda i: i<=50, # Results up to 50 + ... (i for i, factor in traversal # Only the value of a vertex + ... if i == traversal.distance))) # Only the primes + [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47] + + +In the tutorial section about `infinitely branching graphs `, +we will see a simplified version of this graph +(function `next_edges_prime_search `), +that shows the idea of the graph more directly. + + +.. _search_aware_start_vertices: + +Search-aware start vertices +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Not only a graph can be `search-aware `, but also +the start vertices. This is possible with *TraversalDepthFirst* +that implements the strategy depth-first search in the lazy +style of NoGraphs: + +TraversalDepthFirst accepts an iterable as start vertices, like other +algorithms of NoGraphs. But it is special in that it fetches +a single start vertex at a time, traverses all vertices reachable from there, +and only then continues with the next start vertex. So, the +**computation of further start vertices can use** +**the partial search results available so far**. + +In the following example, both the computation of further start vertices and +the computation of further edges depend on partial search results. + +.. _iterative_deepening_dfs: + +**Example: Iterative deepening depth-first search with just a single** +**run of TraversalDepthFirst**. + +We use the graph and the function *next_vertices* from example +`Breadth First Search in a maze ` +(see there for details), and want to get from field *S* to field *G* +by the shortest route (number of horizontal and/or vertical steps) +without entering the fields *\**: + +:: + + S*.*. + .*... + .*.*. + ...*. + .*.*G + +.. + Repetition of the example from section Graphs and Adaptation. Does not + go into docs. + >>> def neighbors_in_grid(position): + ... pos_x, pos_y = position + ... for move_x, move_y in (-1, 0), (1, 0), (0, -1), (0, 1): + ... new_x, new_y = pos_x + move_x, pos_y + move_y + ... if new_x in range(5) and new_y in range(5): + ... yield new_x, new_y + ... + >>> def next_vertices(position, _): + ... for x, y in neighbors_in_grid(position): + ... # Report the neighbor position, if it is allowed to enter it + ... if not((x==1 and y!=3) or (x==3 and y!=1)): + ... yield (x, y) + +But unlike in the example mentioned above, we will +not use a breadth-first search: +We want to avoid its bookkeeping +of the vertices that have been found at a specific depth and +of the vertices, that have already been visited. + +Thus, we implement an +`iterative deepening depth-first search (IDDFS) +`_: +We use the depth-first strategy to generate either +`paths or walks ` to reachable vertices. +We limit the search depth by limiting the length of the generated paths (resp. walks). +And we search several times, with an increasing limit for the search depth. So, when +we first find the / a goal vertex, we have found it following a shortest path. +When we are at a certain depth limit and recognize that a higher limit +would not lead to finding additional vertices, we know that we cannot find the / a goal +vertex in the graph. + +If we generate +`paths ` +in an IDDFS, we avoid the effort of generating walks that are no paths. +If we generate +`walks `, +we avoid using memory for storing the vertices that are on the +current path. Thus, we implement both options. + +IDDFS can search paths from any vertex of a set of start vertices to any vertex +of a set of goal vertices. We also implement this flexibility. + +The special thing about the following implementation of the IDDFS is: + +1) The start vertices for the search + are provided by a generator (here: function *start_vertices*), + that yields the original start vertices several time, + but each time with an increased search depth. And its stops to do so when + the search with the current depth limit could not detect further vertices that + require a higher limit. + +2) The NextVertices function *next_vertices_pruned* implements the + depth restriction based on the depth limit set within *start_vertices*. And + it detects whether increasing the limit would result in further vertices + (variable *successors_over_depth_limit*) + - information, that *start_vertices* uses to decide when to stop going deeper. + +This means, during the search, **the computation of start vertices and the** +**computation of edges communicate and adapt to results of each other.** + +From the perspective of NoGraphs, we do **a single depth-first search**, in +**a graph that always grows when the search continues with the next start vertex.** +And the stream of **start vertices ends, when the graph cannot grow any more**. + +.. code-block:: python + + >>> def iterative_deepening_dfs(start_vertices, goal_vertices, next_vertices, mode): + ... depth_limit = 0 + ... successors_over_depth_limit = True + ... + ... def start_vertices_of_the_search(): + ... nonlocal depth_limit, successors_over_depth_limit + ... while successors_over_depth_limit: + ... successors_over_depth_limit = False + ... yield from start_vertices + ... depth_limit += 1 + ... + ... def next_vertices_pruned(v, traversal): + ... nonlocal successors_over_depth_limit + ... depth = len(traversal.trace) - 1 + ... if depth < depth_limit: + ... yield from next_vertices(v, None) + ... elif not successors_over_depth_limit: + ... for successor in next_vertices(v, None): + ... successors_over_depth_limit = True + ... break + ... + ... traversal = nog.TraversalDepthFirst(next_vertices_pruned) + ... _ = traversal.start_from(start_vertices=start_vertices_of_the_search(), + ... compute_trace=True, mode=mode, report=nog.DFSEvent.ENTERING) + ... for v in traversal.go_for_vertices_in(goal_vertices, fail_silently=True): + ... return traversal.trace + ... return [] + +Now, we start the search: First based on walks and then based on paths. + +.. code-block:: python + + >>> iterative_deepening_dfs([(0, 0)], {(4, 4)}, next_vertices, mode=nog.DFSMode.ALL_WALKS) # doctest: +NORMALIZE_WHITESPACE + [(0, 0), (0, 1), (0, 2), (0, 3), (1, 3), (2, 3), (2, 2), (2, 1), (3, 1), (4, 1), + (4, 2), (4, 3), (4, 4)] + + >>> iterative_deepening_dfs([(0, 0)], {(4, 4)}, next_vertices, mode=nog.DFSMode.ALL_PATHS) # doctest: +NORMALIZE_WHITESPACE + [(0, 0), (0, 1), (0, 2), (0, 3), (1, 3), (2, 3), (2, 2), (2, 1), (3, 1), (4, 1), + (4, 2), (4, 3), (4, 4)] + + +For comparison, a more conventional implementation of the same algorithm is +given below, which works without search-aware start_vertices and next_vertices. +It starts a new search each time the search depth limit is increased. + +.. code-block:: python + + >>> import itertools + >>> def iterative_deepening_dfs(start_vertices, goal_vertices, next_vertices, mode): + ... traversal = nog.TraversalDepthFirst(next_vertices) + ... for depth_limit in itertools.count(0): + ... generator = iter(traversal.start_from( + ... start_vertices=start_vertices, compute_trace=True, + ... mode=mode, report=nog.DFSEvent.ENTERING)) + ... successors_over_depth_limit = False + ... for v in generator: + ... if v in goal_vertices: + ... return traversal.trace + ... if (depth:=len(traversal.trace)-1) == depth_limit: + ... _ = generator.throw(StopIteration()) + ... if not successors_over_depth_limit: + ... for successor in next_vertices(v, None): + ... successors_over_depth_limit = True + ... break + ... if not successors_over_depth_limit: + ... return [] + +Of cause, we get the same results: + +.. code-block:: python + + >>> iterative_deepening_dfs([(0, 0)], {(4, 4)}, next_vertices, mode=nog.DFSMode.ALL_WALKS) # doctest: +NORMALIZE_WHITESPACE + [(0, 0), (0, 1), (0, 2), (0, 3), (1, 3), (2, 3), (2, 2), (2, 1), (3, 1), (4, 1), + (4, 2), (4, 3), (4, 4)] + + >>> iterative_deepening_dfs([(0, 0)], {(4, 4)}, next_vertices, mode=nog.DFSMode.ALL_PATHS) # doctest: +NORMALIZE_WHITESPACE + [(0, 0), (0, 1), (0, 2), (0, 3), (1, 3), (2, 3), (2, 2), (2, 1), (3, 1), (4, 1), + (4, 2), (4, 3), (4, 4)] diff --git a/docs/source/traversals.rst b/docs/source/traversals.rst index c744aba..d169594 100644 --- a/docs/source/traversals.rst +++ b/docs/source/traversals.rst @@ -30,14 +30,14 @@ class documentation in the API reference for details. - Algorithm *Breadth First Search* ("BFS"). - - Visits and reports vertices in *breadth first order*, i.e., **with ascending - depth** (the depth of a vertex is the edge count of the path with least edges - from a start vertex). - A vertex is reported before the first outgoing edge is taken. - Start vertices are not reported. + - Visits and reports vertices in *breadth first order*, i.e., + **with ascending depth** (the depth of a vertex is the edge count of + the path with least edges from a start vertex). + A vertex is reported when it is "seen" (read from the graph) for the + first time. Start vertices are not reported. - - The traversal state provides **vertex depth** / **search depth**, **paths** - (optionally), and set of **visited vertices**. + - The traversal state provides **vertex depth** / **search depth**, + **paths** (all optionally), and set of **visited vertices**. - Examples: See `example-traversal-breadth-first-in-maze` and `example-traversal-breadth-first-towers-hanoi`. @@ -47,35 +47,59 @@ class documentation in the API reference for details. - Algorithm *Depth First Search* ("DFS"). - Follows just one outgoing edge per vertex as long as possible, - and **goes back a step to some already visited vertex and follows a - further edge starting there only when necessary** to come to new vertices. - **A vertex is reported before the first outgoing edge is taken**. - Start vertices are not reported. - - - The traversal state provides **search depth** (optionally), - **paths** (optionally), and set of **visited vertices**. - - - Example: See `example-traversal-depth-first-integers`. + and **goes back a step and follows a further edge starting there,** + **or then an edge starting at the next start vertex,** + **only when necessary** to come to new vertices. + A vertex is considered *visited* when its expansion starts (its + successors are about to be read from the graph). And, by default, + the vertex is also reported at this moment, except for the start + vertices - they are not reported. + + - The traversal state provides **search depth**, **paths**, + **trace**, **trace_labels**, **on_trace**, **index**, + and set of **visited vertices** (all optionally), + and **event** (when a vertex is reported). + + - Examples: See + `depth-first search in the integers `, + `depth-limited depth-first search `, + `iterative deepening depth-first search `, + `longest path ` + between two vertices in a weighted graph or in an unweighted graph, + `strongly connected components ` + of a graph, and + `biconnected components of a connected undirected graph + `. + + - Note: This class supports to + `skip the expansion of individual vertices `. + + .. versionchanged:: 3.4 + Start vertices are now evaluated successively. + Attributes event, trace, trace_labels, + on_trace, and index added. Options to control them + added. Expansion of vertices can be skipped. - Class `nographs.TraversalNeighborsThenDepth` - - Algorithm similar to *Depth First Search* ("DFS"), but vertex order - is slightly changed. + - Algorithm similar to *Depth First Search* ("DFS"), but with changed + vertex order. - Follows just one outgoing edge per vertex as long as possible, - and **goes back a step to some already visited vertex and follows a - further edge starting there only when necessary** to come to new vertices. - **Vertices are reported when they are first "seen" as direct neighbors** - of a currently visited vertex. + and **goes back a step and follows a further edge starting there,** + **or then an edge starting at the next start vertex,** + **only when necessary** to come to new vertices. - - The traversal state provides **search depth**, **paths** (optionally), - and set of **visited vertices**. + A vertex is reported and marked as *visited* when it is "seen" + (read from the graph) for the first time. Start vertices + are considered visited, but they are not reported. - - Example: See `example-traversal-depth-first-integers`. + - The traversal state provides **search depth**, **paths** (all optionally), + and set of **visited vertices**. - .. versionchanged:: 3.0 + - Examples: See `example-traversal-depth-first-integers`. - Strategy class `nographs.TraversalNeighborsThenDepth` introduced. + .. versionadded:: 3.0 - Class `nographs.TraversalTopologicalSort` @@ -88,10 +112,15 @@ class documentation in the API reference for details. (indirect) predecessors of each other. If there is such a cycle in the graph, this is detected. - - The traversal state provides **search depth**, **paths** (optionally), + - The traversal state provides **search depth**, **paths** (all optionally), and set of **visited vertices**. - - Example: See `example-topological_sorting_processes`. + - Example: See `example-topological_sorting_processes`, + `critical path in a weighted, acyclic graph `, + and + `longest path between two vertices in a weighted, acyclic graph + `. + .. _examples_all_graphs: @@ -513,8 +542,11 @@ vertex depths or distances. These are the following: iterator skips vertices as long as their depth is lower than *start*. From then on, it reports the found vertices. It stops when the reached depth is higher than *stop*. - Note: The first vertex with a depth higher than *stop* will be consumed from the - traversal, but will not be reported, so it is lost (compare *itertools.takewhile*). + .. note:: + + The first vertex with a depth higher than *stop* will be consumed from the + traversal, but will not be reported, so it is lost + (compare *itertools.takewhile*). .. _example_go_for_depth_range: @@ -539,8 +571,11 @@ vertex depths or distances. These are the following: then on, is reports the found vertices. It stops when the reached distance is higher than *stop*. - Note: The first vertex with a distance higher than *stop* will be consumed from the - traversal, but will not be reported, so it is lost (compare *itertools.takewhile*). + .. note:: + + The first vertex with a distance higher than *stop* will be consumed from the + traversal, but will not be reported, so it is lost + (compare *itertools.takewhile*). .. _example_go_for_distance_range: @@ -558,6 +593,111 @@ vertex depths or distances. These are the following: (10, 11, 12, 13, 14, 15, 16, 17, 18, 19) +.. _dfs_expansion_skipping: + +Skipping vertex expansion in TraversalDepthFirst +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 3.4 + +In section +`DFS: all paths and walks `, +when we computed the possible paths from vertex *A* to vertex *C* +in the following graph, we +removed the successors of *C* from the graph before the search to +prevent the search from further extending a path beyond *C*. + + >>> successors = { + ... "A": ["B1", "B2"], + ... "B1": ["C", "B"], + ... "B2": ["C", "B"], + ... "B": ["B1", "B2"], + ... "C": ["B"], + ... } + >>> def next_vertices(v, _): + ... return successors.get(v, ()) + +The class *TraversalDepthFirst* offers another method to achieve the same effect +in a more dynamical way: The +**application code can signal to the traversal that** +**the vertex that has just been entered should not be expanded**, i.e., +edges to successors should be ignored. + +There are two equivalent ways to do this: + +- **Calling method** *skip_expanding_entered_vertex()* **of the traversal object**. + +- **Throwing a** *StopIteration()* **to the generator** provided by method + *__iter__* **of the traversal**. This is what the above method does. + *throw()* returns the vertex to confirm the success. + +**Example: Pruning paths at the required end vertex** + +The following code shows, how all paths starting at *A* and ending at *C* +can be computed with skipping the expansion of *C* during the traversal +instead of removing the edges from *C* to successors before the traversal. + +.. code-block:: python + + >>> traversal = nog.TraversalDepthFirst(next_vertices) + >>> _ = traversal.start_from("A", mode=nog.DFSMode.ALL_PATHS, compute_trace=True) + + >>> for v in traversal: + ... if v == "C": + ... print(traversal.trace) + ... traversal.skip_expanding_entered_vertex() + ['A', 'B2', 'B', 'B1', 'C'] + ['A', 'B2', 'C'] + ['A', 'B1', 'B', 'B2', 'C'] + ['A', 'B1', 'C'] + +Caution is advised when using the +*report* parameter of method *TraversalDepthFirst.start_from()* +to get reports about events other than +*DFSEvent.ENTERING* and *DFSEvent.ENTERING_START*: +If such an event occurs, **no vertex has been entered, and** +it is therefor +**not allowed to signal to the traversal to skip the entered (!) vertex**. +If you do this anyway, the traversal intentionally won’t catch the +*StopIteration* you throw, and a *RuntimeError* will be raised +(according to `PEP 497 `_). + +This also means, that it is always save to ignore the return value of +throwing the *StopIteration* into the generator: it can only be the entered +vertex again (the success signal for skipping the expansion of +the vertex). Otherwise, a *RuntimeError* would have been raised. + +**Example: Only the expansion of entered (!) vertices can be skipped** + +We visit the vertices of the DFS-tree of the above graph. +As start vertices, we give two times (!) vertex *A*. +And we demand that the traversal both reports when a start vertex is entered and +when it is skipped because it has already been entered before. + +First, vertex *A* it reported with event *ENTERING_START*. Here, +throwing *StopIteration* is accepted +by the generator and the generator skips expanding the vertex. + +Then, vertex *A* is reported with event *SKIPPING_START*. +This means, *A* it is not entered. Here, throwing *StopIteration* is not +accepted and a *RuntimeError* is raised. + +.. code-block:: python + + >>> _ = traversal.start_from( + ... start_vertices="AA", mode=nog.DFSMode.DFS_TREE, + ... report=nog.DFSEvent.ENTERING_START | nog.DFSEvent.SKIPPING_START) + >>> generator = iter(traversal) + >>> next(generator), str(traversal.event) + ('A', 'DFSEvent.ENTERING_START') + >>> generator.throw(StopIteration()) + 'A' + >>> next(generator), str(traversal.event) + ('A', 'DFSEvent.SKIPPING_START') + >>> generator.throw(StopIteration()) + Traceback (most recent call last): + RuntimeError: generator raised StopIteration + .. _is_tree: Traversing trees diff --git a/docs/source/vertex_identity.rst b/docs/source/vertex_identity.rst index 6b8b7bb..d25a9d8 100644 --- a/docs/source/vertex_identity.rst +++ b/docs/source/vertex_identity.rst @@ -38,6 +38,8 @@ Here are some cases, and examples for each case: - **Two vertex objects are the same vertex if and only if they are identical** + Example: + You use instances of some vertex class of your application as vertices. They are mutable, and thus not hashable. Each vertex object is to be seen as a different vertex. @@ -47,6 +49,8 @@ Here are some cases, and examples for each case: - **Mutable vertex objects, and their immutable counterparts identify them** + Example: + You use lists as your vertices. You know that their content will not change during a traversal run. And the immutable tuple counterpart of a vertex is well suited for getting a hash value. @@ -55,6 +59,8 @@ Here are some cases, and examples for each case: - **Traversal in equivalence classes of vertices** + Example: + You have defined an abstraction function, that assigns an equivalence class to a vertex. And you know: Whenever there is a path of vertices, there is a respective path in the equivalence classes of these vertices. And whenever @@ -67,7 +73,7 @@ Here are some cases, and examples for each case: .. _equivalence_class_example: -**Example:** +**Example: Traversal of vertex equivalence classes, on the fly** We have a maze of the following form, where "S" denotes a start position of a player, "G" a goal position, "." additional positions that can be occupied, @@ -201,7 +207,7 @@ Of cause, we get the same result: depth 5. But now, we get it after only 7,290 instead of 76,519 search steps. So, **vertex equivalences helped us to reduce the needed search effort**. -And NoGraphs helped us: +And NoGraphs assisted us: - We just define the `VertexToID` function, and NoGraphs **computes the graphs** **of vertex equivalence classes automatically**. diff --git a/pyproject.toml b/pyproject.toml index 01c105d..6a00d58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,49 @@ -[build-system] -requires = ["setuptools>=57"] -build-backend = "setuptools.build_meta" +[project] +name = "nographs" +version = "3.4.0" +authors = [ + { name="Dr. Helmut Melcher", email="HeWeMel@web.de" }, +] +description = """\ + Graph analysis – the lazy (evaluation) way: Analysis on the fly, \ + for graphs, that are computed and/or adapted on the fly.\ + """ +readme = "README.rst" +requires-python = ">=3.9" +keywords = [ + "graph", "network", "search", "traverse", "analysis", "infinite", "lazy", + "shortest", "distance", "depth", "DFS", "breadth", "BFS", "Dijkstra", + "topological", "spanning", "MST", "TSP" +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "Operating System :: OS Independent", + "Topic :: Scientific/Engineering :: Information Analysis", + "Topic :: Scientific/Engineering :: Mathematics", + "Topic :: Software Development :: Libraries :: Python Modules", + "Typing :: Typed", +] + +[project.urls] +Homepage = "https://github.com/hewemel/nographs" +Documentation = "https://nographs.readthedocs.io/" +Repository = "https://github.com/hewemel/nographs.git" +Issues = "https://github.com/hewemel/nographs/issues" +Changelog = "https://nographs.readthedocs.io/en/latest/changelog.html" + [tool.coverage.report] exclude_lines =["pragma: no cover", "@overload"] + +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" diff --git a/requirements_ci.txt b/requirements_ci.txt index 9f2f4df..a205fa4 100644 --- a/requirements_ci.txt +++ b/requirements_ci.txt @@ -2,7 +2,10 @@ black coverage>=6.3 flake8 +flake8-bugbear mypy -# -- additional packaged used only for test with them -- +# -- source consistency checking -- +pymacros4py>=0.8.2 ; python_version >= "3.10" +# -- additional packages for tests using them -- mpmath -intbitset ; python_version < "3.12" and implementation_name == "cpython" # missing on PyPy and (so far) CPython 3.12 +intbitset ; python_version < "3.13" and implementation_name == "cpython" # missing on PyPy and often on new CPython-versions diff --git a/requirements_dev.txt b/requirements_dev.txt index 64ac93c..5405348 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -10,7 +10,10 @@ sphinx-rtd-theme==1.2.2 black coverage>=6.3 flake8 +flake8-bugbear mypy -# -- additional packaged used only for test with them -- +# -- source consistency checking -- +pymacros4py>=0.8.2 ; python_version >= "3.10" +# -- additional packages for tests using them -- mpmath -intbitset ; python_version < "3.12" and implementation_name == "cpython" # missing on PyPy 3.11 and (so far) CPython 3.12 \ No newline at end of file +intbitset ; python_version < "3.13" and implementation_name == "cpython" # missing on PyPy and often on new CPython-versions diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..99ccfc8 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,7 @@ +[pycodestyle] +max-line-length = 88 +exclude = tpl,html,.coverage,py.typed +[flake8] +max-line-length = 80 +extend-select = B950 +extend-ignore = E203,E501,E701 diff --git a/setup.py b/setup.py deleted file mode 100644 index 44b1255..0000000 --- a/setup.py +++ /dev/null @@ -1,50 +0,0 @@ -from setuptools import setup, find_packages -import pathlib - -here = pathlib.Path(__file__).parent.resolve() - -long_description = (here / 'README.rst').read_text(encoding='utf-8') - -setup( - name="nographs", - version="3.3.2", - description=("Graph analysis – the lazy (evaluation) way: Analysis on the fly, " - + "for graphs, that are computed and/or adapted on the fly."), - long_description=long_description, - long_description_content_type="text/x-rst", - url="https://github.com/HeWeMel/nographs", - author="Dr. Helmut Melcher", - author_email='HeWeMel@web.de', - packages=find_packages('src'), - package_dir={'': 'src'}, - package_data={"nographs": ["py.typed"]}, - # install_requires=[], - classifiers=[ - 'Development Status :: 5 - Production/Stable', - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", - "Intended Audience :: Developers", - "Intended Audience :: Science/Research", - "Operating System :: OS Independent", - "Topic :: Scientific/Engineering :: Information Analysis", - "Topic :: Scientific/Engineering :: Mathematics", - "Topic :: Software Development :: Libraries :: Python Modules", - "Typing :: Typed", - ], - keywords=('graph,network,search,traverse,analysis,infinite,lazy,shortest,distance,' - + 'depth,DFS,breadth,BFS,Dijkstra,topological,spanning,MST,TSP'), - python_requires='>=3.9, <4', - project_urls={ - 'Documentation': 'https://nographs.readthedocs.io/', - 'Source': 'https://github.com/hewemel/nographs/', - 'Bug Reports': 'https://github.com/hewemel/nographs/issues', - # 'Say Thanks!': 'http://saythanks.io/to/HeWeMel', - }, - license_files='LICENSE', -) diff --git a/src/nographs/__init__.py b/src/nographs/__init__.py index f62c04f..5680248 100644 --- a/src/nographs/__init__.py +++ b/src/nographs/__init__.py @@ -64,7 +64,7 @@ # VertexIdSet, # VertexIdToVertexMapping, # VertexIdToDistanceMapping, - # VertexIdToPathEdgeDataMapping, + # VertexIdToEdgeLabelsMapping, # MutableSequenceOfVertices, # -- Gear protocols GearWithoutDistances, @@ -91,41 +91,29 @@ # PathsOfUnlabeledEdges, # PathsOfLabeledEdges, # PathsDummy, - # DummyPredecessorOrAttributesMapping, + # DummyPredecessorOrLabelsMapping, ) +from ._path import Path from ._strategies import ( - # StrRepr, - Strategy, T_strategy, - NextVertices, # Usable, in rare cases, for typing application-defined functions + NextVertices, NextEdges, NextLabeledEdges, NextWeightedEdges, NextWeightedLabeledEdges, - # NextEdgesOrVertices, - # NextWeightedMaybeLabeledEdges, BNextVertices, BNextEdges, BNextLabeledEdges, BNextWeightedEdges, BNextWeightedLabeledEdges, - # BNextEdgesOrVertices, - # BNextWeightedMaybeLabeledEdges, - # iter_start_ids, - # iter_start_vertices_and_ids, - # define_visited, - # define_distances, - # create_paths, - # NoIterator, - # NoVisitedSet, - # NoDistancesMapping, -) -from ._traversals import ( + Strategy, Traversal, TraversalBreadthFirstFlex, TraversalBreadthFirst, TraversalDepthFirstFlex, TraversalDepthFirst, + DFSEvent, + DFSMode, TraversalNeighborsThenDepthFlex, TraversalNeighborsThenDepth, TraversalTopologicalSortFlex, @@ -136,22 +124,18 @@ TraversalAStar, TraversalMinimumSpanningTreeFlex, TraversalMinimumSpanningTree, -) -from ._path import Path -from ._bidir_search import ( BSearchBreadthFirstFlex, BSearchBreadthFirst, BSearchShortestPathFlex, BSearchShortestPath, + TraversalShortestPathsInfBranchingSortedFlex, + TraversalShortestPathsInfBranchingSorted, ) + from ._extra_edge_gadgets import ( adapt_edge_index, adapt_edge_iterable, ) -from ._extra_infinite_branching import ( - TraversalShortestPathsInfBranchingSortedFlex, - TraversalShortestPathsInfBranchingSorted, -) from ._extra_matrix_gadgets import ( Vector, Limits, @@ -241,6 +225,8 @@ "TraversalBreadthFirst", "TraversalDepthFirstFlex", "TraversalDepthFirst", + "DFSEvent", + "DFSMode", "TraversalNeighborsThenDepthFlex", "TraversalNeighborsThenDepth", "TraversalTopologicalSortFlex", diff --git a/src/nographs/_extra_edge_gadgets.py b/src/nographs/_extra_edge_gadgets.py index 7a96b09..eb29976 100644 --- a/src/nographs/_extra_edge_gadgets.py +++ b/src/nographs/_extra_edge_gadgets.py @@ -206,7 +206,7 @@ def adapt_edge_iterable( else: # Only start and end vertices of the edge should be used. Optional # additional data is ignored. - for from_vertex, to_vertex, *data in edges: + for from_vertex, to_vertex, *_data in edges: edge_dict[from_vertex].append(to_vertex) edge_dict[to_vertex].append(from_vertex) return adapt_edge_index(edge_dict, add_inverted=False, attributes=False) diff --git a/src/nographs/_extra_matrix_gadgets.py b/src/nographs/_extra_matrix_gadgets.py index da6ee48..9865807 100644 --- a/src/nographs/_extra_matrix_gadgets.py +++ b/src/nographs/_extra_matrix_gadgets.py @@ -177,7 +177,7 @@ def size(self) -> Sequence[int]: # inspection PyShadowingNames size = [] area = self.content - for d in range(self.dimensions): + for _d in range(self.dimensions): size.append(len(area)) area = area[0] return size diff --git a/src/nographs/_extra_tsp.py b/src/nographs/_extra_tsp.py index 8463543..e33f324 100644 --- a/src/nographs/_extra_tsp.py +++ b/src/nographs/_extra_tsp.py @@ -13,10 +13,7 @@ Gear, GearDefault, ) -from ._bidir_search import ( - BSearchShortestPathFlex, -) - +from ._strategies import BSearchShortestPathFlex # -- Protocols -- T_key_contra = TypeVar("T_key_contra", contravariant=True) @@ -102,7 +99,7 @@ def undo_weight_changes_in_travel_length( # no_of_edges times. # (We have no multiplication in T_weight, so we repeat a subtraction. But since we # do this only once per vertex in the path, the additional runtime does not matter.) - for i in range(no_of_edges): + for _i in range(no_of_edges): length -= weight_offset # Undo the negation of the edge weights. We can do this in one step for the whole # path length. diff --git a/src/nographs/_gears.py b/src/nographs/_gears.py index c377ca9..ae5b213 100644 --- a/src/nographs/_gears.py +++ b/src/nographs/_gears.py @@ -65,10 +65,11 @@ class based on a wrapped sequence, NoGraphs directly accesses the sequence for VertexMappingWrappingSequenceWithNone class based on a wrapped sequence, NoGraphs directly accesses the sequence for better performance. -(The sequence might need to represent gaps without stored values for some -key by None as artificial value. Thus, NoGraphs must be prepared to retrieve +The sequence might need to represent gaps without stored values for some +key by None as artificial value, since non-empty content of T_vertex +can be any other value. Thus, NoGraphs must be prepared to retrieve None from the sequence, even if it cannot store this value in case it is -outside T_vertex. Therefore, here, VertexMappingWrappingSequenceWithNone is used.) +outside T_vertex. Therefore, here, VertexMappingWrappingSequenceWithNone is used. """ VertexIdToVertexMapping = VertexMapping[T_vertex_id, T_vertex] @@ -87,23 +88,41 @@ class based on a wrapped sequence, NoGraphs directly accesses the sequence for """ABC for a collection that is intended to be used by NoGraphs for storing mappings from your chosen type of hashable vertex ids to edge data. -If a VertexIdToPathEdgeDataMapping is implemented by a +If a VertexIdToEdgeLabelsMapping is implemented by a VertexMappingWrappingSequenceWithNone class based on a wrapped sequence, NoGraphs directly accesses the sequence for better performance. -(The sequence might need to represent gaps without stored values for some -key by None as artificial value. Thus, NoGraphs must be prepared to retrieve +The sequence might need to represent gaps without stored values for some +key by None as artificial value, since non-empty content of T_labels +can be any other value. Thus, NoGraphs must be prepared to retrieve None from the sequence, even if it cannot store this value because it is outside type T_labels.Therefore, here, -VertexMappingWrappingSequenceWithNone is used.) +VertexMappingWrappingSequenceWithNone is used. """ -VertexIdToPathEdgeDataMapping = VertexMapping[T_vertex_id, T_labels] +VertexIdToEdgeLabelsMapping = VertexMapping[T_vertex_id, T_labels] +""" +ABC for a collection that is intended to be used by NoGraphs for +storing mappings from vertices, represented by your chosen type of hashable +vertex ids, to non-negative integers (e.g., time values from 0 on, that +represent the order in that the vertices have be entered or left during a +traversal). + +If a VertexIdToDistanceMapping is implemented by a +VertexMappingWrappingSequenceWithoutNone +class based on a wrapped sequence, NoGraphs directly accesses the sequence for +better performance. +""" +VertexIdToNumberMapping = VertexMapping[T_vertex_id, int] """ ABC for a MutableSequence of vertices. """ MutableSequenceOfVertices = MutableSequence[T_vertex] +""" ABC for a MutableSequence of edge attributes. +""" +MutableSequenceOfLabels = MutableSequence[T_labels] + # -- Private support functions -- @@ -149,9 +168,9 @@ def vertex_id_to_vertex_mapping( raise NotImplementedError @abstractmethod - def vertex_id_to_path_attributes_mapping( + def vertex_id_to_edge_labels_mapping( self, initial_content: Iterable[Tuple[T_vertex_id, T_labels]] - ) -> VertexIdToPathEdgeDataMapping[T_vertex_id, T_labels]: + ) -> VertexIdToEdgeLabelsMapping[T_vertex_id, T_labels]: """Factory for a mapping from a vertex id to edge data. :param initial_content: The collection is created with this initial content. @@ -165,6 +184,27 @@ def sequence_of_vertices( """Factory for a sequence of vertices.""" raise NotImplementedError + @abstractmethod + def sequence_of_edge_labels( + self, initial_content: Iterable[T_labels] + ) -> MutableSequenceOfLabels[T_labels]: + """Factory for a sequence of edge attributes.""" + raise NotImplementedError + + @abstractmethod + def vertex_id_to_number_mapping( + self, initial_content: Iterable[Tuple[T_vertex_id, int]] + ) -> VertexIdToNumberMapping[T_vertex_id]: + """Factory for a mapping from a vertex id to non-negative integers + starting at zero, that represent a numbering of some vertices. + + If the returned mapping does not contain a value for some key, + its method __getitem__ needs to return 0. + + :param initial_content: The collection is created with this initial content. + """ + raise NotImplementedError + class Gear( Generic[T_vertex, T_vertex_id, T_weight, T_labels], @@ -275,7 +315,7 @@ def vertex_id_to_vertex_mapping( ) -> VertexMapping[T_vertex_id, T_vertex]: return dict[T_vertex_id, T_vertex](initial_content) - def vertex_id_to_path_attributes_mapping( + def vertex_id_to_edge_labels_mapping( self, initial_content: Iterable[Tuple[T_vertex_id, T_labels]] ) -> VertexMapping[T_vertex_id, T_labels]: return dict[T_vertex_id, T_labels](initial_content) @@ -285,6 +325,11 @@ def sequence_of_vertices( ) -> MutableSequenceOfVertices[T_vertex]: return list[T_vertex](initial_content) + def sequence_of_edge_labels( + self, initial_content: Iterable[T_labels] + ) -> MutableSequenceOfLabels[T_labels]: + return list[T_labels](initial_content) + def zero(self) -> T_weight: return self._zero_value @@ -298,6 +343,11 @@ def vertex_id_to_distance_mapping( lambda: self._infinity_value, initial_content ) + def vertex_id_to_number_mapping( + self, initial_content: Iterable[Tuple[T_vertex_id, int]] + ) -> VertexIdToNumberMapping[T_vertex_id]: + return DefaultdictWithNiceStr[T_vertex_id, int](lambda: 0, initial_content) + # Implementation note: # In the following, a separate class GearForHashableVertexIDsAndIntsMaybeFloats is used @@ -486,7 +536,7 @@ def vertex_id_to_vertex_mapping( lambda: [None] * self._pre_allocate, None, 1024, initial_content ) - def vertex_id_to_path_attributes_mapping( + def vertex_id_to_edge_labels_mapping( self, initial_content: Iterable[Tuple[IntVertexID, T_labels]] ) -> VertexMapping[IntVertexID, T_labels]: return VertexMappingWrappingSequenceWithNone[T_labels]( @@ -504,6 +554,11 @@ def zero(self) -> T_weight: def infinity(self) -> T_weight: return self._infinity_value + def sequence_of_edge_labels( + self, initial_content: Iterable[T_labels] + ) -> MutableSequenceOfLabels[T_labels]: + return list[T_labels](initial_content) + def vertex_id_to_distance_mapping( self, initial_content: Iterable[Tuple[IntVertexID, T_weight]] ) -> VertexMapping[IntVertexID, T_weight]: @@ -514,6 +569,21 @@ def vertex_id_to_distance_mapping( initial_content, ) + def vertex_id_to_number_mapping( + self, initial_content: Iterable[Tuple[IntVertexID, int]] + ) -> VertexIdToNumberMapping[IntVertexID]: + # This implementation is limited to 2^32 values, meaning 2^31 vertices + # when numbering enter and leave events, thus 2.147.483.648 vertices. + return VertexMappingWrappingSequenceWithoutNone[int]( + lambda: array( + "L", + repeat(0, self._pre_allocate), + ), + 0, + 1024, + initial_content, + ) + class GearForIntVertexIDsAndIntsMaybeFloats( GearForIntVertexIDs[T_vertex, float, T_labels] diff --git a/src/nographs/_paths.py b/src/nographs/_paths.py index 989a536..9d9a0d3 100644 --- a/src/nographs/_paths.py +++ b/src/nographs/_paths.py @@ -22,7 +22,7 @@ ) from ._gears import ( VertexIdToVertexMapping, - VertexIdToPathEdgeDataMapping, + VertexIdToEdgeLabelsMapping, ) @@ -113,6 +113,10 @@ def predecessor(self, vertex: T_vertex) -> Optional[T_vertex]: path is empty. Raise RuntimeError is no path to *vertex* is stored. :param vertex: The predecessor of this vertex will be returned. + + .. versionchanged:: 3.3 + + Method added. """ vertex_id = self._check_vertex(vertex) predecessor_collection = self._predecessor_collection @@ -190,8 +194,8 @@ def iter_edges_to_start( :param vertex: The path ending at this vertex will be iterated. """ - for from_vertex, vertex, vertex_id in self._iter_raw_edges_to_start(vertex): - yield (from_vertex, vertex) + for from_vertex, to_vertex, _vertex_id in self._iter_raw_edges_to_start(vertex): + yield (from_vertex, to_vertex) def iter_edges_from_start( self, vertex: T_vertex @@ -262,7 +266,7 @@ def __getitem__(self, vertex: T_vertex) -> Union[ """ -class DummyPredecessorOrAttributesMapping(MutableMapping[T_vertex_id, T]): +class DummyPredecessorOrLabelsMapping(MutableMapping[T_vertex_id, T]): def __getitem__(self, key: T_vertex_id) -> T: raise KeyError @@ -291,7 +295,7 @@ class PathsDummy(Paths[T_vertex, T_vertex_id, T_labels]): def __init__(self, vertex_to_id: VertexToID[T_vertex, T_vertex_id]) -> None: super().__init__( - DummyPredecessorOrAttributesMapping[T_vertex_id, T_vertex](), + DummyPredecessorOrLabelsMapping[T_vertex_id, T_vertex](), vertex_to_id, ) @@ -379,7 +383,7 @@ class PathsOfLabeledEdges(Paths[T_vertex, T_vertex_id, T_labels]): :param predecessor: The predecessor information of the paths will be stored in the given mapping. - :param attributes: The edge data of the paths will be stored in + :param labels: The edge data of the paths will be stored in the given mapping. :param vertex_to_id: See `VertexToID` function. @@ -388,24 +392,24 @@ class PathsOfLabeledEdges(Paths[T_vertex, T_vertex_id, T_labels]): def __init__( self, predecessor: VertexIdToVertexMapping[T_vertex_id, T_vertex], - attributes: VertexIdToPathEdgeDataMapping[T_vertex_id, T_labels], + labels: VertexIdToEdgeLabelsMapping[T_vertex_id, T_labels], vertex_to_id: VertexToID[T_vertex, T_vertex_id], ): super().__init__(predecessor, vertex_to_id) - self._attributes = attributes - self._attributes_collection: GettableSettableForGearProto[ + self._labels = labels + self._labels_collection: GettableSettableForGearProto[ T_vertex_id, T_labels, Optional[T_labels] ] - self._attributes_wrapper: Optional[ + self._labels_wrapper: Optional[ VertexSequenceWrapperForMappingProto[ T_vertex_id, T_labels, Optional[T_labels] ] ] ( _, - self._attributes_collection, - self._attributes_wrapper, - ) = access_to_vertex_mapping_expect_none(attributes) + self._labels_collection, + self._labels_wrapper, + ) = access_to_vertex_mapping_expect_none(labels) def append_edge( self, from_vertex: T_vertex, to_vertex_id: T_vertex_id, to_edge: LabeledOutEdge @@ -432,31 +436,31 @@ def append_edge( data_of_edge = to_edge[-1] try: - self._attributes_collection[to_vertex_id] = data_of_edge + self._labels_collection[to_vertex_id] = data_of_edge except IndexError: # See access_to_vertex_mapping_expect_none for the following: - assert self._attributes_wrapper is not None - self._attributes_wrapper.extend_and_set(to_vertex_id, data_of_edge) + assert self._labels_wrapper is not None + self._labels_wrapper.extend_and_set(to_vertex_id, data_of_edge) def _iter_path_edges( - self, path_edges: Iterator[tuple[T_vertex, T_vertex, T_vertex_id]] + self, edges: Iterator[tuple[T_vertex, T_vertex, T_vertex_id]] ) -> Iterator[UnweightedLabeledFullEdge[T_vertex, T_labels]]: """Iterate given raw edge data tuples und return them as UnweightedLabeledFullEdge. Internal support method. - :param path_edges: tuples (from_vertex, to_vertex, to_vertex_id) + :param edges: tuples (from_vertex, to_vertex, to_vertex_id) """ - path_attributes: Optional[T_labels] + labels: Optional[T_labels] res: UnweightedLabeledFullEdge[T_vertex, T_labels] - for from_vertex, vertex, vertex_id in path_edges: + for from_vertex, vertex, vertex_id in edges: # to_vertex_id is contained in self._predecessor # (see _iter_raw_edges_to_start). # So we know: No KeyError or IndexError will occur here, and we will not # get None as result. - path_attributes = self._attributes_collection[vertex_id] - assert path_attributes is not None + labels = self._labels_collection[vertex_id] + assert labels is not None # noinspection PyTypeChecker - res = (from_vertex, vertex, path_attributes) # PyCharm cannot check this + res = (from_vertex, vertex, labels) # PyCharm cannot check this yield res def iter_labeled_edges_to_start( diff --git a/src/nographs/_strategies/__init__.py b/src/nographs/_strategies/__init__.py new file mode 100644 index 0000000..6ca027d --- /dev/null +++ b/src/nographs/_strategies/__init__.py @@ -0,0 +1,100 @@ +from .strategy import Strategy + +from .type_aliases import ( + T_strategy, + NextVertices, + NextEdges, + NextLabeledEdges, + NextWeightedEdges, + NextWeightedLabeledEdges, + BNextVertices, + BNextEdges, + BNextLabeledEdges, + BNextWeightedEdges, + BNextWeightedLabeledEdges, +) + +from .traversals.traversal import Traversal + +from .traversals.without_weights.breadth_first import ( + TraversalBreadthFirst, + TraversalBreadthFirstFlex, +) +from .traversals.without_weights.depth_first import ( + TraversalDepthFirst, + TraversalDepthFirstFlex, + DFSEvent, + DFSMode, +) +from .traversals.without_weights.neighbors_then_depth import ( + TraversalNeighborsThenDepth, + TraversalNeighborsThenDepthFlex, +) +from .traversals.without_weights.topological_sort import ( + TraversalTopologicalSort, + TraversalTopologicalSortFlex, +) + +from .traversals.with_weights.shortest_paths import ( + TraversalShortestPaths, + TraversalShortestPathsFlex, +) +from .traversals.with_weights.a_star import TraversalAStar, TraversalAStarFlex +from .traversals.with_weights.minimum_spanning_tree import ( + TraversalMinimumSpanningTree, + TraversalMinimumSpanningTreeFlex, +) +from .traversals.with_weights.extra_infinite_branching import ( + TraversalShortestPathsInfBranchingSortedFlex, + TraversalShortestPathsInfBranchingSorted, +) + +from .bidirectional_search.breadth_first import ( + BSearchBreadthFirst, + BSearchBreadthFirstFlex, +) +from .bidirectional_search.shortest_path import ( + BSearchShortestPath, + BSearchShortestPathFlex, +) + +__all__ = ( + # -- strategy -- + "Strategy", + "T_strategy", + "NextVertices", + "NextEdges", + "NextLabeledEdges", + "NextWeightedEdges", + "NextWeightedLabeledEdges", + "BNextVertices", + "BNextEdges", + "BNextLabeledEdges", + "BNextWeightedEdges", + "BNextWeightedLabeledEdges", + # -- traversal -- + "Traversal", + "TraversalBreadthFirstFlex", + "TraversalBreadthFirst", + "TraversalDepthFirstFlex", + "TraversalDepthFirst", + "DFSEvent", + "DFSMode", + "TraversalNeighborsThenDepthFlex", + "TraversalNeighborsThenDepth", + "TraversalTopologicalSortFlex", + "TraversalTopologicalSort", + "TraversalShortestPathsFlex", + "TraversalShortestPaths", + "TraversalAStarFlex", + "TraversalAStar", + "TraversalMinimumSpanningTreeFlex", + "TraversalMinimumSpanningTree", + "TraversalShortestPathsInfBranchingSortedFlex", + "TraversalShortestPathsInfBranchingSorted", + # -- bidir search -- + "BSearchBreadthFirstFlex", + "BSearchBreadthFirst", + "BSearchShortestPathFlex", + "BSearchShortestPath", +) diff --git a/src/nographs/_strategies/bidirectional_search/__init__.py b/src/nographs/_strategies/bidirectional_search/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/nographs/_strategies/bidirectional_search/base.py b/src/nographs/_strategies/bidirectional_search/base.py new file mode 100644 index 0000000..2b69252 --- /dev/null +++ b/src/nographs/_strategies/bidirectional_search/base.py @@ -0,0 +1,95 @@ +from __future__ import annotations + +from typing import Optional, Any + +from nographs._types import ( + T_vertex, + T_weight, + T_labels, +) +from ..type_aliases import ( + T_strategy, + BNextVertices, + BNextEdges, + BNextEdgesOrVertices, + BNextWeightedMaybeLabeledEdges, + BNextLabeledEdges, + BNextWeightedEdges, + BNextWeightedLabeledEdges, +) + + +# --------------- internal support functions ------------- + + +def _search_needs_search_object(obj: Any, needed_class: type) -> None: + if not isinstance(obj, needed_class): + raise RuntimeError( + "Method start_from can only be called on a search strategy object." + ) + + +def _create_unified_next_bidirectional( + next_vertices: Optional[BNextVertices[T_vertex, T_strategy]], + next_edges: Optional[BNextEdges[T_vertex, T_strategy]], + next_labeled_edges: Optional[BNextLabeledEdges[T_vertex, T_strategy, T_labels]], +) -> tuple[BNextEdgesOrVertices[T_vertex, T_strategy, T_labels], bool, bool]: + """Check configuration of given next_vertices, next_edges, and next_labeled_edges + function pairs + and calculate a pair of unified NextEdgesOrVertices + and whether we have edges with data (weights and/or labels) and/or labeled_edges. + """ + next_edges_or_vertices: BNextEdgesOrVertices[T_vertex, T_strategy, T_labels] + if next_vertices is not None: + if next_edges is not None: + raise RuntimeError("Both next_vertices and next_edges provided.") + if next_labeled_edges is not None: + raise RuntimeError("Both next_vertices and next_labeled_edges provided.") + next_edges_or_vertices = next_vertices + edges_with_data = False + labeled_edges = False + elif next_edges is not None: + if next_labeled_edges is not None: + raise RuntimeError("Both next_edges and next_labeled_edges provided.") + next_edges_or_vertices = next_edges + edges_with_data = True + labeled_edges = False + else: + if next_labeled_edges is None: + raise RuntimeError( + "Neither next_vertices nor next_edges " + + "nor next_labeled_edges provided." + ) + next_edges_or_vertices = next_labeled_edges + edges_with_data = True + labeled_edges = True + return next_edges_or_vertices, edges_with_data, labeled_edges + + +def _create_unified_next_weighted_bidirectional( + next_edges: Optional[BNextWeightedEdges[T_vertex, T_strategy, T_weight]], + next_labeled_edges: Optional[ + BNextWeightedLabeledEdges[T_vertex, T_strategy, T_weight, T_labels] + ], +) -> tuple[ + BNextWeightedMaybeLabeledEdges[T_vertex, T_strategy, T_weight, T_labels], + bool, +]: + """Check configuration of given next_edges and next_labeled_edges function pairs + and calculate a pair of unified _NextWeightedMaybeLabeledEdges and whether we have + labeled_edges. + """ + next_maybe_labeled_edges: BNextWeightedMaybeLabeledEdges[ + T_vertex, T_strategy, T_weight, T_labels + ] + if next_edges is not None: + if next_labeled_edges is not None: + raise RuntimeError("Both next_edges and next_labeled_edges provided.") + next_maybe_labeled_edges = next_edges + labeled_edges = False + else: + if next_labeled_edges is None: + raise RuntimeError("Neither next_edges and next_labeled_edges provided.") + next_maybe_labeled_edges = next_labeled_edges + labeled_edges = True + return next_maybe_labeled_edges, labeled_edges diff --git a/src/nographs/_strategies/bidirectional_search/breadth_first.py b/src/nographs/_strategies/bidirectional_search/breadth_first.py new file mode 100644 index 0000000..72030bc --- /dev/null +++ b/src/nographs/_strategies/bidirectional_search/breadth_first.py @@ -0,0 +1,399 @@ +from __future__ import annotations + +import itertools +from typing import Optional, Iterable, Generic + +from nographs._types import ( + T_vertex, + T_vertex_id, + T_labels, + VertexToID, + vertex_as_id, +) +from nographs._gears import ( + GearWithoutDistances, + GearDefault, +) +from nographs._path import ( + Path, + PathOfLabeledEdges, + PathOfUnlabeledEdges, +) +from nographs._gear_collections import access_to_vertex_set + +from ..type_aliases import ( + Strategy, + BNextVertices, + BNextEdges, + BNextLabeledEdges, +) +from ..utils import iter_start_vertices_and_ids + +from ..traversals.without_weights.breadth_first import ( + TraversalBreadthFirstFlex, +) +from .base import ( + _create_unified_next_bidirectional, + _search_needs_search_object, +) + + +class BSearchBreadthFirstFlex(Strategy[T_vertex, T_vertex_id, T_labels]): + """ + Bases: `Strategy` [`T_vertex`, `T_vertex_id`, `T_labels`] + + :param vertex_to_id: See `VertexToID` function. + + :param gear: See `gears API ` and class `GearWithoutDistances`. + + :param next_vertices: Tuple `BNextVertices` of two NextVertices function. + If None, provide next_edges or next_labeled_edges. + + :param next_edges: Tuple `BNextEdges` of two NextEdges functions. + See paragraph *input* below for details. Only allowed if next_vertices equals + None. If both are None, provide next_labeled_edges. + + :param next_labeled_edges: Tuple `BNextLabeledEdges` of two NextEdges + functions. See paragraph *input* below for details. The parameter is only + allowed if next_vertices and next_edges equal None. If given, paths will record + the given labels. + + **Algorithm:** Bidirectional version of the Breadth First Search algorithm, + non-recursive, based on FIFO queues. + + **Properties:** In both directions, vertices are visited by increasing depth + from a start (resp. a goal) vertex (minimal number of edges), till a shortest + path (minimal number of edges) from a start to a goal vertex is found. Each + vertex is visited only once. + + **Input:** Directed graph. Unlabeled or labeled edges. One or more start vertices, + and one or more goal vertices. NextVertices (resp. NextEdges) functions both for + the outgoing edges from a vertex and the incoming edges to a vertex have to be + provided, and they need to describe the same graph. Optional calculation limit. + + Note: A shortest path from a vertex *v* to itself always exists, has an edge count + of 0, and will be found by the class, whilst TraversalBreadthFirst does not report + start vertices and thus, + TraversalBreadthFirst().start_at(v).go_to(v) fails. + """ + + def __init__( + self, + vertex_to_id: VertexToID[T_vertex, T_vertex_id], + gear: GearWithoutDistances[T_vertex, T_vertex_id, T_labels], + next_vertices: Optional[ + BNextVertices[ + T_vertex, TraversalBreadthFirstFlex[T_vertex, T_vertex_id, T_labels] + ] + ] = None, + *, + next_edges: Optional[ + BNextEdges[ + T_vertex, + TraversalBreadthFirstFlex[T_vertex, T_vertex_id, T_labels], + ], + ] = None, + next_labeled_edges: Optional[ + BNextLabeledEdges[ + T_vertex, + TraversalBreadthFirstFlex[T_vertex, T_vertex_id, T_labels], + T_labels, + ], + ] = None, + ) -> None: + self._vertex_to_id = vertex_to_id + self._gear = gear + + ( + _, + self._edges_with_data, + self._labeled_edges, + ) = _create_unified_next_bidirectional( + next_vertices, next_edges, next_labeled_edges + ) + + self._traversal_bi = tuple( + TraversalBreadthFirstFlex[T_vertex, T_vertex_id, T_labels]( + vertex_to_id, + gear, + next_vertices=None if next_vertices is None else next_vertices[i], + next_edges=None if next_edges is None else next_edges[i], + next_labeled_edges=( + None if next_labeled_edges is None else next_labeled_edges[i] + ), + ) + for i in range(2) + ) + + def start_from( + self, + start_and_goal_vertex: Optional[tuple[T_vertex, T_vertex]] = None, + *, + start_and_goal_vertices: Optional[ + tuple[Iterable[T_vertex], Iterable[T_vertex]] + ] = None, + build_path: bool = False, + calculation_limit: Optional[int] = None, + fail_silently: bool = False, + ) -> tuple[int, Path[T_vertex, T_vertex_id, T_labels]]: + """ + Start the search both from a start vertex and a goal vertex, resp. both + from a set of start vertices and a set of goal vertices. Return the + length of a shortest (sum of edge weights) path between the/a start vertex + and the/a goal vertex. If building a path was requested, also return the path, + and otherwise, return a dummy path object. + + If the search ends without having found a path, raise KeyError, + or, if a silent fail is demanded, return -1 and a dummy path object. + Here, infinity means the value for infinite distance that is defined by the + used `Gear` (as provided by a call of gear.infinity()). + + NoGraphs gives no guarantees about a dummy path. Do not use it for anything. + + :param start_and_goal_vertex: The start vertex and the goal vertex of the + search. If None, provide start_and_goal_vertices. + + :param start_and_goal_vertices: The start vertices and the + goal vertices of the search. Only allowed if start_and_goal_vertex + equals None. + + :param build_path: If true, build and return a path of the minimum possible + length. + + :param calculation_limit: If provided, maximal number of vertices to process + (read in) from your graph in each of the searches in one of the two + directions. If it is exceeded, a RuntimeError will be raised. + + :param fail_silently: If no path can be found, fail silently (see above) + instead of raising an exception. + """ + # steps that other strategies do in start_from + + _search_needs_search_object(self, BSearchBreadthFirstFlex) + + start_vertices: Iterable[T_vertex] + goal_vertices: Iterable[T_vertex] + if start_and_goal_vertex is not None: + if start_and_goal_vertices is not None: + raise RuntimeError( + "Both start_and_goal_vertex and " + + "start_and_goal_vertices provided." + ) + start_vertex, goal_vertex = start_and_goal_vertex + start_vertices, goal_vertices = (start_vertex,), (goal_vertex,) + else: + if start_and_goal_vertices is None: + raise RuntimeError( + "Neither start_and_goal_vertex nor " + + "start_and_goal_vertices provided." + ) + start_vertices, goal_vertices = start_and_goal_vertices + + for t, vertices in zip(self._traversal_bi, (start_vertices, goal_vertices)): + t.start_from( + start_vertices=vertices, + build_paths=build_path, + calculation_limit=calculation_limit, + _report_depth_increase=True, + ) + + visited_bi = tuple(t.visited for t in self._traversal_bi) + + # Copy Traversal attributes into method scope (faster access) + # labeled_edges = self._labeled_edges + maybe_vertex_to_id = ( + None if self._vertex_to_id == vertex_as_id else self._vertex_to_id + ) # Case vertex_as_id: not apply; T_vertex_id > T_vertex + + # Get references of used gear objects and methods (avoid attribute resolution) + ( + visited_forwards_uses_sequence, + visited_forwards_sequence, + visited_forwards_wrapper, + visited_forwards_uses_bits, + visited_forwards_index_and_bit_method, + ) = access_to_vertex_set(visited_bi[0]) + ( + visited_backwards_uses_sequence, + visited_backwards_sequence, + visited_backwards_wrapper, + visited_backwards_uses_bits, + visited_backwards_index_and_bit_method, + ) = access_to_vertex_set(visited_bi[1]) + + # ----- Initialize method specific bookkeeping ----- + + # Get the right class for storing a path (labeled or not) + path_cls: type[Path] + if self._labeled_edges: + path_cls = PathOfLabeledEdges[T_vertex, T_vertex_id, T_labels] + else: + path_cls = PathOfUnlabeledEdges[T_vertex, T_vertex_id, T_labels] + + # Detect if a start vertex is also goal vertex, and report result manually. + # (Without this manual handling, a non-self loop from such a start vertex back + # to itself with a length > 0 would be reported as smallest distance. This + # would be unexpected for users, since they expect that the distance from a + # vertex to itself is always 0.) + common_vertex_ids = set( + v_id + for v, v_id in iter_start_vertices_and_ids( + start_vertices, self._vertex_to_id + ) + ).intersection( + v_id + for v, v_id in iter_start_vertices_and_ids( + goal_vertices, self._vertex_to_id + ) + ) + if common_vertex_ids: + for c_vertex, cv_id in iter_start_vertices_and_ids( + start_vertices, self._vertex_to_id + ): + if cv_id in common_vertex_ids: + p = path_cls.from_vertex(c_vertex) + return 0, p + + # ----- Inner loop ----- + + for ( + traversal_iter, + visited_other, + visited_other_uses_sequence, + visited_other_sequence, + visited_other_uses_bits, + visited_other_index_and_bit_method, + ) in itertools.cycle( + ( + ( + iter(self._traversal_bi[0]), + visited_bi[1], + visited_backwards_uses_sequence, + visited_backwards_sequence, + visited_backwards_uses_bits, + visited_backwards_index_and_bit_method, + ), + ( + iter(self._traversal_bi[1]), + visited_bi[0], + visited_forwards_uses_sequence, + visited_forwards_sequence, + visited_forwards_uses_bits, + visited_forwards_index_and_bit_method, + ), + ) + ): + prev_vertex: Optional[T_vertex] = None + for vertex in traversal_iter: + # If we get the same vertex twice, directly after each other, + # this signals, that the depth will increase with the next reported + # vertex. So we leave the loop here. + if prev_vertex == vertex: + break + prev_vertex = vertex + + # If vertex is not in visited vertices of other traversal: continue + v_id: T_vertex_id = ( + maybe_vertex_to_id(vertex) # type: ignore[assignment] + if maybe_vertex_to_id + else vertex + ) + + if not visited_other_uses_sequence: + # Standard implementation for "normal" MutableMapping + if v_id not in visited_other: + continue + elif visited_other_uses_bits: + # Same as above, but with bits in byte sequence + sequence_key, bit_number = visited_other_index_and_bit_method( + v_id, 8 + ) + bit_mask = 1 << bit_number + try: + value = visited_other_sequence[sequence_key] + if not (value & bit_mask): + continue + except IndexError: + continue + else: + # Same as above, but with booleans in byte sequence + try: + if not visited_other_sequence[v_id]: + continue + except IndexError: + continue + + # We found a vertex from both directions + path = ( + path_cls.from_bidirectional_search( + self._traversal_bi[0].paths, self._traversal_bi[1].paths, vertex + ) + if build_path + else path_cls.of_nothing() + ) + return sum(t.depth for t in self._traversal_bi), path + else: + # No new vertices reported by traversal in this direction and depth: + # Whole search is over. + break + + if fail_silently: + return -1, path_cls.of_nothing() + else: + raise KeyError("No path to (a) goal vertex found") + + +class BSearchBreadthFirst( + Generic[T_vertex, T_labels], BSearchBreadthFirstFlex[T_vertex, T_vertex, T_labels] +): + """ + Eases the use of `BSearchBreadthFirstFlex` for typical cases. + For documentation of functionality and parameters, see there. + + .. code-block:: python + + BSearchBreadthFirst[T_vertex, T_labels](*args, **keywords) + + is a short form for + + .. code-block:: python + + BSearchBreadthFirstFlex[ + T_vertex, T_vertex, T_labels], + ](nog.vertex_as_id, nog.GearDefault(), *args, **keywords) + + Implication: + + - `GearDefault` is used, see there how it and its superclass work + - T_vertex is bound to Hashable (T_vertex is used as `T_vertex_id`, see there) + """ + + def __init__( + self, + next_vertices: Optional[ + BNextVertices[ + T_vertex, TraversalBreadthFirstFlex[T_vertex, T_vertex, T_labels] + ] + ] = None, + *, + next_edges: Optional[ + BNextEdges[ + T_vertex, + TraversalBreadthFirstFlex[T_vertex, T_vertex, T_labels], + ], + ] = None, + next_labeled_edges: Optional[ + BNextLabeledEdges[ + T_vertex, + TraversalBreadthFirstFlex[T_vertex, T_vertex, T_labels], + T_labels, + ], + ] = None, + ) -> None: + super().__init__( + vertex_as_id, + GearDefault(), + next_vertices, + next_edges=next_edges, + next_labeled_edges=next_labeled_edges, + ) diff --git a/src/nographs/_bidir_search.py b/src/nographs/_strategies/bidirectional_search/shortest_path.py similarity index 55% rename from src/nographs/_bidir_search.py rename to src/nographs/_strategies/bidirectional_search/shortest_path.py index 69ad756..1725199 100644 --- a/src/nographs/_bidir_search.py +++ b/src/nographs/_strategies/bidirectional_search/shortest_path.py @@ -1,498 +1,47 @@ from __future__ import annotations import itertools -from collections.abc import ( - Iterable, -) from heapq import heapify, heappop, heappush -from typing import Optional, Any, Generic, Union +from typing import Generic, Optional, Union +from collections.abc import Iterable, Collection -from ._gear_collections import ( - access_to_vertex_set, - access_to_vertex_mapping, - access_to_vertex_mapping_expect_none, +from nographs._types import ( + T_vertex, + T_vertex_id, + T_labels, + VertexToID, + vertex_as_id, + T_weight, ) -from ._gears import ( - GearWithoutDistances, +from nographs._gears import ( Gear, GearDefault, ) -from ._path import ( +from nographs._path import ( Path, - PathOfUnlabeledEdges, PathOfLabeledEdges, + PathOfUnlabeledEdges, ) -from ._strategies import ( +from nographs._gear_collections import ( + access_to_vertex_mapping_expect_none, + access_to_vertex_mapping, +) +from ..type_aliases import ( Strategy, - T_strategy, - BNextVertices, - BNextEdges, - BNextLabeledEdges, BNextWeightedEdges, BNextWeightedLabeledEdges, - BNextEdgesOrVertices, - BNextWeightedMaybeLabeledEdges, +) +from ..utils import ( + create_paths, iter_start_vertices_and_ids, define_distances, - create_paths, ) -from ._traversals import TraversalBreadthFirstFlex -from ._types import ( - T_vertex, - T_vertex_id, - T_weight, - T_labels, - VertexToID, - vertex_as_id, +from .base import ( + _create_unified_next_weighted_bidirectional, + _search_needs_search_object, ) -# --------------- internal support functions ------------- - - -def _search_needs_search_object(obj: Any, needed_class: type) -> None: - if not isinstance(obj, needed_class): - raise RuntimeError( - "Method start_from can only be called on a search strategy object." - ) - - -def _create_unified_next_bidirectional( - next_vertices: Optional[BNextVertices[T_vertex, T_strategy]], - next_edges: Optional[BNextEdges[T_vertex, T_strategy]], - next_labeled_edges: Optional[BNextLabeledEdges[T_vertex, T_strategy, T_labels]], -) -> tuple[BNextEdgesOrVertices[T_vertex, T_strategy, T_labels], bool, bool]: - """Check configuration of given next_vertices, next_edges, and next_labeled_edges - function pairs - and calculate a pair of unified NextEdgesOrVertices - and whether we have edges with data (weights and/or labels) and/or labeled_edges. - """ - next_edges_or_vertices: BNextEdgesOrVertices[T_vertex, T_strategy, T_labels] - if next_vertices is not None: - if next_edges is not None: - raise RuntimeError("Both next_vertices and next_edges provided.") - if next_labeled_edges is not None: - raise RuntimeError("Both next_vertices and next_labeled_edges provided.") - next_edges_or_vertices = next_vertices - edges_with_data = False - labeled_edges = False - elif next_edges is not None: - if next_labeled_edges is not None: - raise RuntimeError("Both next_edges and next_labeled_edges provided.") - next_edges_or_vertices = next_edges - edges_with_data = True - labeled_edges = False - else: - if next_labeled_edges is None: - raise RuntimeError( - "Neither next_vertices nor next_edges " - + "nor next_labeled_edges provided." - ) - next_edges_or_vertices = next_labeled_edges - edges_with_data = True - labeled_edges = True - return next_edges_or_vertices, edges_with_data, labeled_edges - - -def _create_unified_next_weighted_bidirectional( - next_edges: Optional[BNextWeightedEdges[T_vertex, T_strategy, T_weight]], - next_labeled_edges: Optional[ - BNextWeightedLabeledEdges[T_vertex, T_strategy, T_weight, T_labels] - ], -) -> tuple[ - BNextWeightedMaybeLabeledEdges[T_vertex, T_strategy, T_weight, T_labels], - bool, -]: - """Check configuration of given next_edges and next_labeled_edges function pairs - and calculate a pair of unified _NextWeightedMaybeLabeledEdges and whether we have - labeled_edges. - """ - next_maybe_labeled_edges: BNextWeightedMaybeLabeledEdges[ - T_vertex, T_strategy, T_weight, T_labels - ] - if next_edges is not None: - if next_labeled_edges is not None: - raise RuntimeError("Both next_edges and next_labeled_edges provided.") - next_maybe_labeled_edges = next_edges - labeled_edges = False - else: - if next_labeled_edges is None: - raise RuntimeError("Neither next_edges and next_labeled_edges provided.") - next_maybe_labeled_edges = next_labeled_edges - labeled_edges = True - return next_maybe_labeled_edges, labeled_edges - - -# ------------------------- Bidirectional search ----------------------- - - -class BSearchBreadthFirstFlex(Strategy[T_vertex, T_vertex_id, T_labels]): - """ - Bases: `Strategy` [`T_vertex`, `T_vertex_id`, `T_labels`] - - :param vertex_to_id: See `VertexToID` function. - - :param gear: See `gears API ` and class `GearWithoutDistances`. - - :param next_vertices: Tuple `BNextVertices` of two NextVertices function. - If None, provide next_edges or next_labeled_edges. - - :param next_edges: Tuple `BNextEdges` of two NextEdges functions. - See paragraph *input* below for details. Only allowed if next_vertices equals - None. If both are None, provide next_labeled_edges. - - :param next_labeled_edges: Tuple `BNextLabeledEdges` of two NextEdges - functions. See paragraph *input* below for details. The parameter is only - allowed if next_vertices and next_edges equal None. If given, paths will record - the given labels. - - **Algorithm:** Bidirectional version of the Breadth First Search algorithm, - non-recursive, based on FIFO queues. - - **Properties:** In both directions, vertices are visited by increasing depth - from a start (resp. a goal) vertex (minimal number of edges), till a shortest - path (minimal number of edges) from a start to a goal vertex is found. Each - vertex is visited only once. - - **Input:** Directed graph. Unlabeled or labeled edges. One or more start vertices, - and one or more goal vertices. NextVertices (resp. NextEdges) functions both for - the outgoing edges from a vertex and the incoming edges to a vertex have to be - provided, and they need to describe the same graph. Optional calculation limit. - - Note: A shortest path from a vertex *v* to itself always exists, has an edge count - of 0, and will be found by the class, whilst TraversalBreadthFirst does not report - start vertices and thus, - TraversalBreadthFirst().start_at(v).go_to(v) fails. - """ - - def __init__( - self, - vertex_to_id: VertexToID[T_vertex, T_vertex_id], - gear: GearWithoutDistances[T_vertex, T_vertex_id, T_labels], - next_vertices: Optional[ - BNextVertices[ - T_vertex, TraversalBreadthFirstFlex[T_vertex, T_vertex_id, T_labels] - ] - ] = None, - *, - next_edges: Optional[ - BNextEdges[ - T_vertex, - TraversalBreadthFirstFlex[T_vertex, T_vertex_id, T_labels], - ], - ] = None, - next_labeled_edges: Optional[ - BNextLabeledEdges[ - T_vertex, - TraversalBreadthFirstFlex[T_vertex, T_vertex_id, T_labels], - T_labels, - ], - ] = None, - ) -> None: - self._vertex_to_id = vertex_to_id - self._gear = gear - - ( - _, - self._edges_with_data, - self._labeled_edges, - ) = _create_unified_next_bidirectional( - next_vertices, next_edges, next_labeled_edges - ) - - self._traversal_bi = tuple( - TraversalBreadthFirstFlex[T_vertex, T_vertex_id, T_labels]( - vertex_to_id, - gear, - next_vertices=None if next_vertices is None else next_vertices[i], - next_edges=None if next_edges is None else next_edges[i], - next_labeled_edges=( - None if next_labeled_edges is None else next_labeled_edges[i] - ), - ) - for i in range(2) - ) - - def start_from( - self, - start_and_goal_vertex: Optional[tuple[T_vertex, T_vertex]] = None, - *, - start_and_goal_vertices: Optional[ - tuple[Iterable[T_vertex], Iterable[T_vertex]] - ] = None, - build_path: bool = False, - calculation_limit: Optional[int] = None, - fail_silently: bool = False, - ) -> tuple[int, Path[T_vertex, T_vertex_id, T_labels]]: - """ - Start the search both from a start vertex and a goal vertex, resp. both - from a set of start vertices and a set of goal vertices. Return the - length of a shortest (sum of edge weights) path between the/a start vertex - and the/a goal vertex. If building a path was requested, also return the path, - and otherwise, return a dummy path object. - - If the search ends without having found a path, raise KeyError, - or, if a silent fail is demanded, return -1 and a dummy path object. - Here, infinity means the value for infinite distance that is defined by the - used `Gear` (as provided by a call of gear.infinity()). - - NoGraphs gives no guarantees about a dummy path. Do not use it for anything. - - :param start_and_goal_vertex: The start vertex and the goal vertex of the - search. If None, provide start_and_goal_vertices. - - :param start_and_goal_vertices: The start vertices and the - goal vertices of the search. Only allowed if start_and_goal_vertex - equals None. - - :param build_path: If true, build and return a path of the minimum possible - length. - - :param calculation_limit: If provided, maximal number of vertices to process - (read in) from your graph in each of the searches in one of the two - directions. If it is exceeded, a RuntimeError will be raised. - - :param fail_silently: If no path can be found, fail silently (see above) - instead of raising an exception. - """ - # steps that other strategies do in start_from - - _search_needs_search_object(self, BSearchBreadthFirstFlex) - - start_vertices: Iterable[T_vertex] - goal_vertices: Iterable[T_vertex] - if start_and_goal_vertex is not None: - if start_and_goal_vertices is not None: - raise RuntimeError( - "Both start_and_goal_vertex and " - + "start_and_goal_vertices provided." - ) - start_vertex, goal_vertex = start_and_goal_vertex - start_vertices, goal_vertices = (start_vertex,), (goal_vertex,) - else: - if start_and_goal_vertices is None: - raise RuntimeError( - "Neither start_and_goal_vertex nor " - + "start_and_goal_vertices provided." - ) - start_vertices, goal_vertices = start_and_goal_vertices - - for t, vertices in zip(self._traversal_bi, (start_vertices, goal_vertices)): - t.start_from( - start_vertices=vertices, - build_paths=build_path, - calculation_limit=calculation_limit, - _report_depth_increase=True, - ) - - visited_bi = tuple(t.visited for t in self._traversal_bi) - - # Copy Traversal attributes into method scope (faster access) - # labeled_edges = self._labeled_edges - maybe_vertex_to_id = ( - None if self._vertex_to_id == vertex_as_id else self._vertex_to_id - ) # Case vertex_as_id: not apply; T_vertex_id > T_vertex - - # Get references of used gear objects and methods (avoid attribute resolution) - ( - visited_forwards_uses_sequence, - visited_forwards_sequence, - visited_forwards_wrapper, - visited_forwards_uses_bits, - visited_forwards_index_and_bit_method, - ) = access_to_vertex_set(visited_bi[0]) - ( - visited_backwards_uses_sequence, - visited_backwards_sequence, - visited_backwards_wrapper, - visited_backwards_uses_bits, - visited_backwards_index_and_bit_method, - ) = access_to_vertex_set(visited_bi[1]) - - # ----- Initialize method specific bookkeeping ----- - - # Get the right class for storing a path (labeled or not) - path_cls: type[Path] - if self._labeled_edges: - path_cls = PathOfLabeledEdges[T_vertex, T_vertex_id, T_labels] - else: - path_cls = PathOfUnlabeledEdges[T_vertex, T_vertex_id, T_labels] - - # Detect if a start vertex is also goal vertex, and report result manually. - # (Without this manual handling, a non-self loop from such a start vertex back - # to itself with a length > 0 would be reported as smallest distance. This - # would be unexpected for users, since they expect that the distance from a - # vertex to itself is always 0.) - common_vertex_ids = set( - v_id - for v, v_id in iter_start_vertices_and_ids( - start_vertices, self._vertex_to_id - ) - ).intersection( - v_id - for v, v_id in iter_start_vertices_and_ids( - goal_vertices, self._vertex_to_id - ) - ) - if common_vertex_ids: - for c_vertex, cv_id in iter_start_vertices_and_ids( - start_vertices, self._vertex_to_id - ): - if cv_id in common_vertex_ids: - p = path_cls.from_vertex(c_vertex) - return 0, p - - # ----- Inner loop ----- - - for ( - traversal_iter, - visited_other, - visited_other_uses_sequence, - visited_other_sequence, - visited_other_wrapper, - visited_other_uses_bits, - visited_other_index_and_bit_method, - paths, - ) in itertools.cycle( - ( - ( - iter(self._traversal_bi[0]), - visited_bi[1], - visited_backwards_uses_sequence, - visited_backwards_sequence, - visited_backwards_wrapper, - visited_backwards_uses_bits, - visited_backwards_index_and_bit_method, - self._traversal_bi[0].paths, - ), - ( - iter(self._traversal_bi[1]), - visited_bi[0], - visited_forwards_uses_sequence, - visited_forwards_sequence, - visited_forwards_wrapper, - visited_forwards_uses_bits, - visited_forwards_index_and_bit_method, - self._traversal_bi[1].paths, - ), - ) - ): - prev_vertex: Optional[T_vertex] = None - for vertex in traversal_iter: - # If we get the same vertex twice, directly after each other, - # this signals, that the depth will increase with the next reported - # vertex. So we leave the loop here. - if prev_vertex == vertex: - break - prev_vertex = vertex - - # If vertex is not in visited vertices of other traversal: continue - v_id: T_vertex_id = ( - maybe_vertex_to_id(vertex) # type: ignore[assignment] - if maybe_vertex_to_id - else vertex - ) - - if not visited_other_uses_sequence: - # Standard implementation for "normal" MutableMapping - if v_id not in visited_other: - continue - elif visited_other_uses_bits: - # Same as above, but with bits in byte sequence - sequence_key, bit_number = visited_other_index_and_bit_method( - v_id, 8 - ) - bit_mask = 1 << bit_number - try: - value = visited_other_sequence[sequence_key] - if not (value & bit_mask): - continue - except IndexError: - continue - else: - # Same as above, but with booleans in byte sequence - try: - if not visited_other_sequence[v_id]: - continue - except IndexError: - continue - - # We found a vertex from both directions - path = ( - path_cls.from_bidirectional_search( - self._traversal_bi[0].paths, self._traversal_bi[1].paths, vertex - ) - if build_path - else path_cls.of_nothing() - ) - return sum(t.depth for t in self._traversal_bi), path - else: - # No new vertices reported by traversal in this direction and depth: - # Whole search is over. - break - - if fail_silently: - return -1, path_cls.of_nothing() - else: - raise KeyError("No path to (a) goal vertex found") - - -class BSearchBreadthFirst( - Generic[T_vertex, T_labels], BSearchBreadthFirstFlex[T_vertex, T_vertex, T_labels] -): - """ - Eases the use of `BSearchBreadthFirstFlex` for typical cases. - For documentation of functionality and parameters, see there. - - .. code-block:: python - - BSearchBreadthFirst[T_vertex, T_labels](*args, **keywords) - - is a short form for - - .. code-block:: python - - BSearchBreadthFirstFlex[ - T_vertex, T_vertex, T_labels], - ](nog.vertex_as_id, nog.GearDefault(), *args, **keywords) - - Implication: - - - `GearDefault` is used, see there how it and its superclass work - - T_vertex is bound to Hashable (T_vertex is used as `T_vertex_id`, see there) - """ - - def __init__( - self, - next_vertices: Optional[ - BNextVertices[ - T_vertex, TraversalBreadthFirstFlex[T_vertex, T_vertex, T_labels] - ] - ] = None, - *, - next_edges: Optional[ - BNextEdges[ - T_vertex, - TraversalBreadthFirstFlex[T_vertex, T_vertex, T_labels], - ], - ] = None, - next_labeled_edges: Optional[ - BNextLabeledEdges[ - T_vertex, - TraversalBreadthFirstFlex[T_vertex, T_vertex, T_labels], - T_labels, - ], - ] = None, - ) -> None: - super().__init__( - vertex_as_id, - GearDefault(), - next_vertices, - next_edges=next_edges, - next_labeled_edges=next_labeled_edges, - ) - - class BSearchShortestPathFlex( Strategy[T_vertex, T_vertex_id, T_labels], Generic[T_vertex, T_vertex_id, T_weight, T_labels], @@ -624,6 +173,14 @@ def start_from( + "start_and_goal_vertices provided." ) start_vertices, goal_vertices = start_and_goal_vertices + if build_path: + # Below, we will consume vertices by the call of *create_paths*, so we + # first make a collection out of start_vertices resp. goal_vertices, + # except they are already given as collection + if not isinstance(start_vertices, Collection): + start_vertices = self._gear.sequence_of_vertices(start_vertices) + if not isinstance(goal_vertices, Collection): + goal_vertices = self._gear.sequence_of_vertices(goal_vertices) paths_forwards, predecessors_forwards, attributes_forwards = create_paths( build_path, @@ -798,12 +355,9 @@ def start_from( distances_sequence, distances_wrapper, distances_sequence_other, - distances_wrapper_other, next_edges, - paths, predecessors_sequence, predecessors_wrapper, - attributes, attributes_sequence, attributes_wrapper, unique_no, @@ -815,12 +369,9 @@ def start_from( distances_sequence_forwards, distances_wrapper_forwards, distances_sequence_backwards, - distances_wrapper_backwards, next_edges_forwards, - paths_forwards, predecessors_sequence_forwards, predecessors_wrapper_forwards, - attributes_forwards, attributes_sequence_forwards, attributes_wrapper_forwards, unique_no_forwards, @@ -831,12 +382,9 @@ def start_from( distances_sequence_backwards, distances_wrapper_backwards, distances_sequence_forwards, - distances_wrapper_forwards, next_edges_backwards, - paths_backwards, predecessors_sequence_backwards, predecessors_wrapper_backwards, - attributes_backwards, attributes_sequence_backwards, attributes_wrapper_backwards, unique_no_backwards, diff --git a/src/nographs/_strategies/strategy.py b/src/nographs/_strategies/strategy.py new file mode 100644 index 0000000..917fb80 --- /dev/null +++ b/src/nographs/_strategies/strategy.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +from abc import ABC +from typing import Generic, Any, Optional, Iterable + +from nographs._types import T_vertex, T_vertex_id, T_labels + + +class Strategy(ABC, Generic[T_vertex, T_vertex_id, T_labels]): + """Base class of the traversal strategies and search strategies of NoGraphs.""" + + def _improve_state( + self, state: dict[str, Any], vertices: Optional[Iterable[T_vertex]] = None + ) -> None: + """Improve the state description + + :param state: State in current form + :param vertices: If the strategy can provide additional state data w.r.t. these + vertices, it will do so. + """ + pass + + def state_to_str(self, vertices: Optional[Iterable[T_vertex]] = None) -> str: + """Return a human-readable description of the public state of the strategy as + a string. + + If an attribute of the traversal is a containers that cannot iterate its + content, or a collection that guarantees for the validity of stored + results only for keys that are already visited vertices (see the API reference + of the traversal classes), its content is only described for vertices given + as parameter *vertices*. + + Implementation details, not covered by the semantic versioning: + + Currently, the method aims at providing a uniform behaviour over different + platforms (*CPython* and *PyPy*) and collection types (Gears with different + *MutableSet* and *MutableMapping* implementations). It behaves roughly as + follows: + + - A *MutableSet*, e.g. attribute *visited*, is described similar to a *set*, + but items are sorted lexicographically in their string representations + (this bridges differences between *CPython* and *PyPy*). + + - Attribute *Paths* is described similar to a *dict* (although keys can contain + unhashable values, and only paths for the given *vertices* are described). + + - A *MutableMapping*, e.g. attribute *distance*, is described similarly to a + *dict*, also in cases, where it is not a *dict*, and although the items + for only the given *vertices* are described. + + :param vertices: If the strategy can provide additional state data w.r.t. these + vertices, it will do so. + """ + state = dict((k, v) for k, v in self.__dict__.items() if k[0] != "_") + self._improve_state(state, vertices) + return str(state) diff --git a/src/nographs/_strategies/traversals/__init__.py b/src/nographs/_strategies/traversals/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/nographs/_strategies/traversals/traversal.py b/src/nographs/_strategies/traversals/traversal.py new file mode 100644 index 0000000..fe4587f --- /dev/null +++ b/src/nographs/_strategies/traversals/traversal.py @@ -0,0 +1,329 @@ +""" Traversal strategies for unweighted graphs with or without edge labels """ + +from __future__ import annotations + +from abc import abstractmethod +from collections.abc import ( + Iterator, + Iterable, + Generator, + Collection, +) +from typing import Optional, Any, cast, overload, Literal + +from nographs._gears import ( + GearWithoutDistances, + VertexIdToVertexMapping, + VertexIdToEdgeLabelsMapping, +) +from nographs._types import ( + T_vertex, + T_vertex_id, + T_labels, + VertexToID, + vertex_as_id, +) +from nographs._paths import ( + Paths, + DummyPredecessorOrLabelsMapping, + PathsDummy, +) +from ..utils import ( + StrRepr, + create_paths, +) +from ..strategy import Strategy + + +def no_generator() -> Generator[Any, None, None]: + """ + >>> next(no_generator()) + Traceback (most recent call last): + RuntimeError: Traversal not started, iteration not possible + """ + raise RuntimeError("Traversal not started, iteration not possible") + # noinspection PyUnreachableCode + yield None + + +class Traversal(Strategy[T_vertex, T_vertex_id, T_labels], Iterable): + """ + Abstract Class. Its subclasses provide methods to iterate through vertices + and edges using some specific traversal strategies. + """ + + @abstractmethod + def __init__( + self, + labeled_edges: bool, + is_tree: bool, + vertex_to_id: VertexToID[T_vertex, T_vertex_id], + ) -> None: + """ + :param labeled_edges: The element *label* of an edge is set for edges + :param is_tree: The traversal does not need to prevent re-visits + :param vertex_to_id: See *VertexToID* + """ + # -- Attributes of graph adaptation + # The come from parameters of __init__ and do not change with _start_from. + self._labeled_edges = labeled_edges + self._is_tree = is_tree + self._vertex_to_id = vertex_to_id + + # -- Further, general attributes set and needed by all traversal strategies + # They have to be initialized when a traversal is started or re-started, + # see method _start_from. Thus, here, they are only initialized in a generic + # default way. + + # Internal attributes + self._generator: Generator[T_vertex, None, Any] = no_generator() + self._start_vertices: Iterable[T_vertex] = tuple[T_vertex]() + self._build_paths: bool = False + self._calculation_limit: Optional[int] = None + + # External paths attributes + self.paths: Paths[T_vertex, T_vertex_id, T_labels] = PathsDummy[ + T_vertex, T_vertex_id, T_labels + ](vertex_to_id) + """ The container *paths* holds the created paths, if path creation has been + demanded. If labeled edges were provided (parameter *next_labeled_edges*), the + paths contain them instead of just vertices. + """ + self._predecessors: VertexIdToVertexMapping[T_vertex_id, T_vertex] = ( + DummyPredecessorOrLabelsMapping[T_vertex_id, T_vertex]() + ) + self._attributes: VertexIdToEdgeLabelsMapping[T_vertex_id, T_labels] = ( + DummyPredecessorOrLabelsMapping[T_vertex_id, T_labels]() + ) + + def _start_from( + self, + start_vertex: Optional[T_vertex], + start_vertices: Optional[Iterable[T_vertex]], + build_paths: bool, + calculation_limit: Optional[int], + gear: GearWithoutDistances[T_vertex, T_vertex_id, T_labels], + empty_path_for_start_vertices: bool = True, + ) -> None: + """ + Initialize the traversal attributes that have to be initialized when + the traversal is started or re-started, see __init__. The configuration of + start_vertex and start_vertices is checked before it is used. + """ + # Check start vertices options. Compute multi-vertices form from single vertex. + if start_vertex is not None: + if start_vertices is not None: + raise RuntimeError("Both start_vertex and start_vertices provided.") + self._start_vertices = (start_vertex,) + else: + if start_vertices is None: + raise RuntimeError("Neither start_vertex nor start_vertices provided.") + if ( + build_paths + and empty_path_for_start_vertices + and not isinstance(start_vertices, Collection) + ): + # Below, we will consume vertices by the call of *create_path*, so we + # first make a collection out of start_vertices, except they + # are already given as collection + self._start_vertices = gear.sequence_of_vertices(start_vertices) + else: + self._start_vertices = start_vertices + + # Create and store path container and path setting + self._build_paths = build_paths + self.paths, self._predecessors, self._attributes = create_paths( + build_paths, + gear, + self._labeled_edges, + self._vertex_to_id, + self._start_vertices if empty_path_for_start_vertices else (), + ) + + # store calculation limit + self._calculation_limit = calculation_limit + + def _start(self) -> None: + self._generator = self._traverse() + + def __iter__( + self, + ) -> Iterator[T_vertex]: # Type alias needed do to a sphinx limitation + """ + Return the iterator of a started traversal. This allows for using a + `Traversal` in *for* loops or as parameter to a call of function + *next()*. + + Subsequent calls return the same iterator again. This allows for using + the same `Traversal` in subsequent *for* loops or *next()* calls, as + long as the iterator is not exhausted. + + The iterator yields vertices reported by the traversal algorithm. + When a vertex is reported, specific attributes of the traversal object + contain additional data about the state of the traversal (see the API + documentation of the respective subclass of `Traversal`).""" + if not isinstance(type(self), type(Traversal)): + raise RuntimeError("Method go can only be called on a Traversal object.") + return self._generator + + def __next__(self) -> T_vertex: + """Returns the next vertex reported by the (started) traversal. This + allows for calls like *next(traversal)*. + + Delegates to the iterator of the traversal.""" + return next(self._generator) + + def go_for_vertices_in( + self, vertices: Iterable[T_vertex], fail_silently: bool = False + ) -> Iterator[T_vertex]: + """ + For a started traversal, return an iterator that fetches vertices + from the traversal, reports a vertex if it is in *vertices*, and stops when + all the *vertices* have been found and reported. + + If the iterator has no more vertices to report (graph is exhausted) without + having found all the *vertices*, KeyError is raised, or the traversal just + terminates, if a silent fail is demanded. + + If *vertices* does not provide any vertices, an empty iterator is returned. + + If a `VertexToID` function is used, the method searches for vertices + that have the same id as one of the *vertices*. + + Whenever a vertex is reported, specific attributes of the traversal object + contain additional data about the state of the traversal (see the API + documentation of the respective subclass of `Traversal`). + + :param vertices: Vertices to find + :param fail_silently: Terminate, but do not raise KeyError, when graph + is exhausted. + """ + if not isinstance(type(self), type(Traversal)): + raise RuntimeError( + "Method go_for_vertices_in can only be called " + + "on a Traversal object." + ) + # In order to make the above check work, the following generator functionality + # needs to be encapsulated in a local function + + def my_generator() -> Iterator[T_vertex]: + vertex_to_id = self._vertex_to_id + if vertex_to_id == vertex_as_id: + vertex_set = set(cast(Iterable[T_vertex_id], vertices)) + v_count = len(vertex_set) + if v_count: + for v in self._generator: + if v not in vertex_set: + continue + yield v + v_count -= 1 + if v_count == 0: + break + else: + vertex_set = set(vertex_to_id(vertex) for vertex in vertices) + v_count = len(vertex_set) + if v_count: + for v in self._generator: + if vertex_to_id(v) not in vertex_set: + continue + yield v + v_count -= 1 + if v_count == 0: + break + if v_count > 0 and not fail_silently: + raise KeyError("Not all of the given vertices have been found") + + return my_generator() + + @overload + def go_to( + self, vertex: T_vertex, fail_silently: Literal[False] = False + ) -> T_vertex: ... + + @overload + def go_to( + self, vertex: T_vertex, fail_silently: Literal[True] + ) -> Optional[T_vertex]: ... + + def go_to( + self, vertex: T_vertex, fail_silently: bool = False + ) -> Optional[T_vertex]: + """ + For a started traversal, walk through the graph, stop at *vertex* and + return it. If the traversal ends (traversal iterator is exhausted) without + having found *vertex*, raise KeyError, or return None, + if a silent fail is demanded. + + If a `VertexToID` function is used, the method searches for a vertex + that has the same id as the given *vertex*. + + When *vertex* is reported, specific attributes of the traversal object + contain additional data about the state of the traversal (see the API + documentation of the respective subclass of `Traversal`). + + :param vertex: Stop search at this vertex. + + :param fail_silently: Terminate and return None, but do not raise KeyError, + when graph is exhausted. + """ + if not isinstance(type(self), type(Traversal)): + raise RuntimeError("Method go_to can only be called on a Traversal object.") + + vertex_to_id = self._vertex_to_id + if vertex_to_id == vertex_as_id: + for v in self._generator: + if v != vertex: + continue + return v + else: + if fail_silently: + return None + else: + raise KeyError("Vertex not found, graph exhausted.") + else: + vertex_id = vertex_to_id(vertex) + for v in self._generator: + if vertex_to_id(v) != vertex_id: + continue + return v + else: + if fail_silently: + return None + else: + raise KeyError("Vertex not found, graph exhausted.") + + @abstractmethod + def _traverse(self) -> Generator[T_vertex, Any, Any]: + """Has to be implemented in subclass""" + + def _improve_state( + self, state: dict[str, Any], vertices: Optional[Iterable[T_vertex]] = None + ) -> None: + # Convert a Paths object to an object that can be converted to a str. + # Paths in the paths object can be temporary values differing from the + # final result, so we cannot iterate them, because this might show + # misleading data (for this reason, they are not even itarable). + # And a path in a paths object does not need to be hashable. So, we + # cannot even convert the valid paths to a dict and from there to a + # string. + # Thus, we manually iterate the *vertices* and create something that + # has a string representation that resembles a dict. + del state["paths"] + if vertices is not None: + state["paths"] = ( + StrRepr.from_iterable( + (vertex, self.paths[vertex]) + for vertex in vertices + if vertex in self.paths + ) + if self._build_paths + else dict() + ) + super()._improve_state(state, vertices) + + +def _start_from_needs_traversal_object(obj: Any) -> None: + if not isinstance(obj, Traversal): + raise RuntimeError( + "Method start_from can only be called on a Traversal object." + ) diff --git a/src/nographs/_strategies/traversals/with_weights/__init__.py b/src/nographs/_strategies/traversals/with_weights/__init__.py new file mode 100644 index 0000000..88a9cce --- /dev/null +++ b/src/nographs/_strategies/traversals/with_weights/__init__.py @@ -0,0 +1 @@ +""" Traversal strategies for weighted edges """ diff --git a/src/nographs/_strategies/traversals/with_weights/a_star.py b/src/nographs/_strategies/traversals/with_weights/a_star.py new file mode 100644 index 0000000..e525b54 --- /dev/null +++ b/src/nographs/_strategies/traversals/with_weights/a_star.py @@ -0,0 +1,489 @@ +from __future__ import annotations + +import itertools +from heapq import heapify, heappop, heappush +from numbers import Real +from typing import Optional, Any, Generic, Union +from collections.abc import Callable, Iterable, Generator + +from nographs._types import ( + T_vertex, + T_labels, + VertexToID, + T_vertex_id, + T_weight, + vertex_as_id, +) +from nographs._gears import ( + Gear, + GearDefault, + VertexIdToDistanceMapping, +) +from nographs._gear_collections import ( + access_to_vertex_mapping_expect_none, + access_to_vertex_mapping, +) +from ...type_aliases import NextWeightedEdges, NextWeightedLabeledEdges +from ...utils import define_distances, iter_start_vertices_and_ids +from ..traversal import Traversal +from .traversal_with_weights import ( + _create_unified_next_weighted, + _TraversalWithDistances, +) + + +class TraversalAStarFlex( + _TraversalWithDistances[T_vertex, T_vertex_id, T_weight, T_labels] +): + """ + | Bases: Generic[`T_vertex`, `T_vertex_id`, `T_weight`, `T_labels`], + | `Traversal` [`T_vertex`, `T_vertex_id`, `T_labels`] + + :param vertex_to_id: See `VertexToID` function. + + :param gear: See `gears API ` and class `Gear`. + + :param next_edges: See `NextWeightedEdges` function. If None, provide + next_labeled_edges. + + :param next_labeled_edges: See `NextWeightedLabeledEdges` function. Only allowed + if next_edges equals None. If given, paths will record the given labels. + + :param is_tree: bool: If it is certain, that during each traversal run, each vertex + can be reached only once, is_tree can be set to True. This improves performance, + but if *start_from* has been called with parameter *known_path_length_guesses* + given, this collection will not be updated during the traversal. + + **Algorithm:** The search algorithm A*, non-recursive, based on heap. + + **Input:** Weighted directed graph. Weights need to be non-negative. + One or more start vertices. Optional calculation limit. + A heuristic function that estimates the cost of the cheapest path from a given + vertex to the goal (resp. to any of your goal vertices, if you have more than + one), and never overestimates the actual needed costs ("admissible heuristic + function"). + + **Properties:** Vertices are reported and expanded ordered by increasing path + length (sum of edge weights) of the shortest paths from a start vertex to the + respective vertex that have been found so far. + + When the goal is reported, the path stored for it in *paths* is a shortest + path from start to goal and the path_length of the search state is the distance + of the goal from start. + + In case the used heuristic function is *consistent* (i.e., following an edge from + one vertex to another never reduces the estimated costs to get to the goal by + more than the weight of the edge), further guarantees hold: Each vertex is only + visited once. And for each visited vertex, the respective path_length and depth + (and optionally, the path) are the data of the shortest existing path from start + (not only from the shortest path found so far). + + **Search state:** When a vertex is *expanded* (traversal calls next_edges or + next_labeled_edges) or *reported* (an iterator of the traversal returns it), + the traversal provides updated values for the attributes + *path_length*, *depth*, *paths*. + """ + + def __init__( + self, + vertex_to_id: VertexToID[T_vertex, T_vertex_id], + gear: Gear[T_vertex, T_vertex_id, T_weight, T_labels], + next_edges: Optional[ + NextWeightedEdges[ + T_vertex, + TraversalAStarFlex[T_vertex, T_vertex_id, T_weight, T_labels], + T_weight, + ] + ] = None, + *, + next_labeled_edges: Optional[ + NextWeightedLabeledEdges[ + T_vertex, + TraversalAStarFlex[T_vertex, T_vertex_id, T_weight, T_labels], + T_weight, + T_labels, + ] + ] = None, + is_tree: bool = False, + ) -> None: + self._next_edges, labeled_edges = _create_unified_next_weighted( + next_edges, next_labeled_edges + ) + super().__init__(labeled_edges, is_tree, vertex_to_id, gear) + + # The following three values are not used by NoGraphs. They are only set + # to have some initialization. + self.path_length: T_weight = self._gear.infinity() + """ Length (sum of edge weights) of the found path to the + vertex (for the goal vertex: a shortest path) + """ + self.depth: int = -1 + """ At this *search depth*, the reported (resp. the expanded) vertex has been + found. It equals the length (number of edges) of the created path to the + vertex, if path creation is demanded. + Note: The search depth does not need to be the depth of the vertex + (see `TraversalBreadthFirstFlex`). + """ + self._start_vertices_and_ids = tuple[tuple[T_vertex, T_vertex_id]]() + + self._heuristic: Optional[Callable[[T_vertex], Real]] = None + self._known_distances: Optional[ + VertexIdToDistanceMapping[T_vertex_id, T_weight] + ] = None + self._known_path_length_guesses: Optional[ + VertexIdToDistanceMapping[T_vertex_id, T_weight] + ] = None + self._path_length_guesses: Optional[ + VertexIdToDistanceMapping[T_vertex_id, T_weight] + ] = None + + def start_from( + self, + heuristic: Callable[[T_vertex], Real], + start_vertex: Optional[T_vertex] = None, + *, + start_vertices: Optional[Iterable[T_vertex]] = None, + build_paths: bool = False, + calculation_limit: Optional[int] = None, + known_distances: Optional[ + VertexIdToDistanceMapping[T_vertex_id, T_weight] + ] = None, + known_path_length_guesses: Optional[ + VertexIdToDistanceMapping[T_vertex_id, T_weight] + ] = None, + ) -> TraversalAStarFlex[T_vertex, T_vertex_id, T_weight, T_labels]: + """ + Start the traversal at a vertex or a set of vertices and set parameters. + + :param heuristic: The admissible and consistent heuristic function that + estimates the cost of the cheapest path from a given vertex to the goal + (resp. one of the goals). + + :param start_vertex: The vertex the search should start at. If None, provide + start_vertices. + + :param start_vertices: The set of vertices the search should start + at. Only allowed if start_vertex equals None. + + :param build_paths: If true, build paths from start vertices for each reported + vertex, and an empty path for each start vertex. + + :param calculation_limit: If provided, maximal number of vertices to process + (read in) from your graph. If it is exceeded, a RuntimeError will be raised. + + :param known_distances: If provided, this mapping is used instead of an internal + one to keep the distances of vertices that have already been visited + (resp. their hashable ids from vertex_to_id is used as key) from some + start vertex. For vertices without known distance, it must yield float( + 'infinity'). The internal default implementation uses a + collections.defaultdict. + + Typical use cases are: 1) preloading known distances of vertices, and the + vertices should not be visited if no smaller distance is found during the + traversal, or 2) getting online access to the internal bookkeeping of + visited vertices and their distances, or 3) providing your own way for + storing the distance of a vertex that has already been visited. + + :param known_path_length_guesses: Like known_distances, but for keeping the sum + distance+heuristic for vertices. + + :return: Traversal, that has been started, e.g., the methods go* can now be + used. + """ + + if not isinstance(type(self), type(Traversal)): + raise RuntimeError( + "Method start_from can only be called on a Traversal object." + ) + + self._start_from( + start_vertex, + start_vertices, + build_paths, + calculation_limit, + self._gear, + ) + + # Explicitly list start vertices and their id. Needed several times. + self._start_vertices_and_ids = tuple( + iter_start_vertices_and_ids(self._start_vertices, self._vertex_to_id) + ) + + # ----- Initialize method specific public bookkeeping ----- + self._heuristic = heuristic + self._known_distances = known_distances + self._known_path_length_guesses = known_path_length_guesses + + # At start, most of the distances from a vertex to a start vertex are not + # known. If accessed for comparison for possibly better distances, infinity + # is used, if no other value is given. Each start vertex has distance 0 + # from a start vertex (itself), if not defined otherwise. + # Here, the distances need to be initialized even if is_tree == True, since + # the algorithm stores path length guesses in the heap and always uses + # the distances from the collection. + zero = self._gear.zero() + self.distances = define_distances( + self._gear, + self._known_distances, + ((vertex_id, zero) for vertex, vertex_id in self._start_vertices_and_ids), + False, + ) + + # The following two values are not used by NoGraphs. They are only set + # to have some defined values before the traversal iterator sets them. + self.path_length = self._gear.infinity() + self.depth = 0 + + super()._start() + return self + + def _traverse(self) -> Generator[T_vertex, None, Any]: + # ----- Prepare efficient environment for inner loop ----- + # Copy Traversal attributes into method scope (faster access) + labeled_edges = self._labeled_edges + maybe_vertex_to_id = ( + None if self._vertex_to_id == vertex_as_id else self._vertex_to_id + ) # Case vertex_as_id: not apply; T_vertex_id > T_vertex + build_paths = self._build_paths + calculation_limit = self._calculation_limit + predecessors = self._predecessors + attributes = self._attributes + next_edges = self._next_edges + + # Prepare limit check done by zero check + if calculation_limit is not None: + calculation_limit += 1 + + # Copy Gear attributes into method scope (faster access) + infinity = self._gear.infinity() + + # Copy traversal specific attributes into method scope + is_tree = self._is_tree + heuristic = self._heuristic + + # Get references of used gear objects and methods (avoid attribute resolution) + ( + _, + predecessors_sequence, + predecessors_wrapper, + ) = access_to_vertex_mapping_expect_none(predecessors) + ( + _, + attributes_sequence, + attributes_wrapper, + ) = access_to_vertex_mapping_expect_none(attributes) + + # ----- Initialize method specific private bookkeeping ----- + + # Unique number, that prevents heapq from sorting by vertices in case of a + # tie in the sort field, because vertices do not need to be pairwise + # comparable. The numbers are generated in decreasing order to make the min + # heap behave like a LIFO queue in case of ties. The integers from -5 to 256 + # are used first, because they are internalized (pre-calculated, and thus + # fastest). + unique_no = itertools.count(256, -1) + + # Get references of used gear objects and methods (avoid attribute resolution) + distances = self.distances + _, distances_sequence, distances_wrapper = access_to_vertex_mapping(distances) + + # Each start vertex has path_length_guess of distance + heuristic(vertex), + # if not defined otherwise. + assert heuristic is not None # set by __init__ + path_length_guesses = define_distances( + self._gear, + self._known_path_length_guesses, + ( + ( + vertex_id, + distances_sequence[vertex_id] + heuristic(vertex), + ) + for vertex, vertex_id in self._start_vertices_and_ids + ), + is_tree, + ) + # Get references of used gear objects and methods (avoid attribute resolution) + _, path_length_guesses_sequence, path_length_guesses_wrapper = ( + access_to_vertex_mapping(path_length_guesses) + ) + + # So far, the start vertices are to be visited. Each has an edge count of 0, + # and its path length guess is the one computed above. + to_visit = [ # used as collection.heapq of tuples, the lowest distance first + ( + path_length_guesses_sequence[ + vertex_id + ], # (This comment is just to prevent Black from moving the comma up) + next(unique_no), + vertex, + 0, + ) + for vertex, vertex_id in self._start_vertices_and_ids + ] + heapify(to_visit) + + # ----- Inner loop ----- + + while to_visit: + # Visit path with the lowest path_length_guess first + path_length_guess, _, vertex, path_edge_count = heappop(to_visit) + + # A vertex can get added to the heap multiple times. + + # For consistent heuristics: We want to process the vertex only once, the + # first time it is removed from the heap, because this is the case with the + # shortest distance estimation. If the heuristic is not consistent: Only + # when the new distance estimation is better than the best found so far, we + # want to process the vertex again. + v_id: T_vertex_id = ( + maybe_vertex_to_id(vertex) # type: ignore[assignment] + if maybe_vertex_to_id + else vertex + ) + + # (No index exception possible at the following indexed access) + if not is_tree and path_length_guess > path_length_guesses_sequence[v_id]: + continue + + # (No index exception possible here) + path_weight = distances_sequence[v_id] + + # Export traversal data to traversal attributes + self.path_length = path_weight + self.depth = path_edge_count + + # We now know the distance of the vertex, so we report it. + if path_edge_count > 0: # do not report start vertex + yield vertex + + if calculation_limit and not (calculation_limit := calculation_limit - 1): + raise RuntimeError("Number of visited vertices reached limit") + + # Expand vertex. New neighbors are one edge count deeper than vertex. + n_path_edge_count = path_edge_count + 1 + for edge in next_edges(vertex, self): + neighbor, weight = edge[0], edge[1] + + n_path_weight = weight + path_weight + # (Distance values equal to or higher than the chosen infinity + # value of the gear are invalid and cannot be handled further.) + if infinity <= n_path_weight: + self._gear.raise_distance_infinity_overflow_error(n_path_weight) + + # If the found path to the neighbor is not shorter than the shortest + # such path found so far, and we are not in a tree, we can safely + # ignore the path. Otherwise, it is a new candidate for a shortest + # path to the neighbor, and we push it to the heap. + n_id: T_vertex_id = ( + maybe_vertex_to_id(neighbor) # type: ignore[assignment] + if maybe_vertex_to_id + else neighbor + ) + + try: + if not is_tree and distances_sequence[n_id] <= n_path_weight: + continue + distances_sequence[n_id] = n_path_weight + except IndexError: + # n_id not in distances_collection. To be regarded as value + # infinity, i.e., n_path_weight is smaller. + distances_wrapper.extend_and_set(n_id, n_path_weight) + + # If we are to generate a path, we have to do it here, since the edge + # we have to add to the path prefix is not stored on the heap + if build_paths: + # Store the predecessor (vertex) of the neighbor + try: + predecessors_sequence[n_id] = vertex + except IndexError: + predecessors_wrapper.extend_and_set(n_id, vertex) + # Store the labels of the edge to the neighbor + if labeled_edges: + # Proof for correctness of the type hole: + # self._labeled_edges -> next_edges (a NextWeightedEdges) + # is a NextWeightedLabeledEdges -> edge[-1] is a T_labels + data_of_edge: T_labels = edge[-1] # type: ignore[assignment] + try: + attributes_sequence[n_id] = data_of_edge + except IndexError: + attributes_wrapper.extend_and_set(n_id, data_of_edge) + + h = heuristic(neighbor) + if h == infinity: + n_guess = infinity + else: + n_guess = n_path_weight + h + # (Distance values equal to or higher than the chosen infinity + # value of the gear are invalid and cannot be handled further.) + if infinity <= n_guess: + self._gear.raise_distance_infinity_overflow_error(n_guess) + + if not is_tree: + try: + path_length_guesses_sequence[n_id] = n_guess + except IndexError: + path_length_guesses_wrapper.extend_and_set(n_id, n_guess) + heappush( + to_visit, + (n_guess, next(unique_no), neighbor, n_path_edge_count), + ) + + +class TraversalAStar( + Generic[T_vertex, T_weight, T_labels], + TraversalAStarFlex[T_vertex, T_vertex, Union[T_weight, float], T_labels], +): + """ + Eases the use of `TraversalAStarFlex` for typical cases. + For documentation of functionality and parameters, see there. + + .. code-block:: python + + TraversalAStar[T_vertex, T_weight, T_labels](*args, **keywords) + + is a short form for + + .. code-block:: python + + TraversalAStarFlex[ + T_vertex, T_vertex, Union[T_weight, float], T_labels], + ](nog.vertex_as_id, nog.GearDefault(), *args, **keywords) + + Implications: + + - `GearDefault` is used, see there how it and its superclass work + - The used weights are defined by Union[T_weight, float], see `GearDefault` + - T_vertex is bound to Hashable (T_vertex is used as `T_vertex_id`, see there) + """ + + def __init__( + self, + next_edges: Optional[ + NextWeightedEdges[ + T_vertex, + TraversalAStarFlex[ + T_vertex, T_vertex, Union[T_weight, float], T_labels + ], + T_weight, + ] + ] = None, + *, + next_labeled_edges: Optional[ + NextWeightedLabeledEdges[ + T_vertex, + TraversalAStarFlex[ + T_vertex, T_vertex, Union[T_weight, float], T_labels + ], + T_weight, + T_labels, + ] + ] = None, + is_tree: bool = False, + ) -> None: + super().__init__( + vertex_as_id, + GearDefault(), + next_edges, + next_labeled_edges=next_labeled_edges, + is_tree=is_tree, + ) diff --git a/src/nographs/_extra_infinite_branching.py b/src/nographs/_strategies/traversals/with_weights/extra_infinite_branching.py similarity index 96% rename from src/nographs/_extra_infinite_branching.py rename to src/nographs/_strategies/traversals/with_weights/extra_infinite_branching.py index dba104f..effa77c 100644 --- a/src/nographs/_extra_infinite_branching.py +++ b/src/nographs/_strategies/traversals/with_weights/extra_infinite_branching.py @@ -1,28 +1,26 @@ from __future__ import annotations from typing import Any, Union, Generic, Optional -from collections.abc import Iterable, Iterator +from collections.abc import Iterable, Iterator, Generator from abc import ABC -from ._types import ( +from nographs._types import ( T_vertex_id, T_weight, WeightedUnlabeledOutEdge, WeightedOutEdge, vertex_as_id, ) -from ._gears import ( +from nographs._gears import ( Gear, GearDefault, VertexIdToDistanceMapping, ) -from ._strategies import ( - NextWeightedEdges, -) -from ._traversals import ( - define_distances, - Traversal, - _TraversalWithDistance, +from ...utils import define_distances +from ...type_aliases import NextWeightedEdges +from ..traversal import Traversal +from .traversal_with_weights import _TraversalWithDistance +from .shortest_paths import ( TraversalShortestPathsFlex, ) @@ -59,6 +57,17 @@ class TraversalShortestPathsInfBranchingSortedFlex( | Bases: Generic[`T_vertex_id`, `T_weight`], | `Traversal` [`T_vertex_id`, `T_vertex_id`, Any] + :param gear: See `gears API ` and class `Gear`. + Used for storing and returning results (graph data in the domain of the + given problem). + + :param internal_gear: See `gears API ` and class `Gear`. + Used for storing internal results (graph data in the domain of the + problem, that the given problem is reduced to). + + :param next_edges: See `NextWeightedEdges` function. If None, provide + next_labeled_edges. + **Algorithm:** Weighted shortest paths in infinitely branching graphs with sorted edges, implemented by `problem reduction ` @@ -85,17 +94,6 @@ class TraversalShortestPathsInfBranchingSortedFlex( next_labeled_edges) or *reported* (an iterator of the traversal returns it), the traversal provides updated values for the attributes *distance*, *paths*, and *distances*. - - :param gear: See `gears API ` and class `Gear`. - Used for storing and returning results (graph data in the domain of the - given problem). - - :param internal_gear: See `gears API ` and class `Gear`. - Used for storing internal results (graph data in the domain of the - problem, that the given problem is reduced to). - - :param next_edges: See `NextWeightedEdges` function. If None, provide - next_labeled_edges. """ def __init__( @@ -194,7 +192,7 @@ def start_from( super()._start() return self - def _traverse(self) -> Iterator[T_vertex_id]: + def _traverse(self) -> Generator[T_vertex_id, None, Any]: def next_state_edges( state: State[T_vertex_id], p_traversal: TraversalShortestPathsFlex[ diff --git a/src/nographs/_strategies/traversals/with_weights/minimum_spanning_tree.py b/src/nographs/_strategies/traversals/with_weights/minimum_spanning_tree.py new file mode 100644 index 0000000..9a2f8cc --- /dev/null +++ b/src/nographs/_strategies/traversals/with_weights/minimum_spanning_tree.py @@ -0,0 +1,411 @@ +from __future__ import annotations + +import itertools +from heapq import heapify, heappop, heappush +from typing import Optional, Any, Generic, Union +from collections.abc import Iterable, Generator + +from nographs._types import ( + T_vertex, + T_labels, + VertexToID, + T_vertex_id, + T_weight, + vertex_as_id, + WeightedFullEdge, + WeightedOutEdge, +) +from nographs._gears import ( + Gear, + GearDefault, + VertexIdSet, +) +from nographs._gear_collections import ( + access_to_vertex_set, + access_to_vertex_mapping_expect_none, +) +from ...type_aliases import ( + NextWeightedEdges, + NextWeightedLabeledEdges, +) +from ...utils import iter_start_vertices_and_ids +from ..traversal import Traversal +from .traversal_with_weights import _create_unified_next_weighted, _TraversalWithWeights + + +class TraversalMinimumSpanningTreeFlex( + _TraversalWithWeights[T_vertex, T_vertex_id, T_weight, T_labels] +): + """ + | Bases: Generic[`T_vertex`, `T_vertex_id`, `T_weight`, `T_labels`], + | `Traversal` [`T_vertex`, `T_vertex_id`, `T_labels`] + + :param vertex_to_id: See `VertexToID` function. + + :param gear: See `gears API ` and class `Gear`. + + :param next_edges: See `NextWeightedEdges` function. If None, provide + next_labeled_edges. + + :param next_labeled_edges: See `NextWeightedLabeledEdges` function. Only allowed + if next_edges equals None. If given, paths will record the given labels. + + **Algorithm:** Minimum spanning tree ("MST") algorithm of Jarnik, Prim, Dijkstra. + Non-recursive, based on heap. A so-called *tie breaker* is implemented, that + prioritizes edges that have been found more recently about edges that have been + found earlier. This is a typical choice that often improves search performance. + + **Properties:** Only edges of the MST from start vertices are reported. Each + vertex is reported (as end vertex of an edge) and expanded only once. Computed + paths only use MST edges. + + **Input:** Weighted undirected graph, given as directed edges with the same + weight in both directions. One or more start vertices (e.g. for components in + unconnected graphs). Optional calculation limit. + + **Search state:** When a vertex is *expanded* (traversal calls next_edges or + next_labeled_edges) or an edge is *reported* (an iterator of the traversal returns + the vertex it leads to), the traversal provides updated values for the attributes + *edge* and *paths*. + """ + + def __init__( + self, + vertex_to_id: VertexToID[T_vertex, T_vertex_id], + gear: Gear[T_vertex, T_vertex_id, T_weight, T_labels], + next_edges: Optional[ + NextWeightedEdges[ + T_vertex, + TraversalMinimumSpanningTreeFlex[ + T_vertex, T_vertex_id, T_weight, T_labels + ], + T_weight, + ] + ] = None, + *, + next_labeled_edges: Optional[ + NextWeightedLabeledEdges[ + T_vertex, + TraversalMinimumSpanningTreeFlex[ + T_vertex, T_vertex_id, T_weight, T_labels + ], + T_weight, + T_labels, + ] + ] = None, + ) -> None: + self._next_edges, labeled_edges = _create_unified_next_weighted( + next_edges, next_labeled_edges + ) + super().__init__(labeled_edges, False, vertex_to_id, gear) + + self.edge: Optional[WeightedFullEdge[T_vertex, T_weight, T_labels]] = None + """ Tuple of from_vertex, to_vertex, the weight of the edge, + and additional data you have provided with the edge (if so). + """ + + # The following value is not used by NoGraphs. It is only set + # to have some initialization. + self._start_vertices_and_ids = tuple[tuple[T_vertex, T_vertex_id]]() + + def start_from( + self, + start_vertex: Optional[T_vertex] = None, + *, + start_vertices: Optional[Iterable[T_vertex]] = None, + build_paths: bool = False, + calculation_limit: Optional[int] = None, + ) -> TraversalMinimumSpanningTreeFlex[T_vertex, T_vertex_id, T_weight, T_labels]: + """ + Start the traversal at a vertex or a set of vertices and set parameters. + If you provide more than one start vertex, the result consists of several + trees that are only connected if the start vertices are connected. + + :param start_vertex: The vertex the search should start at. If None, provide + start_vertices. + + :param start_vertices: The set of vertices the search should start + at. Only allowed if start_vertex equals None. + + :param build_paths: If true, build paths from start vertices for each reported + vertex, and an empty path for each start vertex. + + :param calculation_limit: If provided, maximal number of vertices to process + (read in) from your graph. If it is exceeded, a RuntimeError will be raised. + + :return: Traversal, that has been started, e.g., the methods go* can now be + used. + """ + + if not isinstance(type(self), type(Traversal)): + raise RuntimeError( + "Method start_from can only be called on a Traversal object." + ) + + self._start_from( + start_vertex, + start_vertices, + build_paths, + calculation_limit, + self._gear, + ) + + # Explicitly list start vertices and their id. Needed several times. + self._start_vertices_and_ids = tuple( + iter_start_vertices_and_ids(self._start_vertices, self._vertex_to_id) + ) + + # ----- Initialize method specific public bookkeeping ----- + + # The following value is not used by NoGraphs. It is only set + # to have some defined value before the traversal iterator sets them. + self.edge = None + + super()._start() + return self + + def _traverse(self) -> Generator[T_vertex, None, Any]: + # ----- Prepare efficient environment for inner loop ----- + # Copy Traversal attributes into method scope (faster access) + labeled_edges = self._labeled_edges + maybe_vertex_to_id = ( + None if self._vertex_to_id == vertex_as_id else self._vertex_to_id + ) # Case vertex_as_id: not apply; T_vertex_id > T_vertex + build_paths = self._build_paths + calculation_limit = self._calculation_limit + predecessors = self._predecessors + attributes = self._attributes + next_edges = self._next_edges + + # Get references of used gear objects and methods (avoid attribute resolution) + ( + _, + predecessors_sequence, + predecessors_wrapper, + ) = access_to_vertex_mapping_expect_none(predecessors) + ( + _, + attributes_sequence, + attributes_wrapper, + ) = access_to_vertex_mapping_expect_none(attributes) + + # ----- Initialize method specific private bookkeeping ----- + + # At start, only the start vertices are regarded as visited + # (The protocol VertexSet abandons checking the element type, see + # VertexSet. Flake8 and MyPy accept this, PyCharm does not and need + # noinspection.) + # noinspection PyTypeChecker + visited: VertexIdSet[T_vertex_id] = self._gear.vertex_id_set( + vertex_id for vertex, vertex_id in self._start_vertices_and_ids + ) + + # Check if we already go over the calculation limit when we evaluate the + # edges from start vertices ("expanding the start vertices"). This avoids a + # step by step check that slows down the to_visit loop for large sets of + # start vertices. Note: A calculation limit below 0 leads nowhere ever to an + # exception. So, neither here. + if calculation_limit is not None and calculation_limit >= 0: + if ( + calculation_limit := calculation_limit + - len(self._start_vertices_and_ids) + ) < 0: + raise RuntimeError("Number of visited vertices reached limit") + + # So far, the edges from the start vertices are to be visited as candidates + # for edges of a MST. (Unique number prevents heapq from sorting by (possibly + # not comparable) fields) + unique_no = itertools.count() + to_visit: list[ + tuple[ + T_weight, int, T_vertex, WeightedOutEdge[T_vertex, T_weight, T_labels] + ] + ] = [ # used as collection.heapq, the lowest edge weight first + (edge[1], next(unique_no), vertex, edge) + for vertex, vertex_id in self._start_vertices_and_ids + for edge in next_edges(vertex, self) + ] + heapify(to_visit) + + # Prepare limit check done by zero check + if calculation_limit is not None: + calculation_limit += 1 + + # Get references of used gear objects and methods (avoid attribute resolution) + visited_add = visited.add + ( + visited_uses_sequence, + visited_sequence, + visited_wrapper, + visited_uses_bits, + visited_index_and_bit_method, + ) = access_to_vertex_set(visited) + + # ----- Inner loop ----- + + while to_visit: + # Visit edge with the lowest weight first + _weight, _, vertex, to_edge = heappop(to_visit) + to_vertex = to_edge[0] + + # A vertex can get added to the heap multiple times, as end vertex of + # several edges. We want to process it only once, as end vertex of a MST + # edge. + # The shortest edge from a visited vertex that leads to a vertex not + # visited so far, must be an edge of the MST. + to_id: T_vertex_id = ( + maybe_vertex_to_id(to_vertex) # type: ignore[assignment] + if maybe_vertex_to_id + else to_vertex + ) + + # (If-nesting optimized for first case) + if not visited_uses_sequence: + # Standard implementation for "normal" MutableMapping + if to_id in visited: + continue + visited_add(to_id) + elif visited_uses_bits: + # Same as above, but with bits in byte sequence + sequence_key, bit_number = visited_index_and_bit_method(to_id, 8) + bit_mask = 1 << bit_number + try: + value = visited_sequence[sequence_key] + if value & bit_mask: + continue + visited_sequence[sequence_key] = value | bit_mask + except IndexError: + visited_wrapper.extend_and_set(sequence_key, bit_mask) + else: + # Same as above, but with booleans in byte sequence + try: + if visited_sequence[to_id]: + continue + visited_sequence[to_id] = True + except IndexError: + visited_wrapper.extend_and_set(to_id, True) + + if build_paths: + # Store the predecessor (vertex) of the neighbor + try: + predecessors_sequence[to_id] = vertex + except IndexError: + predecessors_wrapper.extend_and_set(to_id, vertex) + # Store the labels of the edge to the neighbor + if labeled_edges: + # Proof for correctness of the type hole: + # self._labeled_edges -> next_edges (a NextWeightedEdges) + # is a NextWeightedLabeledEdges -> to_edge[-1] is a T_labels + data_of_edge: T_labels = to_edge[-1] # type: ignore[assignment] + try: + attributes_sequence[to_id] = data_of_edge + except IndexError: + attributes_wrapper.extend_and_set(to_id, data_of_edge) + + # Export traversal data to traversal attribute and report vertex + # (Expression type follows from types of vertex and to_edge and the + # definition of WeightedFullEdge. MyPy + PyCharm cannot derive this.) + # noinspection PyTypeChecker + full_edge: WeightedFullEdge[T_vertex, T_weight, T_labels] = ( + vertex, + ) + to_edge # type: ignore[assignment] + self.edge = full_edge + yield to_vertex + + if calculation_limit and not (calculation_limit := calculation_limit - 1): + raise RuntimeError("Number of visited vertices reached limit") + + for n_to_edge in next_edges(to_vertex, self): + n_to_vertex, n_weight = n_to_edge[0], n_to_edge[1] + # If the edge leads to a vertex that is, so far, not reached by edges + # of the MST, it is a candidate for a MST edge. We push it to the heap. + n_to_id: T_vertex_id = ( + maybe_vertex_to_id(n_to_vertex) # type: ignore[assignment] + if maybe_vertex_to_id + else n_to_vertex + ) + + if not visited_uses_sequence: + # Standard implementation for "normal" MutableMapping: + if n_to_id in visited: + continue + elif visited_uses_bits: + # Same as above, but with bits in byte sequence + sequence_key, bit_number = visited_index_and_bit_method(n_to_id, 8) + try: + if visited_sequence[sequence_key] & (1 << bit_number): + continue + except IndexError: + pass + else: + # Same as above, but with booleans in byte sequence + try: + if visited_sequence[n_to_id]: + continue + except IndexError: + pass + + heappush( + to_visit, + (n_weight, next(unique_no), to_vertex, n_to_edge), + ) + + +class TraversalMinimumSpanningTree( + Generic[T_vertex, T_weight, T_labels], + TraversalMinimumSpanningTreeFlex[ + T_vertex, T_vertex, Union[T_weight, float], T_labels + ], +): + """ + Eases the use of `TraversalMinimumSpanningTreeFlex` for typical cases. + For documentation of functionality and parameters, see there. + + .. code-block:: python + + TraversalMinimumSpanningTree[T_vertex, T_weight, T_labels](*args, **keywords) + + is a short form for + + .. code-block:: python + + TraversalMinimumSpanningTreeFlex[ + T_vertex, T_vertex, Union[T_weight, float], T_labels], + ](nog.vertex_as_id, nog.GearDefault(), *args, **keywords) + + Implications: + + - `GearDefault` is used, see there how it and its superclass work + - The used weights are defined by Union[T_weight, float], see `GearDefault` + - T_vertex is bound to Hashable (T_vertex is used as `T_vertex_id`, see there) + """ + + def __init__( + self, + next_edges: Optional[ + NextWeightedEdges[ + T_vertex, + TraversalMinimumSpanningTreeFlex[ + T_vertex, T_vertex, Union[T_weight, float], T_labels + ], + T_weight, + ] + ] = None, + *, + next_labeled_edges: Optional[ + NextWeightedLabeledEdges[ + T_vertex, + TraversalMinimumSpanningTreeFlex[ + T_vertex, T_vertex, Union[T_weight, float], T_labels + ], + T_weight, + T_labels, + ] + ] = None, + ) -> None: + super().__init__( + vertex_as_id, + GearDefault(), + next_edges, + next_labeled_edges=next_labeled_edges, + ) diff --git a/src/nographs/_strategies/traversals/with_weights/shortest_paths.py b/src/nographs/_strategies/traversals/with_weights/shortest_paths.py new file mode 100644 index 0000000..e2cb040 --- /dev/null +++ b/src/nographs/_strategies/traversals/with_weights/shortest_paths.py @@ -0,0 +1,428 @@ +from __future__ import annotations + +import itertools +from heapq import heapify, heappop, heappush +from typing import Optional, Any, Generic, Union +from collections.abc import Iterable, Generator + +from nographs._gears import VertexIdToDistanceMapping +from nographs._types import ( + T_vertex, + T_labels, + VertexToID, + T_vertex_id, + T_weight, + vertex_as_id, +) +from nographs._gears import ( + Gear, + GearDefault, +) +from nographs._gear_collections import ( + access_to_vertex_mapping, + access_to_vertex_mapping_expect_none, +) +from ...type_aliases import ( + NextWeightedEdges, + NextWeightedLabeledEdges, +) +from ...utils import iter_start_vertices_and_ids, define_distances +from ..traversal import Traversal +from .traversal_with_weights import ( + _create_unified_next_weighted, + _TraversalWithDistance, +) + + +class TraversalShortestPathsFlex( + _TraversalWithDistance[T_vertex, T_vertex_id, T_weight, T_labels] +): + """ + | Bases: Generic[`T_vertex`, `T_vertex_id`, `T_weight`, `T_labels`], + | `Traversal` [`T_vertex`, `T_vertex_id`, `T_labels`] + + :param vertex_to_id: See `VertexToID` function. + + :param gear: See `gears API ` and class `Gear`. + + :param next_edges: See `NextWeightedEdges` function. If None, provide + next_labeled_edges. + + :param next_labeled_edges: See `NextWeightedLabeledEdges` function. Only allowed + if next_edges equals None. If given, paths will record the given labels. + + :param is_tree: bool: If it is certain, that during each traversal run, each vertex + can be reached only once, is_tree can be set to True. This improves performance, + but attribute *distances* of the traversal will not be updated during and after + the traversal. + + **Algorithm:** Shortest paths algorithm of Dijkstra, non-recursive, based on heap. + + **Properties:** Vertices are reported (and expanded) ordered by increasing distance + (minimally necessary sum of edge weights) from a start vertex. + + **Input:** Weighted directed graph. Weights need to be non-negative. + One or more start vertices. Optional calculation limit. + + **Search state:** When a vertex is *expanded* (traversal calls next_edges or + next_labeled_edges) or *reported* (an iterator of the traversal returns it), + the traversal provides updated values for the attributes + *distance*, *depth*, *paths*, and *distances*. + """ + + def __init__( + self, + vertex_to_id: VertexToID[T_vertex, T_vertex_id], + gear: Gear[T_vertex, T_vertex_id, T_weight, T_labels], + next_edges: Optional[ + NextWeightedEdges[ + T_vertex, + TraversalShortestPathsFlex[T_vertex, T_vertex_id, T_weight, T_labels], + T_weight, + ] + ] = None, + *, + next_labeled_edges: Optional[ + NextWeightedLabeledEdges[ + T_vertex, + TraversalShortestPathsFlex[T_vertex, T_vertex_id, T_weight, T_labels], + T_weight, + T_labels, + ] + ] = None, + is_tree: bool = False, + ) -> None: + self._next_edges, labeled_edges = _create_unified_next_weighted( + next_edges, next_labeled_edges + ) + super().__init__(labeled_edges, is_tree, vertex_to_id, gear) + + self._known_distances: Optional[ + VertexIdToDistanceMapping[T_vertex_id, T_weight] + ] = None + self._keep_distances = False + + # The following value is not used by NoGraphs. It is only set + # to have some initialization. + self.depth: int = -1 + """ At this *search depth*, the reported (resp. the expanded) vertex has been + found. It equals the length (number of edges) of the created path to the + vertex, if path creation is demanded. + Note: The search depth does not need to be the depth of the vertex + (see `TraversalBreadthFirstFlex`). + When a traversal has been started, but no vertex has been reported or expanded + so far, the depth is 0 (depth of the start vertices). + """ + self._start_vertices_and_ids = tuple[tuple[T_vertex, T_vertex_id]]() + + def start_from( + self, + start_vertex: Optional[T_vertex] = None, + *, + start_vertices: Optional[Iterable[T_vertex]] = None, + build_paths: bool = False, + calculation_limit: Optional[int] = None, + keep_distances: bool = False, + known_distances: Optional[ + VertexIdToDistanceMapping[T_vertex_id, T_weight] + ] = None, + ) -> TraversalShortestPathsFlex[T_vertex, T_vertex_id, T_weight, T_labels]: + """ + Start the traversal at a vertex or a set of vertices and set parameters. + + :param start_vertex: The vertex the search should start at. If None, provide + start_vertices. + + :param start_vertices: The set of vertices the search should start + at. Only allowed if start_vertex equals None. + + :param build_paths: If true, build paths from start vertices for each reported + vertex, and an empty path for each start vertex. + + :param calculation_limit: If provided, maximal number of vertices to process + (read in) from your graph. If it is exceeded, a RuntimeError will be raised. + + :param keep_distances: If True, the found distances of vertices are + collected in traversal attribute distances, and not deleted after + having reported the vertex. See attribute distances. + + :param known_distances: If provided, this mapping is used instead of an internal + one to keep distance candidates and final distances values of reported + vertices (resp. their hashable ids from vertex_to_id is used as key) from + some start vertex. + + For vertices without known distance, it must yield float('infinity'). The + internal default implementation uses a collections.defaultdict. Typical + use cases are: 1) preloading known distances of vertices, and the + vertices should not be visited if no smaller distance is found during the + traversal, and 2) providing your own way for storing the distances. + + :return: Traversal, that has been started, e.g., the methods go* can now be + used. + """ + + if not isinstance(type(self), type(Traversal)): + raise RuntimeError( + "Method start_from can only be called on a Traversal object." + ) + + self._start_from( + start_vertex, + start_vertices, + build_paths, + calculation_limit, + self._gear, + ) + + # Explicitly list start vertices and their id. Needed several times. + self._start_vertices_and_ids = tuple( + iter_start_vertices_and_ids(self._start_vertices, self._vertex_to_id) + ) + + # ----- Initialize method specific public bookkeeping ----- + self._keep_distances = keep_distances + self._known_distances = known_distances + + # At start, most of the distances from a vertex to a start vertex are not + # known. If accessed for comparison for possibly better distances, infinity + # is used, if no other value is given. Each start vertex has distance 0 + # from a start vertex (itself), if not defined otherwise. + zero = self._gear.zero() + self.distances = define_distances( + self._gear, + self._known_distances, + ((vertex_id, zero) for vertex, vertex_id in self._start_vertices_and_ids), + self._is_tree, + ) + + # The following two values are not used by NoGraphs. They are only set + # to have some defined values before the traversal iterator sets them. + self.distance = self._gear.infinity() + self.depth = 0 + + super()._start() + return self + + def _traverse(self) -> Generator[T_vertex, None, Any]: + # ----- Prepare efficient environment for inner loop ----- + # Copy Traversal attributes into method scope (faster access) + labeled_edges = self._labeled_edges + maybe_vertex_to_id = ( + None if self._vertex_to_id == vertex_as_id else self._vertex_to_id + ) # Case vertex_as_id: not apply; T_vertex_id > T_vertex + build_paths = self._build_paths + calculation_limit = self._calculation_limit + predecessors = self._predecessors + attributes = self._attributes + next_edges = self._next_edges + + # Prepare limit check done by zero check + if calculation_limit is not None: + calculation_limit += 1 + + # Copy Gear attributes into method scope (faster access) + infinity = self._gear.infinity() + + # Copy traversal specific attributes into method scope + is_tree = self._is_tree + keep_distances = self._keep_distances + + # Get references of used gear objects and methods (avoid attribute resolution) + ( + _, + predecessors_sequence, + predecessors_wrapper, + ) = access_to_vertex_mapping_expect_none(predecessors) + ( + _, + attributes_sequence, + attributes_wrapper, + ) = access_to_vertex_mapping_expect_none(attributes) + zero = self._gear.zero() + + # ----- Initialize method specific private bookkeeping ----- + + # Unique number, that prevents heapq from sorting by vertices in case of a + # tie in the sort field, because vertices do not need to be pairwise + # comparable. The integers from -5 to 256 are used first, because they are + # internalized (pre-calculated, and thus fastest). We count downwards like we + # do in A* search. There, it is preferable, because a LIFO behavior makes A* + # often faster. Here, we do it simply to do it the same way. + unique_no = itertools.count(256, -1) + + # Get references of used gear objects and methods (avoid attribute resolution) + distances = self.distances + _, distances_sequence, distances_wrapper = access_to_vertex_mapping(distances) + + # So far, the start vertices are to be visited. Each has an edge count of 0. + # (We know: vertex_id in distances. No index exception possible here.) + to_visit = [ # used as collection.heapq of tuples, the lowest distance first + ( + (distances_sequence[vertex_id]), + next(unique_no), + vertex, + 0, + ) + for vertex, vertex_id in self._start_vertices_and_ids + ] + heapify(to_visit) + + # ----- Inner loop ----- + + while to_visit: + # Visit path with the lowest distance first + path_weight, _, vertex, path_edge_count = heappop(to_visit) + + # A vertex can get added to the heap multiple times. We want to process + # it only once, the first time it is removed from the heap, because this + # is the case with the shortest distance from start. + if not is_tree: + v_id: T_vertex_id = ( + maybe_vertex_to_id(vertex) # type: ignore[assignment] + if maybe_vertex_to_id + else vertex + ) + + # (We know: v_id in distances. No index exception possible here.) + if distances_sequence[v_id] < path_weight: + continue + if not keep_distances: + # Allow garbage collector to free distance value (not the entry in + # the collection!) if nowhere else needed any more. Note that + # we can only set to the zero value here, since we still have to + # exclude further longer paths to the vertex, and only zero does + # this job for all possible distances. + # (We know: v_id in distances. No index exception possible here.) + distances_sequence[v_id] = zero + + # Export traversal data to traversal attributes + self.distance = path_weight + self.depth = path_edge_count + + # We now know the distance of the vertex, so we report it. + if path_edge_count > 0: # do not report start vertex + yield vertex + + if calculation_limit and not (calculation_limit := calculation_limit - 1): + raise RuntimeError("Number of visited vertices reached limit") + + # Expand vertex. New neighbors are one edge count deeper than vertex. + n_path_edge_count = path_edge_count + 1 + for edge in next_edges(vertex, self): + neighbor, weight = edge[0], edge[1] + + n_path_weight = weight + path_weight + # (Distance values equal to or higher than the chosen infinity + # value of the gear are invalid and cannot be handled further.) + if infinity <= n_path_weight: + self._gear.raise_distance_infinity_overflow_error(n_path_weight) + + # If the found path to the neighbor is not shorter than the shortest + # such path found so far, we can safely ignore it. Otherwise, it is a + # new candidate for a shortest path to the neighbor, and we push it to + # the heap. + if build_paths or not is_tree: + n_id: T_vertex_id = ( + maybe_vertex_to_id(neighbor) # type: ignore[assignment] + if maybe_vertex_to_id + else neighbor + ) + + if not is_tree: + try: + if distances_sequence[n_id] <= n_path_weight: + continue + distances_sequence[n_id] = n_path_weight + except IndexError: + distances_wrapper.extend_and_set(n_id, n_path_weight) + + # If we are to generate a path, we have to do it here, since the + # edge we have to add to the path prefix is not stored on the heap. + if build_paths: + # Store the predecessor (vertex) of the neighbor + try: + predecessors_sequence[n_id] = vertex + except IndexError: + predecessors_wrapper.extend_and_set(n_id, vertex) + # Store the labels of the edge to the neighbor + if labeled_edges: + # Proof for correctness of the type hole: + # self._labeled_edges -> next_edges (a NextWeightedEdges) + # is a NextWeightedLabeledEdges -> edge[-1] is a T_labels + data_of_edge: T_labels = edge[ + -1 + ] # type: ignore[assignment] + try: + attributes_sequence[n_id] = data_of_edge + except IndexError: + attributes_wrapper.extend_and_set(n_id, data_of_edge) + + heappush( + to_visit, + ( + n_path_weight, + next(unique_no), + neighbor, + n_path_edge_count, + ), + ) + + +class TraversalShortestPaths( + Generic[T_vertex, T_weight, T_labels], + TraversalShortestPathsFlex[T_vertex, T_vertex, Union[T_weight, float], T_labels], +): + """ + Eases the use of `TraversalShortestPathsFlex` for typical cases. + For documentation of functionality and parameters, see there. + + .. code-block:: python + + TraversalShortestPaths[T_vertex, T_weight, T_labels](*args, **keywords) + + is a short form for + + .. code-block:: python + + TraversalShortestPathsFlex[ + T_vertex, T_vertex, Union[T_weight, float], T_labels], + ](nog.vertex_as_id, nog.GearDefault(), *args, **keywords) + + Implications: + + - `GearDefault` is used, see there how it and its superclass work + - The used weights are defined by Union[T_weight, float], see `GearDefault` + - T_vertex is bound to Hashable (T_vertex is used as `T_vertex_id`, see there) + """ + + def __init__( + self, + next_edges: Optional[ + NextWeightedEdges[ + T_vertex, + TraversalShortestPathsFlex[ + T_vertex, T_vertex, Union[T_weight, float], T_labels + ], + T_weight, + ] + ] = None, + *, + next_labeled_edges: Optional[ + NextWeightedLabeledEdges[ + T_vertex, + TraversalShortestPathsFlex[ + T_vertex, T_vertex, Union[T_weight, float], T_labels + ], + T_weight, + T_labels, + ] + ] = None, + is_tree: bool = False, + ) -> None: + super().__init__( + vertex_as_id, + GearDefault(), + next_edges, + next_labeled_edges=next_labeled_edges, + is_tree=is_tree, + ) diff --git a/src/nographs/_strategies/traversals/with_weights/traversal_with_weights.py b/src/nographs/_strategies/traversals/with_weights/traversal_with_weights.py new file mode 100644 index 0000000..726ee2d --- /dev/null +++ b/src/nographs/_strategies/traversals/with_weights/traversal_with_weights.py @@ -0,0 +1,175 @@ +from __future__ import annotations + +from abc import ABC +from numbers import Real +from typing import Generic, Any, Optional, Iterable, Iterator + +from nographs._types import ( + T_vertex, + T_labels, + VertexToID, + T_vertex_id, + T_weight, +) +from nographs._gears import ( + Gear, + VertexIdToDistanceMapping, +) +from ...type_aliases import ( + NextWeightedMaybeLabeledEdges, + NextWeightedEdges, + NextWeightedLabeledEdges, +) +from ...utils import NoDistancesMapping, StrRepr +from ..traversal import Traversal +from ...type_aliases import ( + T_strategy, +) + + +def _create_unified_next_weighted( + next_edges: Optional[NextWeightedEdges[T_vertex, T_strategy, T_weight]], + next_labeled_edges: Optional[ + NextWeightedLabeledEdges[T_vertex, T_strategy, T_weight, T_labels] + ], +) -> tuple[ + NextWeightedMaybeLabeledEdges[T_vertex, T_strategy, T_weight, T_labels], bool +]: + """Check configuration of given next_edges and next_labeled_edges and calculate + unified _NextWeightedMaybeLabeledEdges[] and whether we have labeled_edges.""" + next_maybe_labeled_edges: NextWeightedMaybeLabeledEdges[ + T_vertex, T_strategy, T_weight, T_labels + ] + if next_edges is not None: + if next_labeled_edges is not None: + raise RuntimeError("Both next_edges and next_labeled_edges provided.") + next_maybe_labeled_edges = next_edges + labeled_edges = False + else: + if next_labeled_edges is None: + raise RuntimeError("Neither next_edges and next_labeled_edges provided.") + next_maybe_labeled_edges = next_labeled_edges + labeled_edges = True + return next_maybe_labeled_edges, labeled_edges + + +class _TraversalWithWeights( + Generic[T_vertex, T_vertex_id, T_weight, T_labels], + Traversal[T_vertex, T_vertex_id, T_labels], + ABC, +): + """A Traversal that needs weighted edges and uses a gear suitable for this.""" + + def __init__( + self, + labeled_edges: bool, + is_tree: bool, + vertex_to_id: VertexToID[T_vertex, T_vertex_id], + gear: Gear[T_vertex, T_vertex_id, T_weight, T_labels], + ) -> None: + self._gear = gear + super().__init__(labeled_edges, is_tree, vertex_to_id) + + +class _TraversalWithDistances( + _TraversalWithWeights[T_vertex, T_vertex_id, T_weight, T_labels], + ABC, +): + """ + A _TraversalWithWeights that provides a distances collection as part of + its state. + """ + + def __init__( + self, + labeled_edges: bool, + is_tree: bool, + vertex_to_id: VertexToID[T_vertex, T_vertex_id], + gear: Gear[T_vertex, T_vertex_id, T_weight, T_labels], + ) -> None: + super().__init__(labeled_edges, is_tree, vertex_to_id, gear) + + self.distances: VertexIdToDistanceMapping[T_vertex_id, T_weight] = ( + NoDistancesMapping[T_vertex_id, T_weight]() + ) + """ Provisional or final distance values of some vertices + (distance from a start vertex). Without option *keep_distances*, + the value for a vertex is removed once the vertex has been reported. With + option *keep_distances*, values are never removed, and that means: During a + traversal, the distance values for already reported vertices can be found in + the collection. After an exhaustive search, the collection contains exactly + and only the distances of all vertices that are reachable from the start + vertices and of the start vertices themselves. + """ + + def _improve_state( + self, state: dict[str, Any], vertices: Optional[Iterable[T_vertex]] = None + ) -> None: + # Assignments in distances are only valid for reported vertices. Thus, + # we need to convert only keys/values for requested vertices to a string, + # not the whole MutableMapping. So, we special case this attribute here. + del state["distances"] + if vertices is not None: + vertex_to_id, distances = self._vertex_to_id, self.distances + state["distances"] = StrRepr.from_iterable( + (v_id := vertex_to_id(vertex), distances[v_id]) for vertex in vertices + ) + super()._improve_state(state, vertices) + + +class _TraversalWithDistance( + _TraversalWithDistances[T_vertex, T_vertex_id, T_weight, T_labels], + ABC, +): + """ + A _TraversalWithDistances that provides a distance as part of its staste. + If offers the go_for_distance_range method based on the distance. + """ + + def __init__( + self, + labeled_edges: bool, + is_tree: bool, + vertex_to_id: VertexToID[T_vertex, T_vertex_id], + gear: Gear[T_vertex, T_vertex_id, T_weight, T_labels], + ) -> None: + super().__init__(labeled_edges, is_tree, vertex_to_id, gear) + + # The following value is not used by NoGraphs. It is only set + # to have some initialization. + self.distance: T_weight = self._gear.infinity() + """ The length of the shortest path (sum of edge weights) from a + start vertex to the visited vertex + """ + + def go_for_distance_range(self, start: Real, stop: Real) -> Iterator[T_vertex]: + """ + For a started traversal, return an iterator. During the traversal, + the iterator skips vertices as long as their distance is lower than *start*. + From then on, is reports the found vertices. It stops when the reached + distance is equal to or higher than *stop*. + + Note: The first vertex with a distance equal or higher than stop will be + consumed from the traversal, but will not be reported, so it is lost (compare + itertools.takewhile). + """ + if not isinstance(type(self), type(Traversal)): + raise RuntimeError( + "Method go_for_distance_range can only be called " + + "on a Traversal object." + ) + + # In order to make the above check work, the following generator functionality + # needs to be encapsulated in a local function + def my_generator() -> Iterator[T_vertex]: + for v in self._generator: + if self.distance >= start: + if self.distance < stop: + yield v + break + for v in self._generator: + if self.distance >= stop: + break + yield v + + return my_generator() diff --git a/src/nographs/_strategies/traversals/without_weights/__init__.py b/src/nographs/_strategies/traversals/without_weights/__init__.py new file mode 100644 index 0000000..b37ba2d --- /dev/null +++ b/src/nographs/_strategies/traversals/without_weights/__init__.py @@ -0,0 +1 @@ +""" Traversal strategies for unweighted edges """ diff --git a/src/nographs/_strategies/traversals/without_weights/breadth_first.py b/src/nographs/_strategies/traversals/without_weights/breadth_first.py new file mode 100644 index 0000000..ccee6d2 --- /dev/null +++ b/src/nographs/_strategies/traversals/without_weights/breadth_first.py @@ -0,0 +1,443 @@ +from __future__ import annotations + +import copy +from typing import Optional, Any, Generic +from collections.abc import Iterable, Iterator, Generator + + +from nographs._types import ( + T_vertex, + T_vertex_id, + T_labels, + VertexToID, + vertex_as_id, +) +from nographs._gears import ( + GearDefault, + GearWithoutDistances, + VertexIdSet, +) +from nographs._gear_collections import ( + access_to_vertex_set, + access_to_vertex_mapping_expect_none, +) +from ...type_aliases import ( + NextVertices, + NextEdges, + NextLabeledEdges, +) +from ..traversal import ( + Traversal, + _start_from_needs_traversal_object, +) +from .traversal_without_weights import ( + _create_unified_next, + _TraversalWithoutWeightsWithVisited, +) + + +class TraversalBreadthFirstFlex( + _TraversalWithoutWeightsWithVisited[T_vertex, T_vertex_id, T_labels] +): + """ + Bases: `Traversal` [`T_vertex`, `T_vertex_id`, `T_labels`] + + :param vertex_to_id: See `VertexToID` function. + + :param gear: See `gears API ` and class `GearWithoutDistances`. + + :param next_vertices: See `NextVertices` function. If None, provide next_edges + or next_labeled_edges. + + :param next_edges: See `NextEdges` function. Only allowed if next_vertices equals + None. If both are None, provide next_labeled_edges. + + :param next_labeled_edges: See `NextLabeledEdges` function. Only allowed if + next_vertices and next_edges equal None. If given, paths will record the given + labels. + + :param is_tree: bool: If it is certain, that during each traversal run, + each vertex can be reached only once, is_tree can be set to True. This + improves performance, but attribute *visited* of the traversal will not be + updated during and after the traversal. + + **Algorithm:** Breadth First Search ("BFS"), non-recursive implementation. + Vertices are reported when they are "seen" (read from the graph) for the + first time. + + **Properties:** + Reports vertices in Breadth First order, i.e., + with ascending depth (edge count of the path with the fewest edges from a + start vertex). All computed paths are *shortest paths* , i.e., paths with + minimal number of edges from a start vertex to their end vertex. + + A vertex is considered visited when it has been reported or if it is a + start vertex. + + **Input:** Directed graph. Unlabeled or labeled edges. One or more start + vertices. Optional calculation limit. + + **Search state:** When a vertex is *expanded* + (traversal calls next_vertices, next_edges or next_labeled_edges) + or *reported* (an iterator of the traversal returns it), + the traversal provides updated values for the attributes + *depth*, *paths*, and *visited*. + """ + + def __init__( + self, + vertex_to_id: VertexToID[T_vertex, T_vertex_id], + gear: GearWithoutDistances[T_vertex, T_vertex_id, T_labels], + next_vertices: Optional[ + NextVertices[ + T_vertex, TraversalBreadthFirstFlex[T_vertex, T_vertex_id, T_labels] + ] + ] = None, + *, + next_edges: Optional[ + NextEdges[ + T_vertex, TraversalBreadthFirstFlex[T_vertex, T_vertex_id, T_labels] + ] + ] = None, + next_labeled_edges: Optional[ + NextLabeledEdges[ + T_vertex, + TraversalBreadthFirstFlex[T_vertex, T_vertex_id, T_labels], + T_labels, + ] + ] = None, + is_tree: bool = False, + ) -> None: + ( + self._next_edge_or_vertex, + edges_with_data, + labeled_edges, + ) = _create_unified_next(next_vertices, next_edges, next_labeled_edges) + super().__init__(edges_with_data, labeled_edges, is_tree, vertex_to_id, gear) + self.depth: int = -1 # value not used, initialized during traversal + """ + At this *search depth*, the reported (resp. the expanded) vertex has been + found. It equals the length (number of edges) of the created path to the + vertex, if path creation is demanded. + + For the special case of this traversal, it equals the + *depth of the vertex* (minimal number of edges needed to come to it + from a start vertex). + When a traversal has been started, but no vertex has been reported or expanded + so far, the depth is 0 (depth of the start vertices). + """ + self._report_depth_increase = False + + def start_from( + self, + start_vertex: Optional[T_vertex] = None, + *, + start_vertices: Optional[Iterable[T_vertex]] = None, + build_paths: bool = False, + calculation_limit: Optional[int] = None, + already_visited: Optional[VertexIdSet[T_vertex_id]] = None, + _report_depth_increase: bool = False, # hidden parameter for internal use + ) -> TraversalBreadthFirstFlex[T_vertex, T_vertex_id, T_labels]: + """ + Start the traversal at a vertex or a set of vertices and set parameters. + + :param start_vertex: The vertex the search should start at. If None, provide + start_vertices. + + :param start_vertices: The vertices the search should start at. Only + allowed if start_vertex equals None. + + :param build_paths: If true, build paths from some start vertex to each visited + vertex. Paths of start vertices are empty paths. + + :param calculation_limit: If provided, maximal number of vertices to process + (read in) from your graph. If it is exceeded, a RuntimeError will be raised. + + :param already_visited: If provided, this set is used instead of an internal + one to keep vertices (resp. their hashable ids from vertex_to_id), + that have already been visited. This parameter can be used to get online + access to the internal bookkeeping of visited vertices, or to preload + vertices that should never be visited, or to provide your own way for + storing the information that a vertex has already been visited. + + :return: Traversal, that has been started, e.g., statements like *iter()*, + *next()*, *for* and the methods "go*" of the Traversal can now be used. + """ + _start_from_needs_traversal_object(self) + self._start_without_weights_with_visited( + start_vertex, + start_vertices, + build_paths, + calculation_limit, + already_visited, + ) + self.depth = 0 + self._report_depth_increase = _report_depth_increase + + super()._start() + return self + + def _traverse(self) -> Generator[T_vertex, None, Any]: + # ----- Prepare efficient environment for inner loop ----- + # Copy Traversal attributes into method scope (faster access) + labeled_edges = self._labeled_edges + maybe_vertex_to_id = ( + None if self._vertex_to_id == vertex_as_id else self._vertex_to_id + ) # Case vertex_as_id: not apply; T_vertex_id > T_vertex + build_paths = self._build_paths + calculation_limit = self._calculation_limit + predecessors = self._predecessors + attributes = self._attributes + + # Prepare limit check done by zero check + if calculation_limit is not None: + calculation_limit += 1 + + # Copy _TraversalWithoutWeightsWithVisited attributes into method scope + edges_with_data = self._edges_with_data + next_edge_or_vertex = self._next_edge_or_vertex + + # Get references of used gear objects and methods (avoid attribute resolution) + ( + _, + predecessors_sequence, + predecessors_wrapper, + ) = access_to_vertex_mapping_expect_none(predecessors) + ( + _, + attributes_sequence, + attributes_wrapper, + ) = access_to_vertex_mapping_expect_none(attributes) + + # Copy further traversal attributes into method scope (faster access) + is_tree = self._is_tree + visited = self.visited + + # Get further references of used gear objects and methods + # (avoid attribute resolution) + visited_add = visited.add + ( + visited_uses_sequence, + visited_sequence, + visited_wrapper, + visited_uses_bits, + visited_index_and_bit_method, + ) = access_to_vertex_set(visited) + + # Copy Traversal-specific attributes into method scope (faster access) + report_depth_increase = self._report_depth_increase + + # ----- Initialize method specific bookkeeping ----- + + # Two lists used as FIFO queue with just two buckets + # (using a queue and counting down the size of current depth horizon is slower, + # and creating a new list instead of clear() is also slower) + to_expand = self._gear.sequence_of_vertices(self._start_vertices) + next_to_expand = self._gear.sequence_of_vertices(()) + + # During an ongoing expansion of some vertex we will already report the + # new found neighbors. For the former, the depth needs to remain the old + # one, while for the latter, it needs to be one higher. In order to avoid + # a cascade of +1 and -1 on the depth, we just use a copy of the traversal, + # that hangs by one in the depth, and give this to next_edge_or_vertices. + prev_traversal = copy.copy(self) # copy of self, for keeping previous depth + self.depth = 1 # used for reporting (prev_traversal starts at 0) + + # Get method references of specific bookkeeping (avoid attribute resolution) + to_expand_append = to_expand.append + next_to_expand_append = next_to_expand.append + + # ----- Typing preparation of inner loop ----- + + # The following type Any opens no space for typing problems, since + # the content of next_edge_or_vertex is type checked and iterates + # objects of type T_vertex for edges_with_data==False and otherwise of + # one of the following: + # WeightedUnlabeledOutEdge[T_vertex, Any], + # UnweightedLabeledOutEdge[T_vertex, T_labels], + # WeightedLabeledOutEdge[T_vertex, Any, T_labels], + # And if labeled_edges==True, the first case is excluded. + # (Any alternative code version of the inner loop without + # Any or 'type: ignore' is slower) + edge_or_vertex: Any # "Hole" in typing, but types "around" make it safe + neighbor: T_vertex # Re-establish type "after" the "hole" + data_of_edge: T_labels # Re-establish type "after" the "hole" + + # ----- Inner loop ----- + + while to_expand: + for vertex in to_expand: + if calculation_limit and not ( + calculation_limit := calculation_limit - 1 + ): + raise RuntimeError("Number of visited vertices reached limit") + + for edge_or_vertex in next_edge_or_vertex(vertex, prev_traversal): + neighbor = edge_or_vertex[0] if edges_with_data else edge_or_vertex + + if not is_tree or build_paths: + n_id: T_vertex_id = ( + maybe_vertex_to_id(neighbor) # type: ignore[assignment] + if maybe_vertex_to_id + else neighbor + ) + + # If not is_tree: Ignore neighbor if already seen, and + # else include its ID in visited set. + # (If-nesting optimized for first case) + if not is_tree and not visited_uses_sequence: + # Standard implementation for "normal" MutableMapping + if n_id in visited: + continue + visited_add(n_id) + elif not is_tree and visited_uses_bits: + # Same as above, but with bits in byte sequence + sequence_key, bit_number = visited_index_and_bit_method( + n_id, 8 + ) + bit_mask = 1 << bit_number + try: + value = visited_sequence[sequence_key] + if value & bit_mask: + continue + visited_sequence[sequence_key] = value | bit_mask + except IndexError: + visited_wrapper.extend_and_set(sequence_key, bit_mask) + elif not is_tree: + # Same as above, but with booleans in byte sequence + try: + if visited_sequence[n_id]: + continue + visited_sequence[n_id] = True + except IndexError: + visited_wrapper.extend_and_set(n_id, True) + + if build_paths: + # Store the predecessor (vertex) of the neighbor + try: + predecessors_sequence[n_id] = vertex + except IndexError: + predecessors_wrapper.extend_and_set(n_id, vertex) + # Store the labels of the edge to the neighbor + if labeled_edges: + data_of_edge = edge_or_vertex[-1] + try: + attributes_sequence[n_id] = data_of_edge + except IndexError: + attributes_wrapper.extend_and_set( + n_id, data_of_edge + ) + + # Vertex has been seen, report it now + yield neighbor + # Needs to be expanded in the next round + next_to_expand_append(neighbor) + + if report_depth_increase and next_to_expand: + # We are not finished yet, because we found new vertices to expand, + # and we are about to increase the depth now, and it is demanded + # to report this situation by reporting the last vertex reported so far + # again. So we report it again. + yield next_to_expand[-1] + + # Update external views (reporting/expanding) on depth + self.depth += 1 + prev_traversal.depth += 1 + # Prepare state for next depth level of vertices + to_expand, next_to_expand, to_expand_append, next_to_expand_append = ( + next_to_expand, + to_expand, + next_to_expand_append, + to_expand_append, + ) + del next_to_expand[:] + + # Correct the depth to the search depth of last visited vertex. If + # start_vertices was given, and the argument was empty, the result will be -1. + # The documentation does not specify this behaviour, but it might be expected. + self.depth -= 2 + + def go_for_depth_range(self, start: int, stop: int) -> Iterator[T_vertex]: + """ + For a started traversal, return an iterator. During the traversal, + the iterator skips vertices as long as their depth is lower than *start*. + From then on, is reports the found vertices. It stops when the reached depth + is equal to or higher than *stop*. + + Note: The first vertex with a depth equal or higher than *stop* will be + consumed from the traversal, but will not be reported, so it is lost (compare + *itertools.takewhile*). + + :param start: Vertices lower than this are skipped. + :param stop: Reporting stops when reached depth is equal or higher than this. + """ + if not isinstance(type(self), type(Traversal)): + raise RuntimeError( + "Method go_for_depth_range can only be called " + + "on a Traversal object." + ) + + # In order to make the above check work, the following generator functionality + # needs to be encapsulated in a local function + def my_generator() -> Iterator[T_vertex]: + for v in self._generator: + if self.depth >= start: + if self.depth < stop: + yield v + break + for v in self._generator: + if self.depth >= stop: + break + yield v + + return my_generator() + + +class TraversalBreadthFirst( + Generic[T_vertex, T_labels], TraversalBreadthFirstFlex[T_vertex, T_vertex, T_labels] +): + """ + Eases the use of `TraversalBreadthFirstFlex` for typical cases. + For documentation of functionality and parameters, see there. + + Uses the following standard arguments for the respective parameters of + the parent class: + + - vertex_to_id = `vertex_as_id` + - gear = `GearDefault` + - `T_vertex_id` = `T_vertex` + + Implications: + + - `GearDefault` is used, see there how it and its superclass work + - T_vertex is bound to Hashable (T_vertex is used as `T_vertex_id`, see there) + """ + + def __init__( + self, + next_vertices: Optional[ + NextVertices[ + T_vertex, TraversalBreadthFirstFlex[T_vertex, T_vertex, T_labels] + ] + ] = None, + *, + next_edges: Optional[ + NextEdges[T_vertex, TraversalBreadthFirstFlex[T_vertex, T_vertex, T_labels]] + ] = None, + next_labeled_edges: Optional[ + NextLabeledEdges[ + T_vertex, + TraversalBreadthFirstFlex[T_vertex, T_vertex, T_labels], + T_labels, + ] + ] = None, + is_tree: bool = False, + ) -> None: + super().__init__( + vertex_as_id, + GearDefault(), + next_vertices, + next_edges=next_edges, + next_labeled_edges=next_labeled_edges, + is_tree=is_tree, + ) diff --git a/src/nographs/_strategies/traversals/without_weights/depth_first.py b/src/nographs/_strategies/traversals/without_weights/depth_first.py new file mode 100644 index 0000000..b33b3d8 --- /dev/null +++ b/src/nographs/_strategies/traversals/without_weights/depth_first.py @@ -0,0 +1,1500 @@ +from __future__ import annotations + +import array +from typing import Optional, Any, Generic +from collections.abc import Iterable, Generator +from enum import Flag, auto + +from nographs._types import ( + T_vertex, + T_vertex_id, + T_labels, + VertexToID, + vertex_as_id, +) +from nographs._gears import ( + GearDefault, + GearWithoutDistances, + VertexIdSet, +) +from nographs._gear_collections import ( + access_to_vertex_set, + access_to_vertex_mapping_expect_none, + access_to_vertex_mapping, +) +from ...type_aliases import ( + NextVertices, + NextEdges, + NextLabeledEdges, +) +from ...utils import ( + StrRepr, +) +from ..traversal import ( + Traversal, + _start_from_needs_traversal_object, +) +from .traversal_without_weights import ( + _create_unified_next, + _TraversalWithoutWeightsWithVisited, +) + + +class DFSEvent(Flag): + """ + An enumeration of the events that can trigger the report of a vertex / edge by + TraversalDepthFirst. + + Events reporting that a vertex is entered or left: + + - ENTERING_START: A start vertex has been entered and the traversal starts + there. + + - LEAVING_START: A start vertex has been left (the traversal may continue + with the next one). + + - ENTERING_SUCCESSOR: A vertex is entered, when an edge + that leads to it is followed. In mode *DFS_TREE*, only *DFS-tree edges* + are followed. + + - LEAVING_SUCCESSOR: A vertex is left, when an edge that leads to it + and has been followed, is now followed in reversed direction, during + backtracking from the edge. In mode *DFS_TREE*, only *DFS-tree edges* + are followed. + + Events reporting that a vertex (or an edge) has been detected but will not be + entered (resp. followed): + + - SKIPPING_START: A start vertex was about to be entered, as start of a + traversal from there, but it has already been visited as descendant of + another start vertex, and thus, it is skipped. + + - BACK_EDGE: An edge *(u, v)* is found, where v has already been entered, + but not left so far. In other words, *v* is on the trace (path that + leads to *u* within the tree). + + - FORWARD_EDGE: An edge *(u, v)* is found, where *v* has already been + left, and it had been entered after *u*. *(u, v)* is a shortcut + forwards in the tree branch from *u* to *v*, so to speak. + + - CROSS_EDGE: An edge *(u, v)* is found, where *v* has already been left, + and it had been entered before *u*. This means, in the DFS tree, + *u* and *v* do not have any ancestor or descendant relationship + between them. + + Events that combine other events as a group (*group-events*): + + - SOME_NON_TREE_EDGE: One of the events FORWARD_EDGE, BACK_EDGE, or CROSS_EDGE + occurred, but it has not been determined which of these events. + + - FORWARD_OR_CROSS_EDGE: One of the events FORWARD_EDGE or CROSS_EDGE + occurred, but it has not been determined which of these events. + + + Aliases for sets of events: + + - NONE = 0 + + - ENTERING = ENTERING_START | ENTERING_SUCCESSOR + - LEAVING = LEAVING_START | LEAVING_SUCCESSOR + + - IN_OUT_START = ENTERING_START | LEAVING_START + - IN_OUT_SUCCESSOR = ENTERING_SUCCESSOR | LEAVING_SUCCESSOR + - IN_OUT = IN_OUT_START | IN_OUT_SUCCESSOR + + - NON_TREE_EDGES = FORWARD_EDGE | BACK_EDGE | CROSS_EDGE + - EDGES = ENTERING_SUCCESSOR | NON_TREE_EDGES + + - ALL = IN_OUT | SKIPPING_START | NON_TREE_EDGES + """ + + ENTERING_START = auto() + LEAVING_START = auto() + + ENTERING_SUCCESSOR = auto() + LEAVING_SUCCESSOR = auto() + + SKIPPING_START = auto() + BACK_EDGE = auto() + FORWARD_EDGE = auto() + CROSS_EDGE = auto() + + SOME_NON_TREE_EDGE = auto() + FORWARD_OR_CROSS_EDGE = auto() + + NONE = 0 + + ENTERING = ENTERING_START | ENTERING_SUCCESSOR + LEAVING = LEAVING_START | LEAVING_SUCCESSOR + + IN_OUT_START = ENTERING_START | LEAVING_START + IN_OUT_SUCCESSOR = ENTERING_SUCCESSOR | LEAVING_SUCCESSOR + IN_OUT = IN_OUT_START | IN_OUT_SUCCESSOR + + NON_TREE_EDGES = FORWARD_EDGE | BACK_EDGE | CROSS_EDGE + EDGES = ENTERING_SUCCESSOR | NON_TREE_EDGES + + ALL = IN_OUT | SKIPPING_START | NON_TREE_EDGES + + +class DFSMode(Flag): + """ + An enumeration of the traversing mode to be used by TraversalDepthFirst. + + The modes are: + + - DFS_TREE: The traversal follows the edges of the DFS tree. If demanded, + non-tree edges are reported, but not followed. Vertices are only + visited once. + + - ALL_PATHS: A simple path is a path that does not contain a vertex twice. + In this mode, the traversal follows all edges, also edges leading to + vertices that have already been visited. But edges to vertices, that are + already on the trace (current path from a start vertex to the current + vertex) are ignored. For example, this can be used to search in the set + of all possible simple paths from some edges to some others. + + - ALL_WALKS: A walk is a sequence of nodes in which each adjacent pair of + nodes in the sequence is adjacent in the graph. + A walk can contain the same vertex or edge more than once. + In this more, the traversal follows all edges, also edges leading to + vertices that have already been followed as part of the trace (the + current walk from a start vertex to the current vertex). + """ + + DFS_TREE = auto() + ALL_PATHS = auto() + ALL_WALKS = auto() + + +class TraversalDepthFirstFlex( + _TraversalWithoutWeightsWithVisited[T_vertex, T_vertex_id, T_labels] +): + """ + Bases: `Traversal` [`T_vertex`, `T_vertex_id`, `T_labels`] + + :param vertex_to_id: See `VertexToID` function. + + :param gear: See `gears API ` and class `GearWithoutDistances`. + + :param next_vertices: See `NextVertices` function. If None, provide next_edges + or next_labeled_edges. + + :param next_edges: See `NextEdges` function. Only allowed if next_vertices equals + None. If both are None, provide next_labeled_edges. + + :param next_labeled_edges: See `NextLabeledEdges` function. Only allowed if + next_vertices and next_edges equal None. If given, paths will record the given + labels. + + :param is_tree: bool: If it is certain, that during each traversal run, + each vertex can be reached only once, is_tree can be set to True. This + improves performance, but attribute *visited* of the traversal will not be + updated during and after the traversal. + + **Algorithm:** Depth First Search ("DFS"), non-recursive implementation. + By default, a vertex is reported when its expansion starts (its + successors are about to be read from the graph). + + **Properties**: + Visits and expands unvisited vertices in depth first order, i.e., + the graphs is explored as far as possible along each branch before + backtracking. + Starts at some unvisited start vertex, and after an exhaustive traversal + from there, continues with another start vertex that has not been visited + so far. + + By default, it reports a vertex when it visits it. + + **Input:** Directed graph. Unlabeled or labeled edges. One or more start + vertices. Optional calculation limit. + + **Search state:** When a vertex is *expanded* + (traversal calls next_vertices, next_edges or next_labeled_edges) + or *reported* (an iterator of the traversal returns it), + the traversal provides updated values for the attributes + *depth*, *paths*, *visited*, *event*, *trace*, *trace_labels*, + *on_trace*, and *index*. + """ + + def __init__( + self, + vertex_to_id: VertexToID[T_vertex, T_vertex_id], + gear: GearWithoutDistances[T_vertex, T_vertex_id, T_labels], + next_vertices: Optional[ + NextVertices[ + T_vertex, TraversalDepthFirstFlex[T_vertex, T_vertex_id, T_labels] + ] + ] = None, + *, + next_edges: Optional[ + NextEdges[ + T_vertex, TraversalDepthFirstFlex[T_vertex, T_vertex_id, T_labels] + ] + ] = None, + next_labeled_edges: Optional[ + NextLabeledEdges[ + T_vertex, + TraversalDepthFirstFlex[T_vertex, T_vertex_id, T_labels], + T_labels, + ] + ] = None, + is_tree: bool = False, + ) -> None: + ( + self._next_edge_or_vertex, + edges_with_data, + labeled_edges, + ) = _create_unified_next(next_vertices, next_edges, next_labeled_edges) + super().__init__(edges_with_data, labeled_edges, is_tree, vertex_to_id, gear) + self.depth: int = -1 # value not used, initialized during traversal + """ + If depth computation has been demanded (see option *compute_depth*): + At this *search depth*, the reported (resp. the expanded) vertex has been + found. It equals the length (number of edges) of the created path to the + vertex, if path creation is demanded. + Note: The search depth does not need to be the depth of the vertex + (see `TraversalBreadthFirstFlex`). + When a traversal has been started, but no vertex has been reported or expanded + so far, the depth is 0 (depth of the start vertices). + """ + # The following values are not used. They are initialized during start_from. + self._report = DFSEvent.NONE + self._mode = DFSMode.DFS_TREE + self._compute_depth = False + self._compute_trace = False + self._compute_on_trace = False + self._compute_index = False + + self.event = DFSEvent.NONE + """ Event that happened when a vertex is reported """ + self.trace = self._gear.sequence_of_vertices([]) + """ Sequence of the vertices on the current path from a start vertex to the + current vertex. See option *compute_trace*. + When a back edge, cross edge, or a forward edge is reported, the edge is + temporarily appended to the trace to make it visible there, + although such an edge is ignored otherwise (e.g., the traversal does not + follow the edge, traversal.depth is not updated, and the vertex it leads + to is not taken to *on_trace*).""" + self.trace_labels = self._gear.sequence_of_edge_labels([]) + """ Sequence of the edge attributes of the edges on the current path + (the first edge goes from a start vertex to a successor). + See attribute *trace* and option *compute_trace*.""" + self.on_trace = self._gear.vertex_id_set([]) + """ Set of the vertices on the current path from a start vertex to the + current vertex. See option *compute_on_trace*. When a cross edge or a forward + edge is reported, the vertex it leads to will not be added to *on_trace*, + unlike *trace* (see there). """ + self.index = self._gear.vertex_id_to_number_mapping([]) + """ Mapping that numbers vertices in pre-order, i.e., the vertex gets its + number when it is entered. The vertices are numbered starting with *1*. + See option *compute_index*. + """ + + def start_from( + self, + start_vertex: Optional[T_vertex] = None, + *, + start_vertices: Optional[Iterable[T_vertex]] = None, + build_paths: bool = False, + calculation_limit: Optional[int] = None, + already_visited: Optional[VertexIdSet[T_vertex_id]] = None, + report: DFSEvent = DFSEvent.ENTERING_SUCCESSOR, + mode: DFSMode = DFSMode.DFS_TREE, + compute_depth: bool = False, + compute_trace: bool = False, + compute_on_trace: bool = False, + compute_index: bool = False, + ) -> TraversalDepthFirstFlex[T_vertex, T_vertex_id, T_labels]: + """ + Start the traversal at a vertex or a set of vertices and set parameters. + + :param start_vertex: The vertex the search should start at. If None, provide + start_vertices. + + :param start_vertices: The vertices the search should start at. Only + allowed if start_vertex equals None. + + :param build_paths: If true, build paths from some start vertex to each visited + vertex. Paths of start vertices are empty paths. + + :param calculation_limit: If provided, maximal number of vertices to process + (read in) from your graph. If it is exceeded, a RuntimeError will be raised. + + :param already_visited: If provided, this set is used instead of an internal + one to keep vertices (resp. their hashable ids from vertex_to_id), + that have already been visited. This parameter can be used to get online + access to the internal bookkeeping of visited vertices, or to preload + vertices that should never be visited, or to provide your own way for + storing the information that a vertex has already been visited. + + :param report: See `DFSEvent`. + When one of the chosen events occurs, the vertex is reported. + + The group-events cannot be combined with the events contained in the + group (see `DFSEvent`). + + If events other than ENTERING_SUCCESSOR and ENTERING_START + are required, option *compute_trace* (see below) will automatically be + used. + + If group-event FORWARD_OR_CROSS_EDGE is required, and the graph + is no tree (is_tree == False), option *compute_on_trace* (see below) + will automatically be set. + + If events from NON_TREE_EDGES are required, and the graph + is no tree (is_tree == False), the options + *compute_on_trace* and *compute_index* (see below) + will automatically be set. + + :param mode: + See `DFSMode`. The mode the search operates in. + + Mode ALL_PATHS cannot be combined with the reporting of events + FORWARD_EDGE and CROSS_EDGE, and event-groups containing them, + since these events are only defined for DFS-trees. + In mode ALL_PATHS, option *compute_on_trace* (see below) + will automatically be set. + + Mode ALL_WALKS cannot be + combined with reporting non-tree edges, neither + alone (events from NON_TREE_EDGES) + nor in group-events + (events SOME_NON_TREE_EDGE or FORWARD_OR_CROSS_EDGE), + since forward and cross edges are only defined for DFS-trees, + and back edges only for DFS-trees and for paths. + The mode cannot be used for trees + (parameter *is_tree* when creating the traversal), + and `visited` is not computed. + + :param compute_depth: For each reported vertex, provide the search depth is has + been found at (Note: Often, this information is not helpful, and the + computation increases memory consumption and runtime). + + :param compute_index: If True, the attribute *index* is updated + during the traversal, and option *compute_trace* + (see below) will automatically be used. *compute_index* is not + compatible with parameter *already_visited*. + + :param compute_on_trace: If True, attribute `on_trace` is updated + during the traversal, and option *compute_trace* will automatically + be set. The computation of set `on_trace` cannot be combined + with mode ALL_WALKS. + + :param compute_trace: If True, attribute `trace` is updated during the + traversal. + :return: Traversal, that has been started, e.g., statements like *iter()*, + *next()*, *for* and the methods "go*" of the Traversal can now be used. + + .. versionchanged:: 3.4 + + Start vertices are evaluated successively. + Parameters *report*, *mode*, *compute_trace*, *compute_on_trace*, + and *compute_index* added. + """ + _start_from_needs_traversal_object(self) + self._start_without_weights_with_visited( + start_vertex, + start_vertices, + build_paths, + calculation_limit, + already_visited, + empty_path_for_start_vertices=False, + visited_for_start_vertices=False, + ) + # Set the externally visible depth to the sensible initial value 0. + # But if depth is not to be computed, use value -1 instead. + self.depth = 0 if compute_depth else -1 + + # Derive further options (and possible incompatibilities) from given options + if DFSEvent.SOME_NON_TREE_EDGE in report and (DFSEvent.NON_TREE_EDGES & report): + raise RuntimeError( + "Reporting of non-tree edges as a group and as individual edge " + "type cannot be combined." + ) + if DFSEvent.FORWARD_OR_CROSS_EDGE in report and ( + (DFSEvent.FORWARD_EDGE | DFSEvent.CROSS_EDGE) & report + ): + raise RuntimeError( + "Reporting of forward or cross edges as a group and as individual edge " + "type cannot be combined." + ) + if not self._is_tree: + # In a tree, there cannot be non-tree edges. So, we do not need + # bookkeeping to detect such edges. + # If we are not in a tree, we need some special bookkeeping. + if ( + report & (DFSEvent.NON_TREE_EDGES | DFSEvent.FORWARD_OR_CROSS_EDGE) + or mode == DFSMode.ALL_PATHS + ): + # For reporting the concrete type of non-tree edges, or + # FORWARD_OR_CROSS_EDGE, we need the *on_trace* set. + # In case of back edges, we need it to detect them. + # In case of forward and cross edges or the group of either a forward + # or a cross edge, we need it to exclude the case of a back edge. + compute_on_trace = True + if report & (DFSEvent.FORWARD_EDGE | DFSEvent.CROSS_EDGE): + # Here, we need a vertex index to distinguish between the + # tho cases. + compute_index = True + if ( + report not in (DFSEvent.ENTERING_SUCCESSOR | DFSEvent.ENTERING_START) + or compute_index + or compute_on_trace + ): + # Only the algorithms that computes a trace can report other + # events than entering of normal or start vertices and can compute + # vertex indexes and the on_trace set + compute_trace = True + + # Prevent illegal option combinations + if report & ( + DFSEvent.NON_TREE_EDGES + | DFSEvent.SOME_NON_TREE_EDGE + | DFSEvent.FORWARD_OR_CROSS_EDGE + ) and (self._is_tree or mode in DFSMode.ALL_WALKS): + raise RuntimeError( + "The events BACK_EDGE, FORWARD_EDGE, and CROSS_EDGE, " + "and groups containing them, " + "cannot be computed for trees and for traversals in mode " + "*ALL_WALKS*." + ) + if compute_on_trace and (self._is_tree or mode == DFSMode.ALL_WALKS): + raise RuntimeError( + "Computation of the on-trace is not allowed for trees and for " + "traversals in mode *ALL_WALKS*." + ) + if ( + report + & ( + DFSEvent.FORWARD_EDGE + | DFSEvent.CROSS_EDGE + | DFSEvent.SOME_NON_TREE_EDGE + | DFSEvent.FORWARD_OR_CROSS_EDGE + ) + and mode == DFSMode.ALL_PATHS + ): + raise RuntimeError( + "The events FORWARD_EDGE and CROSS_EDGE, " + "and groups containing them, " + "cannot be computed for traversals in mode *ALL_PATHS*." + ) + if build_paths and mode == DFSMode.ALL_WALKS: + raise RuntimeError( + "Paths cannot be computed in mode *ALL_WALKS*, because " + "walks can be cyclic." + ) + if compute_index and already_visited is not None: + raise RuntimeError( + "Parameter *already_visited* not allowed when vertex indexes " + "are demanded." + ) + + self._report = report + self._mode = mode + self._compute_depth = compute_depth + self._compute_trace = compute_trace + self._compute_on_trace = compute_on_trace + self._compute_index = compute_index + self.event = DFSEvent.NONE + self.trace = self._gear.sequence_of_vertices([]) + self.on_trace = self._gear.vertex_id_set([]) + self.trace_labels = self._gear.sequence_of_edge_labels([]) + self.index = self._gear.vertex_id_to_number_mapping([]) + + super()._start() + return self + + def _traverse(self) -> Generator[T_vertex, None, Any]: + # We provide two different implementations: the first with many features, + # and the second optimized for speed and memory for a typical configuration + # of options. + if self._compute_trace or self._mode != DFSMode.DFS_TREE: + return self._traverse_with_trace() + else: + return self._traverse_without_trace() + + def _traverse_with_trace(self) -> Generator[T_vertex, None, Any]: + # ----- Prepare efficient environment for inner loop ----- + # Copy Traversal attributes into method scope (faster access) + labeled_edges = self._labeled_edges + maybe_vertex_to_id = ( + None if self._vertex_to_id == vertex_as_id else self._vertex_to_id + ) # Case vertex_as_id: not apply; T_vertex_id > T_vertex + build_paths = self._build_paths + calculation_limit = self._calculation_limit + predecessors = self._predecessors + attributes = self._attributes + + # Prepare limit check done by zero check + if calculation_limit is not None: + calculation_limit += 1 + + # Copy _TraversalWithoutWeightsWithVisited attributes into method scope + edges_with_data = self._edges_with_data + next_edge_or_vertex = self._next_edge_or_vertex + + # Get references of used gear objects and methods (avoid attribute resolution) + ( + _, + predecessors_sequence, + predecessors_wrapper, + ) = access_to_vertex_mapping_expect_none(predecessors) + ( + _, + attributes_sequence, + attributes_wrapper, + ) = access_to_vertex_mapping_expect_none(attributes) + + # Copy further traversal attributes into method scope (faster access) + is_tree = self._is_tree + visited = self.visited + + # Get further references of used gear objects and methods + # (avoid attribute resolution) + visited_add = visited.add + ( + visited_uses_sequence, + visited_sequence, + visited_wrapper, + visited_uses_bits, + visited_index_and_bit_method, + ) = access_to_vertex_set(visited) + + # Copy Traversal-specific attributes into method scope (faster access) + report = self._report + mode = self._mode + compute_depth = self._compute_depth + compute_on_trace = self._compute_on_trace + compute_index = self._compute_index + trace = self.trace + on_trace = self.on_trace + trace_labels = self.trace_labels + index = self.index + + # Create individual flags for events that are to be reported + # (Avoids attribute resolution both for checking if an event has + # to be reported and for the value that is then to report) + event_entering = DFSEvent.ENTERING_SUCCESSOR + event_entering_start = DFSEvent.ENTERING_START + report_leaving = DFSEvent.LEAVING_SUCCESSOR & report + report_leaving_start = DFSEvent.LEAVING_START & report + report_skipping_start = DFSEvent.SKIPPING_START & report + report_non_tree_edges = DFSEvent.NON_TREE_EDGES & report + report_forward_edge = DFSEvent.FORWARD_EDGE & report + report_back_edge = DFSEvent.BACK_EDGE & report + report_cross_edge = DFSEvent.CROSS_EDGE & report + report_some_non_tree_edge = DFSEvent.SOME_NON_TREE_EDGE & report + report_forward_or_cross_edge = DFSEvent.FORWARD_OR_CROSS_EDGE & report + report_none = DFSEvent.NONE + # The same for the mode + mode_dfs_tree = mode & DFSMode.DFS_TREE + # mode_simple_paths = mode & DFSMode.ALL_PATHS + mode_walks = mode & DFSMode.ALL_WALKS + + # ----- Initialize method specific bookkeeping ----- + + if compute_depth: + # Since a start vertex is the first that is put on the trace, we have to + # start one lower + self.depth -= 1 + + # Vertices to enter or leave + to_visit = self._gear.sequence_of_vertices([]) + to_visit_append = to_visit.append + to_visit_pop = to_visit.pop + # For non-start vertices in to_visit: edge attributes of edge to them + to_visit_labels = self._gear.sequence_of_edge_labels([]) + to_visit_labels_append = to_visit_labels.append + to_visit_labels_pop = to_visit_labels.pop + # Sequence of flag bytes (store in a Q array) marking the vertices in + # to_visit that we want to leave by 1 and the vertices to enter by 0. + # Initially, store a zero flag for each start vertex. + to_leave_markers = array.array("B") + to_leave_markers_append = to_leave_markers.append + + # Get references of the methods of the gear objects this traversal uses + # (avoid attribute resolution) + trace_append = trace.append + trace_pop = trace.pop + on_trace_add = on_trace.add + trace_labels_append = trace_labels.append + trace_labels_pop = trace_labels.pop + on_trace_add = on_trace.add + on_trace_discard = on_trace.discard + ( + on_trace_uses_sequence, + on_trace_sequence, + on_trace_wrapper, + on_trace_uses_bits, + on_trace_index_and_bit_method, + ) = access_to_vertex_set(on_trace) + # Check compatibility of visited and trace_set. It is used for + # performance optimization later on. + assert ( + visited_uses_sequence == on_trace_uses_sequence + and visited_uses_bits == on_trace_uses_bits + ), ("Collection visited is incompatible " + "with collection on_trace") + set_uses_sequence = visited_uses_sequence + del visited_uses_sequence, on_trace_uses_sequence + set_uses_bits = visited_uses_bits + del visited_uses_bits, on_trace_uses_bits + if set_uses_sequence and set_uses_bits: + assert visited_index_and_bit_method is on_trace_index_and_bit_method, ( + "Collection visited is incompatible " + "with collection on_trace" + ) + set_index_and_bit_method = visited_index_and_bit_method + del visited_index_and_bit_method, on_trace_index_and_bit_method + + # Start times of vertices (the collection implicitly default to 0) + _, index_sequence, index_wrapper = access_to_vertex_mapping(index) + + # We start the time at number 1, because vertices without time have time + # 0, and we want to distinguish the two cases. + time = 1 + + # ----- Typing preparation of inner loop ----- + + # The following type Any opens no space for typing problems, since + # the content of next_edge_or_vertex is type checked and iterates + # objects of type T_vertex for edges_with_data==False and otherwise of + # one of the following: + # WeightedUnlabeledOutEdge[T_vertex, Any], + # UnweightedLabeledOutEdge[T_vertex, T_labels], + # WeightedLabeledOutEdge[T_vertex, Any, T_labels], + # And if labeled_edges==True, the first case is excluded. + # (Any alternative code version of the inner loop without + # Any or 'type: ignore' is slower) + edge_or_vertex: Any # "Hole" in typing, but types "around" make it safe + neighbor: T_vertex # Re-establish type "after" the "hole" + data_of_edge: T_labels # Re-establish type "after" the "hole" + labels: T_labels + + for start_vertex in self._start_vertices: + # A start vertex needs to be visited and entered, and it has an empty path + to_visit_append(start_vertex) + to_leave_markers_append(False) + + # ----- Inner loop ----- + + while True: + # Leave vertices in the trace (stack) that we already fully processed + # (backtracking along edges) and find the vertex to enter next. + # If there is none, we are finished with processing the current + # start vertex. + + no_leaving_reported_so_far = True + # (noinspection necessary due to bug PY-9479, also below...) + # noinspection PyUnboundLocalVariable + while to_leave_markers: + marker = to_leave_markers.pop() + if not marker: + # Not a leave marker, but an enter marker. Break loop + # of handling leave markers. + break + # We got marker "leave a vertex" and leave one vertex + vertex = trace[-1] + # Report leaving, if demanded, and update the trace + if to_leave_markers: + # We are leaving a vertex that is not the only one on the trace + # (Note: We cannot compare vertex != start_vertex here, because + # a start vertex can re-occur as target of a back edge, and + # then, the trace is not empty!) + if report_leaving: + if no_leaving_reported_so_far: + self.event = report_leaving + no_leaving_reported_so_far = False + yield vertex + if labeled_edges: + trace_labels_pop() + else: + # We are leaving the only vertex on the trace + if report_leaving_start: + self.event = report_leaving_start + yield vertex + # Pop vertex from trace + _ = trace_pop() + # Remove vertex from on_trace set + if compute_on_trace: + r_id: T_vertex_id = ( + maybe_vertex_to_id(vertex) # type: ignore[assignment] + if maybe_vertex_to_id + else vertex + ) + # (If-nesting optimized for first case) + if not set_uses_sequence: + # Standard implementation for "normal" MutableMapping + on_trace_discard(r_id) + elif set_uses_bits: + # Same as above, but with bits in byte sequence + sequence_key, bit_number = set_index_and_bit_method(r_id, 8) + on_trace_sequence[sequence_key] -= 1 << bit_number + else: + # Same as above, but with booleans in byte sequence + on_trace_sequence[r_id] = False + # Update depth + if compute_depth: + self.depth -= 1 + else: + # Last marker has been read. We are done with the start_vertex and + # all its successors. Break loop of handling markers and vertices + # to visit. + break + + # Enter marker found. Follow an edge in its forward direction. + # The last added vertex is the first to (possibly) enter (depth-first) + # print(">>", to_visit, to_visit_labels) + vertex = to_visit_pop() + if labeled_edges and to_visit: + # Further vertices to visit means vertex is here not in role of + # a start vertex. Thus, we followed an edge. Thus, if edges + # are labeled, we have a label. And get it. + # (Note: We cannot compare vertex != start_vertex here, because + # a start vertex can re-occur as target of a back edge, and + # then, we need to get the labels!) + labels = to_visit_labels_pop() + # print(">>>", vertex, labels) + v_id: T_vertex_id = ( + maybe_vertex_to_id(vertex) # type: ignore[assignment] + if maybe_vertex_to_id + else vertex + ) + + # If the graph is no tree: We might come to a vertex that we have + # already visited and need to check and handle this; and we have to + # update the visited set. + # In mode ALL_WALKS, we ignore all this + if not is_tree and mode != mode_walks: + + event = report_none + + # Find out if vertex has already been visited and entering + # it again is forbidden. + # In mode DFS_TREE, "visited" means here that the vertex is in + # set visited (because it has already been visited as descendant + # of another vertex, or because it is given as start vertex twice), + # and in mode ALL_PATHS that it already is on_trace. + re_visit = False + if mode == mode_dfs_tree: + if not set_uses_sequence: + # Standard implementation for "normal" MutableSet + if v_id in visited: + re_visit = True + else: + visited_add(v_id) + + elif not set_uses_bits: + # Same as above, but with booleans in byte sequence + try: + if visited_sequence[v_id]: + re_visit = True + else: + visited_sequence[v_id] = True + except IndexError: + visited_wrapper.extend_and_set(v_id, True) + + else: + # Same as above, but with bits in byte sequence + sequence_key, bit_number = set_index_and_bit_method(v_id, 8) + bit_mask = 1 << bit_number + try: + value = visited_sequence[sequence_key] + if value & bit_mask: + re_visit = True + else: + visited_sequence[sequence_key] = value | bit_mask + except IndexError: + visited_wrapper.extend_and_set(sequence_key, bit_mask) + if re_visit and not trace: + # We try to enter a start vertex when the trace is + # empty, but is has already been visited (can only + # happen in mode DFS_TREE.) + if report_skipping_start: + self.event = report_skipping_start + trace_append(vertex) + yield vertex + trace_pop() + continue + + else: # mode ALL_PATHS, since ALL_WALKS has been excluded above + if not set_uses_sequence: + # Standard implementation for "normal" MutableSet + if v_id in on_trace: + re_visit = True + event = report_back_edge + else: + on_trace_add(v_id) + + elif not set_uses_bits: + # Same as above, but with booleans in byte sequence + try: + if on_trace_sequence[v_id]: + re_visit = True + event = report_back_edge + else: + on_trace_sequence[v_id] = True + except IndexError: + on_trace_wrapper.extend_and_set(v_id, True) + + else: + # Same as above, but with bits in byte sequence + sequence_key, bit_number = set_index_and_bit_method(v_id, 8) + bit_mask = 1 << bit_number + try: + value = on_trace_sequence[sequence_key] + if value & bit_mask: + re_visit = True + event = report_back_edge + else: + on_trace_sequence[sequence_key] = value | bit_mask + except IndexError: + on_trace_wrapper.extend_and_set(sequence_key, bit_mask) + if re_visit: + # Report re-visit of non-start vertex, and ignore the + # vertex (continue). + + if report_some_non_tree_edge: + # We need to report all kinds of non-tree edges as a group + event = report_some_non_tree_edge + + elif ( + report_non_tree_edges + | report_forward_or_cross_edge + | report_back_edge + | report_forward_edge + | report_cross_edge + ): + # We need to report a more concrete type that just + # some_non_tree_edge + + # Detect back edge, if not already done + if report_back_edge and event != report_back_edge: + if not set_uses_sequence: + # Standard implementation for "normal" MutableSet + if v_id in on_trace: + event = report_back_edge + + elif not set_uses_bits: + # Same as above, but with booleans in byte sequence + try: + if on_trace_sequence[v_id]: + event = report_back_edge + except IndexError: # pragma: no cover + raise AssertionError( + "Internal error: IndexError " + "should never happen" + ) + + else: + # Same as above, but with bits in byte sequence + try: + if on_trace_sequence[sequence_key] & bit_mask: + event = report_back_edge + except IndexError: # pragma: no cover + raise AssertionError( + "Internal error: IndexError " + "should never happen" + ) + + # If we have no back edge, distinguish between forward and + # cross edge + if event == report_none and ( + report_forward_edge | report_cross_edge != report_none + ): + parent = trace[-1] + p_id: T_vertex_id = ( + maybe_vertex_to_id(parent) # type: ignore[assignment] + if maybe_vertex_to_id + else parent + ) + vertex_index = index_sequence[v_id] + parent_index = index_sequence[p_id] + event = ( + report_forward_edge + if parent_index < vertex_index + else report_cross_edge + ) + else: + continue + + if event and event in report: + # Report the edge. Append it temporarily to the end of + # the trace and remove it again. + self.event = event + trace_append(vertex) + if labeled_edges: + trace_labels_append(labels) + yield vertex + trace_pop() + if labeled_edges: + trace_labels_pop() + + continue + + # The vertex has not been visited before, and we are still + # in case "not is_tree and mode != mode_walks" + + if compute_on_trace: + # Add to trace set, if not already done + if not set_uses_sequence: + # Standard implementation for "normal" MutableSet + on_trace_add(v_id) + + elif not set_uses_bits: + # Same as above, but with booleans in byte sequence + try: + on_trace_sequence[v_id] = True + except IndexError: + on_trace_wrapper.extend_and_set(v_id, True) + + else: + # Same as above, but with bits in byte sequence + try: + on_trace_sequence[sequence_key] |= bit_mask + except IndexError: + on_trace_wrapper.extend_and_set(sequence_key, bit_mask) + + # It is allowed to visit the vertex, so we visit it now. + # (The trace has already been maintained, if necessary.) + + if compute_index: + try: + index_sequence[v_id] = time + except IndexError: + index_wrapper.extend_and_set(v_id, time) + time += 1 + + if build_paths: + if trace: + # We are not visiting a start vertex. Store edge to it. + # Store the predecessor (trace[-1]) of the neighbor + try: + predecessors_sequence[v_id] = trace[-1] + except IndexError: + predecessors_wrapper.extend_and_set(v_id, trace[-1]) + # Store the labels of the edge to the neighbor + if labeled_edges: + data_of_edge = labels + try: + attributes_sequence[v_id] = data_of_edge + except IndexError: + attributes_wrapper.extend_and_set(v_id, data_of_edge) + else: + # We are visiting a start vertex. Store empty path for it. + vs_id: T_vertex_id = ( + maybe_vertex_to_id(start_vertex) # type: ignore[assignment] + if maybe_vertex_to_id + else start_vertex + ) + # Store the predecessor (start_vertex) of vs_id, if there + # is none so far. In this case, a MutableMapping raises a + # KeyError, and a Sequence contains None or raises an + # IndexError. + try: + if predecessors_sequence[vs_id] is None: + predecessors_sequence[vs_id] = start_vertex + except KeyError: + predecessors_sequence[vs_id] = start_vertex + except IndexError: + predecessors_wrapper.extend_and_set(vs_id, start_vertex) + + # Store the marker True: When it is reached later on, we will know + # that we have to leave the vertex again. + # noinspection PyUnboundLocalVariable + to_leave_markers_append(True) + + if compute_depth: + self.depth += 1 + + # self.event = event = event_entering if trace else event_entering_start + self.event = event = ( + event_entering_start if vertex == start_vertex else event_entering + ) + + if labeled_edges and trace: + trace_labels_append(labels) + trace_append(vertex) + + # Report that we enter a vertex (all state attributes have to be + # updated before) + if report & event: + try: + yield vertex + except StopIteration: + # We confirm the skip signal and skip the expansion + yield vertex + continue + + if calculation_limit and not ( + calculation_limit := calculation_limit - 1 + ): + raise RuntimeError("Number of visited vertices reached limit") + + for edge_or_vertex in next_edge_or_vertex(vertex, self): + neighbor = edge_or_vertex[0] if edges_with_data else edge_or_vertex + + # Needs to be visited, in stack order + to_visit_append(neighbor) + if labeled_edges: + # Proof for correctness of the type hole: + # self._labeled_edges -> next_edges (a NextWeightedEdges) is + # a NextWeightedLabeledEdges -> {edge_data_expr} is a T_labels + labels = edge_or_vertex[-1] # type: ignore[assignment] + to_visit_labels_append(labels) + + # Store marker False: when it is reached later on, we know + # that we have to enter the vertex now + to_leave_markers_append(False) + + def _traverse_without_trace(self) -> Generator[T_vertex, None, Any]: + """This implementation does not maintain the trace. + If a vertex is visited, there is no parent information, and if + a vertex is left, the vertex is now known. + Thus, it can only report ENTERING_SUCCESSOR and ENTERING_START, + cannot maintain the on-trace set, and cannot run in mode + ALL_PATHS. + Additionally, it does not offer vertex indices and mode ALL_WALKS, + but this could probably be added if necessary.""" + + # ----- Prepare efficient environment for inner loop ----- + # Copy Traversal attributes into method scope (faster access) + labeled_edges = self._labeled_edges + maybe_vertex_to_id = ( + None if self._vertex_to_id == vertex_as_id else self._vertex_to_id + ) # Case vertex_as_id: not apply; T_vertex_id > T_vertex + build_paths = self._build_paths + calculation_limit = self._calculation_limit + predecessors = self._predecessors + attributes = self._attributes + + # Prepare limit check done by zero check + if calculation_limit is not None: + calculation_limit += 1 + + # Copy _TraversalWithoutWeightsWithVisited attributes into method scope + edges_with_data = self._edges_with_data + next_edge_or_vertex = self._next_edge_or_vertex + + # Get references of used gear objects and methods (avoid attribute resolution) + ( + _, + predecessors_sequence, + predecessors_wrapper, + ) = access_to_vertex_mapping_expect_none(predecessors) + ( + _, + attributes_sequence, + attributes_wrapper, + ) = access_to_vertex_mapping_expect_none(attributes) + + # Copy further traversal attributes into method scope (faster access) + is_tree = self._is_tree + visited = self.visited + + # Get further references of used gear objects and methods + # (avoid attribute resolution) + visited_add = visited.add + ( + visited_uses_sequence, + visited_sequence, + visited_wrapper, + visited_uses_bits, + visited_index_and_bit_method, + ) = access_to_vertex_set(visited) + visited = self.visited + visited_add = visited.add + ( + visited_uses_sequence, + visited_sequence, + visited_wrapper, + visited_uses_bits, + visited_index_and_bit_method, + ) = access_to_vertex_set(visited) + + # Copy Traversal-specific attributes into method scope (faster access) + compute_depth = self._compute_depth + report = self._report + + # Copy Traversal-specific constants into method scope (faster access) + event_entering = DFSEvent.ENTERING_SUCCESSOR + event_entering_start = DFSEvent.ENTERING_START + + # ----- Initialize method specific bookkeeping ----- + + depth = -1 # The inner loop starts with incrementing, so, we pre-decrement + if not compute_depth: + self.depth = depth # In this case, we leave the -1 the whole time + + # vertices to enter or leave + to_visit = self._gear.sequence_of_vertices([]) + to_visit_append = to_visit.append + to_visit_pop = to_visit.pop + + if compute_depth: + # Sequence of flag bytes (store in a Q array) marking the vertices + # to leave by 1 and the vertices to enter by 0. + to_leave_markers = array.array("B") + to_leave_markers_pop = to_leave_markers.pop + to_leave_markers_append = to_leave_markers.append + + # ----- Typing preparation of inner loop ----- + + # The following type Any opens no space for typing problems, since + # the content of next_edge_or_vertex is type checked and iterates + # objects of type T_vertex for edges_with_data==False and otherwise of + # one of the following: + # WeightedUnlabeledOutEdge[T_vertex, Any], + # UnweightedLabeledOutEdge[T_vertex, T_labels], + # WeightedLabeledOutEdge[T_vertex, Any, T_labels], + # And if labeled_edges==True, the first case is excluded. + # (Any alternative code version of the inner loop without + # Any or 'type: ignore' is slower) + edge_or_vertex: Any # "Hole" in typing, but types "around" make it safe + neighbor: T_vertex # Re-establish type "after" the "hole" + data_of_edge: T_labels # Re-establish type "after" the "hole" + + for start_vertex in self._start_vertices: + to_visit_append(start_vertex) + if compute_depth: + to_leave_markers_append(False) + if build_paths: + # If the start vertex is not already visited, store empty path + # for it + sv_id: T_vertex_id = ( + maybe_vertex_to_id(start_vertex) # type: ignore[assignment] + if maybe_vertex_to_id + else start_vertex + ) + # Store the predecessor (start_vertex) of sv_id, if there + # is none so far. In this case, a MutableMapping raises a + # KeyError, and a Sequence contains None or raises an + # IndexError. + try: + if predecessors_sequence[sv_id] is None: + predecessors_sequence[sv_id] = start_vertex + except KeyError: + predecessors_sequence[sv_id] = start_vertex + except IndexError: + predecessors_wrapper.extend_and_set(sv_id, start_vertex) + + # ----- Inner loop ----- + while True: + if compute_depth: + # Update depth w.r.t. all vertices we are leaving before afterwards, + # we enter the next one. + # This also done for the start vertex, before exiting the + # inner loop, in order to reset the sequence to_leave_markers + # (Instead of this, we could use "del s[:]" above, but this + # requires the MutableSequence to support slice objects as key for + # __delitem__ - what is often given, but not guaranteed) + # (noinspection necessary due to bug PY-9479, also below...) + # noinspection PyUnboundLocalVariable + while to_leave_markers and to_leave_markers_pop(): + # We decrement the depth for each vertex we are "leaving". + depth -= 1 + + if not to_visit: + # No vertices to visit are left: The start vertex and its + # descendants are processed. We can leave the loop. + break + + # Next vertex to enter (except it is already visited) + vertex = to_visit_pop() + if not is_tree: + # Ignore vertex if already visited, and + # else include its ID in visited set. + v_id: T_vertex_id = ( + maybe_vertex_to_id(vertex) # type: ignore[assignment] + if maybe_vertex_to_id + else vertex + ) + # (If-nesting optimized for first case) + if not visited_uses_sequence: + # Standard implementation for "normal" MutableMapping + if v_id in visited: + continue + visited_add(v_id) + elif visited_uses_bits: + # Same as above, but with bits in byte sequence + sequence_key, bit_number = visited_index_and_bit_method(v_id, 8) + bit_mask = 1 << bit_number + try: + value = visited_sequence[sequence_key] + if value & bit_mask: + continue + visited_sequence[sequence_key] = value | bit_mask + except IndexError: + visited_wrapper.extend_and_set(sequence_key, bit_mask) + else: + # Same as above, but with booleans in byte sequence + try: + if visited_sequence[v_id]: + continue + visited_sequence[v_id] = True + except IndexError: + visited_wrapper.extend_and_set(v_id, True) + + # We will now enter the vertex + if compute_depth: + depth += 1 + self.depth = depth + # Store marker True: when reached, we are leaving a vertex + # noinspection PyUnboundLocalVariable + to_leave_markers_append(True) + + if vertex == start_vertex: + # In this variant (!) of DFS, below, only neighbors that are + # not visited so far are taken to to_visited. Since start_vertex + # is visited immediately in its role as start vertex, this means, + # it cannot occur again in the role of a neighbor. So, here, we + # know, that we are in the case event_entering_start. + # Set event, both for a possible report here and for + # expanding the vertex + self.event = event_entering_start + if report & event_entering_start: + try: + yield vertex + except StopIteration: + # We confirm the skip signal and skip the expansion. + # Since this also skips resetting the event type, + # we need to do it here before continuing. + yield vertex + self.event = event_entering + continue + + else: + if report & event_entering: + # The event has already been reset to report_entering + # (see below) + try: + yield vertex + except StopIteration: + # We confirm the skip signal and skip the expansion. + yield vertex + continue + + if calculation_limit and not ( + calculation_limit := calculation_limit - 1 + ): + raise RuntimeError("Number of visited vertices reached limit") + + for edge_or_vertex in next_edge_or_vertex(vertex, self): + neighbor = edge_or_vertex[0] if edges_with_data else edge_or_vertex + + if not is_tree or build_paths: + n_id: T_vertex_id = ( + maybe_vertex_to_id(neighbor) # type: ignore[assignment] + if maybe_vertex_to_id + else neighbor + ) + + # Ignore neighbor if already visited/expanded, i.e., not put + # it onto the stack to_visit. + # If we do not build paths, this is just a variant of the + # algorithms: The results do not change, since visited + # vertices will not be visited again. + # But in case that we create paths, it is necessary: We like + # to store the predecessor of the neighbor directly here, + # when we first see the neighbor and have the predecessor + # still present. And we need to make sure not to overwrite + # the real predecessor of a vertex that has been already + # expanded. If the neighbor has not been visited, it is + # safe (and necessary) to overwrite a pre-existing + # predecessor of neighbor because a later found edge + # (predecessor, neighbor) will be evaluated first (to_visit + # is a stack) and so, this predecessor need to "win". + # (If-nesting optimized for first case) + if not visited_uses_sequence: + # Standard implementation for "normal" MutableMapping: + if n_id in visited: + continue + elif visited_uses_bits: + # Same as above, but with bits in byte sequence + sequence_key, bit_number = visited_index_and_bit_method( + n_id, 8 + ) + try: + if visited_sequence[sequence_key] & (1 << bit_number): + continue + except IndexError: + pass + else: + # Same as above, but with booleans in byte sequence + try: + if visited_sequence[n_id]: + continue + except IndexError: + pass + + if build_paths: + # Store the predecessor (vertex) of the neighbor + try: + predecessors_sequence[n_id] = vertex + except IndexError: + predecessors_wrapper.extend_and_set(n_id, vertex) + # Store the labels of the edge to the neighbor + if labeled_edges: + data_of_edge = edge_or_vertex[-1] + try: + attributes_sequence[n_id] = data_of_edge + except IndexError: + attributes_wrapper.extend_and_set( + n_id, data_of_edge + ) + + # Needs to be visited, in stack order + to_visit_append(neighbor) + + if compute_depth: + # Store marker False: when reached, we are entering a vertex + to_leave_markers_append(False) + + if vertex == start_vertex: + # We directly reset the event to report_entering after having + # processed the start vertex, because this is less expensive + # than doing it every time re report a non-start vertex. + # And we do it only if necessary, because the *if* is less + # expensive than the attribute access. + self.event = event_entering + + # After the traversal, set depth to something that the user can understand, + # here: -1 is the value given if no depth computation is demanded, -1 is the + # initial value before the first vertex has been entered, and -1 + # is the depth after having backtracked from a start vertex (that is at + # depth 0). The documentation does not specify a value here. + self.depth = -1 + + def _improve_state( + self, state: dict[str, Any], vertices: Optional[Iterable[T_vertex]] = None + ) -> None: + + # For events: replace repr() text by str() text, because state as optimized + # for readability + if state["event"]: + state["event"] = str(state["event"]) + + # If ony of these attributes is not computed, do not show it as part of the + # state + for attribute in ["event", "trace", "on_trace", "trace_labels"]: + if not state[attribute]: + del state[attribute] + + # Assignments in times are only valid for reported vertices. Thus, + # we need to convert only keys/values for requested vertices to a string, + # not the whole MutableMapping. So, we special case this attribute here. + vertex_to_id = self._vertex_to_id + for times_key, times_collection in [ + ("index", self.index), + ]: + del state[times_key] + # List content for the given vertices, but leave out key without values + # or default value 0. + # (Explicitly check "v_id in collection", because simply accessing + # always returns (and for defaultdict: also set) the default value) + if vertices is not None: + # content = [ + # (v_id, timestamp) + # for vertex in vertices + # if (timestamp := times_collection[v_id := vertex_to_id(vertex)]) + # ] + content = [ + (v_id, time) + for vertex in vertices + if (v_id := vertex_to_id(vertex)) in times_collection + and (time := times_collection[v_id]) != 0 + ] + if content: + state[times_key] = StrRepr.from_iterable(content) + + super()._improve_state(state, vertices) + + def __iter__( + self, + ) -> Generator[ + T_vertex, None, None + ]: # Type alias needed due to a sphinx limitation + """ + Like `nographs.Traversal.__iter__`, but return a generator + instead of an interator. + + If *StopIteration()* is thrown into the generator: + + - When a vertex has been entered (events + *DFSEvent.ENTERING_START* or *DFSEvent.ENTERING_SUCCESSOR* is reported), + do not expand the vertex and reported it again as a confirmation. + - In any other situation, raise a *RuntimeError* (according to *PEP 497*, + see https://peps.python.org/pep-0479). + + .. versionchanged:: 3.4 + + Now returns a generator instead of just an iterator, and + a thrown *StopIteration* is handled, see above. + """ + if not isinstance(type(self), type(Traversal)): + raise RuntimeError("Method go can only be called on a Traversal object.") + return self._generator + + def skip_expanding_entered_vertex(self) -> None: + """If called when a vertex has been entered (events + *DFSEvent.ENTERING_START* or *DFSEvent.ENTERING_SUCCESSOR*), + skip the expansion of this vertex. + + If called when another event happened, raise a *RuntimeError*. + + (The method simply throws a *StopIteration* at *traversal.__iter__()*.) + + .. versionchanged:: 3.4 + + Method added. + """ + self._generator.throw(StopIteration()) + + +class TraversalDepthFirst( + Generic[T_vertex, T_labels], TraversalDepthFirstFlex[T_vertex, T_vertex, T_labels] +): + """ + Eases the use of `TraversalDepthFirstFlex` for typical cases. + For documentation of functionality and parameters, see there. + + Uses the following standard arguments for the respective parameters of + the parent class: + + - vertex_to_id = `vertex_as_id` + - gear = `GearDefault` + - `T_vertex_id` = `T_vertex` + + Implications: + + - `GearDefault` is used, see there how it and its superclass work + - T_vertex is bound to Hashable (T_vertex is used as `T_vertex_id`, see there) + """ + + def __init__( + self, + next_vertices: Optional[ + NextVertices[ + T_vertex, TraversalDepthFirstFlex[T_vertex, T_vertex, T_labels] + ] + ] = None, + *, + next_edges: Optional[ + NextEdges[T_vertex, TraversalDepthFirstFlex[T_vertex, T_vertex, T_labels]] + ] = None, + next_labeled_edges: Optional[ + NextLabeledEdges[ + T_vertex, + TraversalDepthFirstFlex[T_vertex, T_vertex, T_labels], + T_labels, + ] + ] = None, + is_tree: bool = False, + ) -> None: + super().__init__( + vertex_as_id, + GearDefault(), + next_vertices, + next_edges=next_edges, + next_labeled_edges=next_labeled_edges, + is_tree=is_tree, + ) diff --git a/src/nographs/_strategies/traversals/without_weights/neighbors_then_depth.py b/src/nographs/_strategies/traversals/without_weights/neighbors_then_depth.py new file mode 100644 index 0000000..dffdbee --- /dev/null +++ b/src/nographs/_strategies/traversals/without_weights/neighbors_then_depth.py @@ -0,0 +1,404 @@ +from __future__ import annotations + +import array +import copy +import itertools +from typing import Optional, Any, Generic +from collections.abc import Iterable, Generator + +from nographs._types import ( + T_vertex, + T_vertex_id, + T_labels, + VertexToID, + vertex_as_id, +) +from nographs._gears import ( + GearDefault, + GearWithoutDistances, + VertexIdSet, +) +from nographs._gear_collections import ( + access_to_vertex_set, + access_to_vertex_mapping_expect_none, +) +from ...type_aliases import ( + NextVertices, + NextEdges, + NextLabeledEdges, +) +from ..traversal import ( + _start_from_needs_traversal_object, +) +from .traversal_without_weights import ( + _create_unified_next, + _TraversalWithoutWeightsWithVisited, +) + + +class TraversalNeighborsThenDepthFlex( + _TraversalWithoutWeightsWithVisited[T_vertex, T_vertex_id, T_labels] +): + """ + Bases: `Traversal` [`T_vertex`, `T_vertex_id`, `T_labels`] + + :param vertex_to_id: See `VertexToID` function. + + :param gear: See `gears API ` and class `GearWithoutDistances`. + + :param next_vertices: See `NextVertices` function. If None, provide next_edges + or next_labeled_edges. + + :param next_edges: See `NextEdges` function. Only allowed if next_vertices equals + None. If both are None, provide next_labeled_edges. + + :param next_labeled_edges: See `NextLabeledEdges` function. Only allowed if + next_vertices and next_edges equal None. If given, paths will record the given + labels. + + :param is_tree: bool: If it is certain, that during each traversal run, + each vertex can be reached only once, is_tree can be set to True. This + improves performance, but attribute *visited* of the traversal will not be + updated during and after the traversal. + + **Algorithm:** Variant of the Depth First Search ("DFS"), + non-recursive implementation. + Vertices are reported when they are "seen" (read from the graph) for the + first time - thus not in DFS order! + + **Properties:** + The graphs is explored as far as possible along each branch before + backtracking, but in contrast to a Depth First Search, the algorithm + first reports all successors of the current vertex and then goes deeper. + A vertex is considered visited when it has been reported or if it is a + start vertex. + + **Input:** Directed graph. Unlabeled or labeled edges. One or more start + vertices. Optional calculation limit. + + **Search state:** When a vertex is *expanded* + (traversal calls next_vertices, next_edges or next_labeled_edges) + or *reported* (an iterator of the traversal returns it), + the traversal provides updated values for the attributes + *depth*, *paths*, and *visited*. + """ + + def __init__( + self, + vertex_to_id: VertexToID[T_vertex, T_vertex_id], + gear: GearWithoutDistances[T_vertex, T_vertex_id, T_labels], + next_vertices: Optional[ + NextVertices[ + T_vertex, + TraversalNeighborsThenDepthFlex[T_vertex, T_vertex_id, T_labels], + ] + ] = None, + *, + next_edges: Optional[ + NextEdges[ + T_vertex, + TraversalNeighborsThenDepthFlex[T_vertex, T_vertex_id, T_labels], + ] + ] = None, + next_labeled_edges: Optional[ + NextLabeledEdges[ + T_vertex, + TraversalNeighborsThenDepthFlex[T_vertex, T_vertex_id, T_labels], + T_labels, + ] + ] = None, + is_tree: bool = False, + ) -> None: + ( + self._next_edge_or_vertex, + edges_with_data, + labeled_edges, + ) = _create_unified_next(next_vertices, next_edges, next_labeled_edges) + super().__init__(edges_with_data, labeled_edges, is_tree, vertex_to_id, gear) + self.depth: int = -1 # value not used, initialized during traversal + """ + If depth computation has been demanded (see option *compute_depth*): + At this *search depth*, the reported (resp. the expanded) vertex has been + found. It equals the length (number of edges) of the created path to the + vertex, if path creation is demanded. + Note: The search depth does not need to be the depth of the vertex + (see `TraversalBreadthFirstFlex`). + When a traversal has been started, but no vertex has been reported or expanded + so far, the depth is 0 (depth of the start vertices). + """ + self._compute_depth = False # value not used, initialized during traversal + + def start_from( + self, + start_vertex: Optional[T_vertex] = None, + *, + start_vertices: Optional[Iterable[T_vertex]] = None, + build_paths: bool = False, + calculation_limit: Optional[int] = None, + already_visited: Optional[VertexIdSet[T_vertex_id]] = None, + compute_depth: bool = False, + ) -> TraversalNeighborsThenDepthFlex[T_vertex, T_vertex_id, T_labels]: + """ + Start the traversal at a vertex or a set of vertices and set parameters. + + :param start_vertex: The vertex the search should start at. If None, provide + start_vertices. + + :param start_vertices: The vertices the search should start at. Only + allowed if start_vertex equals None. + + :param build_paths: If true, build paths from some start vertex to each visited + vertex. Paths of start vertices are empty paths. + + :param calculation_limit: If provided, maximal number of vertices to process + (read in) from your graph. If it is exceeded, a RuntimeError will be raised. + + :param already_visited: If provided, this set is used instead of an internal + one to keep vertices (resp. their hashable ids from vertex_to_id), + that have already been visited. This parameter can be used to get online + access to the internal bookkeeping of visited vertices, or to preload + vertices that should never be visited, or to provide your own way for + storing the information that a vertex has already been visited. + + :param compute_depth: For each reported vertex, provide the search depth is has + been found at (Note: Often, this information is not helpful, and the + computation increases memory consumption and runtime). + + :return: Traversal, that has been started, e.g., statements like *iter()*, + *next()*, *for* and the methods "go*" of the Traversal can now be used. + """ + _start_from_needs_traversal_object(self) + self._start_without_weights_with_visited( + start_vertex, + start_vertices, + build_paths, + calculation_limit, + already_visited, + ) + self.depth = 0 + self._compute_depth = compute_depth + # Set the externally visible depth to the sensible initial value 0. + # But if depth is not to be computed, use value -1 instead. + self.depth = 0 if compute_depth else -1 + + super()._start() + return self + + def _traverse(self) -> Generator[T_vertex, None, Any]: + # ----- Prepare efficient environment for inner loop ----- + # Copy Traversal attributes into method scope (faster access) + labeled_edges = self._labeled_edges + maybe_vertex_to_id = ( + None if self._vertex_to_id == vertex_as_id else self._vertex_to_id + ) # Case vertex_as_id: not apply; T_vertex_id > T_vertex + build_paths = self._build_paths + calculation_limit = self._calculation_limit + predecessors = self._predecessors + attributes = self._attributes + + # Prepare limit check done by zero check + if calculation_limit is not None: + calculation_limit += 1 + + # Copy _TraversalWithoutWeightsWithVisited attributes into method scope + edges_with_data = self._edges_with_data + next_edge_or_vertex = self._next_edge_or_vertex + + # Get references of used gear objects and methods (avoid attribute resolution) + ( + _, + predecessors_sequence, + predecessors_wrapper, + ) = access_to_vertex_mapping_expect_none(predecessors) + ( + _, + attributes_sequence, + attributes_wrapper, + ) = access_to_vertex_mapping_expect_none(attributes) + + # Copy further traversal attributes into method scope (faster access) + is_tree = self._is_tree + visited = self.visited + + # Get further references of used gear objects and methods + # (avoid attribute resolution) + visited_add = visited.add + ( + visited_uses_sequence, + visited_sequence, + visited_wrapper, + visited_uses_bits, + visited_index_and_bit_method, + ) = access_to_vertex_set(visited) + + # Copy Traversal-specific attributes into method scope (faster access) + compute_depth = self._compute_depth + + # ----- Initialize method specific bookkeeping ----- + + depth = -1 # The inner loop starts with incrementing, so, we pre-decrement + if not compute_depth: + self.depth = depth # In this case, we leave the -1 the whole time + prev_traversal = copy.copy(self) # copy of self, for keeping previous depth + + # vertices to expand + to_expand = self._gear.sequence_of_vertices(self._start_vertices) + to_expand_append = to_expand.append + to_expand_pop = to_expand.pop + + if compute_depth: + # Sequence of flag bytes (store in a Q array) marking the vertices to leave + # by 1 and the vertices to enter (these are in to_expand) by 0. + # Initially, store a zero flag for each start vertex. + to_leave_marker = array.array("B", itertools.repeat(False, len(to_expand))) + to_leave_marker_pop = to_leave_marker.pop + to_leave_marker_append = to_leave_marker.append + + # ----- Typing preparation of inner loop ----- + + # The following type Any opens no space for typing problems, since + # the content of next_edge_or_vertex is type checked and iterates + # objects of type T_vertex for edges_with_data==False and otherwise of + # one of the following: + # WeightedUnlabeledOutEdge[T_vertex, Any], + # UnweightedLabeledOutEdge[T_vertex, T_labels], + # WeightedLabeledOutEdge[T_vertex, Any, T_labels], + # And if labeled_edges==True, the first case is excluded. + # (Any alternative code version of the inner loop without + # Any or 'type: ignore' is slower) + edge_or_vertex: Any # "Hole" in typing, but types "around" make it safe + neighbor: T_vertex # Re-establish type "after" the "hole" + data_of_edge: T_labels # Re-establish type "after" the "hole" + + # ----- Inner loop ----- + + while to_expand: + vertex = to_expand_pop() # Enter first added vertex first + if compute_depth: + depth += 1 + # noinspection PyUnboundLocalVariable + while to_leave_marker_pop(): + depth -= 1 # Got marker "leave a vertex", update depth + # Update external view on depth + prev_traversal.depth = depth + self.depth = depth + 1 + # Store marker True: when reached, we are leaving a vertex + # noinspection PyUnboundLocalVariable + to_leave_marker_append(True) + + if calculation_limit and not (calculation_limit := calculation_limit - 1): + raise RuntimeError("Number of visited vertices reached limit") + + for edge_or_vertex in next_edge_or_vertex(vertex, prev_traversal): + neighbor = edge_or_vertex[0] if edges_with_data else edge_or_vertex + + if not is_tree or build_paths: + n_id: T_vertex_id = ( + maybe_vertex_to_id(neighbor) # type: ignore[assignment] + if maybe_vertex_to_id + else neighbor + ) + + # If not is_tree: Ignore neighbor if already seen, and + # else include its ID in visited set. + # (If-nesting optimized for first case) + if not is_tree and not visited_uses_sequence: + # Standard implementation for "normal" MutableMapping + if n_id in visited: + continue + visited_add(n_id) + elif not is_tree and visited_uses_bits: + # Same as above, but with bits in byte sequence + sequence_key, bit_number = visited_index_and_bit_method(n_id, 8) + bit_mask = 1 << bit_number + try: + value = visited_sequence[sequence_key] + if value & bit_mask: + continue + visited_sequence[sequence_key] = value | bit_mask + except IndexError: + visited_wrapper.extend_and_set(sequence_key, bit_mask) + elif not is_tree: + # Same as above, but with booleans in byte sequence + try: + if visited_sequence[n_id]: + continue + visited_sequence[n_id] = True + except IndexError: + visited_wrapper.extend_and_set(n_id, True) + + if build_paths: + # Store the predecessor (vertex) of the neighbor + try: + predecessors_sequence[n_id] = vertex + except IndexError: + predecessors_wrapper.extend_and_set(n_id, vertex) + # Store the labels of the edge to the neighbor + if labeled_edges: + data_of_edge = edge_or_vertex[-1] + try: + attributes_sequence[n_id] = data_of_edge + except IndexError: + attributes_wrapper.extend_and_set(n_id, data_of_edge) + + yield neighbor + + # Needs to be expanded, in stack order + to_expand_append(neighbor) + + if compute_depth: + # Store marker False: when reached, we are entering a vertex + to_leave_marker_append(False) + + +class TraversalNeighborsThenDepth( + Generic[T_vertex, T_labels], + TraversalNeighborsThenDepthFlex[T_vertex, T_vertex, T_labels], +): + """ + Eases the use of `TraversalNeighborsThenDepthFlex` for typical cases. + For documentation of functionality and parameters, see there. + + Uses the following standard arguments for the respective parameters of + the parent class: + + - vertex_to_id = `vertex_as_id` + - gear = `GearDefault` + - `T_vertex_id` = `T_vertex` + + Implications: + + - `GearDefault` is used, see there how it and its superclass work + - T_vertex is bound to Hashable (T_vertex is used as `T_vertex_id`, see there) + """ + + def __init__( + self, + next_vertices: Optional[ + NextVertices[ + T_vertex, TraversalNeighborsThenDepthFlex[T_vertex, T_vertex, T_labels] + ] + ] = None, + *, + next_edges: Optional[ + NextEdges[ + T_vertex, TraversalNeighborsThenDepthFlex[T_vertex, T_vertex, T_labels] + ] + ] = None, + next_labeled_edges: Optional[ + NextLabeledEdges[ + T_vertex, + TraversalNeighborsThenDepthFlex[T_vertex, T_vertex, T_labels], + T_labels, + ] + ] = None, + is_tree: bool = False, + ) -> None: + super().__init__( + vertex_as_id, + GearDefault(), + next_vertices, + next_edges=next_edges, + next_labeled_edges=next_labeled_edges, + is_tree=is_tree, + ) diff --git a/src/nographs/_strategies/traversals/without_weights/topological_sort.py b/src/nographs/_strategies/traversals/without_weights/topological_sort.py new file mode 100644 index 0000000..3114c73 --- /dev/null +++ b/src/nographs/_strategies/traversals/without_weights/topological_sort.py @@ -0,0 +1,706 @@ +from __future__ import annotations + +import array +import itertools +from typing import Optional, Any, Generic +from collections.abc import Iterable, Generator + +from nographs._types import ( + T_vertex, + T_vertex_id, + T_labels, + VertexToID, + vertex_as_id, +) +from nographs._gears import ( + GearDefault, + GearWithoutDistances, + VertexIdSet, + MutableSequenceOfVertices, +) +from nographs._gear_collections import ( + access_to_vertex_set, + access_to_vertex_mapping_expect_none, +) +from ...type_aliases import ( + NextVertices, + NextEdges, + NextLabeledEdges, +) +from ..traversal import ( + _start_from_needs_traversal_object, +) +from .traversal_without_weights import ( + _create_unified_next, + _TraversalWithoutWeightsWithVisited, +) + + +class TraversalTopologicalSortFlex( + _TraversalWithoutWeightsWithVisited[T_vertex, T_vertex_id, T_labels] +): + """ + Bases: `Traversal` [`T_vertex`, `T_vertex_id`, `T_labels`] + + :param vertex_to_id: See `VertexToID` function. + + :param gear: See `gears API ` and class `GearWithoutDistances`. + + :param next_vertices: See `NextVertices` function. If None, provide next_edges + or next_labeled_edges. + + :param next_edges: See `NextEdges` function. Only allowed if next_vertices equals + None. If both are None, provide next_labeled_edges. + + :param next_labeled_edges: See `NextLabeledEdges` function. Only allowed if + next_vertices and next_edges equal None. If given, paths will record the given + labels. + + :param is_tree: bool: If it is certain, that during each traversal run, + each vertex can be reached only once, is_tree can be set to True. This + improves performance, but attribute *visited* of the traversal will not be + updated during and after the traversal. + + **Algorithm:** Topological Search, non-recursive implementation. + Vertices are reported when they "are left" for backtracking. + + **Properties:** Vertices are reported in topological ordering, i.e. a + linear ordering of the vertices such that for every directed edge *uv* + from vertex *u* to vertex *v* ("*u* depends on *v*"), *v* comes before + *u* in the ordering. If the graph contains a cycle that can be reached + within the sorting process, a RuntimeError exception is raised and a + cyclic path from a start vertex is provided. + + Vertices are expanded following the strategy + `nographs.TraversalDepthFirst`. + + A vertex is considered visited from the moment its expansion begins. + Start vertices are considered visited directly from the start of the + traversal. + + **Input:** Directed graph. Unlabeled or labeled edges. One or more start + vertices. Optional calculation limit. + + **Search state:** When a vertex is *expanded* + (traversal calls next_vertices, next_edges or next_labeled_edges) + or *reported* (an iterator of the traversal returns it), + the traversal provides updated values for the attributes + *depth*, *paths*, and *visited*. + """ + + def __init__( + self, + vertex_to_id: VertexToID[T_vertex, T_vertex_id], + gear: GearWithoutDistances[T_vertex, T_vertex_id, T_labels], + next_vertices: Optional[ + NextVertices[ + T_vertex, TraversalTopologicalSortFlex[T_vertex, T_vertex_id, T_labels] + ] + ] = None, + *, + next_edges: Optional[ + NextEdges[ + T_vertex, TraversalTopologicalSortFlex[T_vertex, T_vertex_id, T_labels] + ] + ] = None, + next_labeled_edges: Optional[ + NextLabeledEdges[ + T_vertex, + TraversalTopologicalSortFlex[T_vertex, T_vertex_id, T_labels], + T_labels, + ] + ] = None, + is_tree: bool = False, + ) -> None: + ( + self._next_edge_or_vertex, + edges_with_data, + labeled_edges, + ) = _create_unified_next(next_vertices, next_edges, next_labeled_edges) + super().__init__(edges_with_data, labeled_edges, is_tree, vertex_to_id, gear) + self.depth: int = -1 # value not used, initialized during traversal + """ + At this *search depth*, the reported (resp. the expanded) vertex has been + found. It equals the length (number of edges) of the created path to the + vertex, if path creation is demanded. + Note: The search depth does not need to be the depth of the vertex + (see `TraversalBreadthFirstFlex`). + When a traversal has been started, but no vertex has been reported or expanded + so far, the depth is 0 (depth of the start vertices). + """ + self.cycle_from_start: list[T_vertex] = [] # value not used, see above + """ If the graph contains a cycle that can be reached within the sorting + process, a RuntimeError exception is raised, and the traversal provides + a cyclic path from a start vertex in attribute cycle_from_start.""" + + def start_from( + self, + start_vertex: Optional[T_vertex] = None, + *, + start_vertices: Optional[Iterable[T_vertex]] = None, + build_paths: bool = False, + calculation_limit: Optional[int] = None, + already_visited: Optional[VertexIdSet[T_vertex_id]] = None, + ) -> TraversalTopologicalSortFlex[T_vertex, T_vertex_id, T_labels]: + """ + Start the traversal at a vertex or a set of vertices and set parameters. + + :param start_vertex: The vertex the search should start at. If None, provide + start_vertices. + + :param start_vertices: The vertices the search should start at. Only + allowed if start_vertex equals None. + + :param build_paths: If true, build paths from some start vertex to each visited + vertex. Paths of start vertices are empty paths. + + :param calculation_limit: If provided, maximal number of vertices to process + (read in) from your graph. If it is exceeded, a RuntimeError will be raised. + + :param already_visited: If provided, this set is used instead of an internal + one to keep vertices (resp. their hashable ids from vertex_to_id), + that have already been visited. This parameter can be used to get online + access to the internal bookkeeping of visited vertices, or to preload + vertices that should never be visited. + + Attention: TraversalTopologicalSortFlex requires, that the collection + given as argument for parameter already_visited is compatible + (in any sense) with the collection that gear.vertex_id_set() + returns. If you have chosen GearDefault, you can just use a dict. + Otherwise, create the collection by calling gear.vertex_id_set() or use the + collection that another traversal with the same gear gives as attribute + visited. + + :return: Traversal, that has been started, e.g., statements like *iter()*, + *next()*, *for* and the methods "go*" of the Traversal can now be used. + """ + + _start_from_needs_traversal_object(self) + self._start_without_weights_with_visited( + start_vertex, + start_vertices, + build_paths, + calculation_limit, + already_visited, + ) + self.depth = 0 + + super()._start() + return self + + def _traverse(self) -> Generator[T_vertex, None, Any]: + # Two separate implementations for the cases is_tree and not is_tree that follow + # different concepts, because a combined approach makes both cases significantly + # slower + if self._is_tree: + return self._traverse_in_tree() + else: + return self._traverse_in_non_tree() + + def _traverse_in_tree(self) -> Generator[T_vertex, None, Any]: + # ----- Prepare efficient environment for inner loop ----- + # Copy Traversal attributes into method scope (faster access) + labeled_edges = self._labeled_edges + maybe_vertex_to_id = ( + None if self._vertex_to_id == vertex_as_id else self._vertex_to_id + ) # Case vertex_as_id: not apply; T_vertex_id > T_vertex + build_paths = self._build_paths + calculation_limit = self._calculation_limit + predecessors = self._predecessors + attributes = self._attributes + + # Prepare limit check done by zero check + if calculation_limit is not None: + calculation_limit += 1 + + # Copy _TraversalWithoutWeightsWithVisited attributes into method scope + edges_with_data = self._edges_with_data + next_edge_or_vertex = self._next_edge_or_vertex + + # Get references of used gear objects and methods (avoid attribute resolution) + ( + _, + predecessors_sequence, + predecessors_wrapper, + ) = access_to_vertex_mapping_expect_none(predecessors) + ( + _, + attributes_sequence, + attributes_wrapper, + ) = access_to_vertex_mapping_expect_none(attributes) + + # ----- Typing preparation of inner loop ----- + + # The following type Any opens no space for typing problems, since + # the content of next_edge_or_vertex is type checked and iterates + # objects of type T_vertex for edges_with_data==False and otherwise of + # one of the following: + # WeightedUnlabeledOutEdge[T_vertex, Any], + # UnweightedLabeledOutEdge[T_vertex, T_labels], + # WeightedLabeledOutEdge[T_vertex, Any, T_labels], + # And if labeled_edges==True, the first case is excluded. + # (Any alternative code version of the inner loop without + # Any or 'type: ignore' is slower) + edge_or_vertex: Any # "Hole" in typing, but types "around" make it safe + neighbor: T_vertex # Re-establish type "after" the "hole" + data_of_edge: T_labels # Re-establish type "after" the "hole" + + # Since the graph is a tree, we need no cycle check and no skipping of + # already seen vertices. + # We just use a stack for vertices we have to enter or leave, and store + # None on top of the vertices we need to enter in order to differentiate + # the two cases. + + # ----- Initialize specific bookkeeping ----- + + self.cycle_from_start = [] + # Sequence used as stack of vertices that we need to enter & expand (if + # it is not on the trace, see below) or leave & report (otherwise) + to_expand_or_leave = self._gear.sequence_of_vertices(self._start_vertices) + + # Sequence of flag bytes (store in a Q array) marking the vertices to leave + # by 1 and the vertices to enter by 0. + # Initially, store a zero flag for each start vertex. + to_leave_markers = array.array( + "B", itertools.repeat(False, len(to_expand_or_leave)) + ) + + # Get method references of specific bookkeeping (avoid attribute resolution) + to_expand_or_leave_append = to_expand_or_leave.append + to_expand_or_leave_pop = to_expand_or_leave.pop + to_leave_markers_pop = to_leave_markers.pop + to_leave_markers_append = to_leave_markers.append + + # ----- Inner loop ----- + + while to_expand_or_leave: + if to_leave_markers[-1]: + # Vertex is to be left: We "leave" and report it, and remove marker + self.depth -= 1 + to_leave_markers_pop() + vertex = to_expand_or_leave_pop() # handle last added vertex first + yield vertex + continue + + # Vertex is to be expanded: We "expand" it, + # but leave it on the stack, so that it will be "left" later on. + vertex = to_expand_or_leave[-1] + # We change the marker in order to remember that we will have to + # leave (not enter) it + to_leave_markers[-1] = True + + if calculation_limit and not (calculation_limit := calculation_limit - 1): + raise RuntimeError("Number of visited vertices reached limit") + + for edge_or_vertex in next_edge_or_vertex(vertex, self): + neighbor = edge_or_vertex[0] if edges_with_data else edge_or_vertex + + n_id: T_vertex_id = ( + maybe_vertex_to_id(neighbor) # type: ignore[assignment] + if maybe_vertex_to_id + else neighbor + ) + + if build_paths: + # We have to store the predecessor here, because at time of + # visit, it is already lost. And we cannot yield here, + # because only the first of the neighbors will indeed be + # visited next. + # But since we are in a tree, no other predecessor can + # be stored for that vertex later on. + # Store the predecessor (vertex) of the neighbor + try: + predecessors_sequence[n_id] = vertex + except IndexError: + predecessors_wrapper.extend_and_set(n_id, vertex) + # Store the labels of the edge to the neighbor + if labeled_edges: + data_of_edge = edge_or_vertex[-1] + try: + attributes_sequence[n_id] = data_of_edge + except IndexError: + attributes_wrapper.extend_and_set(n_id, data_of_edge) + + # Put vertex on the stack + to_expand_or_leave_append(neighbor) + # Remember that we have to expand it + to_leave_markers_append(False) + + self.depth += 1 + + def _traverse_in_non_tree(self) -> Generator[T_vertex, None, Any]: + # ----- Prepare efficient environment for inner loop ----- + # Copy Traversal attributes into method scope (faster access) + labeled_edges = self._labeled_edges + maybe_vertex_to_id = ( + None if self._vertex_to_id == vertex_as_id else self._vertex_to_id + ) # Case vertex_as_id: not apply; T_vertex_id > T_vertex + build_paths = self._build_paths + calculation_limit = self._calculation_limit + predecessors = self._predecessors + attributes = self._attributes + + # Prepare limit check done by zero check + if calculation_limit is not None: + calculation_limit += 1 + + # Copy _TraversalWithoutWeightsWithVisited attributes into method scope + edges_with_data = self._edges_with_data + next_edge_or_vertex = self._next_edge_or_vertex + + # Get references of used gear objects and methods (avoid attribute resolution) + ( + _, + predecessors_sequence, + predecessors_wrapper, + ) = access_to_vertex_mapping_expect_none(predecessors) + ( + _, + attributes_sequence, + attributes_wrapper, + ) = access_to_vertex_mapping_expect_none(attributes) + visited = self.visited + visited_add = visited.add + ( + visited_uses_sequence, + visited_sequence, + visited_wrapper, + visited_uses_bits, + visited_index_and_bit_method, + ) = access_to_vertex_set(visited) + + # ----- Typing preparation of inner loop ----- + + # The following type Any opens no space for typing problems, since + # the content of next_edge_or_vertex is type checked and iterates + # objects of type T_vertex for edges_with_data==False and otherwise of + # one of the following: + # WeightedUnlabeledOutEdge[T_vertex, Any], + # UnweightedLabeledOutEdge[T_vertex, T_labels], + # WeightedLabeledOutEdge[T_vertex, Any, T_labels], + # And if labeled_edges==True, the first case is excluded. + # (Any alternative code version of the inner loop without + # Any or 'type: ignore' is slower) + edge_or_vertex: Any # "Hole" in typing, but types "around" make it safe + neighbor: T_vertex # Re-establish type "after" the "hole" + data_of_edge: T_labels # Re-establish type "after" the "hole" + + # Since the graph is not guaranteed to be a tree, we need a cycle check and + # need to skip already seen vertices. + # For detecting cycles, we store the vertices, that are on the current + # path, in a set. We use a stack for storing the vertices we have to + # enter or leave (when such a vertex is in the path set, we need to leave + # the vertex). + + # ----- Initialize specific bookkeeping ----- + + self.cycle_from_start = [] + + # Sequence used as stack of vertices that we need to enter & expand (if + # it is not on the trace, see below) or leave & report (otherwise) + to_enter_or_leave = self._gear.sequence_of_vertices(self._start_vertices) + to_visit_pop = to_enter_or_leave.pop + to_visit_append = to_enter_or_leave.append + + # Set of vertices along the current path + # (We need this for fast cycle detection. We could use additionally + # a trace sequence to speed up the check if the current vertex is + # the top vertex of the trace instead of checking if it is "in" the + # trace, but this would cost maintenance runtime and memory for the + # sequence). + trace_set = self._gear.vertex_id_set(()) + trace_set_add = trace_set.add + trace_set_discard = trace_set.discard + ( + trace_set_uses_sequence, + trace_set_sequence, + trace_set_wrapper, + trace_set_uses_bits, + trace_set_index_and_bit_method, + ) = access_to_vertex_set(trace_set) + + # Check compatibility of visited and trace_set. It is used for + # performance optimization later on. + assert ( + visited_uses_sequence == trace_set_uses_sequence + and visited_uses_bits == trace_set_uses_bits + ), ("Collection visited is incompatible " + "with collection trace_set") + set_uses_sequence = visited_uses_sequence + del visited_uses_sequence, trace_set_uses_sequence + set_uses_bits = visited_uses_bits + del visited_uses_bits, trace_set_uses_bits + if set_uses_sequence and set_uses_bits: + assert visited_index_and_bit_method is trace_set_index_and_bit_method, ( + "Collection visited is incompatible " + "with collection trace_set" + ) + set_index_and_bit_method = visited_index_and_bit_method + del visited_index_and_bit_method, trace_set_index_and_bit_method + + # ----- Inner loop ----- + + while to_enter_or_leave: + vertex = to_enter_or_leave[-1] # visit/report last added vertex first + v_id: T_vertex_id = ( + maybe_vertex_to_id(vertex) # type: ignore[assignment] + if maybe_vertex_to_id + else vertex + ) + + # If v_id is in trace_set: + # Back to trace, from visits/reports of further vertices, + # that trace vertices depend on: We "leave" and report the head + # vertex of the trace + # (A note about the above "in" check: + # If v_id is in the set, it needs to be + # the last id added there. But this does not help us, since + # sets are not ordered as dicts nowadays are). + # Otherwise: + # Ignore v_id if visited, else include vertex n_id in visited set. + # Then, take it to the trace. + if not set_uses_sequence: + # Standard implementation for "normal" MutableSet + if v_id in trace_set: + self.depth -= 1 + to_visit_pop() + trace_set_discard(v_id) + yield vertex + continue + if self.depth > 0: + if v_id in visited: + to_visit_pop() + continue + else: + visited_add(v_id) + trace_set_add(v_id) + + elif not set_uses_bits: + # Same as above, but with booleans in byte sequence + try: + if trace_set_sequence[v_id]: + self.depth -= 1 + to_visit_pop() + trace_set_sequence[v_id] = False + yield vertex + continue + except IndexError: + pass + if self.depth > 0: + try: + if visited_sequence[v_id]: + to_visit_pop() + continue + else: + visited_sequence[v_id] = True + except IndexError: + visited_wrapper.extend_and_set(v_id, True) + try: + trace_set_sequence[v_id] = True + except IndexError: + trace_set_wrapper.extend_and_set(v_id, True) + + else: + # Same as above, but with bits in byte sequence + sequence_key, bit_number = set_index_and_bit_method(v_id, 8) + bit_mask = 1 << bit_number + try: + value = trace_set_sequence[sequence_key] + if value & bit_mask: + self.depth -= 1 + to_visit_pop() + trace_set_sequence[sequence_key] = value - bit_mask + yield vertex + continue + except IndexError: + pass + if self.depth > 0: + try: + value = visited_sequence[sequence_key] + if value & bit_mask: + to_visit_pop() + continue + else: + visited_sequence[sequence_key] = value | bit_mask + except IndexError: + visited_wrapper.extend_and_set(sequence_key, bit_mask) + try: + trace_set_sequence[sequence_key] |= bit_mask + except IndexError: + trace_set_wrapper.extend_and_set(sequence_key, bit_mask) + + # We "expand" the vertex + if calculation_limit and not (calculation_limit := calculation_limit - 1): + raise RuntimeError("Number of visited vertices reached limit") + + for edge_or_vertex in next_edge_or_vertex(vertex, self): + neighbor = edge_or_vertex[0] if edges_with_data else edge_or_vertex + + n_id2: T_vertex_id = ( + maybe_vertex_to_id(neighbor) # type: ignore[assignment] + if maybe_vertex_to_id + else neighbor + ) + + # If neighbor is already visited and in trace_set: + # We found a dependency (edge) back to a vertex, whose + # dependencies we are currently following (trace). We + # build and report this trace: a cycle. + # If neighbor is already visited: + # ignore it (in case of path generation, this is necessary, and + # otherwise, it is a small optimization.) + # Note: In order to become visited, a vertex needs to + # get into the trace set and then be discarded + # from it. Thus, in the inner if, we know that the neighbour + # has already been in the trace set sometimes. + if not set_uses_sequence: + # Standard implementation for "normal" MutableSet + if n_id2 in visited: + if n_id2 in trace_set: + self._report_cycle( + neighbor, + to_enter_or_leave, + trace_set, + maybe_vertex_to_id, + ) + continue + + elif not set_uses_bits: + # Same as above, but with booleans in byte sequence + try: + if visited_sequence[n_id2]: + try: + if trace_set_sequence[n_id2]: + self._report_cycle( + neighbor, + to_enter_or_leave, + trace_set, + maybe_vertex_to_id, + ) + except IndexError: # pragma: no cover + raise AssertionError( + "Internal error: IndexError " "should never happen" + ) + continue + except IndexError: + pass + + else: + # Same as above, but with bits in byte sequence + sequence_key, bit_number = set_index_and_bit_method(n_id2, 8) + bit_mask = 1 << bit_number + try: + if visited_sequence[sequence_key] & bit_mask: + try: + if trace_set_sequence[sequence_key] & bit_mask: + self._report_cycle( + neighbor, + to_enter_or_leave, + trace_set, + maybe_vertex_to_id, + ) + except IndexError: # pragma: no cover + raise AssertionError( + "Internal error: IndexError " "should never happen" + ) + continue + except IndexError: + pass + + if build_paths: + # We have to store the predecessor here, because at time of + # visit, it is already lost. And we cannot yield here, + # because TopologicalSorted reports not until leaving vertices. + # But since the visiting order is defined by a stack we know + # that for each vertex, the predecessor stored last is the + # edge visited first, and after that no other predecessor can + # be stored for that vertex. + # Store the predecessor (vertex) of the neighbor + try: + predecessors_sequence[n_id2] = vertex + except IndexError: + predecessors_wrapper.extend_and_set(n_id2, vertex) + # Store the labels of the edge to the neighbor + if labeled_edges: + data_of_edge = edge_or_vertex[-1] + try: + attributes_sequence[n_id2] = data_of_edge + except IndexError: + attributes_wrapper.extend_and_set(n_id2, data_of_edge) + + # Needs to be visited, in stack order + to_visit_append(neighbor) + + # Update depth. The following vertices are one level deeper. + self.depth += 1 + + def _report_cycle( + self, + neighbor: T_vertex, + to_visit: MutableSequenceOfVertices[T_vertex], + trace_set: VertexIdSet[T_vertex_id], + maybe_vertex_to_id: Optional[VertexToID[T_vertex, T_vertex_id]], + ) -> None: + trace = list() + for vertex in to_visit: + v_id2: T_vertex_id = ( + maybe_vertex_to_id(vertex) # type: ignore[assignment] + if maybe_vertex_to_id + else vertex + ) + if v_id2 in trace_set: + trace.append(vertex) + trace.append(neighbor) + self.cycle_from_start = trace + raise RuntimeError("Graph contains cycle") + + +class TraversalTopologicalSort( + Generic[T_vertex, T_labels], + TraversalTopologicalSortFlex[T_vertex, T_vertex, T_labels], +): + """ + Eases the use of `TraversalTopologicalSortFlex` for typical cases. + For documentation of functionality and parameters, see there. + + Uses the following standard arguments for the respective parameters of + the parent class: + + - vertex_to_id = `vertex_as_id` + - gear = `GearDefault` + - `T_vertex_id` = `T_vertex` + + Implications: + + - `GearDefault` is used, see there how it and its superclass work + - T_vertex is bound to Hashable (T_vertex is used as `T_vertex_id`, see there) + """ + + def __init__( + self, + next_vertices: Optional[ + NextVertices[ + T_vertex, TraversalTopologicalSortFlex[T_vertex, T_vertex, T_labels] + ] + ] = None, + *, + next_edges: Optional[ + NextEdges[ + T_vertex, TraversalTopologicalSortFlex[T_vertex, T_vertex, T_labels] + ] + ] = None, + next_labeled_edges: Optional[ + NextLabeledEdges[ + T_vertex, + TraversalTopologicalSortFlex[T_vertex, T_vertex, T_labels], + T_labels, + ] + ] = None, + is_tree: bool = False, + ) -> None: + super().__init__( + vertex_as_id, + GearDefault(), + next_vertices, + next_edges=next_edges, + next_labeled_edges=next_labeled_edges, + is_tree=is_tree, + ) diff --git a/src/nographs/_strategies/traversals/without_weights/traversal_without_weights.py b/src/nographs/_strategies/traversals/without_weights/traversal_without_weights.py new file mode 100644 index 0000000..d533832 --- /dev/null +++ b/src/nographs/_strategies/traversals/without_weights/traversal_without_weights.py @@ -0,0 +1,163 @@ +from __future__ import annotations + +from abc import ABC +from typing import Optional, Any +from _collections_abc import Iterable, Collection + +from nographs._types import ( + T_vertex, + T_vertex_id, + T_labels, + VertexToID, +) +from nographs._gears import GearWithoutDistances, VertexIdSet +from ..traversal import ( + Traversal, + _start_from_needs_traversal_object, +) +from ...type_aliases import ( + NextVertices, + T_strategy, + NextEdges, + NextLabeledEdges, + NextEdgesOrVertices, +) +from ...utils import ( + NoVisitedSet, + define_visited, + iter_start_ids, + StrRepr, +) + + +def _create_unified_next( + next_vertices: Optional[NextVertices[T_vertex, T_strategy]], + next_edges: Optional[NextEdges[T_vertex, T_strategy]], + next_labeled_edges: Optional[NextLabeledEdges[T_vertex, T_strategy, T_labels]], +) -> tuple[NextEdgesOrVertices[T_vertex, T_strategy, T_labels], bool, bool]: + """Check configuration of given next_vertices, next_edges, and next_labeled_edges + and calculate unified NextEdgesOrVertices + and whether we have edges with data (weights and/or labels) and/or labeled_edges. + """ + next_edges_or_vertices: NextEdgesOrVertices[T_vertex, T_strategy, T_labels] + if next_vertices is not None: + if next_edges is not None: + raise RuntimeError("Both next_vertices and next_edges provided.") + if next_labeled_edges is not None: + raise RuntimeError("Both next_vertices and next_labeled_edges provided.") + next_edges_or_vertices = next_vertices + edges_with_data = False + labeled_edges = False + elif next_edges is not None: + if next_labeled_edges is not None: + raise RuntimeError("Both next_edges and next_labeled_edges provided.") + next_edges_or_vertices = next_edges + edges_with_data = True + labeled_edges = False + else: + if next_labeled_edges is None: + raise RuntimeError( + "Neither next_vertices nor next_edges " + + "nor next_labeled_edges provided." + ) + next_edges_or_vertices = next_labeled_edges + edges_with_data = True + labeled_edges = True + return next_edges_or_vertices, edges_with_data, labeled_edges + + +class _TraversalWithoutWeights(Traversal[T_vertex, T_vertex_id, T_labels], ABC): + """ + A traversal that needs no weight type. Edges can be given with or without data. + """ + + def __init__( + self, + edges_with_data: bool, + labeled_edges: bool, + is_tree: bool, + vertex_to_id: VertexToID[T_vertex, T_vertex_id], + gear: GearWithoutDistances[T_vertex, T_vertex_id, T_labels], + ) -> None: + """ + :param edges_with_data: Edges tuples, not just successor vertices + :param gear: The traversal will use this gear + + For the other parameters, see super class. + """ + super().__init__(labeled_edges, is_tree, vertex_to_id) + self._edges_with_data = edges_with_data + self._gear = gear + + +class _TraversalWithoutWeightsWithVisited( + _TraversalWithoutWeights[T_vertex, T_vertex_id, T_labels], ABC +): + """A _TraversalWithoutWeights with attribute visited.""" + + def __init__( + self, + edges_with_data: bool, + labeled_edges: bool, + is_tree: bool, + vertex_to_id: VertexToID[T_vertex, T_vertex_id], + gear: GearWithoutDistances[T_vertex, T_vertex_id, T_labels], + ) -> None: + super().__init__(edges_with_data, labeled_edges, is_tree, vertex_to_id, gear) + self.visited: VertexIdSet[T_vertex_id] = NoVisitedSet[T_vertex_id]() + """ A collection that contains the visited vertices (resp. their hashable ids + from vertex_to_id). After an exhaustive search, it contains + the vertices (resp. vertex ids) reachable from the start vertices. + """ + + def _start_without_weights_with_visited( + self, + start_vertex: Optional[T_vertex], + start_vertices: Optional[Iterable[T_vertex]], + build_paths: bool, + calculation_limit: Optional[int], + already_visited: Optional[VertexIdSet[T_vertex_id]], + empty_path_for_start_vertices: bool = True, + visited_for_start_vertices: bool = True, + ) -> None: + """ + Check configuration of start_vertex and start_vertices. Set attributes + _start_vertices, _build_path, paths, _predecessors, _attributes, + and visited. + Empty paths for start vertices are only set if demanded (default: True). + Start vertices are only set as visited if demanded (default: True). + """ + _start_from_needs_traversal_object(self) + self._start_from( + start_vertex, + start_vertices, + build_paths, + calculation_limit, + self._gear, + empty_path_for_start_vertices, + ) + if visited_for_start_vertices and not isinstance(start_vertices, Collection): + # We will consume vertices by the call of *iter_start_ids*, so we + # first make a collection out of start_vertices, except they + # are already given as a collection + self._start_vertices = self._gear.sequence_of_vertices(self._start_vertices) + self.visited = define_visited( + self._gear, + already_visited, + ( + iter_start_ids(self._start_vertices, self._vertex_to_id) + if visited_for_start_vertices + else () + ), + self._is_tree, + ) + + def _improve_state( + self, state: dict[str, Any], vertices: Optional[Iterable[T_vertex]] = None + ) -> None: + # Visited, a MutableSet, is typically not ordered. str(self.visited) + # results in different strings for different interpreters (PyPy) and + # the keys are not sorted. Here, we create a normalized description. + del state["visited"] + state["visited"] = StrRepr.from_set(self.visited) + super()._improve_state(state, vertices) diff --git a/src/nographs/_strategies/type_aliases.py b/src/nographs/_strategies/type_aliases.py new file mode 100644 index 0000000..997cc6e --- /dev/null +++ b/src/nographs/_strategies/type_aliases.py @@ -0,0 +1,107 @@ +""" Type aliases used for defining the signatures of methods of strategies + +Warning: The following types are manually documented in api.rst +""" + +from __future__ import annotations + +from typing import TypeVar, Callable, Iterable, Any, Union + +from nographs._types import ( + T_vertex, + OutEdge, + LabeledOutEdge, + T_labels, + WeightedUnlabeledOutEdge, + T_weight, + WeightedLabeledOutEdge, + UnweightedLabeledOutEdge, +) +from .strategy import Strategy + + +T_strategy = TypeVar("T_strategy", bound=Strategy) + + +# next vertices and next edges functions for traversals +# that work with and without weights + +NextVertices = Callable[[T_vertex, T_strategy], Iterable[T_vertex]] +NextEdges = Callable[[T_vertex, T_strategy], Iterable[OutEdge[T_vertex, Any, Any]]] + + +# next edges functions for traversal that work with weights +NextLabeledEdges = Callable[ + [T_vertex, T_strategy], Iterable[LabeledOutEdge[T_vertex, Any, T_labels]] +] +NextWeightedEdges = Callable[ + [T_vertex, T_strategy], + Iterable[ + Union[ + WeightedUnlabeledOutEdge[T_vertex, T_weight], + WeightedLabeledOutEdge[T_vertex, T_weight, Any], + ] + ], +] +NextWeightedLabeledEdges = Callable[ + [T_vertex, T_strategy], + Iterable[WeightedLabeledOutEdge[T_vertex, T_weight, T_labels]], +] + + +# The same, but as a tuple, for bidirectional search strategies + +BNextVertices = tuple[ + NextVertices[T_vertex, T_strategy], + NextVertices[T_vertex, T_strategy], +] +BNextEdges = tuple[ + NextEdges[T_vertex, T_strategy], + NextEdges[T_vertex, T_strategy], +] +BNextLabeledEdges = tuple[ + NextLabeledEdges[T_vertex, T_strategy, T_labels], + NextLabeledEdges[T_vertex, T_strategy, T_labels], +] +BNextWeightedEdges = tuple[ + NextWeightedEdges[T_vertex, T_strategy, T_weight], + NextWeightedEdges[T_vertex, T_strategy, T_weight], +] +BNextWeightedLabeledEdges = tuple[ + NextWeightedLabeledEdges[T_vertex, T_strategy, T_weight, T_labels], + NextWeightedLabeledEdges[T_vertex, T_strategy, T_weight, T_labels], +] + + +# --------------- package internal types ------------- + +NextEdgesOrVertices = Callable[ + [T_vertex, T_strategy], + Iterable[ + Union[ + T_vertex, + WeightedUnlabeledOutEdge[T_vertex, Any], + UnweightedLabeledOutEdge[T_vertex, T_labels], + WeightedLabeledOutEdge[T_vertex, Any, T_labels], + ] + ], +] +NextWeightedMaybeLabeledEdges = Callable[ + [T_vertex, T_strategy], + Iterable[ + Union[ + WeightedUnlabeledOutEdge[T_vertex, T_weight], + WeightedLabeledOutEdge[T_vertex, T_weight, T_labels], + ] + ], +] +# The same, but as a tuple, for bidirectional search strategies + +BNextEdgesOrVertices = tuple[ + NextEdgesOrVertices[T_vertex, T_strategy, T_labels], + NextEdgesOrVertices[T_vertex, T_strategy, T_labels], +] +BNextWeightedMaybeLabeledEdges = tuple[ + NextWeightedMaybeLabeledEdges[T_vertex, T_strategy, T_weight, T_labels], + NextWeightedMaybeLabeledEdges[T_vertex, T_strategy, T_weight, T_labels], +] diff --git a/src/nographs/_strategies.py b/src/nographs/_strategies/utils.py similarity index 56% rename from src/nographs/_strategies.py rename to src/nographs/_strategies/utils.py index ba28763..1218a79 100644 --- a/src/nographs/_strategies.py +++ b/src/nographs/_strategies/utils.py @@ -1,47 +1,39 @@ from __future__ import annotations -from abc import ABC - from collections.abc import ( - Callable, Iterator, Iterable, MutableSet, MutableMapping, ) -from typing import TypeVar, Optional, Any, Union, cast, Generic +from typing import Optional, Any, cast -from ._gear_collections import ( +from nographs._gear_collections import ( get_wrapper_from_vertex_set, get_wrapper_from_vertex_mapping, ) -from ._gears import ( +from nographs._gears import ( GearWithoutDistances, Gear, VertexIdSet, VertexIdToVertexMapping, VertexIdToDistanceMapping, - VertexIdToPathEdgeDataMapping, + VertexIdToEdgeLabelsMapping, ) -from ._paths import ( +from nographs._paths import ( Paths, PathsOfUnlabeledEdges, PathsOfLabeledEdges, - DummyPredecessorOrAttributesMapping, + DummyPredecessorOrLabelsMapping, PathsDummy, ) -from ._types import ( +from nographs._types import ( T_vertex, T_vertex_id, T_weight, T_labels, VertexToID, vertex_as_id, - WeightedUnlabeledOutEdge, - WeightedLabeledOutEdge, - UnweightedLabeledOutEdge, - LabeledOutEdge, - OutEdge, ) @@ -80,149 +72,6 @@ def __repr__(self) -> str: return self.s -class Strategy(ABC, Generic[T_vertex, T_vertex_id, T_labels]): - """Base class of the traversal strategies and search strategies of NoGraphs.""" - - def _improve_state( - self, state: dict[str, Any], vertices: Optional[Iterable[T_vertex]] = None - ) -> None: - """Improve the state description - - :param state: State in current form - :param vertices: If the strategy can provide additional state data w.r.t. these - vertices, it will do so. - """ - pass - - def state_to_str(self, vertices: Optional[Iterable[T_vertex]] = None) -> str: - """Return a human-readable description of the public state of the strategy as - a string. - - Implementation details, not covered by the semantic versioning: - - Currently, the method aims at providing a uniform behaviour over different - platforms (*CPython* and *PyPy*) and collection types (Gears with different - *MutableSet* and *MutableMapping* implementations). It behaves roughly as - follows: - - - A *MutableSet*, e.g. attribute *visited*, is described similar to a *set*, - but items are sorted lexicographically in their string representations - (this bridges differences between *CPython* and *PyPy*). - - - Attribute *Paths* is described similar to a *dict* (although keys can contain - unhashable values, and only paths for the given *vertices* are described). - - - A *MutableMapping*, e.g. attribute *distance*, is described similarly to a - *dict*, also in cases, where it is not a *dict*, and although the items - for only the given *vertices* are described. - - :param vertices: If the strategy can provide additional state data w.r.t. these - vertices, it will do so. - """ - state = dict((k, v) for k, v in self.__dict__.items() if k[0] != "_") - self._improve_state(state, vertices) - return str(state) - - -# --------------- exported types ------------- - -# todo: Warning: The following types are manually documented in api.rst - -# next vertices and next edges functions for traversals -# that work with and without weights - -T_strategy = TypeVar("T_strategy", bound=Strategy) - -NextVertices = Callable[[T_vertex, T_strategy], Iterable[T_vertex]] - -NextEdges = Callable[[T_vertex, T_strategy], Iterable[OutEdge[T_vertex, Any, Any]]] - -NextLabeledEdges = Callable[ - [T_vertex, T_strategy], Iterable[LabeledOutEdge[T_vertex, Any, T_labels]] -] - -# next edges functions for traversal that work with weights -NextWeightedEdges = Callable[ - [T_vertex, T_strategy], - Iterable[ - Union[ - WeightedUnlabeledOutEdge[T_vertex, T_weight], - WeightedLabeledOutEdge[T_vertex, T_weight, Any], - ] - ], -] - -NextWeightedLabeledEdges = Callable[ - [T_vertex, T_strategy], - Iterable[WeightedLabeledOutEdge[T_vertex, T_weight, T_labels]], -] - -# The same, but as a tuple, for bidirectional search strategies - -BNextVertices = tuple[ - NextVertices[T_vertex, T_strategy], - NextVertices[T_vertex, T_strategy], -] - -BNextEdges = tuple[ - NextEdges[T_vertex, T_strategy], - NextEdges[T_vertex, T_strategy], -] - -BNextLabeledEdges = tuple[ - NextLabeledEdges[T_vertex, T_strategy, T_labels], - NextLabeledEdges[T_vertex, T_strategy, T_labels], -] - -BNextWeightedEdges = tuple[ - NextWeightedEdges[T_vertex, T_strategy, T_weight], - NextWeightedEdges[T_vertex, T_strategy, T_weight], -] - -BNextWeightedLabeledEdges = tuple[ - NextWeightedLabeledEdges[T_vertex, T_strategy, T_weight, T_labels], - NextWeightedLabeledEdges[T_vertex, T_strategy, T_weight, T_labels], -] - - -# --------------- package internal types ------------- - -NextEdgesOrVertices = Callable[ - [T_vertex, T_strategy], - Iterable[ - Union[ - T_vertex, - WeightedUnlabeledOutEdge[T_vertex, Any], - UnweightedLabeledOutEdge[T_vertex, T_labels], - WeightedLabeledOutEdge[T_vertex, Any, T_labels], - ] - ], -] - -NextWeightedMaybeLabeledEdges = Callable[ - [T_vertex, T_strategy], - Iterable[ - Union[ - WeightedUnlabeledOutEdge[T_vertex, T_weight], - WeightedLabeledOutEdge[T_vertex, T_weight, T_labels], - ] - ], -] - - -# The same, but as a tuple, for bidirectional search strategies - -BNextEdgesOrVertices = tuple[ - NextEdgesOrVertices[T_vertex, T_strategy, T_labels], - NextEdgesOrVertices[T_vertex, T_strategy, T_labels], -] - -BNextWeightedMaybeLabeledEdges = tuple[ - NextWeightedMaybeLabeledEdges[T_vertex, T_strategy, T_weight, T_labels], - NextWeightedMaybeLabeledEdges[T_vertex, T_strategy, T_weight, T_labels], -] - - # --------------- internal support functions ------------- @@ -315,10 +164,10 @@ def create_paths( ) -> tuple[ Paths[T_vertex, T_vertex_id, T_labels], VertexIdToVertexMapping[T_vertex_id, T_vertex], - VertexIdToPathEdgeDataMapping[T_vertex_id, T_labels], + VertexIdToEdgeLabelsMapping[T_vertex_id, T_labels], ]: """Translate from configuration of path generation to setting of - paths, predecessors and attributes collection. Store empty paths for start + paths, predecessors and labels collection. Store empty paths for start vertices. If no paths should be build, create dummy Paths container.""" if build_paths: @@ -333,54 +182,26 @@ def create_paths( ) ) paths: Paths[T_vertex, T_vertex_id, T_labels] - attributes: Optional[VertexIdToPathEdgeDataMapping[T_vertex_id, T_labels]] + labels: Optional[VertexIdToEdgeLabelsMapping[T_vertex_id, T_labels]] if labeled_edges: - attributes = gear.vertex_id_to_path_attributes_mapping(()) + labels = gear.vertex_id_to_edge_labels_mapping(()) paths = PathsOfLabeledEdges[T_vertex, T_vertex_id, T_labels]( - predecessor, attributes, vertex_to_id + predecessor, labels, vertex_to_id ) else: paths = PathsOfUnlabeledEdges[T_vertex, T_vertex_id]( predecessor, vertex_to_id ) - attributes = DummyPredecessorOrAttributesMapping[T_vertex_id, T_labels]() - return paths, predecessor, attributes + labels = DummyPredecessorOrLabelsMapping[T_vertex_id, T_labels]() + return paths, predecessor, labels else: return ( PathsDummy[T_vertex, T_vertex_id, T_labels](vertex_to_id), - DummyPredecessorOrAttributesMapping[T_vertex_id, T_vertex](), - DummyPredecessorOrAttributesMapping[T_vertex_id, T_labels](), + DummyPredecessorOrLabelsMapping[T_vertex_id, T_vertex](), + DummyPredecessorOrLabelsMapping[T_vertex_id, T_labels](), ) -class NoIterator(Generic[T_vertex]): - """An iterator that raises RuntimeError instead of iterating. - - When the application requests an iterator from a traversal that has not been - started so far and uses this iterator, this is a programming error in the - application: When the traversal is started, the iterator will be replaced, but the - application will still have and maybe use the one it has requested to early. Thus, - the traversal returns a _NoIterator in this case, so that any use of it clearly - shows the problem. - """ - - def __next__(self) -> T_vertex: - """ - >>> next(NoIterator()) - Traceback (most recent call last): - RuntimeError: Traversal not started, iteration not possible - """ - raise RuntimeError("Traversal not started, iteration not possible") - - def __iter__(self) -> Iterator[T_vertex]: - """ - >>> iter(NoIterator()) - Traceback (most recent call last): - RuntimeError: Traversal not started, iteration not possible - """ - raise RuntimeError("Traversal not started, iteration not possible") - - class NoVisitedSet(MutableSet[T_vertex_id]): """A MutableSet for vertex ids that raises an exception on each operation. diff --git a/src/nographs/_traversals.py b/src/nographs/_traversals.py deleted file mode 100644 index 2b17302..0000000 --- a/src/nographs/_traversals.py +++ /dev/null @@ -1,3498 +0,0 @@ -from __future__ import annotations - -import array -import copy -import itertools -from abc import ABC, abstractmethod -from collections.abc import ( - Callable, - Iterator, - Iterable, -) -from heapq import heapify, heappop, heappush -from numbers import Real -from typing import Optional, Any, Union, cast, overload, Generic, Literal - -from ._gear_collections import ( - access_to_vertex_set, - access_to_vertex_mapping, - access_to_vertex_mapping_expect_none, -) -from ._gears import ( - GearWithoutDistances, - Gear, - GearDefault, - VertexIdSet, - VertexIdToVertexMapping, - VertexIdToDistanceMapping, - VertexIdToPathEdgeDataMapping, - MutableSequenceOfVertices, -) -from ._paths import ( - Paths, - DummyPredecessorOrAttributesMapping, - PathsDummy, -) -from ._strategies import ( - StrRepr, - Strategy, - T_strategy, - NextVertices, - NextEdges, - NextLabeledEdges, - NextWeightedEdges, - NextWeightedLabeledEdges, - NextEdgesOrVertices, - NextWeightedMaybeLabeledEdges, - iter_start_ids, - iter_start_vertices_and_ids, - define_visited, - define_distances, - create_paths, - NoIterator, - NoVisitedSet, - NoDistancesMapping, -) -from ._types import ( - T_vertex, - T_vertex_id, - T_weight, - T_labels, - VertexToID, - vertex_as_id, - WeightedOutEdge, - WeightedFullEdge, -) - - -# --------------- internal support functions ------------- - - -def _start_from_needs_traversal_object(obj: Any) -> None: - if not isinstance(obj, Traversal): - raise RuntimeError( - "Method start_from can only be called on a Traversal object." - ) - - -def _create_unified_next( - next_vertices: Optional[NextVertices[T_vertex, T_strategy]], - next_edges: Optional[NextEdges[T_vertex, T_strategy]], - next_labeled_edges: Optional[NextLabeledEdges[T_vertex, T_strategy, T_labels]], -) -> tuple[NextEdgesOrVertices[T_vertex, T_strategy, T_labels], bool, bool]: - """Check configuration of given next_vertices, next_edges, and next_labeled_edges - and calculate unified NextEdgesOrVertices - and whether we have edges with data (weights and/or labels) and/or labeled_edges. - """ - next_edges_or_vertices: NextEdgesOrVertices[T_vertex, T_strategy, T_labels] - if next_vertices is not None: - if next_edges is not None: - raise RuntimeError("Both next_vertices and next_edges provided.") - if next_labeled_edges is not None: - raise RuntimeError("Both next_vertices and next_labeled_edges provided.") - next_edges_or_vertices = next_vertices - edges_with_data = False - labeled_edges = False - elif next_edges is not None: - if next_labeled_edges is not None: - raise RuntimeError("Both next_edges and next_labeled_edges provided.") - next_edges_or_vertices = next_edges - edges_with_data = True - labeled_edges = False - else: - if next_labeled_edges is None: - raise RuntimeError( - "Neither next_vertices nor next_edges " - + "nor next_labeled_edges provided." - ) - next_edges_or_vertices = next_labeled_edges - edges_with_data = True - labeled_edges = True - return next_edges_or_vertices, edges_with_data, labeled_edges - - -def _create_unified_next_weighted( - next_edges: Optional[NextWeightedEdges[T_vertex, T_strategy, T_weight]], - next_labeled_edges: Optional[ - NextWeightedLabeledEdges[T_vertex, T_strategy, T_weight, T_labels] - ], -) -> tuple[ - NextWeightedMaybeLabeledEdges[T_vertex, T_strategy, T_weight, T_labels], bool -]: - """Check configuration of given next_edges and next_labeled_edges and calculate - unified _NextWeightedMaybeLabeledEdges[] and whether we have labeled_edges.""" - next_maybe_labeled_edges: NextWeightedMaybeLabeledEdges[ - T_vertex, T_strategy, T_weight, T_labels - ] - if next_edges is not None: - if next_labeled_edges is not None: - raise RuntimeError("Both next_edges and next_labeled_edges provided.") - next_maybe_labeled_edges = next_edges - labeled_edges = False - else: - if next_labeled_edges is None: - raise RuntimeError("Neither next_edges and next_labeled_edges provided.") - next_maybe_labeled_edges = next_labeled_edges - labeled_edges = True - return next_maybe_labeled_edges, labeled_edges - - -# -- traversal strategies for unweighted graphs with or without edge labels -- - - -class Traversal(Strategy[T_vertex, T_vertex_id, T_labels]): - """ - Abstract Class. Its subclasses provide methods to iterate through vertices - and edges using some specific traversal strategies. - """ - - @abstractmethod - def __init__( - self, - labeled_edges: bool, - is_tree: bool, - vertex_to_id: VertexToID[T_vertex, T_vertex_id], - ) -> None: - # -- attributes of graph adaptation - self._labeled_edges = labeled_edges - self._is_tree = is_tree - self._vertex_to_id = vertex_to_id - - # -- general attributes set and needed by all traversal strategies - self._generator: Iterator[T_vertex] = NoIterator[T_vertex]() - self._start_vertices: Iterable[T_vertex] = tuple[T_vertex]() - self._build_paths: bool = False - self._calculation_limit: Optional[int] = None - - # -- attributes for path data, needed by all traversal strategies - self.paths: Paths[T_vertex, T_vertex_id, T_labels] = PathsDummy[ - T_vertex, T_vertex_id, T_labels - ](vertex_to_id) - """ The container *paths* holds the created paths, if path creation has been - demanded. If labeled edges were provided (parameter *next_labeled_edges*), the - paths contain them instead of just vertices. - """ - self._predecessors: VertexIdToVertexMapping[T_vertex_id, T_vertex] = ( - DummyPredecessorOrAttributesMapping[T_vertex_id, T_vertex]() - ) - self._attributes: VertexIdToPathEdgeDataMapping[T_vertex_id, T_labels] = ( - DummyPredecessorOrAttributesMapping[T_vertex_id, T_labels]() - ) - - def __iter__( - self, - ) -> Iterator[T_vertex]: # Type alias needed do to a sphinx limitation - """ - Return the iterator of a started traversal. This allows for using a - `Traversal` in *for* loops or as parameter to a call of function - *next()*. - - Subsequent calls return the same iterator again. This allows for using - the same `Traversal` in subsequent *for* loops or *next()* calls, as - long as the iterator is not exhausted. - - The iterator yields vertices reported by the traversal algorithm. - When a vertex is reported, specific attributes of the traversal object - contain additional data about the state of the traversal (see the API - documentation of the respective subclass of `Traversal`).""" - if not isinstance(type(self), type(Traversal)): - raise RuntimeError("Method go can only be called on a Traversal object.") - return self._generator - - def __next__(self) -> T_vertex: - """Returns the next vertex reported by the (started) traversal. This - allows for calls like *next(traversal)*. - - Delegates to the iterator of the traversal.""" - return next(self._generator) - - def go_for_vertices_in( - self, vertices: Iterable[T_vertex], fail_silently: bool = False - ) -> Iterator[T_vertex]: - """ - For a started traversal, return an iterator that fetches vertices - from the traversal, reports a vertex if it is in *vertices*, and stops when - all the *vertices* have been found and reported. - - If the iterator has no more vertices to report (graph is exhausted) without - having found all the *vertices*, KeyError is raised, or the traversal just - terminates, if a silent fail is demanded. - - If *vertices* does not provide any vertices, an empty iterator is returned. - - If a `VertexToID` function is used, the method searches for vertices - that have the same id as one of the *vertices*. - - Whenever a vertex is reported, specific attributes of the traversal object - contain additional data about the state of the traversal (see the API - documentation of the respective subclass of `Traversal`). - - :param vertices: Vertices to find - :param fail_silently: Terminate, but do not raise KeyError, when graph - is exhausted. - """ - if not isinstance(type(self), type(Traversal)): - raise RuntimeError( - "Method go_for_vertices_in can only be called " - + "on a Traversal object." - ) - # In order to make the above check work, the following generator functionality - # needs to be encapsulated in a local function - - def my_generator() -> Iterator[T_vertex]: - vertex_to_id = self._vertex_to_id - if vertex_to_id == vertex_as_id: - vertex_set = set(cast(Iterable[T_vertex_id], vertices)) - v_count = len(vertex_set) - if v_count: - for v in self._generator: - if v not in vertex_set: - continue - yield v - v_count -= 1 - if v_count == 0: - break - else: - vertex_set = set(vertex_to_id(vertex) for vertex in vertices) - v_count = len(vertex_set) - if v_count: - for v in self._generator: - if vertex_to_id(v) not in vertex_set: - continue - yield v - v_count -= 1 - if v_count == 0: - break - if v_count > 0 and not fail_silently: - raise KeyError("Not all of the given vertices have been found") - - return my_generator() - - @overload - def go_to( - self, vertex: T_vertex, fail_silently: Literal[False] = False - ) -> T_vertex: ... - - @overload - def go_to( - self, vertex: T_vertex, fail_silently: Literal[True] - ) -> Optional[T_vertex]: ... - - def go_to( - self, vertex: T_vertex, fail_silently: bool = False - ) -> Optional[T_vertex]: - """ - For a started traversal, walk through the graph, stop at *vertex* and - return it. If the traversal ends (traversal iterator is exhausted) without - having found *vertex*, raise KeyError, or return None, - if a silent fail is demanded. - - If a `VertexToID` function is used, the method searches for a vertex - that has the same id as the given *vertex*. - - When *vertex* is reported, specific attributes of the traversal object - contain additional data about the state of the traversal (see the API - documentation of the respective subclass of `Traversal`). - - :param vertex: Stop search at this vertex. - - :param fail_silently: Terminate and return None, but do not raise KeyError, - when graph is exhausted. - """ - if not isinstance(type(self), type(Traversal)): - raise RuntimeError("Method go_to can only be called on a Traversal object.") - - vertex_to_id = self._vertex_to_id - if vertex_to_id == vertex_as_id: - for v in self._generator: - if v != vertex: - continue - return v - else: - if fail_silently: - return None - else: - raise KeyError("Vertex not found, graph exhausted.") - else: - vertex_id = vertex_to_id(vertex) - for v in self._generator: - if vertex_to_id(v) != vertex_id: - continue - return v - else: - if fail_silently: - return None - else: - raise KeyError("Vertex not found, graph exhausted.") - - def _start_from( - self, - start_vertex: Optional[T_vertex], - start_vertices: Optional[Iterable[T_vertex]], - build_paths: bool, - calculation_limit: Optional[int], - gear: GearWithoutDistances[T_vertex, T_vertex_id, T_labels], - ) -> None: - # Check start vertices options. Compute multi-vertices form from single vertex. - if start_vertex is not None: - if start_vertices is not None: - raise RuntimeError("Both start_vertex and start_vertices provided.") - self._start_vertices = (start_vertex,) - else: - if start_vertices is None: - raise RuntimeError("Neither start_vertex nor start_vertices provided.") - self._start_vertices = start_vertices - - # Create and store path container and path setting - self._build_paths = build_paths - self.paths, self._predecessors, self._attributes = create_paths( - build_paths, - gear, - self._labeled_edges, - self._vertex_to_id, - self._start_vertices, - ) - - # store calculation limit - self._calculation_limit = calculation_limit - - def _start(self) -> None: - self._generator = self._traverse() - - @abstractmethod - def _traverse(self) -> Iterator[T_vertex]: - """Has to be implemented in subclass""" - - def _improve_state( - self, state: dict[str, Any], vertices: Optional[Iterable[T_vertex]] = None - ) -> None: - # Convert a Paths object to an object that can be converted to a str. - # Paths in the paths object are only valid for reported vertices, - # and these do not need to be hashable. So, we cannot convert them - # to a dict and from there to a string. - del state["paths"] - if vertices is not None: - state["paths"] = ( - StrRepr.from_iterable( - (vertex, self.paths[vertex]) for vertex in vertices - ) - if self._build_paths - else dict() - ) - super()._improve_state(state, vertices) - - -# -------------- Traversal strategies for unweighted edges ----------------- - - -class _TraversalWithoutWeights(Traversal[T_vertex, T_vertex_id, T_labels], ABC): - """Internal: A traversal without weight type and distances collection, but - with attribute visited.""" - - def __init__( - self, - edges_with_data: bool, - labeled_edges: bool, - is_tree: bool, - vertex_to_id: VertexToID[T_vertex, T_vertex_id], - gear: GearWithoutDistances[T_vertex, T_vertex_id, T_labels], - ) -> None: - super().__init__(labeled_edges, is_tree, vertex_to_id) - self._edges_with_data = edges_with_data - self._gear = gear - self.visited: VertexIdSet[T_vertex_id] = NoVisitedSet[T_vertex_id]() - """ A collection that contains the visited vertices (resp. their hashable ids - from vertex_to_id). After an exhaustive search, it contains - the vertices (resp. vertex ids) reachable from the start vertices. - """ - - def _start_with_or_without_labels_from( - self, - start_vertex: Optional[T_vertex], - start_vertices: Optional[Iterable[T_vertex]], - build_paths: bool, - calculation_limit: Optional[int], - already_visited: Optional[VertexIdSet[T_vertex_id]], - ) -> None: - _start_from_needs_traversal_object(self) - self._start_from( - start_vertex, - start_vertices, - build_paths, - calculation_limit, - self._gear, - ) - self.visited = define_visited( - self._gear, - already_visited, - iter_start_ids(self._start_vertices, self._vertex_to_id), - self._is_tree, - ) - super()._start() - - def _improve_state( - self, state: dict[str, Any], vertices: Optional[Iterable[T_vertex]] = None - ) -> None: - # Visited, a MutableSet, is typically not ordered. str(self.visited) - # results in different strings for different interpreters (PyPy) and - # the keys are not sorted. Here, we create a normalized description. - del state["visited"] - state["visited"] = StrRepr.from_set(self.visited) - super()._improve_state(state, vertices) - - -class _TraversalWithoutWeightsBasic( - _TraversalWithoutWeights[T_vertex, T_vertex_id, T_labels], ABC -): - @abstractmethod - def start_from( - self, - start_vertex: Optional[T_vertex] = None, - *, - start_vertices: Optional[Iterable[T_vertex]] = None, - build_paths: bool = False, - calculation_limit: Optional[int] = None, - already_visited: Optional[VertexIdSet[T_vertex_id]] = None, - ) -> Traversal[T_vertex, T_vertex_id, T_labels]: - """ - Start the traversal at a vertex or a set of vertices and set parameters. - - :param start_vertex: The vertex the search should start at. If None, provide - start_vertices. - - :param start_vertices: The vertices the search should start at. Only - allowed if start_vertex equals None. - - :param build_paths: If true, build paths from some start vertex to each visited - vertex. Paths of start vertices are empty paths. - - :param calculation_limit: If provided, maximal number of vertices to process - (read in) from your graph. If it is exceeded, a RuntimeError will be raised. - - :param already_visited: If provided, this set is used instead of an internal - one to keep vertices (resp. their hashable ids from vertex_to_id), - that have already been visited. This parameter can be used to get online - access to the internal bookkeeping of visited vertices, or to preload - vertices that should never be visited, or to provide your own way for - storing the information that a vertex has already been visited. - - :return: Traversal, that has been started, e.g., statements like *iter()*, - *next()*, *for* and the methods "go*" of the Traversal can now be used. - """ - - -class _TraversalWithoutWeightsDFS( - _TraversalWithoutWeights[T_vertex, T_vertex_id, T_labels], ABC -): - @abstractmethod - def start_from( - self, - start_vertex: Optional[T_vertex] = None, - *, - start_vertices: Optional[Iterable[T_vertex]] = None, - build_paths: bool = False, - compute_depth: bool = False, - calculation_limit: Optional[int] = None, - already_visited: Optional[VertexIdSet[T_vertex_id]] = None, - ) -> Traversal[T_vertex, T_vertex_id, T_labels]: - """ - Start the traversal at a vertex or a set of vertices and set parameters. - - :param start_vertex: The vertex the search should start at. If None, provide - start_vertices. - - :param start_vertices: The vertices the search should start at. Only - allowed if start_vertex equals None. - - :param build_paths: If true, build paths from some start vertex to each visited - vertex. - - :param compute_depth: For each reported vertex, provide the search depth is has - been found at (Note: Often, this information is not helpful, and the - computation increases memory consumption and runtime). - - :param calculation_limit: If provided, maximal number of vertices to process - (read in) from your graph. If it is exceeded, a RuntimeError will be raised. - - :param already_visited: If provided, this set is used instead of an internal - one to keep vertices (resp. their hashable ids from vertex_to_id), - that have already been visited. This parameter can be used to get online - access to the internal bookkeeping of visited vertices, or to preload - vertices that should never be visited, or to provide your own way for - storing the information that a vertex has already been visited. - - :return: Traversal, that has been started, e.g., statements like *iter()*, - *next()*, *for* and the methods "go*" of the Traversal can now be used. - """ - - -class TraversalBreadthFirstFlex( - _TraversalWithoutWeightsBasic[T_vertex, T_vertex_id, T_labels] -): - """ - Bases: `Traversal` [`T_vertex`, `T_vertex_id`, `T_labels`] - - :param vertex_to_id: See `VertexToID` function. - - :param gear: See `gears API ` and class `GearWithoutDistances`. - - :param next_vertices: See `NextVertices` function. If None, provide next_edges - or next_labeled_edges. - - :param next_edges: See `NextEdges` function. Only allowed if next_vertices equals - None. If both are None, provide next_labeled_edges. - - :param next_labeled_edges: See `NextLabeledEdges` function. Only allowed if - next_vertices and next_edges equal None. If given, paths will record the given - labels. - - :param is_tree: bool: If it is certain, that during each traversal run, - each vertex can be reached only once, is_tree can be set to True. This - improves performance, but attribute *visited* of the traversal will not be - updated during and after the traversal. - - **Algorithm:** Breadth First Search, non-recursive, based on FIFO queue, - vertices are reported when they are first "seen". - - **Properties:** Reports and expands vertices in breadth first order, i.e., - with ascending depth (edge count of the path with the fewest edges from a start - vertex). All computed paths are *shortest paths* , i.e., paths with minimal number - of edges from a start vertex to their end vertex. - A vertex is regarded as visited when it has been reported or if it is a start - vertex. - - **Input:** Directed graph. Unlabeled or labeled edges. One or more start - vertices. Optional calculation limit. - - **Search state:** When a vertex is - *expanded* (traversal calls next_vertices, next_edges or next_labeled_edges) - or *reported* (an iterator of the traversal returns it), - the traversal provides updated values for the attributes - *depth*, *paths*, and *visited*. - """ - - def __init__( - self, - vertex_to_id: VertexToID[T_vertex, T_vertex_id], - gear: GearWithoutDistances[T_vertex, T_vertex_id, T_labels], - next_vertices: Optional[ - NextVertices[ - T_vertex, TraversalBreadthFirstFlex[T_vertex, T_vertex_id, T_labels] - ] - ] = None, - *, - next_edges: Optional[ - NextEdges[ - T_vertex, TraversalBreadthFirstFlex[T_vertex, T_vertex_id, T_labels] - ] - ] = None, - next_labeled_edges: Optional[ - NextLabeledEdges[ - T_vertex, - TraversalBreadthFirstFlex[T_vertex, T_vertex_id, T_labels], - T_labels, - ] - ] = None, - is_tree: bool = False, - ) -> None: - ( - self._next_edge_or_vertex, - edges_with_data, - labeled_edges, - ) = _create_unified_next(next_vertices, next_edges, next_labeled_edges) - super().__init__(edges_with_data, labeled_edges, is_tree, vertex_to_id, gear) - self.depth: int = -1 # value not used, initialized during traversal - """ At this *search depth*, the reported (resp. the expanded) vertex has been - found. It equals the length (number of edges) of the created path to the - vertex, if path creation is demanded. - For the special case of TraversalBreadthFirst, it equals the - *depth of the vertex* (minimal number of edges needed to come to it - from a start vertex). - When a traversal has been started, but no vertex has been reported or expanded - so far, the depth is 0 (depth of the start vertices). - """ - self._report_depth_increase = False - - def start_from( - self, - start_vertex: Optional[T_vertex] = None, - *, - start_vertices: Optional[Iterable[T_vertex]] = None, - build_paths: bool = False, - calculation_limit: Optional[int] = None, - already_visited: Optional[VertexIdSet[T_vertex_id]] = None, - _report_depth_increase: bool = False, # hidden parameter for internal use - ) -> TraversalBreadthFirstFlex[T_vertex, T_vertex_id, T_labels]: - _start_from_needs_traversal_object(self) - self._start_with_or_without_labels_from( - start_vertex, - start_vertices, - build_paths, - calculation_limit, - already_visited, - ) - self._report_depth_increase = _report_depth_increase - self.depth = 0 - return self - - def _traverse(self) -> Iterator[T_vertex]: - # ----- Prepare efficient environment for inner loop ----- - # Copy Traversal attributes into method scope (faster access) - labeled_edges = self._labeled_edges - is_tree = self._is_tree - maybe_vertex_to_id = ( - None if self._vertex_to_id == vertex_as_id else self._vertex_to_id - ) # Case vertex_as_id: not apply; T_vertex_id > T_vertex - build_paths = self._build_paths - calculation_limit = self._calculation_limit - report_depth_increase = self._report_depth_increase - predecessors = self._predecessors - attributes = self._attributes - - # Prepare limit check done by zero check - if calculation_limit is not None: - calculation_limit += 1 - - # Copy _TraversalWithoutWeights attributes into method scope - edges_with_data = self._edges_with_data - next_edge_or_vertex = self._next_edge_or_vertex - visited = self.visited - - # Get references of used gear objects and methods (avoid attribute resolution) - visited_add = visited.add - ( - visited_uses_sequence, - visited_sequence, - visited_wrapper, - visited_uses_bits, - index_and_bit_method, - ) = access_to_vertex_set(visited) - ( - _, - predecessors_sequence, - predecessors_wrapper, - ) = access_to_vertex_mapping_expect_none(predecessors) - ( - _, - attributes_sequence, - attributes_wrapper, - ) = access_to_vertex_mapping_expect_none(attributes) - - # ----- Initialize method specific bookkeeping ----- - - # Two lists used as FIFO queue with just two buckets - # (using a queue and counting down the size of current depth horizon is slower, - # and creating a new list instead of clear() is also slower) - - to_expand = self._gear.sequence_of_vertices(self._start_vertices) - next_to_expand = self._gear.sequence_of_vertices(()) - - prev_traversal = copy.copy(self) # copy of self, for keeping previous depth - self.depth = 1 # used for reporting (prev_traversal starts at 0) - - # Get method references of specific bookkeeping (avoid attribute resolution) - to_expand_append = to_expand.append - next_to_expand_append = next_to_expand.append - - # ----- Typing preparation of inner loop ----- - - # The following type Any opens no space for typing problems, since - # the content of next_edge_or_vertex is type checked and iterates - # objects of type T_vertex for edges_with_data==False and otherwise of - # one of the following: - # WeightedUnlabeledOutEdge[T_vertex, Any], - # UnweightedLabeledOutEdge[T_vertex, T_labels], - # WeightedLabeledOutEdge[T_vertex, Any, T_labels], - # And if labeled_edges==True, the first case is excluded. - # (Any alternative code version of the inner loop without - # Any or 'type: ignore' is slower) - edge_or_vertex: Any # "Hole" in typing, but types "around" make it safe - neighbor: T_vertex # Re-establish type "after" the "hole" - edge_data: T_labels # Re-establish type "after" the "hole" - - # ----- Inner loop ----- - - while to_expand: - for vertex in to_expand: - if calculation_limit and not ( - calculation_limit := calculation_limit - 1 - ): - raise RuntimeError("Number of visited vertices reached limit") - - for edge_or_vertex in next_edge_or_vertex(vertex, prev_traversal): - neighbor = edge_or_vertex[0] if edges_with_data else edge_or_vertex - - if not is_tree or build_paths: - n_id: T_vertex_id = ( - maybe_vertex_to_id(neighbor) # type: ignore[assignment] - if maybe_vertex_to_id - else neighbor - ) - - # If not is_tree: Ignore neighbor if already seen, and - # else include its ID in visited set. - # (If-nesting optimized for first case) - if not is_tree and not visited_uses_sequence: - # Standard implementation for "normal" MutableMapping - if n_id in visited: - continue - visited_add(n_id) - elif not is_tree: - if visited_uses_bits: - # Same as above, but with bits in byte sequence - sequence_key, bit_number = index_and_bit_method(n_id, 8) - bit_mask = 1 << bit_number - try: - value = visited_sequence[sequence_key] - if value & bit_mask: - continue - visited_sequence[sequence_key] = value | bit_mask - except IndexError: - visited_wrapper.extend_and_set( - sequence_key, bit_mask - ) - else: - # Same as above, but with booleans in byte sequence - try: - if visited_sequence[n_id]: - continue - visited_sequence[n_id] = True - except IndexError: - visited_wrapper.extend_and_set(n_id, True) - - if build_paths: - # Store the predecessor (vertex) of the neighbor - try: - predecessors_sequence[n_id] = vertex - except IndexError: - predecessors_wrapper.extend_and_set(n_id, vertex) - # Store the labels of the edge to the neighbor - if labeled_edges: - edge_data = edge_or_vertex[-1] - try: - attributes_sequence[n_id] = edge_data - except IndexError: - attributes_wrapper.extend_and_set(n_id, edge_data) - - # Vertex has been seen, report it now - yield neighbor - # Needs to be expanded in the next round - next_to_expand_append(neighbor) - - if report_depth_increase and next_to_expand: - # We are not finished yet, because we found new vertices to expand, - # and we are about to increase the depth now, and it is demanded - # to report this situation by reporting the last vertex reported so far - # again. So we report it again. - yield next_to_expand[-1] - - # Update external views (reporting/expanding) on depth - self.depth += 1 - prev_traversal.depth += 1 - # Prepare state for next depth level of vertices - to_expand, next_to_expand, to_expand_append, next_to_expand_append = ( - next_to_expand, - to_expand, - next_to_expand_append, - to_expand_append, - ) - del next_to_expand[:] - - def go_for_depth_range(self, start: int, stop: int) -> Iterator[T_vertex]: - """ - For a started traversal, return an iterator. During the traversal, - the iterator skips vertices as long as their depth is lower than *start*. - From then on, is reports the found vertices. It stops when the reached depth - is equal to or higher than *stop*. - - Note: The first vertex with a depth equal or higher than *stop* will be - consumed from the traversal, but will not be reported, so it is lost (compare - *itertools.takewhile*). - - :param start: Vertices lower than this are skipped. - :param stop: Reporting stops when reached depth is equal or higher than this. - """ - if not isinstance(type(self), type(Traversal)): - raise RuntimeError( - "Method go_for_depth_range can only be called " - + "on a Traversal object." - ) - - # In order to make the above check work, the following generator functionality - # needs to be encapsulated in a local function - def my_generator() -> Iterator[T_vertex]: - for v in self._generator: - if self.depth >= start: - if self.depth < stop: - yield v - break - for v in self._generator: - if self.depth >= stop: - break - yield v - - return my_generator() - - -class TraversalBreadthFirst( - Generic[T_vertex, T_labels], TraversalBreadthFirstFlex[T_vertex, T_vertex, T_labels] -): - """ - Eases the use of `TraversalBreadthFirstFlex` for typical cases. - For documentation of functionality and parameters, see there. - - Uses the following standard arguments for the respective parameters of - the parent class: - - - vertex_to_id = `vertex_as_id` - - gear = `GearDefault` - - `T_vertex_id` = `T_vertex` - - Implications: - - - `GearDefault` is used, see there how it and its superclass work - - T_vertex is bound to Hashable (T_vertex is used as `T_vertex_id`, see there) - """ - - def __init__( - self, - next_vertices: Optional[ - NextVertices[ - T_vertex, TraversalBreadthFirstFlex[T_vertex, T_vertex, T_labels] - ] - ] = None, - *, - next_edges: Optional[ - NextEdges[T_vertex, TraversalBreadthFirstFlex[T_vertex, T_vertex, T_labels]] - ] = None, - next_labeled_edges: Optional[ - NextLabeledEdges[ - T_vertex, - TraversalBreadthFirstFlex[T_vertex, T_vertex, T_labels], - T_labels, - ] - ] = None, - is_tree: bool = False, - ) -> None: - super().__init__( - vertex_as_id, - GearDefault(), - next_vertices, - next_edges=next_edges, - next_labeled_edges=next_labeled_edges, - is_tree=is_tree, - ) - - -class TraversalDepthFirstFlex( - _TraversalWithoutWeightsDFS[T_vertex, T_vertex_id, T_labels] -): - """ - Bases: `Traversal` [`T_vertex`, `T_vertex_id`, `T_labels`] - - :param vertex_to_id: See `VertexToID` function. - - :param gear: See `gears API ` and class `GearWithoutDistances`. - - :param next_vertices: See `NextVertices` function. If None, provide next_edges - or next_labeled_edges. - - :param next_edges: See `NextEdges` function. Only allowed if next_vertices equals - None. If both are None, provide next_labeled_edges. - - :param next_labeled_edges: See `NextLabeledEdges` function. Only allowed if - next_vertices and next_edges equal None. If given, paths will record the given - labels. - - :param is_tree: bool: If it is certain, that during each traversal run, each vertex - can be reached only once, is_tree can be set to True. This improves - performance, but attribute *visited* of the traversal will not be updated - during and after the traversal. - - **Algorithm:** Depth First Search ("BFS"), non-recursive, based on stack, - vertices are reported when they are about to be expanded (neighbors read from the - graph). - - **Properties:** Follows edges to new vertices (and reports and expands them) as - long as possible, and goes back a step and follows further edges that start at some - visited vertex only if necessary to come to new vertices. - A vertex is regarded as visited when it has been reported or if it is a start - vertex. - - **Input:** Directed graph. One or more start vertices. Vertices must be - hashable, or hashable id can be provided. Unlabeled or labeled edges. Optional - calculation limit. - - **Search state:** When a vertex is - *expanded* (traversal calls next_vertices, next_edges or next_labeled_edges) - or *reported* (an iterator of the traversal returns it), - the traversal provides updated values for the attributes - *depth*, *paths*, and *visited*. - """ - - def __init__( - self, - vertex_to_id: VertexToID[T_vertex, T_vertex_id], - gear: GearWithoutDistances[T_vertex, T_vertex_id, T_labels], - next_vertices: Optional[ - NextVertices[ - T_vertex, TraversalDepthFirstFlex[T_vertex, T_vertex_id, T_labels] - ] - ] = None, - *, - next_edges: Optional[ - NextEdges[ - T_vertex, TraversalDepthFirstFlex[T_vertex, T_vertex_id, T_labels] - ] - ] = None, - next_labeled_edges: Optional[ - NextLabeledEdges[ - T_vertex, - TraversalDepthFirstFlex[T_vertex, T_vertex_id, T_labels], - T_labels, - ] - ] = None, - is_tree: bool = False, - ) -> None: - ( - self._next_edge_or_vertex, - edges_with_data, - labeled_edges, - ) = _create_unified_next(next_vertices, next_edges, next_labeled_edges) - super().__init__(edges_with_data, labeled_edges, is_tree, vertex_to_id, gear) - self.depth: int = -1 # value not used - """ If depth computation has been demanded: - At this *search depth*, the reported (resp. the expanded) vertex has been - found. It equals the length (number of edges) of the created path to the - vertex, if path creation is demanded. - Note: The search depth does not need to be the depth of the vertex - (see `TraversalBreadthFirstFlex`). - When a traversal has been started, but no vertex has been reported or expanded - so far, the depth is 0 (depth of the start vertices). - """ - self._compute_depth = False # value not used - self._allow_reordering = False # value not used - - def start_from( - self, - start_vertex: Optional[T_vertex] = None, - *, - start_vertices: Optional[Iterable[T_vertex]] = None, - build_paths: bool = False, - compute_depth: bool = False, - calculation_limit: Optional[int] = None, - already_visited: Optional[VertexIdSet[T_vertex_id]] = None, - ) -> TraversalDepthFirstFlex[T_vertex, T_vertex_id, T_labels]: - _start_from_needs_traversal_object(self) - self._start_with_or_without_labels_from( - start_vertex, - start_vertices, - build_paths, - calculation_limit, - already_visited, - ) - self._compute_depth = compute_depth - self.depth = 0 - return self - - def _traverse(self) -> Iterator[T_vertex]: - # ----- Prepare efficient environment for inner loop ----- - # Copy Traversal attributes into method scope (faster access) - labeled_edges = self._labeled_edges - compute_depth = self._compute_depth - is_tree = self._is_tree - maybe_vertex_to_id = ( - None if self._vertex_to_id == vertex_as_id else self._vertex_to_id - ) # Case vertex_as_id: not apply; T_vertex_id > T_vertex - build_paths = self._build_paths - calculation_limit = self._calculation_limit - paths = self.paths - predecessors = self._predecessors - attributes = self._attributes - - # Prepare limit check done by zero check - if calculation_limit is not None: - calculation_limit += 1 # Allows for limit check by zero check - - # Copy _TraversalWithoutWeights attributes into method scope - edges_with_data = self._edges_with_data - next_edge_or_vertex = self._next_edge_or_vertex - visited = self.visited - - # Create booleans (avoid checks with "is", make decisions clear) - check_and_set_visited_on_expand = not is_tree - check_visited_when_seen = paths and not is_tree - # An alternative is, to choose here: not is_tree. - # This always checks and avoids taking already visited vertices - # on top of the stack. It saves space, but costs runtime. - # For an average of n children per node, it reduces the - # space needed by the stack by factor 1/(n/2), e.g. for 6 - # children, it reduces memory consumption to 2/3, i.e., by 1/3. - # But the runtime increase is seen as more important here, so the - # alternative is not chosen. The application can often just - # use TraversalNeighborsThenDepthFlex, that brings the advantage - # without the disadvantage. - - # Get references of used gear objects and methods (avoid attribute resolution) - visited_add = visited.add - ( - visited_uses_sequence, - visited_sequence, - visited_wrapper, - visited_uses_bits, - index_and_bit_method, - ) = access_to_vertex_set(visited) - ( - _, - predecessors_sequence, - predecessors_wrapper, - ) = access_to_vertex_mapping_expect_none(predecessors) - ( - _, - attributes_sequence, - attributes_wrapper, - ) = access_to_vertex_mapping_expect_none(attributes) - - # ----- Initialize method specific bookkeeping ----- - - depth = -1 # The inner loop starts with incrementing, so, we pre-decrement - if not compute_depth: - self.depth = depth # In this case, we leave the -1 the whole time - - # vertices to enter or leave - to_visit = self._gear.sequence_of_vertices(self._start_vertices) - to_visit_append = to_visit.append - to_visit_pop = to_visit.pop - - if compute_depth: - # Sequence of flag bytes (store in a Q array) marking the vertices to leave - # by 1 and the vertices to enter by 0. - # Initially, store a zero flag for each start vertex. - to_leave_markers = array.array("B", itertools.repeat(False, len(to_visit))) - to_leave_markers_pop = to_leave_markers.pop - to_leave_markers_append = to_leave_markers.append - else: - # the following is needed to replace the real depth with at least - # the information whether it is 0 oder higher. - # This does not work if we have no start vertices, but in this case, - # we can just stop working because we will not report results anyway - if len(to_visit) == 0: - return - top_start_vertex: Optional[T_vertex] = to_visit[-1] - - # ----- Typing preparation of inner loop (for details see BFS) ----- - - edge_or_vertex: Any # "Hole" in typing, but types "around" make it safe - neighbor: T_vertex # Re-establish type "after" the "hole" - data_of_edge: T_labels # Re-establish type "after" the "hole" - - # ----- Inner loop ----- - - while to_visit: - vertex = to_visit_pop() # Enter first added vertex first - if compute_depth: - depth += 1 - # (noinspection necessary due to bug PY-9479, also below...) - # noinspection PyUnboundLocalVariable - while to_leave_markers_pop(): - depth -= 1 # Got marker "leave a vertex", update depth - # Update external view on depth - self.depth = depth - # Store marker True: when reached, we are leaving a vertex - # noinspection PyUnboundLocalVariable - to_leave_markers_append(True) - else: - # if vertex is top_start_vertex: - # noinspection PyUnboundLocalVariable - if vertex is top_start_vertex: # "is" is used on purpose - depth = 0 # is start vertex - if to_visit: - top_start_vertex = to_visit[-1] # new top start vertex - else: - top_start_vertex = None - else: - depth = 1 # is no start vertex - - if depth: # Vertex is no start vertex, ready to expand: Report it now - if check_and_set_visited_on_expand: - # Ignore neighbor if already visited, and - # else include its ID in visited set. - v_id: T_vertex_id = ( - maybe_vertex_to_id(vertex) # type: ignore[assignment] - if maybe_vertex_to_id - else vertex - ) - # (If-nesting optimized for first case) - if not visited_uses_sequence: - # Standard implementation for "normal" MutableMapping - if v_id in visited: - continue - visited_add(v_id) - elif visited_uses_bits: - # Same as above, but with bits in byte sequence - sequence_key, bit_number = index_and_bit_method(v_id, 8) - bit_mask = 1 << bit_number - try: - value = visited_sequence[sequence_key] - if value & bit_mask: - continue - visited_sequence[sequence_key] = value | bit_mask - except IndexError: - visited_wrapper.extend_and_set(sequence_key, bit_mask) - else: - # Same as above, but with booleans in byte sequence - try: - if visited_sequence[v_id]: - continue - visited_sequence[v_id] = True - except IndexError: - visited_wrapper.extend_and_set(v_id, True) - - # (Note: Each seen vertex can enter the stack only once, because later - # seen identical vertices are blocked. The exception are start vertices, - # but they are not regarded here. Thus: "Each vertex is reported - # at most once.") - yield vertex - - if calculation_limit and not (calculation_limit := calculation_limit - 1): - raise RuntimeError("Number of visited vertices reached limit") - - for edge_or_vertex in next_edge_or_vertex(vertex, self): - neighbor = edge_or_vertex[0] if edges_with_data else edge_or_vertex - - if check_visited_when_seen or build_paths: - n_id: T_vertex_id = ( - maybe_vertex_to_id(neighbor) # type: ignore[assignment] - if maybe_vertex_to_id - else neighbor - ) - # Ignore neighbor if already visited/expanded, i.e., not put it - # onto the stack to_visit. - # If we do not create paths, this is just a variant of the - # algorithms: The results do not change, since visited vertices - # will not be visited again. - # But in case that we create paths, it is necessary: We like to - # store the predecessor of the neighbor directly here, when we first - # see the neighbor and have the predecessor still present. And we - # need to make sure not to overwrite the real predecessor of a - # vertex the has been already expanded. If - # the neighbor has not been visited, it is safe (and necessary) - # to overwrite a pre-existing predecessor of neighbor because - # a later found edge (predecessor, neighbor) will be evaluated - # first (to_visit is a stack) and so, this predecessor need to - # "win". - # (If-nesting optimized for first case) - if check_visited_when_seen and not visited_uses_sequence: - # Standard implementation for "normal" MutableMapping: - if n_id in visited: - continue - elif check_visited_when_seen and visited_uses_bits: - # Same as above, but with bits in byte sequence - sequence_key, bit_number = index_and_bit_method(n_id, 8) - try: - if visited_sequence[sequence_key] & (1 << bit_number): - continue - except IndexError: - pass - elif check_visited_when_seen: - # Same as above, but with booleans in byte sequence - try: - if visited_sequence[n_id]: - continue - except IndexError: - pass - if build_paths: - # Store the predecessor (vertex) of the neighbor - try: - predecessors_sequence[n_id] = vertex - except IndexError: - predecessors_wrapper.extend_and_set(n_id, vertex) - # Store the labels of the edge to the neighbor - if labeled_edges: - data_of_edge = edge_or_vertex[-1] - try: - attributes_sequence[n_id] = data_of_edge - except IndexError: - attributes_wrapper.extend_and_set(n_id, data_of_edge) - - # Needs to be visited, in stack order - to_visit_append(neighbor) - - if compute_depth: - # Store marker False: when reached, we are entering a vertex - to_leave_markers_append(False) - - -class TraversalDepthFirst( - Generic[T_vertex, T_labels], TraversalDepthFirstFlex[T_vertex, T_vertex, T_labels] -): - """ - Eases the use of `TraversalDepthFirstFlex` for typical cases. - For documentation of functionality and parameters, see there. - - Uses the following standard arguments for the respective parameters of - the parent class: - - - vertex_to_id = `vertex_as_id` - - gear = `GearDefault` - - `T_vertex_id` = `T_vertex` - - Implications: - - - `GearDefault` is used, see there how it and its superclass work - - T_vertex is bound to Hashable (T_vertex is used as `T_vertex_id`, see there) - """ - - def __init__( - self, - next_vertices: Optional[ - NextVertices[ - T_vertex, TraversalDepthFirstFlex[T_vertex, T_vertex, T_labels] - ] - ] = None, - *, - next_edges: Optional[ - NextEdges[T_vertex, TraversalDepthFirstFlex[T_vertex, T_vertex, T_labels]] - ] = None, - next_labeled_edges: Optional[ - NextLabeledEdges[ - T_vertex, - TraversalDepthFirstFlex[T_vertex, T_vertex, T_labels], - T_labels, - ] - ] = None, - is_tree: bool = False, - ) -> None: - super().__init__( - vertex_as_id, - GearDefault(), - next_vertices, - next_edges=next_edges, - next_labeled_edges=next_labeled_edges, - is_tree=is_tree, - ) - - -class TraversalNeighborsThenDepthFlex( - _TraversalWithoutWeightsDFS[T_vertex, T_vertex_id, T_labels] -): - """ - Bases: `Traversal` [`T_vertex`, `T_vertex_id`, `T_labels`] - - :param vertex_to_id: See `VertexToID` function. - - :param gear: See `gears API ` and class `GearWithoutDistances`. - - :param next_vertices: See `NextVertices` function. If None, provide next_edges - or next_labeled_edges. - - :param next_edges: See `NextEdges` function. Only allowed if next_vertices equals - None. If both are None, provide next_labeled_edges. - - :param next_labeled_edges: See `NextLabeledEdges` function. Only allowed if - next_vertices and next_edges equal None. If given, paths will record the given - labels. - - :param is_tree: bool: If it is certain, that during each traversal run, each vertex - can be reached only once, is_tree can be set to True. This improves - performance, but attribute *visited* of the traversal will not be updated - during and after the traversal. - - **Algorithm:** Variant of Depth First Search ("DFS"), non-recursive, based on stack, - vertices are reported when they are seen (neighbors read from the graph). - - **Properties:** Like `nographs.TraversalDepthFirst`, but first reports all - neighbors of the current vertex and then goes deeper. - A vertex is regarded as visited when it has been reported or if it is a start - vertex. - - **Input:** Directed graph. One or more start vertices. Vertices must be - hashable, or hashable id can be provided. Unlabeled or labeled edges. Optional - calculation limit. - - **Search state:** When a vertex is - *expanded* (traversal calls next_vertices, next_edges or next_labeled_edges) - or *reported* (an iterator of the traversal returns it), - the traversal provides updated values for the attributes - *depth*, *paths*, and *visited*. - """ - - def __init__( - self, - vertex_to_id: VertexToID[T_vertex, T_vertex_id], - gear: GearWithoutDistances[T_vertex, T_vertex_id, T_labels], - next_vertices: Optional[ - NextVertices[ - T_vertex, - TraversalNeighborsThenDepthFlex[T_vertex, T_vertex_id, T_labels], - ] - ] = None, - *, - next_edges: Optional[ - NextEdges[ - T_vertex, - TraversalNeighborsThenDepthFlex[T_vertex, T_vertex_id, T_labels], - ] - ] = None, - next_labeled_edges: Optional[ - NextLabeledEdges[ - T_vertex, - TraversalNeighborsThenDepthFlex[T_vertex, T_vertex_id, T_labels], - T_labels, - ] - ] = None, - is_tree: bool = False, - ) -> None: - ( - self._next_edge_or_vertex, - edges_with_data, - labeled_edges, - ) = _create_unified_next(next_vertices, next_edges, next_labeled_edges) - super().__init__(edges_with_data, labeled_edges, is_tree, vertex_to_id, gear) - self.depth: int = -1 # value not used - """ If depth computation has been demanded: - At this *search depth*, the reported (resp. the expanded) vertex has been - found. It equals the length (number of edges) of the created path to the - vertex, if path creation is demanded. - Note: The search depth does not need to be the depth of the vertex - (see `TraversalBreadthFirstFlex`). - When a traversal has been started, but no vertex has been reported or expanded - so far, the depth is 0 (depth of the start vertices). - """ - self._compute_depth = False # value not used - - def start_from( - self, - start_vertex: Optional[T_vertex] = None, - *, - start_vertices: Optional[Iterable[T_vertex]] = None, - build_paths: bool = False, - compute_depth: bool = False, - calculation_limit: Optional[int] = None, - already_visited: Optional[VertexIdSet[T_vertex_id]] = None, - ) -> TraversalNeighborsThenDepthFlex[T_vertex, T_vertex_id, T_labels]: - _start_from_needs_traversal_object(self) - self._start_with_or_without_labels_from( - start_vertex, - start_vertices, - build_paths, - calculation_limit, - already_visited, - ) - self.depth = 0 - self._compute_depth = compute_depth - return self - - def _traverse(self) -> Iterator[T_vertex]: - # ----- Prepare efficient environment for inner loop ----- - # Copy Traversal attributes into method scope (faster access) - labeled_edges = self._labeled_edges - compute_depth = self._compute_depth - is_tree = self._is_tree - maybe_vertex_to_id = ( - None if self._vertex_to_id == vertex_as_id else self._vertex_to_id - ) # Case vertex_as_id: not apply; T_vertex_id > T_vertex - build_paths = self._build_paths - calculation_limit = self._calculation_limit - predecessors = self._predecessors - attributes = self._attributes - - # Prepare limit check done by zero check - if calculation_limit is not None: - calculation_limit += 1 # Allows for limit check by zero check - - # Copy TraversalWithoutWeights attributes into method scope - edges_with_data = self._edges_with_data - next_edge_or_vertex = self._next_edge_or_vertex - visited = self.visited - - # Get references of used gear objects and methods (avoid attribute resolution) - visited_add = visited.add - ( - visited_uses_sequence, - visited_sequence, - visited_wrapper, - visited_uses_bits, - visited_index_and_bit_method, - ) = access_to_vertex_set(visited) - ( - _, - predecessors_sequence, - predecessors_wrapper, - ) = access_to_vertex_mapping_expect_none(predecessors) - ( - _, - attributes_sequence, - attributes_wrapper, - ) = access_to_vertex_mapping_expect_none(attributes) - - # ----- Initialize method specific bookkeeping ----- - - depth = -1 # The inner loop starts with incrementing, so, we pre-decrement - if not compute_depth: - self.depth = depth # In this case, we leave the -1 the whole time - prev_traversal = copy.copy(self) # copy of self, for keeping previous depth - - # vertices to expand - to_expand = self._gear.sequence_of_vertices(self._start_vertices) - to_expand_append = to_expand.append - to_expand_pop = to_expand.pop - - if compute_depth: - # Sequence of flag bytes (store in a Q array) marking the vertices to leave - # by 1 and the vertices to enter (these are in to_expand) by 0. - # Initially, store a zero flag for each start vertex. - to_leave_marker = array.array("B", itertools.repeat(False, len(to_expand))) - to_leave_marker_pop = to_leave_marker.pop - to_leave_marker_append = to_leave_marker.append - - # ----- Typing preparation of inner loop (for details see BFS) ----- - - edge_or_vertex: Any # "Hole" in typing, but types "around" make it safe - neighbor: T_vertex # Re-establish type "after" the "hole" - data_of_edge: T_labels # Re-establish type "after" the "hole" - - # ----- Inner loop ----- - - while to_expand: - vertex = to_expand_pop() # Enter first added vertex first - if compute_depth: - depth += 1 - # noinspection PyUnboundLocalVariable - while to_leave_marker_pop(): - depth -= 1 # Got marker "leave a vertex", update depth - # Update external view on depth - prev_traversal.depth = depth - self.depth = depth + 1 - # Store marker True: when reached, we are leaving a vertex - # noinspection PyUnboundLocalVariable - to_leave_marker_append(True) - - if calculation_limit and not (calculation_limit := calculation_limit - 1): - raise RuntimeError("Number of visited vertices reached limit") - - for edge_or_vertex in next_edge_or_vertex(vertex, prev_traversal): - neighbor = edge_or_vertex[0] if edges_with_data else edge_or_vertex - - if not is_tree or build_paths: - n_id: T_vertex_id = ( - maybe_vertex_to_id(neighbor) # type: ignore[assignment] - if maybe_vertex_to_id - else neighbor - ) - # Ignore neighbor if already visited/expanded, i.e., not put it - # onto the stack to_expand. - # (If-nesting optimized for first case) - if not is_tree and not visited_uses_sequence: - # Standard implementation for "normal" MutableMapping: - if n_id in visited: - continue - visited_add(n_id) - elif not is_tree and visited_uses_bits: - # Same as above, but with bits in byte sequence - sequence_key, bit_number = visited_index_and_bit_method(n_id, 8) - bit_mask = 1 << bit_number - try: - value = visited_sequence[sequence_key] - if value & bit_mask: - continue - visited_sequence[sequence_key] = value | bit_mask - except IndexError: - visited_wrapper.extend_and_set(sequence_key, bit_mask) - elif not is_tree: - # Same as above, but with booleans in byte sequence - try: - if visited_sequence[n_id]: - continue - visited_sequence[n_id] = True - except IndexError: - visited_wrapper.extend_and_set(n_id, True) - if build_paths: - # Store the predecessor (vertex) of the neighbor - try: - predecessors_sequence[n_id] = vertex - except IndexError: - predecessors_wrapper.extend_and_set(n_id, vertex) - # Store the labels of the edge to the neighbor - if labeled_edges: - data_of_edge = edge_or_vertex[-1] - try: - attributes_sequence[n_id] = data_of_edge - except IndexError: - attributes_wrapper.extend_and_set(n_id, data_of_edge) - - yield neighbor - - # Needs to be expanded, in stack order - to_expand_append(neighbor) - - if compute_depth: - # Store marker False: when reached, we are entering a vertex - to_leave_marker_append(False) - - -class TraversalNeighborsThenDepth( - Generic[T_vertex, T_labels], - TraversalNeighborsThenDepthFlex[T_vertex, T_vertex, T_labels], -): - """ - Eases the use of `TraversalNeighborsThenDepthFlex` for typical cases. - For documentation of functionality and parameters, see there. - - Uses the following standard arguments for the respective parameters of - the parent class: - - - vertex_to_id = `vertex_as_id` - - gear = `GearDefault` - - `T_vertex_id` = `T_vertex` - - Implications: - - - `GearDefault` is used, see there how it and its superclass work - - T_vertex is bound to Hashable (T_vertex is used as `T_vertex_id`, see there) - """ - - def __init__( - self, - next_vertices: Optional[ - NextVertices[ - T_vertex, TraversalNeighborsThenDepthFlex[T_vertex, T_vertex, T_labels] - ] - ] = None, - *, - next_edges: Optional[ - NextEdges[ - T_vertex, TraversalNeighborsThenDepthFlex[T_vertex, T_vertex, T_labels] - ] - ] = None, - next_labeled_edges: Optional[ - NextLabeledEdges[ - T_vertex, - TraversalNeighborsThenDepthFlex[T_vertex, T_vertex, T_labels], - T_labels, - ] - ] = None, - is_tree: bool = False, - ) -> None: - super().__init__( - vertex_as_id, - GearDefault(), - next_vertices, - next_edges=next_edges, - next_labeled_edges=next_labeled_edges, - is_tree=is_tree, - ) - - -class TraversalTopologicalSortFlex( - _TraversalWithoutWeightsBasic[T_vertex, T_vertex_id, T_labels] -): - """ - Bases: `Traversal` [`T_vertex`, `T_vertex_id`, `T_labels`] - - :param vertex_to_id: See `VertexToID` function. - - :param gear: See `gears API ` and class `GearWithoutDistances`. - - :param next_vertices: See `NextVertices` function. If None, provide next_edges - or next_labeled_edges. - - :param next_edges: See `NextEdges` function. Only allowed if next_vertex equals - None. If both are None, provide next_labeled_edges. - - :param next_labeled_edges: See `NextLabeledEdges` function. Only allowed if - next_vertices and next_edges equal None. If given, paths will record the given - labels. - - :param is_tree: bool: If it is certain, that during each traversal run, each vertex - can be reached only once, is_tree can be set to True. This improves performance, - but attribute *visited* of the traversal will not be updated during and after - the traversal. - - **Algorithm:** Topological Search, non-recursive, based on stack, vertices are - reported when they "are left" for backtracking. - - **Properties:** Vertices are reported in topological ordering, i.e. a linear - ordering of the vertices such that for every directed edge *uv* from vertex *u* to - vertex *v* ("*u* depends on *v*"), *v* comes before *u* in the ordering. If the - graph contains a cycle that can be reached within the sorting process, a - RuntimeError exception is raised and a cyclic path from a start vertex is provided. - - Vertices are expanded following the strategy `nographs.TraversalDepthFirst`. - - For topological search, a vertex is regarded as visited when it has been expanded - (see search state) or if it is a start vertex. Note this difference in comparison - to other traversals that mark a vertex as visited when it is reported. - - **Input:** Directed graph. One or more start vertices. Vertices must be - hashable, or hashable id can be provided. Unlabeled or labeled edges. Optional - calculation limit. - - **Search state:** When a vertex is - *expanded* (traversal calls next_vertices, next_edges or next_labeled_edges) - or *reported* (an iterator of the traversal returns it), - the traversal provides updated values for the attributes - *depth*, *paths*, and *visited*. - """ - - def __init__( - self, - vertex_to_id: VertexToID[T_vertex, T_vertex_id], - gear: GearWithoutDistances[T_vertex, T_vertex_id, T_labels], - next_vertices: Optional[ - NextVertices[ - T_vertex, TraversalTopologicalSortFlex[T_vertex, T_vertex_id, T_labels] - ] - ] = None, - *, - next_edges: Optional[ - NextEdges[ - T_vertex, TraversalTopologicalSortFlex[T_vertex, T_vertex_id, T_labels] - ] - ] = None, - next_labeled_edges: Optional[ - NextLabeledEdges[ - T_vertex, - TraversalTopologicalSortFlex[T_vertex, T_vertex_id, T_labels], - T_labels, - ] - ] = None, - is_tree: bool = False, - ) -> None: - ( - self._next_edge_or_vertex, - edges_with_data, - labeled_edges, - ) = _create_unified_next(next_vertices, next_edges, next_labeled_edges) - super().__init__(edges_with_data, labeled_edges, is_tree, vertex_to_id, gear) - # the following values are not used, and initialized during traversal - self.depth: int = -1 - """ At this *search depth*, the reported (resp. the expanded) vertex has been - found. It equals the length (number of edges) of the created path to the - vertex, if path creation is demanded. - Note: The search depth does not need to be the depth of the vertex - (see `TraversalBreadthFirstFlex`). - When a traversal has been started, but no vertex has been reported or expanded - so far, the depth is 0 (depth of the start vertices). - """ - self.cycle_from_start: list[T_vertex] = [] - """ If the graph contains a cycle that can be reached within the sorting - process, a RuntimeError exception is raised, and the traversal provides - a cyclic path from a start vertex in attribute cycle_from_start.""" - - def start_from( - self, - start_vertex: Optional[T_vertex] = None, - *, - start_vertices: Optional[Iterable[T_vertex]] = None, - build_paths: bool = False, - calculation_limit: Optional[int] = None, - already_visited: Optional[VertexIdSet[T_vertex_id]] = None, - ) -> TraversalTopologicalSortFlex[T_vertex, T_vertex_id, T_labels]: - """ - Start the traversal at a vertex or a set of vertices and set parameters. - - :param start_vertex: The vertex the search should start at. If None, provide - start_vertices. - - :param start_vertices: The vertices the search should start at. Only - allowed if start_vertex equals None. - - :param build_paths: If true, build paths from some start vertex to each visited - vertex. - - :param calculation_limit: If provided, maximal number of vertices to process - (read in) from your graph. If it is exceeded, a RuntimeError will be raised. - - :param already_visited: If provided, this set is used instead of an internal - one to keep vertices (resp. their hashable ids from vertex_to_id), - that have already been visited. This parameter can be used to get online - access to the internal bookkeeping of visited vertices, or to preload - vertices that should never be visited. - - Attention: TraversalTopologicalSortFlex requires, that the collection - given as argument for parameter already_visited is compatible - (in any sense) with the collection that gear.vertex_id_set() - returns. If you have chosen GearDefault, you can just use a dict. - Otherwise, create the collection by calling gear.vertex_id_set() or use the - collection that another traversal with the same gear gives as attribute - visited. - - :return: Traversal, that has been started, e.g., statements like *iter()*, - *next()*, *for* and the methods "go*" of the Traversal can now be used. - """ - - _start_from_needs_traversal_object(self) - self._start_with_or_without_labels_from( - start_vertex, - start_vertices, - build_paths, - calculation_limit, - already_visited, - ) - self.depth = 0 - return self - - def _traverse(self) -> Iterator[T_vertex]: - # ----- Prepare efficient environment for inner loop ----- - # Copy Traversal attributes into method scope (faster access) - labeled_edges = self._labeled_edges - is_tree = self._is_tree - maybe_vertex_to_id = ( - None if self._vertex_to_id == vertex_as_id else self._vertex_to_id - ) # Case vertex_as_id: not apply; T_vertex_id > T_vertex - build_paths = self._build_paths - calculation_limit = self._calculation_limit - predecessors = self._predecessors - attributes = self._attributes - - # Prepare limit check done by zero check - if calculation_limit is not None: - calculation_limit += 1 # allows for limit check by zero check - - # Copy _TraversalWithoutWeights attributes into method scope - edges_with_data = self._edges_with_data - next_edge_or_vertex = self._next_edge_or_vertex - visited = self.visited - - # Get references of used gear objects and methods (avoid attribute resolution) - visited_add = visited.add - ( - visited_uses_sequence, - visited_sequence, - visited_wrapper, - visited_uses_bits, - visited_index_and_bit_method, - ) = access_to_vertex_set(visited) - ( - predecessors_uses_sequence, - predecessors_sequence, - predecessors_wrapper, - ) = access_to_vertex_mapping_expect_none(predecessors) - ( - attributes_uses_sequence, - attributes_sequence, - attributes_wrapper, - ) = access_to_vertex_mapping_expect_none(attributes) - - # ----- Typing preparation of inner loop (for details see DFS) ----- - - edge_or_vertex: Any # "Hole" in typing, but types "around" make it safe - neighbor: T_vertex # Re-establish type "after" the "hole" - data_of_edge: T_labels # Re-establish type "after" the "hole" - - # Two separate implementations for the cases is_tree and not is_tree that follow - # different concepts, because a combined approach makes both cases significantly - # slower - if is_tree: - # Since the graph is a tree, we need no cycle check and no skipping of - # already seen vertices. - # We just use a stack for vertices we have to enter or leave, and store - # None on top of the vertices we need to enter in order to differentiate - # the two cases. - - # ----- Initialize specific bookkeeping ----- - - self.cycle_from_start = [] - # Sequence used as stack of vertices that we need to enter & expand (if - # it is not on the trace, see below) or leave & report (otherwise) - to_expand_or_leave = self._gear.sequence_of_vertices(self._start_vertices) - - # Sequence of flag bytes (store in a Q array) marking the vertices to leave - # by 1 and the vertices to enter by 0. - # Initially, store a zero flag for each start vertex. - to_leave_markers = array.array( - "B", itertools.repeat(False, len(to_expand_or_leave)) - ) - - # Get method references of specific bookkeeping (avoid attribute resolution) - to_expand_or_leave_append = to_expand_or_leave.append - to_expand_or_leave_pop = to_expand_or_leave.pop - to_leave_markers_pop = to_leave_markers.pop - to_leave_markers_append = to_leave_markers.append - - # ----- Inner loop ----- - - while to_expand_or_leave: - if to_leave_markers[-1]: - # Vertex is to be left: We "leave" and report it, and remove marker - self.depth -= 1 - to_leave_markers_pop() - vertex = to_expand_or_leave_pop() # handle last added vertex first - yield vertex - continue - - # Vertex is to be expanded: We "expand" it, - # but leave it on the stack, so that it will be "left" later on. - vertex = to_expand_or_leave[-1] - # We change the marker in order to remember that we will have to - # leave (not enter) it - to_leave_markers[-1] = True - - if calculation_limit and not ( - calculation_limit := calculation_limit - 1 - ): - raise RuntimeError("Number of visited vertices reached limit") - - for edge_or_vertex in next_edge_or_vertex(vertex, self): - neighbor = edge_or_vertex[0] if edges_with_data else edge_or_vertex - - n_id: T_vertex_id = ( - maybe_vertex_to_id(neighbor) # type: ignore[assignment] - if maybe_vertex_to_id - else neighbor - ) - - if build_paths: - # We have to store the predecessor here, because at time of - # visit, it is already lost. And we cannot yield here, - # because only the first of the neighbors will indeed be - # visited next. - # But since we are in a tree, no other predecessor can - # be stored for that vertex later on. - - # Store the predecessor (vertex) of the neighbor - try: - predecessors_sequence[n_id] = vertex - except IndexError: - predecessors_wrapper.extend_and_set(n_id, vertex) - # Store the labels of the edge to the neighbor - if labeled_edges: - data_of_edge = edge_or_vertex[-1] - try: - attributes_sequence[n_id] = data_of_edge - except IndexError: - attributes_wrapper.extend_and_set(n_id, data_of_edge) - - # Put vertex on the stack - to_expand_or_leave_append(neighbor) - # Remember that we have to expand it - to_leave_markers_append(False) - - self.depth += 1 - - else: # not is_tree - # Since the graph is not guaranteed to be a tree, we need a cycle check and - # need to skip already seen vertices. - # For detecting cycles, we store the vertices, that are on the current - # path, in a set. We use a stack for storing the vertices we have to - # enter or leave (when such a vertex is in the path set, we need to leave - # the vertex). - - # ----- Initialize specific bookkeeping ----- - - self.cycle_from_start = [] - - # Sequence used as stack of vertices that we need to enter & expand (if - # it is not on the trace, see below) or leave & report (otherwise) - to_enter_or_leave = self._gear.sequence_of_vertices(self._start_vertices) - - # Set of vertices along the current path - # (We need this for fast cycle detection. We could use additionally - # a trace sequence to speed up the check if the current vertex is - # the top vertex of the trace instead of checking if it is "in" the - # trace, but this would cost maintenance runtime and memory for the - # sequence). - trace_set = self._gear.vertex_id_set(()) - ( - trace_set_uses_sequence, - trace_set_sequence, - trace_set_wrapper, - trace_set_uses_bits, - trace_set_index_and_bit_method, - ) = access_to_vertex_set(trace_set) - - # Check compatibility of visited and trace_set. It is used for - # performance optimization later on. - assert ( - visited_uses_sequence == trace_set_uses_sequence - and visited_uses_bits == trace_set_uses_bits - ), ( - "Collection already_visited is incompatible " - + "with gear.sequence_of_vertices" - ) - sets_use_sequences = visited_uses_sequence - del visited_uses_sequence, trace_set_uses_sequence - sets_use_bits = visited_uses_bits - del visited_uses_bits, trace_set_uses_bits - if sets_use_sequences and sets_use_bits: - assert visited_index_and_bit_method is trace_set_index_and_bit_method, ( - "Collection already_visited is incompatible " - + "with gear.sequence_of_vertices" - ) - sets_index_and_bit_method = visited_index_and_bit_method - del visited_index_and_bit_method, trace_set_index_and_bit_method - - # Get method references of specific bookkeeping (avoid attribute resolution) - to_visit_pop = to_enter_or_leave.pop - to_visit_append = to_enter_or_leave.append - trace_set_add = trace_set.add - trace_set_discard = trace_set.discard - - # ----- Inner loop ----- - - while to_enter_or_leave: - vertex = to_enter_or_leave[-1] # visit/report last added vertex first - v_id: T_vertex_id = ( - maybe_vertex_to_id(vertex) # type: ignore[assignment] - if maybe_vertex_to_id - else vertex - ) - - if not sets_use_sequences: - # Standard implementation for "normal" MutableSet - if v_id in trace_set: - # Back to trace, from visits/reports of further vertices, - # that trace vertices depend on: We "leave" and report the head - # vertex of the trace - # (A note about the above "in" check: - # If v_id is in the set, it needs to be - # the last id added there. But this does not help us, since - # sets are not ordered as dicts nowadays are). - self.depth -= 1 - to_visit_pop() - trace_set_discard(v_id) - yield vertex - continue - # Ignore v_id if visited, else include vertex n_id in visited set. - # Ignore this precondition for trees and start vertices. - if self.depth > 0: - # Standard implementation for "normal" MutableMapping - if v_id in visited: - to_visit_pop() - continue - visited_add(v_id) - # Now, vertex belongs to trace from start. As long as this is so, - # seeing it as neighbor would be a cycle. - trace_set_add(v_id) - - elif sets_use_bits: - # Same as above, but with bits in byte sequence - sequence_key, bit_number = sets_index_and_bit_method(v_id, 8) - bit_mask = 1 << bit_number - try: - value = trace_set_sequence[sequence_key] - if value & bit_mask: - self.depth -= 1 - to_visit_pop() - trace_set_sequence[sequence_key] = value - bit_mask - yield vertex - continue - except IndexError: - pass - if self.depth > 0: - try: - value = visited_sequence[sequence_key] - if value & bit_mask: - to_visit_pop() - continue - visited_sequence[sequence_key] = value + bit_mask - except IndexError: - visited_wrapper.extend_and_set(sequence_key, bit_mask) - try: - trace_set_sequence[sequence_key] += bit_mask - except IndexError: - trace_set_wrapper.extend_and_set(sequence_key, bit_mask) - - else: - # Same as above, but with booleans in byte sequence - try: - if trace_set_sequence[v_id]: - self.depth -= 1 - to_visit_pop() - trace_set_sequence[v_id] = False - yield vertex - continue - except IndexError: - pass - if self.depth > 0: - try: - if visited_sequence[v_id]: - to_visit_pop() - continue - visited_sequence[v_id] = True - except IndexError: - visited_wrapper.extend_and_set(v_id, True) - try: - trace_set_sequence[v_id] = True - except IndexError: - trace_set_wrapper.extend_and_set(v_id, True) - - # We "expand" the vertex - if calculation_limit and not ( - calculation_limit := calculation_limit - 1 - ): - raise RuntimeError("Number of visited vertices reached limit") - - for edge_or_vertex in next_edge_or_vertex(vertex, self): - neighbor = edge_or_vertex[0] if edges_with_data else edge_or_vertex - - n_id2: T_vertex_id = ( - maybe_vertex_to_id(neighbor) # type: ignore[assignment] - if maybe_vertex_to_id - else neighbor - ) - - # If neighbor is already visited and in trace_set: cycle found - # If neighbor is already visited: ignore it (in case of path - # generation, this is necessary, and otherwise, it is a small - # optimization.) - if not sets_use_sequences: - # Standard implementation for "normal" MutableSet - if n_id2 in visited: - if n_id2 in trace_set: - # We found a dependency (edge) back to a vertex, whose - # dependencies we are currently following (trace). We - # build and report this trace: a cycle. - self._report_cycle( - neighbor, - to_enter_or_leave, - trace_set, - maybe_vertex_to_id, - ) - # Visited but does not close a cycle: ignore it - continue - elif sets_use_bits: - # Same as above, but with bits in byte sequence - sequence_key, bit_number = sets_index_and_bit_method(n_id2, 8) - bit_mask = 1 << bit_number - try: - value = visited_sequence[sequence_key] - if value & bit_mask: - try: - if trace_set_sequence[sequence_key] & bit_mask: - self._report_cycle( - neighbor, - to_enter_or_leave, - trace_set, - maybe_vertex_to_id, - ) - except IndexError: - # In order to become visited, a vertex needs to - # get into the trace set and then be discarded - # from it. Thus, if a vertex is visited, the - # trace set sequence already has the necessary - # length to store it. So, no IndexError can happen - # here. - raise AssertionError( - "Internal error in TS" - ) # pragma: no cover - continue - except IndexError: - pass - else: - # Same as above, but with booleans in byte sequence - try: - if visited_sequence[n_id2]: - try: - if trace_set_sequence[n_id2]: - self._report_cycle( - neighbor, - to_enter_or_leave, - trace_set, - maybe_vertex_to_id, - ) - except IndexError: - # See above case for the reason for the pragma - raise AssertionError( - "Internal error in TS" - ) # pragma: no cover - continue - except IndexError: - pass - - if build_paths: - # We have to store the predecessor here, because at time of - # visit, it is already lost. And we cannot yield here, - # because TopologicalSorted reports not until leaving vertices. - # But since the visiting order is defined by a stack we know - # that for each vertex, the predecessor stored last is the - # edge visited first, and after that no other predecessor can - # be stored for that vertex. - try: - predecessors_sequence[n_id2] = vertex - except IndexError: - predecessors_wrapper.extend_and_set(n_id2, vertex) - if labeled_edges: - data_of_edge = edge_or_vertex[-1] - try: - attributes_sequence[n_id2] = data_of_edge - except IndexError: - attributes_wrapper.extend_and_set(n_id2, data_of_edge) - - # Needs to be visited, in stack order - to_visit_append(neighbor) - - # Update depth. The following vertices are one level deeper. - self.depth += 1 - - def _report_cycle( - self, - neighbor: T_vertex, - to_visit: MutableSequenceOfVertices[T_vertex], - trace_set: VertexIdSet[T_vertex_id], - maybe_vertex_to_id: Optional[VertexToID[T_vertex, T_vertex_id]], - ) -> None: - trace = list() - for vertex in to_visit: - v_id2: T_vertex_id = ( - maybe_vertex_to_id(vertex) # type: ignore[assignment] - if maybe_vertex_to_id - else vertex - ) - if v_id2 in trace_set: - trace.append(vertex) - trace.append(neighbor) - self.cycle_from_start = trace - raise RuntimeError("Graph contains cycle") - - -class TraversalTopologicalSort( - Generic[T_vertex, T_labels], - TraversalTopologicalSortFlex[T_vertex, T_vertex, T_labels], -): - """ - Eases the use of `TraversalTopologicalSortFlex` for typical cases. - For documentation of functionality and parameters, see there. - - Uses the following standard arguments for the respective parameters of - the parent class: - - - vertex_to_id = `vertex_as_id` - - gear = `GearDefault` - - `T_vertex_id` = `T_vertex` - - Implications: - - - `GearDefault` is used, see there how it and its superclass work - - T_vertex is bound to Hashable (T_vertex is used as `T_vertex_id`, see there) - """ - - def __init__( - self, - next_vertices: Optional[ - NextVertices[ - T_vertex, TraversalTopologicalSortFlex[T_vertex, T_vertex, T_labels] - ] - ] = None, - *, - next_edges: Optional[ - NextEdges[ - T_vertex, TraversalTopologicalSortFlex[T_vertex, T_vertex, T_labels] - ] - ] = None, - next_labeled_edges: Optional[ - NextLabeledEdges[ - T_vertex, - TraversalTopologicalSortFlex[T_vertex, T_vertex, T_labels], - T_labels, - ] - ] = None, - is_tree: bool = False, - ) -> None: - super().__init__( - vertex_as_id, - GearDefault(), - next_vertices, - next_edges=next_edges, - next_labeled_edges=next_labeled_edges, - is_tree=is_tree, - ) - - -# --------------- Traversal strategies for weighted edges ------------- - - -class _TraversalWithWeights( - Generic[T_vertex, T_vertex_id, T_weight, T_labels], - Traversal[T_vertex, T_vertex_id, T_labels], - ABC, -): - """A Traversal that needs weighted edges and uses a gear suitable for this.""" - - def __init__( - self, - labeled_edges: bool, - is_tree: bool, - vertex_to_id: VertexToID[T_vertex, T_vertex_id], - gear: Gear[T_vertex, T_vertex_id, T_weight, T_labels], - ) -> None: - self._gear = gear - super().__init__(labeled_edges, is_tree, vertex_to_id) - - -class _TraversalWithDistances( - _TraversalWithWeights[T_vertex, T_vertex_id, T_weight, T_labels], - ABC, -): - """ - A _TraversalWithWeights that provides a distances collection as part of - its state. - """ - - def __init__( - self, - labeled_edges: bool, - is_tree: bool, - vertex_to_id: VertexToID[T_vertex, T_vertex_id], - gear: Gear[T_vertex, T_vertex_id, T_weight, T_labels], - ) -> None: - super().__init__(labeled_edges, is_tree, vertex_to_id, gear) - - self.distances: VertexIdToDistanceMapping[T_vertex_id, T_weight] = ( - NoDistancesMapping[T_vertex_id, T_weight]() - ) - """ Provisional or final distance values of some vertices - (distance from a start vertex). Without option *keep_distances*, - the value for a vertex is removed once the vertex has been reported. With - option *keep_distances*, values are never removed, and that means: During a - traversal, the distance values for already reported vertices can be found in - the collection. After an exhaustive search, the collection contains exactly - and only the distances of all vertices that are reachable from the start - vertices and of the start vertices themselves. - """ - - def _improve_state( - self, state: dict[str, Any], vertices: Optional[Iterable[T_vertex]] = None - ) -> None: - # Assignments in distances are only valid for reported vertices. Thus, - # we need to convert only keys/values for requested vertices to a string, - # not the whole MutableMapping. So, we special case this attribute here. - del state["distances"] - if vertices is not None: - vertex_to_id, distances = self._vertex_to_id, self.distances - state["distances"] = StrRepr.from_iterable( - (v_id := vertex_to_id(vertex), distances[v_id]) for vertex in vertices - ) - super()._improve_state(state, vertices) - - -class _TraversalWithDistance( - _TraversalWithDistances[T_vertex, T_vertex_id, T_weight, T_labels], - ABC, -): - """ - A _TraversalWithDistances that provides a distance as part of its staste. - If offers the go_for_distance_range method based on the distance. - """ - - def __init__( - self, - labeled_edges: bool, - is_tree: bool, - vertex_to_id: VertexToID[T_vertex, T_vertex_id], - gear: Gear[T_vertex, T_vertex_id, T_weight, T_labels], - ) -> None: - super().__init__(labeled_edges, is_tree, vertex_to_id, gear) - - # The following value is not used by NoGraphs. It is only set - # to have some initialization. - self.distance: T_weight = self._gear.infinity() - """ The length of the shortest path (sum of edge weights) from a - start vertex to the visited vertex - """ - - def go_for_distance_range(self, start: Real, stop: Real) -> Iterator[T_vertex]: - """ - For a started traversal, return an iterator. During the traversal, - the iterator skips vertices as long as their distance is lower than *start*. - From then on, is reports the found vertices. It stops when the reached - distance is equal to or higher than *stop*. - - Note: The first vertex with a distance equal or higher than stop will be - consumed from the traversal, but will not be reported, so it is lost (compare - itertools.takewhile). - """ - if not isinstance(type(self), type(Traversal)): - raise RuntimeError( - "Method go_for_distance_range can only be called " - + "on a Traversal object." - ) - - # In order to make the above check work, the following generator functionality - # needs to be encapsulated in a local function - def my_generator() -> Iterator[T_vertex]: - for v in self._generator: - if self.distance >= start: - if self.distance < stop: - yield v - break - for v in self._generator: - if self.distance >= stop: - break - yield v - - return my_generator() - - -class TraversalShortestPathsFlex( - _TraversalWithDistance[T_vertex, T_vertex_id, T_weight, T_labels] -): - """ - | Bases: Generic[`T_vertex`, `T_vertex_id`, `T_weight`, `T_labels`], - | `Traversal` [`T_vertex`, `T_vertex_id`, `T_labels`] - - :param vertex_to_id: See `VertexToID` function. - - :param gear: See `gears API ` and class `Gear`. - - :param next_edges: See `NextWeightedEdges` function. If None, provide - next_labeled_edges. - - :param next_labeled_edges: See `NextWeightedLabeledEdges` function. Only allowed - if next_edges equals None. If given, paths will record the given labels. - - :param is_tree: bool: If it is certain, that during each traversal run, each vertex - can be reached only once, is_tree can be set to True. This improves performance, - but attribute *distances* of the traversal will not be updated during and after - the traversal. - - **Algorithm:** Shortest paths algorithm of Dijkstra, non-recursive, based on heap. - - **Properties:** Vertices are reported (and expanded) ordered by increasing distance - (minimally necessary sum of edge weights) from a start vertex. - - **Input:** Weighted directed graph. One or more start vertices. Vertices must be - hashable, or hashable id can be provided. Weights need to be non-negative. - Optional calculation limit. - - **Search state:** When a vertex is *expanded* (traversal calls next_edges or - next_labeled_edges) or *reported* (an iterator of the traversal returns it), - the traversal provides updated values for the attributes - *distance*, *depth*, *paths*, and *distances*. - """ - - def __init__( - self, - vertex_to_id: VertexToID[T_vertex, T_vertex_id], - gear: Gear[T_vertex, T_vertex_id, T_weight, T_labels], - next_edges: Optional[ - NextWeightedEdges[ - T_vertex, - TraversalShortestPathsFlex[T_vertex, T_vertex_id, T_weight, T_labels], - T_weight, - ] - ] = None, - *, - next_labeled_edges: Optional[ - NextWeightedLabeledEdges[ - T_vertex, - TraversalShortestPathsFlex[T_vertex, T_vertex_id, T_weight, T_labels], - T_weight, - T_labels, - ] - ] = None, - is_tree: bool = False, - ) -> None: - self._next_edges, labeled_edges = _create_unified_next_weighted( - next_edges, next_labeled_edges - ) - super().__init__(labeled_edges, is_tree, vertex_to_id, gear) - - self._known_distances: Optional[ - VertexIdToDistanceMapping[T_vertex_id, T_weight] - ] = None - self._keep_distances = False - - # The following value is not used by NoGraphs. It is only set - # to have some initialization. - self.depth: int = -1 - """ At this *search depth*, the reported (resp. the expanded) vertex has been - found. It equals the length (number of edges) of the created path to the - vertex, if path creation is demanded. - Note: The search depth does not need to be the depth of the vertex - (see `TraversalBreadthFirstFlex`). - When a traversal has been started, but no vertex has been reported or expanded - so far, the depth is 0 (depth of the start vertices). - """ - self._start_vertices_and_ids = tuple[tuple[T_vertex, T_vertex_id]]() - - def start_from( - self, - start_vertex: Optional[T_vertex] = None, - *, - start_vertices: Optional[Iterable[T_vertex]] = None, - build_paths: bool = False, - calculation_limit: Optional[int] = None, - keep_distances: bool = False, - known_distances: Optional[ - VertexIdToDistanceMapping[T_vertex_id, T_weight] - ] = None, - ) -> TraversalShortestPathsFlex[T_vertex, T_vertex_id, T_weight, T_labels]: - """ - Start the traversal at a vertex or a set of vertices and set parameters. - - :param start_vertex: The vertex the search should start at. If None, provide - start_vertices. - - :param start_vertices: The set of vertices the search should start - at. Only allowed if start_vertex equals None. - - :param build_paths: If true, build paths from start vertices for each reported - vertex, and an empty path for each start vertex. - - :param calculation_limit: If provided, maximal number of vertices to process - (read in) from your graph. If it is exceeded, a RuntimeError will be raised. - - :param keep_distances: If True, the found distances of vertices are - collected in traversal attribute distances, and not deleted after - having reported the vertex. See attribute distances. - - :param known_distances: If provided, this mapping is used instead of an internal - one to keep distance candidates and final distances values of reported - vertices (resp. their hashable ids from vertex_to_id is used as key) from - some start vertex. - - For vertices without known distance, it must yield float('infinity'). The - internal default implementation uses a collections.defaultdict. Typical - use cases are: 1) preloading known distances of vertices, and the - vertices should not be visited if no smaller distance is found during the - traversal, and 2) providing your own way for storing the distances. - - :return: Traversal, that has been started, e.g., the methods go* can now be - used. - """ - - if not isinstance(type(self), type(Traversal)): - raise RuntimeError( - "Method start_from can only be called on a Traversal object." - ) - - # ----- Initialize method specific public bookkeeping ----- - self._start_from( - start_vertex, - start_vertices, - build_paths, - calculation_limit, - self._gear, - ) - self._keep_distances = keep_distances - self._known_distances = known_distances - - # Explicitly list start vertices and their id. Needed several times. - self._start_vertices_and_ids = tuple( - iter_start_vertices_and_ids(self._start_vertices, self._vertex_to_id) - ) - - # At start, most of the distances from a vertex to a start vertex are not - # known. If accessed for comparison for possibly better distances, infinity - # is used, if no other value is given. Each start vertex has distance 0 - # from a start vertex (itself), if not defined otherwise. - zero = self._gear.zero() - self.distances = define_distances( - self._gear, - self._known_distances, - ((vertex_id, zero) for vertex, vertex_id in self._start_vertices_and_ids), - self._is_tree, - ) - - # The following two values are not used by NoGraphs. They are only set - # to have some defined values before the traversal iterator sets them. - self.distance = self._gear.infinity() - self.depth = 0 - - super()._start() - return self - - def _traverse(self) -> Iterator[T_vertex]: - # ----- Prepare efficient environment for inner loop ----- - # Copy Gear attributes into method scope (faster access) - infinity = self._gear.infinity() - - # Copy Traversal attributes into method scope (faster access) - labeled_edges = self._labeled_edges - is_tree = self._is_tree - maybe_vertex_to_id = ( - None if self._vertex_to_id == vertex_as_id else self._vertex_to_id - ) # Case vertex_as_id: not apply; T_vertex_id > T_vertex - build_paths = self._build_paths - calculation_limit = self._calculation_limit - predecessors = self._predecessors - attributes = self._attributes - - # Prepare limit check done by zero check - if calculation_limit is not None: - calculation_limit += 1 - - # Copy traversal specific attributes into method scope - next_edges = self._next_edges - keep_distances = self._keep_distances - - # Get references of used gear objects and methods (avoid attribute resolution) - ( - _, - predecessors_sequence, - predecessors_wrapper, - ) = access_to_vertex_mapping_expect_none(predecessors) - ( - _, - attributes_sequence, - attributes_wrapper, - ) = access_to_vertex_mapping_expect_none(attributes) - zero = self._gear.zero() - - # ----- Initialize method specific private bookkeeping ----- - - # Unique number, that prevents heapq from sorting by vertices in case of a - # tie in the sort field, because vertices do not need to be pairwise - # comparable. The integers from -5 to 256 are used first, because they are - # internalized (pre-calculated, and thus fastest). We count downwards like we - # do in A* search. There, it is preferable, because a LIFO behavior makes A* - # often faster. Here, we do it simply to do it the same way. - unique_no = itertools.count(256, -1) - - # Get references of used gear objects and methods (avoid attribute resolution) - _, distances_sequence, distances_wrapper = access_to_vertex_mapping( - self.distances - ) - - # So far, the start vertices are to be visited. Each has an edge count of 0. - # (No index exception possible at the following indexed access) - to_visit = [ # used as collection.heapq of tuples, the lowest distance first - (distances_sequence[vertex_id], next(unique_no), vertex, 0) - for vertex, vertex_id in self._start_vertices_and_ids - ] - heapify(to_visit) - - # ----- Inner loop ----- - - while to_visit: - # Visit path with the lowest distance first - path_weight, _, vertex, path_edge_count = heappop(to_visit) - - # A vertex can get added to the heap multiple times. We want to process - # it only once, the first time it is removed from the heap, because this - # is the case with the shortest distance from start. - if not is_tree: - v_id: T_vertex_id = ( - maybe_vertex_to_id(vertex) # type: ignore[assignment] - if maybe_vertex_to_id - else vertex - ) - - # (No index exception possible at the following indexed access) - if distances_sequence[v_id] < path_weight: - continue - if not keep_distances: - # (Allow garbage collector to free distance value if nowhere else - # needed any more) - distances_sequence[v_id] = zero # No index exception possible here - - # Export traversal data to traversal attributes - self.distance = path_weight - self.depth = path_edge_count - - # We now know the distance of the vertex, so we report it. - if path_edge_count > 0: # do not report start vertex - yield vertex - - if calculation_limit and not (calculation_limit := calculation_limit - 1): - raise RuntimeError("Number of visited vertices reached limit") - - # Expand vertex. New neighbors are one edge count deeper than vertex. - n_path_edge_count = path_edge_count + 1 - for edge in next_edges(vertex, self): - neighbor, weight = edge[0], edge[1] - - n_path_weight = weight + path_weight - # (Distance values equal to or higher than the chosen infinity value of - # the gear are invalid and cannot be handled further.) - if infinity <= n_path_weight: - self._gear.raise_distance_infinity_overflow_error(n_path_weight) - - # If the found path to the neighbor is not shorter than the shortest - # such path found so far, we can safely ignore it. Otherwise, it is a - # new candidate for a shortest path to the neighbor, and we push it to - # the heap. - if build_paths or not is_tree: - n_id: T_vertex_id = ( - maybe_vertex_to_id(neighbor) # type: ignore[assignment] - if maybe_vertex_to_id - else neighbor - ) - - if not is_tree: - try: - if distances_sequence[n_id] <= n_path_weight: - continue - distances_sequence[n_id] = n_path_weight - except IndexError: - distances_wrapper.extend_and_set(n_id, n_path_weight) - - # If we are to generate a path, we have to do it here, since the - # edge we have to add to the path prefix is not stored on the heap - if build_paths: - try: - predecessors_sequence[n_id] = vertex - except IndexError: - predecessors_wrapper.extend_and_set(n_id, vertex) - if labeled_edges: - # self._labeled_edges -> next_edges (a NextWeightedEdges) - # is a NextWeightedLabeledEdges -> edge[-1] is a T_labels - data_of_edge: T_labels = edge[ - -1 - ] # type: ignore[assignment] - try: - attributes_sequence[n_id] = data_of_edge - except IndexError: - attributes_wrapper.extend_and_set(n_id, data_of_edge) - - heappush( - to_visit, - ( - n_path_weight, - next(unique_no), - neighbor, - n_path_edge_count, - ), - ) - - -class TraversalShortestPaths( - Generic[T_vertex, T_weight, T_labels], - TraversalShortestPathsFlex[T_vertex, T_vertex, Union[T_weight, float], T_labels], -): - """ - Eases the use of `TraversalShortestPathsFlex` for typical cases. - For documentation of functionality and parameters, see there. - - .. code-block:: python - - TraversalShortestPaths[T_vertex, T_weight, T_labels](*args, **keywords) - - is a short form for - - .. code-block:: python - - TraversalShortestPathsFlex[ - T_vertex, T_vertex, Union[T_weight, float], T_labels], - ](nog.vertex_as_id, nog.GearDefault(), *args, **keywords) - - Implications: - - - `GearDefault` is used, see there how it and its superclass work - - The used weights are defined by Union[T_weight, float], see `GearDefault` - - T_vertex is bound to Hashable (T_vertex is used as `T_vertex_id`, see there) - """ - - def __init__( - self, - next_edges: Optional[ - NextWeightedEdges[ - T_vertex, - TraversalShortestPathsFlex[ - T_vertex, T_vertex, Union[T_weight, float], T_labels - ], - T_weight, - ] - ] = None, - *, - next_labeled_edges: Optional[ - NextWeightedLabeledEdges[ - T_vertex, - TraversalShortestPathsFlex[ - T_vertex, T_vertex, Union[T_weight, float], T_labels - ], - T_weight, - T_labels, - ] - ] = None, - is_tree: bool = False, - ) -> None: - super().__init__( - vertex_as_id, - GearDefault(), - next_edges, - next_labeled_edges=next_labeled_edges, - is_tree=is_tree, - ) - - -class TraversalAStarFlex( - _TraversalWithDistances[T_vertex, T_vertex_id, T_weight, T_labels] -): - """ - | Bases: Generic[`T_vertex`, `T_vertex_id`, `T_weight`, `T_labels`], - | `Traversal` [`T_vertex`, `T_vertex_id`, `T_labels`] - - :param vertex_to_id: See `VertexToID` function. - - :param gear: See `gears API ` and class `Gear`. - - :param next_edges: See `NextWeightedEdges` function. If None, provide - next_labeled_edges. - - :param next_labeled_edges: See `NextWeightedLabeledEdges` function. Only allowed - if next_edges equals None. If given, paths will record the given labels. - - :param is_tree: bool: If it is certain, that during each traversal run, each vertex - can be reached only once, is_tree can be set to True. This improves performance, - but if *start_from* has been called with parameter *known_path_length_guesses* - given, this collection will not be updated during the traversal. - - **Algorithm:** The search algorithm A*, non-recursive, based on heap. - - **Input:** Weighted directed graph. One or more start vertices. Vertices must be - hashable, or hashable id can be provided. Weights need to be non-negative. - A heuristic function that estimates the cost of the cheapest path from a given - vertex to the goal (resp. to any of your goal vertices, if you have more than - one), and never overestimates the actual needed costs ("admissible heuristic - function"). Optionally, a calculation limit. - - **Properties:** Vertices are reported and expanded ordered by increasing path - length (sum of edge weights) of the shortest paths from a start vertex to the - respective vertex that have been found so far. - - When the goal is reported, the path stored for it in *paths* is a shortest - path from start to goal and the path_length of the search state is the distance - of the goal from start. - - In case the used heuristic function is *consistent* (i.e., following an edge from - one vertex to another never reduces the estimated costs to get to the goal by - more than the weight of the edge), further guarantees hold: Each vertex is only - visited once. And for each visited vertex, the respective path_length and depth - (and optionally, the path) are the data of the shortest existing path from start - (not only from the shortest path found so far). - - **Search state:** When a vertex is *expanded* (traversal calls next_edges or - next_labeled_edges) or *reported* (an iterator of the traversal returns it), - the traversal provides updated values for the attributes - *path_length*, *depth*, *paths*. - """ - - def __init__( - self, - vertex_to_id: VertexToID[T_vertex, T_vertex_id], - gear: Gear[T_vertex, T_vertex_id, T_weight, T_labels], - next_edges: Optional[ - NextWeightedEdges[ - T_vertex, - TraversalAStarFlex[T_vertex, T_vertex_id, T_weight, T_labels], - T_weight, - ] - ] = None, - *, - next_labeled_edges: Optional[ - NextWeightedLabeledEdges[ - T_vertex, - TraversalAStarFlex[T_vertex, T_vertex_id, T_weight, T_labels], - T_weight, - T_labels, - ] - ] = None, - is_tree: bool = False, - ) -> None: - self._next_edges, labeled_edges = _create_unified_next_weighted( - next_edges, next_labeled_edges - ) - super().__init__(labeled_edges, is_tree, vertex_to_id, gear) - - # The following three values are not used by NoGraphs. They are only set - # to have some initialization. - self.path_length: T_weight = self._gear.infinity() - """ Length (sum of edge weights) of the found path to the - vertex (for the goal vertex: a shortest path) - """ - self.depth: int = -1 - """ At this *search depth*, the reported (resp. the expanded) vertex has been - found. It equals the length (number of edges) of the created path to the - vertex, if path creation is demanded. - Note: The search depth does not need to be the depth of the vertex - (see `TraversalBreadthFirstFlex`). - """ - self._start_vertices_and_ids = tuple[tuple[T_vertex, T_vertex_id]]() - - self._heuristic: Optional[Callable[[T_vertex], Real]] = None - self._known_distances: Optional[ - VertexIdToDistanceMapping[T_vertex_id, T_weight] - ] = None - self._known_path_length_guesses: Optional[ - VertexIdToDistanceMapping[T_vertex_id, T_weight] - ] = None - self._path_length_guesses: Optional[ - VertexIdToDistanceMapping[T_vertex_id, T_weight] - ] = None - - def start_from( - self, - heuristic: Callable[[T_vertex], Real], - start_vertex: Optional[T_vertex] = None, - *, - start_vertices: Optional[Iterable[T_vertex]] = None, - build_paths: bool = False, - calculation_limit: Optional[int] = None, - known_distances: Optional[ - VertexIdToDistanceMapping[T_vertex_id, T_weight] - ] = None, - known_path_length_guesses: Optional[ - VertexIdToDistanceMapping[T_vertex_id, T_weight] - ] = None, - ) -> TraversalAStarFlex[T_vertex, T_vertex_id, T_weight, T_labels]: - """ - Start the traversal at a vertex or a set of vertices and set parameters. - - :param heuristic: The admissible and consistent heuristic function that - estimates the cost of the cheapest path from a given vertex to the goal - (resp. one of the goals). - - :param start_vertex: The vertex the search should start at. Provide either - start_vertex or start_vertices, but not both. - - :param start_vertices: The set of vertices the search should start - at. Provide either start_vertex or start_vertices, but not both. - - :param build_paths: If true, build paths from start vertices for each reported - vertex, and an empty path for each start vertex. - - :param calculation_limit: If provided, maximal number of vertices to process - (read in) from your graph. If it is exceeded, a RuntimeError will be raised. - - :param known_distances: If provided, this mapping is used instead of an internal - one to keep the distances of vertices that have already been visited - (resp. their hashable ids from vertex_to_id is used as key) from some - start vertex. For vertices without known distance, it must yield float( - 'infinity'). The internal default implementation uses a - collections.defaultdict. - - Typical use cases are: 1) preloading known distances of vertices, and the - vertices should not be visited if no smaller distance is found during the - traversal, or 2) getting online access to the internal bookkeeping of - visited vertices and their distances, or 3) providing your own way for - storing the distance of a vertex that has already been visited. - - :param known_path_length_guesses: Like known_distances, but for keeping the sum - distance+heuristic for vertices. - - :return: Traversal, that has been started, e.g., the methods go* can now be - used. - """ - - if not isinstance(type(self), type(Traversal)): - raise RuntimeError( - "Method start_from can only be called on a Traversal object." - ) - - self._start_from( - start_vertex, - start_vertices, - build_paths, - calculation_limit, - self._gear, - ) - - self._heuristic = heuristic - self._known_distances = known_distances - self._known_path_length_guesses = known_path_length_guesses - - # Explicitly list start vertices and their id. Needed several times. - self._start_vertices_and_ids = tuple( - iter_start_vertices_and_ids(self._start_vertices, self._vertex_to_id) - ) - - # At start, most of the distances from a vertex to a start vertex are not - # known. If accessed for comparison for possibly better distances, infinity - # is used, if no other value is given. - # Each start vertex has distance 0 from a start vertex (itself), - # and a path_length_guess of distance + heuristic(vertex), both if not - # defined otherwise. - zero = self._gear.zero() - self.distances = define_distances( - self._gear, - self._known_distances, - ((vertex_id, zero) for vertex, vertex_id in self._start_vertices_and_ids), - self._is_tree, - ) - - # The following two values are not used by NoGraphs. They are only set - # to have some defined values before the traversal iterator sets them. - self.path_length = self._gear.infinity() - self.depth = 0 - - super()._start() - return self - - def _traverse(self) -> Iterator[T_vertex]: - # Copy Gear attributes into method scope (faster access) - infinity = self._gear.infinity() - - # Copy Traversal attributes into method scope (faster access) - labeled_edges = self._labeled_edges - is_tree = self._is_tree - maybe_vertex_to_id = ( - None if self._vertex_to_id == vertex_as_id else self._vertex_to_id - ) # Case vertex_as_id: not apply; T_vertex_id > T_vertex - build_paths = self._build_paths - calculation_limit = self._calculation_limit - predecessors = self._predecessors - attributes = self._attributes - - # Prepare limit check done by zero check - if calculation_limit is not None: - calculation_limit += 1 # allows for limit check by zero check - - # Copy traversal specific attributes into method scope - next_edges = self._next_edges - heuristic = self._heuristic - - # Get references of used gear objects and methods (avoid attribute resolution) - ( - _, - predecessors_sequence, - predecessors_wrapper, - ) = access_to_vertex_mapping_expect_none(predecessors) - ( - _, - attributes_sequence, - attributes_wrapper, - ) = access_to_vertex_mapping_expect_none(attributes) - - # ----- Initialize method specific private bookkeeping ----- - - # Unique number, that prevents heapq from sorting by vertices in case of a - # tie in the sort field, because vertices do not need to be pairwise - # comparable. The numbers are generated in decreasing order to make the min - # heap behave like a LIFO queue in case of ties. The integers from -5 to 256 - # are used first, because they are internalized (pre-calculated, and thus - # fastest). - unique_no = itertools.count(256, -1) - - # Get references of used gear objects and methods (avoid attribute resolution) - _, distances_sequence, distances_wrapper = access_to_vertex_mapping( - self.distances - ) - - assert heuristic is not None # set by __init__ - path_length_guesses = define_distances( - self._gear, - self._known_path_length_guesses, - ( - (vertex_id, distances_sequence[vertex_id] + heuristic(vertex)) - for vertex, vertex_id in self._start_vertices_and_ids - ), - is_tree, - ) - # Get references of used gear objects and methods (avoid attribute resolution) - ( - _, - path_length_guesses_sequence, - path_length_guesses_wrapper, - ) = access_to_vertex_mapping(path_length_guesses) - - # So far, the start vertices are to be visited. Each has an edge count of 0, - # and its path length guess is the one computed above. - to_visit = [ # used as collection.heapq of tuples, the lowest distance first - (path_length_guesses_sequence[vertex_id], next(unique_no), vertex, 0) - for vertex, vertex_id in self._start_vertices_and_ids - ] - heapify(to_visit) - - # ----- Inner loop ----- - - while to_visit: - # Visit path with the lowest path_length_guess first - path_length_guess, _, vertex, path_edge_count = heappop(to_visit) - - # A vertex can get added to the heap multiple times. - - # For consistent heuristics: We want to process the vertex only once, the - # first time it is removed from the heap, because this is the case with the - # shortest distance estimation. If the heuristic is not consistent: Only - # when the new distance estimation is better than the best found so far, we - # want to process the vertex again. - v_id: T_vertex_id = ( - maybe_vertex_to_id(vertex) # type: ignore[assignment] - if maybe_vertex_to_id - else vertex - ) - - # (No index exception possible at the following indexed access) - if not is_tree and path_length_guess > path_length_guesses_sequence[v_id]: - continue - - # (No index exception possible here) - path_weight = distances_sequence[v_id] - - # Export traversal data to traversal attributes - self.path_length = path_weight - self.depth = path_edge_count - - # We now know the distance of the vertex, so we report it. - if path_edge_count > 0: # do not report start vertex - yield vertex - - if calculation_limit and not (calculation_limit := calculation_limit - 1): - raise RuntimeError("Number of visited vertices reached limit") - - # Expand vertex. New neighbors are one edge count deeper than vertex. - n_path_edge_count = path_edge_count + 1 - for edge in next_edges(vertex, self): - neighbor, weight = edge[0], edge[1] - - n_path_weight = weight + path_weight - # (Distance values equal to or higher than the chosen infinity value of - # the gear are invalid and cannot be handled further.) - if infinity <= n_path_weight: - self._gear.raise_distance_infinity_overflow_error(n_path_weight) - - # If the found path to the neighbor is not shorter than the shortest - # such path found so far, we can safely ignore it. Otherwise, it is a - # new candidate for a shortest path to the neighbor, and we push it to - # the heap. - n_id: T_vertex_id = ( - maybe_vertex_to_id(neighbor) # type: ignore[assignment] - if maybe_vertex_to_id - else neighbor - ) - - try: - if not is_tree and distances_sequence[n_id] <= n_path_weight: - continue - distances_sequence[n_id] = n_path_weight - except IndexError: - # n_id not in distances_collection. To be regarded as value - # infinity, i.e., n_path_weight is smaller. - distances_wrapper.extend_and_set(n_id, n_path_weight) - - # If we are to generate a path, we have to do it here, since the edge - # we have to add to the path prefix is not stored on the heap - if build_paths: - try: - predecessors_sequence[n_id] = vertex - except IndexError: - predecessors_wrapper.extend_and_set(n_id, vertex) - if labeled_edges: - data_of_edge: T_labels = edge[-1] # type: ignore[assignment] - try: - attributes_sequence[n_id] = data_of_edge - except IndexError: - attributes_wrapper.extend_and_set(n_id, data_of_edge) - - h = heuristic(neighbor) - if h == infinity: - n_guess = infinity - else: - n_guess = n_path_weight + h - # (Distance values equal to or higher than the chosen infinity - # value of the gear are invalid and cannot be handled further.) - if infinity <= n_guess: - self._gear.raise_distance_infinity_overflow_error(n_guess) - - if not is_tree: - try: - path_length_guesses_sequence[n_id] = n_guess - except IndexError: - path_length_guesses_wrapper.extend_and_set(n_id, n_guess) - heappush( - to_visit, - (n_guess, next(unique_no), neighbor, n_path_edge_count), - ) - - -class TraversalAStar( - Generic[T_vertex, T_weight, T_labels], - TraversalAStarFlex[T_vertex, T_vertex, Union[T_weight, float], T_labels], -): - """ - Eases the use of `TraversalAStarFlex` for typical cases. - For documentation of functionality and parameters, see there. - - .. code-block:: python - - TraversalAStar[T_vertex, T_weight, T_labels](*args, **keywords) - - is a short form for - - .. code-block:: python - - TraversalAStarFlex[ - T_vertex, T_vertex, Union[T_weight, float], T_labels], - ](nog.vertex_as_id, nog.GearDefault(), *args, **keywords) - - Implications: - - - `GearDefault` is used, see there how it and its superclass work - - The used weights are defined by Union[T_weight, float], see `GearDefault` - - T_vertex is bound to Hashable (T_vertex is used as `T_vertex_id`, see there) - """ - - def __init__( - self, - next_edges: Optional[ - NextWeightedEdges[ - T_vertex, - TraversalAStarFlex[ - T_vertex, T_vertex, Union[T_weight, float], T_labels - ], - T_weight, - ] - ] = None, - *, - next_labeled_edges: Optional[ - NextWeightedLabeledEdges[ - T_vertex, - TraversalAStarFlex[ - T_vertex, T_vertex, Union[T_weight, float], T_labels - ], - T_weight, - T_labels, - ] - ] = None, - is_tree: bool = False, - ) -> None: - super().__init__( - vertex_as_id, - GearDefault(), - next_edges, - next_labeled_edges=next_labeled_edges, - is_tree=is_tree, - ) - - -class TraversalMinimumSpanningTreeFlex( - _TraversalWithWeights[T_vertex, T_vertex_id, T_weight, T_labels] -): - """ - | Bases: Generic[`T_vertex`, `T_vertex_id`, `T_weight`, `T_labels`], - | `Traversal` [`T_vertex`, `T_vertex_id`, `T_labels`] - - :param vertex_to_id: See `VertexToID` function. - - :param gear: See `gears API ` and class `Gear`. - - :param next_edges: See `NextWeightedEdges` function. If None, provide - next_labeled_edges. - - :param next_labeled_edges: See `NextWeightedLabeledEdges` function. Only allowed - if next_edges equals None. If given, paths will record the given labels. - - **Algorithm:** Minimum spanning tree ("MST") algorithm of Jarnik, Prim, Dijkstra. - Non-recursive, based on heap. A so-called *tie breaker* is implemented, that - prioritizes edges that have been found more recently about edges that have been - found earlier. This is a typical choice that often improves search performance. - - **Properties:** Only edges of the MST from start vertices are reported. Each - vertex is reported (as end vertex of an edge) and expanded only once. Computed - paths only use MST edges. - - **Input:** Weighted undirected graph, given as directed edges with the same - weight in both directions. One or more start vertices (e.g. for components in - unconnected graphs). Optional calculation limit. - - **Search state:** When a vertex is *expanded* (traversal calls next_edges or - next_labeled_edges) or an edge is *reported* (an iterator of the traversal returns - the vertex it leads to), the traversal provides updated values for the attributes - *edge* and *paths*. - """ - - def __init__( - self, - vertex_to_id: VertexToID[T_vertex, T_vertex_id], - gear: Gear[T_vertex, T_vertex_id, T_weight, T_labels], - next_edges: Optional[ - NextWeightedEdges[ - T_vertex, - TraversalMinimumSpanningTreeFlex[ - T_vertex, T_vertex_id, T_weight, T_labels - ], - T_weight, - ] - ] = None, - *, - next_labeled_edges: Optional[ - NextWeightedLabeledEdges[ - T_vertex, - TraversalMinimumSpanningTreeFlex[ - T_vertex, T_vertex_id, T_weight, T_labels - ], - T_weight, - T_labels, - ] - ] = None, - ) -> None: - self._next_edges, labeled_edges = _create_unified_next_weighted( - next_edges, next_labeled_edges - ) - super().__init__(labeled_edges, False, vertex_to_id, gear) - self.edge: Optional[WeightedFullEdge[T_vertex, T_weight, T_labels]] = None - """ Tuple of from_vertex, to_vertex, the weight of the edge, - and additional data you have provided with the edge (if so). - """ - - # The following value is not used by NoGraphs. It is only set - # to have some initialization. - self._start_vertices_and_ids = tuple[tuple[T_vertex, T_vertex_id]]() - - def start_from( - self, - start_vertex: Optional[T_vertex] = None, - *, - start_vertices: Optional[Iterable[T_vertex]] = None, - build_paths: bool = False, - calculation_limit: Optional[int] = None, - ) -> TraversalMinimumSpanningTreeFlex[T_vertex, T_vertex_id, T_weight, T_labels]: - """ - Start the traversal at a vertex or a set of vertices and set parameters. - - :param start_vertex: The vertex the search should start at. If None, provide - start_vertices. - - :param start_vertices: The set of vertices the search should start - at. Only allowed if start_vertex equals None. Leads to a result - consisting of several trees that are only connected if the start vertices - are connected. - - :param build_paths: If true, build paths from start vertices for each reported - vertex, and an empty path for each start vertex. - - :param calculation_limit: If provided, maximal number of vertices to process - (read in) from your graph. If it is exceeded, a RuntimeError will be raised. - - :return: Traversal, that has been started, e.g., the methods go* can now be - used. - """ - - if not isinstance(type(self), type(Traversal)): - raise RuntimeError( - "Method start_from can only be called on a Traversal object." - ) - - # ----- Initialize method specific public bookkeeping ----- - self._start_from( - start_vertex, - start_vertices, - build_paths, - calculation_limit, - self._gear, - ) - - # Explicitly list start vertices and their id. Needed several times. - self._start_vertices_and_ids = tuple( - iter_start_vertices_and_ids(self._start_vertices, self._vertex_to_id) - ) - - # The following value is not used by NoGraphs. It is only set - # to have some defined value before the traversal iterator sets them. - self.edge = None - - super()._start() - return self - - def _traverse(self) -> Iterator[T_vertex]: - # ----- Prepare efficient environment for inner loop ----- - # Copy Traversal attributes into method scope (faster access) - labeled_edges = self._labeled_edges - maybe_vertex_to_id = ( - None if self._vertex_to_id == vertex_as_id else self._vertex_to_id - ) # Case vertex_as_id: not apply; T_vertex_id > T_vertex - build_paths = self._build_paths - calculation_limit = self._calculation_limit - predecessors = self._predecessors - attributes = self._attributes - - # Copy traversal specific attributes into method scope - next_edges = self._next_edges - - # Get references of used gear objects and methods (avoid attribute resolution) - ( - _, - predecessors_sequence, - predecessors_wrapper, - ) = access_to_vertex_mapping_expect_none(predecessors) - ( - _, - attributes_sequence, - attributes_wrapper, - ) = access_to_vertex_mapping_expect_none(attributes) - - # ----- Initialize method specific private bookkeeping ----- - - # At start, only the start vertices are regarded as visited - # (The protocol VertexSet abandons checking the element type, see - # VertexSet. Flake8 and MyPy accept this, PyCharm does not and need - # noinspection.) - # noinspection PyTypeChecker - visited: VertexIdSet[T_vertex_id] = self._gear.vertex_id_set( - vertex_id for vertex, vertex_id in self._start_vertices_and_ids - ) - - # Check if we already go over the calculation limit when we evaluate the - # edges from start vertices ("expanding the start vertices"). This avoids a - # step by step check that slows down the to_visit loop for large sets of - # start vertices. Note: A calculation limit below 0 leads nowhere ever to an - # exception. So, neither here. - if calculation_limit is not None and calculation_limit >= 0: - if ( - calculation_limit := calculation_limit - - len(self._start_vertices_and_ids) - ) < 0: - raise RuntimeError("Number of visited vertices reached limit") - - # So far, the edges from the start vertices are to be visited as candidates - # for edges of a MST. (Unique number prevents heapq from sorting by (possibly - # not comparable) fields) - unique_no = itertools.count() - to_visit: list[ - tuple[ - T_weight, int, T_vertex, WeightedOutEdge[T_vertex, T_weight, T_labels] - ] - ] = [ # used as collection.heapq, the lowest edge weight first - (edge[1], next(unique_no), vertex, edge) - for vertex in self._start_vertices - for edge in next_edges(vertex, self) - ] - heapify(to_visit) - - # Prepare limit check done by zero check - if calculation_limit is not None: - calculation_limit += 1 # allows for limit check by zero check - - # Get references of used gear objects and methods (avoid attribute resolution) - visited_add = visited.add - ( - visited_uses_sequence, - visited_sequence, - visited_wrapper, - visited_uses_bits, - index_and_bit_method, - ) = access_to_vertex_set(visited) - - # ----- Inner loop ----- - - while to_visit: - # Visit edge with the lowest weight first - _weight, _, vertex, to_edge = heappop(to_visit) - to_vertex = to_edge[0] - - # A vertex can get added to the heap multiple times, as end vertex of - # several edges. We want to process it only once, as end vertex of a MST - # edge. - # The shortest edge from a visited vertex that leads to a vertex not - # visited so far, must be an edge of the MST. - to_id: T_vertex_id = ( - maybe_vertex_to_id(to_vertex) # type: ignore[assignment] - if maybe_vertex_to_id - else to_vertex - ) - - if visited_uses_sequence: - try: - if visited_sequence[to_id]: - continue - visited_sequence[to_id] = True - except IndexError: - visited_wrapper.extend_and_set(to_id, True) - else: - if to_id in visited: - continue - visited_add(to_id) - - if build_paths: - try: - predecessors_sequence[to_id] = vertex - except IndexError: - predecessors_wrapper.extend_and_set(to_id, vertex) - if labeled_edges: - # self._labeled_edges -> next_edges (a NextWeightedEdges) is a - # NextWeightedLabeledEdges -> edge[-1] is a T_labels. - data_of_edge: T_labels = to_edge[-1] # type: ignore[assignment] - try: - attributes_sequence[to_id] = data_of_edge - except IndexError: - attributes_wrapper.extend_and_set(to_id, data_of_edge) - - # Export traversal data to traversal attribute and report vertex - # (Expression type follows from types of vertex and to_edge and the - # definition of WeightedFullEdge. MyPy + PyCharm cannot derive this.) - # noinspection PyTypeChecker - full_edge: WeightedFullEdge[T_vertex, T_weight, T_labels] = ( - vertex, - ) + to_edge # type: ignore[assignment] - self.edge = full_edge - yield to_vertex - - if calculation_limit and not (calculation_limit := calculation_limit - 1): - raise RuntimeError("Number of visited vertices reached limit") - - for n_to_edge in next_edges(to_vertex, self): - n_to_vertex, n_weight = n_to_edge[0], n_to_edge[1] - # If the edge leads to a vertex that is, so far, not reached by edges - # of the MST, it is a candidate for a MST edge. We push it to the heap. - n_to_id: T_vertex_id = ( - maybe_vertex_to_id(n_to_vertex) # type: ignore[assignment] - if maybe_vertex_to_id - else n_to_vertex - ) - - try: - if ( - (visited_sequence[n_to_id]) - if visited_uses_sequence - else (n_to_id in visited) - ): - continue - except IndexError: - pass # IndexError means case sequence and n_to_id is not visited - heappush( - to_visit, - (n_weight, next(unique_no), to_vertex, n_to_edge), - ) - - -class TraversalMinimumSpanningTree( - Generic[T_vertex, T_weight, T_labels], - TraversalMinimumSpanningTreeFlex[ - T_vertex, T_vertex, Union[T_weight, float], T_labels - ], -): - """ - Eases the use of `TraversalMinimumSpanningTreeFlex` for typical cases. - For documentation of functionality and parameters, see there. - - .. code-block:: python - - TraversalMinimumSpanningTree[T_vertex, T_weight, T_labels](*args, **keywords) - - is a short form for - - .. code-block:: python - - TraversalMinimumSpanningTreeFlex[ - T_vertex, T_vertex, Union[T_weight, float], T_labels], - ](nog.vertex_as_id, nog.GearDefault(), *args, **keywords) - - Implications: - - - `GearDefault` is used, see there how it and its superclass work - - The used weights are defined by Union[T_weight, float], see `GearDefault` - - T_vertex is bound to Hashable (T_vertex is used as `T_vertex_id`, see there) - """ - - def __init__( - self, - next_edges: Optional[ - NextWeightedEdges[ - T_vertex, - TraversalMinimumSpanningTreeFlex[ - T_vertex, T_vertex, Union[T_weight, float], T_labels - ], - T_weight, - ] - ] = None, - *, - next_labeled_edges: Optional[ - NextWeightedLabeledEdges[ - T_vertex, - TraversalMinimumSpanningTreeFlex[ - T_vertex, T_vertex, Union[T_weight, float], T_labels - ], - T_weight, - T_labels, - ] - ] = None, - ) -> None: - super().__init__( - vertex_as_id, - GearDefault(), - next_edges, - next_labeled_edges=next_labeled_edges, - ) diff --git a/tests/extra_test_tsp_performance.py b/tests/extra_test_tsp_performance.py index 655b332..6d0fc5f 100644 --- a/tests/extra_test_tsp_performance.py +++ b/tests/extra_test_tsp_performance.py @@ -275,7 +275,10 @@ def next_edges_backwards( solve( tsp_text + "_traveling_salesman_basic, " + mode_text, lambda: _traveling_salesman_basic( - graph, 0, float("inf"), find_longest=find_longest + graph, # noqa: B023 + 0, + float("inf"), + find_longest=find_longest, # noqa: B023 ), correct_length=correct_result, graph=graph, @@ -286,7 +289,8 @@ def next_edges_backwards( solve( tsp_text + "traveling_salesman, " + mode_text, lambda: traveling_salesman( - range(len(graph)), graph, find_longest=find_longest + range(len(graph)), graph, # noqa: B023 # fmt: skip + find_longest=find_longest # noqa: B023 # fmt: skip ), correct_length=correct_result, graph=graph, @@ -297,10 +301,9 @@ def next_edges_backwards( solve( tsp_text + "traveling_salesman_flex IntVertexIDs, " + mode_text, lambda: traveling_salesman_flex( - range(len(graph)), - graph, + range(len(graph)), graph, # noqa: B023 # fmt: skip GearForIntVertexIDsAndCFloats[int, Any](), - find_longest=find_longest, + find_longest=find_longest, # noqa: B023 # fmt: skip ), correct_length=correct_result, graph=graph, @@ -311,13 +314,13 @@ def next_edges_backwards( solve( tsp_text + "traveling_salesman_flex IntVerticesAndIDs, " + mode_text, lambda: traveling_salesman_flex( - range(len(graph)), - graph, + # fmt: off + range(len(graph)), graph, # noqa: B023 # fmt: skip GearForIntVerticesAndIDsAndCFloats[Any](), - find_longest=find_longest, + find_longest=find_longest, # noqa: B023 # fmt: skip ), correct_length=correct_result, - graph=graph, + graph=graph, # noqa: B023 time_stats=True, mem_stats=True, ) @@ -327,29 +330,26 @@ def next_edges_backwards( + "traveling_salesman_flex IntVerticesAndIDsAndCInt, " + mode_text, lambda: traveling_salesman_flex( - range(len(graph)), - graph, + range(len(graph)), graph, # noqa: B023 # fmt: skip GearForIntVerticesAndIDsAndCInts[Any](), - find_longest=find_longest, + find_longest=find_longest, # noqa: B023 # fmt: skip ), correct_length=correct_result, graph=graph, time_stats=True, mem_stats=True, ) - solve( tsp_text + "traveling_salesman_flex IntVerticesAndIDsAndCInt I, " + mode_text, lambda: traveling_salesman_flex( - range(len(graph)), - graph, + range(len(graph)), graph, # noqa: B023 # fmt: skip GearForIntVerticesAndIDsAndCInts[Any](distance_type_code="I"), - find_longest=find_longest, + find_longest=find_longest, # noqa: B023 # fmt: skip ), correct_length=correct_result, - graph=graph, + graph=graph, # noqa: B023 time_stats=True, mem_stats=True, ) diff --git a/tests/test_expanded_template.py b/tests/test_expanded_template.py new file mode 100644 index 0000000..e6824f2 --- /dev/null +++ b/tests/test_expanded_template.py @@ -0,0 +1,62 @@ +import unittest +import pymacros4py +import pathlib + + +class TemplateExpansionTest(unittest.TestCase): + overwrite = False + + def test_expansion_result(self) -> None: + """Test that all template files have been correctly expanded""" + pp = pymacros4py.PreProcessor() + templates_root = pathlib.Path("tpl") + for template in templates_root.glob("**/*"): + if not template.is_file() or not template.name.startswith("!"): + continue + print("Processing template", template) + + template_without_exclamation_mark = template.with_name(template.name[1:]) + goal = template_without_exclamation_mark.relative_to(templates_root) + + expanded = pp.expand_file(template) + + # format expansion results of .py files with black + if template.suffix == ".py": + tmp_file_path = pymacros4py.write_to_tempfile(expanded) + pymacros4py.run_process_with_file( + ["black", tmp_file_path], tmp_file_path + ) + expanded = pymacros4py.read_file(tmp_file_path, finally_remove=True) + + if goal.exists() and not self.overwrite: + current_content = pymacros4py.read_file(goal) + try: + self.assertMultiLineEqual(current_content, expanded) + except AssertionError as e: + d = pp.diff( + current_content, + expanded, + str(goal), + f'expanded("{str(template)}")', + ) + print(d, "\n") + raise e + else: + pymacros4py.write_file(goal, expanded) + + +if __name__ == "__main__": + # This code allows to start the input/output test cases manually, without + # all other tests triggered by the test procedure for the whole package. + # Note: If this module is run on itself, the current directory needs to + # be the base directory of the package. + + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument("overwrite", help="If 'True', overwrite") + args = parser.parse_args() + + c = TemplateExpansionTest() + c.overwrite = args.overwrite == "True" + c.test_expansion_result() diff --git a/tests/test_extra_tsp.py b/tests/test_extra_tsp.py index 4fd017e..cf9ea4f 100644 --- a/tests/test_extra_tsp.py +++ b/tests/test_extra_tsp.py @@ -344,15 +344,17 @@ def solve( solve( tsp_text + "traveling_salesman, shortest", - lambda: traveling_salesman(range(len(graph)), graph), + lambda: traveling_salesman( + range(len(graph)), graph), # noqa: B023 # fmt: skip shortest, - graph, + graph, # noqa: B023 print_only_on_error=True, ) solve( tsp_text + "traveling_salesman, longest", - lambda: traveling_salesman(range(len(graph)), graph, find_longest=True), + lambda: traveling_salesman( + range(len(graph)), graph, find_longest=True), # noqa: B023 # fmt: skip longest, graph, print_only_on_error=True, diff --git a/tests/test_gears.py b/tests/test_gears.py index 8edc952..7235ced 100644 --- a/tests/test_gears.py +++ b/tests/test_gears.py @@ -25,7 +25,7 @@ class ProtocolAndABCNotImplementedErrors: Traceback (most recent call last): NotImplementedError - >>> nog.GearWithoutDistances.vertex_id_to_path_attributes_mapping(None, None) + >>> nog.GearWithoutDistances.vertex_id_to_edge_labels_mapping(None, None) Traceback (most recent call last): NotImplementedError @@ -33,6 +33,14 @@ class ProtocolAndABCNotImplementedErrors: Traceback (most recent call last): NotImplementedError + >>> nog.GearWithoutDistances.sequence_of_edge_labels(None, None) + Traceback (most recent call last): + NotImplementedError + + >>> nog.GearWithoutDistances.vertex_id_to_number_mapping(None, None) + Traceback (most recent call last): + NotImplementedError + >>> nog.Gear.zero(None) Traceback (most recent call last): NotImplementedError diff --git a/tests/test_nographs.py b/tests/test_nographs.py index c0a864a..d51c08a 100644 --- a/tests/test_nographs.py +++ b/tests/test_nographs.py @@ -1,9 +1,39 @@ if __name__ == "__main__": + """Test functionality and documentation of NoGraphs. With parameter *fast*, + slow tests are skipped. + """ import doctest import unittest import coverage # type: ignore import importlib import pathlib + from utils import DocTestFinderSkippingSlowTests, DocTestParserSkippingSlowTests + import sys + + # detect if we have package pymacros4py + skip_macro_consistency_check = False + try: + import pymacros4py # noqa: F401 + except ImportError: + skip_macro_consistency_check = True + + # Choose test finder for DocTestSuite and test parser for DocFileSuite + # When skip_slow_tests is True, skip tests marked by "doctest:+SLOW_TEST". + # (Only tests that are not needed for 100% coverage are marked as slow. Thus, + # during development, skip_slow_tests can be set to True without compromising + # test coverage. During CI, the flag needs to be set to FALSE in order to + # fully test everything, e.g., also long-running examples of the tutorial.) + skip_slow_tests = False + print(">>", sys.argv) + if len(sys.argv) > 1 and sys.argv[1] == "fast": + skip_slow_tests = True + print("Executing only fast tests") + test_finder = ( + DocTestFinderSkippingSlowTests() if skip_slow_tests else doctest.DocTestFinder() + ) + test_parser = ( + DocTestParserSkippingSlowTests() if skip_slow_tests else doctest.DocTestParser() + ) # Start recording coverage cov = coverage.Coverage(source_pkgs=["nographs"]) @@ -13,27 +43,41 @@ test_suite = unittest.TestSuite() # Test modules: doc tests + # (Slow are here the test of class + # test_traversals_and_searches.GearTestsTraversalsWithOrWithoutLabels + # but they are not marked as slow tests because they are needed for keeping + # coverage at 100% even during debugging.) for file in pathlib.Path("tests").glob("*.py"): file_name = file.name.removesuffix(".py") + if file_name == "test_expanded_template" and skip_macro_consistency_check: + print("Skipped", file, "(package pymacros4py not available)") + continue __import__(file_name) - test_suite.addTests(doctest.DocTestSuite(file_name)) + test_suite.addTests(doctest.DocTestSuite(file_name, test_finder=test_finder)) # Test modules: unit test classes new_suite = unittest.defaultTestLoader.discover("tests", pattern="test_unit*.py") test_suite.addTests(new_suite) # Package module source: doc tests - for file in pathlib.Path("src", "nographs").glob("*.py"): - file_name = file.name.removesuffix(".py") - if file_name == "__init__": + root = pathlib.Path("src", "nographs") + for file in root.glob("**/*.py"): + if file.name.removesuffix(".py") == "__init__": continue - module = "." + file_name + file = file.relative_to(root) + parts = list(file.parts) + parts[-1] = parts[-1][:-3] # remove suffix + module = "." + ".".join(parts) temp_module = importlib.import_module(module, "nographs") - test_suite.addTests(doctest.DocTestSuite(temp_module)) + test_suite.addTests(doctest.DocTestSuite(temp_module, test_finder=test_finder)) # Documentation: doc tests for file_path in pathlib.Path(".", "docs", "source").glob("*.rst"): - test_suite.addTests(doctest.DocFileSuite(str(file_path), module_relative=False)) + test_suite.addTests( + doctest.DocFileSuite( + str(file_path), module_relative=False, parser=test_parser + ) + ) verbosity = 1 # 1 normal, 2 for more details failfast = True # True diff --git a/tests/test_paths.py b/tests/test_paths.py index dc1e86a..7dc88d1 100644 --- a/tests/test_paths.py +++ b/tests/test_paths.py @@ -118,7 +118,7 @@ class PathsHandling: >>> gear = nog.GearForIntVertexIDsAndCFloats() >>> paths = _paths.PathsOfLabeledEdges( ... gear.vertex_id_to_vertex_mapping(()), - ... gear.vertex_id_to_path_attributes_mapping(()), + ... gear.vertex_id_to_edge_labels_mapping(()), ... None ... ) >>> paths.append_edge(0, 0, [0]) diff --git a/tests/test_strategies.py b/tests/test_strategies.py index 491ce06..84c7a01 100644 --- a/tests/test_strategies.py +++ b/tests/test_strategies.py @@ -1,6 +1,3 @@ -import nographs as nog # noqa: F401 (used only by doctests) - - class MethodsOfDummyCollectionClasses: """-- Methods of dummy collection classes. Objects of these classes are used as content of traversal objects as long as the true collections @@ -17,10 +14,7 @@ class MethodsOfDummyCollectionClasses: an Exceptioon or to return a constant value directly and in all cases. >>> # noinspection PyProtectedMember - >>> strategies = nog._strategies - - - >>> NoVisitedSet = strategies.NoVisitedSet + >>> from nographs._strategies.utils import NoVisitedSet >>> NoVisitedSet.__contains__(None, None) Traceback (most recent call last): RuntimeError: Traversal not started, no data to be accessed @@ -42,7 +36,8 @@ class MethodsOfDummyCollectionClasses: RuntimeError: Traversal not started, no data to be accessed - >>> NoDistancesMapping = strategies.NoDistancesMapping + >>> # noinspection PyProtectedMember + >>> from nographs._strategies.utils import NoDistancesMapping >>> NoDistancesMapping.__getitem__(None, None) Traceback (most recent call last): RuntimeError: Traversal not started, no data to be accessed diff --git a/tests/test_traversals_and_searches.py b/tests/test_traversals_and_searches.py index d63c97d..4470857 100644 --- a/tests/test_traversals_and_searches.py +++ b/tests/test_traversals_and_searches.py @@ -12,7 +12,7 @@ from nographs._compatibility import pairwise # noinspection PyProtectedMember -from nographs._strategies import StrRepr # NOQA F401 (import needed by doc tests) +from nographs._strategies.utils import StrRepr # NOQA F401 (import needed by doc tests) # ----- Utilities: Printing test results ----- @@ -34,6 +34,7 @@ def print_filled(s: str) -> None: class Comparable(Protocol): + @abstractmethod def __lt__(self: T_bound_comparable, other: T_bound_comparable) -> bool: ... @@ -54,52 +55,77 @@ class HashableComparable(Hashable, Comparable, Protocol): def _results_of_traversal( traversal: nog.Traversal[T_sortable_vertex, T_vertex_id, T_labels], - start_vertices: Iterable[T_sortable_vertex], -) -> dict[str, Any]: - """Completely traverse graph and print each state, collect vertices, check - that path container remains the same object, print all paths, return - original content of __dict__ of traversal object (caller can use this - for further checks).""" - vertices = set(start_vertices) + additional_vertices: Iterable[T_sortable_vertex], +) -> tuple[list[T_sortable_vertex], dict[str, Any]]: + """ + Print initial state, also covering the start vertices. Then, + completely traverse graph, and print each state. + + Return a sorted list of vertices that cover the vertices that have been reported + during the traversal, and the additional vertices. + + Check that path container remains the same object, and return the original content + of __dict__ of traversal object so that the caller can perform further checks. + """ # get traversal attributes before iterator starts, for comparison org_dict = dict(traversal.__dict__) + vertices = set(additional_vertices) print_filled(f"After start: {traversal.state_to_str(vertices)}") for vertex in traversal: vertices.add(vertex) print_filled(f"-> {vertex}: {traversal.state_to_str([vertex])}") if org_dict["paths"] is not traversal.paths: print("traversal.paths before and while traversal differ!") - # noinspection PyProtectedMember - if traversal._build_paths: - print("All paths:", [traversal.paths[vertex] for vertex in sorted(vertices)]) - return org_dict + return sorted(vertices), org_dict def results_standard( traversal: nog.Traversal[T_sortable_vertex, T_vertex_id, T_labels], start_vertices: Iterable[T_sortable_vertex], ) -> None: - """Completely traverse graph and print each state, collect vertices, check - that path container remains the same object, print all paths. """ - _ = _results_of_traversal(traversal, start_vertices) + Print the initial state, with details for the start vertices. + Completely traverse graph and print each state. + Print the final state, with details for the start vertices and the reported + vertices. + Check that path container remains the same object. + """ + reported_and_start_vertices, _ = _results_of_traversal(traversal, start_vertices) + print("Final state for the reported and the start vertices:") + print_filled(traversal.state_to_str(reported_and_start_vertices)) def results_with_visited( - traversal: TraversalWithDepthAndVisited[T_sortable_vertex, T_vertex_id, T_labels], - start_vertices: Iterable[T_sortable_vertex], + traversal: TraversalWithDepthAndVisited[ + T_sortable_vertex, T_sortable_vertex, T_labels + ], + additional_vertices: Iterable[T_sortable_vertex], + use_reported: bool = False, ) -> None: - """Completely traverse graph and print each state, collect vertices, check - that path container remains the same object, print all paths. - Additionally, print visited container and check that it remained the same object - during traversal. """ - org_dict = _results_of_traversal(traversal, start_vertices) + Like *results_standard*, but: + + Print the final state with details for the visited vertices, instead of for the + start vertices and the reported vertices (this is useful if results are + computed for all visited vertices, but not all of them are reported or + start vertices). This behaviour can be disabled by use_reported. + + And also check that traversal.visited remains the same object during traversal. + + Note: This function is limited to cases where both T_vertex_id and + T_vertex are T_sortable_vertex, and vertex_to_id == vertex_as_id. + """ + reported_and_start_vertices, org_dict = _results_of_traversal( + traversal, additional_vertices + ) if org_dict["visited"] is not traversal.visited: print("traversal.visited before and while traversal differ!") - visited = list(traversal.visited) - visited.sort() - print("All visited:", visited) + if use_reported: + print("Final state for the reported and the start vertices:") + print_filled(traversal.state_to_str(reported_and_start_vertices)) + else: + print("Final state for the visited vertices:") + print_filled(traversal.state_to_str(traversal.visited)) def results_with_distances( @@ -108,15 +134,17 @@ def results_with_distances( ], start_vertices: Iterable[T_sortable_vertex], ) -> None: - """Completely traverse graph and print each state, collect vertices, check - that path container remains the same object, print all paths. - Additionally, print distances container and check that it remained the same object - during traversal. """ - org_dict = _results_of_traversal(traversal, start_vertices) + Like *results_standard*, but also checks that traversal.distances stays + the same object during the traversal. + """ + reported_and_start_vertices, org_dict = _results_of_traversal( + traversal, start_vertices + ) if org_dict["distances"] is not traversal.distances: print("traversal.distances before and while traversal differ!") - print("All distances:", str(dict(traversal.distances))) + print("Final state for the reported and the start vertices:") + print_filled(traversal.state_to_str(reported_and_start_vertices)) def print_partial_results( @@ -124,7 +152,7 @@ def print_partial_results( paths_to: Optional[T_sortable_vertex] = None, ) -> None: """Completely traverse graph, collect reported vertices, and print the - fist 5 and the last 5. Print path prefixes and suffixes for vertices given + first 5 and the last 5. Print path prefixes and suffixes for vertices given as parameter. """ vertices = [] @@ -346,16 +374,16 @@ def __init__(self) -> None: self.start_impossible_bi = (self.start, self.goal_impossible) -class FDiamond(FixtureFull[int, tuple[int, int, int]]): +class FDiamond(FixtureFull[int, tuple[int, int, int, int]]): """Diamond-shaped graph of fixed size. Variant with weight 2 from vertices 1 and - 2 to 3 (useful foo TraversalShortestPaths). Edges can be interpreted as being - labeled instead of weighed. Additionally, a vertex for a test with this vertex - as already visited vertex is given. + 2 to 3 (useful for TraversalShortestPaths). Additionally, a vertex for a test with + this vertex as already visited vertex is given, and values for know distances for + vertex 0 and 1. """ def __init__(self) -> None: super().__init__( - [(0, 1, 2), (0, 2, 1), (1, 3, 2), (2, 3, 2)], + [(0, 1, 2, 1), (0, 2, 1, 2), (1, 3, 2, 3), (2, 3, 2, 4)], 0, 3, lambda v: 1 if v == 0 else 0, @@ -377,7 +405,31 @@ def __init__(self) -> None: ) -class FDiamond2(FixtureFull[int, tuple[int, int, int]]): +class FDiamondDFS(FixtureFull[int, tuple[int, int, int, int]]): + """Diamond-shaped graph of fixed size. Variant with two additional edges of kind + forward and back edge. Additionally, a vertex for a test with + this vertex as already visited vertex is given. + """ + + def __init__(self) -> None: + super().__init__( + [ + (0, 3, 2, 0), # Additional forward edge + (0, 1, 2, 1), + (0, 2, 1, 2), + (1, 3, 2, 3), + (2, 3, 2, 4), + (3, 0, 1, 5), # Additional back edge + ], + 0, + 3, + lambda v: 1 if v == 0 else 0, + report=True, + ) + self.vertex_for_already_visited = 1 + + +class FDiamondMST(FixtureFull[int, tuple[int, int, int]]): """Diamond-shaped graph of fixed size. Variant with weight 3 from vertices 1 and 2 to 3 (used for MST). No heuristic is given, since it is not used for A*. """ @@ -431,7 +483,7 @@ def __init__(self) -> None: class FMultiStart(FixtureFull[int, tuple[int, int, int]]): """Graph for testing multiple start vertices. Used for all strategies - except of A* and the bidirectional search strategies. Outgoing edges + except of DFS, A* and the bidirectional search strategies. Outgoing edges are sorted by ascending weight, since all weights are equal.""" def __init__(self) -> None: @@ -454,6 +506,35 @@ def __init__(self) -> None: self.goal_vertices = (4,) +class FMultiStartDFS(FixtureFull[int, tuple[int, int, int]]): + """Graph for testing multiple start vertices. Used for DFS - and for this, + it is equipped with forward, back, and cross edges and with already + visited start vertices. + Outgoing edges are sorted by ascending weight, since all weights are + equal.""" + + def __init__(self) -> None: + super().__init__( + [ + (0, 1, 1), + (1, 3, 2), # Forward edge + (1, 2, 3), + (5, 6, 4), + (6, 3, 5), + (2, 3, 6), + (2, 4, 7), + (3, 4, 8), # Cross edge? + (3, 1, 9), # Back edge + ], + -1, + 4, + lambda v: {6: 0, 3: 3}.get(v, 11), + report=True, + ) + self.start_vertices = (0, 5, 6) + self.goal_vertices = (4,) + + class FMultiStartAStar(FixtureFull[int, tuple[int, int, int]]): """Graph for testing multiple start vertices. Used for all strategies except of A* and the bidirectional search strategies.""" @@ -527,7 +608,7 @@ def next_edges(i: int, _: Any) -> Iterable[tuple[int, int, int]]: class FOvertaking(FixtureFull[int, tuple[int, int, int, int]]): """Graph for testing all strategies with different gears. It can be used to create a distance overflow for distance values stored in an array of - byte.""" + byte. It contains no cycles, which is required for topological search.""" def __init__(self) -> None: _enough_for_index_error = (1 + 128) * 8 # index error even for seq of bits @@ -546,6 +627,29 @@ def __init__(self) -> None: super().__init__(edges, 0, goal, lambda v: 0, report=False) +class FOvertakingDFSWithBackEdges(FixtureFull[int, tuple[int, int, int, int]]): + """Graph for testing DFS with different gears. It can be used + to create a distance overflow for distance values stored in an array of + byte. And it has back edges, which is required for a full DFS test.""" + + def __init__(self) -> None: + _enough_for_index_error = (1 + 128) * 8 # index error even for seq of bits + goal = 2 * _enough_for_index_error + limit = 3 * _enough_for_index_error + self.last_vertex = limit + 2 # todo: Goal? Remove? + + edges = [] + for v in range(0, limit, 2): + edges.append((v, v + 1, 1, 1)) + edges.append((v, v + 3, 3, 3)) + for v in range(1, limit, 2): + edges.append((v, v // 2, 8, 8)) + edges.append((v, v + 3, 1, 1)) + edges.append((v, v + 1, 3, 3)) + + super().__init__(edges, 0, goal, lambda v: 0, report=False) + + class FSequenceTo255(Fixture[int, tuple[int, int]]): """Graph with vertices from 0 to 255, edges from i to i+1, and 255 as goal. It can be used to create an infinity overflow of guessed distance (guess >= 255) @@ -600,7 +704,13 @@ class GraphWithoutEdges: [] >>> list(nog.TraversalDepthFirst(f.next_vertices).start_from(f.start, ... compute_depth=True)) - ? 0: {'depth': 0, 'visited': {0}, 'paths': {}} + ? 0: {'depth': 0, 'event': 'DFSEvent.ENTERING_START', 'visited': {0}, 'paths': + {}} + [] + >>> list(nog.TraversalDepthFirst(f.next_vertices).start_from(f.start, + ... compute_depth=True, compute_trace=True)) + ? 0: {'depth': 0, 'event': 'DFSEvent.ENTERING_START', 'trace': [0], 'visited': + {0}, 'paths': {}} [] >>> list(nog.TraversalNeighborsThenDepth(f.next_vertices).start_from(f.start, ... compute_depth=True)) @@ -742,22 +852,49 @@ class GraphWithoutEdges: ... next_labeled_edges=f.next_edges_bi) Traceback (most recent call last): RuntimeError: Both next_edges and next_labeled_edges provided. + + 4) For DFS with and without trace, we need do cover the following special + case: GearIntVertexID..., two start vertices, and the second comes first. + Line numbers at time where this test was made: + With trace: Line 656 of depth_first.py, line 450 of !depth_first.py + Without trace: Line 816 of depth_first.py, line 530 of !depth_first.py + >>> gear = nog.GearForIntVertexIDsAndCFloats() + >>> t = nog.TraversalDepthFirstFlex( + ... nog.vertex_as_id, gear, f.next_vertices) + >>> list(t.start_from(start_vertices=[1, 0], compute_trace=True, + ... build_paths=True)) + ? 1: {'depth': -1, 'event': 'DFSEvent.ENTERING_START', 'trace': [1], 'visited': + {1}, 'paths': {1: (1,)}} + ? 0: {'depth': -1, 'event': 'DFSEvent.ENTERING_START', 'trace': [0], 'visited': + {0, 1}, 'paths': {0: (0,)}} + [] + >>> list(t.start_from(start_vertices=[1, 0], compute_trace=False, + ... build_paths=True)) + ? 1: {'depth': -1, 'event': 'DFSEvent.ENTERING_START', 'visited': {1}, 'paths': + {1: (1,)}} + ? 0: {'depth': -1, 'event': 'DFSEvent.ENTERING_START', 'visited': {0, 1}, + 'paths': {0: (0,)}} + [] """ class GraphWithOneEdgeAndPathVariants: """-- Graph with one edge -- - Start vertex not reported (exception: topological sorting). First edge followed. + Start vertex not reported (exception: topological sorting, or DFS with + ENTERING_START). First edge followed. Paths not build if not demanded, and build if demanded. Labeled paths not allowed for unlabeled edges, and build for labeled edges, if demanded. Calculation limit raises Exception at exactly correct number of visited vertices. - >>> def test(traversal, goal, labeled_paths, *start_args): - ... print(list(traversal.start_from(*start_args))) # reported vertices + >>> def test(traversal, goal, labeled_paths, *start_vargs, **start_kwargs): + ... print(list(traversal.start_from(*start_vargs, **start_kwargs)) + ... ) # reported vertices ... print(goal not in traversal.paths) # no path build if not requested - ... print(next(traversal.start_from(*start_args))) # first reported vertex - ... _ = traversal.start_from(*start_args, build_paths=True).go_to(goal) + ... print(next(traversal.start_from(*start_vargs, **start_kwargs)) + ... ) # first reported vertex + ... _ = traversal.start_from(*start_vargs, **start_kwargs, build_paths=True) + ... _ = traversal.go_to(goal) ... if labeled_paths: # also print just vertices (edges are default here) ... print(tuple(traversal.paths.iter_vertices_from_start(1))) ... print(traversal.paths[goal]) @@ -794,12 +931,40 @@ class GraphWithOneEdgeAndPathVariants: True 1 (0, 1) + >>> test(traversal, f.goal, False, f.start, + ... report=nog.DFSEvent.ENTERING_SUCCESSOR|nog.DFSEvent.ENTERING_START) + [0, 1] + True + 0 + (0, 1) + >>> test(traversal, f.goal, False, f.start, compute_trace=True) + [1] + True + 1 + (0, 1) + >>> test(traversal, f.goal, False, f.start, compute_trace=True, + ... report=nog.DFSEvent.ENTERING_SUCCESSOR|nog.DFSEvent.ENTERING_START) + [0, 1] + True + 0 + (0, 1) + >>> test(traversal, f.goal, False, f.start, compute_trace=True, + ... report=nog.DFSEvent.LEAVING_SUCCESSOR|nog.DFSEvent.LEAVING_START) + [1, 0] + True + 1 + (0, 1) >>> traversal = nog.TraversalDepthFirst(next_edges=f.next_edges) >>> test(traversal, f.goal, False, f.start) [1] True 1 (0, 1) + >>> test(traversal, f.goal, False, f.start, compute_trace=True) + [1] + True + 1 + (0, 1) >>> traversal = nog.TraversalNeighborsThenDepth(f.next_vertices) >>> test(traversal, f.goal, False, f.start) [1] @@ -881,6 +1046,11 @@ class GraphWithOneEdgeAndPathVariants: >>> _ = list(traversal.start_from(f.start, calculation_limit=1)) Traceback (most recent call last): RuntimeError: Number of visited vertices reached limit + >>> list(traversal.start_from(f.start, calculation_limit=2, compute_trace=True)) + [1] + >>> _ = list(traversal.start_from(f.start, calculation_limit=1, compute_trace=True)) + Traceback (most recent call last): + RuntimeError: Number of visited vertices reached limit >>> traversal = nog.TraversalNeighborsThenDepth(next_edges=f.next_edges) >>> list(traversal.start_from(f.start, calculation_limit=2)) [1] @@ -966,6 +1136,12 @@ class GraphWithOneEdgeAndPathVariants: 1 (0, 1) ((0, 1, 2),) + >>> test(traversal, f.goal, True, f.start, compute_trace=True) + [1] + True + 1 + (0, 1) + ((0, 1, 2),) >>> traversal = nog.TraversalNeighborsThenDepth(next_labeled_edges=f.next_edges) >>> test(traversal, f.goal, True, f.start) [1] @@ -1051,6 +1227,7 @@ class GraphWithOneEdgeAndVertexToId: [1] ([0], [1]) {'depth': 1, 'visited': {0, 1}, 'paths': {[1]: ([0], [1])}} + >>> traversal = nog.TraversalDepthFirstFlex( ... first_of, nog.GearDefault(), f.next_vertices) >>> test(traversal, f.start, f.goal, labeled_paths=False) @@ -1058,7 +1235,39 @@ class GraphWithOneEdgeAndVertexToId: True [1] ([0], [1]) - {'depth': -1, 'visited': {0, 1}, 'paths': {[1]: ([0], [1])}} + {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1}, + 'paths': {[1]: ([0], [1])}} + >>> test(traversal, f.start, f.goal, labeled_paths=False, compute_trace=True) + [[1]] + True + [1] + ([0], [1]) + {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [[0], [1]], + 'visited': {0, 1}, 'paths': {[1]: ([0], [1])}} + >>> test(traversal, f.start, f.goal, labeled_paths=False, + ... report=nog.DFSEvent.ALL) + [[0], [1], [1], [0]] + True + [0] + ([0], [1]) + {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [[0], [1]], + 'on_trace': {0, 1}, 'index': {1: 2}, 'visited': {0, 1}, 'paths': {[1]: ([0], + [1])}} + >>> _ = traversal.start_from(start_vertex=f.start, build_paths=True, + ... report=nog.DFSEvent.ALL) + >>> for v in traversal: + ... print_filled(traversal.state_to_str([v])) + {'depth': -1, 'event': 'DFSEvent.ENTERING_START', 'trace': [[0]], 'on_trace': + {0}, 'index': {0: 1}, 'visited': {0}, 'paths': {[0]: ([0],)}} + {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [[0], [1]], + 'on_trace': {0, 1}, 'index': {1: 2}, 'visited': {0, 1}, 'paths': {[1]: ([0], + [1])}} + {'depth': -1, 'event': 'DFSEvent.LEAVING_SUCCESSOR', 'trace': [[0], [1]], + 'on_trace': {0, 1}, 'index': {1: 2}, 'visited': {0, 1}, 'paths': {[1]: ([0], + [1])}} + {'depth': -1, 'event': 'DFSEvent.LEAVING_START', 'trace': [[0]], 'on_trace': + {0}, 'index': {0: 1}, 'visited': {0, 1}, 'paths': {[0]: ([0],)}} + >>> traversal = nog.TraversalNeighborsThenDepthFlex( ... first_of, nog.GearDefault(), f.next_vertices) >>> test(traversal, f.start, f.goal, labeled_paths=False) @@ -1067,6 +1276,7 @@ class GraphWithOneEdgeAndVertexToId: [1] ([0], [1]) {'depth': -1, 'visited': {0, 1}, 'paths': {[1]: ([0], [1])}} + >>> traversal = nog.TraversalTopologicalSortFlex( ... first_of, nog.GearDefault(), f.next_vertices) >>> test(traversal, f.start, f.goal, labeled_paths=False) @@ -1076,6 +1286,7 @@ class GraphWithOneEdgeAndVertexToId: ([0], [1]) {'depth': 1, 'cycle_from_start': [], 'visited': {0, 1}, 'paths': {[1]: ([0], [1])}} + >>> search = nog.BSearchBreadthFirstFlex( ... first_of, nog.GearDefault(), f.next_vertices_bi) >>> test_bidirectional(search, f.start, f.goal, labeled_paths=False) @@ -1111,7 +1322,8 @@ class GraphWithOneEdgeAndVertexToId: True [1] ([0], [1]) - {'depth': -1, 'visited': {0, 1}, 'paths': {[1]: ([0], [1])}} + {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1}, + 'paths': {[1]: ([0], [1])}} >>> traversal = nog.TraversalDepthFirstFlex( ... first_of, nog.GearDefault(), @@ -1122,7 +1334,32 @@ class GraphWithOneEdgeAndVertexToId: [1] ([0], [1]) (([0], [1], 2),) - {'depth': -1, 'visited': {0, 1}, 'paths': {[1]: (([0], [1], 2),)}} + {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1}, + 'paths': {[1]: (([0], [1], 2),)}} + >>> test(traversal, f.start, f.goal, labeled_paths=True, + ... report=nog.DFSEvent.ALL) + [[0], [1], [1], [0]] + True + [0] + ([0], [1]) + (([0], [1], 2),) + {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [[0], [1]], + 'trace_labels': [2], 'on_trace': {0, 1}, 'index': {1: 2}, 'visited': {0, 1}, + 'paths': {[1]: (([0], [1], 2),)}} + >>> _ = traversal.start_from(start_vertex=f.start, build_paths=True, + ... report=nog.DFSEvent.ALL) + >>> for v in traversal: + ... print_filled(traversal.state_to_str([v])) + {'depth': -1, 'event': 'DFSEvent.ENTERING_START', 'trace': [[0]], 'on_trace': + {0}, 'index': {0: 1}, 'visited': {0}, 'paths': {[0]: ()}} + {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [[0], [1]], + 'trace_labels': [2], 'on_trace': {0, 1}, 'index': {1: 2}, 'visited': {0, 1}, + 'paths': {[1]: (([0], [1], 2),)}} + {'depth': -1, 'event': 'DFSEvent.LEAVING_SUCCESSOR', 'trace': [[0], [1]], + 'trace_labels': [2], 'on_trace': {0, 1}, 'index': {1: 2}, 'visited': {0, 1}, + 'paths': {[1]: (([0], [1], 2),)}} + {'depth': -1, 'event': 'DFSEvent.LEAVING_START', 'trace': [[0]], 'on_trace': + {0}, 'index': {0: 1}, 'visited': {0, 1}, 'paths': {[0]: ()}} >>> traversal = nog.TraversalNeighborsThenDepthFlex( ... first_of, nog.GearDefault(), next_edges=f.next_edges) @@ -1245,11 +1482,11 @@ class GraphWithOneEdgeAndVertexToId: class VertexToIdWithGoForVerticesInAndGoTo: """ >>> f = FSequenceUnhashable() - >>> def test_traversal(traversal, *vargs): - ... print(list(traversal.start_from(*vargs, f.start))) - ... _ = traversal.start_from(*vargs, f.start) + >>> def test_traversal(traversal, *vargs, **kwargs): + ... print(list(traversal.start_from(*vargs, f.start, **kwargs))) + ... _ = traversal.start_from(*vargs, f.start, **kwargs) ... print(list(traversal.go_for_vertices_in(f.goals))) - ... _ = traversal.start_from(*vargs, f.start, build_paths=True) + ... _ = traversal.start_from(*vargs, f.start, **kwargs, build_paths=True) ... print(list(traversal.go_to(f.goal))) ... print(traversal.paths[f.goal]) ... print_filled(traversal.state_to_str([f.goal])) @@ -1272,7 +1509,16 @@ class VertexToIdWithGoForVerticesInAndGoTo: [[1], [3]] [3] ([0], [1], [2], [3]) - {'depth': -1, 'visited': {0, 1, 2, 3}, 'paths': {[3]: ([0], [1], [2], [3])}} + {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, 3}, + 'paths': {[3]: ([0], [1], [2], [3])}} + >>> test_traversal(nog.TraversalDepthFirstFlex( + ... first_of, nog.GearDefault(), next_edges=f.next_edges), compute_trace=True) + [[1], [2], [3], [4]] + [[1], [3]] + [3] + ([0], [1], [2], [3]) + {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [[0], [1], [2], + [3]], 'visited': {0, 1, 2, 3}, 'paths': {[3]: ([0], [1], [2], [3])}} >>> test_traversal(nog.TraversalNeighborsThenDepthFlex( ... first_of, nog.GearDefault(), next_edges=f.next_edges)) [[1], [2], [3], [4]] @@ -1373,6 +1619,8 @@ class NormalGraphTraversalsWithOrWithoutLabels: def unattributed_edges() -> None: """ >>> f = FDiamond() + >>> fdfs = FDiamondDFS() + >>> traversal = nog.TraversalBreadthFirst(f.next_vertices) >>> traversal = traversal.start_from(f.start, build_paths=True) >>> results_with_visited(traversal, {f.start}) @@ -1384,37 +1632,214 @@ def unattributed_edges() -> None: -> 3: {'depth': 2, 'visited': {0, 1, 2, 3}, 'paths': {3: (0, 1, 3)}} ? 2: {'depth': 1, 'visited': {0, 1, 2, 3}, 'paths': {2: (0, 2)}} ? 3: {'depth': 2, 'visited': {0, 1, 2, 3}, 'paths': {3: (0, 1, 3)}} - All paths: [(0,), (0, 1), (0, 2), (0, 1, 3)] - All visited: [0, 1, 2, 3] + Final state for the visited vertices: + {'depth': 2, 'visited': {0, 1, 2, 3}, 'paths': {0: (0,), 1: (0, 1), 2: (0, 2), + 3: (0, 1, 3)}} + + + For DFS, we use a special graph that, when DFS is used, generates all + possible kinds of edges. And we need to check all the combinations of + options that uses different parts of the code. + >>> traversal = nog.TraversalDepthFirst(fdfs.next_vertices) + >>> traversal = traversal.start_from(fdfs.start, build_paths=True) + >>> results_with_visited(traversal, {fdfs.start}) + After start: {'depth': -1, 'visited': {}, 'paths': {}} + ? 0: {'depth': -1, 'event': 'DFSEvent.ENTERING_START', 'visited': {0}, 'paths': + {0: (0,)}} + -> 2: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 2}, + 'paths': {2: (0, 2)}} + ? 2: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 2}, + 'paths': {2: (0, 2)}} + -> 3: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 2, + 3}, 'paths': {3: (0, 2, 3)}} + ? 3: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 2, 3}, + 'paths': {3: (0, 2, 3)}} + -> 1: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, + 3}, 'paths': {1: (0, 1)}} + ? 1: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, + 3}, 'paths': {1: (0, 1)}} + Final state for the visited vertices: + {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, 3}, + 'paths': {0: (0,), 1: (0, 1), 2: (0, 2), 3: (0, 2, 3)}} + + >>> _ = traversal.start_from( + ... fdfs.start, build_paths=True, compute_depth=True) + >>> results_with_visited(traversal, {fdfs.start}) + After start: {'depth': 0, 'visited': {}, 'paths': {}} + ? 0: {'depth': 0, 'event': 'DFSEvent.ENTERING_START', 'visited': {0}, 'paths': + {0: (0,)}} + -> 2: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 2}, + 'paths': {2: (0, 2)}} + ? 2: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 2}, + 'paths': {2: (0, 2)}} + -> 3: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 2, 3}, + 'paths': {3: (0, 2, 3)}} + ? 3: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 2, 3}, + 'paths': {3: (0, 2, 3)}} + -> 1: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, + 3}, 'paths': {1: (0, 1)}} + ? 1: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, + 3}, 'paths': {1: (0, 1)}} + Final state for the visited vertices: + {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, 3}, + 'paths': {0: (0,), 1: (0, 1), 2: (0, 2), 3: (0, 2, 3)}} + + >>> _ = traversal.start_from( + ... fdfs.start, compute_trace=True) + >>> results_with_visited(traversal, {fdfs.start}) + After start: {'depth': -1, 'visited': {}, 'paths': {}} + ? 0: {'depth': -1, 'event': 'DFSEvent.ENTERING_START', 'trace': [0], 'visited': + {0}, 'paths': {}} + -> 2: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 2], + 'visited': {0, 2}, 'paths': {}} + ? 2: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 2], + 'visited': {0, 2}, 'paths': {}} + -> 3: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 2, 3], + 'visited': {0, 2, 3}, 'paths': {}} + ? 3: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 2, 3], + 'visited': {0, 2, 3}, 'paths': {}} + -> 1: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1], + 'visited': {0, 1, 2, 3}, 'paths': {}} + ? 1: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1], + 'visited': {0, 1, 2, 3}, 'paths': {}} + Final state for the visited vertices: + {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, 3}, + 'paths': {}} + + >>> _ = traversal.start_from(fdfs.start, build_paths=True, + ... report=nog.DFSEvent.ALL) + >>> results_with_visited(traversal, {fdfs.start}) + After start: {'depth': -1, 'visited': {}, 'paths': {}} + -> 0: {'depth': -1, 'event': 'DFSEvent.ENTERING_START', 'trace': [0], + 'on_trace': {0}, 'index': {0: 1}, 'visited': {0}, 'paths': {0: (0,)}} + ? 0: {'depth': -1, 'event': 'DFSEvent.ENTERING_START', 'trace': [0], 'on_trace': + {0}, 'index': {0: 1}, 'visited': {0}, 'paths': {0: (0,)}} + -> 2: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 2], + 'on_trace': {0, 2}, 'index': {2: 2}, 'visited': {0, 2}, 'paths': {2: (0, 2)}} + ? 2: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 2], + 'on_trace': {0, 2}, 'index': {2: 2}, 'visited': {0, 2}, 'paths': {2: (0, 2)}} + -> 3: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 2, 3], + 'on_trace': {0, 2, 3}, 'index': {3: 3}, 'visited': {0, 2, 3}, 'paths': {3: (0, + 2, 3)}} + ? 3: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 2, 3], + 'on_trace': {0, 2, 3}, 'index': {3: 3}, 'visited': {0, 2, 3}, 'paths': {3: (0, + 2, 3)}} + -> 0: {'depth': -1, 'event': 'DFSEvent.BACK_EDGE', 'trace': [0, 2, 3, 0], + 'on_trace': {0, 2, 3}, 'index': {0: 1}, 'visited': {0, 2, 3}, 'paths': {0: + (0,)}} + -> 3: {'depth': -1, 'event': 'DFSEvent.LEAVING_SUCCESSOR', 'trace': [0, 2, 3], + 'on_trace': {0, 2, 3}, 'index': {3: 3}, 'visited': {0, 2, 3}, 'paths': {3: (0, + 2, 3)}} + -> 2: {'depth': -1, 'event': 'DFSEvent.LEAVING_SUCCESSOR', 'trace': [0, 2], + 'on_trace': {0, 2}, 'index': {2: 2}, 'visited': {0, 2, 3}, 'paths': {2: (0, + 2)}} + -> 1: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1], + 'on_trace': {0, 1}, 'index': {1: 4}, 'visited': {0, 1, 2, 3}, 'paths': {1: (0, + 1)}} + ? 1: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1], + 'on_trace': {0, 1}, 'index': {1: 4}, 'visited': {0, 1, 2, 3}, 'paths': {1: (0, + 1)}} + -> 3: {'depth': -1, 'event': 'DFSEvent.CROSS_EDGE', 'trace': [0, 1, 3], + 'on_trace': {0, 1}, 'index': {3: 3}, 'visited': {0, 1, 2, 3}, 'paths': {3: (0, + 2, 3)}} + -> 1: {'depth': -1, 'event': 'DFSEvent.LEAVING_SUCCESSOR', 'trace': [0, 1], + 'on_trace': {0, 1}, 'index': {1: 4}, 'visited': {0, 1, 2, 3}, 'paths': {1: (0, + 1)}} + -> 3: {'depth': -1, 'event': 'DFSEvent.FORWARD_EDGE', 'trace': [0, 3], + 'on_trace': {0}, 'index': {3: 3}, 'visited': {0, 1, 2, 3}, 'paths': {3: (0, 2, + 3)}} + -> 0: {'depth': -1, 'event': 'DFSEvent.LEAVING_START', 'trace': [0], 'on_trace': + {0}, 'index': {0: 1}, 'visited': {0, 1, 2, 3}, 'paths': {0: (0,)}} + Final state for the visited vertices: + {'depth': -1, 'event': 'DFSEvent.LEAVING_START', 'index': {0: 1, 1: 4, 2: 2, 3: + 3}, 'visited': {0, 1, 2, 3}, 'paths': {0: (0,), 1: (0, 1), 2: (0, 2), 3: (0, + 2, 3)}} - >>> traversal = nog.TraversalDepthFirst(f.next_vertices) - >>> traversal = traversal.start_from(f.start, build_paths=True, - ... compute_depth=True) - >>> results_with_visited(traversal, {f.start}) - After start: {'depth': 0, 'visited': {0}, 'paths': {0: (0,)}} - ? 0: {'depth': 0, 'visited': {0}, 'paths': {0: (0,)}} - -> 2: {'depth': 1, 'visited': {0, 2}, 'paths': {2: (0, 2)}} - ? 2: {'depth': 1, 'visited': {0, 2}, 'paths': {2: (0, 2)}} - -> 3: {'depth': 2, 'visited': {0, 2, 3}, 'paths': {3: (0, 2, 3)}} - ? 3: {'depth': 2, 'visited': {0, 2, 3}, 'paths': {3: (0, 2, 3)}} - -> 1: {'depth': 1, 'visited': {0, 1, 2, 3}, 'paths': {1: (0, 1)}} - ? 1: {'depth': 1, 'visited': {0, 1, 2, 3}, 'paths': {1: (0, 1)}} - All paths: [(0,), (0, 1), (0, 2), (0, 2, 3)] - All visited: [0, 1, 2, 3] + >>> _ = traversal.start_from(fdfs.start, build_paths=True, + ... report=nog.DFSEvent.ALL, compute_depth=True) + >>> results_with_visited(traversal, {fdfs.start}) + After start: {'depth': 0, 'visited': {}, 'paths': {}} + -> 0: {'depth': 0, 'event': 'DFSEvent.ENTERING_START', 'trace': [0], 'on_trace': + {0}, 'index': {0: 1}, 'visited': {0}, 'paths': {0: (0,)}} + ? 0: {'depth': 0, 'event': 'DFSEvent.ENTERING_START', 'trace': [0], 'on_trace': + {0}, 'index': {0: 1}, 'visited': {0}, 'paths': {0: (0,)}} + -> 2: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 2], + 'on_trace': {0, 2}, 'index': {2: 2}, 'visited': {0, 2}, 'paths': {2: (0, 2)}} + ? 2: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 2], + 'on_trace': {0, 2}, 'index': {2: 2}, 'visited': {0, 2}, 'paths': {2: (0, 2)}} + -> 3: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 2, 3], + 'on_trace': {0, 2, 3}, 'index': {3: 3}, 'visited': {0, 2, 3}, 'paths': {3: (0, + 2, 3)}} + ? 3: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 2, 3], + 'on_trace': {0, 2, 3}, 'index': {3: 3}, 'visited': {0, 2, 3}, 'paths': {3: (0, + 2, 3)}} + -> 0: {'depth': 2, 'event': 'DFSEvent.BACK_EDGE', 'trace': [0, 2, 3, 0], + 'on_trace': {0, 2, 3}, 'index': {0: 1}, 'visited': {0, 2, 3}, 'paths': {0: + (0,)}} + -> 3: {'depth': 2, 'event': 'DFSEvent.LEAVING_SUCCESSOR', 'trace': [0, 2, 3], + 'on_trace': {0, 2, 3}, 'index': {3: 3}, 'visited': {0, 2, 3}, 'paths': {3: (0, + 2, 3)}} + -> 2: {'depth': 1, 'event': 'DFSEvent.LEAVING_SUCCESSOR', 'trace': [0, 2], + 'on_trace': {0, 2}, 'index': {2: 2}, 'visited': {0, 2, 3}, 'paths': {2: (0, + 2)}} + -> 1: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1], + 'on_trace': {0, 1}, 'index': {1: 4}, 'visited': {0, 1, 2, 3}, 'paths': {1: (0, + 1)}} + ? 1: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1], + 'on_trace': {0, 1}, 'index': {1: 4}, 'visited': {0, 1, 2, 3}, 'paths': {1: (0, + 1)}} + -> 3: {'depth': 1, 'event': 'DFSEvent.CROSS_EDGE', 'trace': [0, 1, 3], + 'on_trace': {0, 1}, 'index': {3: 3}, 'visited': {0, 1, 2, 3}, 'paths': {3: (0, + 2, 3)}} + -> 1: {'depth': 1, 'event': 'DFSEvent.LEAVING_SUCCESSOR', 'trace': [0, 1], + 'on_trace': {0, 1}, 'index': {1: 4}, 'visited': {0, 1, 2, 3}, 'paths': {1: (0, + 1)}} + -> 3: {'depth': 0, 'event': 'DFSEvent.FORWARD_EDGE', 'trace': [0, 3], + 'on_trace': {0}, 'index': {3: 3}, 'visited': {0, 1, 2, 3}, 'paths': {3: (0, 2, + 3)}} + -> 0: {'depth': 0, 'event': 'DFSEvent.LEAVING_START', 'trace': [0], 'on_trace': + {0}, 'index': {0: 1}, 'visited': {0, 1, 2, 3}, 'paths': {0: (0,)}} + Final state for the visited vertices: + {'depth': -1, 'event': 'DFSEvent.LEAVING_START', 'index': {0: 1, 1: 4, 2: 2, 3: + 3}, 'visited': {0, 1, 2, 3}, 'paths': {0: (0,), 1: (0, 1), 2: (0, 2), 3: (0, + 2, 3)}} + + >>> _ = traversal.start_from( + ... fdfs.start, build_paths=True, compute_depth=True, + ... report=nog.DFSEvent.IN_OUT | nog.DFSEvent.BACK_EDGE + ... ) + >>> results_with_visited(traversal, {fdfs.start}) + After start: {'depth': 0, 'visited': {}, 'paths': {}} + -> 0: {'depth': 0, 'event': 'DFSEvent.ENTERING_START', 'trace': [0], 'on_trace': + {0}, 'visited': {0}, 'paths': {0: (0,)}} + ? 0: {'depth': 0, 'event': 'DFSEvent.ENTERING_START', 'trace': [0], 'on_trace': + {0}, 'visited': {0}, 'paths': {0: (0,)}} + -> 2: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 2], + 'on_trace': {0, 2}, 'visited': {0, 2}, 'paths': {2: (0, 2)}} + ? 2: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 2], + 'on_trace': {0, 2}, 'visited': {0, 2}, 'paths': {2: (0, 2)}} + -> 3: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 2, 3], + 'on_trace': {0, 2, 3}, 'visited': {0, 2, 3}, 'paths': {3: (0, 2, 3)}} + ? 3: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 2, 3], + 'on_trace': {0, 2, 3}, 'visited': {0, 2, 3}, 'paths': {3: (0, 2, 3)}} + -> 0: {'depth': 2, 'event': 'DFSEvent.BACK_EDGE', 'trace': [0, 2, 3, 0], + 'on_trace': {0, 2, 3}, 'visited': {0, 2, 3}, 'paths': {0: (0,)}} + -> 3: {'depth': 2, 'event': 'DFSEvent.LEAVING_SUCCESSOR', 'trace': [0, 2, 3], + 'on_trace': {0, 2, 3}, 'visited': {0, 2, 3}, 'paths': {3: (0, 2, 3)}} + -> 2: {'depth': 1, 'event': 'DFSEvent.LEAVING_SUCCESSOR', 'trace': [0, 2], + 'on_trace': {0, 2}, 'visited': {0, 2, 3}, 'paths': {2: (0, 2)}} + -> 1: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1], + 'on_trace': {0, 1}, 'visited': {0, 1, 2, 3}, 'paths': {1: (0, 1)}} + ? 1: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1], + 'on_trace': {0, 1}, 'visited': {0, 1, 2, 3}, 'paths': {1: (0, 1)}} + -> 1: {'depth': 1, 'event': 'DFSEvent.LEAVING_SUCCESSOR', 'trace': [0, 1], + 'on_trace': {0, 1}, 'visited': {0, 1, 2, 3}, 'paths': {1: (0, 1)}} + -> 0: {'depth': 0, 'event': 'DFSEvent.LEAVING_START', 'trace': [0], 'on_trace': + {0}, 'visited': {0, 1, 2, 3}, 'paths': {0: (0,)}} + Final state for the visited vertices: + {'depth': -1, 'event': 'DFSEvent.LEAVING_START', 'visited': {0, 1, 2, 3}, + 'paths': {0: (0,), 1: (0, 1), 2: (0, 2), 3: (0, 2, 3)}} - >>> traversal = nog.TraversalDepthFirst(f.next_vertices) - >>> traversal = traversal.start_from(f.start, build_paths=True) - >>> results_with_visited(traversal, {f.start}) - After start: {'depth': 0, 'visited': {0}, 'paths': {0: (0,)}} - ? 0: {'depth': -1, 'visited': {0}, 'paths': {0: (0,)}} - -> 2: {'depth': -1, 'visited': {0, 2}, 'paths': {2: (0, 2)}} - ? 2: {'depth': -1, 'visited': {0, 2}, 'paths': {2: (0, 2)}} - -> 3: {'depth': -1, 'visited': {0, 2, 3}, 'paths': {3: (0, 2, 3)}} - ? 3: {'depth': -1, 'visited': {0, 2, 3}, 'paths': {3: (0, 2, 3)}} - -> 1: {'depth': -1, 'visited': {0, 1, 2, 3}, 'paths': {1: (0, 1)}} - ? 1: {'depth': -1, 'visited': {0, 1, 2, 3}, 'paths': {1: (0, 1)}} - All paths: [(0,), (0, 1), (0, 2), (0, 2, 3)] - All visited: [0, 1, 2, 3] >>> traversal = nog.TraversalNeighborsThenDepth(f.next_vertices) >>> traversal = traversal.start_from(f.start, build_paths=True, @@ -1428,13 +1853,14 @@ def unattributed_edges() -> None: -> 3: {'depth': 2, 'visited': {0, 1, 2, 3}, 'paths': {3: (0, 2, 3)}} ? 3: {'depth': 2, 'visited': {0, 1, 2, 3}, 'paths': {3: (0, 2, 3)}} ? 1: {'depth': 1, 'visited': {0, 1, 2, 3}, 'paths': {1: (0, 1)}} - All paths: [(0,), (0, 1), (0, 2), (0, 2, 3)] - All visited: [0, 1, 2, 3] + Final state for the visited vertices: + {'depth': 2, 'visited': {0, 1, 2, 3}, 'paths': {0: (0,), 1: (0, 1), 2: (0, 2), + 3: (0, 2, 3)}} >>> traversal = nog.TraversalNeighborsThenDepth(f.next_vertices) >>> traversal = traversal.start_from(f.start, build_paths=True) >>> results_with_visited(traversal, {f.start}) - After start: {'depth': 0, 'visited': {0}, 'paths': {0: (0,)}} + After start: {'depth': -1, 'visited': {0}, 'paths': {0: (0,)}} ? 0: {'depth': -1, 'visited': {0}, 'paths': {0: (0,)}} -> 1: {'depth': -1, 'visited': {0, 1}, 'paths': {1: (0, 1)}} -> 2: {'depth': -1, 'visited': {0, 1, 2}, 'paths': {2: (0, 2)}} @@ -1442,8 +1868,9 @@ def unattributed_edges() -> None: -> 3: {'depth': -1, 'visited': {0, 1, 2, 3}, 'paths': {3: (0, 2, 3)}} ? 3: {'depth': -1, 'visited': {0, 1, 2, 3}, 'paths': {3: (0, 2, 3)}} ? 1: {'depth': -1, 'visited': {0, 1, 2, 3}, 'paths': {1: (0, 1)}} - All paths: [(0,), (0, 1), (0, 2), (0, 2, 3)] - All visited: [0, 1, 2, 3] + Final state for the visited vertices: + {'depth': -1, 'visited': {0, 1, 2, 3}, 'paths': {0: (0,), 1: (0, 1), 2: (0, 2), + 3: (0, 2, 3)}} >>> traversal = nog.TraversalTopologicalSort(f.next_vertices) >>> traversal = traversal.start_from(f.start, build_paths=True) @@ -1465,8 +1892,9 @@ def unattributed_edges() -> None: (0, 1)}} -> 0: {'depth': 0, 'cycle_from_start': [], 'visited': {0, 1, 2, 3}, 'paths': {0: (0,)}} - All paths: [(0,), (0, 1), (0, 2), (0, 2, 3)] - All visited: [0, 1, 2, 3] + Final state for the visited vertices: + {'depth': 0, 'cycle_from_start': [], 'visited': {0, 1, 2, 3}, 'paths': {0: (0,), + 1: (0, 1), 2: (0, 2), 3: (0, 2, 3)}} >>> search = nog.BSearchBreadthFirst(f.next_vertices_bi) >>> l, p = search.start_from(f.start_bi, build_path=True) @@ -1481,6 +1909,7 @@ def unattributed_edges() -> None: def unattributed_edges_and_int_id_gear() -> None: """ >>> f = FDiamond() + >>> fdfs = FDiamondDFS() >>> gear = nog.GearForIntVertexIDsAndCFloats() >>> traversal = nog.TraversalBreadthFirstFlex( @@ -1495,39 +1924,153 @@ def unattributed_edges_and_int_id_gear() -> None: -> 3: {'depth': 2, 'visited': {0, 1, 2, 3}, 'paths': {3: (0, 1, 3)}} ? 2: {'depth': 1, 'visited': {0, 1, 2, 3}, 'paths': {2: (0, 2)}} ? 3: {'depth': 2, 'visited': {0, 1, 2, 3}, 'paths': {3: (0, 1, 3)}} - All paths: [(0,), (0, 1), (0, 2), (0, 1, 3)] - All visited: [0, 1, 2, 3] + Final state for the visited vertices: + {'depth': 2, 'visited': {0, 1, 2, 3}, 'paths': {0: (0,), 1: (0, 1), 2: (0, 2), + 3: (0, 1, 3)}} >>> traversal = nog.TraversalDepthFirstFlex( - ... nog.vertex_as_id, gear, f.next_vertices) - >>> traversal = traversal.start_from(f.start, build_paths=True, + ... nog.vertex_as_id, gear, fdfs.next_vertices) + >>> traversal = traversal.start_from(fdfs.start, build_paths=True) + >>> results_with_visited(traversal, {fdfs.start}) + After start: {'depth': -1, 'visited': {}, 'paths': {}} + ? 0: {'depth': -1, 'event': 'DFSEvent.ENTERING_START', 'visited': {0}, 'paths': + {0: (0,)}} + -> 2: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 2}, + 'paths': {2: (0, 2)}} + ? 2: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 2}, + 'paths': {2: (0, 2)}} + -> 3: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 2, + 3}, 'paths': {3: (0, 2, 3)}} + ? 3: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 2, 3}, + 'paths': {3: (0, 2, 3)}} + -> 1: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, + 3}, 'paths': {1: (0, 1)}} + ? 1: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, + 3}, 'paths': {1: (0, 1)}} + Final state for the visited vertices: + {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, 3}, + 'paths': {0: (0,), 1: (0, 1), 2: (0, 2), 3: (0, 2, 3)}} + + >>> traversal = traversal.start_from(fdfs.start, build_paths=True, ... compute_depth=True) - >>> results_with_visited(traversal, {f.start}) - After start: {'depth': 0, 'visited': {0}, 'paths': {0: (0,)}} - ? 0: {'depth': 0, 'visited': {0}, 'paths': {0: (0,)}} - -> 2: {'depth': 1, 'visited': {0, 2}, 'paths': {2: (0, 2)}} - ? 2: {'depth': 1, 'visited': {0, 2}, 'paths': {2: (0, 2)}} - -> 3: {'depth': 2, 'visited': {0, 2, 3}, 'paths': {3: (0, 2, 3)}} - ? 3: {'depth': 2, 'visited': {0, 2, 3}, 'paths': {3: (0, 2, 3)}} - -> 1: {'depth': 1, 'visited': {0, 1, 2, 3}, 'paths': {1: (0, 1)}} - ? 1: {'depth': 1, 'visited': {0, 1, 2, 3}, 'paths': {1: (0, 1)}} - All paths: [(0,), (0, 1), (0, 2), (0, 2, 3)] - All visited: [0, 1, 2, 3] + >>> results_with_visited(traversal, {fdfs.start}) + After start: {'depth': 0, 'visited': {}, 'paths': {}} + ? 0: {'depth': 0, 'event': 'DFSEvent.ENTERING_START', 'visited': {0}, 'paths': + {0: (0,)}} + -> 2: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 2}, + 'paths': {2: (0, 2)}} + ? 2: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 2}, + 'paths': {2: (0, 2)}} + -> 3: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 2, 3}, + 'paths': {3: (0, 2, 3)}} + ? 3: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 2, 3}, + 'paths': {3: (0, 2, 3)}} + -> 1: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, + 3}, 'paths': {1: (0, 1)}} + ? 1: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, + 3}, 'paths': {1: (0, 1)}} + Final state for the visited vertices: + {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, 3}, + 'paths': {0: (0,), 1: (0, 1), 2: (0, 2), 3: (0, 2, 3)}} + + >>> traversal = traversal.start_from(fdfs.start, build_paths=True, + ... report=nog.DFSEvent.ALL) + >>> results_with_visited(traversal, {fdfs.start}) + After start: {'depth': -1, 'visited': {}, 'paths': {}} + -> 0: {'depth': -1, 'event': 'DFSEvent.ENTERING_START', 'trace': [0], + 'on_trace': {0}, 'index': {0: 1}, 'visited': {0}, 'paths': {0: (0,)}} + ? 0: {'depth': -1, 'event': 'DFSEvent.ENTERING_START', 'trace': [0], 'on_trace': + {0}, 'index': {0: 1}, 'visited': {0}, 'paths': {0: (0,)}} + -> 2: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 2], + 'on_trace': {0, 2}, 'index': {2: 2}, 'visited': {0, 2}, 'paths': {2: (0, 2)}} + ? 2: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 2], + 'on_trace': {0, 2}, 'index': {2: 2}, 'visited': {0, 2}, 'paths': {2: (0, 2)}} + -> 3: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 2, 3], + 'on_trace': {0, 2, 3}, 'index': {3: 3}, 'visited': {0, 2, 3}, 'paths': {3: (0, + 2, 3)}} + ? 3: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 2, 3], + 'on_trace': {0, 2, 3}, 'index': {3: 3}, 'visited': {0, 2, 3}, 'paths': {3: (0, + 2, 3)}} + -> 0: {'depth': -1, 'event': 'DFSEvent.BACK_EDGE', 'trace': [0, 2, 3, 0], + 'on_trace': {0, 2, 3}, 'index': {0: 1}, 'visited': {0, 2, 3}, 'paths': {0: + (0,)}} + -> 3: {'depth': -1, 'event': 'DFSEvent.LEAVING_SUCCESSOR', 'trace': [0, 2, 3], + 'on_trace': {0, 2, 3}, 'index': {3: 3}, 'visited': {0, 2, 3}, 'paths': {3: (0, + 2, 3)}} + -> 2: {'depth': -1, 'event': 'DFSEvent.LEAVING_SUCCESSOR', 'trace': [0, 2], + 'on_trace': {0, 2}, 'index': {2: 2}, 'visited': {0, 2, 3}, 'paths': {2: (0, + 2)}} + -> 1: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1], + 'on_trace': {0, 1}, 'index': {1: 4}, 'visited': {0, 1, 2, 3}, 'paths': {1: (0, + 1)}} + ? 1: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1], + 'on_trace': {0, 1}, 'index': {1: 4}, 'visited': {0, 1, 2, 3}, 'paths': {1: (0, + 1)}} + -> 3: {'depth': -1, 'event': 'DFSEvent.CROSS_EDGE', 'trace': [0, 1, 3], + 'on_trace': {0, 1}, 'index': {3: 3}, 'visited': {0, 1, 2, 3}, 'paths': {3: (0, + 2, 3)}} + -> 1: {'depth': -1, 'event': 'DFSEvent.LEAVING_SUCCESSOR', 'trace': [0, 1], + 'on_trace': {0, 1}, 'index': {1: 4}, 'visited': {0, 1, 2, 3}, 'paths': {1: (0, + 1)}} + -> 3: {'depth': -1, 'event': 'DFSEvent.FORWARD_EDGE', 'trace': [0, 3], + 'on_trace': {0}, 'index': {3: 3}, 'visited': {0, 1, 2, 3}, 'paths': {3: (0, 2, + 3)}} + -> 0: {'depth': -1, 'event': 'DFSEvent.LEAVING_START', 'trace': [0], 'on_trace': + {0}, 'index': {0: 1}, 'visited': {0, 1, 2, 3}, 'paths': {0: (0,)}} + Final state for the visited vertices: + {'depth': -1, 'event': 'DFSEvent.LEAVING_START', 'index': {0: 1, 1: 4, 2: 2, 3: + 3}, 'visited': {0, 1, 2, 3}, 'paths': {0: (0,), 1: (0, 1), 2: (0, 2), 3: (0, + 2, 3)}} - >>> traversal = nog.TraversalDepthFirstFlex( - ... nog.vertex_as_id, gear, f.next_vertices) - >>> traversal = traversal.start_from(f.start, build_paths=True) - >>> results_with_visited(traversal, {f.start}) - After start: {'depth': 0, 'visited': {0}, 'paths': {0: (0,)}} - ? 0: {'depth': -1, 'visited': {0}, 'paths': {0: (0,)}} - -> 2: {'depth': -1, 'visited': {0, 2}, 'paths': {2: (0, 2)}} - ? 2: {'depth': -1, 'visited': {0, 2}, 'paths': {2: (0, 2)}} - -> 3: {'depth': -1, 'visited': {0, 2, 3}, 'paths': {3: (0, 2, 3)}} - ? 3: {'depth': -1, 'visited': {0, 2, 3}, 'paths': {3: (0, 2, 3)}} - -> 1: {'depth': -1, 'visited': {0, 1, 2, 3}, 'paths': {1: (0, 1)}} - ? 1: {'depth': -1, 'visited': {0, 1, 2, 3}, 'paths': {1: (0, 1)}} - All paths: [(0,), (0, 1), (0, 2), (0, 2, 3)] - All visited: [0, 1, 2, 3] + >>> traversal = traversal.start_from( + ... fdfs.start, build_paths=True, + ... compute_depth=True, report=nog.DFSEvent.ALL) + >>> results_with_visited(traversal, {fdfs.start}) + After start: {'depth': 0, 'visited': {}, 'paths': {}} + -> 0: {'depth': 0, 'event': 'DFSEvent.ENTERING_START', 'trace': [0], 'on_trace': + {0}, 'index': {0: 1}, 'visited': {0}, 'paths': {0: (0,)}} + ? 0: {'depth': 0, 'event': 'DFSEvent.ENTERING_START', 'trace': [0], 'on_trace': + {0}, 'index': {0: 1}, 'visited': {0}, 'paths': {0: (0,)}} + -> 2: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 2], + 'on_trace': {0, 2}, 'index': {2: 2}, 'visited': {0, 2}, 'paths': {2: (0, 2)}} + ? 2: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 2], + 'on_trace': {0, 2}, 'index': {2: 2}, 'visited': {0, 2}, 'paths': {2: (0, 2)}} + -> 3: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 2, 3], + 'on_trace': {0, 2, 3}, 'index': {3: 3}, 'visited': {0, 2, 3}, 'paths': {3: (0, + 2, 3)}} + ? 3: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 2, 3], + 'on_trace': {0, 2, 3}, 'index': {3: 3}, 'visited': {0, 2, 3}, 'paths': {3: (0, + 2, 3)}} + -> 0: {'depth': 2, 'event': 'DFSEvent.BACK_EDGE', 'trace': [0, 2, 3, 0], + 'on_trace': {0, 2, 3}, 'index': {0: 1}, 'visited': {0, 2, 3}, 'paths': {0: + (0,)}} + -> 3: {'depth': 2, 'event': 'DFSEvent.LEAVING_SUCCESSOR', 'trace': [0, 2, 3], + 'on_trace': {0, 2, 3}, 'index': {3: 3}, 'visited': {0, 2, 3}, 'paths': {3: (0, + 2, 3)}} + -> 2: {'depth': 1, 'event': 'DFSEvent.LEAVING_SUCCESSOR', 'trace': [0, 2], + 'on_trace': {0, 2}, 'index': {2: 2}, 'visited': {0, 2, 3}, 'paths': {2: (0, + 2)}} + -> 1: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1], + 'on_trace': {0, 1}, 'index': {1: 4}, 'visited': {0, 1, 2, 3}, 'paths': {1: (0, + 1)}} + ? 1: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1], + 'on_trace': {0, 1}, 'index': {1: 4}, 'visited': {0, 1, 2, 3}, 'paths': {1: (0, + 1)}} + -> 3: {'depth': 1, 'event': 'DFSEvent.CROSS_EDGE', 'trace': [0, 1, 3], + 'on_trace': {0, 1}, 'index': {3: 3}, 'visited': {0, 1, 2, 3}, 'paths': {3: (0, + 2, 3)}} + -> 1: {'depth': 1, 'event': 'DFSEvent.LEAVING_SUCCESSOR', 'trace': [0, 1], + 'on_trace': {0, 1}, 'index': {1: 4}, 'visited': {0, 1, 2, 3}, 'paths': {1: (0, + 1)}} + -> 3: {'depth': 0, 'event': 'DFSEvent.FORWARD_EDGE', 'trace': [0, 3], + 'on_trace': {0}, 'index': {3: 3}, 'visited': {0, 1, 2, 3}, 'paths': {3: (0, 2, + 3)}} + -> 0: {'depth': 0, 'event': 'DFSEvent.LEAVING_START', 'trace': [0], 'on_trace': + {0}, 'index': {0: 1}, 'visited': {0, 1, 2, 3}, 'paths': {0: (0,)}} + Final state for the visited vertices: + {'depth': -1, 'event': 'DFSEvent.LEAVING_START', 'index': {0: 1, 1: 4, 2: 2, 3: + 3}, 'visited': {0, 1, 2, 3}, 'paths': {0: (0,), 1: (0, 1), 2: (0, 2), 3: (0, + 2, 3)}} >>> traversal = nog.TraversalNeighborsThenDepthFlex( ... nog.vertex_as_id, gear, f.next_vertices) @@ -1542,14 +2085,15 @@ def unattributed_edges_and_int_id_gear() -> None: -> 3: {'depth': 2, 'visited': {0, 1, 2, 3}, 'paths': {3: (0, 2, 3)}} ? 3: {'depth': 2, 'visited': {0, 1, 2, 3}, 'paths': {3: (0, 2, 3)}} ? 1: {'depth': 1, 'visited': {0, 1, 2, 3}, 'paths': {1: (0, 1)}} - All paths: [(0,), (0, 1), (0, 2), (0, 2, 3)] - All visited: [0, 1, 2, 3] + Final state for the visited vertices: + {'depth': 2, 'visited': {0, 1, 2, 3}, 'paths': {0: (0,), 1: (0, 1), 2: (0, 2), + 3: (0, 2, 3)}} >>> traversal = nog.TraversalNeighborsThenDepthFlex( ... nog.vertex_as_id, gear, f.next_vertices) >>> traversal = traversal.start_from(f.start, build_paths=True) >>> results_with_visited(traversal, {f.start}) - After start: {'depth': 0, 'visited': {0}, 'paths': {0: (0,)}} + After start: {'depth': -1, 'visited': {0}, 'paths': {0: (0,)}} ? 0: {'depth': -1, 'visited': {0}, 'paths': {0: (0,)}} -> 1: {'depth': -1, 'visited': {0, 1}, 'paths': {1: (0, 1)}} -> 2: {'depth': -1, 'visited': {0, 1, 2}, 'paths': {2: (0, 2)}} @@ -1557,8 +2101,9 @@ def unattributed_edges_and_int_id_gear() -> None: -> 3: {'depth': -1, 'visited': {0, 1, 2, 3}, 'paths': {3: (0, 2, 3)}} ? 3: {'depth': -1, 'visited': {0, 1, 2, 3}, 'paths': {3: (0, 2, 3)}} ? 1: {'depth': -1, 'visited': {0, 1, 2, 3}, 'paths': {1: (0, 1)}} - All paths: [(0,), (0, 1), (0, 2), (0, 2, 3)] - All visited: [0, 1, 2, 3] + Final state for the visited vertices: + {'depth': -1, 'visited': {0, 1, 2, 3}, 'paths': {0: (0,), 1: (0, 1), 2: (0, 2), + 3: (0, 2, 3)}} >>> traversal = nog.TraversalTopologicalSortFlex( ... nog.vertex_as_id, gear, f.next_vertices) @@ -1581,8 +2126,9 @@ def unattributed_edges_and_int_id_gear() -> None: (0, 1)}} -> 0: {'depth': 0, 'cycle_from_start': [], 'visited': {0, 1, 2, 3}, 'paths': {0: (0,)}} - All paths: [(0,), (0, 1), (0, 2), (0, 2, 3)] - All visited: [0, 1, 2, 3] + Final state for the visited vertices: + {'depth': 0, 'cycle_from_start': [], 'visited': {0, 1, 2, 3}, 'paths': {0: (0,), + 1: (0, 1), 2: (0, 2), 3: (0, 2, 3)}} >>> search = nog.BSearchBreadthFirstFlex( ... nog.vertex_as_id, gear, f.next_vertices_bi) @@ -1599,6 +2145,7 @@ def unlabeled_edges_and_already_visited() -> None: """ 1b. Unlabeled edges and already_visited >>> f = FDiamond() + >>> fdfs = FDiamondDFS() >>> traversal = nog.TraversalBreadthFirst(f.next_vertices) >>> already_visited={f.vertex_for_already_visited} @@ -1611,8 +2158,9 @@ def unlabeled_edges_and_already_visited() -> None: ? 2: {'depth': 1, 'visited': {0, 1, 2}, 'paths': {2: (0, 2)}} -> 3: {'depth': 2, 'visited': {0, 1, 2, 3}, 'paths': {3: (0, 2, 3)}} ? 3: {'depth': 2, 'visited': {0, 1, 2, 3}, 'paths': {3: (0, 2, 3)}} - All paths: [(0,), (0, 2), (0, 2, 3)] - All visited: [0, 1, 2, 3] + Final state for the visited vertices: + {'depth': 2, 'visited': {0, 1, 2, 3}, 'paths': {0: (0,), 2: (0, 2), 3: (0, 2, + 3)}} >>> print("Already visited:", StrRepr.from_set(already_visited)) Already visited: {0, 1, 2, 3} >>> traversal.paths[f.vertex_for_already_visited] @@ -1620,22 +2168,53 @@ def unlabeled_edges_and_already_visited() -> None: RuntimeError: Paths: No path for given vertex. >>> traversal = nog.TraversalDepthFirst(f.next_vertices) - >>> already_visited={f.vertex_for_already_visited} + >>> already_visited={fdfs.vertex_for_already_visited} >>> traversal = traversal.start_from( - ... f.start, build_paths=True, compute_depth=True, + ... fdfs.start, build_paths=True, compute_depth=True, ... already_visited=already_visited) - >>> results_with_visited(traversal, {f.start}) - After start: {'depth': 0, 'visited': {0, 1}, 'paths': {0: (0,)}} - ? 0: {'depth': 0, 'visited': {0, 1}, 'paths': {0: (0,)}} - -> 2: {'depth': 1, 'visited': {0, 1, 2}, 'paths': {2: (0, 2)}} - ? 2: {'depth': 1, 'visited': {0, 1, 2}, 'paths': {2: (0, 2)}} - -> 3: {'depth': 2, 'visited': {0, 1, 2, 3}, 'paths': {3: (0, 2, 3)}} - ? 3: {'depth': 2, 'visited': {0, 1, 2, 3}, 'paths': {3: (0, 2, 3)}} - All paths: [(0,), (0, 2), (0, 2, 3)] - All visited: [0, 1, 2, 3] + >>> results_with_visited(traversal, {fdfs.start}) + After start: {'depth': 0, 'visited': {1}, 'paths': {}} + ? 0: {'depth': 0, 'event': 'DFSEvent.ENTERING_START', 'visited': {0, 1}, + 'paths': {0: (0,)}} + -> 2: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2}, + 'paths': {2: (0, 2)}} + ? 2: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2}, + 'paths': {2: (0, 2)}} + -> 3: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, + 3}, 'paths': {3: (0, 2, 3)}} + ? 3: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, + 3}, 'paths': {3: (0, 2, 3)}} + Final state for the visited vertices: + {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, 3}, + 'paths': {0: (0,), 2: (0, 2), 3: (0, 2, 3)}} >>> print("Already visited:", StrRepr.from_set(already_visited)) Already visited: {0, 1, 2, 3} - >>> traversal.paths[f.vertex_for_already_visited] + >>> traversal.paths[fdfs.vertex_for_already_visited] + Traceback (most recent call last): + RuntimeError: Paths: No path for given vertex. + + >>> already_visited={fdfs.vertex_for_already_visited} + >>> traversal = traversal.start_from( + ... fdfs.start, build_paths=True, compute_depth=True, + ... compute_trace=True, already_visited=already_visited) + >>> results_with_visited(traversal, {fdfs.start}) + After start: {'depth': 0, 'visited': {1}, 'paths': {}} + ? 0: {'depth': 0, 'event': 'DFSEvent.ENTERING_START', 'trace': [0], 'visited': + {0, 1}, 'paths': {0: (0,)}} + -> 2: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 2], + 'visited': {0, 1, 2}, 'paths': {2: (0, 2)}} + ? 2: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 2], + 'visited': {0, 1, 2}, 'paths': {2: (0, 2)}} + -> 3: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 2, 3], + 'visited': {0, 1, 2, 3}, 'paths': {3: (0, 2, 3)}} + ? 3: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 2, 3], + 'visited': {0, 1, 2, 3}, 'paths': {3: (0, 2, 3)}} + Final state for the visited vertices: + {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, 3}, + 'paths': {0: (0,), 2: (0, 2), 3: (0, 2, 3)}} + >>> print("Already visited:", StrRepr.from_set(already_visited)) + Already visited: {0, 1, 2, 3} + >>> traversal.paths[fdfs.vertex_for_already_visited] Traceback (most recent call last): RuntimeError: Paths: No path for given vertex. @@ -1651,8 +2230,9 @@ def unlabeled_edges_and_already_visited() -> None: ? 2: {'depth': 1, 'visited': {0, 1, 2}, 'paths': {2: (0, 2)}} -> 3: {'depth': 2, 'visited': {0, 1, 2, 3}, 'paths': {3: (0, 2, 3)}} ? 3: {'depth': 2, 'visited': {0, 1, 2, 3}, 'paths': {3: (0, 2, 3)}} - All paths: [(0,), (0, 2), (0, 2, 3)] - All visited: [0, 1, 2, 3] + Final state for the visited vertices: + {'depth': 3, 'visited': {0, 1, 2, 3}, 'paths': {0: (0,), 2: (0, 2), 3: (0, 2, + 3)}} >>> print("Already visited:", StrRepr.from_set(already_visited)) Already visited: {0, 1, 2, 3} >>> traversal.paths[f.vertex_for_already_visited] @@ -1677,8 +2257,9 @@ def unlabeled_edges_and_already_visited() -> None: (0, 2)}} -> 0: {'depth': 0, 'cycle_from_start': [], 'visited': {0, 1, 2, 3}, 'paths': {0: (0,)}} - All paths: [(0,), (0, 2), (0, 2, 3)] - All visited: [0, 1, 2, 3] + Final state for the visited vertices: + {'depth': 0, 'cycle_from_start': [], 'visited': {0, 1, 2, 3}, 'paths': {0: (0,), + 2: (0, 2), 3: (0, 2, 3)}} >>> print("Already visited:", StrRepr.from_set(already_visited)) Already visited: {0, 1, 2, 3} >>> traversal.paths[f.vertex_for_already_visited] @@ -1704,8 +2285,9 @@ def unattributed_edges_and_seq_based_already_visited() -> None: ? 2: {'depth': 1, 'visited': {0, 1, 2}, 'paths': {2: (0, 2)}} -> 3: {'depth': 2, 'visited': {0, 1, 2, 3}, 'paths': {3: (0, 2, 3)}} ? 3: {'depth': 2, 'visited': {0, 1, 2, 3}, 'paths': {3: (0, 2, 3)}} - All paths: [(0,), (0, 2), (0, 2, 3)] - All visited: [0, 1, 2, 3] + Final state for the visited vertices: + {'depth': 2, 'visited': {0, 1, 2, 3}, 'paths': {0: (0,), 2: (0, 2), 3: (0, 2, + 3)}} >>> print("Already visited:", already_visited) Already visited: {0, 1, 2, 3} >>> traversal.paths[f.vertex_for_already_visited] @@ -1718,76 +2300,143 @@ def unattributed_edges_and_seq_based_already_visited() -> None: def labeled_edges() -> None: """ >>> f = FDiamond() + >>> fdfs = FDiamondDFS() >>> traversal = nog.TraversalBreadthFirst( - ... next_edges=f.next_edges) + ... next_labeled_edges=f.next_edges) >>> traversal = traversal.start_from(f.start, build_paths=True) >>> results_with_visited(traversal, {f.start}) - After start: {'depth': 0, 'visited': {0}, 'paths': {0: (0,)}} - ? 0: {'depth': 0, 'visited': {0}, 'paths': {0: (0,)}} - -> 1: {'depth': 1, 'visited': {0, 1}, 'paths': {1: (0, 1)}} - -> 2: {'depth': 1, 'visited': {0, 1, 2}, 'paths': {2: (0, 2)}} - ? 1: {'depth': 1, 'visited': {0, 1, 2}, 'paths': {1: (0, 1)}} - -> 3: {'depth': 2, 'visited': {0, 1, 2, 3}, 'paths': {3: (0, 1, 3)}} - ? 2: {'depth': 1, 'visited': {0, 1, 2, 3}, 'paths': {2: (0, 2)}} - ? 3: {'depth': 2, 'visited': {0, 1, 2, 3}, 'paths': {3: (0, 1, 3)}} - All paths: [(0,), (0, 1), (0, 2), (0, 1, 3)] - All visited: [0, 1, 2, 3] - - >>> traversal = nog.TraversalDepthFirst( - ... next_edges=f.next_edges) - >>> traversal = traversal.start_from(f.start, build_paths=True, + After start: {'depth': 0, 'visited': {0}, 'paths': {0: ()}} + ? 0: {'depth': 0, 'visited': {0}, 'paths': {0: ()}} + -> 1: {'depth': 1, 'visited': {0, 1}, 'paths': {1: ((0, 1, 1),)}} + -> 2: {'depth': 1, 'visited': {0, 1, 2}, 'paths': {2: ((0, 2, 2),)}} + ? 1: {'depth': 1, 'visited': {0, 1, 2}, 'paths': {1: ((0, 1, 1),)}} + -> 3: {'depth': 2, 'visited': {0, 1, 2, 3}, 'paths': {3: ((0, 1, 1), (1, 3, + 3))}} + ? 2: {'depth': 1, 'visited': {0, 1, 2, 3}, 'paths': {2: ((0, 2, 2),)}} + ? 3: {'depth': 2, 'visited': {0, 1, 2, 3}, 'paths': {3: ((0, 1, 1), (1, 3, 3))}} + Final state for the visited vertices: + {'depth': 2, 'visited': {0, 1, 2, 3}, 'paths': {0: (), 1: ((0, 1, 1),), 2: ((0, + 2, 2),), 3: ((0, 1, 1), (1, 3, 3))}} + + >>> traversal = nog.TraversalDepthFirst(next_labeled_edges=fdfs.next_edges) + >>> traversal = traversal.start_from(fdfs.start, build_paths=True, ... compute_depth=True) - >>> results_with_visited(traversal, {f.start}) - After start: {'depth': 0, 'visited': {0}, 'paths': {0: (0,)}} - ? 0: {'depth': 0, 'visited': {0}, 'paths': {0: (0,)}} - -> 2: {'depth': 1, 'visited': {0, 2}, 'paths': {2: (0, 2)}} - ? 2: {'depth': 1, 'visited': {0, 2}, 'paths': {2: (0, 2)}} - -> 3: {'depth': 2, 'visited': {0, 2, 3}, 'paths': {3: (0, 2, 3)}} - ? 3: {'depth': 2, 'visited': {0, 2, 3}, 'paths': {3: (0, 2, 3)}} - -> 1: {'depth': 1, 'visited': {0, 1, 2, 3}, 'paths': {1: (0, 1)}} - ? 1: {'depth': 1, 'visited': {0, 1, 2, 3}, 'paths': {1: (0, 1)}} - All paths: [(0,), (0, 1), (0, 2), (0, 2, 3)] - All visited: [0, 1, 2, 3] + >>> results_with_visited(traversal, {fdfs.start}) + After start: {'depth': 0, 'visited': {}, 'paths': {}} + ? 0: {'depth': 0, 'event': 'DFSEvent.ENTERING_START', 'visited': {0}, 'paths': + {0: ()}} + -> 2: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 2}, + 'paths': {2: ((0, 2, 2),)}} + ? 2: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 2}, + 'paths': {2: ((0, 2, 2),)}} + -> 3: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 2, 3}, + 'paths': {3: ((0, 2, 2), (2, 3, 4))}} + ? 3: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 2, 3}, + 'paths': {3: ((0, 2, 2), (2, 3, 4))}} + -> 1: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, + 3}, 'paths': {1: ((0, 1, 1),)}} + ? 1: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, + 3}, 'paths': {1: ((0, 1, 1),)}} + Final state for the visited vertices: + {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, 3}, + 'paths': {0: (), 1: ((0, 1, 1),), 2: ((0, 2, 2),), 3: ((0, 2, 2), (2, 3, 4))}} + + >>> traversal = nog.TraversalDepthFirst(next_labeled_edges=fdfs.next_edges) + >>> traversal = traversal.start_from(fdfs.start, build_paths=True, + ... compute_depth=True, compute_trace=True, + ... report=nog.DFSEvent.ALL) + >>> results_with_visited(traversal, {fdfs.start}) + After start: {'depth': 0, 'visited': {}, 'paths': {}} + -> 0: {'depth': 0, 'event': 'DFSEvent.ENTERING_START', 'trace': [0], 'on_trace': + {0}, 'index': {0: 1}, 'visited': {0}, 'paths': {0: ()}} + ? 0: {'depth': 0, 'event': 'DFSEvent.ENTERING_START', 'trace': [0], 'on_trace': + {0}, 'index': {0: 1}, 'visited': {0}, 'paths': {0: ()}} + -> 2: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 2], + 'trace_labels': [2], 'on_trace': {0, 2}, 'index': {2: 2}, 'visited': {0, 2}, + 'paths': {2: ((0, 2, 2),)}} + ? 2: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 2], + 'trace_labels': [2], 'on_trace': {0, 2}, 'index': {2: 2}, 'visited': {0, 2}, + 'paths': {2: ((0, 2, 2),)}} + -> 3: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 2, 3], + 'trace_labels': [2, 4], 'on_trace': {0, 2, 3}, 'index': {3: 3}, 'visited': {0, + 2, 3}, 'paths': {3: ((0, 2, 2), (2, 3, 4))}} + ? 3: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 2, 3], + 'trace_labels': [2, 4], 'on_trace': {0, 2, 3}, 'index': {3: 3}, 'visited': {0, + 2, 3}, 'paths': {3: ((0, 2, 2), (2, 3, 4))}} + -> 0: {'depth': 2, 'event': 'DFSEvent.BACK_EDGE', 'trace': [0, 2, 3, 0], + 'trace_labels': [2, 4, 5], 'on_trace': {0, 2, 3}, 'index': {0: 1}, 'visited': + {0, 2, 3}, 'paths': {0: ()}} + -> 3: {'depth': 2, 'event': 'DFSEvent.LEAVING_SUCCESSOR', 'trace': [0, 2, 3], + 'trace_labels': [2, 4], 'on_trace': {0, 2, 3}, 'index': {3: 3}, 'visited': {0, + 2, 3}, 'paths': {3: ((0, 2, 2), (2, 3, 4))}} + -> 2: {'depth': 1, 'event': 'DFSEvent.LEAVING_SUCCESSOR', 'trace': [0, 2], + 'trace_labels': [2], 'on_trace': {0, 2}, 'index': {2: 2}, 'visited': {0, 2, + 3}, 'paths': {2: ((0, 2, 2),)}} + -> 1: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1], + 'trace_labels': [1], 'on_trace': {0, 1}, 'index': {1: 4}, 'visited': {0, 1, 2, + 3}, 'paths': {1: ((0, 1, 1),)}} + ? 1: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1], + 'trace_labels': [1], 'on_trace': {0, 1}, 'index': {1: 4}, 'visited': {0, 1, 2, + 3}, 'paths': {1: ((0, 1, 1),)}} + -> 3: {'depth': 1, 'event': 'DFSEvent.CROSS_EDGE', 'trace': [0, 1, 3], + 'trace_labels': [1, 3], 'on_trace': {0, 1}, 'index': {3: 3}, 'visited': {0, 1, + 2, 3}, 'paths': {3: ((0, 2, 2), (2, 3, 4))}} + -> 1: {'depth': 1, 'event': 'DFSEvent.LEAVING_SUCCESSOR', 'trace': [0, 1], + 'trace_labels': [1], 'on_trace': {0, 1}, 'index': {1: 4}, 'visited': {0, 1, 2, + 3}, 'paths': {1: ((0, 1, 1),)}} + -> 3: {'depth': 0, 'event': 'DFSEvent.FORWARD_EDGE', 'trace': [0, 3], + 'trace_labels': [3], 'on_trace': {0}, 'index': {3: 3}, 'visited': {0, 1, 2, + 3}, 'paths': {3: ((0, 2, 2), (2, 3, 4))}} + -> 0: {'depth': 0, 'event': 'DFSEvent.LEAVING_START', 'trace': [0], 'on_trace': + {0}, 'index': {0: 1}, 'visited': {0, 1, 2, 3}, 'paths': {0: ()}} + Final state for the visited vertices: + {'depth': -1, 'event': 'DFSEvent.LEAVING_START', 'index': {0: 1, 1: 4, 2: 2, 3: + 3}, 'visited': {0, 1, 2, 3}, 'paths': {0: (), 1: ((0, 1, 1),), 2: ((0, 2, + 2),), 3: ((0, 2, 2), (2, 3, 4))}} + >>> traversal = nog.TraversalNeighborsThenDepth( - ... next_edges=f.next_edges) + ... next_labeled_edges=f.next_edges) >>> traversal = traversal.start_from(f.start, build_paths=True, ... compute_depth=True) >>> results_with_visited(traversal, {f.start}) - After start: {'depth': 0, 'visited': {0}, 'paths': {0: (0,)}} - ? 0: {'depth': 0, 'visited': {0}, 'paths': {0: (0,)}} - -> 1: {'depth': 1, 'visited': {0, 1}, 'paths': {1: (0, 1)}} - -> 2: {'depth': 1, 'visited': {0, 1, 2}, 'paths': {2: (0, 2)}} - ? 2: {'depth': 1, 'visited': {0, 1, 2}, 'paths': {2: (0, 2)}} - -> 3: {'depth': 2, 'visited': {0, 1, 2, 3}, 'paths': {3: (0, 2, 3)}} - ? 3: {'depth': 2, 'visited': {0, 1, 2, 3}, 'paths': {3: (0, 2, 3)}} - ? 1: {'depth': 1, 'visited': {0, 1, 2, 3}, 'paths': {1: (0, 1)}} - All paths: [(0,), (0, 1), (0, 2), (0, 2, 3)] - All visited: [0, 1, 2, 3] + After start: {'depth': 0, 'visited': {0}, 'paths': {0: ()}} + ? 0: {'depth': 0, 'visited': {0}, 'paths': {0: ()}} + -> 1: {'depth': 1, 'visited': {0, 1}, 'paths': {1: ((0, 1, 1),)}} + -> 2: {'depth': 1, 'visited': {0, 1, 2}, 'paths': {2: ((0, 2, 2),)}} + ? 2: {'depth': 1, 'visited': {0, 1, 2}, 'paths': {2: ((0, 2, 2),)}} + -> 3: {'depth': 2, 'visited': {0, 1, 2, 3}, 'paths': {3: ((0, 2, 2), (2, 3, + 4))}} + ? 3: {'depth': 2, 'visited': {0, 1, 2, 3}, 'paths': {3: ((0, 2, 2), (2, 3, 4))}} + ? 1: {'depth': 1, 'visited': {0, 1, 2, 3}, 'paths': {1: ((0, 1, 1),)}} + Final state for the visited vertices: + {'depth': 2, 'visited': {0, 1, 2, 3}, 'paths': {0: (), 1: ((0, 1, 1),), 2: ((0, + 2, 2),), 3: ((0, 2, 2), (2, 3, 4))}} >>> traversal = nog.TraversalTopologicalSort( - ... next_edges=f.next_edges) + ... next_labeled_edges=f.next_edges) >>> traversal = traversal.start_from(f.start, build_paths=True) >>> results_with_visited(traversal, {f.start}) After start: {'depth': 0, 'cycle_from_start': [], 'visited': {0}, 'paths': {0: - (0,)}} - ? 0: {'depth': 0, 'cycle_from_start': [], 'visited': {0}, 'paths': {0: (0,)}} - ? 2: {'depth': 1, 'cycle_from_start': [], 'visited': {0, 2}, 'paths': {2: (0, - 2)}} - ? 3: {'depth': 2, 'cycle_from_start': [], 'visited': {0, 2, 3}, 'paths': {3: (0, - 2, 3)}} + ()}} + ? 0: {'depth': 0, 'cycle_from_start': [], 'visited': {0}, 'paths': {0: ()}} + ? 2: {'depth': 1, 'cycle_from_start': [], 'visited': {0, 2}, 'paths': {2: ((0, + 2, 2),)}} + ? 3: {'depth': 2, 'cycle_from_start': [], 'visited': {0, 2, 3}, 'paths': {3: + ((0, 2, 2), (2, 3, 4))}} -> 3: {'depth': 2, 'cycle_from_start': [], 'visited': {0, 2, 3}, 'paths': {3: - (0, 2, 3)}} + ((0, 2, 2), (2, 3, 4))}} -> 2: {'depth': 1, 'cycle_from_start': [], 'visited': {0, 2, 3}, 'paths': {2: - (0, 2)}} + ((0, 2, 2),)}} ? 1: {'depth': 1, 'cycle_from_start': [], 'visited': {0, 1, 2, 3}, 'paths': {1: - (0, 1)}} + ((0, 1, 1),)}} -> 1: {'depth': 1, 'cycle_from_start': [], 'visited': {0, 1, 2, 3}, 'paths': {1: - (0, 1)}} + ((0, 1, 1),)}} -> 0: {'depth': 0, 'cycle_from_start': [], 'visited': {0, 1, 2, 3}, 'paths': {0: - (0,)}} - All paths: [(0,), (0, 1), (0, 2), (0, 2, 3)] - All visited: [0, 1, 2, 3] + ()}} + Final state for the visited vertices: + {'depth': 0, 'cycle_from_start': [], 'visited': {0, 1, 2, 3}, 'paths': {0: (), + 1: ((0, 1, 1),), 2: ((0, 2, 2),), 3: ((0, 2, 2), (2, 3, 4))}} >>> search = nog.BSearchBreadthFirst(next_edges=f.next_edges_bi) >>> l, p = search.start_from(f.start_bi, build_path=True) @@ -1804,7 +2453,7 @@ def is_tree() -> None: >>> f = FSmallBinaryTree() >>> traversal = nog.TraversalBreadthFirst(f.next_vertices,is_tree=True) >>> traversal = traversal.start_from(f.start, build_paths=True) - >>> results_with_visited(traversal, {f.start}) + >>> results_with_visited(traversal, {f.start}, use_reported=True) After start: {'depth': 0, 'visited': {}, 'paths': {1: (1,)}} ? 1: {'depth': 0, 'visited': {}, 'paths': {1: (1,)}} -> 2: {'depth': 1, 'visited': {}, 'paths': {2: (1, 2)}} @@ -1819,34 +2468,85 @@ def is_tree() -> None: ? 5: {'depth': 2, 'visited': {}, 'paths': {5: (1, 2, 5)}} ? 6: {'depth': 2, 'visited': {}, 'paths': {6: (1, 3, 6)}} ? 7: {'depth': 2, 'visited': {}, 'paths': {7: (1, 3, 7)}} - All paths: [(1,), (1, 2), (1, 3), (1, 2, 4), (1, 2, 5), (1, 3, 6), (1, 3, 7)] - All visited: [] + Final state for the reported and the start vertices: + {'depth': 2, 'visited': {}, 'paths': {1: (1,), 2: (1, 2), 3: (1, 3), 4: (1, 2, + 4), 5: (1, 2, 5), 6: (1, 3, 6), 7: (1, 3, 7)}} >>> traversal = nog.TraversalDepthFirst(f.next_vertices, is_tree=True) >>> traversal = traversal.start_from(f.start, build_paths=True, ... compute_depth=True) - >>> results_with_visited(traversal, {f.start}) - After start: {'depth': 0, 'visited': {}, 'paths': {1: (1,)}} - ? 1: {'depth': 0, 'visited': {}, 'paths': {1: (1,)}} - -> 3: {'depth': 1, 'visited': {}, 'paths': {3: (1, 3)}} - ? 3: {'depth': 1, 'visited': {}, 'paths': {3: (1, 3)}} - -> 7: {'depth': 2, 'visited': {}, 'paths': {7: (1, 3, 7)}} - ? 7: {'depth': 2, 'visited': {}, 'paths': {7: (1, 3, 7)}} - -> 6: {'depth': 2, 'visited': {}, 'paths': {6: (1, 3, 6)}} - ? 6: {'depth': 2, 'visited': {}, 'paths': {6: (1, 3, 6)}} - -> 2: {'depth': 1, 'visited': {}, 'paths': {2: (1, 2)}} - ? 2: {'depth': 1, 'visited': {}, 'paths': {2: (1, 2)}} - -> 5: {'depth': 2, 'visited': {}, 'paths': {5: (1, 2, 5)}} - ? 5: {'depth': 2, 'visited': {}, 'paths': {5: (1, 2, 5)}} - -> 4: {'depth': 2, 'visited': {}, 'paths': {4: (1, 2, 4)}} - ? 4: {'depth': 2, 'visited': {}, 'paths': {4: (1, 2, 4)}} - All paths: [(1,), (1, 2), (1, 3), (1, 2, 4), (1, 2, 5), (1, 3, 6), (1, 3, 7)] - All visited: [] + >>> results_with_visited(traversal, {f.start}, use_reported=True) + After start: {'depth': 0, 'visited': {}, 'paths': {}} + ? 1: {'depth': 0, 'event': 'DFSEvent.ENTERING_START', 'visited': {}, 'paths': + {1: (1,)}} + -> 3: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {}, + 'paths': {3: (1, 3)}} + ? 3: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {}, + 'paths': {3: (1, 3)}} + -> 7: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {}, + 'paths': {7: (1, 3, 7)}} + ? 7: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {}, + 'paths': {7: (1, 3, 7)}} + -> 6: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {}, + 'paths': {6: (1, 3, 6)}} + ? 6: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {}, + 'paths': {6: (1, 3, 6)}} + -> 2: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {}, + 'paths': {2: (1, 2)}} + ? 2: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {}, + 'paths': {2: (1, 2)}} + -> 5: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {}, + 'paths': {5: (1, 2, 5)}} + ? 5: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {}, + 'paths': {5: (1, 2, 5)}} + -> 4: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {}, + 'paths': {4: (1, 2, 4)}} + ? 4: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {}, + 'paths': {4: (1, 2, 4)}} + Final state for the reported and the start vertices: + {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {}, 'paths': + {1: (1,), 2: (1, 2), 3: (1, 3), 4: (1, 2, 4), 5: (1, 2, 5), 6: (1, 3, 6), 7: + (1, 3, 7)}} + + >>> traversal = traversal.start_from(f.start, build_paths=True, + ... compute_trace=True, compute_depth=True) + >>> results_with_visited(traversal, {f.start}, use_reported=True) + After start: {'depth': 0, 'visited': {}, 'paths': {}} + ? 1: {'depth': 0, 'event': 'DFSEvent.ENTERING_START', 'trace': [1], 'visited': + {}, 'paths': {1: (1,)}} + -> 3: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [1, 3], + 'visited': {}, 'paths': {3: (1, 3)}} + ? 3: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [1, 3], + 'visited': {}, 'paths': {3: (1, 3)}} + -> 7: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [1, 3, 7], + 'visited': {}, 'paths': {7: (1, 3, 7)}} + ? 7: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [1, 3, 7], + 'visited': {}, 'paths': {7: (1, 3, 7)}} + -> 6: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [1, 3, 6], + 'visited': {}, 'paths': {6: (1, 3, 6)}} + ? 6: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [1, 3, 6], + 'visited': {}, 'paths': {6: (1, 3, 6)}} + -> 2: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [1, 2], + 'visited': {}, 'paths': {2: (1, 2)}} + ? 2: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [1, 2], + 'visited': {}, 'paths': {2: (1, 2)}} + -> 5: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [1, 2, 5], + 'visited': {}, 'paths': {5: (1, 2, 5)}} + ? 5: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [1, 2, 5], + 'visited': {}, 'paths': {5: (1, 2, 5)}} + -> 4: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [1, 2, 4], + 'visited': {}, 'paths': {4: (1, 2, 4)}} + ? 4: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [1, 2, 4], + 'visited': {}, 'paths': {4: (1, 2, 4)}} + Final state for the reported and the start vertices: + {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {}, 'paths': + {1: (1,), 2: (1, 2), 3: (1, 3), 4: (1, 2, 4), 5: (1, 2, 5), 6: (1, 3, 6), 7: + (1, 3, 7)}} >>> traversal = nog.TraversalNeighborsThenDepth(f.next_vertices, is_tree=True) >>> traversal = traversal.start_from(f.start, build_paths=True, ... compute_depth=True) - >>> results_with_visited(traversal, {f.start}) + >>> results_with_visited(traversal, {f.start}, use_reported=True) After start: {'depth': 0, 'visited': {}, 'paths': {1: (1,)}} ? 1: {'depth': 0, 'visited': {}, 'paths': {1: (1,)}} -> 2: {'depth': 1, 'visited': {}, 'paths': {2: (1, 2)}} @@ -1861,12 +2561,13 @@ def is_tree() -> None: -> 5: {'depth': 2, 'visited': {}, 'paths': {5: (1, 2, 5)}} ? 5: {'depth': 2, 'visited': {}, 'paths': {5: (1, 2, 5)}} ? 4: {'depth': 2, 'visited': {}, 'paths': {4: (1, 2, 4)}} - All paths: [(1,), (1, 2), (1, 3), (1, 2, 4), (1, 2, 5), (1, 3, 6), (1, 3, 7)] - All visited: [] + Final state for the reported and the start vertices: + {'depth': 3, 'visited': {}, 'paths': {1: (1,), 2: (1, 2), 3: (1, 3), 4: (1, 2, + 4), 5: (1, 2, 5), 6: (1, 3, 6), 7: (1, 3, 7)}} >>> traversal = nog.TraversalTopologicalSort(f.next_vertices, is_tree=True) >>> traversal = traversal.start_from(f.start, build_paths=True) - >>> results_with_visited(traversal, {f.start}) + >>> results_with_visited(traversal, {f.start}, use_reported=True) After start: {'depth': 0, 'cycle_from_start': [], 'visited': {}, 'paths': {1: (1,)}} ? 1: {'depth': 0, 'cycle_from_start': [], 'visited': {}, 'paths': {1: (1,)}} @@ -1891,8 +2592,9 @@ def is_tree() -> None: 4)}} -> 2: {'depth': 1, 'cycle_from_start': [], 'visited': {}, 'paths': {2: (1, 2)}} -> 1: {'depth': 0, 'cycle_from_start': [], 'visited': {}, 'paths': {1: (1,)}} - All paths: [(1,), (1, 2), (1, 3), (1, 2, 4), (1, 2, 5), (1, 3, 6), (1, 3, 7)] - All visited: [] + Final state for the reported and the start vertices: + {'depth': 0, 'cycle_from_start': [], 'visited': {}, 'paths': {1: (1,), 2: (1, + 2), 3: (1, 3), 4: (1, 2, 4), 5: (1, 2, 5), 6: (1, 3, 6), 7: (1, 3, 7)}} """ pass @@ -1918,9 +2620,9 @@ class NormalGraphTraversalsWithWeights: ? 1: {'distance': 2, 'depth': 1, 'distances': {1: 2}, 'paths': {1: (0, 1)}} -> 3: {'distance': 3, 'depth': 2, 'distances': {3: 3}, 'paths': {3: (0, 2, 3)}} ? 3: {'distance': 3, 'depth': 2, 'distances': {3: 3}, 'paths': {3: (0, 2, 3)}} - All paths: [(0,), (0, 1), (0, 2), (0, 2, 3)] - All distances: {0: 0, 1: 2, 2: 1, 3: 3} - + Final state for the reported and the start vertices: + {'distance': 3, 'depth': 2, 'distances': {0: 0, 1: 2, 2: 1, 3: 3}, 'paths': {0: + (0,), 1: (0, 1), 2: (0, 2), 3: (0, 2, 3)}} Now we test with known_distances, but without option keep_distances. Start vertex starts with distance 2, and we pretend to have a path to 1 with @@ -1940,8 +2642,9 @@ class NormalGraphTraversalsWithWeights: ? 2: {'distance': 3, 'depth': 1, 'distances': {2: 0}, 'paths': {2: (0, 2)}} -> 3: {'distance': 5, 'depth': 2, 'distances': {3: 0}, 'paths': {3: (0, 2, 3)}} ? 3: {'distance': 5, 'depth': 2, 'distances': {3: 0}, 'paths': {3: (0, 2, 3)}} - All paths: [(0,), (0, 2), (0, 2, 3)] - All distances: {0: 0, 1: 0, 2: 0, 3: 0} + Final state for the reported and the start vertices: + {'distance': 5, 'depth': 2, 'distances': {0: 0, 2: 0, 3: 0}, 'paths': {0: (0,), + 2: (0, 2), 3: (0, 2, 3)}} >>> traversal.distances is known_distances True @@ -1959,8 +2662,9 @@ class NormalGraphTraversalsWithWeights: ? 2: {'distance': 3, 'depth': 1, 'distances': {2: 0}, 'paths': {2: (0, 2)}} -> 3: {'distance': 5, 'depth': 2, 'distances': {3: 0}, 'paths': {3: (0, 2, 3)}} ? 3: {'distance': 5, 'depth': 2, 'distances': {3: 0}, 'paths': {3: (0, 2, 3)}} - All paths: [(0,), (0, 2), (0, 2, 3)] - All distances: {0: 0, 1: 0, 2: 0, 3: 0} + Final state for the reported and the start vertices: + {'distance': 5, 'depth': 2, 'distances': {0: 0, 2: 0, 3: 0}, 'paths': {0: (0,), + 2: (0, 2), 3: (0, 2, 3)}} >>> traversal.distances is known_distances True @@ -1984,8 +2688,9 @@ class NormalGraphTraversalsWithWeights: ? 1: {'distance': 2, 'distances': {1: 2}, 'paths': {1: (0, 1)}} -> 3: {'distance': 3, 'distances': {3: 3}, 'paths': {3: (0, 2, 3)}} ? 3: {'distance': 3, 'distances': {3: 3}, 'paths': {3: (0, 2, 3)}} - All paths: [(0,), (0, 1), (0, 2), (0, 2, 3)] - All distances: {0: 0, 2: 1, 1: 2, 3: 3} + Final state for the reported and the start vertices: + {'distance': 3, 'distances': {0: 0, 1: 2, 2: 1, 3: 3}, 'paths': {0: (0,), 1: (0, + 1), 2: (0, 2), 3: (0, 2, 3)}} Now we test without option store_distances. >>> f = FDiamondSorted() @@ -2000,12 +2705,13 @@ class NormalGraphTraversalsWithWeights: ? 1: {'distance': 2, 'distances': {1: inf}, 'paths': {1: (0, 1)}} -> 3: {'distance': 3, 'distances': {3: inf}, 'paths': {3: (0, 2, 3)}} ? 3: {'distance': 3, 'distances': {3: inf}, 'paths': {3: (0, 2, 3)}} - All paths: [(0,), (0, 1), (0, 2), (0, 2, 3)] - All distances: {0: inf, 2: inf, 1: inf, 3: inf} + Final state for the reported and the start vertices: + {'distance': 3, 'distances': {0: inf, 1: inf, 2: inf, 3: inf}, 'paths': {0: + (0,), 1: (0, 1), 2: (0, 2), 3: (0, 2, 3)}} -- TraversalMinimumSpanningTree -- - >>> fmst = FDiamond2() + >>> fmst = FDiamondMST() >>> traversal = nog.TraversalMinimumSpanningTree(next_edges=fmst.next_edges) >>> traversal = traversal.start_from(fmst.start, build_paths=True) >>> results_standard(traversal, {fmst.start}) @@ -2017,7 +2723,8 @@ class NormalGraphTraversalsWithWeights: ? 1: {'edge': (0, 1, 2), 'paths': {1: (0, 1)}} -> 3: {'edge': (2, 3, 3), 'paths': {3: (0, 2, 3)}} ? 3: {'edge': (2, 3, 3), 'paths': {3: (0, 2, 3)}} - All paths: [(0,), (0, 1), (0, 2), (0, 2, 3)] + Final state for the reported and the start vertices: + {'edge': (2, 3, 3), 'paths': {0: (0,), 1: (0, 1), 2: (0, 2), 3: (0, 2, 3)}} -- TraversaAStar -- @@ -2040,7 +2747,9 @@ class NormalGraphTraversalsWithWeights: 3)}} -> 4: {'path_length': 1, 'depth': 1, 'distances': {4: 1}, 'paths': {4: (0, 4)}} ? 4: {'path_length': 1, 'depth': 1, 'distances': {4: 1}, 'paths': {4: (0, 4)}} - All paths: [(0,), (0, 1), (0, 2), (0, 2, 3), (0, 4)] + Final state for the reported and the start vertices: + {'path_length': 1, 'depth': 1, 'distances': {0: 0, 1: 3, 2: 3, 3: 5, 4: 1}, + 'paths': {0: (0,), 1: (0, 1), 2: (0, 2), 3: (0, 2, 3), 4: (0, 4)}} Variant of the test with option known_distances. @@ -2065,7 +2774,9 @@ class NormalGraphTraversalsWithWeights: 3)}} -> 4: {'path_length': 3, 'depth': 1, 'distances': {4: 3}, 'paths': {4: (0, 4)}} ? 4: {'path_length': 3, 'depth': 1, 'distances': {4: 3}, 'paths': {4: (0, 4)}} - All paths: [(0,), (0, 2), (0, 2, 3), (0, 4)] + Final state for the reported and the start vertices: + {'path_length': 3, 'depth': 1, 'distances': {0: 2, 2: 5, 3: 7, 4: 3}, 'paths': + {0: (0,), 2: (0, 2), 3: (0, 2, 3), 4: (0, 4)}} >>> print("Distance at goal:", known_distances[fa.goal]) Distance at goal: 7 >>> print("Best distances found so far:", dict(known_distances)) @@ -2123,8 +2834,10 @@ class NormalGraphTraversalsWithWeights: 7)}} ? 7: {'distance': 10, 'depth': 2, 'distances': {7: inf}, 'paths': {7: (1, 3, 7)}} - All paths: [(1,), (1, 2), (1, 3), (1, 2, 4), (1, 2, 5), (1, 3, 6), (1, 3, 7)] - All distances: {1: 0, 2: inf, 3: inf, 4: inf, 5: inf, 6: inf, 7: inf} + Final state for the reported and the start vertices: + {'distance': 10, 'depth': 2, 'distances': {1: 0, 2: inf, 3: inf, 4: inf, 5: inf, + 6: inf, 7: inf}, 'paths': {1: (1,), 2: (1, 2), 3: (1, 3), 4: (1, 2, 4), 5: (1, + 2, 5), 6: (1, 3, 6), 7: (1, 3, 7)}} Test TraversaAStar. Typically, one would use go_to(6) for our goal vertex 6, but for the test purposes we continue to iterate the rest of the graph. @@ -2154,7 +2867,10 @@ class NormalGraphTraversalsWithWeights: 7)}} ? 7: {'path_length': 10, 'depth': 2, 'distances': {7: 10}, 'paths': {7: (1, 3, 7)}} - All paths: [(1,), (1, 2), (1, 3), (1, 2, 4), (1, 2, 5), (1, 3, 6), (1, 3, 7)] + Final state for the reported and the start vertices: + {'path_length': 10, 'depth': 2, 'distances': {1: 0, 2: 2, 3: 3, 4: 6, 5: 7, 6: + 9, 7: 10}, 'paths': {1: (1,), 2: (1, 2), 3: (1, 3), 4: (1, 2, 4), 5: (1, 2, + 5), 6: (1, 3, 6), 7: (1, 3, 7)}} """ @@ -2162,8 +2878,8 @@ class MultipleOrNoneStartVerticesTraversalsWithOrWithoutLabels: """-- Traversal with none or multiple start vertices, first 3 traversal strategies Checks: Correct traversal of TraversalBreadthFirst, TraversalDepthFirst, TraversalNeighborsThenDepth, and TraversalTopologicalSort in case of multiple - start vertices. No traversal in case of no start vertex resp. no goal vertex - (this is checked only for unlabeled edges). + start vertices, with iterator as start_vertices. No traversal in case of no + start vertex resp. no goal vertex (this is checked only for unlabeled edges). Uses implementation descisions: - All strategies travers start vertices in given order - TraversalBreadthFirst traverses edges in given order, while TraversalDepthFirst @@ -2171,10 +2887,11 @@ class MultipleOrNoneStartVerticesTraversalsWithOrWithoutLabels: 1. Unlabeled edges >>> f = FMultiStart() + >>> fdfs = FMultiStartDFS() >>> fb = FMultiStartB() >>> t = nog.TraversalBreadthFirst(f.next_vertices).start_from( - ... start_vertices=f.start_vertices, build_paths=True) + ... start_vertices=iter(f.start_vertices), build_paths=True) >>> results_with_visited(t, f.start_vertices) After start: {'depth': 0, 'visited': {0, 5}, 'paths': {0: (0,), 5: (5,)}} ? 0: {'depth': 0, 'visited': {0, 5}, 'paths': {0: (0,)}} @@ -2189,60 +2906,241 @@ class MultipleOrNoneStartVerticesTraversalsWithOrWithoutLabels: -> 4: {'depth': 3, 'visited': {0, 1, 2, 3, 4, 5, 6}, 'paths': {4: (0, 1, 2, 4)}} ? 3: {'depth': 2, 'visited': {0, 1, 2, 3, 4, 5, 6}, 'paths': {3: (5, 6, 3)}} ? 4: {'depth': 3, 'visited': {0, 1, 2, 3, 4, 5, 6}, 'paths': {4: (0, 1, 2, 4)}} - All paths: [(0,), (0, 1), (0, 1, 2), (5, 6, 3), (0, 1, 2, 4), (5,), (5, 6)] - All visited: [0, 1, 2, 3, 4, 5, 6] + Final state for the visited vertices: + {'depth': 3, 'visited': {0, 1, 2, 3, 4, 5, 6}, 'paths': {0: (0,), 1: (0, 1), 2: + (0, 1, 2), 3: (5, 6, 3), 4: (0, 1, 2, 4), 5: (5,), 6: (5, 6)}} >>> list(t.start_from(start_vertices=(), build_paths=True)) [] >>> list(t.start_from(start_vertices=f.start_vertices, build_paths=True ... ).go_for_vertices_in([])) [] - For TraversalDepthFirst, we need to specify compute_depth to get depth. - And we need to check the extra execution path for the case without depth and paths. - >>> t = nog.TraversalDepthFirst(f.next_vertices).start_from( - ... start_vertices=f.start_vertices, build_paths=True, compute_depth=True) - >>> results_with_visited(t, f.start_vertices) - After start: {'depth': 0, 'visited': {0, 5}, 'paths': {0: (0,), 5: (5,)}} - ? 5: {'depth': 0, 'visited': {0, 5}, 'paths': {5: (5,)}} - -> 6: {'depth': 1, 'visited': {0, 5, 6}, 'paths': {6: (5, 6)}} - ? 6: {'depth': 1, 'visited': {0, 5, 6}, 'paths': {6: (5, 6)}} - -> 3: {'depth': 2, 'visited': {0, 3, 5, 6}, 'paths': {3: (5, 6, 3)}} - ? 3: {'depth': 2, 'visited': {0, 3, 5, 6}, 'paths': {3: (5, 6, 3)}} - -> 4: {'depth': 3, 'visited': {0, 3, 4, 5, 6}, 'paths': {4: (5, 6, 3, 4)}} - ? 4: {'depth': 3, 'visited': {0, 3, 4, 5, 6}, 'paths': {4: (5, 6, 3, 4)}} - ? 0: {'depth': 0, 'visited': {0, 3, 4, 5, 6}, 'paths': {0: (0,)}} - -> 1: {'depth': 1, 'visited': {0, 1, 3, 4, 5, 6}, 'paths': {1: (0, 1)}} - ? 1: {'depth': 1, 'visited': {0, 1, 3, 4, 5, 6}, 'paths': {1: (0, 1)}} - -> 2: {'depth': 2, 'visited': {0, 1, 2, 3, 4, 5, 6}, 'paths': {2: (0, 1, 2)}} - ? 2: {'depth': 2, 'visited': {0, 1, 2, 3, 4, 5, 6}, 'paths': {2: (0, 1, 2)}} - All paths: [(0,), (0, 1), (0, 1, 2), (5, 6, 3), (5, 6, 3, 4), (5,), (5, 6)] - All visited: [0, 1, 2, 3, 4, 5, 6] - >>> _ = t.start_from(start_vertices=f.start_vertices) - >>> results_with_visited(t, f.start_vertices) - After start: {'depth': 0, 'visited': {0, 5}, 'paths': {}} - ? 5: {'depth': -1, 'visited': {0, 5}, 'paths': {}} - -> 6: {'depth': -1, 'visited': {0, 5, 6}, 'paths': {}} - ? 6: {'depth': -1, 'visited': {0, 5, 6}, 'paths': {}} - -> 3: {'depth': -1, 'visited': {0, 3, 5, 6}, 'paths': {}} - ? 3: {'depth': -1, 'visited': {0, 3, 5, 6}, 'paths': {}} - -> 4: {'depth': -1, 'visited': {0, 3, 4, 5, 6}, 'paths': {}} - ? 4: {'depth': -1, 'visited': {0, 3, 4, 5, 6}, 'paths': {}} - ? 0: {'depth': -1, 'visited': {0, 3, 4, 5, 6}, 'paths': {}} - -> 1: {'depth': -1, 'visited': {0, 1, 3, 4, 5, 6}, 'paths': {}} - ? 1: {'depth': -1, 'visited': {0, 1, 3, 4, 5, 6}, 'paths': {}} - -> 2: {'depth': -1, 'visited': {0, 1, 2, 3, 4, 5, 6}, 'paths': {}} - ? 2: {'depth': -1, 'visited': {0, 1, 2, 3, 4, 5, 6}, 'paths': {}} - All visited: [0, 1, 2, 3, 4, 5, 6] - >>> list(t.start_from(start_vertices=(), build_paths=True)) + For TraversalDepthFirst, we need to check for several execution paths: + >>> t = nog.TraversalDepthFirst(fdfs.next_vertices) + + a) Without trace + No start vertices or for_vertices_in with empty vertices + >>> list(t.start_from(start_vertices=())) [] - >>> list(t.start_from(start_vertices=f.start_vertices, build_paths=True - ... ).go_for_vertices_in([])) + >>> list(t.start_from(start_vertices=fdfs.start_vertices).go_for_vertices_in([])) [] + a1) Without depth and paths. + >>> _ = t.start_from(start_vertices=iter(fdfs.start_vertices)) + >>> results_with_visited(t, []) + After start: {'depth': -1, 'visited': {}, 'paths': {}} + ? 0: {'depth': -1, 'event': 'DFSEvent.ENTERING_START', 'visited': {0}, 'paths': + {}} + -> 1: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1}, + 'paths': {}} + ? 1: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1}, + 'paths': {}} + -> 2: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, + 2}, 'paths': {}} + ? 2: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2}, + 'paths': {}} + -> 4: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, + 4}, 'paths': {}} + ? 4: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, + 4}, 'paths': {}} + -> 3: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, + 3, 4}, 'paths': {}} + ? 3: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, + 3, 4}, 'paths': {}} + ? 5: {'depth': -1, 'event': 'DFSEvent.ENTERING_START', 'visited': {0, 1, 2, 3, + 4, 5}, 'paths': {}} + -> 6: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, + 3, 4, 5, 6}, 'paths': {}} + ? 6: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, + 3, 4, 5, 6}, 'paths': {}} + Final state for the visited vertices: + {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, 3, 4, + 5, 6}, 'paths': {}} + + a2) With depth and paths and reporting of all entered vertices (start and non-start) + >>> t = t.start_from( + ... start_vertices=iter(fdfs.start_vertices), build_paths=True, + ... compute_depth=True, + ... report=nog.DFSEvent.ENTERING_START | nog.DFSEvent.ENTERING_SUCCESSOR) + >>> results_with_visited(t, fdfs.start_vertices) + After start: {'depth': 0, 'visited': {}, 'paths': {}} + -> 0: {'depth': 0, 'event': 'DFSEvent.ENTERING_START', 'visited': {0}, 'paths': + {0: (0,)}} + ? 0: {'depth': 0, 'event': 'DFSEvent.ENTERING_START', 'visited': {0}, 'paths': + {0: (0,)}} + -> 1: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1}, + 'paths': {1: (0, 1)}} + ? 1: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1}, + 'paths': {1: (0, 1)}} + -> 2: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2}, + 'paths': {2: (0, 1, 2)}} + ? 2: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2}, + 'paths': {2: (0, 1, 2)}} + -> 4: {'depth': 3, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, + 4}, 'paths': {4: (0, 1, 2, 4)}} + ? 4: {'depth': 3, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, + 4}, 'paths': {4: (0, 1, 2, 4)}} + -> 3: {'depth': 3, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, + 3, 4}, 'paths': {3: (0, 1, 2, 3)}} + ? 3: {'depth': 3, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, + 3, 4}, 'paths': {3: (0, 1, 2, 3)}} + -> 5: {'depth': 0, 'event': 'DFSEvent.ENTERING_START', 'visited': {0, 1, 2, 3, + 4, 5}, 'paths': {5: (5,)}} + ? 5: {'depth': 0, 'event': 'DFSEvent.ENTERING_START', 'visited': {0, 1, 2, 3, 4, + 5}, 'paths': {5: (5,)}} + -> 6: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, + 3, 4, 5, 6}, 'paths': {6: (5, 6)}} + ? 6: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, + 3, 4, 5, 6}, 'paths': {6: (5, 6)}} + Final state for the visited vertices: + {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, 3, 4, + 5, 6}, 'paths': {0: (0,), 1: (0, 1), 2: (0, 1, 2), 3: (0, 1, 2, 3), 4: (0, 1, + 2, 4), 5: (5,), 6: (5, 6)}} + + + a3) With depth and paths and reporting of entered start vertices, but only entering + of start vertices. + >>> t = t.start_from(start_vertices=iter(fdfs.start_vertices), build_paths=True, + ... compute_depth=True, report=nog.DFSEvent.ENTERING_START) + >>> results_with_visited(t, fdfs.start_vertices) + After start: {'depth': 0, 'visited': {}, 'paths': {}} + -> 0: {'depth': 0, 'event': 'DFSEvent.ENTERING_START', 'visited': {0}, 'paths': + {0: (0,)}} + ? 0: {'depth': 0, 'event': 'DFSEvent.ENTERING_START', 'visited': {0}, 'paths': + {0: (0,)}} + ? 1: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1}, + 'paths': {1: (0, 1)}} + ? 2: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2}, + 'paths': {2: (0, 1, 2)}} + ? 4: {'depth': 3, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, + 4}, 'paths': {4: (0, 1, 2, 4)}} + ? 3: {'depth': 3, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, + 3, 4}, 'paths': {3: (0, 1, 2, 3)}} + -> 5: {'depth': 0, 'event': 'DFSEvent.ENTERING_START', 'visited': {0, 1, 2, 3, + 4, 5}, 'paths': {5: (5,)}} + ? 5: {'depth': 0, 'event': 'DFSEvent.ENTERING_START', 'visited': {0, 1, 2, 3, 4, + 5}, 'paths': {5: (5,)}} + ? 6: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, + 3, 4, 5, 6}, 'paths': {6: (5, 6)}} + Final state for the visited vertices: + {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, 3, 4, + 5, 6}, 'paths': {0: (0,), 1: (0, 1), 2: (0, 1, 2), 3: (0, 1, 2, 3), 4: (0, 1, + 2, 4), 5: (5,), 6: (5, 6)}} + + b) With trace + No start vertices or for_vertices_in with empty vertices + >>> list(t.start_from(start_vertices=())) + [] + >>> list(t.start_from(start_vertices=fdfs.start_vertices).go_for_vertices_in([])) + [] + + b1) Without times, paths, and reporting. + >>> _ = t.start_from(start_vertices=iter(fdfs.start_vertices), compute_trace=True, + ... report=nog.DFSEvent.NONE) + >>> results_with_visited(t, fdfs.start_vertices) + After start: {'depth': -1, 'visited': {}, 'paths': {}} + ? 0: {'depth': -1, 'event': 'DFSEvent.ENTERING_START', 'trace': [0], 'visited': + {0}, 'paths': {}} + ? 1: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1], + 'visited': {0, 1}, 'paths': {}} + ? 2: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1, 2], + 'visited': {0, 1, 2}, 'paths': {}} + ? 4: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1, 2, + 4], 'visited': {0, 1, 2, 4}, 'paths': {}} + ? 3: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1, 2, + 3], 'visited': {0, 1, 2, 3, 4}, 'paths': {}} + ? 5: {'depth': -1, 'event': 'DFSEvent.ENTERING_START', 'trace': [5], 'visited': + {0, 1, 2, 3, 4, 5}, 'paths': {}} + ? 6: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [5, 6], + 'visited': {0, 1, 2, 3, 4, 5, 6}, 'paths': {}} + Final state for the visited vertices: + {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, 3, 4, + 5, 6}, 'paths': {}} + + b2) With times, paths. depth, and reporting everything + >>> t = t.start_from(start_vertices=iter(fdfs.start_vertices), + ... report=nog.DFSEvent.ALL, build_paths=True, compute_depth=True) + >>> results_with_visited(t, []) + After start: {'depth': 0, 'visited': {}, 'paths': {}} + -> 0: {'depth': 0, 'event': 'DFSEvent.ENTERING_START', 'trace': [0], 'on_trace': + {0}, 'index': {0: 1}, 'visited': {0}, 'paths': {0: (0,)}} + ? 0: {'depth': 0, 'event': 'DFSEvent.ENTERING_START', 'trace': [0], 'on_trace': + {0}, 'index': {0: 1}, 'visited': {0}, 'paths': {0: (0,)}} + -> 1: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1], + 'on_trace': {0, 1}, 'index': {1: 2}, 'visited': {0, 1}, 'paths': {1: (0, 1)}} + ? 1: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1], + 'on_trace': {0, 1}, 'index': {1: 2}, 'visited': {0, 1}, 'paths': {1: (0, 1)}} + -> 2: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1, 2], + 'on_trace': {0, 1, 2}, 'index': {2: 3}, 'visited': {0, 1, 2}, 'paths': {2: (0, + 1, 2)}} + ? 2: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1, 2], + 'on_trace': {0, 1, 2}, 'index': {2: 3}, 'visited': {0, 1, 2}, 'paths': {2: (0, + 1, 2)}} + -> 4: {'depth': 3, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1, 2, + 4], 'on_trace': {0, 1, 2, 4}, 'index': {4: 4}, 'visited': {0, 1, 2, 4}, + 'paths': {4: (0, 1, 2, 4)}} + ? 4: {'depth': 3, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1, 2, 4], + 'on_trace': {0, 1, 2, 4}, 'index': {4: 4}, 'visited': {0, 1, 2, 4}, 'paths': + {4: (0, 1, 2, 4)}} + -> 4: {'depth': 3, 'event': 'DFSEvent.LEAVING_SUCCESSOR', 'trace': [0, 1, 2, 4], + 'on_trace': {0, 1, 2, 4}, 'index': {4: 4}, 'visited': {0, 1, 2, 4}, 'paths': + {4: (0, 1, 2, 4)}} + -> 3: {'depth': 3, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1, 2, + 3], 'on_trace': {0, 1, 2, 3}, 'index': {3: 5}, 'visited': {0, 1, 2, 3, 4}, + 'paths': {3: (0, 1, 2, 3)}} + ? 3: {'depth': 3, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1, 2, 3], + 'on_trace': {0, 1, 2, 3}, 'index': {3: 5}, 'visited': {0, 1, 2, 3, 4}, + 'paths': {3: (0, 1, 2, 3)}} + -> 1: {'depth': 3, 'event': 'DFSEvent.BACK_EDGE', 'trace': [0, 1, 2, 3, 1], + 'on_trace': {0, 1, 2, 3}, 'index': {1: 2}, 'visited': {0, 1, 2, 3, 4}, + 'paths': {1: (0, 1)}} + -> 4: {'depth': 3, 'event': 'DFSEvent.CROSS_EDGE', 'trace': [0, 1, 2, 3, 4], + 'on_trace': {0, 1, 2, 3}, 'index': {4: 4}, 'visited': {0, 1, 2, 3, 4}, + 'paths': {4: (0, 1, 2, 4)}} + -> 3: {'depth': 3, 'event': 'DFSEvent.LEAVING_SUCCESSOR', 'trace': [0, 1, 2, 3], + 'on_trace': {0, 1, 2, 3}, 'index': {3: 5}, 'visited': {0, 1, 2, 3, 4}, + 'paths': {3: (0, 1, 2, 3)}} + -> 2: {'depth': 2, 'event': 'DFSEvent.LEAVING_SUCCESSOR', 'trace': [0, 1, 2], + 'on_trace': {0, 1, 2}, 'index': {2: 3}, 'visited': {0, 1, 2, 3, 4}, 'paths': + {2: (0, 1, 2)}} + -> 3: {'depth': 1, 'event': 'DFSEvent.FORWARD_EDGE', 'trace': [0, 1, 3], + 'on_trace': {0, 1}, 'index': {3: 5}, 'visited': {0, 1, 2, 3, 4}, 'paths': {3: + (0, 1, 2, 3)}} + -> 1: {'depth': 1, 'event': 'DFSEvent.LEAVING_SUCCESSOR', 'trace': [0, 1], + 'on_trace': {0, 1}, 'index': {1: 2}, 'visited': {0, 1, 2, 3, 4}, 'paths': {1: + (0, 1)}} + -> 0: {'depth': 0, 'event': 'DFSEvent.LEAVING_START', 'trace': [0], 'on_trace': + {0}, 'index': {0: 1}, 'visited': {0, 1, 2, 3, 4}, 'paths': {0: (0,)}} + -> 5: {'depth': 0, 'event': 'DFSEvent.ENTERING_START', 'trace': [5], 'on_trace': + {5}, 'index': {5: 6}, 'visited': {0, 1, 2, 3, 4, 5}, 'paths': {5: (5,)}} + ? 5: {'depth': 0, 'event': 'DFSEvent.ENTERING_START', 'trace': [5], 'on_trace': + {5}, 'index': {5: 6}, 'visited': {0, 1, 2, 3, 4, 5}, 'paths': {5: (5,)}} + -> 6: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [5, 6], + 'on_trace': {5, 6}, 'index': {6: 7}, 'visited': {0, 1, 2, 3, 4, 5, 6}, + 'paths': {6: (5, 6)}} + ? 6: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [5, 6], + 'on_trace': {5, 6}, 'index': {6: 7}, 'visited': {0, 1, 2, 3, 4, 5, 6}, + 'paths': {6: (5, 6)}} + -> 3: {'depth': 1, 'event': 'DFSEvent.CROSS_EDGE', 'trace': [5, 6, 3], + 'on_trace': {5, 6}, 'index': {3: 5}, 'visited': {0, 1, 2, 3, 4, 5, 6}, + 'paths': {3: (0, 1, 2, 3)}} + -> 6: {'depth': 1, 'event': 'DFSEvent.LEAVING_SUCCESSOR', 'trace': [5, 6], + 'on_trace': {5, 6}, 'index': {6: 7}, 'visited': {0, 1, 2, 3, 4, 5, 6}, + 'paths': {6: (5, 6)}} + -> 5: {'depth': 0, 'event': 'DFSEvent.LEAVING_START', 'trace': [5], 'on_trace': + {5}, 'index': {5: 6}, 'visited': {0, 1, 2, 3, 4, 5, 6}, 'paths': {5: (5,)}} + -> 6: {'depth': -1, 'event': 'DFSEvent.SKIPPING_START', 'trace': [6], 'index': + {6: 7}, 'visited': {0, 1, 2, 3, 4, 5, 6}, 'paths': {6: (5, 6)}} + Final state for the visited vertices: + {'depth': -1, 'event': 'DFSEvent.SKIPPING_START', 'index': {0: 1, 1: 2, 2: 3, 3: + 5, 4: 4, 5: 6, 6: 7}, 'visited': {0, 1, 2, 3, 4, 5, 6}, 'paths': {0: (0,), 1: + (0, 1), 2: (0, 1, 2), 3: (0, 1, 2, 3), 4: (0, 1, 2, 4), 5: (5,), 6: (5, 6)}} + For TraversalNeighborsThenDepth, we need to specify compute_depth to get depth. And we need to check the extra execution path for the case without depth and paths. >>> t = nog.TraversalNeighborsThenDepth(f.next_vertices).start_from( - ... start_vertices=f.start_vertices, build_paths=True, compute_depth=True) + ... start_vertices=iter(f.start_vertices), build_paths=True, + ... compute_depth=True) >>> results_with_visited(t, f.start_vertices) After start: {'depth': 0, 'visited': {0, 5}, 'paths': {0: (0,), 5: (5,)}} ? 5: {'depth': 0, 'visited': {0, 5}, 'paths': {5: (5,)}} @@ -2257,11 +3155,12 @@ class MultipleOrNoneStartVerticesTraversalsWithOrWithoutLabels: ? 1: {'depth': 1, 'visited': {0, 1, 3, 4, 5, 6}, 'paths': {1: (0, 1)}} -> 2: {'depth': 2, 'visited': {0, 1, 2, 3, 4, 5, 6}, 'paths': {2: (0, 1, 2)}} ? 2: {'depth': 2, 'visited': {0, 1, 2, 3, 4, 5, 6}, 'paths': {2: (0, 1, 2)}} - All paths: [(0,), (0, 1), (0, 1, 2), (5, 6, 3), (5, 6, 3, 4), (5,), (5, 6)] - All visited: [0, 1, 2, 3, 4, 5, 6] + Final state for the visited vertices: + {'depth': 3, 'visited': {0, 1, 2, 3, 4, 5, 6}, 'paths': {0: (0,), 1: (0, 1), 2: + (0, 1, 2), 3: (5, 6, 3), 4: (5, 6, 3, 4), 5: (5,), 6: (5, 6)}} >>> _ = t.start_from(start_vertices=f.start_vertices) >>> results_with_visited(t, f.start_vertices) - After start: {'depth': 0, 'visited': {0, 5}, 'paths': {}} + After start: {'depth': -1, 'visited': {0, 5}, 'paths': {}} ? 5: {'depth': -1, 'visited': {0, 5}, 'paths': {}} -> 6: {'depth': -1, 'visited': {0, 5, 6}, 'paths': {}} ? 6: {'depth': -1, 'visited': {0, 5, 6}, 'paths': {}} @@ -2274,7 +3173,8 @@ class MultipleOrNoneStartVerticesTraversalsWithOrWithoutLabels: ? 1: {'depth': -1, 'visited': {0, 1, 3, 4, 5, 6}, 'paths': {}} -> 2: {'depth': -1, 'visited': {0, 1, 2, 3, 4, 5, 6}, 'paths': {}} ? 2: {'depth': -1, 'visited': {0, 1, 2, 3, 4, 5, 6}, 'paths': {}} - All visited: [0, 1, 2, 3, 4, 5, 6] + Final state for the visited vertices: + {'depth': -1, 'visited': {0, 1, 2, 3, 4, 5, 6}, 'paths': {}} >>> list(t.start_from(start_vertices=(), build_paths=True)) [] >>> list(t.start_from(start_vertices=f.start_vertices, build_paths=True @@ -2282,7 +3182,7 @@ class MultipleOrNoneStartVerticesTraversalsWithOrWithoutLabels: [] >>> t = nog.TraversalTopologicalSort(f.next_vertices).start_from( - ... start_vertices=f.start_vertices, build_paths=True) + ... start_vertices=iter(f.start_vertices), build_paths=True) >>> results_with_visited(t, f.start_vertices) After start: {'depth': 0, 'cycle_from_start': [], 'visited': {0, 5}, 'paths': {0: (0,), 5: (5,)}} @@ -2313,8 +3213,10 @@ class MultipleOrNoneStartVerticesTraversalsWithOrWithoutLabels: 'paths': {1: (0, 1)}} -> 0: {'depth': 0, 'cycle_from_start': [], 'visited': {0, 1, 2, 3, 4, 5, 6}, 'paths': {0: (0,)}} - All paths: [(0,), (0, 1), (0, 1, 2), (5, 6, 3), (5, 6, 3, 4), (5,), (5, 6)] - All visited: [0, 1, 2, 3, 4, 5, 6] + Final state for the visited vertices: + {'depth': 0, 'cycle_from_start': [], 'visited': {0, 1, 2, 3, 4, 5, 6}, 'paths': + {0: (0,), 1: (0, 1), 2: (0, 1, 2), 3: (5, 6, 3), 4: (5, 6, 3, 4), 5: (5,), 6: + (5, 6)}} >>> list(t.start_from(start_vertices=(), build_paths=True)) [] >>> list(t.start_from(start_vertices=f.start_vertices, build_paths=True @@ -2330,7 +3232,8 @@ class MultipleOrNoneStartVerticesTraversalsWithOrWithoutLabels: >>> print(l, f.goal not in p) 3 True >>> l, p = nog.BSearchBreadthFirst(fb.next_vertices_bi - ... ).start_from(start_and_goal_vertices=fb.start_vertices_bi, build_path=True) + ... ).start_from(start_and_goal_vertices=[ + ... iter(s) for s in fb.start_vertices_bi], build_path=True) ? 0: {'depth': 0, 'visited': {0, 5}, 'paths': {0: (0,)}} ? 5: {'depth': 0, 'visited': {0, 1, 5}, 'paths': {5: (5,)}} ?<4: {'depth': 0, 'visited': {4}, 'paths': {4: (4,)}} @@ -2383,29 +3286,88 @@ class MultipleOrNoneStartVerticesTraversalsWithOrWithoutLabels: -> 4: {'depth': 3, 'visited': {0, 1, 2, 3, 4, 5, 6}, 'paths': {4: (0, 1, 2, 4)}} ? 3: {'depth': 2, 'visited': {0, 1, 2, 3, 4, 5, 6}, 'paths': {3: (5, 6, 3)}} ? 4: {'depth': 3, 'visited': {0, 1, 2, 3, 4, 5, 6}, 'paths': {4: (0, 1, 2, 4)}} - All paths: [(0,), (0, 1), (0, 1, 2), (5, 6, 3), (0, 1, 2, 4), (5,), (5, 6)] - All visited: [0, 1, 2, 3, 4, 5, 6] - - >>> traversal = nog.TraversalDepthFirst( - ... next_edges=f.next_edges - ... ).start_from( - ... start_vertices=f.start_vertices, build_paths=True, compute_depth=True) - >>> results_with_visited(traversal, f.start_vertices) - After start: {'depth': 0, 'visited': {0, 5}, 'paths': {0: (0,), 5: (5,)}} - ? 5: {'depth': 0, 'visited': {0, 5}, 'paths': {5: (5,)}} - -> 6: {'depth': 1, 'visited': {0, 5, 6}, 'paths': {6: (5, 6)}} - ? 6: {'depth': 1, 'visited': {0, 5, 6}, 'paths': {6: (5, 6)}} - -> 3: {'depth': 2, 'visited': {0, 3, 5, 6}, 'paths': {3: (5, 6, 3)}} - ? 3: {'depth': 2, 'visited': {0, 3, 5, 6}, 'paths': {3: (5, 6, 3)}} - -> 4: {'depth': 3, 'visited': {0, 3, 4, 5, 6}, 'paths': {4: (5, 6, 3, 4)}} - ? 4: {'depth': 3, 'visited': {0, 3, 4, 5, 6}, 'paths': {4: (5, 6, 3, 4)}} - ? 0: {'depth': 0, 'visited': {0, 3, 4, 5, 6}, 'paths': {0: (0,)}} - -> 1: {'depth': 1, 'visited': {0, 1, 3, 4, 5, 6}, 'paths': {1: (0, 1)}} - ? 1: {'depth': 1, 'visited': {0, 1, 3, 4, 5, 6}, 'paths': {1: (0, 1)}} - -> 2: {'depth': 2, 'visited': {0, 1, 2, 3, 4, 5, 6}, 'paths': {2: (0, 1, 2)}} - ? 2: {'depth': 2, 'visited': {0, 1, 2, 3, 4, 5, 6}, 'paths': {2: (0, 1, 2)}} - All paths: [(0,), (0, 1), (0, 1, 2), (5, 6, 3), (5, 6, 3, 4), (5,), (5, 6)] - All visited: [0, 1, 2, 3, 4, 5, 6] + Final state for the visited vertices: + {'depth': 3, 'visited': {0, 1, 2, 3, 4, 5, 6}, 'paths': {0: (0,), 1: (0, 1), 2: + (0, 1, 2), 3: (5, 6, 3), 4: (0, 1, 2, 4), 5: (5,), 6: (5, 6)}} + + >>> traversal = nog.TraversalDepthFirst(next_edges=fdfs.next_edges) + >>> _ = traversal.start_from( + ... start_vertices=iter(fdfs.start_vertices), build_paths=True, + ... compute_depth=True) + >>> results_with_visited(traversal, fdfs.start_vertices) + After start: {'depth': 0, 'visited': {}, 'paths': {}} + ? 0: {'depth': 0, 'event': 'DFSEvent.ENTERING_START', 'visited': {0}, 'paths': + {0: (0,)}} + -> 1: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1}, + 'paths': {1: (0, 1)}} + ? 1: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1}, + 'paths': {1: (0, 1)}} + -> 2: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2}, + 'paths': {2: (0, 1, 2)}} + ? 2: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2}, + 'paths': {2: (0, 1, 2)}} + -> 4: {'depth': 3, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, + 4}, 'paths': {4: (0, 1, 2, 4)}} + ? 4: {'depth': 3, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, + 4}, 'paths': {4: (0, 1, 2, 4)}} + -> 3: {'depth': 3, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, + 3, 4}, 'paths': {3: (0, 1, 2, 3)}} + ? 3: {'depth': 3, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, + 3, 4}, 'paths': {3: (0, 1, 2, 3)}} + ? 5: {'depth': 0, 'event': 'DFSEvent.ENTERING_START', 'visited': {0, 1, 2, 3, 4, + 5}, 'paths': {5: (5,)}} + -> 6: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, + 3, 4, 5, 6}, 'paths': {6: (5, 6)}} + ? 6: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, + 3, 4, 5, 6}, 'paths': {6: (5, 6)}} + Final state for the visited vertices: + {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, 3, 4, + 5, 6}, 'paths': {0: (0,), 1: (0, 1), 2: (0, 1, 2), 3: (0, 1, 2, 3), 4: (0, 1, + 2, 4), 5: (5,), 6: (5, 6)}} + + >>> traversal = nog.TraversalDepthFirst(next_labeled_edges=fdfs.next_edges) + >>> _ = traversal.start_from( + ... start_vertices=iter(f.start_vertices), build_paths=True, + ... compute_trace=True, compute_depth=True) + >>> results_with_visited(traversal, fdfs.start_vertices) + After start: {'depth': 0, 'visited': {}, 'paths': {}} + ? 0: {'depth': 0, 'event': 'DFSEvent.ENTERING_START', 'trace': [0], 'visited': + {0}, 'paths': {0: ()}} + -> 1: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1], + 'trace_labels': [1], 'visited': {0, 1}, 'paths': {1: ((0, 1, 1),)}} + ? 1: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1], + 'trace_labels': [1], 'visited': {0, 1}, 'paths': {1: ((0, 1, 1),)}} + -> 2: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1, 2], + 'trace_labels': [1, 3], 'visited': {0, 1, 2}, 'paths': {2: ((0, 1, 1), (1, 2, + 3))}} + ? 2: {'depth': 2, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1, 2], + 'trace_labels': [1, 3], 'visited': {0, 1, 2}, 'paths': {2: ((0, 1, 1), (1, 2, + 3))}} + -> 4: {'depth': 3, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1, 2, + 4], 'trace_labels': [1, 3, 7], 'visited': {0, 1, 2, 4}, 'paths': {4: ((0, 1, + 1), (1, 2, 3), (2, 4, 7))}} + ? 4: {'depth': 3, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1, 2, 4], + 'trace_labels': [1, 3, 7], 'visited': {0, 1, 2, 4}, 'paths': {4: ((0, 1, 1), + (1, 2, 3), (2, 4, 7))}} + -> 3: {'depth': 3, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1, 2, + 3], 'trace_labels': [1, 3, 6], 'visited': {0, 1, 2, 3, 4}, 'paths': {3: ((0, + 1, 1), (1, 2, 3), (2, 3, 6))}} + ? 3: {'depth': 3, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1, 2, 3], + 'trace_labels': [1, 3, 6], 'visited': {0, 1, 2, 3, 4}, 'paths': {3: ((0, 1, + 1), (1, 2, 3), (2, 3, 6))}} + ? 5: {'depth': 0, 'event': 'DFSEvent.ENTERING_START', 'trace': [5], 'visited': + {0, 1, 2, 3, 4, 5}, 'paths': {5: ()}} + -> 6: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [5, 6], + 'trace_labels': [4], 'visited': {0, 1, 2, 3, 4, 5, 6}, 'paths': {6: ((5, 6, + 4),)}} + ? 6: {'depth': 1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [5, 6], + 'trace_labels': [4], 'visited': {0, 1, 2, 3, 4, 5, 6}, 'paths': {6: ((5, 6, + 4),)}} + Final state for the visited vertices: + {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'visited': {0, 1, 2, 3, 4, + 5, 6}, 'paths': {0: (), 1: ((0, 1, 1),), 2: ((0, 1, 1), (1, 2, 3)), 3: ((0, 1, + 1), (1, 2, 3), (2, 3, 6)), 4: ((0, 1, 1), (1, 2, 3), (2, 4, 7)), 5: (), 6: + ((5, 6, 4),)}} >>> traversal = nog.TraversalNeighborsThenDepth( ... next_edges=f.next_edges @@ -2425,12 +3387,13 @@ class MultipleOrNoneStartVerticesTraversalsWithOrWithoutLabels: ? 1: {'depth': 1, 'visited': {0, 1, 3, 4, 5, 6}, 'paths': {1: (0, 1)}} -> 2: {'depth': 2, 'visited': {0, 1, 2, 3, 4, 5, 6}, 'paths': {2: (0, 1, 2)}} ? 2: {'depth': 2, 'visited': {0, 1, 2, 3, 4, 5, 6}, 'paths': {2: (0, 1, 2)}} - All paths: [(0,), (0, 1), (0, 1, 2), (5, 6, 3), (5, 6, 3, 4), (5,), (5, 6)] - All visited: [0, 1, 2, 3, 4, 5, 6] + Final state for the visited vertices: + {'depth': 3, 'visited': {0, 1, 2, 3, 4, 5, 6}, 'paths': {0: (0,), 1: (0, 1), 2: + (0, 1, 2), 3: (5, 6, 3), 4: (5, 6, 3, 4), 5: (5,), 6: (5, 6)}} >>> traversal = nog.TraversalTopologicalSort( ... next_edges=f.next_edges - ... ).start_from(start_vertices=f.start_vertices, build_paths=True) + ... ).start_from(start_vertices=iter(f.start_vertices), build_paths=True) >>> results_with_visited(traversal, f.start_vertices) After start: {'depth': 0, 'cycle_from_start': [], 'visited': {0, 5}, 'paths': {0: (0,), 5: (5,)}} @@ -2461,8 +3424,10 @@ class MultipleOrNoneStartVerticesTraversalsWithOrWithoutLabels: 'paths': {1: (0, 1)}} -> 0: {'depth': 0, 'cycle_from_start': [], 'visited': {0, 1, 2, 3, 4, 5, 6}, 'paths': {0: (0,)}} - All paths: [(0,), (0, 1), (0, 1, 2), (5, 6, 3), (5, 6, 3, 4), (5,), (5, 6)] - All visited: [0, 1, 2, 3, 4, 5, 6] + Final state for the visited vertices: + {'depth': 0, 'cycle_from_start': [], 'visited': {0, 1, 2, 3, 4, 5, 6}, 'paths': + {0: (0,), 1: (0, 1), 2: (0, 1, 2), 3: (5, 6, 3), 4: (5, 6, 3, 4), 5: (5,), 6: + (5, 6)}} >>> search = nog.BSearchBreadthFirst(next_edges=fb.next_edges_bi) >>> l, p = search.start_from(start_and_goal_vertices=((), fb.goal_vertices)) @@ -2525,7 +3490,7 @@ class MultipleStartVerticesTraversalsWithWeights: >>> traversal = nog.TraversalShortestPaths( ... next_edges=f.next_edges) >>> traversal = traversal.start_from( - ... start_vertices=f.start_vertices, build_paths=True) + ... start_vertices=iter(f.start_vertices), build_paths=True) >>> results_with_distances(traversal, f.start_vertices) After start: {'distance': inf, 'depth': 0, 'distances': {0: 0, 5: 0}, 'paths': {0: (0,), 5: (5,)}} @@ -2543,8 +3508,10 @@ class MultipleStartVerticesTraversalsWithWeights: 4)}} ? 4: {'distance': 3, 'depth': 3, 'distances': {4: 0}, 'paths': {4: (5, 6, 3, 4)}} - All paths: [(0,), (0, 1), (0, 1, 2), (5, 6, 3), (5, 6, 3, 4), (5,), (5, 6)] - All distances: {0: 0, 5: 0, 6: 0, 1: 0, 2: 0, 3: 0, 4: 0} + Final state for the reported and the start vertices: + {'distance': 3, 'depth': 3, 'distances': {0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: + 0}, 'paths': {0: (0,), 1: (0, 1), 2: (0, 1, 2), 3: (5, 6, 3), 4: (5, 6, 3, 4), + 5: (5,), 6: (5, 6)}} >>> traversal = traversal.start_from(start_vertices=(), build_paths=True) >>> list(traversal) [] @@ -2552,7 +3519,7 @@ class MultipleStartVerticesTraversalsWithWeights: >>> traversal = nog.TraversalShortestPathsInfBranchingSorted( ... next_edges=f.next_edges) >>> traversal = traversal.start_from( - ... start_vertices=f.start_vertices, build_paths=True) + ... start_vertices=iter(f.start_vertices), build_paths=True) >>> results_with_distances(traversal, f.start_vertices) After start: {'distance': inf, 'distances': {0: inf, 5: inf}, 'paths': {0: (0,), 5: (5,)}} @@ -2568,8 +3535,10 @@ class MultipleStartVerticesTraversalsWithWeights: ? 2: {'distance': 2, 'distances': {2: inf}, 'paths': {2: (0, 1, 2)}} -> 4: {'distance': 3, 'distances': {4: inf}, 'paths': {4: (5, 6, 3, 4)}} ? 4: {'distance': 3, 'distances': {4: inf}, 'paths': {4: (5, 6, 3, 4)}} - All paths: [(0,), (0, 1), (0, 1, 2), (5, 6, 3), (5, 6, 3, 4), (5,), (5, 6)] - All distances: {0: inf, 5: inf, 1: inf, 6: inf, 3: inf, 2: inf, 4: inf} + Final state for the reported and the start vertices: + {'distance': 3, 'distances': {0: inf, 1: inf, 2: inf, 3: inf, 4: inf, 5: inf, 6: + inf}, 'paths': {0: (0,), 1: (0, 1), 2: (0, 1, 2), 3: (5, 6, 3), 4: (5, 6, 3, + 4), 5: (5,), 6: (5, 6)}} >>> traversal = traversal.start_from(start_vertices=(), build_paths=True) >>> list(traversal) [] @@ -2577,7 +3546,7 @@ class MultipleStartVerticesTraversalsWithWeights: >>> traversal = nog.TraversalMinimumSpanningTree( ... next_edges=f.next_edges) >>> traversal = traversal.start_from( - ... start_vertices=f.start_vertices, build_paths=True) + ... start_vertices=iter(f.start_vertices), build_paths=True) >>> results_standard(traversal, f.start_vertices) After start: {'edge': None, 'paths': {0: (0,), 5: (5,)}} ? 0: {'edge': None, 'paths': {0: (0,)}} @@ -2592,7 +3561,9 @@ class MultipleStartVerticesTraversalsWithWeights: ? 3: {'edge': (6, 3, 1), 'paths': {3: (5, 6, 3)}} -> 4: {'edge': (2, 4, 1), 'paths': {4: (0, 1, 2, 4)}} ? 4: {'edge': (2, 4, 1), 'paths': {4: (0, 1, 2, 4)}} - All paths: [(0,), (0, 1), (0, 1, 2), (5, 6, 3), (0, 1, 2, 4), (5,), (5, 6)] + Final state for the reported and the start vertices: + {'edge': (2, 4, 1), 'paths': {0: (0,), 1: (0, 1), 2: (0, 1, 2), 3: (5, 6, 3), 4: + (0, 1, 2, 4), 5: (5,), 6: (5, 6)}} >>> traversal = traversal.start_from(start_vertices=(), build_paths=True) >>> list(traversal) [] @@ -2601,7 +3572,7 @@ class MultipleStartVerticesTraversalsWithWeights: >>> fa = FMultiStartAStar() >>> traversal = nog.TraversalAStar(next_edges=fa.next_edges) >>> traversal = traversal.start_from( - ... fa.heuristic, start_vertices=fa.start_vertices, build_paths=True) + ... fa.heuristic, start_vertices=iter(fa.start_vertices), build_paths=True) >>> results_standard(traversal, fa.start_vertices) After start: {'path_length': inf, 'depth': 0, 'distances': {0: 0, 1: 0}, 'paths': {0: (0,), 1: (1,)}} @@ -2617,7 +3588,10 @@ class MultipleStartVerticesTraversalsWithWeights: ? 2: {'path_length': 1, 'depth': 1, 'distances': {2: 1}, 'paths': {2: (0, 2)}} -> 4: {'path_length': 1, 'depth': 1, 'distances': {4: 1}, 'paths': {4: (1, 4)}} ? 4: {'path_length': 1, 'depth': 1, 'distances': {4: 1}, 'paths': {4: (1, 4)}} - All paths: [(0,), (1,), (0, 2), (1, 3), (1, 4), (1, 3, 5)] + Final state for the reported and the start vertices: + {'path_length': 1, 'depth': 1, 'distances': {0: 0, 1: 0, 2: 1, 3: 1, 4: 1, 5: + 2}, 'paths': {0: (0,), 1: (1,), 2: (0, 2), 3: (1, 3), 4: (1, 4), 5: (1, 3, + 5)}} >>> traversal = traversal.start_from( ... fa.heuristic, start_vertices=(), build_paths=True) >>> list(traversal) @@ -2650,7 +3624,8 @@ class MultipleStartVerticesTraversalsWithWeights: >>> print(l, fb.goal not in p) 4 True >>> l, p = search.start_from( - ... start_and_goal_vertices = fb.start_vertices_bi, build_path=True) + ... start_and_goal_vertices = [iter(s) for s in fb.start_vertices_bi], + ... build_path=True) ? 5: {} ?<4: {} ? 0: {} @@ -2679,6 +3654,157 @@ class MultipleStartVerticesTraversalsWithWeights: """ +class IllegalParameters: + """Check if the library detects illegal parameter combinations. + + >>> f = FNoEdgesGoalIsStart() + + >>> nog.TraversalDepthFirst(f.next_vertices).start_from(f.start, + ... report=nog.DFSEvent.SOME_NON_TREE_EDGE | nog.DFSEvent.FORWARD_EDGE + ... ) # doctest: +NORMALIZE_WHITESPACE + Traceback (most recent call last): + RuntimeError: Reporting of non-tree edges as a group and as individual edge + type cannot be combined. + >>> nog.TraversalDepthFirst(f.next_vertices).start_from(f.start, + ... report=nog.DFSEvent.SOME_NON_TREE_EDGE | nog.DFSEvent.BACK_EDGE + ... ) # doctest: +NORMALIZE_WHITESPACE + Traceback (most recent call last): + RuntimeError: Reporting of non-tree edges as a group and as individual edge + type cannot be combined. + >>> nog.TraversalDepthFirst(f.next_vertices).start_from(f.start, + ... report=nog.DFSEvent.SOME_NON_TREE_EDGE | nog.DFSEvent.CROSS_EDGE + ... ) # doctest: +NORMALIZE_WHITESPACE + Traceback (most recent call last): + RuntimeError: Reporting of non-tree edges as a group and as individual edge + type cannot be combined. + >>> nog.TraversalDepthFirst(f.next_vertices).start_from(f.start, + ... report=nog.DFSEvent.FORWARD_OR_CROSS_EDGE | nog.DFSEvent.FORWARD_EDGE + ... ) # doctest: +NORMALIZE_WHITESPACE + Traceback (most recent call last): + RuntimeError: Reporting of forward or cross edges as a group and as individual edge + type cannot be combined. + >>> nog.TraversalDepthFirst(f.next_vertices).start_from(f.start, + ... report=nog.DFSEvent.FORWARD_OR_CROSS_EDGE | nog.DFSEvent.CROSS_EDGE + ... ) # doctest: +NORMALIZE_WHITESPACE + Traceback (most recent call last): + RuntimeError: Reporting of forward or cross edges as a group and as individual edge + type cannot be combined. + + >>> nog.TraversalDepthFirst(f.next_vertices, is_tree=True).start_from(f.start, + ... report=nog.DFSEvent.BACK_EDGE + ... ) # doctest: +NORMALIZE_WHITESPACE + Traceback (most recent call last): + RuntimeError: The events BACK_EDGE, FORWARD_EDGE, and CROSS_EDGE, + and groups containing them, + cannot be computed for trees and for traversals in mode *ALL_WALKS*. + >>> nog.TraversalDepthFirst(f.next_vertices).start_from(f.start, + ... report=nog.DFSEvent.BACK_EDGE, mode=nog.DFSMode.ALL_WALKS + ... ) # doctest: +NORMALIZE_WHITESPACE + Traceback (most recent call last): + RuntimeError: The events BACK_EDGE, FORWARD_EDGE, and CROSS_EDGE, + and groups containing them, + cannot be computed for trees and for traversals in mode *ALL_WALKS*. + >>> nog.TraversalDepthFirst(f.next_vertices, is_tree=True).start_from(f.start, + ... report=nog.DFSEvent.FORWARD_EDGE + ... ) # doctest: +NORMALIZE_WHITESPACE + Traceback (most recent call last): + RuntimeError: The events BACK_EDGE, FORWARD_EDGE, and CROSS_EDGE, + and groups containing them, + cannot be computed for trees and for traversals in mode *ALL_WALKS*. + >>> nog.TraversalDepthFirst(f.next_vertices).start_from(f.start, + ... report=nog.DFSEvent.FORWARD_EDGE, mode=nog.DFSMode.ALL_WALKS + ... ) # doctest: +NORMALIZE_WHITESPACE + Traceback (most recent call last): + RuntimeError: The events BACK_EDGE, FORWARD_EDGE, and CROSS_EDGE, + and groups containing them, + cannot be computed for trees and for traversals in mode *ALL_WALKS*. + >>> nog.TraversalDepthFirst(f.next_vertices, is_tree=True).start_from(f.start, + ... report=nog.DFSEvent.CROSS_EDGE + ... ) # doctest: +NORMALIZE_WHITESPACE + Traceback (most recent call last): + RuntimeError: The events BACK_EDGE, FORWARD_EDGE, and CROSS_EDGE, + and groups containing them, + cannot be computed for trees and for traversals in mode *ALL_WALKS*. + >>> nog.TraversalDepthFirst(f.next_vertices).start_from(f.start, + ... report=nog.DFSEvent.CROSS_EDGE, mode=nog.DFSMode.ALL_WALKS + ... ) # doctest: +NORMALIZE_WHITESPACE + Traceback (most recent call last): + RuntimeError: The events BACK_EDGE, FORWARD_EDGE, and CROSS_EDGE, + and groups containing them, + cannot be computed for trees and for traversals in mode *ALL_WALKS*. + >>> nog.TraversalDepthFirst(f.next_vertices, is_tree=True).start_from(f.start, + ... report=nog.DFSEvent.SOME_NON_TREE_EDGE + ... ) # doctest: +NORMALIZE_WHITESPACE + Traceback (most recent call last): + RuntimeError: The events BACK_EDGE, FORWARD_EDGE, and CROSS_EDGE, + and groups containing them, + cannot be computed for trees and for traversals in mode *ALL_WALKS*. + >>> nog.TraversalDepthFirst(f.next_vertices).start_from(f.start, + ... report=nog.DFSEvent.SOME_NON_TREE_EDGE, mode=nog.DFSMode.ALL_WALKS + ... ) # doctest: +NORMALIZE_WHITESPACE + Traceback (most recent call last): + RuntimeError: The events BACK_EDGE, FORWARD_EDGE, and CROSS_EDGE, + and groups containing them, + cannot be computed for trees and for traversals in mode *ALL_WALKS*. + >>> nog.TraversalDepthFirst(f.next_vertices, is_tree=True).start_from(f.start, + ... report=nog.DFSEvent.FORWARD_OR_CROSS_EDGE + ... ) # doctest: +NORMALIZE_WHITESPACE + Traceback (most recent call last): + RuntimeError: The events BACK_EDGE, FORWARD_EDGE, and CROSS_EDGE, + and groups containing them, + cannot be computed for trees and for traversals in mode *ALL_WALKS*. + >>> nog.TraversalDepthFirst(f.next_vertices).start_from(f.start, + ... report=nog.DFSEvent.FORWARD_OR_CROSS_EDGE, mode=nog.DFSMode.ALL_WALKS + ... ) # doctest: +NORMALIZE_WHITESPACE + Traceback (most recent call last): + RuntimeError: The events BACK_EDGE, FORWARD_EDGE, and CROSS_EDGE, + and groups containing them, + cannot be computed for trees and for traversals in mode *ALL_WALKS*. + + >>> nog.TraversalDepthFirst(f.next_vertices, is_tree=True).start_from(f.start, + ... compute_on_trace=True + ... ) # doctest: +NORMALIZE_WHITESPACE + Traceback (most recent call last): + RuntimeError: Computation of the on-trace is not allowed for trees and for + traversals in mode *ALL_WALKS*. + >>> nog.TraversalDepthFirst(f.next_vertices).start_from(f.start, + ... compute_on_trace=True, mode=nog.DFSMode.ALL_WALKS + ... ) # doctest: +NORMALIZE_WHITESPACE + Traceback (most recent call last): + RuntimeError: Computation of the on-trace is not allowed for trees and for + traversals in mode *ALL_WALKS*. + + >>> nog.TraversalDepthFirst(f.next_vertices).start_from(f.start, + ... mode=nog.DFSMode.ALL_PATHS, report=nog.DFSEvent.FORWARD_EDGE + ... ) # doctest: +NORMALIZE_WHITESPACE + Traceback (most recent call last): + RuntimeError: The events FORWARD_EDGE and CROSS_EDGE, + and groups containing them, + cannot be computed for traversals in mode *ALL_PATHS*. + >>> nog.TraversalDepthFirst(f.next_vertices).start_from(f.start, + ... mode=nog.DFSMode.ALL_PATHS, report=nog.DFSEvent.CROSS_EDGE + ... ) # doctest: +NORMALIZE_WHITESPACE + Traceback (most recent call last): + RuntimeError: The events FORWARD_EDGE and CROSS_EDGE, + and groups containing them, + cannot be computed for traversals in mode *ALL_PATHS*. + + >>> nog.TraversalDepthFirst(f.next_vertices).start_from(f.start, + ... build_paths=True, mode=nog.DFSMode.ALL_WALKS + ... ) # doctest: +NORMALIZE_WHITESPACE + Traceback (most recent call last): + RuntimeError: Paths cannot be computed in mode *ALL_WALKS*, because + walks can be cyclic. + + >>> nog.TraversalDepthFirst(f.next_vertices).start_from(f.start, + ... compute_index=True, already_visited=set() + ... ) # doctest: +NORMALIZE_WHITESPACE + Traceback (most recent call last): + RuntimeError: Parameter *already_visited* not allowed when vertex indexes + are demanded. + """ + + class InitiationForgotten: """Check if the library detects the mistake that start_from or one of the go_... methods are called on a traversal class instead of an object, i.e., the @@ -3002,17 +4128,18 @@ class GearTestsTraversalsWithOrWithoutLabels: additionally. >>> f = FOvertaking() + >>> f2 = FOvertakingDFSWithBackEdges() >>> test_gears = [nog.GearForHashableVertexIDsAndIntsMaybeFloats(), ... nog.GearForIntVertexIDsAndCFloats(), ... nog.GearForIntVerticesAndIDsAndCFloats(), ... nog.GearForIntVerticesAndIDsAndCFloats(no_bit_packing=True), ... ] - >>> def gear_test_traversals(traversal_class, next_labeled_edges, *args, **nargs): + >>> def gear_test_traversals(traversal_class, next_labeled_edges, *args, **kwargs): ... for gear in test_gears: ... yield traversal_class( ... nog.vertex_as_id, gear, ... next_labeled_edges=next_labeled_edges, - ... *args, **nargs) + ... *args, **kwargs) >>> for t in gear_test_traversals(nog.TraversalBreadthFirstFlex, f.next_edges): ... print_partial_results(t.start_from(f.start, build_paths=True), @@ -3038,7 +4165,8 @@ class GearTestsTraversalsWithOrWithoutLabels: [3, 4, 7, 8, 11] [9, 10, 5, 6, 1, 2] ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) - For DFS, we also test without paths, because this changes the process + For DFS without trace and events, we also test without paths, because this + changes the process >>> for t in gear_test_traversals(nog.TraversalDepthFirstFlex, f.next_edges): ... print_partial_results(t.start_from(f.start, build_paths=False)) [3, 4, 7, 8, 11] [9, 10, 5, 6, 1, 2] @@ -3046,6 +4174,66 @@ class GearTestsTraversalsWithOrWithoutLabels: [3, 4, 7, 8, 11] [9, 10, 5, 6, 1, 2] [3, 4, 7, 8, 11] [9, 10, 5, 6, 1, 2] + For DFS, we also test with computation or vertex indexes and on_trace, + because both cases change the execution process. We do not change the + default setting for events in order to get the same vertex reports as for + the run above. + >>> for t in gear_test_traversals(nog.TraversalDepthFirstFlex, f.next_edges): + ... print_partial_results(t.start_from( + ... f.start, build_paths=True, compute_on_trace=True), + ... paths_to=f.goal) + [3, 4, 7, 8, 11] [9, 10, 5, 6, 1, 2] + ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) + [3, 4, 7, 8, 11] [9, 10, 5, 6, 1, 2] + ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) + [3, 4, 7, 8, 11] [9, 10, 5, 6, 1, 2] + ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) + [3, 4, 7, 8, 11] [9, 10, 5, 6, 1, 2] + ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) + >>> for t in gear_test_traversals(nog.TraversalDepthFirstFlex, f.next_edges): + ... print_partial_results(t.start_from( + ... f.start, build_paths=True, compute_index=True), + ... paths_to=f.goal) + [3, 4, 7, 8, 11] [9, 10, 5, 6, 1, 2] + ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) + [3, 4, 7, 8, 11] [9, 10, 5, 6, 1, 2] + ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) + [3, 4, 7, 8, 11] [9, 10, 5, 6, 1, 2] + ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) + [3, 4, 7, 8, 11] [9, 10, 5, 6, 1, 2] + ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) + + Now, we also report NON_TREE_EDGESs, since this enables and uses the + functionality of options index and on_trace + >>> for t in gear_test_traversals(nog.TraversalDepthFirstFlex, f.next_edges): + ... print_partial_results(t.start_from( + ... f.start, build_paths=True, + ... report=nog.DFSEvent.ENTERING_SUCCESSOR | nog.DFSEvent.NON_TREE_EDGES), + ... paths_to=f.goal) + [3, 4, 7, 8, 11] [6, 1, 2, 5, 3, 4] + ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) + [3, 4, 7, 8, 11] [6, 1, 2, 5, 3, 4] + ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) + [3, 4, 7, 8, 11] [6, 1, 2, 5, 3, 4] + ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) + [3, 4, 7, 8, 11] [6, 1, 2, 5, 3, 4] + ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) + + >>> for t in gear_test_traversals(nog.TraversalDepthFirstFlex, f2.next_edges): + ... print_partial_results(t.start_from( + ... f2.start, build_paths=True, + ... report=nog.DFSEvent.ENTERING_SUCCESSOR | nog.DFSEvent.BACK_EDGE), + ... paths_to=f2.goal) + [3, 4, 7, 8, 11] [2, 5, 3, 3, 1, 0] + ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) + [3, 4, 7, 8, 11] [2, 5, 3, 3, 1, 0] + ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) + [3, 4, 7, 8, 11] [2, 5, 3, 3, 1, 0] + ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) + [3, 4, 7, 8, 11] [2, 5, 3, 3, 1, 0] + ((0, 3, 3), (3, 4, 3)) ((2060, 2063, 3), (2063, 2064, 3)) + + >>> for t in gear_test_traversals(nog.TraversalNeighborsThenDepthFlex, ... f.next_edges): ... print_partial_results(t.start_from(f.start, build_paths=True), @@ -3161,6 +4349,71 @@ class GearTestsTraversalsWithOrWithoutLabels: the chosen gear and its configuration + Tests for BFS with mode ALL_PATHS. + We test the traversal behaviour with different gears. + >>> f_diamond = FDiamondDFS() + >>> for t in gear_test_traversals(nog.TraversalDepthFirstFlex, + ... f_diamond.next_edges): + ... print_partial_results(t.start_from( + ... f_diamond.start, mode=nog.DFSMode.ALL_PATHS)) + ? 0: {'depth': -1, 'event': 'DFSEvent.ENTERING_START', 'trace': [0], 'on_trace': + {0}, 'visited': {}, 'paths': {}} + ? 2: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 2], + 'trace_labels': [2], 'on_trace': {0, 2}, 'visited': {}, 'paths': {}} + ? 3: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 2, 3], + 'trace_labels': [2, 4], 'on_trace': {0, 2, 3}, 'visited': {}, 'paths': {}} + ? 1: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1], + 'trace_labels': [1], 'on_trace': {0, 1}, 'visited': {}, 'paths': {}} + ? 3: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1, 3], + 'trace_labels': [1, 3], 'on_trace': {0, 1, 3}, 'visited': {}, 'paths': {}} + ? 3: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 3], + 'trace_labels': [5], 'on_trace': {0, 3}, 'visited': {}, 'paths': {}} + [2, 3, 1, 3, 3] [2, 3, 1, 3, 3] + ? 0: {'depth': -1, 'event': 'DFSEvent.ENTERING_START', 'trace': [0], 'on_trace': + {0}, 'visited': {}, 'paths': {}} + ? 2: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 2], + 'trace_labels': [2], 'on_trace': {0, 2}, 'visited': {}, 'paths': {}} + ? 3: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 2, 3], + 'trace_labels': [2, 4], 'on_trace': {0, 2, 3}, 'visited': {}, 'paths': {}} + ? 1: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1], + 'trace_labels': [1], 'on_trace': {0, 1}, 'visited': {}, 'paths': {}} + ? 3: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 1, 3], + 'trace_labels': [1, 3], 'on_trace': {0, 1, 3}, 'visited': {}, 'paths': {}} + ? 3: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': [0, 3], + 'trace_labels': [5], 'on_trace': {0, 3}, 'visited': {}, 'paths': {}} + [2, 3, 1, 3, 3] [2, 3, 1, 3, 3] + ? 0: {'depth': -1, 'event': 'DFSEvent.ENTERING_START', 'trace': array('L', [0]), + 'on_trace': {0}, 'visited': {}, 'paths': {}} + ? 2: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': array('L', + [0, 2]), 'trace_labels': [2], 'on_trace': {0, 2}, 'visited': {}, 'paths': {}} + ? 3: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': array('L', + [0, 2, 3]), 'trace_labels': [2, 4], 'on_trace': {0, 2, 3}, 'visited': {}, + 'paths': {}} + ? 1: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': array('L', + [0, 1]), 'trace_labels': [1], 'on_trace': {0, 1}, 'visited': {}, 'paths': {}} + ? 3: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': array('L', + [0, 1, 3]), 'trace_labels': [1, 3], 'on_trace': {0, 1, 3}, 'visited': {}, + 'paths': {}} + ? 3: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': array('L', + [0, 3]), 'trace_labels': [5], 'on_trace': {0, 3}, 'visited': {}, 'paths': {}} + [2, 3, 1, 3, 3] [2, 3, 1, 3, 3] + ? 0: {'depth': -1, 'event': 'DFSEvent.ENTERING_START', 'trace': array('L', [0]), + 'on_trace': {0}, 'visited': {}, 'paths': {}} + ? 2: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': array('L', + [0, 2]), 'trace_labels': [2], 'on_trace': {0, 2}, 'visited': {}, 'paths': {}} + ? 3: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': array('L', + [0, 2, 3]), 'trace_labels': [2, 4], 'on_trace': {0, 2, 3}, 'visited': {}, + 'paths': {}} + ? 1: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': array('L', + [0, 1]), 'trace_labels': [1], 'on_trace': {0, 1}, 'visited': {}, 'paths': {}} + ? 3: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': array('L', + [0, 1, 3]), 'trace_labels': [1, 3], 'on_trace': {0, 1, 3}, 'visited': {}, + 'paths': {}} + ? 3: {'depth': -1, 'event': 'DFSEvent.ENTERING_SUCCESSOR', 'trace': array('L', + [0, 3]), 'trace_labels': [5], 'on_trace': {0, 3}, 'visited': {}, 'paths': {}} + [2, 3, 1, 3, 3] [2, 3, 1, 3, 3] + + Tests for BSearchBreadthFirst and BSearchShortestPaths: >>> def test_for_gear(strategy_class, g): ... s = strategy_class(nog.vertex_as_id, g, @@ -3335,6 +4588,17 @@ class GearTestsTraversalsWithOrWithoutLabels: [3, 7, 15, 31, 63] [4097, 8195, 8194, 4096, 8193, 8192] [3, 7, 15, 31, 63] [4097, 8195, 8194, 4096, 8193, 8192] + For DFS, we also test with traces, because this changes the process + >>> for t in gear_test_traversals(nog.TraversalDepthFirstFlex, + ... f.next_edges, + ... is_tree=True): + ... print_partial_results( + ... t.start_from(f.start, build_paths=True, compute_trace=True)) + [3, 7, 15, 31, 63] [4097, 8195, 8194, 4096, 8193, 8192] + [3, 7, 15, 31, 63] [4097, 8195, 8194, 4096, 8193, 8192] + [3, 7, 15, 31, 63] [4097, 8195, 8194, 4096, 8193, 8192] + [3, 7, 15, 31, 63] [4097, 8195, 8194, 4096, 8193, 8192] + >>> for t in gear_test_traversals(nog.TraversalNeighborsThenDepthFlex, ... f.next_edges, ... is_tree=True): diff --git a/tests/test_unit_typed.py b/tests/test_unit_typed.py index 55d73b6..e124408 100644 --- a/tests/test_unit_typed.py +++ b/tests/test_unit_typed.py @@ -1,16 +1,16 @@ import sys from decimal import Decimal, getcontext -from collections.abc import Iterator +from collections.abc import Iterator, Iterable from typing import Any, TypeVar, Union, TYPE_CHECKING +import nographs as nog + if sys.version_info >= (3, 11): from typing import assert_type import unittest from mpmath import mp, mpf # type: ignore -import nographs as nog - # noinspection PyProtectedMember from nographs._extra_tsp import GettableProto @@ -101,3 +101,45 @@ def test_tsp_typing_docs_example(self) -> None: """ g1: GettableProto[int, str] = dict[int, str]([(0, "a"), (1, "b")]) # NOQA F841 g2: GettableProto[int, str] = ["a", "b"] # NOQA F841 + + def test_dfs_throw(self) -> None: + """DFS function iter() returns a Collections.Generator. Its type is tested + my the MyPy run here. And The behaviour of the throw, both for stops + at a start vertex and at other vertices.""" + + def next_vertices(i: int, _: Any) -> Iterator[int]: + yield i + 1 + + stop = StopIteration() + traversal = nog.TraversalDepthFirst[int, None](next_vertices) + + # Check effect of throw + it = iter( + traversal.start_from(start_vertices=[0, 10], report=nog.DFSEvent.ENTERING) + ) + vertices = [] + for v in it: + if v >= 5: + # noinspection PyUnresolvedReferences + _ = it.throw(stop) + vertices.append(v) + self.assertEqual(vertices, [0, 1, 2, 3, 4, 5, 10]) + + # Check if the following reports 0 + def next_vertices2(i: int, _: Any) -> Iterable[int]: + return [] + + traversal = nog.TraversalDepthFirst[int, None](next_vertices2) + it = iter( + traversal.start_from( + start_vertices=[0, 0, 0], report=nog.DFSEvent.SKIPPING_START + ) + ) + self.assertEqual(next(it), 0) + # Check if throwing StopIteration at this moment falls through + try: + # noinspection PyUnresolvedReferences + _ = it.throw(stop) + except RuntimeError: + return + self.fail("StopIteration, thrown at illegal moment, raises RuntimeError.") diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..f4d84fd --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,52 @@ +""" +Adaptations to the DocTest framework that allows for skipping slow tests +on demand. + +Add the following flag to slow tests: # doctest:+SLOW_TEST + + +""" + +import doctest +from typing import Any, Union +import types + + +SLOW_TEST = doctest.register_optionflag("SLOW_TEST") + + +class DocTestFinderSkippingSlowTests(doctest.DocTestFinder): + """A DocTestFinder that skips examples having option SLOW_TEST set""" + + def find( + self, + obj: object, + name: Union[str, None] = None, + module: Union[bool, types.ModuleType, None] = None, + globs: Union[dict[str, Any], None] = None, + extraglobs: Union[dict[str, Any], None] = None, + ) -> list[doctest.DocTest]: + tests = super().find(obj, name, module, globs, extraglobs) + for test in tests: + test.examples = [ + example for example in test.examples if SLOW_TEST not in example.options + ] + return tests + + +class DocTestParserSkippingSlowTests(doctest.DocTestParser): + """A DocTestParser that skips examples having option SLOW_TEST set""" + + def get_doctest( + self, + string: str, + globs: dict[str, Any], + name: str, + filename: Union[str, None], + lineno: Union[int, None], + ) -> doctest.DocTest: + test = super().get_doctest(string, globs, name, filename, lineno) + test.examples = [ + example for example in test.examples if SLOW_TEST not in example.options + ] + return test diff --git a/tpl/!LICENSE b/tpl/!LICENSE new file mode 100644 index 0000000..91be59b --- /dev/null +++ b/tpl/!LICENSE @@ -0,0 +1,27 @@ +'''$$ +def current_year(): + import time + now = time.gmtime() + return time.strftime('%Y', now) +$$''' +MIT License + +Copyright (c) 2022 - '$$ insert(current_year()) $$' HeWeMel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tpl/base_lib.py b/tpl/base_lib.py new file mode 100644 index 0000000..7d5b97a --- /dev/null +++ b/tpl/base_lib.py @@ -0,0 +1,23 @@ +""" Basis functionality easing the use of the template library """ + +from tpl.make_insert_look_defined import * + + +# """$$ +# Above, the leading comment character is just to hide the macro start mark +# from the syntax analysis of the used IDE. This makes the IDE check +# the syntax within the macro code after this macro start mark. + + +def insert_with_indent(indent: str, code: str) -> None: + """Change each line of code as follows, and call function *insert* + on each result: Remove the indentation string given by the last line + of *code*. Prepend the given indentation string *indent*. + """ + lines = code.splitlines(keepends=True) + ignore_indent = lines[-1] + for line in lines[:-1]: + insert(indent + line.removeprefix(ignore_indent)) + + +# $$""" diff --git a/tpl/make_insert_look_defined.py b/tpl/make_insert_look_defined.py new file mode 100644 index 0000000..30a1141 --- /dev/null +++ b/tpl/make_insert_look_defined.py @@ -0,0 +1,20 @@ +# These definitions are just to silence the QA of the used IDE for +# calls of *insert* and similar functions in macro code. +# Usage in a text section (!) of a template, that will be +# imported (!) in another template: +# from tpl.make_insert_look_defined import * +# Since text sections are not evaluated and the text is ignored by +# *import_from()*, it has no effect. But the IDE thinks, the functions +# are really defined here. + + +def insert(*args, **kwargs): + pass + + +def import_from(*args, **kwargs): + pass + + +def insert_from(*args, **kwargs): + pass diff --git a/tpl/src/nographs/_strategies/MCalculationLimit.py b/tpl/src/nographs/_strategies/MCalculationLimit.py new file mode 100644 index 0000000..39f824b --- /dev/null +++ b/tpl/src/nographs/_strategies/MCalculationLimit.py @@ -0,0 +1,45 @@ +""" Functionality that ease the handling of a calculation limit in a strategy. """ + +from tpl.make_insert_look_defined import * + +# """$$ + + +class MCalculationLimit: + """Methods to ease implementing the handling of the calculation limit.""" + + @staticmethod + def prepare(): + insert( + f"""\ + # Prepare limit check done by zero check + if calculation_limit is not None: + calculation_limit += 1 + +""" + ) + + @staticmethod + def step(reduction_value: str = ""): + if reduction_value: + insert( + f"""\ + if calculation_limit is not None and calculation_limit >= 0: + if ( + calculation_limit := calculation_limit - {reduction_value} + ) < 0: + raise RuntimeError("Number of visited vertices reached limit") +""" + ) + else: + insert( + f"""\ + if calculation_limit and not ( + calculation_limit := calculation_limit - 1 + ): + raise RuntimeError("Number of visited vertices reached limit") +""" + ) + + +# $$""" diff --git a/tpl/src/nographs/_strategies/MStrategy.py b/tpl/src/nographs/_strategies/MStrategy.py new file mode 100644 index 0000000..75d8b1d --- /dev/null +++ b/tpl/src/nographs/_strategies/MStrategy.py @@ -0,0 +1,32 @@ +""" Macros that ease implementing strategies """ + +from tpl.make_insert_look_defined import * + +# """$$ + +import_from("tpl/base_lib.py") +import_from("$$/MCalculationLimit.py") +import_from("$$/MVertexSet.py") +import_from("$$/MVertexMapping.py") +import_from("$$/MVertexMappingExpectNone.py") + + +class MStrategy: + """Methods that ease implementing subclasses of Strategy.""" + + @staticmethod + def vertex_to_id(vertex_name: str, vertex_id_name: str) -> None: + insert( + f"""\ + {vertex_id_name}: T_vertex_id = ( + maybe_vertex_to_id( + {vertex_name} + ) # type: ignore[assignment] + if maybe_vertex_to_id + else {vertex_name} + ) +""" + ) + + +# $$""" diff --git a/tpl/src/nographs/_strategies/MVertexMapping.py b/tpl/src/nographs/_strategies/MVertexMapping.py new file mode 100644 index 0000000..46e9ce5 --- /dev/null +++ b/tpl/src/nographs/_strategies/MVertexMapping.py @@ -0,0 +1,98 @@ +""" Functionality that ease the use of gear collection VertexMapping +in traversals. """ + +from tpl.make_insert_look_defined import * + +# """$$ +import_from("tpl/base_lib.py") + + +class MVertexMapping: + """Methods to ease implementing performant access to VertexMapping objects.""" + + @staticmethod + def access(name: str): + insert( + f"""\ + _, {name}_sequence, {name}_wrapper = access_to_vertex_mapping({name}) +""" + ) + + @staticmethod + def get_included(collection: str, key: str): + """Reference to value for *key*, and we are sure that *key* exists.""" + insert(f"""{collection}_sequence[{key}]""") + + @staticmethod + def set_included(collection: str, key: str, value: str): + """Reference to value for *key*, and we are sure that *key* exists.""" + insert(f"""{collection}_sequence[{key}] = {value}\n""") + + @staticmethod + def get_with_default(collection: str, key: str, value: str, variable: str): + """ + Roughly equivalent to: + variable = collection[key] if key in collection else value. + There is no guarantee whether the default *value* is set in *collection* + in the else case (like a defaultdict does is) or not. + """ + insert( + f"""\ + try: + {variable} = {collection}_sequence[{key}] + except IndexError: + {variable} = {value} +""" + ) + + @staticmethod + def set(collection: str, key: str, value: str): + """collection[key] = value.""" + insert( + f"""\ + try: + {collection}_sequence[{key}] = {value} + except IndexError: + {collection}_wrapper.extend_and_set({key}, {value}) +""" + ) + + @staticmethod + def if_value_smaller_set_else_continue(collection: str, key: str, value: str): + """If value < collection[key] then collection[key] = value else continue.""" + insert( + f"""\ + try: + if {collection}_sequence[{key}] <= {value}: + continue + {collection}_sequence[{key}] = {value} + except IndexError: + {collection}_wrapper.extend_and_set({key}, {value}) +""" + ) + + @staticmethod + def if_value_smaller_or_condition_set_else_continue( + collection: str, key: str, value: str, condition: str + ): + """ + If condition or value < collection[key]: + collection[key] = value + else: + continue + """ + insert( + f"""\ + try: + if not {condition} and {collection}_sequence[{key}] <= {value}: + continue + {collection}_sequence[{key}] = {value} + except IndexError: + # n_id not in distances_collection. To be regarded as value + # infinity, i.e., n_path_weight is smaller. + {collection}_wrapper.extend_and_set({key}, {value}) +""" + ) + + +# $$""" diff --git a/tpl/src/nographs/_strategies/MVertexMappingExpectNone.py b/tpl/src/nographs/_strategies/MVertexMappingExpectNone.py new file mode 100644 index 0000000..45337c0 --- /dev/null +++ b/tpl/src/nographs/_strategies/MVertexMappingExpectNone.py @@ -0,0 +1,104 @@ +""" Functionality that ease the use of gear collection VertexMapping +in traversals. """ + +from tpl.make_insert_look_defined import * + +# """$$ +import_from("tpl/base_lib.py") + + +class MVertexMappingExpectNone: + """Methods to ease implementing performant access to VertexMappingExpectNone + objects. + """ + + @staticmethod + def access(name: str): + insert( + f"""\ + ( + _, + {name}_sequence, + {name}_wrapper, + ) = access_to_vertex_mapping_expect_none({name}) +""" + ) + + @staticmethod + def store_vertex(predecessors: str, predecessor: str, vertex_id: str): + insert( + f"""\ + # Store the predecessor ({predecessor}) of {vertex_id} + try: + {predecessors}_sequence[{vertex_id}] = {predecessor} + except IndexError: + {predecessors}_wrapper.extend_and_set({vertex_id}, {predecessor}) +""" + ) + + @staticmethod + def store_vertex_if_empty(predecessors: str, predecessor: str, vertex_id: str): + insert( + f"""\ + # Store the predecessor ({predecessor}) of {vertex_id}, if there + # is none so far. In this case, a MutableMapping raises a + # KeyError, and a Sequence contains None or raises an + # IndexError. + try: + if {predecessors}_sequence[{vertex_id}] is None: + {predecessors}_sequence[{vertex_id}] = {predecessor} + except KeyError: + {predecessors}_sequence[{vertex_id}] = {predecessor} + except IndexError: + {predecessors}_wrapper.extend_and_set({vertex_id}, {predecessor}) +""" + ) + + @staticmethod + def store_vertex_and_edge_data( + predecessors: str, + attributes: str, + predecessor: str, + vertex_id: str, + edge_data_expr: str, + from_edge: bool = False, + ): + insert( + f"""\ + # Store the predecessor ({predecessor}) of the neighbor + try: + {predecessors}_sequence[{vertex_id}] = {predecessor} + except IndexError: + {predecessors}_wrapper.extend_and_set({vertex_id}, {predecessor}) + # Store the labels of the edge to the neighbor + if labeled_edges: +""" + ) + if from_edge: + insert( + f"""\ + # Proof for correctness of the type hole: + # self._labeled_edges -> next_edges (a NextWeightedEdges) + # is a NextWeightedLabeledEdges -> {edge_data_expr} is a T_labels + data_of_edge: T_labels = ( + {edge_data_expr} + ) # type: ignore[assignment] +""" + ) + else: + insert( + f"""\ + data_of_edge = {edge_data_expr} +""" + ) + insert( + f"""\ + try: + {attributes}_sequence[{vertex_id}] = data_of_edge + except IndexError: + {attributes}_wrapper.extend_and_set({vertex_id}, data_of_edge) +""" + ) + + +# $$""" diff --git a/tpl/src/nographs/_strategies/MVertexSet.py b/tpl/src/nographs/_strategies/MVertexSet.py new file mode 100644 index 0000000..e64373b --- /dev/null +++ b/tpl/src/nographs/_strategies/MVertexSet.py @@ -0,0 +1,466 @@ +""" Functionality that ease the use of gear collection VertexSet +in traversals. """ + +from tpl.make_insert_look_defined import * + +# """$$ +import re +from collections.abc import Generator + +import_from("tpl/base_lib.py") + + +class MVertexSet: + """Methods to ease implementing performant access to VertexSet objects.""" + + @staticmethod + def access(name: str, ops: list[str] = ("add",)): + for op in ops: + insert( + f"""\ + {name}_{op} = {name}.{op} +""" + ) + insert( + f"""\ + ( + {name}_uses_sequence, + {name}_sequence, + {name}_wrapper, + {name}_uses_bits, + {name}_index_and_bit_method, + ) = access_to_vertex_set({name}) +""" + ) + + @staticmethod + def combine_access(collection1: str, collection2: str, combined: str): + insert( + f"""\ + # Check compatibility of visited and trace_set. It is used for + # performance optimization later on. + assert ( + {collection1}_uses_sequence == {collection2}_uses_sequence + and {collection1}_uses_bits == {collection2}_uses_bits + ), ( + "Collection {collection1} is incompatible " + + "with collection {collection2}" + ) + {combined}_uses_sequence = {collection1}_uses_sequence + del {collection1}_uses_sequence, {collection2}_uses_sequence + {combined}_uses_bits = {collection1}_uses_bits + del {collection1}_uses_bits, {collection2}_uses_bits + if {combined}_uses_sequence and {combined}_uses_bits: + assert {collection1}_index_and_bit_method is {collection2}_index_and_bit_method, ( + "Collection {collection1} is incompatible " + + "with collection {collection2}" + ) + {combined}_index_and_bit_method = {collection1}_index_and_bit_method + del {collection1}_index_and_bit_method, {collection2}_index_and_bit_method +""" + ) + + @staticmethod + def if_visited_continue_else_add(visited: str, vertex_id: str, condition: str): + condition_and = f"{condition} and " if condition else "" + else_or_elif_condition = f"elif {condition}" if condition else "else" + insert( + f"""\ + # (If-nesting optimized for first case) + if {condition_and}not {visited}_uses_sequence: + # Standard implementation for "normal" MutableMapping + if {vertex_id} in {visited}: + continue + {visited}_add({vertex_id}) + elif {condition_and}{visited}_uses_bits: + # Same as above, but with bits in byte sequence + sequence_key, bit_number = {visited}_index_and_bit_method( + {vertex_id}, 8) + bit_mask = 1 << bit_number + try: + value = {visited}_sequence[sequence_key] + if value & bit_mask: + continue + {visited}_sequence[sequence_key] = value | bit_mask + except IndexError: + {visited}_wrapper.extend_and_set( + sequence_key, bit_mask + ) + {else_or_elif_condition}: + # Same as above, but with booleans in byte sequence + try: + if {visited}_sequence[{vertex_id}]: + continue + {visited}_sequence[{vertex_id}] = True + except IndexError: + {visited}_wrapper.extend_and_set({vertex_id}, True) +""" + ) # noqa: E272 + + @staticmethod + def if_visited_continue(visited: str, vertex_id: str, condition: str): + condition_and = f"{condition} and " if condition else "" + else_or_elif_condition = f"elif {condition}" if condition else "else" + insert( + f"""\ + if {condition_and}not {visited}_uses_sequence: + # Standard implementation for "normal" MutableMapping: + if {vertex_id} in {visited}: + continue + elif {condition_and}{visited}_uses_bits: + # Same as above, but with bits in byte sequence + sequence_key, bit_number = {visited}_index_and_bit_method( + {vertex_id}, 8) + try: + if {visited}_sequence[sequence_key] & (1 << bit_number): + continue + except IndexError: + pass + {else_or_elif_condition}: + # Same as above, but with booleans in byte sequence + try: + if {visited}_sequence[{vertex_id}]: + continue + except IndexError: + pass +""" + ) + + @staticmethod + def add(collection: str, vertex_id: str, index: str = ""): + """Add vertex_id to collection. If a combined index is given, + use this.""" + if not index: + index = collection + insert( + f"""\ + # (If-nesting optimized for first case) + if not {index}_uses_sequence: + # Standard implementation for "normal" MutableMapping + {collection}add({vertex_id}) + elif {index}_uses_bits: + # Same as above, but with bits in byte sequence + sequence_key, bit_number = {index}_index_and_bit_method( + {vertex_id}, 8) + try: + {collection}_sequence[sequence_key] |= 1 << bit_number + except IndexError: + {collection}_wrapper.extend_and_set( + sequence_key, 1 << bit_number + ) + else: + # Same as above, but with booleans in byte sequence + try: + {collection}_sequence[{vertex_id}] = True + except IndexError: + {collection}_wrapper.extend_and_set({vertex_id}, True) +""" + ) # noqa: E272 + + @staticmethod + def discard(collection: str, vertex_id: str, index: str = ""): + """Discard vertex_id from collection. If a combined index is given, + use this.""" + if not index: + index = collection + insert( + f"""\ + # (If-nesting optimized for first case) + if not {index}_uses_sequence: + # Standard implementation for "normal" MutableMapping + {collection}_discard({vertex_id}) + elif {index}_uses_bits: + # Same as above, but with bits in byte sequence + sequence_key, bit_number = {index}_index_and_bit_method( + {vertex_id}, 8) + try: + {collection}_sequence[sequence_key] &= ~(1 << bit_number) + except IndexError: + {collection}_wrapper.extend_and_set( + sequence_key, 0 + ) + else: + # Same as above, but with booleans in byte sequence + try: + {collection}_sequence[{vertex_id}] = False + except IndexError: + {collection}_wrapper.extend_and_set({vertex_id}, False) +""" + ) # noqa: E272 + + @staticmethod + def remove(collection: str, vertex_id: str, index: str = ""): + """Remove vertex_id from collection. If a combined index is given, + use this. The collection need to contain *vertex_id* before the + operation, otherwise, the behaviour of the method is not specified! + """ + if not index: + index = collection + insert( + f"""\ + # (If-nesting optimized for first case) + if not {index}_uses_sequence: + # Standard implementation for "normal" MutableMapping + {collection}_discard({vertex_id}) + elif {index}_uses_bits: + # Same as above, but with bits in byte sequence + sequence_key, bit_number = {index}_index_and_bit_method( + {vertex_id}, 8) + {collection}_sequence[sequence_key] -= (1 << bit_number) + else: + # Same as above, but with booleans in byte sequence + {collection}_sequence[{vertex_id}] = False +""" + ) # noqa: E272 + + @staticmethod + def compile_access( + vertex_id: str, + condition: str, + index: str, + code: str, + already_indexed: bool = False, + ): + """ + Replace set.add(), set.remove(), set.discard(), and set.contains() in *code* + by the optimized access prepared by method *access* and, optionally, + *combined_access*. + If *combines_access* has been used, *index* is the combined access path, + otherwise, give the name of the used *vertex_set*. + """ + condition_and = f"{condition} and " if condition else "" + else_or_elif_condition = f"elif {condition}" if condition else "else" + + def iterate_matches(lines: str) -> Generator[tuple[str, str, str]]: + """ + Iterate *lines*, match them on the form + $. + and return tuples match, line, given_indent, set_name, operation. + For lines that do not match, the last three result values are undefined. + """ + for line in lines.splitlines(): + match = re.fullmatch(r"( *)\$(\w+).(\w+.?)", line) + if match is None: + yield match, line, "", "", "" + continue + given_indent, set_name, operation = match.groups() + yield match, line, given_indent, set_name, operation + + insert_with_indent( + "", + f"""\ + if {condition_and}not {index}_uses_sequence: + # Standard implementation for "normal" MutableSet + """, + ) + indent = " " + for match, line, given_indent, set_name, operation in iterate_matches(code): + if match is None: + insert(indent + line + "\n") + elif operation == "add_vertex_id": + insert(f"{indent}{given_indent}{set_name}_add({vertex_id})\n") + elif operation == "discard_vertex_id": + insert(f"{indent}{given_indent}{set_name}_remove({vertex_id})\n") + elif ( + operation == "if_contains_vertex_id:" + or operation == "if_contains_vertex_id_prepare_remove_and_elseadd:" + ): + insert(f"{indent}{given_indent}if {vertex_id} in {set_name}:\n") + elif operation == "prepared_remove_vertex_id": + insert(f"{indent}{given_indent}{set_name}_discard({vertex_id})\n") + elif operation == "else_prepared_add_endif": + insert( + f"{indent}{given_indent}else:" + f"{indent}{given_indent} {set_name}_add({vertex_id})\n" + ) + elif operation == "endif": + pass + elif operation == "endif_with_vertex_included_in_past": + pass + else: + raise RuntimeError("Operation not supported:" + operation) + + insert_with_indent( + "", + f"""\ + + elif {condition_and}not {index}_uses_bits: + # Same as above, but with booleans in byte sequence + """, + ) + for match, line, given_indent, set_name, operation in iterate_matches(code): + if match is None: + insert(indent + line + "\n") + elif operation == "add_vertex_id": + insert_with_indent( + indent + given_indent, + f"""\ + try: + {set_name}_sequence[{vertex_id}] = True + except IndexError: + {set_name}_wrapper.extend_and_set({vertex_id}, True) + """, + ) + elif operation == "discard_vertex_id": + insert_with_indent( + indent + given_indent, + f"""\ + try: + {set_name}_sequence[{vertex_id}] = False + except IndexError: + {set_name}_wrapper.extend_and_set({vertex_id}, False) + """, + ) + elif ( + operation == "if_contains_vertex_id:" + or operation == "if_contains_vertex_id_prepare_remove_and_elseadd:" + ): + insert_with_indent( + indent + given_indent, + f"""\ + try: + if {set_name}_sequence[{vertex_id}]: + """, + ) + indent += " " + elif operation == "prepared_remove_vertex_id": + insert_with_indent( + indent + given_indent, + f"""\ + {set_name}_sequence[{vertex_id}] = False + """, + ) + elif operation == "else_prepared_add_endif": + indent = indent.removesuffix(" ") + insert_with_indent( + indent + given_indent, + f"""\ + else: + {set_name}_sequence[{vertex_id}] = True + except IndexError: + {set_name}_wrapper.extend_and_set({vertex_id}, True) + """, + ) + elif operation == "endif": + indent = indent.removesuffix(" ") + insert_with_indent( + indent + given_indent, + f"""\ + except IndexError: + pass + """, + ) + elif operation == "endif_with_vertex_included_in_past": + indent = indent.removesuffix(" ") + insert_with_indent( + indent + given_indent, + f"""\ + except IndexError: # pragma: no cover + raise AssertionError( + "Internal error: IndexError " + "should never happen" + ) + """, + ) + else: + raise RuntimeError("Operation not supported:" + operation) + + insert_with_indent( + "", + f"""\ + + {else_or_elif_condition}: + # Same as above, but with bits in byte sequence + """, + ) + if not already_indexed: + insert_with_indent( + "", + f"""\ + sequence_key, bit_number = {index}_index_and_bit_method({vertex_id}, 8) + bit_mask = 1 << bit_number + """, + ) + for match, line, given_indent, set_name, operation in iterate_matches(code): + if match is None: + insert(indent + line + "\n") + elif operation == "add_vertex_id": + insert_with_indent( + indent + given_indent, + f"""\ + try: + {set_name}_sequence[sequence_key] |= bit_mask + except IndexError: + {set_name}_wrapper.extend_and_set(sequence_key, bit_mask) + """, + ) + elif operation == "discard_vertex_id": + insert_with_indent( + indent + given_indent, + f"""\ + try: + {set_name}_sequence[sequence_key] &= ~bit_mask + except IndexError: + {set_name}_wrapper.extend_and_set(sequence_key, bit_mask) + """, + ) + elif operation == "if_contains_vertex_id:": + insert_with_indent( + indent + given_indent, + f"""\ + try: + if {set_name}_sequence[sequence_key] & bit_mask: + """, + ) + indent += " " + elif operation == "if_contains_vertex_id_prepare_remove_and_elseadd:": + insert_with_indent( + indent + given_indent, + f"""\ + try: + value = {set_name}_sequence[sequence_key] + if value & bit_mask: + """, + ) + indent += " " + elif operation == "prepared_remove_vertex_id": + insert( + f"{given_indent}{indent}{set_name}_sequence[sequence_key] = value - bit_mask\n" + ) + elif operation == "else_prepared_add_endif": + indent = indent.removesuffix(" ") + insert_with_indent( + indent + given_indent, + f"""\ + else: + {set_name}_sequence[sequence_key] = value | bit_mask + except IndexError: + {set_name}_wrapper.extend_and_set(sequence_key, bit_mask) + """, + ) + elif operation == "endif": + indent = indent.removesuffix(" ") + insert_with_indent( + indent + given_indent, + f"""\ + except IndexError: + pass + """, + ) + elif operation == "endif_with_vertex_included_in_past": + indent = indent.removesuffix(" ") + insert_with_indent( + indent + given_indent, + f"""\ + except IndexError: # pragma: no cover + raise AssertionError( + "Internal error: IndexError " + "should never happen" + ) + """, + ) + else: + raise RuntimeError("Operation not supported:" + operation) + + +# $$""" diff --git a/tpl/src/nographs/_strategies/traversals/with_weights/!a_star.py b/tpl/src/nographs/_strategies/traversals/with_weights/!a_star.py new file mode 100644 index 0000000..42ccdf0 --- /dev/null +++ b/tpl/src/nographs/_strategies/traversals/with_weights/!a_star.py @@ -0,0 +1,324 @@ +from __future__ import annotations + +import itertools +from heapq import heapify, heappop, heappush +from numbers import Real +from typing import Optional, Any, Generic, Union +from collections.abc import Callable, Iterable, Generator + +from nographs._types import ( + T_vertex, + T_labels, + VertexToID, + T_vertex_id, + T_weight, + vertex_as_id, +) +from nographs._gears import ( + Gear, + GearDefault, + VertexIdToDistanceMapping, +) +from nographs._gear_collections import ( + access_to_vertex_mapping_expect_none, + access_to_vertex_mapping, +) +from ...type_aliases import NextWeightedEdges, NextWeightedLabeledEdges +from ...utils import define_distances, iter_start_vertices_and_ids +from ..traversal import Traversal +from .traversal_with_weights import ( + _create_unified_next_weighted, + _TraversalWithDistances, +) + +"$$ import_from('$$/MTraversalWithWeights.py') $$" + + +class TraversalAStarFlex( + _TraversalWithDistances[T_vertex, T_vertex_id, T_weight, T_labels] +): + """ + # $$ insert_from('$$/cls_traversal/doc_start.rst') + + :param is_tree: bool: If it is certain, that during each traversal run, each vertex + can be reached only once, is_tree can be set to True. This improves performance, + but if *start_from* has been called with parameter *known_path_length_guesses* + given, this collection will not be updated during the traversal. + + **Algorithm:** The search algorithm A*, non-recursive, based on heap. + + # $$ insert_from('$$/cls_traversal/doc_input.rst') + A heuristic function that estimates the cost of the cheapest path from a given + vertex to the goal (resp. to any of your goal vertices, if you have more than + one), and never overestimates the actual needed costs ("admissible heuristic + function"). + + **Properties:** Vertices are reported and expanded ordered by increasing path + length (sum of edge weights) of the shortest paths from a start vertex to the + respective vertex that have been found so far. + + When the goal is reported, the path stored for it in *paths* is a shortest + path from start to goal and the path_length of the search state is the distance + of the goal from start. + + In case the used heuristic function is *consistent* (i.e., following an edge from + one vertex to another never reduces the estimated costs to get to the goal by + more than the weight of the edge), further guarantees hold: Each vertex is only + visited once. And for each visited vertex, the respective path_length and depth + (and optionally, the path) are the data of the shortest existing path from start + (not only from the shortest path found so far). + + **Search state:** When a vertex is *expanded* (traversal calls next_edges or + next_labeled_edges) or *reported* (an iterator of the traversal returns it), + the traversal provides updated values for the attributes + *path_length*, *depth*, *paths*. + """ + + def __init__( + self, + # $$ MStrategyWithWeights.init_signature('TraversalAStarFlex') + is_tree: bool = False, + ) -> None: + # $$ MStrategyWithWeights.init_code('is_tree') + + # The following three values are not used by NoGraphs. They are only set + # to have some initialization. + self.path_length: T_weight = self._gear.infinity() + """ Length (sum of edge weights) of the found path to the + vertex (for the goal vertex: a shortest path) + """ + self.depth: int = -1 + """ At this *search depth*, the reported (resp. the expanded) vertex has been + found. It equals the length (number of edges) of the created path to the + vertex, if path creation is demanded. + Note: The search depth does not need to be the depth of the vertex + (see `TraversalBreadthFirstFlex`). + """ + self._start_vertices_and_ids = tuple[tuple[T_vertex, T_vertex_id]]() + + self._heuristic: Optional[Callable[[T_vertex], Real]] = None + self._known_distances: Optional[ + VertexIdToDistanceMapping[T_vertex_id, T_weight] + ] = None + self._known_path_length_guesses: Optional[ + VertexIdToDistanceMapping[T_vertex_id, T_weight] + ] = None + self._path_length_guesses: Optional[ + VertexIdToDistanceMapping[T_vertex_id, T_weight] + ] = None + + def start_from( + self, + heuristic: Callable[[T_vertex], Real], + # $$ insert_from('$$/method_start_from/signature_standard.py') + known_distances: Optional[ + VertexIdToDistanceMapping[T_vertex_id, T_weight] + ] = None, + known_path_length_guesses: Optional[ + VertexIdToDistanceMapping[T_vertex_id, T_weight] + ] = None, + ) -> TraversalAStarFlex[T_vertex, T_vertex_id, T_weight, T_labels]: + """ + Start the traversal at a vertex or a set of vertices and set parameters. + + :param heuristic: The admissible and consistent heuristic function that + estimates the cost of the cheapest path from a given vertex to the goal + (resp. one of the goals). + + # $$ insert_from('$$/method_start_from/doc_std.rst') + + :param known_distances: If provided, this mapping is used instead of an internal + one to keep the distances of vertices that have already been visited + (resp. their hashable ids from vertex_to_id is used as key) from some + start vertex. For vertices without known distance, it must yield float( + 'infinity'). The internal default implementation uses a + collections.defaultdict. + + Typical use cases are: 1) preloading known distances of vertices, and the + vertices should not be visited if no smaller distance is found during the + traversal, or 2) getting online access to the internal bookkeeping of + visited vertices and their distances, or 3) providing your own way for + storing the distance of a vertex that has already been visited. + + :param known_path_length_guesses: Like known_distances, but for keeping the sum + distance+heuristic for vertices. + + :return: Traversal, that has been started, e.g., the methods go* can now be + used. + """ + + "$$ insert_from('$$/method_start_from/code_start.py') $$" + self._heuristic = heuristic + self._known_distances = known_distances + self._known_path_length_guesses = known_path_length_guesses + + # At start, most of the distances from a vertex to a start vertex are not + # known. If accessed for comparison for possibly better distances, infinity + # is used, if no other value is given. Each start vertex has distance 0 + # from a start vertex (itself), if not defined otherwise. + # Here, the distances need to be initialized even if is_tree == True, since + # the algorithm stores path length guesses in the heap and always uses + # the distances from the collection. + zero = self._gear.zero() + self.distances = define_distances( + self._gear, + self._known_distances, + ((vertex_id, zero) for vertex, vertex_id in self._start_vertices_and_ids), + False, + ) + + # The following two values are not used by NoGraphs. They are only set + # to have some defined values before the traversal iterator sets them. + self.path_length = self._gear.infinity() + self.depth = 0 + + super()._start() + return self + + def _traverse(self) -> Generator[T_vertex, None, Any]: + "$$ insert_from('$$/method_traverse/code_start.py') $$" + + "$$ MCalculationLimit.prepare() $$" + + # Copy Gear attributes into method scope (faster access) + infinity = self._gear.infinity() + + # Copy traversal specific attributes into method scope + is_tree = self._is_tree + heuristic = self._heuristic + + # Get references of used gear objects and methods (avoid attribute resolution) + "$$ MVertexMappingExpectNone.access(name='predecessors') $$" + "$$ MVertexMappingExpectNone.access(name='attributes') $$" + + # ----- Initialize method specific private bookkeeping ----- + + # Unique number, that prevents heapq from sorting by vertices in case of a + # tie in the sort field, because vertices do not need to be pairwise + # comparable. The numbers are generated in decreasing order to make the min + # heap behave like a LIFO queue in case of ties. The integers from -5 to 256 + # are used first, because they are internalized (pre-calculated, and thus + # fastest). + unique_no = itertools.count(256, -1) + + # Get references of used gear objects and methods (avoid attribute resolution) + distances = self.distances + "$$ MVertexMapping.access(name='distances') $$" + + # Each start vertex has path_length_guess of distance + heuristic(vertex), + # if not defined otherwise. + assert heuristic is not None # set by __init__ + path_length_guesses = define_distances( + self._gear, + self._known_path_length_guesses, + ( + ( + vertex_id, + """$$ MVertexMapping.get_included('distances', 'vertex_id') + $$""" + + heuristic(vertex), + ) + for vertex, vertex_id in self._start_vertices_and_ids + ), + is_tree, + ) + # Get references of used gear objects and methods (avoid attribute resolution) + "$$ MVertexMapping.access('path_length_guesses') $$" + + # So far, the start vertices are to be visited. Each has an edge count of 0, + # and its path length guess is the one computed above. + to_visit = [ # used as collection.heapq of tuples, the lowest distance first + ( + """$$ MVertexMapping.get_included('path_length_guesses', 'vertex_id') + $$""" + # (This comment is just to prevent Black from moving the comma up) + , + next(unique_no), + vertex, + 0, + ) + for vertex, vertex_id in self._start_vertices_and_ids + ] + heapify(to_visit) + + # ----- Inner loop ----- + + while to_visit: + # Visit path with the lowest path_length_guess first + path_length_guess, _, vertex, path_edge_count = heappop(to_visit) + + # A vertex can get added to the heap multiple times. + + # For consistent heuristics: We want to process the vertex only once, the + # first time it is removed from the heap, because this is the case with the + # shortest distance estimation. If the heuristic is not consistent: Only + # when the new distance estimation is better than the best found so far, we + # want to process the vertex again. + "$$ MStrategy.vertex_to_id('vertex', 'v_id') $$" + + # (No index exception possible at the following indexed access) + if ( + not is_tree + and path_length_guess + > """$$ + MVertexMapping.get_included('path_length_guesses', 'v_id')$$""" + ): + continue + + # (No index exception possible here) + path_weight = "$$ MVertexMapping.get_included('distances', 'v_id') $$" + + # Export traversal data to traversal attributes + self.path_length = path_weight + self.depth = path_edge_count + + # We now know the distance of the vertex, so we report it. + if path_edge_count > 0: # do not report start vertex + yield vertex + + "$$ MCalculationLimit.step() $$" + + # Expand vertex. New neighbors are one edge count deeper than vertex. + n_path_edge_count = path_edge_count + 1 + for edge in next_edges(vertex, self): + neighbor, weight = edge[0], edge[1] + + n_path_weight = weight + path_weight + "$$ MStrategyWithWeights.check_overflow('n_path_weight') $$" + + # If the found path to the neighbor is not shorter than the shortest + # such path found so far, and we are not in a tree, we can safely + # ignore the path. Otherwise, it is a new candidate for a shortest + # path to the neighbor, and we push it to the heap. + "$$ MStrategy.vertex_to_id('neighbor', 'n_id') $$" + + """$$ MVertexMapping.if_value_smaller_or_condition_set_else_continue( + 'distances', 'n_id', 'n_path_weight', 'is_tree') $$""" + + # If we are to generate a path, we have to do it here, since the edge + # we have to add to the path prefix is not stored on the heap + if build_paths: + """$$ MVertexMappingExpectNone.store_vertex_and_edge_data( + 'predecessors', 'attributes', + 'vertex', 'n_id', + 'edge[-1]', from_edge=True) + $$""" + + h = heuristic(neighbor) + if h == infinity: + n_guess = infinity + else: + n_guess = n_path_weight + h + "$$ MStrategyWithWeights.check_overflow('n_guess') $$" + + if not is_tree: + "$$ MVertexMapping.set('path_length_guesses', 'n_id', 'n_guess') $$" + heappush( + to_visit, + (n_guess, next(unique_no), neighbor, n_path_edge_count), + ) + + +"""$$ MStrategyWithWeights.standard_for_flex( + 'TraversalAStar', 'is_tree: bool = False,\n', 'is_tree=is_tree,\n') +$$""" diff --git a/tpl/src/nographs/_strategies/traversals/with_weights/!minimum_spanning_tree.py b/tpl/src/nographs/_strategies/traversals/with_weights/!minimum_spanning_tree.py new file mode 100644 index 0000000..479e302 --- /dev/null +++ b/tpl/src/nographs/_strategies/traversals/with_weights/!minimum_spanning_tree.py @@ -0,0 +1,196 @@ +from __future__ import annotations + +import itertools +from heapq import heapify, heappop, heappush +from typing import Optional, Any, Generic, Union +from collections.abc import Iterable, Generator + +from nographs._types import ( + T_vertex, + T_labels, + VertexToID, + T_vertex_id, + T_weight, + vertex_as_id, + WeightedFullEdge, + WeightedOutEdge, +) +from nographs._gears import ( + Gear, + GearDefault, + VertexIdSet, +) +from nographs._gear_collections import ( + access_to_vertex_set, + access_to_vertex_mapping_expect_none, +) +from ...type_aliases import ( + NextWeightedEdges, + NextWeightedLabeledEdges, +) +from ...utils import iter_start_vertices_and_ids +from ..traversal import Traversal +from .traversal_with_weights import _create_unified_next_weighted, _TraversalWithWeights + +"$$ import_from('$$/MTraversalWithWeights.py') $$" + + +class TraversalMinimumSpanningTreeFlex( + _TraversalWithWeights[T_vertex, T_vertex_id, T_weight, T_labels] +): + """ + # $$ insert_from('$$/cls_traversal/doc_start.rst') + + **Algorithm:** Minimum spanning tree ("MST") algorithm of Jarnik, Prim, Dijkstra. + Non-recursive, based on heap. A so-called *tie breaker* is implemented, that + prioritizes edges that have been found more recently about edges that have been + found earlier. This is a typical choice that often improves search performance. + + **Properties:** Only edges of the MST from start vertices are reported. Each + vertex is reported (as end vertex of an edge) and expanded only once. Computed + paths only use MST edges. + + **Input:** Weighted undirected graph, given as directed edges with the same + weight in both directions. One or more start vertices (e.g. for components in + unconnected graphs). Optional calculation limit. + + **Search state:** When a vertex is *expanded* (traversal calls next_edges or + next_labeled_edges) or an edge is *reported* (an iterator of the traversal returns + the vertex it leads to), the traversal provides updated values for the attributes + *edge* and *paths*. + """ + + def __init__( + self, + # $$ MStrategyWithWeights.init_signature('TraversalMinimumSpanningTreeFlex') + ) -> None: + # $$ MStrategyWithWeights.init_code(is_tree='False') + + self.edge: Optional[WeightedFullEdge[T_vertex, T_weight, T_labels]] = None + """ Tuple of from_vertex, to_vertex, the weight of the edge, + and additional data you have provided with the edge (if so). + """ + + # The following value is not used by NoGraphs. It is only set + # to have some initialization. + self._start_vertices_and_ids = tuple[tuple[T_vertex, T_vertex_id]]() + + def start_from( + self, + # $$ insert_from('$$/method_start_from/signature_standard.py') + ) -> TraversalMinimumSpanningTreeFlex[T_vertex, T_vertex_id, T_weight, T_labels]: + """ + Start the traversal at a vertex or a set of vertices and set parameters. + If you provide more than one start vertex, the result consists of several + trees that are only connected if the start vertices are connected. + + # $$ insert_from('$$/method_start_from/doc_std.rst') + + :return: Traversal, that has been started, e.g., the methods go* can now be + used. + """ + + "$$ insert_from('$$/method_start_from/code_start.py') $$" + + # The following value is not used by NoGraphs. It is only set + # to have some defined value before the traversal iterator sets them. + self.edge = None + + super()._start() + return self + + def _traverse(self) -> Generator[T_vertex, None, Any]: + "$$ insert_from('$$/method_traverse/code_start.py') $$" + + # Get references of used gear objects and methods (avoid attribute resolution) + "$$ MVertexMappingExpectNone.access(name='predecessors') $$" + "$$ MVertexMappingExpectNone.access(name='attributes') $$" + + # ----- Initialize method specific private bookkeeping ----- + + # At start, only the start vertices are regarded as visited + # (The protocol VertexSet abandons checking the element type, see + # VertexSet. Flake8 and MyPy accept this, PyCharm does not and need + # noinspection.) + # noinspection PyTypeChecker + visited: VertexIdSet[T_vertex_id] = self._gear.vertex_id_set( + vertex_id for vertex, vertex_id in self._start_vertices_and_ids + ) + + # Check if we already go over the calculation limit when we evaluate the + # edges from start vertices ("expanding the start vertices"). This avoids a + # step by step check that slows down the to_visit loop for large sets of + # start vertices. Note: A calculation limit below 0 leads nowhere ever to an + # exception. So, neither here. + # $$ MCalculationLimit.step("len(self._start_vertices_and_ids)") + + # So far, the edges from the start vertices are to be visited as candidates + # for edges of a MST. (Unique number prevents heapq from sorting by (possibly + # not comparable) fields) + unique_no = itertools.count() + to_visit: list[ + tuple[ + T_weight, int, T_vertex, WeightedOutEdge[T_vertex, T_weight, T_labels] + ] + ] = [ # used as collection.heapq, the lowest edge weight first + (edge[1], next(unique_no), vertex, edge) + for vertex, vertex_id in self._start_vertices_and_ids + for edge in next_edges(vertex, self) + ] + heapify(to_visit) + + "$$ MCalculationLimit.prepare() $$" + + # Get references of used gear objects and methods (avoid attribute resolution) + "$$ MVertexSet.access(name='visited') $$" + + # ----- Inner loop ----- + + while to_visit: + # Visit edge with the lowest weight first + _weight, _, vertex, to_edge = heappop(to_visit) + to_vertex = to_edge[0] + + # A vertex can get added to the heap multiple times, as end vertex of + # several edges. We want to process it only once, as end vertex of a MST + # edge. + # The shortest edge from a visited vertex that leads to a vertex not + # visited so far, must be an edge of the MST. + "$$ MStrategy.vertex_to_id('to_vertex', 'to_id') $$" + + "$$ MVertexSet.if_visited_continue_else_add('visited', 'to_id', '') $$" + + if build_paths: + """$$ MVertexMappingExpectNone.store_vertex_and_edge_data( + 'predecessors', 'attributes', + 'vertex', 'to_id', + 'to_edge[-1]', from_edge=True) + $$""" + + # Export traversal data to traversal attribute and report vertex + # (Expression type follows from types of vertex and to_edge and the + # definition of WeightedFullEdge. MyPy + PyCharm cannot derive this.) + # noinspection PyTypeChecker + full_edge: WeightedFullEdge[T_vertex, T_weight, T_labels] = ( + vertex, + ) + to_edge # type: ignore[assignment] + self.edge = full_edge + yield to_vertex + + "$$ MCalculationLimit.step() $$" + + for n_to_edge in next_edges(to_vertex, self): + n_to_vertex, n_weight = n_to_edge[0], n_to_edge[1] + # If the edge leads to a vertex that is, so far, not reached by edges + # of the MST, it is a candidate for a MST edge. We push it to the heap. + "$$ MStrategy.vertex_to_id('n_to_vertex', 'n_to_id') $$" + + "$$ MVertexSet.if_visited_continue('visited', 'n_to_id', '') $$" + + heappush( + to_visit, + (n_weight, next(unique_no), to_vertex, n_to_edge), + ) + + +"$$ MStrategyWithWeights.standard_for_flex('TraversalMinimumSpanningTree') $$" diff --git a/tpl/src/nographs/_strategies/traversals/with_weights/!shortest_paths.py b/tpl/src/nographs/_strategies/traversals/with_weights/!shortest_paths.py new file mode 100644 index 0000000..f88f48d --- /dev/null +++ b/tpl/src/nographs/_strategies/traversals/with_weights/!shortest_paths.py @@ -0,0 +1,257 @@ +from __future__ import annotations + +import itertools +from heapq import heapify, heappop, heappush +from typing import Optional, Any, Generic, Union +from collections.abc import Iterable, Generator + +from nographs._gears import VertexIdToDistanceMapping +from nographs._types import ( + T_vertex, + T_labels, + VertexToID, + T_vertex_id, + T_weight, + vertex_as_id, +) +from nographs._gears import ( + Gear, + GearDefault, +) +from nographs._gear_collections import ( + access_to_vertex_mapping, + access_to_vertex_mapping_expect_none, +) +from ...type_aliases import ( + NextWeightedEdges, + NextWeightedLabeledEdges, +) +from ...utils import iter_start_vertices_and_ids, define_distances +from ..traversal import Traversal +from .traversal_with_weights import ( + _create_unified_next_weighted, + _TraversalWithDistance, +) + +"$$ import_from('$$/MTraversalWithWeights.py') $$" + + +class TraversalShortestPathsFlex( + _TraversalWithDistance[T_vertex, T_vertex_id, T_weight, T_labels] +): + """ + # $$ insert_from('$$/cls_traversal/doc_start.rst') + + :param is_tree: bool: If it is certain, that during each traversal run, each vertex + can be reached only once, is_tree can be set to True. This improves performance, + but attribute *distances* of the traversal will not be updated during and after + the traversal. + + **Algorithm:** Shortest paths algorithm of Dijkstra, non-recursive, based on heap. + + **Properties:** Vertices are reported (and expanded) ordered by increasing distance + (minimally necessary sum of edge weights) from a start vertex. + + # $$ insert_from('$$/cls_traversal/doc_input.rst') + + **Search state:** When a vertex is *expanded* (traversal calls next_edges or + next_labeled_edges) or *reported* (an iterator of the traversal returns it), + the traversal provides updated values for the attributes + *distance*, *depth*, *paths*, and *distances*. + """ + + def __init__( + self, + # $$ MStrategyWithWeights.init_signature('TraversalShortestPathsFlex') + is_tree: bool = False, + ) -> None: + # $$ MStrategyWithWeights.init_code('is_tree') + + self._known_distances: Optional[ + VertexIdToDistanceMapping[T_vertex_id, T_weight] + ] = None + self._keep_distances = False + + # The following value is not used by NoGraphs. It is only set + # to have some initialization. + self.depth: int = -1 + """ At this *search depth*, the reported (resp. the expanded) vertex has been + found. It equals the length (number of edges) of the created path to the + vertex, if path creation is demanded. + Note: The search depth does not need to be the depth of the vertex + (see `TraversalBreadthFirstFlex`). + When a traversal has been started, but no vertex has been reported or expanded + so far, the depth is 0 (depth of the start vertices). + """ + self._start_vertices_and_ids = tuple[tuple[T_vertex, T_vertex_id]]() + + def start_from( + self, + # $$ insert_from('$$/method_start_from/signature_standard.py') + keep_distances: bool = False, + known_distances: Optional[ + VertexIdToDistanceMapping[T_vertex_id, T_weight] + ] = None, + ) -> TraversalShortestPathsFlex[T_vertex, T_vertex_id, T_weight, T_labels]: + """ + Start the traversal at a vertex or a set of vertices and set parameters. + + # $$ insert_from('$$/method_start_from/doc_std.rst') + + :param keep_distances: If True, the found distances of vertices are + collected in traversal attribute distances, and not deleted after + having reported the vertex. See attribute distances. + + :param known_distances: If provided, this mapping is used instead of an internal + one to keep distance candidates and final distances values of reported + vertices (resp. their hashable ids from vertex_to_id is used as key) from + some start vertex. + + For vertices without known distance, it must yield float('infinity'). The + internal default implementation uses a collections.defaultdict. Typical + use cases are: 1) preloading known distances of vertices, and the + vertices should not be visited if no smaller distance is found during the + traversal, and 2) providing your own way for storing the distances. + + :return: Traversal, that has been started, e.g., the methods go* can now be + used. + """ + + "$$ insert_from('$$/method_start_from/code_start.py') $$" + self._keep_distances = keep_distances + self._known_distances = known_distances + + "$$ insert_from('$$/method_start_from/code_init_distances_and_zero.py') $$" + + # The following two values are not used by NoGraphs. They are only set + # to have some defined values before the traversal iterator sets them. + self.distance = self._gear.infinity() + self.depth = 0 + + super()._start() + return self + + def _traverse(self) -> Generator[T_vertex, None, Any]: + "$$ insert_from('$$/method_traverse/code_start.py') $$" + + "$$ MCalculationLimit.prepare() $$" + + # Copy Gear attributes into method scope (faster access) + infinity = self._gear.infinity() + + # Copy traversal specific attributes into method scope + is_tree = self._is_tree + keep_distances = self._keep_distances + + # Get references of used gear objects and methods (avoid attribute resolution) + "$$ MVertexMappingExpectNone.access(name='predecessors') $$" + "$$ MVertexMappingExpectNone.access(name='attributes') $$" + zero = self._gear.zero() + + # ----- Initialize method specific private bookkeeping ----- + + # Unique number, that prevents heapq from sorting by vertices in case of a + # tie in the sort field, because vertices do not need to be pairwise + # comparable. The integers from -5 to 256 are used first, because they are + # internalized (pre-calculated, and thus fastest). We count downwards like we + # do in A* search. There, it is preferable, because a LIFO behavior makes A* + # often faster. Here, we do it simply to do it the same way. + unique_no = itertools.count(256, -1) + + # Get references of used gear objects and methods (avoid attribute resolution) + distances = self.distances + "$$ MVertexMapping.access('distances') $$" + + # So far, the start vertices are to be visited. Each has an edge count of 0. + # (We know: vertex_id in distances. No index exception possible here.) + to_visit = [ # used as collection.heapq of tuples, the lowest distance first + ( + ("""$$ MVertexMapping.get_included('distances', 'vertex_id') $$"""), + next(unique_no), + vertex, + 0, + ) + for vertex, vertex_id in self._start_vertices_and_ids + ] + heapify(to_visit) + + # ----- Inner loop ----- + + while to_visit: + # Visit path with the lowest distance first + path_weight, _, vertex, path_edge_count = heappop(to_visit) + + # A vertex can get added to the heap multiple times. We want to process + # it only once, the first time it is removed from the heap, because this + # is the case with the shortest distance from start. + if not is_tree: + "$$ MStrategy.vertex_to_id('vertex', 'v_id') $$" + + # (We know: v_id in distances. No index exception possible here.) + if ( + "$$ MVertexMapping.get_included('distances', 'v_id') $$" + < path_weight + ): + continue + if not keep_distances: + # Allow garbage collector to free distance value (not the entry in + # the collection!) if nowhere else needed any more. Note that + # we can only set to the zero value here, since we still have to + # exclude further longer paths to the vertex, and only zero does + # this job for all possible distances. + # (We know: v_id in distances. No index exception possible here.) + "$$ MVertexMapping.set_included('distances', 'v_id', 'zero') $$" + + # Export traversal data to traversal attributes + self.distance = path_weight + self.depth = path_edge_count + + # We now know the distance of the vertex, so we report it. + if path_edge_count > 0: # do not report start vertex + yield vertex + + "$$ MCalculationLimit.step() $$" + + # Expand vertex. New neighbors are one edge count deeper than vertex. + n_path_edge_count = path_edge_count + 1 + for edge in next_edges(vertex, self): + neighbor, weight = edge[0], edge[1] + + n_path_weight = weight + path_weight + "$$ MStrategyWithWeights.check_overflow('n_path_weight') $$" + + # If the found path to the neighbor is not shorter than the shortest + # such path found so far, we can safely ignore it. Otherwise, it is a + # new candidate for a shortest path to the neighbor, and we push it to + # the heap. + if build_paths or not is_tree: + "$$ MStrategy.vertex_to_id('neighbor', 'n_id') $$" + + if not is_tree: + """$$ + MVertexMapping.if_value_smaller_set_else_continue( + 'distances', 'n_id', 'n_path_weight') $$""" + + # If we are to generate a path, we have to do it here, since the + # edge we have to add to the path prefix is not stored on the heap. + if build_paths: + """$$ MVertexMappingExpectNone.store_vertex_and_edge_data( + 'predecessors', 'attributes', + 'vertex', 'n_id', + 'edge[-1]', from_edge=True) + $$""" + + heappush( + to_visit, + ( + n_path_weight, + next(unique_no), + neighbor, + n_path_edge_count, + ), + ) + + +"""$$ MStrategyWithWeights.standard_for_flex( + 'TraversalShortestPaths', 'is_tree: bool = False,\n', 'is_tree=is_tree,\n') +$$""" diff --git a/tpl/src/nographs/_strategies/traversals/with_weights/MTraversalWithWeights.py b/tpl/src/nographs/_strategies/traversals/with_weights/MTraversalWithWeights.py new file mode 100644 index 0000000..959bcb2 --- /dev/null +++ b/tpl/src/nographs/_strategies/traversals/with_weights/MTraversalWithWeights.py @@ -0,0 +1,141 @@ +from tpl.make_insert_look_defined import * + +# """$$ +import_from("$$/../../MStrategy.py") + + +class MStrategyWithWeights: + """Methods to implement subclasses of _TraversalWithWeight.""" + + @staticmethod + def init_signature(traversal_type: str) -> None: + insert( + f"""\ + vertex_to_id: VertexToID[T_vertex, T_vertex_id], + gear: Gear[T_vertex, T_vertex_id, T_weight, T_labels], + next_edges: Optional[ + NextWeightedEdges[ + T_vertex, + {traversal_type}[T_vertex, T_vertex_id, T_weight, T_labels], + T_weight, + ] + ] = None, + *, + next_labeled_edges: Optional[ + NextWeightedLabeledEdges[ + T_vertex, + {traversal_type}[T_vertex, T_vertex_id, T_weight, T_labels], + T_weight, + T_labels, + ] + ] = None, +""" + ) + + @staticmethod + def init_code(is_tree: str) -> None: + insert( + f"""\ + self._next_edges, labeled_edges = _create_unified_next_weighted( + next_edges, next_labeled_edges + ) + super().__init__(labeled_edges, {is_tree}, vertex_to_id, gear) +""" + ) + + @staticmethod + def check_overflow(variable: str) -> None: + insert( + f"""\ + # (Distance values equal to or higher than the chosen infinity + # value of the gear are invalid and cannot be handled further.) + if infinity <= {variable}: + self._gear.raise_distance_infinity_overflow_error({variable}) +""" + ) + + @staticmethod + def standard_for_flex( + class_name: str, add_parameters: str = "", add_code: str = "" + ) -> None: + insert( + f'''\ + class {class_name} ( + Generic[T_vertex, T_weight, T_labels], + {class_name}Flex[T_vertex, T_vertex, Union[T_weight, float], T_labels] + ): + """ + Eases the use of `{class_name}Flex` for typical cases. + For documentation of functionality and parameters, see there. + + .. code-block:: python + + {class_name}[T_vertex, T_weight, T_labels](*args, **keywords) + + is a short form for + + .. code-block:: python + + {class_name}Flex[ + T_vertex, T_vertex, Union[T_weight, float], T_labels], + ](nog.vertex_as_id, nog.GearDefault(), *args, **keywords) + + Implications: + + - `GearDefault` is used, see there how it and its superclass work + - The used weights are defined by Union[T_weight, float], see `GearDefault` + - T_vertex is bound to Hashable (T_vertex is used as `T_vertex_id`, see there) + """ + + def __init__( + self, + next_edges: Optional[ + NextWeightedEdges[ + T_vertex, + {class_name}Flex[ + T_vertex, T_vertex, Union[T_weight, float], T_labels + ], + T_weight, + ] + ] = None, + *, + next_labeled_edges: Optional[ + NextWeightedLabeledEdges[ + T_vertex, + {class_name}Flex[ + T_vertex, T_vertex, Union[T_weight, float], T_labels + ], + T_weight, + T_labels, + ] + ] = None, +''' + ) + if add_parameters: + insert( + f"""\ + {add_parameters}""" + ) + insert( + f"""\ + ) -> None: + super().__init__( + vertex_as_id, + GearDefault(), + next_edges, + next_labeled_edges=next_labeled_edges, +""" + ) + if add_code: + insert( + f"""\ + {add_code}""" + ) + insert( + f"""\ + ) +""" + ) + + +# $$""" diff --git a/tpl/src/nographs/_strategies/traversals/with_weights/cls_traversal/doc_input.rst b/tpl/src/nographs/_strategies/traversals/with_weights/cls_traversal/doc_input.rst new file mode 100644 index 0000000..ae7d5c6 --- /dev/null +++ b/tpl/src/nographs/_strategies/traversals/with_weights/cls_traversal/doc_input.rst @@ -0,0 +1,2 @@ +**Input:** Weighted directed graph. Weights need to be non-negative. +One or more start vertices. Optional calculation limit. diff --git a/tpl/src/nographs/_strategies/traversals/with_weights/cls_traversal/doc_start.rst b/tpl/src/nographs/_strategies/traversals/with_weights/cls_traversal/doc_start.rst new file mode 100644 index 0000000..c0657ca --- /dev/null +++ b/tpl/src/nographs/_strategies/traversals/with_weights/cls_traversal/doc_start.rst @@ -0,0 +1,12 @@ +| Bases: Generic[`T_vertex`, `T_vertex_id`, `T_weight`, `T_labels`], +| `Traversal` [`T_vertex`, `T_vertex_id`, `T_labels`] + +:param vertex_to_id: See `VertexToID` function. + +:param gear: See `gears API ` and class `Gear`. + +:param next_edges: See `NextWeightedEdges` function. If None, provide + next_labeled_edges. + +:param next_labeled_edges: See `NextWeightedLabeledEdges` function. Only allowed + if next_edges equals None. If given, paths will record the given labels. diff --git a/tpl/src/nographs/_strategies/traversals/with_weights/method_start_from/code_init_distances_and_zero.py b/tpl/src/nographs/_strategies/traversals/with_weights/method_start_from/code_init_distances_and_zero.py new file mode 100644 index 0000000..bebb0e2 --- /dev/null +++ b/tpl/src/nographs/_strategies/traversals/with_weights/method_start_from/code_init_distances_and_zero.py @@ -0,0 +1,11 @@ +# At start, most of the distances from a vertex to a start vertex are not +# known. If accessed for comparison for possibly better distances, infinity +# is used, if no other value is given. Each start vertex has distance 0 +# from a start vertex (itself), if not defined otherwise. +zero = self._gear.zero() +self.distances = define_distances( + self._gear, + self._known_distances, + ((vertex_id, zero) for vertex, vertex_id in self._start_vertices_and_ids), + self._is_tree, +) diff --git a/tpl/src/nographs/_strategies/traversals/with_weights/method_start_from/code_start.py b/tpl/src/nographs/_strategies/traversals/with_weights/method_start_from/code_start.py new file mode 100644 index 0000000..1e21895 --- /dev/null +++ b/tpl/src/nographs/_strategies/traversals/with_weights/method_start_from/code_start.py @@ -0,0 +1,17 @@ +if not isinstance(type(self), type(Traversal)): + raise RuntimeError("Method start_from can only be called on a Traversal object.") + +self._start_from( + start_vertex, + start_vertices, + build_paths, + calculation_limit, + self._gear, +) + +# Explicitly list start vertices and their id. Needed several times. +self._start_vertices_and_ids = tuple( + iter_start_vertices_and_ids(self._start_vertices, self._vertex_to_id) +) + +# ----- Initialize method specific public bookkeeping ----- diff --git a/tpl/src/nographs/_strategies/traversals/with_weights/method_start_from/doc_std.rst b/tpl/src/nographs/_strategies/traversals/with_weights/method_start_from/doc_std.rst new file mode 100644 index 0000000..60ce390 --- /dev/null +++ b/tpl/src/nographs/_strategies/traversals/with_weights/method_start_from/doc_std.rst @@ -0,0 +1,11 @@ +:param start_vertex: The vertex the search should start at. If None, provide + start_vertices. + +:param start_vertices: The set of vertices the search should start + at. Only allowed if start_vertex equals None. + +:param build_paths: If true, build paths from start vertices for each reported + vertex, and an empty path for each start vertex. + +:param calculation_limit: If provided, maximal number of vertices to process + (read in) from your graph. If it is exceeded, a RuntimeError will be raised. diff --git a/tpl/src/nographs/_strategies/traversals/with_weights/method_start_from/signature_standard.py b/tpl/src/nographs/_strategies/traversals/with_weights/method_start_from/signature_standard.py new file mode 100644 index 0000000..210a2a9 --- /dev/null +++ b/tpl/src/nographs/_strategies/traversals/with_weights/method_start_from/signature_standard.py @@ -0,0 +1,5 @@ +start_vertex: Optional[T_vertex] = None, +*, +start_vertices: Optional[Iterable[T_vertex]] = None, +build_paths: bool = False, +calculation_limit: Optional[int] = None, diff --git a/tpl/src/nographs/_strategies/traversals/with_weights/method_traverse/code_start.py b/tpl/src/nographs/_strategies/traversals/with_weights/method_traverse/code_start.py new file mode 100644 index 0000000..92eac1d --- /dev/null +++ b/tpl/src/nographs/_strategies/traversals/with_weights/method_traverse/code_start.py @@ -0,0 +1,11 @@ +# ----- Prepare efficient environment for inner loop ----- +# Copy Traversal attributes into method scope (faster access) +labeled_edges = self._labeled_edges +maybe_vertex_to_id = ( + None if self._vertex_to_id == vertex_as_id else self._vertex_to_id +) # Case vertex_as_id: not apply; T_vertex_id > T_vertex +build_paths = self._build_paths +calculation_limit = self._calculation_limit +predecessors = self._predecessors +attributes = self._attributes +next_edges = self._next_edges diff --git a/tpl/src/nographs/_strategies/traversals/without_weights/!breadth_first.py b/tpl/src/nographs/_strategies/traversals/without_weights/!breadth_first.py new file mode 100644 index 0000000..1278d6f --- /dev/null +++ b/tpl/src/nographs/_strategies/traversals/without_weights/!breadth_first.py @@ -0,0 +1,209 @@ +from __future__ import annotations + +import copy +from typing import Optional, Any, Generic +from collections.abc import Iterable, Iterator, Generator + + +from nographs._types import ( + T_vertex, + T_vertex_id, + T_labels, + VertexToID, + vertex_as_id, +) +from nographs._gears import ( + GearDefault, + GearWithoutDistances, + VertexIdSet, +) +from nographs._gear_collections import ( + access_to_vertex_set, + access_to_vertex_mapping_expect_none, +) +from ...type_aliases import ( + NextVertices, + NextEdges, + NextLabeledEdges, +) +from ..traversal import ( + Traversal, + _start_from_needs_traversal_object, +) +from .traversal_without_weights import ( + _create_unified_next, + _TraversalWithoutWeightsWithVisited, +) + +"$$ import_from('$$/MTraversalWithoutWeights.py') $$" + + +class TraversalBreadthFirstFlex( + _TraversalWithoutWeightsWithVisited[T_vertex, T_vertex_id, T_labels] +): + """ + # $$ insert_from('$$/cls_traversal/doc_start.rst') + + **Algorithm:** Breadth First Search ("BFS"), non-recursive implementation. + Vertices are reported when they are "seen" (read from the graph) for the + first time. + + **Properties:** + Reports vertices in Breadth First order, i.e., + with ascending depth (edge count of the path with the fewest edges from a + start vertex). All computed paths are *shortest paths* , i.e., paths with + minimal number of edges from a start vertex to their end vertex. + + A vertex is considered visited when it has been reported or if it is a + start vertex. + + # $$ insert_from('$$/cls_traversal/doc_input.rst') + + **Search state:** When a vertex is *expanded* + (traversal calls next_vertices, next_edges or next_labeled_edges) + or *reported* (an iterator of the traversal returns it), + the traversal provides updated values for the attributes + *depth*, *paths*, and *visited*. + """ + + def __init__( + self, + # $$ MStrategyWithoutWeights.init_signature('TraversalBreadthFirstFlex') + ) -> None: + "$$ MStrategyWithoutWeights.init_code(search_depth_is_vertex_depth=True) $$" + self._report_depth_increase = False + + def start_from( + self, + # $$ insert_from('$$/method_start_from/signature.py') + _report_depth_increase: bool = False, # hidden parameter for internal use + ) -> TraversalBreadthFirstFlex[T_vertex, T_vertex_id, T_labels]: + """ + # $$ insert_from('$$/method_start_from/doc_start.rst') + # $$ insert_from('$$/method_start_from/doc_already_visited_std.txt') + # $$ insert_from('$$/method_start_from/doc_end.rst') + """ + "$$ insert_from('$$/method_start_from/code_start.py') $$" + self._report_depth_increase = _report_depth_increase + + super()._start() + return self + + def _traverse(self) -> Generator[T_vertex, None, Any]: + "$$ insert_from('$$/method_traverse/code_start_with_tree_and_visited.py') $$" + + # Copy Traversal-specific attributes into method scope (faster access) + report_depth_increase = self._report_depth_increase + + # ----- Initialize method specific bookkeeping ----- + + # Two lists used as FIFO queue with just two buckets + # (using a queue and counting down the size of current depth horizon is slower, + # and creating a new list instead of clear() is also slower) + to_expand = self._gear.sequence_of_vertices(self._start_vertices) + next_to_expand = self._gear.sequence_of_vertices(()) + + # During an ongoing expansion of some vertex we will already report the + # new found neighbors. For the former, the depth needs to remain the old + # one, while for the latter, it needs to be one higher. In order to avoid + # a cascade of +1 and -1 on the depth, we just use a copy of the traversal, + # that hangs by one in the depth, and give this to next_edge_or_vertices. + prev_traversal = copy.copy(self) # copy of self, for keeping previous depth + self.depth = 1 # used for reporting (prev_traversal starts at 0) + + # Get method references of specific bookkeeping (avoid attribute resolution) + to_expand_append = to_expand.append + next_to_expand_append = next_to_expand.append + + "$$ insert_from('$$/method_traverse/code_prepare_edges_loop.py') $$" + + # ----- Inner loop ----- + + while to_expand: + for vertex in to_expand: + "$$ MCalculationLimit.step() $$" + + for edge_or_vertex in next_edge_or_vertex(vertex, prev_traversal): + neighbor = edge_or_vertex[0] if edges_with_data else edge_or_vertex + + if not is_tree or build_paths: + "$$ MStrategy.vertex_to_id('neighbor', 'n_id') $$" + + # If not is_tree: Ignore neighbor if already seen, and + # else include its ID in visited set. + """$$ MVertexSet.if_visited_continue_else_add( + 'visited', 'n_id', 'not is_tree') + $$""" + + if build_paths: + """$$ MVertexMappingExpectNone.store_vertex_and_edge_data( + 'predecessors', 'attributes', + 'vertex', 'n_id', 'edge_or_vertex[-1]') + $$""" + + # Vertex has been seen, report it now + yield neighbor + # Needs to be expanded in the next round + next_to_expand_append(neighbor) + + if report_depth_increase and next_to_expand: + # We are not finished yet, because we found new vertices to expand, + # and we are about to increase the depth now, and it is demanded + # to report this situation by reporting the last vertex reported so far + # again. So we report it again. + yield next_to_expand[-1] + + # Update external views (reporting/expanding) on depth + self.depth += 1 + prev_traversal.depth += 1 + # Prepare state for next depth level of vertices + to_expand, next_to_expand, to_expand_append, next_to_expand_append = ( + next_to_expand, + to_expand, + next_to_expand_append, + to_expand_append, + ) + del next_to_expand[:] + + # Correct the depth to the search depth of last visited vertex. If + # start_vertices was given, and the argument was empty, the result will be -1. + # The documentation does not specify this behaviour, but it might be expected. + self.depth -= 2 + + def go_for_depth_range(self, start: int, stop: int) -> Iterator[T_vertex]: + """ + For a started traversal, return an iterator. During the traversal, + the iterator skips vertices as long as their depth is lower than *start*. + From then on, is reports the found vertices. It stops when the reached depth + is equal to or higher than *stop*. + + Note: The first vertex with a depth equal or higher than *stop* will be + consumed from the traversal, but will not be reported, so it is lost (compare + *itertools.takewhile*). + + :param start: Vertices lower than this are skipped. + :param stop: Reporting stops when reached depth is equal or higher than this. + """ + if not isinstance(type(self), type(Traversal)): + raise RuntimeError( + "Method go_for_depth_range can only be called " + + "on a Traversal object." + ) + + # In order to make the above check work, the following generator functionality + # needs to be encapsulated in a local function + def my_generator() -> Iterator[T_vertex]: + for v in self._generator: + if self.depth >= start: + if self.depth < stop: + yield v + break + for v in self._generator: + if self.depth >= stop: + break + yield v + + return my_generator() + + +"$$ MStrategyWithoutWeights.standard_for_flex('TraversalBreadthFirst') $$" diff --git a/tpl/src/nographs/_strategies/traversals/without_weights/!depth_first.py b/tpl/src/nographs/_strategies/traversals/without_weights/!depth_first.py new file mode 100644 index 0000000..69b4556 --- /dev/null +++ b/tpl/src/nographs/_strategies/traversals/without_weights/!depth_first.py @@ -0,0 +1,1034 @@ +from __future__ import annotations + +import array +from typing import Optional, Any, Generic +from collections.abc import Iterable, Generator +from enum import Flag, auto + +from nographs._types import ( + T_vertex, + T_vertex_id, + T_labels, + VertexToID, + vertex_as_id, +) +from nographs._gears import ( + GearDefault, + GearWithoutDistances, + VertexIdSet, +) +from nographs._gear_collections import ( + access_to_vertex_set, + access_to_vertex_mapping_expect_none, + access_to_vertex_mapping, +) +from ...type_aliases import ( + NextVertices, + NextEdges, + NextLabeledEdges, +) +from ...utils import ( + StrRepr, +) +from ..traversal import ( + Traversal, + _start_from_needs_traversal_object, +) +from .traversal_without_weights import ( + _create_unified_next, + _TraversalWithoutWeightsWithVisited, +) + +"$$ import_from('$$/MTraversalWithoutWeights.py') $$" +"$$ import_from('$$/../../MVertexMapping.py') $$" + + +class DFSEvent(Flag): + """ + An enumeration of the events that can trigger the report of a vertex / edge by + TraversalDepthFirst. + + Events reporting that a vertex is entered or left: + + - ENTERING_START: A start vertex has been entered and the traversal starts + there. + + - LEAVING_START: A start vertex has been left (the traversal may continue + with the next one). + + - ENTERING_SUCCESSOR: A vertex is entered, when an edge + that leads to it is followed. In mode *DFS_TREE*, only *DFS-tree edges* + are followed. + + - LEAVING_SUCCESSOR: A vertex is left, when an edge that leads to it + and has been followed, is now followed in reversed direction, during + backtracking from the edge. In mode *DFS_TREE*, only *DFS-tree edges* + are followed. + + Events reporting that a vertex (or an edge) has been detected but will not be + entered (resp. followed): + + - SKIPPING_START: A start vertex was about to be entered, as start of a + traversal from there, but it has already been visited as descendant of + another start vertex, and thus, it is skipped. + + - BACK_EDGE: An edge *(u, v)* is found, where v has already been entered, + but not left so far. In other words, *v* is on the trace (path that + leads to *u* within the tree). + + - FORWARD_EDGE: An edge *(u, v)* is found, where *v* has already been + left, and it had been entered after *u*. *(u, v)* is a shortcut + forwards in the tree branch from *u* to *v*, so to speak. + + - CROSS_EDGE: An edge *(u, v)* is found, where *v* has already been left, + and it had been entered before *u*. This means, in the DFS tree, + *u* and *v* do not have any ancestor or descendant relationship + between them. + + Events that combine other events as a group (*group-events*): + + - SOME_NON_TREE_EDGE: One of the events FORWARD_EDGE, BACK_EDGE, or CROSS_EDGE + occurred, but it has not been determined which of these events. + + - FORWARD_OR_CROSS_EDGE: One of the events FORWARD_EDGE or CROSS_EDGE + occurred, but it has not been determined which of these events. + + + Aliases for sets of events: + + - NONE = 0 + + - ENTERING = ENTERING_START | ENTERING_SUCCESSOR + - LEAVING = LEAVING_START | LEAVING_SUCCESSOR + + - IN_OUT_START = ENTERING_START | LEAVING_START + - IN_OUT_SUCCESSOR = ENTERING_SUCCESSOR | LEAVING_SUCCESSOR + - IN_OUT = IN_OUT_START | IN_OUT_SUCCESSOR + + - NON_TREE_EDGES = FORWARD_EDGE | BACK_EDGE | CROSS_EDGE + - EDGES = ENTERING_SUCCESSOR | NON_TREE_EDGES + + - ALL = IN_OUT | SKIPPING_START | NON_TREE_EDGES + """ + + ENTERING_START = auto() + LEAVING_START = auto() + + ENTERING_SUCCESSOR = auto() + LEAVING_SUCCESSOR = auto() + + SKIPPING_START = auto() + BACK_EDGE = auto() + FORWARD_EDGE = auto() + CROSS_EDGE = auto() + + SOME_NON_TREE_EDGE = auto() + FORWARD_OR_CROSS_EDGE = auto() + + NONE = 0 + + ENTERING = ENTERING_START | ENTERING_SUCCESSOR + LEAVING = LEAVING_START | LEAVING_SUCCESSOR + + IN_OUT_START = ENTERING_START | LEAVING_START + IN_OUT_SUCCESSOR = ENTERING_SUCCESSOR | LEAVING_SUCCESSOR + IN_OUT = IN_OUT_START | IN_OUT_SUCCESSOR + + NON_TREE_EDGES = FORWARD_EDGE | BACK_EDGE | CROSS_EDGE + EDGES = ENTERING_SUCCESSOR | NON_TREE_EDGES + + ALL = IN_OUT | SKIPPING_START | NON_TREE_EDGES + + +class DFSMode(Flag): + """ + An enumeration of the traversing mode to be used by TraversalDepthFirst. + + The modes are: + + - DFS_TREE: The traversal follows the edges of the DFS tree. If demanded, + non-tree edges are reported, but not followed. Vertices are only + visited once. + + - ALL_PATHS: A simple path is a path that does not contain a vertex twice. + In this mode, the traversal follows all edges, also edges leading to + vertices that have already been visited. But edges to vertices, that are + already on the trace (current path from a start vertex to the current + vertex) are ignored. For example, this can be used to search in the set + of all possible simple paths from some edges to some others. + + - ALL_WALKS: A walk is a sequence of nodes in which each adjacent pair of + nodes in the sequence is adjacent in the graph. + A walk can contain the same vertex or edge more than once. + In this more, the traversal follows all edges, also edges leading to + vertices that have already been followed as part of the trace (the + current walk from a start vertex to the current vertex). + """ + + DFS_TREE = auto() + ALL_PATHS = auto() + ALL_WALKS = auto() + + +class TraversalDepthFirstFlex( + _TraversalWithoutWeightsWithVisited[T_vertex, T_vertex_id, T_labels] +): + """ + # $$ insert_from('$$/cls_traversal/doc_start.rst') + + **Algorithm:** Depth First Search ("DFS"), non-recursive implementation. + By default, a vertex is reported when its expansion starts (its + successors are about to be read from the graph). + + **Properties**: + Visits and expands unvisited vertices in depth first order, i.e., + the graphs is explored as far as possible along each branch before + backtracking. + Starts at some unvisited start vertex, and after an exhaustive traversal + from there, continues with another start vertex that has not been visited + so far. + + By default, it reports a vertex when it visits it. + + # $$ insert_from('$$/cls_traversal/doc_input.rst') + + **Search state:** When a vertex is *expanded* + (traversal calls next_vertices, next_edges or next_labeled_edges) + or *reported* (an iterator of the traversal returns it), + the traversal provides updated values for the attributes + *depth*, *paths*, *visited*, *event*, *trace*, *trace_labels*, + *on_trace*, and *index*. + """ + + def __init__( + self, + # $$ MStrategyWithoutWeights.init_signature('TraversalDepthFirstFlex') + ) -> None: + "$$ MStrategyWithoutWeights.init_code(depth_computation_optional = True) $$" + # The following values are not used. They are initialized during start_from. + self._report = DFSEvent.NONE + self._mode = DFSMode.DFS_TREE + self._compute_depth = False + self._compute_trace = False + self._compute_on_trace = False + self._compute_index = False + + self.event = DFSEvent.NONE + """ Event that happened when a vertex is reported """ + self.trace = self._gear.sequence_of_vertices([]) + """ Sequence of the vertices on the current path from a start vertex to the + current vertex. See option *compute_trace*. + When a back edge, cross edge, or a forward edge is reported, the edge is + temporarily appended to the trace to make it visible there, + although such an edge is ignored otherwise (e.g., the traversal does not + follow the edge, traversal.depth is not updated, and the vertex it leads + to is not taken to *on_trace*).""" + self.trace_labels = self._gear.sequence_of_edge_labels([]) + """ Sequence of the edge attributes of the edges on the current path + (the first edge goes from a start vertex to a successor). + See attribute *trace* and option *compute_trace*.""" + self.on_trace = self._gear.vertex_id_set([]) + """ Set of the vertices on the current path from a start vertex to the + current vertex. See option *compute_on_trace*. When a cross edge or a forward + edge is reported, the vertex it leads to will not be added to *on_trace*, + unlike *trace* (see there). """ + self.index = self._gear.vertex_id_to_number_mapping([]) + """ Mapping that numbers vertices in pre-order, i.e., the vertex gets its + number when it is entered. The vertices are numbered starting with *1*. + See option *compute_index*. + """ + + def start_from( + self, + # $$ insert_from('$$/method_start_from/signature.py') + report: DFSEvent = DFSEvent.ENTERING_SUCCESSOR, + mode: DFSMode = DFSMode.DFS_TREE, + compute_depth: bool = False, + compute_trace: bool = False, + compute_on_trace: bool = False, + compute_index: bool = False, + ) -> TraversalDepthFirstFlex[T_vertex, T_vertex_id, T_labels]: + """ + # $$ insert_from('$$/method_start_from/doc_start.rst') + # $$ insert_from('$$/method_start_from/doc_already_visited_std.txt') + :param report: See `DFSEvent`. + When one of the chosen events occurs, the vertex is reported. + + The group-events cannot be combined with the events contained in the + group (see `DFSEvent`). + + If events other than ENTERING_SUCCESSOR and ENTERING_START + are required, option *compute_trace* (see below) will automatically be + used. + + If group-event FORWARD_OR_CROSS_EDGE is required, and the graph + is no tree (is_tree == False), option *compute_on_trace* (see below) + will automatically be set. + + If events from NON_TREE_EDGES are required, and the graph + is no tree (is_tree == False), the options + *compute_on_trace* and *compute_index* (see below) + will automatically be set. + + :param mode: + See `DFSMode`. The mode the search operates in. + + Mode ALL_PATHS cannot be combined with the reporting of events + FORWARD_EDGE and CROSS_EDGE, and event-groups containing them, + since these events are only defined for DFS-trees. + In mode ALL_PATHS, option *compute_on_trace* (see below) + will automatically be set. + + Mode ALL_WALKS cannot be + combined with reporting non-tree edges, neither + alone (events from NON_TREE_EDGES) + nor in group-events + (events SOME_NON_TREE_EDGE or FORWARD_OR_CROSS_EDGE), + since forward and cross edges are only defined for DFS-trees, + and back edges only for DFS-trees and for paths. + The mode cannot be used for trees + (parameter *is_tree* when creating the traversal), + and `visited` is not computed. + + # $$ insert_from('$$/method_start_from/doc_compute_depth.txt') + :param compute_index: If True, the attribute *index* is updated + during the traversal, and option *compute_trace* + (see below) will automatically be used. *compute_index* is not + compatible with parameter *already_visited*. + + :param compute_on_trace: If True, attribute `on_trace` is updated + during the traversal, and option *compute_trace* will automatically + be set. The computation of set `on_trace` cannot be combined + with mode ALL_WALKS. + + :param compute_trace: If True, attribute `trace` is updated during the + traversal. + # $$ insert_from('$$/method_start_from/doc_end.rst') + + .. versionchanged:: 3.4 + + Start vertices are evaluated successively. + Parameters *report*, *mode*, *compute_trace*, *compute_on_trace*, + and *compute_index* added. + """ + _start_from_needs_traversal_object(self) + self._start_without_weights_with_visited( + start_vertex, + start_vertices, + build_paths, + calculation_limit, + already_visited, + empty_path_for_start_vertices=False, + visited_for_start_vertices=False, + ) + # Set the externally visible depth to the sensible initial value 0. + # But if depth is not to be computed, use value -1 instead. + self.depth = 0 if compute_depth else -1 + + # Derive further options (and possible incompatibilities) from given options + if DFSEvent.SOME_NON_TREE_EDGE in report and (DFSEvent.NON_TREE_EDGES & report): + raise RuntimeError( + "Reporting of non-tree edges as a group and as individual edge " + "type cannot be combined." + ) + if DFSEvent.FORWARD_OR_CROSS_EDGE in report and ( + (DFSEvent.FORWARD_EDGE | DFSEvent.CROSS_EDGE) & report + ): + raise RuntimeError( + "Reporting of forward or cross edges as a group and as individual edge " + "type cannot be combined." + ) + if not self._is_tree: + # In a tree, there cannot be non-tree edges. So, we do not need + # bookkeeping to detect such edges. + # If we are not in a tree, we need some special bookkeeping. + if ( + report & (DFSEvent.NON_TREE_EDGES | DFSEvent.FORWARD_OR_CROSS_EDGE) + or mode == DFSMode.ALL_PATHS + ): + # For reporting the concrete type of non-tree edges, or + # FORWARD_OR_CROSS_EDGE, we need the *on_trace* set. + # In case of back edges, we need it to detect them. + # In case of forward and cross edges or the group of either a forward + # or a cross edge, we need it to exclude the case of a back edge. + compute_on_trace = True + if report & (DFSEvent.FORWARD_EDGE | DFSEvent.CROSS_EDGE): + # Here, we need a vertex index to distinguish between the + # tho cases. + compute_index = True + if ( + report not in (DFSEvent.ENTERING_SUCCESSOR | DFSEvent.ENTERING_START) + or compute_index + or compute_on_trace + ): + # Only the algorithms that computes a trace can report other + # events than entering of normal or start vertices and can compute + # vertex indexes and the on_trace set + compute_trace = True + + # Prevent illegal option combinations + if report & ( + DFSEvent.NON_TREE_EDGES + | DFSEvent.SOME_NON_TREE_EDGE + | DFSEvent.FORWARD_OR_CROSS_EDGE + ) and (self._is_tree or mode in DFSMode.ALL_WALKS): + raise RuntimeError( + "The events BACK_EDGE, FORWARD_EDGE, and CROSS_EDGE, " + "and groups containing them, " + "cannot be computed for trees and for traversals in mode " + "*ALL_WALKS*." + ) + if compute_on_trace and (self._is_tree or mode == DFSMode.ALL_WALKS): + raise RuntimeError( + "Computation of the on-trace is not allowed for trees and for " + "traversals in mode *ALL_WALKS*." + ) + if ( + report + & ( + DFSEvent.FORWARD_EDGE + | DFSEvent.CROSS_EDGE + | DFSEvent.SOME_NON_TREE_EDGE + | DFSEvent.FORWARD_OR_CROSS_EDGE + ) + and mode == DFSMode.ALL_PATHS + ): + raise RuntimeError( + "The events FORWARD_EDGE and CROSS_EDGE, " + "and groups containing them, " + "cannot be computed for traversals in mode *ALL_PATHS*." + ) + if build_paths and mode == DFSMode.ALL_WALKS: + raise RuntimeError( + "Paths cannot be computed in mode *ALL_WALKS*, because " + "walks can be cyclic." + ) + if compute_index and already_visited is not None: + raise RuntimeError( + "Parameter *already_visited* not allowed when vertex indexes " + "are demanded." + ) + + self._report = report + self._mode = mode + self._compute_depth = compute_depth + self._compute_trace = compute_trace + self._compute_on_trace = compute_on_trace + self._compute_index = compute_index + self.event = DFSEvent.NONE + self.trace = self._gear.sequence_of_vertices([]) + self.on_trace = self._gear.vertex_id_set([]) + self.trace_labels = self._gear.sequence_of_edge_labels([]) + self.index = self._gear.vertex_id_to_number_mapping([]) + + super()._start() + return self + + def _traverse(self) -> Generator[T_vertex, None, Any]: + # We provide two different implementations: the first with many features, + # and the second optimized for speed and memory for a typical configuration + # of options. + if self._compute_trace or self._mode != DFSMode.DFS_TREE: + return self._traverse_with_trace() + else: + return self._traverse_without_trace() + + def _traverse_with_trace(self) -> Generator[T_vertex, None, Any]: + "$$ insert_from('$$/method_traverse/code_start_with_tree_and_visited.py') $$" + + # Copy Traversal-specific attributes into method scope (faster access) + report = self._report + mode = self._mode + compute_depth = self._compute_depth + compute_on_trace = self._compute_on_trace + compute_index = self._compute_index + trace = self.trace + on_trace = self.on_trace + trace_labels = self.trace_labels + index = self.index + + # Create individual flags for events that are to be reported + # (Avoids attribute resolution both for checking if an event has + # to be reported and for the value that is then to report) + event_entering = DFSEvent.ENTERING_SUCCESSOR + event_entering_start = DFSEvent.ENTERING_START + report_leaving = DFSEvent.LEAVING_SUCCESSOR & report + report_leaving_start = DFSEvent.LEAVING_START & report + report_skipping_start = DFSEvent.SKIPPING_START & report + report_non_tree_edges = DFSEvent.NON_TREE_EDGES & report + report_forward_edge = DFSEvent.FORWARD_EDGE & report + report_back_edge = DFSEvent.BACK_EDGE & report + report_cross_edge = DFSEvent.CROSS_EDGE & report + report_some_non_tree_edge = DFSEvent.SOME_NON_TREE_EDGE & report + report_forward_or_cross_edge = DFSEvent.FORWARD_OR_CROSS_EDGE & report + report_none = DFSEvent.NONE + # The same for the mode + mode_dfs_tree = mode & DFSMode.DFS_TREE + # mode_simple_paths = mode & DFSMode.ALL_PATHS + mode_walks = mode & DFSMode.ALL_WALKS + + # ----- Initialize method specific bookkeeping ----- + + if compute_depth: + # Since a start vertex is the first that is put on the trace, we have to + # start one lower + self.depth -= 1 + + # Vertices to enter or leave + to_visit = self._gear.sequence_of_vertices([]) + to_visit_append = to_visit.append + to_visit_pop = to_visit.pop + # For non-start vertices in to_visit: edge attributes of edge to them + to_visit_labels = self._gear.sequence_of_edge_labels([]) + to_visit_labels_append = to_visit_labels.append + to_visit_labels_pop = to_visit_labels.pop + # Sequence of flag bytes (store in a Q array) marking the vertices in + # to_visit that we want to leave by 1 and the vertices to enter by 0. + # Initially, store a zero flag for each start vertex. + to_leave_markers = array.array("B") + to_leave_markers_append = to_leave_markers.append + + # Get references of the methods of the gear objects this traversal uses + # (avoid attribute resolution) + trace_append = trace.append + trace_pop = trace.pop + on_trace_add = on_trace.add + trace_labels_append = trace_labels.append + trace_labels_pop = trace_labels.pop + "$$ MVertexSet.access(name='on_trace', ops=['add', 'discard']) $$" + "$$ MVertexSet.combine_access('visited', 'on_trace', 'set') $$" + + # Start times of vertices (the collection implicitly default to 0) + "$$ MVertexMapping.access('index') $$" + + # We start the time at number 1, because vertices without time have time + # 0, and we want to distinguish the two cases. + time = 1 + + "$$ insert_from('$$/method_traverse/code_prepare_edges_loop.py') $$" + labels: T_labels + + for start_vertex in self._start_vertices: + # A start vertex needs to be visited and entered, and it has an empty path + to_visit_append(start_vertex) + to_leave_markers_append(False) + + # ----- Inner loop ----- + + while True: + # Leave vertices in the trace (stack) that we already fully processed + # (backtracking along edges) and find the vertex to enter next. + # If there is none, we are finished with processing the current + # start vertex. + + no_leaving_reported_so_far = True + # (noinspection necessary due to bug PY-9479, also below...) + # noinspection PyUnboundLocalVariable + while to_leave_markers: + marker = to_leave_markers.pop() + if not marker: + # Not a leave marker, but an enter marker. Break loop + # of handling leave markers. + break + # We got marker "leave a vertex" and leave one vertex + vertex = trace[-1] + # Report leaving, if demanded, and update the trace + if to_leave_markers: + # We are leaving a vertex that is not the only one on the trace + # (Note: We cannot compare vertex != start_vertex here, because + # a start vertex can re-occur as target of a back edge, and + # then, the trace is not empty!) + if report_leaving: + if no_leaving_reported_so_far: + self.event = report_leaving + no_leaving_reported_so_far = False + yield vertex + if labeled_edges: + trace_labels_pop() + else: + # We are leaving the only vertex on the trace + if report_leaving_start: + self.event = report_leaving_start + yield vertex + # Pop vertex from trace + _ = trace_pop() + # Remove vertex from on_trace set + if compute_on_trace: + "$$ MStrategy.vertex_to_id('vertex', 'r_id') $$" + "$$ MVertexSet.remove('on_trace', 'r_id', 'set') $$" + # Update depth + if compute_depth: + self.depth -= 1 + else: + # Last marker has been read. We are done with the start_vertex and + # all its successors. Break loop of handling markers and vertices + # to visit. + break + + # Enter marker found. Follow an edge in its forward direction. + # The last added vertex is the first to (possibly) enter (depth-first) + # print(">>", to_visit, to_visit_labels) + vertex = to_visit_pop() + if labeled_edges and to_visit: + # Further vertices to visit means vertex is here not in role of + # a start vertex. Thus, we followed an edge. Thus, if edges + # are labeled, we have a label. And get it. + # (Note: We cannot compare vertex != start_vertex here, because + # a start vertex can re-occur as target of a back edge, and + # then, we need to get the labels!) + labels = to_visit_labels_pop() + # print(">>>", vertex, labels) + "$$ MStrategy.vertex_to_id('vertex', 'v_id') $$" + + # If the graph is no tree: We might come to a vertex that we have + # already visited and need to check and handle this; and we have to + # update the visited set. + # In mode ALL_WALKS, we ignore all this + if not is_tree and mode != mode_walks: + + event = report_none + + # Find out if vertex has already been visited and entering + # it again is forbidden. + # In mode DFS_TREE, "visited" means here that the vertex is in + # set visited (because it has already been visited as descendant + # of another vertex, or because it is given as start vertex twice), + # and in mode ALL_PATHS that it already is on_trace. + re_visit = False + if mode == mode_dfs_tree: + """$$ + MVertexSet.compile_access('v_id', '', 'set', '''\ + $visited.if_contains_vertex_id_prepare_remove_and_elseadd: + re_visit = True + $visited.else_prepared_add_endif +''')$$""" + if re_visit and not trace: + # We try to enter a start vertex when the trace is + # empty, but is has already been visited (can only + # happen in mode DFS_TREE.) + if report_skipping_start: + self.event = report_skipping_start + trace_append(vertex) + yield vertex + trace_pop() + continue + + else: # mode ALL_PATHS, since ALL_WALKS has been excluded above + """$$ + MVertexSet.compile_access('v_id', '', 'set', '''\ + $on_trace.if_contains_vertex_id_prepare_remove_and_elseadd: + re_visit = True + event = report_back_edge + $on_trace.else_prepared_add_endif +''')$$""" + if re_visit: + # Report re-visit of non-start vertex, and ignore the + # vertex (continue). + + if report_some_non_tree_edge: + # We need to report all kinds of non-tree edges as a group + event = report_some_non_tree_edge + + elif ( + report_non_tree_edges + | report_forward_or_cross_edge + | report_back_edge + | report_forward_edge + | report_cross_edge + ): + # We need to report a more concrete type that just + # some_non_tree_edge + + # Detect back edge, if not already done + if report_back_edge and event != report_back_edge: + """$$ + # Needs to be done when index of previous access to + # v_id is still valid. + # We know, that v_id has already been in on_trace, + # because it has already been visited, but we do + # now know, if it is currently contained in the set. + # Thus, we use the respective version of endif. + MVertexSet.compile_access('v_id', '', 'set', '''\ + $on_trace.if_contains_vertex_id: + event = report_back_edge + $on_trace.endif_with_vertex_included_in_past +''', already_indexed=True)$$""" + + # If we have no back edge, distinguish between forward and + # cross edge + if event == report_none and ( + report_forward_edge | report_cross_edge != report_none + ): + parent = trace[-1] + "$$ MStrategy.vertex_to_id('parent', 'p_id') $$" + vertex_index = """$$ + MVertexMapping.get_included('index', 'v_id') $$""" + parent_index = """$$ + MVertexMapping.get_included('index', 'p_id') $$""" + event = ( + report_forward_edge + if parent_index < vertex_index + else report_cross_edge + ) + else: + continue + + if event and event in report: + # Report the edge. Append it temporarily to the end of + # the trace and remove it again. + self.event = event + trace_append(vertex) + if labeled_edges: + trace_labels_append(labels) + yield vertex + trace_pop() + if labeled_edges: + trace_labels_pop() + + continue + + # The vertex has not been visited before, and we are still + # in case "not is_tree and mode != mode_walks" + + if compute_on_trace: + # Add to trace set, if not already done + """$$ + # This macro call needs to be used directly after the one that + # accessed *visited* in order to re-use the correct index. + # And only if this other macro call has not been skipped + # because is_tree. + MVertexSet.compile_access('v_id', '', 'set', '''\ + $on_trace.add_vertex_id +''', already_indexed = True)$$""" + + # It is allowed to visit the vertex, so we visit it now. + # (The trace has already been maintained, if necessary.) + + if compute_index: + "$$ MVertexMapping.set('index', 'v_id', 'time') $$" + time += 1 + + if build_paths: + if trace: + # We are not visiting a start vertex. Store edge to it. + """$$ MVertexMappingExpectNone.store_vertex_and_edge_data( + 'predecessors', 'attributes', + 'trace[-1]', 'v_id', 'labels') + $$""" + else: + # We are visiting a start vertex. Store empty path for it. + "$$ MStrategy.vertex_to_id('start_vertex', 'vs_id')$$" + """$$ MVertexMappingExpectNone.store_vertex_if_empty( + 'predecessors', 'start_vertex', 'vs_id') $$""" + + # Store the marker True: When it is reached later on, we will know + # that we have to leave the vertex again. + # noinspection PyUnboundLocalVariable + to_leave_markers_append(True) + + if compute_depth: + self.depth += 1 + + # self.event = event = event_entering if trace else event_entering_start + self.event = event = ( + event_entering_start if vertex == start_vertex else event_entering + ) + + if labeled_edges and trace: + trace_labels_append(labels) + trace_append(vertex) + + # Report that we enter a vertex (all state attributes have to be + # updated before) + if report & event: + try: + yield vertex + except StopIteration: + # We confirm the skip signal and skip the expansion + yield vertex + continue + + "$$ MCalculationLimit.step() $$" + + for edge_or_vertex in next_edge_or_vertex(vertex, self): + neighbor = edge_or_vertex[0] if edges_with_data else edge_or_vertex + + # Needs to be visited, in stack order + to_visit_append(neighbor) + if labeled_edges: + # Proof for correctness of the type hole: + # self._labeled_edges -> next_edges (a NextWeightedEdges) is + # a NextWeightedLabeledEdges -> {edge_data_expr} is a T_labels + labels = edge_or_vertex[-1] # type: ignore[assignment] + to_visit_labels_append(labels) + + # Store marker False: when it is reached later on, we know + # that we have to enter the vertex now + to_leave_markers_append(False) + + def _traverse_without_trace(self) -> Generator[T_vertex, None, Any]: + """This implementation does not maintain the trace. + If a vertex is visited, there is no parent information, and if + a vertex is left, the vertex is now known. + Thus, it can only report ENTERING_SUCCESSOR and ENTERING_START, + cannot maintain the on-trace set, and cannot run in mode + ALL_PATHS. + Additionally, it does not offer vertex indices and mode ALL_WALKS, + but this could probably be added if necessary.""" + + "$$ insert_from('$$/method_traverse/code_start_with_tree_and_visited.py') $$" + visited = self.visited + "$$ MVertexSet.access(name='visited') $$" + + # Copy Traversal-specific attributes into method scope (faster access) + compute_depth = self._compute_depth + report = self._report + + # Copy Traversal-specific constants into method scope (faster access) + event_entering = DFSEvent.ENTERING_SUCCESSOR + event_entering_start = DFSEvent.ENTERING_START + + # ----- Initialize method specific bookkeeping ----- + + depth = -1 # The inner loop starts with incrementing, so, we pre-decrement + if not compute_depth: + self.depth = depth # In this case, we leave the -1 the whole time + + # vertices to enter or leave + to_visit = self._gear.sequence_of_vertices([]) + to_visit_append = to_visit.append + to_visit_pop = to_visit.pop + + if compute_depth: + # Sequence of flag bytes (store in a Q array) marking the vertices + # to leave by 1 and the vertices to enter by 0. + to_leave_markers = array.array("B") + to_leave_markers_pop = to_leave_markers.pop + to_leave_markers_append = to_leave_markers.append + + "$$ insert_from('$$/method_traverse/code_prepare_edges_loop.py') $$" + + for start_vertex in self._start_vertices: + to_visit_append(start_vertex) + if compute_depth: + to_leave_markers_append(False) + if build_paths: + # If the start vertex is not already visited, store empty path + # for it + "$$ MStrategy.vertex_to_id('start_vertex', 'sv_id')$$" + """$$ MVertexMappingExpectNone.store_vertex_if_empty( + 'predecessors', 'start_vertex', 'sv_id') $$""" + + # ----- Inner loop ----- + while True: + if compute_depth: + # Update depth w.r.t. all vertices we are leaving before afterwards, + # we enter the next one. + # This also done for the start vertex, before exiting the + # inner loop, in order to reset the sequence to_leave_markers + # (Instead of this, we could use "del s[:]" above, but this + # requires the MutableSequence to support slice objects as key for + # __delitem__ - what is often given, but not guaranteed) + # (noinspection necessary due to bug PY-9479, also below...) + # noinspection PyUnboundLocalVariable + while to_leave_markers and to_leave_markers_pop(): + # We decrement the depth for each vertex we are "leaving". + depth -= 1 + + if not to_visit: + # No vertices to visit are left: The start vertex and its + # descendants are processed. We can leave the loop. + break + + # Next vertex to enter (except it is already visited) + vertex = to_visit_pop() + if not is_tree: + # Ignore vertex if already visited, and + # else include its ID in visited set. + "$$ MStrategy.vertex_to_id('vertex', 'v_id')$$" + """$$ MVertexSet.if_visited_continue_else_add( + 'visited', 'v_id', '') $$""" + + # We will now enter the vertex + if compute_depth: + depth += 1 + self.depth = depth + # Store marker True: when reached, we are leaving a vertex + # noinspection PyUnboundLocalVariable + to_leave_markers_append(True) + + if vertex == start_vertex: + # In this variant (!) of DFS, below, only neighbors that are + # not visited so far are taken to to_visited. Since start_vertex + # is visited immediately in its role as start vertex, this means, + # it cannot occur again in the role of a neighbor. So, here, we + # know, that we are in the case event_entering_start. + # Set event, both for a possible report here and for + # expanding the vertex + self.event = event_entering_start + if report & event_entering_start: + try: + yield vertex + except StopIteration: + # We confirm the skip signal and skip the expansion. + # Since this also skips resetting the event type, + # we need to do it here before continuing. + yield vertex + self.event = event_entering + continue + + else: + if report & event_entering: + # The event has already been reset to report_entering + # (see below) + try: + yield vertex + except StopIteration: + # We confirm the skip signal and skip the expansion. + yield vertex + continue + + "$$ MCalculationLimit.step() $$" + + for edge_or_vertex in next_edge_or_vertex(vertex, self): + neighbor = edge_or_vertex[0] if edges_with_data else edge_or_vertex + + if not is_tree or build_paths: + "$$ MStrategy.vertex_to_id('neighbor', 'n_id') $$" + + # Ignore neighbor if already visited/expanded, i.e., not put + # it onto the stack to_visit. + # If we do not build paths, this is just a variant of the + # algorithms: The results do not change, since visited + # vertices will not be visited again. + # But in case that we create paths, it is necessary: We like + # to store the predecessor of the neighbor directly here, + # when we first see the neighbor and have the predecessor + # still present. And we need to make sure not to overwrite + # the real predecessor of a vertex that has been already + # expanded. If the neighbor has not been visited, it is + # safe (and necessary) to overwrite a pre-existing + # predecessor of neighbor because a later found edge + # (predecessor, neighbor) will be evaluated first (to_visit + # is a stack) and so, this predecessor need to "win". + # (If-nesting optimized for first case) + """$$ MVertexSet.if_visited_continue( + 'visited', 'n_id', '') + $$""" + + if build_paths: + """$$ + MVertexMappingExpectNone.store_vertex_and_edge_data( + 'predecessors', 'attributes', + 'vertex', 'n_id', 'edge_or_vertex[-1]') + $$""" + + # Needs to be visited, in stack order + to_visit_append(neighbor) + + if compute_depth: + # Store marker False: when reached, we are entering a vertex + to_leave_markers_append(False) + + if vertex == start_vertex: + # We directly reset the event to report_entering after having + # processed the start vertex, because this is less expensive + # than doing it every time re report a non-start vertex. + # And we do it only if necessary, because the *if* is less + # expensive than the attribute access. + self.event = event_entering + + # After the traversal, set depth to something that the user can understand, + # here: -1 is the value given if no depth computation is demanded, -1 is the + # initial value before the first vertex has been entered, and -1 + # is the depth after having backtracked from a start vertex (that is at + # depth 0). The documentation does not specify a value here. + self.depth = -1 + + def _improve_state( + self, state: dict[str, Any], vertices: Optional[Iterable[T_vertex]] = None + ) -> None: + + # For events: replace repr() text by str() text, because state as optimized + # for readability + if state["event"]: + state["event"] = str(state["event"]) + + # If ony of these attributes is not computed, do not show it as part of the + # state + for attribute in ["event", "trace", "on_trace", "trace_labels"]: + if not state[attribute]: + del state[attribute] + + # Assignments in times are only valid for reported vertices. Thus, + # we need to convert only keys/values for requested vertices to a string, + # not the whole MutableMapping. So, we special case this attribute here. + vertex_to_id = self._vertex_to_id + for times_key, times_collection in [ + ("index", self.index), + ]: + del state[times_key] + # List content for the given vertices, but leave out key without values + # or default value 0. + # (Explicitly check "v_id in collection", because simply accessing + # always returns (and for defaultdict: also set) the default value) + if vertices is not None: + # content = [ + # (v_id, timestamp) + # for vertex in vertices + # if (timestamp := times_collection[v_id := vertex_to_id(vertex)]) + # ] + content = [ + (v_id, time) + for vertex in vertices + if (v_id := vertex_to_id(vertex)) in times_collection + and (time := times_collection[v_id]) != 0 + ] + if content: + state[times_key] = StrRepr.from_iterable(content) + + super()._improve_state(state, vertices) + + def __iter__( + self, + ) -> Generator[ + T_vertex, None, None + ]: # Type alias needed due to a sphinx limitation + """ + Like `nographs.Traversal.__iter__`, but return a generator + instead of an interator. + + If *StopIteration()* is thrown into the generator: + + - When a vertex has been entered (events + *DFSEvent.ENTERING_START* or *DFSEvent.ENTERING_SUCCESSOR* is reported), + do not expand the vertex and reported it again as a confirmation. + - In any other situation, raise a *RuntimeError* (according to *PEP 497*, + see https://peps.python.org/pep-0479). + + .. versionchanged:: 3.4 + + Now returns a generator instead of just an iterator, and + a thrown *StopIteration* is handled, see above. + """ + if not isinstance(type(self), type(Traversal)): + raise RuntimeError("Method go can only be called on a Traversal object.") + return self._generator + + def skip_expanding_entered_vertex(self) -> None: + """If called when a vertex has been entered (events + *DFSEvent.ENTERING_START* or *DFSEvent.ENTERING_SUCCESSOR*), + skip the expansion of this vertex. + + If called when another event happened, raise a *RuntimeError*. + + (The method simply throws a *StopIteration* at *traversal.__iter__()*.) + + .. versionchanged:: 3.4 + + Method added. + """ + self._generator.throw(StopIteration()) + + +"$$ MStrategyWithoutWeights.standard_for_flex('TraversalDepthFirst') $$" diff --git a/tpl/src/nographs/_strategies/traversals/without_weights/!neighbors_then_depth.py b/tpl/src/nographs/_strategies/traversals/without_weights/!neighbors_then_depth.py new file mode 100644 index 0000000..a3f8503 --- /dev/null +++ b/tpl/src/nographs/_strategies/traversals/without_weights/!neighbors_then_depth.py @@ -0,0 +1,168 @@ +from __future__ import annotations + +import array +import copy +import itertools +from typing import Optional, Any, Generic +from collections.abc import Iterable, Generator + +from nographs._types import ( + T_vertex, + T_vertex_id, + T_labels, + VertexToID, + vertex_as_id, +) +from nographs._gears import ( + GearDefault, + GearWithoutDistances, + VertexIdSet, +) +from nographs._gear_collections import ( + access_to_vertex_set, + access_to_vertex_mapping_expect_none, +) +from ...type_aliases import ( + NextVertices, + NextEdges, + NextLabeledEdges, +) +from ..traversal import ( + _start_from_needs_traversal_object, +) +from .traversal_without_weights import ( + _create_unified_next, + _TraversalWithoutWeightsWithVisited, +) + +"$$ import_from('$$/MTraversalWithoutWeights.py') $$" + + +class TraversalNeighborsThenDepthFlex( + _TraversalWithoutWeightsWithVisited[T_vertex, T_vertex_id, T_labels] +): + """ + # $$ insert_from('$$/cls_traversal/doc_start.rst') + + **Algorithm:** Variant of the Depth First Search ("DFS"), + non-recursive implementation. + Vertices are reported when they are "seen" (read from the graph) for the + first time - thus not in DFS order! + + **Properties:** + The graphs is explored as far as possible along each branch before + backtracking, but in contrast to a Depth First Search, the algorithm + first reports all successors of the current vertex and then goes deeper. + A vertex is considered visited when it has been reported or if it is a + start vertex. + + # $$ insert_from('$$/cls_traversal/doc_input.rst') + + **Search state:** When a vertex is *expanded* + (traversal calls next_vertices, next_edges or next_labeled_edges) + or *reported* (an iterator of the traversal returns it), + the traversal provides updated values for the attributes + *depth*, *paths*, and *visited*. + """ + + def __init__( + self, + # $$ MStrategyWithoutWeights.init_signature('TraversalNeighborsThenDepthFlex') + ) -> None: + "$$ MStrategyWithoutWeights.init_code(depth_computation_optional = True) $$" + self._compute_depth = False # value not used, initialized during traversal + + def start_from( + self, + # $$ insert_from('$$/method_start_from/signature.py') + compute_depth: bool = False, + ) -> TraversalNeighborsThenDepthFlex[T_vertex, T_vertex_id, T_labels]: + """ + # $$ insert_from('$$/method_start_from/doc_start.rst') + # $$ insert_from('$$/method_start_from/doc_already_visited_std.txt') + # $$ insert_from('$$/method_start_from/doc_compute_depth.txt') + # $$ insert_from('$$/method_start_from/doc_end.rst') + """ + "$$ insert_from('$$/method_start_from/code_start.py') $$" + self._compute_depth = compute_depth + # Set the externally visible depth to the sensible initial value 0. + # But if depth is not to be computed, use value -1 instead. + self.depth = 0 if compute_depth else -1 + + super()._start() + return self + + def _traverse(self) -> Generator[T_vertex, None, Any]: + "$$ insert_from('$$/method_traverse/code_start_with_tree_and_visited.py') $$" + + # Copy Traversal-specific attributes into method scope (faster access) + compute_depth = self._compute_depth + + # ----- Initialize method specific bookkeeping ----- + + depth = -1 # The inner loop starts with incrementing, so, we pre-decrement + if not compute_depth: + self.depth = depth # In this case, we leave the -1 the whole time + prev_traversal = copy.copy(self) # copy of self, for keeping previous depth + + # vertices to expand + to_expand = self._gear.sequence_of_vertices(self._start_vertices) + to_expand_append = to_expand.append + to_expand_pop = to_expand.pop + + if compute_depth: + # Sequence of flag bytes (store in a Q array) marking the vertices to leave + # by 1 and the vertices to enter (these are in to_expand) by 0. + # Initially, store a zero flag for each start vertex. + to_leave_marker = array.array("B", itertools.repeat(False, len(to_expand))) + to_leave_marker_pop = to_leave_marker.pop + to_leave_marker_append = to_leave_marker.append + + "$$ insert_from('$$/method_traverse/code_prepare_edges_loop.py') $$" + + # ----- Inner loop ----- + + while to_expand: + vertex = to_expand_pop() # Enter first added vertex first + if compute_depth: + depth += 1 + # noinspection PyUnboundLocalVariable + while to_leave_marker_pop(): + depth -= 1 # Got marker "leave a vertex", update depth + # Update external view on depth + prev_traversal.depth = depth + self.depth = depth + 1 + # Store marker True: when reached, we are leaving a vertex + # noinspection PyUnboundLocalVariable + to_leave_marker_append(True) + + "$$ MCalculationLimit.step() $$" + + for edge_or_vertex in next_edge_or_vertex(vertex, prev_traversal): + neighbor = edge_or_vertex[0] if edges_with_data else edge_or_vertex + + if not is_tree or build_paths: + "$$ MStrategy.vertex_to_id('neighbor', 'n_id')$$" + + # If not is_tree: Ignore neighbor if already seen, and + # else include its ID in visited set. + """$$ MVertexSet.if_visited_continue_else_add( + 'visited', 'n_id', 'not is_tree') $$""" + + if build_paths: + """$$ MVertexMappingExpectNone.store_vertex_and_edge_data( + 'predecessors', 'attributes', + 'vertex', 'n_id', 'edge_or_vertex[-1]') + $$""" + + yield neighbor + + # Needs to be expanded, in stack order + to_expand_append(neighbor) + + if compute_depth: + # Store marker False: when reached, we are entering a vertex + to_leave_marker_append(False) + + +"$$ MStrategyWithoutWeights.standard_for_flex('TraversalNeighborsThenDepth') $$" diff --git a/tpl/src/nographs/_strategies/traversals/without_weights/!topological_sort.py b/tpl/src/nographs/_strategies/traversals/without_weights/!topological_sort.py new file mode 100644 index 0000000..6a3e659 --- /dev/null +++ b/tpl/src/nographs/_strategies/traversals/without_weights/!topological_sort.py @@ -0,0 +1,323 @@ +from __future__ import annotations + +import array +import itertools +from typing import Optional, Any, Generic +from collections.abc import Iterable, Generator + +from nographs._types import ( + T_vertex, + T_vertex_id, + T_labels, + VertexToID, + vertex_as_id, +) +from nographs._gears import ( + GearDefault, + GearWithoutDistances, + VertexIdSet, + MutableSequenceOfVertices, +) +from nographs._gear_collections import ( + access_to_vertex_set, + access_to_vertex_mapping_expect_none, +) +from ...type_aliases import ( + NextVertices, + NextEdges, + NextLabeledEdges, +) +from ..traversal import ( + _start_from_needs_traversal_object, +) +from .traversal_without_weights import ( + _create_unified_next, + _TraversalWithoutWeightsWithVisited, +) + +"$$ import_from('$$/MTraversalWithoutWeights.py') $$" + + +class TraversalTopologicalSortFlex( + _TraversalWithoutWeightsWithVisited[T_vertex, T_vertex_id, T_labels] +): + """ + # $$ insert_from('$$/cls_traversal/doc_start.rst') + + **Algorithm:** Topological Search, non-recursive implementation. + Vertices are reported when they "are left" for backtracking. + + **Properties:** Vertices are reported in topological ordering, i.e. a + linear ordering of the vertices such that for every directed edge *uv* + from vertex *u* to vertex *v* ("*u* depends on *v*"), *v* comes before + *u* in the ordering. If the graph contains a cycle that can be reached + within the sorting process, a RuntimeError exception is raised and a + cyclic path from a start vertex is provided. + + Vertices are expanded following the strategy + `nographs.TraversalDepthFirst`. + + A vertex is considered visited from the moment its expansion begins. + Start vertices are considered visited directly from the start of the + traversal. + + # $$ insert_from('$$/cls_traversal/doc_input.rst') + + **Search state:** When a vertex is *expanded* + (traversal calls next_vertices, next_edges or next_labeled_edges) + or *reported* (an iterator of the traversal returns it), + the traversal provides updated values for the attributes + *depth*, *paths*, and *visited*. + """ + + def __init__( + self, + # $$ MStrategyWithoutWeights.init_signature('TraversalTopologicalSortFlex') + ) -> None: + "$$ MStrategyWithoutWeights.init_code() $$" + self.cycle_from_start: list[T_vertex] = [] # value not used, see above + """ If the graph contains a cycle that can be reached within the sorting + process, a RuntimeError exception is raised, and the traversal provides + a cyclic path from a start vertex in attribute cycle_from_start.""" + + def start_from( + self, + # $$ insert_from('$$/method_start_from/signature.py') + ) -> TraversalTopologicalSortFlex[T_vertex, T_vertex_id, T_labels]: + """ + # $$ insert_from('$$/method_start_from/doc_start.rst') + # $$ insert_from('$$/method_start_from/doc_already_visited_compatible.txt') + # $$ insert_from('$$/method_start_from/doc_end.rst') + """ + + "$$ insert_from('$$/method_start_from/code_start.py') $$" + + super()._start() + return self + + def _traverse(self) -> Generator[T_vertex, None, Any]: + # Two separate implementations for the cases is_tree and not is_tree that follow + # different concepts, because a combined approach makes both cases significantly + # slower + if self._is_tree: + return self._traverse_in_tree() + else: + return self._traverse_in_non_tree() + + def _traverse_in_tree(self) -> Generator[T_vertex, None, Any]: + "$$ insert_from('$$/method_traverse/code_start.py') $$" + + "$$ insert_from('$$/method_traverse/code_prepare_edges_loop.py') $$" + + # Since the graph is a tree, we need no cycle check and no skipping of + # already seen vertices. + # We just use a stack for vertices we have to enter or leave, and store + # None on top of the vertices we need to enter in order to differentiate + # the two cases. + + # ----- Initialize specific bookkeeping ----- + + self.cycle_from_start = [] + # Sequence used as stack of vertices that we need to enter & expand (if + # it is not on the trace, see below) or leave & report (otherwise) + to_expand_or_leave = self._gear.sequence_of_vertices(self._start_vertices) + + # Sequence of flag bytes (store in a Q array) marking the vertices to leave + # by 1 and the vertices to enter by 0. + # Initially, store a zero flag for each start vertex. + to_leave_markers = array.array( + "B", itertools.repeat(False, len(to_expand_or_leave)) + ) + + # Get method references of specific bookkeeping (avoid attribute resolution) + to_expand_or_leave_append = to_expand_or_leave.append + to_expand_or_leave_pop = to_expand_or_leave.pop + to_leave_markers_pop = to_leave_markers.pop + to_leave_markers_append = to_leave_markers.append + + # ----- Inner loop ----- + + while to_expand_or_leave: + if to_leave_markers[-1]: + # Vertex is to be left: We "leave" and report it, and remove marker + self.depth -= 1 + to_leave_markers_pop() + vertex = to_expand_or_leave_pop() # handle last added vertex first + yield vertex + continue + + # Vertex is to be expanded: We "expand" it, + # but leave it on the stack, so that it will be "left" later on. + vertex = to_expand_or_leave[-1] + # We change the marker in order to remember that we will have to + # leave (not enter) it + to_leave_markers[-1] = True + + "$$ MCalculationLimit.step() $$" + + for edge_or_vertex in next_edge_or_vertex(vertex, self): + neighbor = edge_or_vertex[0] if edges_with_data else edge_or_vertex + + "$$ MStrategy.vertex_to_id('neighbor', 'n_id')$$" + + if build_paths: + # We have to store the predecessor here, because at time of + # visit, it is already lost. And we cannot yield here, + # because only the first of the neighbors will indeed be + # visited next. + # But since we are in a tree, no other predecessor can + # be stored for that vertex later on. + """$$ MVertexMappingExpectNone.store_vertex_and_edge_data( + 'predecessors', 'attributes', + 'vertex', 'n_id', 'edge_or_vertex[-1]') + $$""" + + # Put vertex on the stack + to_expand_or_leave_append(neighbor) + # Remember that we have to expand it + to_leave_markers_append(False) + + self.depth += 1 + + def _traverse_in_non_tree(self) -> Generator[T_vertex, None, Any]: + "$$ insert_from('$$/method_traverse/code_start.py') $$" + visited = self.visited + "$$ MVertexSet.access(name='visited') $$" + + "$$ insert_from('$$/method_traverse/code_prepare_edges_loop.py') $$" + + # Since the graph is not guaranteed to be a tree, we need a cycle check and + # need to skip already seen vertices. + # For detecting cycles, we store the vertices, that are on the current + # path, in a set. We use a stack for storing the vertices we have to + # enter or leave (when such a vertex is in the path set, we need to leave + # the vertex). + + # ----- Initialize specific bookkeeping ----- + + self.cycle_from_start = [] + + # Sequence used as stack of vertices that we need to enter & expand (if + # it is not on the trace, see below) or leave & report (otherwise) + to_enter_or_leave = self._gear.sequence_of_vertices(self._start_vertices) + to_visit_pop = to_enter_or_leave.pop + to_visit_append = to_enter_or_leave.append + + # Set of vertices along the current path + # (We need this for fast cycle detection. We could use additionally + # a trace sequence to speed up the check if the current vertex is + # the top vertex of the trace instead of checking if it is "in" the + # trace, but this would cost maintenance runtime and memory for the + # sequence). + trace_set = self._gear.vertex_id_set(()) + "$$ MVertexSet.access(name='trace_set', ops=['add', 'discard']) $$" + + "$$ MVertexSet.combine_access('visited', 'trace_set', 'set') $$" + + # ----- Inner loop ----- + + while to_enter_or_leave: + vertex = to_enter_or_leave[-1] # visit/report last added vertex first + "$$ MStrategy.vertex_to_id('vertex', 'v_id')$$" + + # If v_id is in trace_set: + # Back to trace, from visits/reports of further vertices, + # that trace vertices depend on: We "leave" and report the head + # vertex of the trace + # (A note about the above "in" check: + # If v_id is in the set, it needs to be + # the last id added there. But this does not help us, since + # sets are not ordered as dicts nowadays are). + # Otherwise: + # Ignore v_id if visited, else include vertex n_id in visited set. + # Then, take it to the trace. + """$$ + MVertexSet.compile_access('v_id', '', 'set', '''\ + $trace_set.if_contains_vertex_id_prepare_remove_and_elseadd: + self.depth -= 1 + to_visit_pop() + $trace_set.prepared_remove_vertex_id + yield vertex + continue + $traceset.endif + if self.depth > 0: + $visited.if_contains_vertex_id_prepare_remove_and_elseadd: + to_visit_pop() + continue + $visited.else_prepared_add_endif + $trace_set.add_vertex_id + ''') +$$""" + + # We "expand" the vertex + "$$ MCalculationLimit.step() $$" + + for edge_or_vertex in next_edge_or_vertex(vertex, self): + neighbor = edge_or_vertex[0] if edges_with_data else edge_or_vertex + + "$$ MStrategy.vertex_to_id('neighbor', 'n_id2')$$" + + # If neighbor is already visited and in trace_set: + # We found a dependency (edge) back to a vertex, whose + # dependencies we are currently following (trace). We + # build and report this trace: a cycle. + # If neighbor is already visited: + # ignore it (in case of path generation, this is necessary, and + # otherwise, it is a small optimization.) + # Note: In order to become visited, a vertex needs to + # get into the trace set and then be discarded + # from it. Thus, in the inner if, we know that the neighbour + # has already been in the trace set sometimes. + """$$ + MVertexSet.compile_access('n_id2', '', 'set', '''\ + $visited.if_contains_vertex_id: + $trace_set.if_contains_vertex_id: + self._report_cycle( + neighbor, + to_enter_or_leave, + trace_set, + maybe_vertex_to_id, + ) + $traceset.endif_with_vertex_included_in_past + continue + $traceset.endif + ''') +$$""" + + if build_paths: + # We have to store the predecessor here, because at time of + # visit, it is already lost. And we cannot yield here, + # because TopologicalSorted reports not until leaving vertices. + # But since the visiting order is defined by a stack we know + # that for each vertex, the predecessor stored last is the + # edge visited first, and after that no other predecessor can + # be stored for that vertex. + """$$ MVertexMappingExpectNone.store_vertex_and_edge_data( + 'predecessors', 'attributes', + 'vertex', 'n_id2', 'edge_or_vertex[-1]') + $$""" + + # Needs to be visited, in stack order + to_visit_append(neighbor) + + # Update depth. The following vertices are one level deeper. + self.depth += 1 + + def _report_cycle( + self, + neighbor: T_vertex, + to_visit: MutableSequenceOfVertices[T_vertex], + trace_set: VertexIdSet[T_vertex_id], + maybe_vertex_to_id: Optional[VertexToID[T_vertex, T_vertex_id]], + ) -> None: + trace = list() + for vertex in to_visit: + "$$ MStrategy.vertex_to_id('vertex', 'v_id2')$$" + if v_id2 in trace_set: + trace.append(vertex) + trace.append(neighbor) + self.cycle_from_start = trace + raise RuntimeError("Graph contains cycle") + + +"$$ MStrategyWithoutWeights.standard_for_flex('TraversalTopologicalSort') $$" diff --git a/tpl/src/nographs/_strategies/traversals/without_weights/MTraversalWithoutWeights.py b/tpl/src/nographs/_strategies/traversals/without_weights/MTraversalWithoutWeights.py new file mode 100644 index 0000000..af25739 --- /dev/null +++ b/tpl/src/nographs/_strategies/traversals/without_weights/MTraversalWithoutWeights.py @@ -0,0 +1,164 @@ +from tpl.make_insert_look_defined import * + +# """$$ +import_from("$$/../../MStrategy.py") + + +class MStrategy: + """Methods to implement subclasses of Traversal.""" + + @staticmethod + def vertex_to_id(vertex_name: str, vertex_id_name: str) -> None: + insert( + f"""\ + {vertex_id_name}: T_vertex_id = ( + maybe_vertex_to_id({vertex_name}) # type: ignore[assignment] + if maybe_vertex_to_id + else {vertex_name} + ) +""" + ) + + +class MStrategyWithoutWeights: + """Methods to implement subclasses of _TraversalWithoutWeight.""" + + @staticmethod + def init_signature(traversal_type: str) -> None: + insert( + f"""\ + vertex_to_id: VertexToID[T_vertex, T_vertex_id], + gear: GearWithoutDistances[T_vertex, T_vertex_id, T_labels], + next_vertices: Optional[ + NextVertices[ + T_vertex, {traversal_type}[T_vertex, T_vertex_id, T_labels] + ] + ] = None, + *, + next_edges: Optional[ + NextEdges[ + T_vertex, {traversal_type}[T_vertex, T_vertex_id, T_labels] + ] + ] = None, + next_labeled_edges: Optional[ + NextLabeledEdges[ + T_vertex, + {traversal_type}[T_vertex, T_vertex_id, T_labels], + T_labels, + ] + ] = None, + is_tree: bool = False, +""" + ) + + @staticmethod + def init_code( + depth_computation_optional: bool = False, + search_depth_is_vertex_depth: bool = False, + ) -> None: + insert( + f'''\ + ( + self._next_edge_or_vertex, + edges_with_data, + labeled_edges, + ) = _create_unified_next(next_vertices, next_edges, next_labeled_edges) + super().__init__(edges_with_data, labeled_edges, is_tree, vertex_to_id, gear) + self.depth: int = -1 # value not used, initialized during traversal + """ +''' + ) + if depth_computation_optional: + insert( + """\ + If depth computation has been demanded (see option *compute_depth*): +""" + ) + insert( + f"""\ + At this *search depth*, the reported (resp. the expanded) vertex has been + found. It equals the length (number of edges) of the created path to the + vertex, if path creation is demanded. +""" + ) + if search_depth_is_vertex_depth: + insert( + """\ + + For the special case of this traversal, it equals the + *depth of the vertex* (minimal number of edges needed to come to it + from a start vertex). +""" + ) + else: + insert( + """\ + Note: The search depth does not need to be the depth of the vertex + (see `TraversalBreadthFirstFlex`). +""" + ) + insert( + f'''\ + When a traversal has been started, but no vertex has been reported or expanded + so far, the depth is 0 (depth of the start vertices). + """ +''' + ) + + @staticmethod + def standard_for_flex(class_name: str) -> None: + insert( + f'''\ + class {class_name} ( + Generic[T_vertex, T_labels], {class_name}Flex[T_vertex, T_vertex, T_labels] + ): + """ + Eases the use of `{class_name}Flex` for typical cases. + For documentation of functionality and parameters, see there. + + Uses the following standard arguments for the respective parameters of + the parent class: + + - vertex_to_id = `vertex_as_id` + - gear = `GearDefault` + - `T_vertex_id` = `T_vertex` + + Implications: + + - `GearDefault` is used, see there how it and its superclass work + - T_vertex is bound to Hashable (T_vertex is used as `T_vertex_id`, see there) + """ + + def __init__( + self, + next_vertices: Optional[ + NextVertices[ + T_vertex, {class_name}Flex[T_vertex, T_vertex, T_labels] + ] + ] = None, + *, + next_edges: Optional[ + NextEdges[T_vertex, {class_name}Flex[T_vertex, T_vertex, T_labels]] + ] = None, + next_labeled_edges: Optional[ + NextLabeledEdges[ + T_vertex, + {class_name}Flex[T_vertex, T_vertex, T_labels], + T_labels, + ] + ] = None, + is_tree: bool = False, + ) -> None: + super().__init__( + vertex_as_id, + GearDefault(), + next_vertices, + next_edges=next_edges, + next_labeled_edges=next_labeled_edges, + is_tree=is_tree, + ) +''' + ) + + +# $$""" diff --git a/tpl/src/nographs/_strategies/traversals/without_weights/cls_traversal/doc_input.rst b/tpl/src/nographs/_strategies/traversals/without_weights/cls_traversal/doc_input.rst new file mode 100644 index 0000000..eb37f1f --- /dev/null +++ b/tpl/src/nographs/_strategies/traversals/without_weights/cls_traversal/doc_input.rst @@ -0,0 +1,2 @@ +**Input:** Directed graph. Unlabeled or labeled edges. One or more start +vertices. Optional calculation limit. diff --git a/tpl/src/nographs/_strategies/traversals/without_weights/cls_traversal/doc_start.rst b/tpl/src/nographs/_strategies/traversals/without_weights/cls_traversal/doc_start.rst new file mode 100644 index 0000000..326d563 --- /dev/null +++ b/tpl/src/nographs/_strategies/traversals/without_weights/cls_traversal/doc_start.rst @@ -0,0 +1,20 @@ +Bases: `Traversal` [`T_vertex`, `T_vertex_id`, `T_labels`] + +:param vertex_to_id: See `VertexToID` function. + +:param gear: See `gears API ` and class `GearWithoutDistances`. + +:param next_vertices: See `NextVertices` function. If None, provide next_edges + or next_labeled_edges. + +:param next_edges: See `NextEdges` function. Only allowed if next_vertices equals + None. If both are None, provide next_labeled_edges. + +:param next_labeled_edges: See `NextLabeledEdges` function. Only allowed if + next_vertices and next_edges equal None. If given, paths will record the given + labels. + +:param is_tree: bool: If it is certain, that during each traversal run, + each vertex can be reached only once, is_tree can be set to True. This + improves performance, but attribute *visited* of the traversal will not be + updated during and after the traversal. diff --git a/tpl/src/nographs/_strategies/traversals/without_weights/method_start_from/code_start.py b/tpl/src/nographs/_strategies/traversals/without_weights/method_start_from/code_start.py new file mode 100644 index 0000000..5f3e11e --- /dev/null +++ b/tpl/src/nographs/_strategies/traversals/without_weights/method_start_from/code_start.py @@ -0,0 +1,9 @@ +_start_from_needs_traversal_object(self) +self._start_without_weights_with_visited( + start_vertex, + start_vertices, + build_paths, + calculation_limit, + already_visited, +) +self.depth = 0 diff --git a/tpl/src/nographs/_strategies/traversals/without_weights/method_start_from/doc_already_visited_compatible.txt b/tpl/src/nographs/_strategies/traversals/without_weights/method_start_from/doc_already_visited_compatible.txt new file mode 100644 index 0000000..a568bbc --- /dev/null +++ b/tpl/src/nographs/_strategies/traversals/without_weights/method_start_from/doc_already_visited_compatible.txt @@ -0,0 +1,14 @@ +:param already_visited: If provided, this set is used instead of an internal + one to keep vertices (resp. their hashable ids from vertex_to_id), + that have already been visited. This parameter can be used to get online + access to the internal bookkeeping of visited vertices, or to preload + vertices that should never be visited. + + Attention: TraversalTopologicalSortFlex requires, that the collection + given as argument for parameter already_visited is compatible + (in any sense) with the collection that gear.vertex_id_set() + returns. If you have chosen GearDefault, you can just use a dict. + Otherwise, create the collection by calling gear.vertex_id_set() or use the + collection that another traversal with the same gear gives as attribute + visited. + diff --git a/tpl/src/nographs/_strategies/traversals/without_weights/method_start_from/doc_already_visited_std.txt b/tpl/src/nographs/_strategies/traversals/without_weights/method_start_from/doc_already_visited_std.txt new file mode 100644 index 0000000..3fe6bc4 --- /dev/null +++ b/tpl/src/nographs/_strategies/traversals/without_weights/method_start_from/doc_already_visited_std.txt @@ -0,0 +1,7 @@ +:param already_visited: If provided, this set is used instead of an internal + one to keep vertices (resp. their hashable ids from vertex_to_id), + that have already been visited. This parameter can be used to get online + access to the internal bookkeeping of visited vertices, or to preload + vertices that should never be visited, or to provide your own way for + storing the information that a vertex has already been visited. + diff --git a/tpl/src/nographs/_strategies/traversals/without_weights/method_start_from/doc_compute_depth.txt b/tpl/src/nographs/_strategies/traversals/without_weights/method_start_from/doc_compute_depth.txt new file mode 100644 index 0000000..39aebf2 --- /dev/null +++ b/tpl/src/nographs/_strategies/traversals/without_weights/method_start_from/doc_compute_depth.txt @@ -0,0 +1,4 @@ +:param compute_depth: For each reported vertex, provide the search depth is has + been found at (Note: Often, this information is not helpful, and the + computation increases memory consumption and runtime). + diff --git a/tpl/src/nographs/_strategies/traversals/without_weights/method_start_from/doc_end.rst b/tpl/src/nographs/_strategies/traversals/without_weights/method_start_from/doc_end.rst new file mode 100644 index 0000000..cc8fff2 --- /dev/null +++ b/tpl/src/nographs/_strategies/traversals/without_weights/method_start_from/doc_end.rst @@ -0,0 +1,2 @@ +:return: Traversal, that has been started, e.g., statements like *iter()*, + *next()*, *for* and the methods "go*" of the Traversal can now be used. diff --git a/tpl/src/nographs/_strategies/traversals/without_weights/method_start_from/doc_start.rst b/tpl/src/nographs/_strategies/traversals/without_weights/method_start_from/doc_start.rst new file mode 100644 index 0000000..5612641 --- /dev/null +++ b/tpl/src/nographs/_strategies/traversals/without_weights/method_start_from/doc_start.rst @@ -0,0 +1,14 @@ +Start the traversal at a vertex or a set of vertices and set parameters. + +:param start_vertex: The vertex the search should start at. If None, provide + start_vertices. + +:param start_vertices: The vertices the search should start at. Only + allowed if start_vertex equals None. + +:param build_paths: If true, build paths from some start vertex to each visited + vertex. Paths of start vertices are empty paths. + +:param calculation_limit: If provided, maximal number of vertices to process + (read in) from your graph. If it is exceeded, a RuntimeError will be raised. + diff --git a/tpl/src/nographs/_strategies/traversals/without_weights/method_start_from/signature.py b/tpl/src/nographs/_strategies/traversals/without_weights/method_start_from/signature.py new file mode 100644 index 0000000..b3f96f7 --- /dev/null +++ b/tpl/src/nographs/_strategies/traversals/without_weights/method_start_from/signature.py @@ -0,0 +1,6 @@ +start_vertex: Optional[T_vertex] = None, +*, +start_vertices: Optional[Iterable[T_vertex]] = None, +build_paths: bool = False, +calculation_limit: Optional[int] = None, +already_visited: Optional[VertexIdSet[T_vertex_id]] = None, diff --git a/tpl/src/nographs/_strategies/traversals/without_weights/method_traverse/code_edges_loop.py b/tpl/src/nographs/_strategies/traversals/without_weights/method_traverse/code_edges_loop.py new file mode 100644 index 0000000..e69de29 diff --git a/tpl/src/nographs/_strategies/traversals/without_weights/method_traverse/code_prepare_edges_loop.py b/tpl/src/nographs/_strategies/traversals/without_weights/method_traverse/code_prepare_edges_loop.py new file mode 100644 index 0000000..1e3b2a2 --- /dev/null +++ b/tpl/src/nographs/_strategies/traversals/without_weights/method_traverse/code_prepare_edges_loop.py @@ -0,0 +1,15 @@ +# ----- Typing preparation of inner loop ----- + +# The following type Any opens no space for typing problems, since +# the content of next_edge_or_vertex is type checked and iterates +# objects of type T_vertex for edges_with_data==False and otherwise of +# one of the following: +# WeightedUnlabeledOutEdge[T_vertex, Any], +# UnweightedLabeledOutEdge[T_vertex, T_labels], +# WeightedLabeledOutEdge[T_vertex, Any, T_labels], +# And if labeled_edges==True, the first case is excluded. +# (Any alternative code version of the inner loop without +# Any or 'type: ignore' is slower) +edge_or_vertex: Any # "Hole" in typing, but types "around" make it safe +neighbor: T_vertex # Re-establish type "after" the "hole" +data_of_edge: T_labels # Re-establish type "after" the "hole" diff --git a/tpl/src/nographs/_strategies/traversals/without_weights/method_traverse/code_start.py b/tpl/src/nographs/_strategies/traversals/without_weights/method_traverse/code_start.py new file mode 100644 index 0000000..048eb05 --- /dev/null +++ b/tpl/src/nographs/_strategies/traversals/without_weights/method_traverse/code_start.py @@ -0,0 +1,21 @@ +"$$ import_from('$$/../../../MStrategy.py') $$" +# ----- Prepare efficient environment for inner loop ----- +# Copy Traversal attributes into method scope (faster access) +labeled_edges = self._labeled_edges +maybe_vertex_to_id = ( + None if self._vertex_to_id == vertex_as_id else self._vertex_to_id +) # Case vertex_as_id: not apply; T_vertex_id > T_vertex +build_paths = self._build_paths +calculation_limit = self._calculation_limit +predecessors = self._predecessors +attributes = self._attributes + +"$$ MCalculationLimit.prepare() $$" + +# Copy _TraversalWithoutWeightsWithVisited attributes into method scope +edges_with_data = self._edges_with_data +next_edge_or_vertex = self._next_edge_or_vertex + +# Get references of used gear objects and methods (avoid attribute resolution) +"$$ MVertexMappingExpectNone.access(name='predecessors') $$" +"$$ MVertexMappingExpectNone.access(name='attributes') $$" diff --git a/tpl/src/nographs/_strategies/traversals/without_weights/method_traverse/code_start_with_tree_and_visited.py b/tpl/src/nographs/_strategies/traversals/without_weights/method_traverse/code_start_with_tree_and_visited.py new file mode 100644 index 0000000..b504ccd --- /dev/null +++ b/tpl/src/nographs/_strategies/traversals/without_weights/method_traverse/code_start_with_tree_and_visited.py @@ -0,0 +1,10 @@ +"$$ insert_from('$$/code_start.py') $$" + +# Copy further traversal attributes into method scope (faster access) +is_tree = self._is_tree +visited = self.visited + +# Get further references of used gear objects and methods +# (avoid attribute resolution) +"$$ import_from('$$/../../../MStrategy.py') $$" +"$$ MVertexSet.access(name='visited') $$"