- Notifications
You must be signed in to change notification settings - Fork5
Addbellman_ford_path#64
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,12 @@ | ||
| from .._bfs import_bfs_plain | ||
| from ..exceptions import PointlessConcept | ||
| def is_connected(G): | ||
| if len(G) == 0: | ||
| raise PointlessConcept("Connectivity is undefined for the null graph.") | ||
| return_bfs_plain(G, next(iter(G))).nvals == len(G) | ||
| def node_connected_component(G, n): | ||
| return_bfs_plain(G, n) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,8 @@ | ||
| from .._bfs import_bfs_plain_bidirectional | ||
| from ..exceptions import PointlessConcept | ||
| def is_weakly_connected(G): | ||
| if len(G) == 0: | ||
| raise PointlessConcept("Connectivity is undefined for the null graph.") | ||
| return_bfs_plain_bidirectional(G, next(iter(G))).nvals == len(G) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,13 @@ | ||
| import numpy as np | ||
| from graphblas import Matrix, Vector, binary,indexunary,monoid, replace, select, unary | ||
| from graphblas.semiring import any_pair, min_plus | ||
| from .._bfs import _bfs_level, _bfs_levels, _bfs_parent, _bfs_plain | ||
| from ..exceptions import Unbounded | ||
| __all__ = [ | ||
| "single_source_bellman_ford_path_length", | ||
| "bellman_ford_path", | ||
| "bellman_ford_path_lengths", | ||
| "negative_edge_cycle", | ||
| ] | ||
| @@ -164,6 +165,117 @@ def bellman_ford_path_lengths(G, nodes=None, *, expand_output=False): | ||
| return D | ||
| def _reconstruct_path_from_parents(G, parents, src, dst): | ||
| indices, values = parents.to_coo(sort=False) | ||
| d = dict(zip(indices.tolist(), values.tolist())) | ||
| if dst not in d: | ||
| return [] | ||
| cur = dst | ||
| path = [cur] | ||
| while cur != src: | ||
| cur = d[cur] | ||
| path.append(cur) | ||
| return G.list_to_keys(reversed(path)) | ||
| def bellman_ford_path(G, source, target): | ||
| src_id = G._key_to_id[source] | ||
| dst_id = G._key_to_id[target] | ||
| if G.get_property("is_iso"): | ||
| # If the edges are iso-valued (and positive), then we can simply do level BFS | ||
| is_negative = G.get_property("has_negative_edges+") | ||
| if not is_negative: | ||
| p = _bfs_parent(G, source, target=target) | ||
| return _reconstruct_path_from_parents(G, p, src_id, dst_id) | ||
| raise Unbounded("Negative cycle detected.") | ||
| A, is_negative, has_negative_diagonal = G.get_properties( | ||
| "offdiag has_negative_edges- has_negative_diagonal" | ||
| ) | ||
| if A.dtype == bool: | ||
| # Should we upcast e.g. INT8 to INT64 as well? | ||
| dtype = int | ||
| else: | ||
| dtype = A.dtype | ||
| cutoff = None | ||
| n = A.nrows | ||
| d = Vector(dtype, n, name="bellman_ford_path_length") | ||
| d[src_id] = 0 | ||
| p = Vector(int, n, name="bellman_ford_path_parent") | ||
| p[src_id] = src_id | ||
| prev = d.dup(name="prev") | ||
| cur = Vector(dtype, n, name="cur") | ||
| indices = Vector(int, n, name="indices") | ||
| mask = Vector(bool, n, name="mask") | ||
| B = Matrix(dtype, n, n, name="B") | ||
| Indices = Matrix(int, n, n, name="Indices") | ||
| cols = prev.to_coo(values=False)[0] | ||
| one = unary.one[bool] | ||
| for _i in range(n - 1): | ||
| # This is a slightly modified Bellman-Ford algorithm. | ||
| # `cur` is the current frontier of values that improved in the previous iteration. | ||
| # This means that in this iteration we drop values from `cur` that are not better. | ||
| cur << min_plus(prev @ A) | ||
| if cutoff is not None: | ||
| cur << select.valuele(cur, cutoff) | ||
| # Mask is True where cur not in d or cur < d | ||
| mask << one(cur) | ||
| mask(binary.second) << binary.lt(cur & d) | ||
| # Drop values from `cur` that didn't improve | ||
| cur(mask.V, replace) << cur | ||
| if cur.nvals == 0: | ||
| break | ||
| # Update `d` with values that improved | ||
| d(cur.S) << cur | ||
| if not is_negative: | ||
| # Limit exploration if we have a target | ||
| cutoff = cur.get(dst_id, cutoff) | ||
| # Now try to find the parents! | ||
| # This is also not standard. Typically, UDTs and UDFs are used to keep | ||
| # track of both the minimum element and the parent id at the same time. | ||
| # Only include rows and columns that were used this iteration. | ||
| rows = cols | ||
| cols = cur.to_coo(values=False)[0] | ||
| B.clear() | ||
| B[rows, cols] = A[rows, cols] | ||
| # Reverse engineer to determine parent | ||
| B << binary.plus(prev & B) | ||
| B << binary.iseq(B & cur) | ||
| B << select.valuene(B, False) | ||
| Indices << indexunary.rowindex(B) | ||
Comment on lines +248 to +249 MemberAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. These two lines could be replaced with Indices(B.V,replace)<<indexunary.rowindex(B) but I think it's clearer in two lines (using masking with replace is kind of ugly). I don't know if there is a performance difference. | ||
| indices << Indices.reduce_columnwise(monoid.min) | ||
| p(indices.S) << indices | ||
| prev, cur = cur, prev | ||
| else: | ||
| # Check for negative cycle when for loop completes without breaking | ||
| cur << min_plus(prev @ A) | ||
| if cutoff is not None: | ||
| cur << select.valuele(cur, cutoff) | ||
| mask << binary.lt(cur & d) | ||
| if mask.get(dst_id): | ||
| raise Unbounded("Negative cycle detected.") | ||
| path = _reconstruct_path_from_parents(G, p, src_id, dst_id) | ||
| if has_negative_diagonal and path: | ||
| mask.clear() | ||
| mask[G.list_to_ids(path)] = True | ||
| diag = G.get_property("diag", mask=mask.S) | ||
| if diag.nvals > 0: | ||
| raise Unbounded("Negative cycle detected.") | ||
| mask << binary.first(mask & cur) # mask(cur.S, replace) << mask | ||
| if mask.nvals > 0: | ||
| # Is there a path from any visited node with negative self-loop to target? | ||
| # We could actually stop as soon as any from `path` is visited | ||
| indices, _ = mask.to_coo(values=False)[0] | ||
| q = _bfs_plain(G, target=target, index=indices, cutoff=_i) | ||
| if dst_id in q: | ||
| raise Unbounded("Negative cycle detected.") | ||
| return path | ||
| def negative_edge_cycle(G): | ||
| # TODO: use a heuristic to try to stop early | ||
| if G.is_directed(): | ||