digraphx 0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- digraphx/__init__.py +16 -0
- digraphx/max_cycle_ratio.tpy +134 -0
- digraphx/min_cycle_ratio.py +174 -0
- digraphx/min_parmetric_q.py +150 -0
- digraphx/neg_cycle.py +241 -0
- digraphx/neg_cycle_q.py +342 -0
- digraphx/parametric.py +135 -0
- digraphx/skeleton.py +149 -0
- digraphx/tiny_digraph.py +79 -0
- digraphx-0.1.dist-info/METADATA +56 -0
- digraphx-0.1.dist-info/RECORD +14 -0
- digraphx-0.1.dist-info/WHEEL +5 -0
- digraphx-0.1.dist-info/licenses/LICENSE.txt +21 -0
- digraphx-0.1.dist-info/top_level.txt +1 -0
digraphx/neg_cycle.py
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NegCycleFinder
|
|
3
|
+
|
|
4
|
+
This code defines a class called NegCycleFinder, which is designed to find negative cycles in a directed graph. A negative cycle is a loop in the graph where the sum of the edge weights is less than zero. This can be important in various applications, such as detecting arbitrage opportunities in currency exchange rates.
|
|
5
|
+
|
|
6
|
+
The NegCycleFinder takes a directed graph as input. The graph is represented as a mapping (like a dictionary) where each key is a node, and its value is another mapping of neighboring nodes and their connecting edges. The class also works with a distance mapping and a function to get the weight of an edge.
|
|
7
|
+
|
|
8
|
+
The main output of this class is a list of edges that form a negative cycle in the graph. It doesn't return this directly, but instead yields these cycles through a generator function called howard().
|
|
9
|
+
|
|
10
|
+
To find negative cycles, the class uses two main algorithms: the Bellman-Ford algorithm and Howard's method. The Bellman-Ford algorithm is used in the relax() method to update the shortest distances between nodes. Howard's method, implemented in the howard() function, uses this relaxation step repeatedly to find negative cycles.
|
|
11
|
+
|
|
12
|
+
The process works like this: First, the relax() method goes through all edges in the graph and updates the distances if a shorter path is found. It also keeps track of which edge led to each node in the pred dictionary. Then, the find_cycle() method looks for cycles in this predecessor graph. If a cycle is found, the is_negative() method checks if it's a negative cycle by comparing the distances and edge weights. If a negative cycle is found, it's yielded by the howard() method.
|
|
13
|
+
|
|
14
|
+
An important part of the logic is how the class maintains and updates the pred dictionary. This dictionary keeps track of which node and edge led to each node in the shortest path found so far. This information is crucial for reconstructing the cycles when they're found.
|
|
15
|
+
|
|
16
|
+
The code uses some advanced Python features like type hinting and generators, but the core logic is based on graph traversal and cycle detection, which are fundamental concepts in graph theory and algorithm design. The class provides a reusable tool for finding negative cycles in any directed graph, which can be useful in many different applications.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from fractions import Fraction
|
|
20
|
+
from typing import (
|
|
21
|
+
Callable,
|
|
22
|
+
Dict,
|
|
23
|
+
Generator,
|
|
24
|
+
Generic,
|
|
25
|
+
List,
|
|
26
|
+
Mapping,
|
|
27
|
+
MutableMapping,
|
|
28
|
+
Tuple,
|
|
29
|
+
TypeVar,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
Node = TypeVar("Node") # Hashable
|
|
33
|
+
Edge = TypeVar("Edge") # Hashable
|
|
34
|
+
Domain = TypeVar("Domain", int, Fraction, float) # Comparable Ring
|
|
35
|
+
Cycle = List[Edge] # List of Edges
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# The `NegCycleFinder` class implements Howard's method, a minimum cycle ratio algorithm, to find
|
|
39
|
+
# negative cycles in a directed graph.
|
|
40
|
+
class NegCycleFinder(Generic[Node, Edge, Domain]):
|
|
41
|
+
"""Negative Cycle Finder by Howard's method
|
|
42
|
+
|
|
43
|
+
This code defines a `NegCycleFinder` class, which is used to find negative cycles in a given directed graph. The `NegCycleFinder` class has the following methods:
|
|
44
|
+
|
|
45
|
+
1. `__init__(self, digraph: MutableMapping[Node, List[Edge]])`:
|
|
46
|
+
The constructor initializes an instance of the `NegCycleFinder` class with the given directed graph.
|
|
47
|
+
2. `relax(self, dist: MutableMapping[Node, Domain], get_weight: Callable[[Edge], Domain]) -> bool`:
|
|
48
|
+
This method performs one iteration of Bellman-Ford algorithm to relax all edges in the graph and update the shortest
|
|
49
|
+
distances to their neighbors. It returns a boolean value indicating if any changes were made during this iteration.
|
|
50
|
+
3. `howard(self, dist: MutableMapping[Node, Domain], get_weight: Callable[[Edge], Domain]) -> Generator[Cycle, None, None]`:
|
|
51
|
+
This method finds negative cycles in the graph using the Howard's algorithm and returns a generator that yields a
|
|
52
|
+
list of edges for each cycle.
|
|
53
|
+
4. `cycle_list(self, handle: Node) -> Cycle`:
|
|
54
|
+
This method returns a list of edges that form a cycle in the graph, starting from a given node.
|
|
55
|
+
5. `is_negative(self, handle: Node, dist: MutableMapping[Node, Domain], get_weight: Callable[[Edge], Domain]) -> bool`:
|
|
56
|
+
This method checks if a cycle is negative by comparing the distances between nodes and the weights of the edges.
|
|
57
|
+
|
|
58
|
+
Here's a brief explanation of the algorithms used in this code:
|
|
59
|
+
|
|
60
|
+
1. Bellman-Ford Algorithm: It is a shortest path algorithm that can find single source shortest paths in a graph with
|
|
61
|
+
negative edge weights. It runs in O(V*E) time complexity.
|
|
62
|
+
2. Howard's Policy Graph Algorithm: It is used to find cycles in a directed graph and is based on the Bellman-Ford
|
|
63
|
+
Algorithm. It runs in O(V*E + V*E^2) time complexity in the worst case.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
pred: Dict[Node, Tuple[Node, Edge]] = {}
|
|
67
|
+
|
|
68
|
+
def __init__(self, digraph: Mapping[Node, Mapping[Node, Edge]]) -> None:
|
|
69
|
+
"""
|
|
70
|
+
The function initializes a graph object with an adjacency list.
|
|
71
|
+
|
|
72
|
+
:param digraph: The parameter `digraph` is a mapping that represents an adjacency list. It is a
|
|
73
|
+
dictionary-like object where the keys are nodes and the values are mappings of nodes to edges. Each
|
|
74
|
+
edge represents a connection between two nodes in a directed graph
|
|
75
|
+
|
|
76
|
+
:type digraph: Mapping[Node, Mapping[Node, Edge]]
|
|
77
|
+
"""
|
|
78
|
+
self.digraph = digraph
|
|
79
|
+
|
|
80
|
+
def find_cycle(self) -> Generator[Node, None, None]:
|
|
81
|
+
"""
|
|
82
|
+
The `find_cycle` function is used to find a cycle in a policy graph and yields the start node of the cycle.
|
|
83
|
+
|
|
84
|
+
Yields:
|
|
85
|
+
Generator[Node, None, None]: a start node of the cycle
|
|
86
|
+
|
|
87
|
+
Examples:
|
|
88
|
+
>>> digraph = {
|
|
89
|
+
... "a0": {"a1": 7, "a2": 5},
|
|
90
|
+
... "a1": {"a0": 0, "a2": 3},
|
|
91
|
+
... "a2": {"a1": 1, "a0": 2},
|
|
92
|
+
... }
|
|
93
|
+
>>> finder = NegCycleFinder(digraph)
|
|
94
|
+
>>> for cycle in finder.find_cycle():
|
|
95
|
+
... print(cycle)
|
|
96
|
+
"""
|
|
97
|
+
visited: Dict[Node, Node] = {}
|
|
98
|
+
for vtx in filter(lambda vtx: vtx not in visited, self.digraph):
|
|
99
|
+
utx = vtx
|
|
100
|
+
visited[utx] = vtx
|
|
101
|
+
while utx in self.pred:
|
|
102
|
+
utx, _ = self.pred[utx]
|
|
103
|
+
if utx in visited:
|
|
104
|
+
if visited[utx] == vtx:
|
|
105
|
+
yield utx
|
|
106
|
+
break
|
|
107
|
+
visited[utx] = vtx
|
|
108
|
+
|
|
109
|
+
def relax(
|
|
110
|
+
self,
|
|
111
|
+
dist: MutableMapping[Node, Domain],
|
|
112
|
+
get_weight: Callable[[Edge], Domain],
|
|
113
|
+
) -> bool:
|
|
114
|
+
"""
|
|
115
|
+
The `relax` function updates the `dist` and `pred` dictionaries based on the current distances and
|
|
116
|
+
weights of edges in a graph.
|
|
117
|
+
|
|
118
|
+
:param dist: `dist` is a mutable mapping that represents the current distances from a source node to
|
|
119
|
+
all other nodes in a graph. It is a mapping from nodes to their corresponding distances
|
|
120
|
+
|
|
121
|
+
:type dist: MutableMapping[Node, Domain]
|
|
122
|
+
|
|
123
|
+
:param get_weight: The `get_weight` parameter is a callable function that takes an `Edge` object as
|
|
124
|
+
input and returns a value of type `Domain`. This function is used to calculate the weight or cost
|
|
125
|
+
associated with an edge in the graph
|
|
126
|
+
|
|
127
|
+
:type get_weight: Callable[[Edge], Domain]
|
|
128
|
+
|
|
129
|
+
:return: a boolean value indicating whether any changes were made to the `dist` mapping and `pred` dictionary.
|
|
130
|
+
"""
|
|
131
|
+
changed = False
|
|
132
|
+
for utx, neighbors in self.digraph.items():
|
|
133
|
+
for vtx, edge in neighbors.items():
|
|
134
|
+
distance = dist[utx] + get_weight(edge)
|
|
135
|
+
if dist[vtx] > distance:
|
|
136
|
+
dist[vtx] = distance
|
|
137
|
+
self.pred[vtx] = (utx, edge)
|
|
138
|
+
changed = True
|
|
139
|
+
return changed
|
|
140
|
+
|
|
141
|
+
def cycle_list(self, handle: Node) -> Cycle:
|
|
142
|
+
"""
|
|
143
|
+
The `cycle_list` function returns a list of edges that form a cycle in a graph, starting from a given node.
|
|
144
|
+
|
|
145
|
+
:param handle: The `handle` parameter is a reference to a node in a graph. It represents the
|
|
146
|
+
starting point of the cycle in the list
|
|
147
|
+
|
|
148
|
+
:type handle: Node
|
|
149
|
+
|
|
150
|
+
:return: a list called "cycle".
|
|
151
|
+
"""
|
|
152
|
+
vtx = handle
|
|
153
|
+
cycle = list()
|
|
154
|
+
while True:
|
|
155
|
+
utx, edge = self.pred[vtx]
|
|
156
|
+
cycle.append(edge)
|
|
157
|
+
vtx = utx
|
|
158
|
+
if vtx == handle:
|
|
159
|
+
break
|
|
160
|
+
return cycle
|
|
161
|
+
|
|
162
|
+
def is_negative(
|
|
163
|
+
self,
|
|
164
|
+
handle: Node,
|
|
165
|
+
dist: MutableMapping[Node, Domain],
|
|
166
|
+
get_weight: Callable[[Edge], Domain],
|
|
167
|
+
) -> bool:
|
|
168
|
+
"""
|
|
169
|
+
The `is_negative` function checks if a cycle list is negative by comparing the distances between
|
|
170
|
+
nodes and the weights of the edges.
|
|
171
|
+
|
|
172
|
+
:param handle: The `handle` parameter is a `Node` object that represents a vertex in a graph. It is
|
|
173
|
+
used as a starting point to check for negative cycles in the graph
|
|
174
|
+
|
|
175
|
+
:type handle: Node
|
|
176
|
+
|
|
177
|
+
:param dist: `dist` is a mutable mapping that maps each node to its corresponding domain value. The
|
|
178
|
+
domain value represents the distance from the starting node to the current node in a graph
|
|
179
|
+
|
|
180
|
+
:type dist: MutableMapping[Node, Domain]
|
|
181
|
+
|
|
182
|
+
:param get_weight: The `get_weight` parameter is a callable function that takes an `Edge` object as
|
|
183
|
+
input and returns the weight of that edge
|
|
184
|
+
|
|
185
|
+
:type get_weight: Callable[[Edge], Domain]
|
|
186
|
+
|
|
187
|
+
:return: a boolean value.
|
|
188
|
+
"""
|
|
189
|
+
vtx = handle
|
|
190
|
+
# do while loop in C++
|
|
191
|
+
while True:
|
|
192
|
+
utx, edge = self.pred[vtx]
|
|
193
|
+
if dist[vtx] > dist[utx] + get_weight(edge):
|
|
194
|
+
return True
|
|
195
|
+
vtx = utx
|
|
196
|
+
if vtx == handle:
|
|
197
|
+
break
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
def howard(
|
|
201
|
+
self,
|
|
202
|
+
dist: MutableMapping[Node, Domain],
|
|
203
|
+
get_weight: Callable[[Edge], Domain],
|
|
204
|
+
) -> Generator[Cycle, None, None]:
|
|
205
|
+
"""
|
|
206
|
+
The `howard` function finds negative cycles in a graph and yields a list of cycles.
|
|
207
|
+
|
|
208
|
+
:param dist: `dist` is a mutable mapping that maps each node in the graph to a domain value. The
|
|
209
|
+
domain value represents the distance or cost from the source node to that particular node
|
|
210
|
+
|
|
211
|
+
:type dist: MutableMapping[Node, Domain]
|
|
212
|
+
|
|
213
|
+
:param get_weight: The `get_weight` parameter is a callable function that takes an `Edge` object as
|
|
214
|
+
input and returns the weight of that edge
|
|
215
|
+
|
|
216
|
+
:type get_weight: Callable[[Edge], Domain]
|
|
217
|
+
|
|
218
|
+
Examples:
|
|
219
|
+
>>> digraph = {
|
|
220
|
+
... "a0": {"a1": 7, "a2": 5},
|
|
221
|
+
... "a1": {"a0": 0, "a2": 3},
|
|
222
|
+
... "a2": {"a1": 1, "a0": 2},
|
|
223
|
+
... }
|
|
224
|
+
>>> dist = {vtx: 0 for vtx in digraph}
|
|
225
|
+
>>> finder = NegCycleFinder(digraph)
|
|
226
|
+
>>> has_neg = False
|
|
227
|
+
>>> for _ in finder.howard(dist, lambda edge: edge):
|
|
228
|
+
... has_neg = True
|
|
229
|
+
... break
|
|
230
|
+
...
|
|
231
|
+
>>> has_neg
|
|
232
|
+
False
|
|
233
|
+
"""
|
|
234
|
+
self.pred = {}
|
|
235
|
+
found = False
|
|
236
|
+
while not found and self.relax(dist, get_weight):
|
|
237
|
+
for vtx in self.find_cycle():
|
|
238
|
+
# Will zero cycle be found???
|
|
239
|
+
assert self.is_negative(vtx, dist, get_weight)
|
|
240
|
+
found = True
|
|
241
|
+
yield self.cycle_list(vtx)
|
digraphx/neg_cycle_q.py
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Negative Cycle Finder (neg_cycle_q.py)
|
|
3
|
+
|
|
4
|
+
This code implements a Negative Cycle Finder for directed graphs using Howard's method. The purpose of this code is to detect and find negative cycles in a directed graph. A negative cycle is a cycle in the graph where the sum of the edge weights is negative.
|
|
5
|
+
|
|
6
|
+
The main input for this code is a directed graph, represented as a mapping of nodes to their neighboring nodes and the edges connecting them. The graph is passed to the NegCycleFinder class when it's initialized.
|
|
7
|
+
|
|
8
|
+
The output of this code is a list of cycles (if any negative cycles are found). Each cycle is represented as a list of edges that form the negative cycle.
|
|
9
|
+
|
|
10
|
+
The code achieves its purpose through an algorithm called Howard's method, which is a minimum cycle ratio (MCR) algorithm. It works by maintaining a set of candidate cycles and iteratively updating them until it finds the minimum cycle ratio or detects a negative cycle.
|
|
11
|
+
|
|
12
|
+
The main logic flow of the algorithm involves two key operations: relaxation and cycle detection. The relaxation process updates the distances between nodes based on the edge weights. This is done in two ways: predecessor relaxation (relax_pred) and successor relaxation (relax_succ). The cycle detection part (find_cycle) looks for cycles in the graph based on the current set of predecessors or successors.
|
|
13
|
+
|
|
14
|
+
The howard_pred and howard_succ methods combine these operations. They repeatedly perform relaxation and then check for cycles. If a negative cycle is found, it's yielded as output.
|
|
15
|
+
|
|
16
|
+
An important data transformation happening in this code is the maintenance of the 'dist' dictionary, which keeps track of the distances between nodes. This dictionary is continuously updated during the relaxation process.
|
|
17
|
+
|
|
18
|
+
The code uses some advanced concepts like generic types and generator functions, but the core idea is straightforward: it's trying to find paths in the graph where going around in a circle results in a negative total weight, which shouldn't happen in many real-world scenarios (like currency exchange rates).
|
|
19
|
+
|
|
20
|
+
Overall, this code provides a tool for analyzing directed graphs and finding problematic cycles, which can be useful in various applications such as detecting arbitrage opportunities in currency exchange or finding inconsistencies in systems modeled as graphs.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from fractions import Fraction
|
|
24
|
+
from typing import (
|
|
25
|
+
Callable,
|
|
26
|
+
Dict,
|
|
27
|
+
Generator,
|
|
28
|
+
Generic,
|
|
29
|
+
List,
|
|
30
|
+
Mapping,
|
|
31
|
+
MutableMapping,
|
|
32
|
+
Tuple,
|
|
33
|
+
TypeVar,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
Node = TypeVar("Node") # Hashable
|
|
37
|
+
Edge = TypeVar("Edge") # Hashable
|
|
38
|
+
Domain = TypeVar("Domain", int, Fraction, float) # Comparable Ring
|
|
39
|
+
Cycle = List[Edge] # List of Edges
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# The `NegCycleFinder` class implements Howard's method, a minimum cycle ratio algorithm, to find
|
|
43
|
+
# negative cycles in a directed graph.
|
|
44
|
+
class NegCycleFinder(Generic[Node, Edge, Domain]):
|
|
45
|
+
"""Negative Cycle Finder by Howard's method
|
|
46
|
+
|
|
47
|
+
Howard's method is a minimum cycle ratio (MCR) algorithm that uses a policy
|
|
48
|
+
iteration algorithm to find the minimum cycle ratio of a directed graph. The
|
|
49
|
+
algorithm maintains a set of candidate cycles and iteratively updates the
|
|
50
|
+
cycle with the minimum ratio until convergence. To detect negative cycles,
|
|
51
|
+
Howard's method uses a cycle detection algorithm that is based on the
|
|
52
|
+
Bellman-Ford relaxation algorithm. Specifically, the algorithm maintains a
|
|
53
|
+
predecessor graph of the original graph and performs cycle detection on this
|
|
54
|
+
graph using the Bellman-Ford relaxation algorithm. If a negative cycle is
|
|
55
|
+
detected, the algorithm terminates and returns the cycle.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
pred: Dict[Node, Tuple[Node, Edge]] = {}
|
|
59
|
+
succ: Dict[Node, Tuple[Node, Edge]] = {}
|
|
60
|
+
|
|
61
|
+
def __init__(self, digraph: Mapping[Node, Mapping[Node, Edge]]) -> None:
|
|
62
|
+
"""
|
|
63
|
+
The function initializes a graph object with an adjacency list.
|
|
64
|
+
|
|
65
|
+
:param digraph: The parameter `digraph` is a mapping that represents an adjacency list. It is a
|
|
66
|
+
dictionary-like object where the keys are nodes and the values are mappings of nodes to edges. Each
|
|
67
|
+
edge represents a connection between two nodes in a directed graph
|
|
68
|
+
|
|
69
|
+
:type digraph: Mapping[Node, Mapping[Node, Edge]]
|
|
70
|
+
"""
|
|
71
|
+
self.digraph = digraph
|
|
72
|
+
|
|
73
|
+
def find_cycle(self, point_to) -> Generator[Node, None, None]:
|
|
74
|
+
"""
|
|
75
|
+
The `find_cycle` function is used to find a cycle in a policy graph and yields the start node of the
|
|
76
|
+
cycle.
|
|
77
|
+
|
|
78
|
+
:param point_to: The `point_to` parameter is a dictionary that represents the edges of a directed
|
|
79
|
+
graph. Each key-value pair in the dictionary represents an edge from the key vertex to the value
|
|
80
|
+
vertex
|
|
81
|
+
|
|
82
|
+
Yields:
|
|
83
|
+
Generator[Node, None, None]: a start node of the cycle
|
|
84
|
+
|
|
85
|
+
Examples:
|
|
86
|
+
>>> digraph = {
|
|
87
|
+
... "a0": {"a1": 7, "a2": 5},
|
|
88
|
+
... "a1": {"a0": 0, "a2": 3},
|
|
89
|
+
... "a2": {"a1": 1, "a0": 2},
|
|
90
|
+
... }
|
|
91
|
+
>>> finder = NegCycleFinder(digraph)
|
|
92
|
+
>>> for cycle in finder.find_cycle(finder.pred):
|
|
93
|
+
... print(cycle)
|
|
94
|
+
"""
|
|
95
|
+
visited: Dict[Node, Node] = {}
|
|
96
|
+
for vtx in filter(lambda vtx: vtx not in visited, self.digraph):
|
|
97
|
+
utx = vtx
|
|
98
|
+
while True:
|
|
99
|
+
visited[utx] = vtx
|
|
100
|
+
if utx not in point_to:
|
|
101
|
+
break
|
|
102
|
+
utx, _ = point_to[utx]
|
|
103
|
+
if utx in visited:
|
|
104
|
+
if visited[utx] == vtx:
|
|
105
|
+
yield utx
|
|
106
|
+
break
|
|
107
|
+
|
|
108
|
+
def relax_pred(
|
|
109
|
+
self,
|
|
110
|
+
dist: MutableMapping[Node, Domain],
|
|
111
|
+
get_weight: Callable[[Edge], Domain],
|
|
112
|
+
update_ok: Callable[[Domain, Domain], bool],
|
|
113
|
+
) -> bool:
|
|
114
|
+
"""
|
|
115
|
+
The `relax_pred` function updates the `dist` and `pred` dictionaries based on the current distances and
|
|
116
|
+
weights of edges in a graph.
|
|
117
|
+
|
|
118
|
+
:param dist: `dist` is a mutable mapping that represents the current distances from a source node to
|
|
119
|
+
all other nodes in a graph. It is a mapping from nodes to their corresponding distances
|
|
120
|
+
|
|
121
|
+
:type dist: MutableMapping[Node, Domain]
|
|
122
|
+
|
|
123
|
+
:param get_weight: The `get_weight` parameter is a callable function that takes an `Edge` object as
|
|
124
|
+
input and returns a value of type `Domain`. This function is used to calculate the weight or cost
|
|
125
|
+
associated with an edge in the graph
|
|
126
|
+
|
|
127
|
+
:type get_weight: Callable[[Edge], Domain]
|
|
128
|
+
|
|
129
|
+
:param update_ok: The `update_ok` parameter is a function that determines whether an update to the
|
|
130
|
+
distance `dist[vtx_v]` is allowed. It takes two arguments: the current value of `dist[vtx_v]` and
|
|
131
|
+
the new value `d`. It should return `True` if the update is
|
|
132
|
+
|
|
133
|
+
:return: a boolean value indicating whether any changes were made to the `dist` mapping and `pred` dictionary.
|
|
134
|
+
"""
|
|
135
|
+
changed = False
|
|
136
|
+
for utx, neighbors in self.digraph.items():
|
|
137
|
+
for vtx, edge in neighbors.items():
|
|
138
|
+
distance = dist[utx] + get_weight(edge)
|
|
139
|
+
if dist[vtx] > distance and update_ok(dist[vtx], distance):
|
|
140
|
+
dist[vtx] = distance
|
|
141
|
+
self.pred[vtx] = (utx, edge)
|
|
142
|
+
changed = True
|
|
143
|
+
return changed
|
|
144
|
+
|
|
145
|
+
def relax_succ(
|
|
146
|
+
self,
|
|
147
|
+
dist: MutableMapping[Node, Domain],
|
|
148
|
+
get_weight: Callable[[Edge], Domain],
|
|
149
|
+
update_ok: Callable[[Domain, Domain], bool],
|
|
150
|
+
) -> bool:
|
|
151
|
+
"""
|
|
152
|
+
The `relax_succ` function updates the `dist` and `succ` dictionaries based on the current distances and
|
|
153
|
+
weights of edges in a graph.
|
|
154
|
+
|
|
155
|
+
:param dist: `dist` is a mutable mapping that represents the current distances from a source node to
|
|
156
|
+
all other nodes in a graph. It is a mapping from nodes to their corresponding distances
|
|
157
|
+
|
|
158
|
+
:type dist: MutableMapping[Node, Domain]
|
|
159
|
+
|
|
160
|
+
:param get_weight: The `get_weight` parameter is a callable function that takes an `Edge` object as
|
|
161
|
+
input and returns a value of type `Domain`. This function is used to calculate the weight or cost
|
|
162
|
+
associated with an edge in the graph
|
|
163
|
+
|
|
164
|
+
:type get_weight: Callable[[Edge], Domain]
|
|
165
|
+
|
|
166
|
+
:param update_ok: The `update_ok` parameter is a function that determines whether an update to the
|
|
167
|
+
distance `dist[vtx_v]` is allowed. It takes two arguments: the current value of `dist[vtx_v]` and
|
|
168
|
+
the new value `d`. It should return `True` if the update is
|
|
169
|
+
|
|
170
|
+
:return: a boolean value indicating whether any changes were made to the `dist` mapping and `pred` dictionary.
|
|
171
|
+
"""
|
|
172
|
+
changed = False
|
|
173
|
+
for utx, neighbors in self.digraph.items():
|
|
174
|
+
for vtx, edge in neighbors.items():
|
|
175
|
+
distance = dist[vtx] - get_weight(edge)
|
|
176
|
+
if dist[utx] < distance and update_ok(dist[utx], distance):
|
|
177
|
+
dist[utx] = distance
|
|
178
|
+
self.succ[utx] = (vtx, edge)
|
|
179
|
+
changed = True
|
|
180
|
+
return changed
|
|
181
|
+
|
|
182
|
+
def howard_pred(
|
|
183
|
+
self,
|
|
184
|
+
dist: MutableMapping[Node, Domain],
|
|
185
|
+
get_weight: Callable[[Edge], Domain],
|
|
186
|
+
update_ok: Callable[[Domain, Domain], bool],
|
|
187
|
+
) -> Generator[Cycle, None, None]:
|
|
188
|
+
"""
|
|
189
|
+
The `howard_pred` function finds negative cycles in a graph and yields a list of cycles.
|
|
190
|
+
|
|
191
|
+
:param dist: `dist` is a mutable mapping that maps each node in the graph to a domain value. The
|
|
192
|
+
domain value represents the distance or cost from the source node to that particular node
|
|
193
|
+
|
|
194
|
+
:type dist: MutableMapping[Node, Domain]
|
|
195
|
+
|
|
196
|
+
:param get_weight: The `get_weight` parameter is a callable function that takes an `Edge` object as
|
|
197
|
+
input and returns the weight of that edge
|
|
198
|
+
|
|
199
|
+
:type get_weight: Callable[[Edge], Domain]
|
|
200
|
+
|
|
201
|
+
:param update_ok: The `update_ok` parameter is a callable function that determines whether an update
|
|
202
|
+
to the distance value of a vertex is allowed. It takes in three arguments: the current distance
|
|
203
|
+
value of the vertex, the weight of the edge being considered for update, and the current distance
|
|
204
|
+
value of the vertex at the other
|
|
205
|
+
|
|
206
|
+
Examples:
|
|
207
|
+
>>> digraph = {
|
|
208
|
+
... "a0": {"a1": 7, "a2": 5},
|
|
209
|
+
... "a1": {"a0": 0, "a2": 3},
|
|
210
|
+
... "a2": {"a1": 1, "a0": 2},
|
|
211
|
+
... }
|
|
212
|
+
>>> dist = {vtx: 0 for vtx in digraph}
|
|
213
|
+
>>> def update_ok(dist, v) : return True
|
|
214
|
+
>>> finder = NegCycleFinder(digraph)
|
|
215
|
+
>>> has_neg = False
|
|
216
|
+
>>> for _ in finder.howard_pred(dist, lambda edge: edge, update_ok):
|
|
217
|
+
... has_neg = True
|
|
218
|
+
... break
|
|
219
|
+
...
|
|
220
|
+
>>> has_neg
|
|
221
|
+
False
|
|
222
|
+
"""
|
|
223
|
+
self.pred = {}
|
|
224
|
+
found = False
|
|
225
|
+
while not found and self.relax_pred(dist, get_weight, update_ok):
|
|
226
|
+
for vtx in self.find_cycle(self.pred):
|
|
227
|
+
# Will zero cycle be found???
|
|
228
|
+
assert self.is_negative(vtx, dist, get_weight)
|
|
229
|
+
found = True
|
|
230
|
+
yield self.cycle_list(vtx, self.pred)
|
|
231
|
+
|
|
232
|
+
def howard_succ(
|
|
233
|
+
self,
|
|
234
|
+
dist: MutableMapping[Node, Domain],
|
|
235
|
+
get_weight: Callable[[Edge], Domain],
|
|
236
|
+
update_ok: Callable[[Domain, Domain], bool],
|
|
237
|
+
) -> Generator[Cycle, None, None]:
|
|
238
|
+
"""
|
|
239
|
+
The `howard_succ` function finds negative cycles in a graph and yields a list of cycles.
|
|
240
|
+
|
|
241
|
+
:param dist: `dist` is a mutable mapping that maps each node in the graph to a domain value. The
|
|
242
|
+
domain value represents the distance or cost from the source node to that particular node
|
|
243
|
+
|
|
244
|
+
:type dist: MutableMapping[Node, Domain]
|
|
245
|
+
|
|
246
|
+
:param get_weight: The `get_weight` parameter is a callable function that takes an `Edge` object as
|
|
247
|
+
input and returns the weight of that edge
|
|
248
|
+
|
|
249
|
+
:type get_weight: Callable[[Edge], Domain]
|
|
250
|
+
|
|
251
|
+
:param update_ok: The `update_ok` parameter is a callable function that determines whether an update
|
|
252
|
+
to the distance value of a vertex is allowed. It takes in three arguments: the current distance
|
|
253
|
+
value of the vertex, the weight of the edge being considered for update, and the current distance
|
|
254
|
+
value of the vertex at the other
|
|
255
|
+
|
|
256
|
+
Examples:
|
|
257
|
+
>>> digraph = {
|
|
258
|
+
... "a0": {"a1": 7, "a2": 5},
|
|
259
|
+
... "a1": {"a0": 0, "a2": 3},
|
|
260
|
+
... "a2": {"a1": 1, "a0": 2},
|
|
261
|
+
... }
|
|
262
|
+
>>> def update_ok(dist, v) : return True
|
|
263
|
+
>>> dist = {vtx: 0 for vtx in digraph}
|
|
264
|
+
>>> finder = NegCycleFinder(digraph)
|
|
265
|
+
>>> has_neg = False
|
|
266
|
+
>>> for _ in finder.howard_succ(dist, lambda edge: edge, update_ok):
|
|
267
|
+
... has_neg = True
|
|
268
|
+
... break
|
|
269
|
+
...
|
|
270
|
+
>>> has_neg
|
|
271
|
+
False
|
|
272
|
+
"""
|
|
273
|
+
self.succ = {}
|
|
274
|
+
found = False
|
|
275
|
+
while not found and self.relax_succ(dist, get_weight, update_ok):
|
|
276
|
+
for vtx in self.find_cycle(self.succ):
|
|
277
|
+
# Will zero cycle be found???
|
|
278
|
+
# assert self.is_negative(vtx, dist, get_weight)
|
|
279
|
+
found = True
|
|
280
|
+
yield self.cycle_list(vtx, self.succ)
|
|
281
|
+
|
|
282
|
+
def cycle_list(self, handle: Node, point_to) -> Cycle:
|
|
283
|
+
"""
|
|
284
|
+
The `cycle_list` function returns a list of edges that form a cycle in a graph, starting from a
|
|
285
|
+
given node.
|
|
286
|
+
|
|
287
|
+
:param handle: The `handle` parameter is a reference to a node in a graph. It represents the
|
|
288
|
+
starting point of the cycle in the list
|
|
289
|
+
|
|
290
|
+
:type handle: Node
|
|
291
|
+
|
|
292
|
+
:param point_to: point_to is a dictionary that maps each graph node to the node it points to
|
|
293
|
+
|
|
294
|
+
:return: a list of edges, which represents a cycle in a graph.
|
|
295
|
+
"""
|
|
296
|
+
vtx = handle
|
|
297
|
+
cycle = list()
|
|
298
|
+
while True:
|
|
299
|
+
utx, edge = point_to[vtx]
|
|
300
|
+
cycle.append(edge)
|
|
301
|
+
vtx = utx
|
|
302
|
+
if vtx == handle:
|
|
303
|
+
break
|
|
304
|
+
return cycle
|
|
305
|
+
|
|
306
|
+
def is_negative(
|
|
307
|
+
self,
|
|
308
|
+
handle: Node,
|
|
309
|
+
dist: MutableMapping[Node, Domain],
|
|
310
|
+
get_weight: Callable[[Edge], Domain],
|
|
311
|
+
) -> bool:
|
|
312
|
+
"""
|
|
313
|
+
The `is_negative` function checks if a cycle list is negative by comparing the distances between
|
|
314
|
+
nodes and the weights of the edges.
|
|
315
|
+
|
|
316
|
+
:param handle: The `handle` parameter is a `Node` object that represents a vertex in a graph. It is
|
|
317
|
+
used as a starting point to check for negative cycles in the graph
|
|
318
|
+
|
|
319
|
+
:type handle: Node
|
|
320
|
+
|
|
321
|
+
:param dist: `dist` is a mutable mapping that maps each node to its corresponding domain value. The
|
|
322
|
+
domain value represents the distance from the starting node to the current node in a graph
|
|
323
|
+
|
|
324
|
+
:type dist: MutableMapping[Node, Domain]
|
|
325
|
+
|
|
326
|
+
:param get_weight: The `get_weight` parameter is a callable function that takes an `Edge` object as
|
|
327
|
+
input and returns the weight of that edge
|
|
328
|
+
|
|
329
|
+
:type get_weight: Callable[[Edge], Domain]
|
|
330
|
+
|
|
331
|
+
:return: a boolean value.
|
|
332
|
+
"""
|
|
333
|
+
vtx = handle
|
|
334
|
+
# do while loop in C++
|
|
335
|
+
while True:
|
|
336
|
+
utx, edge = self.pred[vtx]
|
|
337
|
+
if dist[vtx] > dist[utx] + get_weight(edge):
|
|
338
|
+
return True
|
|
339
|
+
vtx = utx
|
|
340
|
+
if vtx == handle:
|
|
341
|
+
break
|
|
342
|
+
return False
|