|
1 | 1 | import numpy as np
|
2 |
| -from graphblas import Matrix, Vector, binary, monoid, replace, select, unary |
| 2 | +from graphblas import Matrix, Vector, binary, indexunary, monoid, replace, select, unary |
3 | 3 | from graphblas.semiring import any_pair, min_plus
|
4 | 4 |
|
5 |
| -from .._bfs import _bfs_level, _bfs_levels |
| 5 | +from .._bfs import _bfs_level, _bfs_levels, _bfs_parent, _bfs_plain |
6 | 6 | from ..exceptions import Unbounded
|
7 | 7 |
|
8 | 8 | __all__ = [
|
9 | 9 | "single_source_bellman_ford_path_length",
|
| 10 | + "bellman_ford_path", |
10 | 11 | "bellman_ford_path_lengths",
|
11 | 12 | "negative_edge_cycle",
|
12 | 13 | ]
|
@@ -164,6 +165,117 @@ def bellman_ford_path_lengths(G, nodes=None, *, expand_output=False):
|
164 | 165 | return D
|
165 | 166 |
|
166 | 167 |
|
| 168 | +def _reconstruct_path_from_parents(G, parents, src, dst): |
| 169 | + indices, values = parents.to_coo(sort=False) |
| 170 | + d = dict(zip(indices.tolist(), values.tolist())) |
| 171 | + if dst not in d: |
| 172 | + return [] |
| 173 | + cur = dst |
| 174 | + path = [cur] |
| 175 | + while cur != src: |
| 176 | + cur = d[cur] |
| 177 | + path.append(cur) |
| 178 | + return G.list_to_keys(reversed(path)) |
| 179 | + |
| 180 | + |
| 181 | +def bellman_ford_path(G, source, target): |
| 182 | + src_id = G._key_to_id[source] |
| 183 | + dst_id = G._key_to_id[target] |
| 184 | + if G.get_property("is_iso"): |
| 185 | + # If the edges are iso-valued (and positive), then we can simply do level BFS |
| 186 | + is_negative = G.get_property("has_negative_edges+") |
| 187 | + if not is_negative: |
| 188 | + p = _bfs_parent(G, source, target=target) |
| 189 | + return _reconstruct_path_from_parents(G, p, src_id, dst_id) |
| 190 | + raise Unbounded("Negative cycle detected.") |
| 191 | + A, is_negative, has_negative_diagonal = G.get_properties( |
| 192 | + "offdiag has_negative_edges- has_negative_diagonal" |
| 193 | + ) |
| 194 | + if A.dtype == bool: |
| 195 | + # Should we upcast e.g. INT8 to INT64 as well? |
| 196 | + dtype = int |
| 197 | + else: |
| 198 | + dtype = A.dtype |
| 199 | + cutoff = None |
| 200 | + n = A.nrows |
| 201 | + d = Vector(dtype, n, name="bellman_ford_path_length") |
| 202 | + d[src_id] = 0 |
| 203 | + p = Vector(int, n, name="bellman_ford_path_parent") |
| 204 | + p[src_id] = src_id |
| 205 | + |
| 206 | + prev = d.dup(name="prev") |
| 207 | + cur = Vector(dtype, n, name="cur") |
| 208 | + indices = Vector(int, n, name="indices") |
| 209 | + mask = Vector(bool, n, name="mask") |
| 210 | + B = Matrix(dtype, n, n, name="B") |
| 211 | + Indices = Matrix(int, n, n, name="Indices") |
| 212 | + cols = prev.to_coo(values=False)[0] |
| 213 | + one = unary.one[bool] |
| 214 | + for _i in range(n - 1): |
| 215 | + # This is a slightly modified Bellman-Ford algorithm. |
| 216 | + # `cur` is the current frontier of values that improved in the previous iteration. |
| 217 | + # This means that in this iteration we drop values from `cur` that are not better. |
| 218 | + cur << min_plus(prev @ A) |
| 219 | + if cutoff is not None: |
| 220 | + cur << select.valuele(cur, cutoff) |
| 221 | + |
| 222 | + # Mask is True where cur not in d or cur < d |
| 223 | + mask << one(cur) |
| 224 | + mask(binary.second) << binary.lt(cur & d) |
| 225 | + |
| 226 | + # Drop values from `cur` that didn't improve |
| 227 | + cur(mask.V, replace) << cur |
| 228 | + if cur.nvals == 0: |
| 229 | + break |
| 230 | + # Update `d` with values that improved |
| 231 | + d(cur.S) << cur |
| 232 | + if not is_negative: |
| 233 | + # Limit exploration if we have a target |
| 234 | + cutoff = cur.get(dst_id, cutoff) |
| 235 | + |
| 236 | + # Now try to find the parents! |
| 237 | + # This is also not standard. Typically, UDTs and UDFs are used to keep |
| 238 | + # track of both the minimum element and the parent id at the same time. |
| 239 | + # Only include rows and columns that were used this iteration. |
| 240 | + rows = cols |
| 241 | + cols = cur.to_coo(values=False)[0] |
| 242 | + B.clear() |
| 243 | + B[rows, cols] = A[rows, cols] |
| 244 | + |
| 245 | + # Reverse engineer to determine parent |
| 246 | + B << binary.plus(prev & B) |
| 247 | + B << binary.iseq(B & cur) |
| 248 | + B << select.valuene(B, False) |
| 249 | + Indices << indexunary.rowindex(B) |
| 250 | + indices << Indices.reduce_columnwise(monoid.min) |
| 251 | + p(indices.S) << indices |
| 252 | + prev, cur = cur, prev |
| 253 | + else: |
| 254 | + # Check for negative cycle when for loop completes without breaking |
| 255 | + cur << min_plus(prev @ A) |
| 256 | + if cutoff is not None: |
| 257 | + cur << select.valuele(cur, cutoff) |
| 258 | + mask << binary.lt(cur & d) |
| 259 | + if mask.get(dst_id): |
| 260 | + raise Unbounded("Negative cycle detected.") |
| 261 | + path = _reconstruct_path_from_parents(G, p, src_id, dst_id) |
| 262 | + if has_negative_diagonal and path: |
| 263 | + mask.clear() |
| 264 | + mask[G.list_to_ids(path)] = True |
| 265 | + diag = G.get_property("diag", mask=mask.S) |
| 266 | + if diag.nvals > 0: |
| 267 | + raise Unbounded("Negative cycle detected.") |
| 268 | + mask << binary.first(mask & cur) # mask(cur.S, replace) << mask |
| 269 | + if mask.nvals > 0: |
| 270 | + # Is there a path from any visited node with negative self-loop to target? |
| 271 | + # We could actually stop as soon as any from `path` is visited |
| 272 | + indices, _ = mask.to_coo(values=False)[0] |
| 273 | + q = _bfs_plain(G, target=target, index=indices, cutoff=_i) |
| 274 | + if dst_id in q: |
| 275 | + raise Unbounded("Negative cycle detected.") |
| 276 | + return path |
| 277 | + |
| 278 | + |
167 | 279 | def negative_edge_cycle(G):
|
168 | 280 | # TODO: use a heuristic to try to stop early
|
169 | 281 | if G.is_directed():
|
|
0 commit comments