digraphx 0.3__tar.gz → 0.4__tar.gz
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-0.3 → digraphx-0.4}/.github/workflows/ci.bak +1 -1
- {digraphx-0.3 → digraphx-0.4}/.github/workflows/python-app.yml +2 -2
- {digraphx-0.3/src/digraphx.egg-info → digraphx-0.4}/PKG-INFO +1 -1
- {digraphx-0.3 → digraphx-0.4}/src/digraphx/neg_cycle.py +42 -11
- {digraphx-0.3 → digraphx-0.4}/src/digraphx/neg_cycle_q.py +38 -14
- {digraphx-0.3 → digraphx-0.4/src/digraphx.egg-info}/PKG-INFO +1 -1
- digraphx-0.4/src/digraphx.egg-info/SOURCES.txt +77 -0
- {digraphx-0.3 → digraphx-0.4}/tests/test_delay_padding_example_howard.py +9 -2
- {digraphx-0.3 → digraphx-0.4}/tests/test_edge_cases.py +81 -20
- {digraphx-0.3 → digraphx-0.4}/tests/test_howard_cycle_cancellation.py +2 -2
- {digraphx-0.3 → digraphx-0.4}/tests/test_neg_cycle_q_simple_timing.py +3 -3
- {digraphx-0.3 → digraphx-0.4}/tests/test_neg_cycle_simple_timing.py +3 -3
- {digraphx-0.3 → digraphx-0.4}/tests/test_stress.py +8 -8
- {digraphx-0.3 → digraphx-0.4}/tests/test_timing_example_howard.py +1 -1
- digraphx-0.3/cpp_ai/.gitignore +0 -152
- digraphx-0.3/cpp_ai/CMakeLists.txt +0 -110
- digraphx-0.3/cpp_ai/README.md +0 -185
- digraphx-0.3/cpp_ai/examples/basic_usage.cpp +0 -126
- digraphx-0.3/cpp_ai/include/cppcoro/async_auto_reset_event.hpp +0 -89
- digraphx-0.3/cpp_ai/include/cppcoro/async_generator.hpp +0 -856
- digraphx-0.3/cpp_ai/include/cppcoro/async_latch.hpp +0 -62
- digraphx-0.3/cpp_ai/include/cppcoro/async_manual_reset_event.hpp +0 -95
- digraphx-0.3/cpp_ai/include/cppcoro/async_mutex.hpp +0 -174
- digraphx-0.3/cpp_ai/include/cppcoro/async_scope.hpp +0 -81
- digraphx-0.3/cpp_ai/include/cppcoro/awaitable_traits.hpp +0 -22
- digraphx-0.3/cpp_ai/include/cppcoro/broken_promise.hpp +0 -20
- digraphx-0.3/cpp_ai/include/cppcoro/cancellation_registration.hpp +0 -80
- digraphx-0.3/cpp_ai/include/cppcoro/cancellation_source.hpp +0 -65
- digraphx-0.3/cpp_ai/include/cppcoro/cancellation_token.hpp +0 -63
- digraphx-0.3/cpp_ai/include/cppcoro/config.hpp +0 -177
- digraphx-0.3/cpp_ai/include/cppcoro/coroutine.hpp +0 -35
- digraphx-0.3/cpp_ai/include/cppcoro/detail/any.hpp +0 -17
- digraphx-0.3/cpp_ai/include/cppcoro/detail/get_awaiter.hpp +0 -40
- digraphx-0.3/cpp_ai/include/cppcoro/detail/is_awaiter.hpp +0 -43
- digraphx-0.3/cpp_ai/include/cppcoro/detail/lightweight_manual_reset_event.hpp +0 -56
- digraphx-0.3/cpp_ai/include/cppcoro/detail/manual_lifetime.hpp +0 -101
- digraphx-0.3/cpp_ai/include/cppcoro/detail/remove_rvalue_reference.hpp +0 -19
- digraphx-0.3/cpp_ai/include/cppcoro/detail/sync_wait_task.hpp +0 -226
- digraphx-0.3/cpp_ai/include/cppcoro/detail/unwrap_reference.hpp +0 -22
- digraphx-0.3/cpp_ai/include/cppcoro/detail/void_value.hpp +0 -14
- digraphx-0.3/cpp_ai/include/cppcoro/detail/when_all_counter.hpp +0 -43
- digraphx-0.3/cpp_ai/include/cppcoro/detail/when_all_ready_awaitable.hpp +0 -170
- digraphx-0.3/cpp_ai/include/cppcoro/detail/when_all_task.hpp +0 -257
- digraphx-0.3/cpp_ai/include/cppcoro/detail/win32.hpp +0 -136
- digraphx-0.3/cpp_ai/include/cppcoro/detail/win32_overlapped_operation.hpp +0 -295
- digraphx-0.3/cpp_ai/include/cppcoro/file.hpp +0 -46
- digraphx-0.3/cpp_ai/include/cppcoro/file_buffering_mode.hpp +0 -27
- digraphx-0.3/cpp_ai/include/cppcoro/file_open_mode.hpp +0 -38
- digraphx-0.3/cpp_ai/include/cppcoro/file_read_operation.hpp +0 -75
- digraphx-0.3/cpp_ai/include/cppcoro/file_share_mode.hpp +0 -39
- digraphx-0.3/cpp_ai/include/cppcoro/file_write_operation.hpp +0 -74
- digraphx-0.3/cpp_ai/include/cppcoro/filesystem.hpp +0 -24
- digraphx-0.3/cpp_ai/include/cppcoro/fmap.hpp +0 -131
- digraphx-0.3/cpp_ai/include/cppcoro/generator.hpp +0 -187
- digraphx-0.3/cpp_ai/include/cppcoro/inline_scheduler.hpp +0 -19
- digraphx-0.3/cpp_ai/include/cppcoro/io_service.hpp +0 -277
- digraphx-0.3/cpp_ai/include/cppcoro/is_awaitable.hpp +0 -21
- digraphx-0.3/cpp_ai/include/cppcoro/multi_producer_sequencer.hpp +0 -721
- digraphx-0.3/cpp_ai/include/cppcoro/net/ip_address.hpp +0 -112
- digraphx-0.3/cpp_ai/include/cppcoro/net/ip_endpoint.hpp +0 -122
- digraphx-0.3/cpp_ai/include/cppcoro/net/ipv4_address.hpp +0 -91
- digraphx-0.3/cpp_ai/include/cppcoro/net/ipv4_endpoint.hpp +0 -54
- digraphx-0.3/cpp_ai/include/cppcoro/net/ipv6_address.hpp +0 -185
- digraphx-0.3/cpp_ai/include/cppcoro/net/ipv6_endpoint.hpp +0 -54
- digraphx-0.3/cpp_ai/include/cppcoro/net/socket.hpp +0 -241
- digraphx-0.3/cpp_ai/include/cppcoro/net/socket_accept_operation.hpp +0 -86
- digraphx-0.3/cpp_ai/include/cppcoro/net/socket_connect_operation.hpp +0 -75
- digraphx-0.3/cpp_ai/include/cppcoro/net/socket_disconnect_operation.hpp +0 -70
- digraphx-0.3/cpp_ai/include/cppcoro/net/socket_recv_from_operation.hpp +0 -83
- digraphx-0.3/cpp_ai/include/cppcoro/net/socket_recv_operation.hpp +0 -70
- digraphx-0.3/cpp_ai/include/cppcoro/net/socket_send_operation.hpp +0 -70
- digraphx-0.3/cpp_ai/include/cppcoro/net/socket_send_to_operation.hpp +0 -77
- digraphx-0.3/cpp_ai/include/cppcoro/on_scope_exit.hpp +0 -111
- digraphx-0.3/cpp_ai/include/cppcoro/operation_cancelled.hpp +0 -19
- digraphx-0.3/cpp_ai/include/cppcoro/read_only_file.hpp +0 -53
- digraphx-0.3/cpp_ai/include/cppcoro/read_write_file.hpp +0 -61
- digraphx-0.3/cpp_ai/include/cppcoro/readable_file.hpp +0 -54
- digraphx-0.3/cpp_ai/include/cppcoro/recursive_generator.hpp +0 -254
- digraphx-0.3/cpp_ai/include/cppcoro/resume_on.hpp +0 -105
- digraphx-0.3/cpp_ai/include/cppcoro/round_robin_scheduler.hpp +0 -96
- digraphx-0.3/cpp_ai/include/cppcoro/schedule_on.hpp +0 -59
- digraphx-0.3/cpp_ai/include/cppcoro/sequence_barrier.hpp +0 -401
- digraphx-0.3/cpp_ai/include/cppcoro/sequence_range.hpp +0 -99
- digraphx-0.3/cpp_ai/include/cppcoro/sequence_traits.hpp +0 -26
- digraphx-0.3/cpp_ai/include/cppcoro/shared_task.hpp +0 -388
- digraphx-0.3/cpp_ai/include/cppcoro/single_consumer_async_auto_reset_event.hpp +0 -86
- digraphx-0.3/cpp_ai/include/cppcoro/single_consumer_event.hpp +0 -102
- digraphx-0.3/cpp_ai/include/cppcoro/single_producer_sequencer.hpp +0 -204
- digraphx-0.3/cpp_ai/include/cppcoro/static_thread_pool.hpp +0 -106
- digraphx-0.3/cpp_ai/include/cppcoro/sync_wait.hpp +0 -33
- digraphx-0.3/cpp_ai/include/cppcoro/task.hpp +0 -374
- digraphx-0.3/cpp_ai/include/cppcoro/when_all.hpp +0 -84
- digraphx-0.3/cpp_ai/include/cppcoro/when_all_ready.hpp +0 -51
- digraphx-0.3/cpp_ai/include/cppcoro/writable_file.hpp +0 -61
- digraphx-0.3/cpp_ai/include/cppcoro/write_only_file.hpp +0 -60
- digraphx-0.3/cpp_ai/include/digraphx/min_cycle_ratio.hpp +0 -193
- digraphx-0.3/cpp_ai/include/digraphx/min_parmetric_q.hpp +0 -164
- digraphx-0.3/cpp_ai/include/digraphx/neg_cycle.hpp +0 -245
- digraphx-0.3/cpp_ai/include/digraphx/neg_cycle_q.hpp +0 -308
- digraphx-0.3/cpp_ai/include/digraphx/parametric.hpp +0 -139
- digraphx-0.3/cpp_ai/include/digraphx/tiny_digraph.hpp +0 -364
- digraphx-0.3/cpp_ai/include/digraphx/types.hpp +0 -198
- digraphx-0.3/cpp_ai/src/min_cycle_ratio.cpp +0 -7
- digraphx-0.3/cpp_ai/src/min_parmetric_q.cpp +0 -6
- digraphx-0.3/cpp_ai/src/neg_cycle.cpp +0 -4
- digraphx-0.3/cpp_ai/src/neg_cycle_q.cpp +0 -4
- digraphx-0.3/cpp_ai/src/parametric.cpp +0 -4
- digraphx-0.3/cpp_ai/src/tiny_digraph.cpp +0 -4
- digraphx-0.3/cpp_ai/tests/integration_tests.cpp +0 -156
- digraphx-0.3/cpp_ai/tests/test_neg_cycle.cpp +0 -134
- digraphx-0.3/cpp_ai/tests/test_tiny_digraph.cpp +0 -160
- digraphx-0.3/cpp_ai/xmake.lua +0 -69
- digraphx-0.3/rust_ai/.gitignore +0 -88
- digraphx-0.3/rust_ai/Cargo.toml +0 -22
- digraphx-0.3/rust_ai/examples/basic_usage.rs +0 -62
- digraphx-0.3/rust_ai/src/lib.rs +0 -49
- digraphx-0.3/rust_ai/src/min_cycle_ratio.rs +0 -215
- digraphx-0.3/rust_ai/src/min_parmetric_q.rs +0 -170
- digraphx-0.3/rust_ai/src/neg_cycle.rs +0 -344
- digraphx-0.3/rust_ai/src/neg_cycle_q.rs +0 -452
- digraphx-0.3/rust_ai/src/parametric.rs +0 -172
- digraphx-0.3/rust_ai/src/tiny_digraph.rs +0 -255
- digraphx-0.3/rust_ai/tests/integration_tests.rs +0 -49
- digraphx-0.3/src/digraphx.egg-info/SOURCES.txt +0 -186
- {digraphx-0.3 → digraphx-0.4}/.coveragerc +0 -0
- {digraphx-0.3 → digraphx-0.4}/.github/workflows/jekyll-gh-pages.yml +0 -0
- {digraphx-0.3 → digraphx-0.4}/.github/workflows/multi-platforms.yml +0 -0
- {digraphx-0.3 → digraphx-0.4}/.github/workflows/python-publish.yml +0 -0
- {digraphx-0.3 → digraphx-0.4}/.gitignore +0 -0
- {digraphx-0.3 → digraphx-0.4}/.isort.cfg +0 -0
- {digraphx-0.3 → digraphx-0.4}/.pre-commit-config.yaml +0 -0
- {digraphx-0.3 → digraphx-0.4}/.readthedocs.yml +0 -0
- {digraphx-0.3 → digraphx-0.4}/.vscode/settings.json +0 -0
- {digraphx-0.3 → digraphx-0.4}/AGENTS.md +0 -0
- {digraphx-0.3 → digraphx-0.4}/AUTHORS.md +0 -0
- {digraphx-0.3 → digraphx-0.4}/CHANGELOG.md +0 -0
- {digraphx-0.3 → digraphx-0.4}/CONTRIBUTING.md +0 -0
- {digraphx-0.3 → digraphx-0.4}/GEMINI.md +0 -0
- {digraphx-0.3 → digraphx-0.4}/IFLOW.md +0 -0
- {digraphx-0.3 → digraphx-0.4}/LICENSE +0 -0
- {digraphx-0.3 → digraphx-0.4}/LICENSE.txt +0 -0
- {digraphx-0.3 → digraphx-0.4}/README.md +0 -0
- {digraphx-0.3 → digraphx-0.4}/coverage.json +0 -0
- {digraphx-0.3 → digraphx-0.4}/docs/Makefile +0 -0
- {digraphx-0.3 → digraphx-0.4}/docs/_static/.gitignore +0 -0
- {digraphx-0.3 → digraphx-0.4}/docs/authors.md +0 -0
- {digraphx-0.3 → digraphx-0.4}/docs/changelog.md +0 -0
- {digraphx-0.3 → digraphx-0.4}/docs/conf.py +0 -0
- {digraphx-0.3 → digraphx-0.4}/docs/contributing.md +0 -0
- {digraphx-0.3 → digraphx-0.4}/docs/index.md +0 -0
- {digraphx-0.3 → digraphx-0.4}/docs/license.md +0 -0
- {digraphx-0.3 → digraphx-0.4}/docs/readme.md +0 -0
- {digraphx-0.3 → digraphx-0.4}/docs/requirements.txt +0 -0
- {digraphx-0.3 → digraphx-0.4}/environment.yml +0 -0
- {digraphx-0.3 → digraphx-0.4}/experiments/plot_node_colormap.ipynb +0 -0
- {digraphx-0.3 → digraphx-0.4}/experiments/plot_node_colormap.py +0 -0
- {digraphx-0.3 → digraphx-0.4}/mypy.ini +0 -0
- {digraphx-0.3 → digraphx-0.4}/note.md +0 -0
- {digraphx-0.3 → digraphx-0.4}/pyproject.toml +0 -0
- {digraphx-0.3 → digraphx-0.4}/requirements/README.md +0 -0
- {digraphx-0.3 → digraphx-0.4}/requirements/default.txt +0 -0
- {digraphx-0.3 → digraphx-0.4}/requirements/doc.txt +0 -0
- {digraphx-0.3 → digraphx-0.4}/requirements/test.txt +0 -0
- {digraphx-0.3 → digraphx-0.4}/requirements.txt +0 -0
- {digraphx-0.3 → digraphx-0.4}/setup.cfg +0 -0
- {digraphx-0.3 → digraphx-0.4}/setup.py +0 -0
- {digraphx-0.3 → digraphx-0.4}/src/digraphx/__init__.py +0 -0
- {digraphx-0.3 → digraphx-0.4}/src/digraphx/min_cycle_ratio.py +0 -0
- {digraphx-0.3 → digraphx-0.4}/src/digraphx/min_parametric_q.py +0 -0
- {digraphx-0.3 → digraphx-0.4}/src/digraphx/parametric.py +0 -0
- {digraphx-0.3 → digraphx-0.4}/src/digraphx/py.typed +0 -0
- {digraphx-0.3 → digraphx-0.4}/src/digraphx/tiny_digraph.py +0 -0
- {digraphx-0.3 → digraphx-0.4}/src/digraphx.egg-info/dependency_links.txt +0 -0
- {digraphx-0.3 → digraphx-0.4}/src/digraphx.egg-info/not-zip-safe +0 -0
- {digraphx-0.3 → digraphx-0.4}/src/digraphx.egg-info/requires.txt +0 -0
- {digraphx-0.3 → digraphx-0.4}/src/digraphx.egg-info/top_level.txt +0 -0
- {digraphx-0.3 → digraphx-0.4}/tests/__init__.py +0 -0
- {digraphx-0.3 → digraphx-0.4}/tests/conftest.py +0 -0
- {digraphx-0.3 → digraphx-0.4}/tests/test_cycle_ratio.py +0 -0
- {digraphx-0.3 → digraphx-0.4}/tests/test_min_cycle_ratio_property.py +0 -0
- {digraphx-0.3 → digraphx-0.4}/tests/test_min_parametric_q.py +0 -0
- {digraphx-0.3 → digraphx-0.4}/tests/test_neg_cycle.py +0 -0
- {digraphx-0.3 → digraphx-0.4}/tests/test_neg_cycle_q.py +0 -0
- {digraphx-0.3 → digraphx-0.4}/tests/test_parametric.py +0 -0
- {digraphx-0.3 → digraphx-0.4}/tests/test_timing_example.py +0 -0
- {digraphx-0.3 → digraphx-0.4}/tests/test_tiny_digraph.py +0 -0
- {digraphx-0.3 → digraphx-0.4}/tests/test_tiny_digraph_property.py +0 -0
- {digraphx-0.3 → digraphx-0.4}/tox.ini +0 -0
|
@@ -8,7 +8,7 @@ on:
|
|
|
8
8
|
push:
|
|
9
9
|
# Avoid using all the resources/limits available by checking only
|
|
10
10
|
# relevant branches and tags. Other branches can be checked via PRs.
|
|
11
|
-
branches: [main]
|
|
11
|
+
branches: [ main, dev ]
|
|
12
12
|
tags: ['v[0-9]*', '[0-9]+.[0-9]+*'] # Match tags that resemble a version
|
|
13
13
|
pull_request: # Run in every PR
|
|
14
14
|
workflow_dispatch: # Allow manually triggering the workflow
|
|
@@ -42,11 +42,14 @@ useful in many different applications.
|
|
|
42
42
|
"""
|
|
43
43
|
|
|
44
44
|
from fractions import Fraction
|
|
45
|
+
from math import floor
|
|
45
46
|
from typing import (
|
|
47
|
+
Any,
|
|
46
48
|
Callable,
|
|
47
49
|
Dict,
|
|
48
50
|
Generator,
|
|
49
51
|
Generic,
|
|
52
|
+
Iterable,
|
|
50
53
|
List,
|
|
51
54
|
Mapping,
|
|
52
55
|
MutableMapping,
|
|
@@ -62,6 +65,30 @@ Domain = TypeVar(
|
|
|
62
65
|
) # Numeric type for weights (must support comparison and arithmetic)
|
|
63
66
|
Cycle = List[Arc] # Alias for a list of edges forming a cycle
|
|
64
67
|
|
|
68
|
+
# NOTE: get_weight return type is `Any` (not `Domain`) to match C++ template
|
|
69
|
+
# semantics where the Callable return type is independent from the dist value
|
|
70
|
+
# type (Mapping). C++ `digraphx-cpp` uses unconstrained template parameters:
|
|
71
|
+
#
|
|
72
|
+
# template <typename Mapping, typename Callable>
|
|
73
|
+
# auto howard(Mapping& dist, Callable get_weight);
|
|
74
|
+
#
|
|
75
|
+
# The `Domain` TypeVar only constrains dist values; get_weight may return a
|
|
76
|
+
# different numeric type (e.g., float weight with int dist). When dist[u] is
|
|
77
|
+
# int and the sum is float, int(floor(...)) truncates like C++ implicit
|
|
78
|
+
# double→int conversion on assignment.
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _view_items(container: Iterable[Tuple[Node, Arc]]) -> Iterable[Tuple[Node, Arc]]:
|
|
82
|
+
"""Get a pair-iterable view of a neighbor container.
|
|
83
|
+
|
|
84
|
+
Mirrors C++ `_view_items` in digraphx-cpp/neg_cycle.hpp:
|
|
85
|
+
- For dict-like containers, use .items() to get (key, value) pairs
|
|
86
|
+
- For list-like containers, iterate directly (each element is a pair)
|
|
87
|
+
"""
|
|
88
|
+
if isinstance(container, dict):
|
|
89
|
+
return container.items()
|
|
90
|
+
return container
|
|
91
|
+
|
|
65
92
|
|
|
66
93
|
class NegCycleFinder(Generic[Node, Arc, Domain]):
|
|
67
94
|
"""Negative Cycle Finder by Howard's method
|
|
@@ -105,14 +132,14 @@ class NegCycleFinder(Generic[Node, Arc, Domain]):
|
|
|
105
132
|
# Dictionary to store predecessor information (node -> (predecessor_node, edge))
|
|
106
133
|
pred: Dict[Node, Tuple[Node, Arc]]
|
|
107
134
|
|
|
108
|
-
def __init__(self, digraph: Mapping[Node,
|
|
135
|
+
def __init__(self, digraph: Mapping[Node, Iterable[Tuple[Node, Arc]]]) -> None:
|
|
109
136
|
"""Initialize the negative cycle finder with a directed graph.
|
|
110
137
|
|
|
111
138
|
Args:
|
|
112
139
|
digraph: A mapping representing a directed graph where:
|
|
113
140
|
- Keys are source nodes
|
|
114
|
-
- Values are
|
|
115
|
-
Example: {u:
|
|
141
|
+
- Values are iterables of (target_node, edge) pairs
|
|
142
|
+
Example: {u: [(v, edge_uv), (w, edge_uw)], v: [(u, edge_vu)]}
|
|
116
143
|
"""
|
|
117
144
|
self.digraph = digraph
|
|
118
145
|
self.pred: Dict[Node, Tuple[Node, Arc]] = {}
|
|
@@ -155,7 +182,7 @@ class NegCycleFinder(Generic[Node, Arc, Domain]):
|
|
|
155
182
|
def relax(
|
|
156
183
|
self,
|
|
157
184
|
dist: MutableMapping[Node, Domain],
|
|
158
|
-
get_weight: Callable[[Arc],
|
|
185
|
+
get_weight: Callable[[Arc], Any],
|
|
159
186
|
) -> bool:
|
|
160
187
|
"""Perform one relaxation pass of the Bellman-Ford algorithm.
|
|
161
188
|
|
|
@@ -189,8 +216,11 @@ class NegCycleFinder(Generic[Node, Arc, Domain]):
|
|
|
189
216
|
"""
|
|
190
217
|
changed = False
|
|
191
218
|
for u_node, neighbors in self.digraph.items():
|
|
192
|
-
for v_node, edge in neighbors
|
|
193
|
-
|
|
219
|
+
for v_node, edge in _view_items(neighbors):
|
|
220
|
+
tmp = dist[u_node] + get_weight(edge)
|
|
221
|
+
if isinstance(dist[u_node], int) and isinstance(tmp, float):
|
|
222
|
+
tmp = int(floor(tmp))
|
|
223
|
+
distance = tmp
|
|
194
224
|
if dist[v_node] > distance: # Found a shorter path
|
|
195
225
|
dist[v_node] = distance
|
|
196
226
|
self.pred[v_node] = (u_node, edge) # Update predecessor
|
|
@@ -234,7 +264,7 @@ class NegCycleFinder(Generic[Node, Arc, Domain]):
|
|
|
234
264
|
self,
|
|
235
265
|
handle: Node,
|
|
236
266
|
dist: MutableMapping[Node, Domain],
|
|
237
|
-
get_weight: Callable[[Arc],
|
|
267
|
+
get_weight: Callable[[Arc], Any],
|
|
238
268
|
) -> bool:
|
|
239
269
|
"""Check if the cycle starting at 'handle' is negative.
|
|
240
270
|
|
|
@@ -268,9 +298,10 @@ class NegCycleFinder(Generic[Node, Arc, Domain]):
|
|
|
268
298
|
# do while loop in C++
|
|
269
299
|
while True:
|
|
270
300
|
u_node, edge = self.pred[v_node]
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
301
|
+
tmp = dist[u_node] + get_weight(edge)
|
|
302
|
+
if isinstance(dist[u_node], int) and isinstance(tmp, float):
|
|
303
|
+
tmp = int(floor(tmp))
|
|
304
|
+
if dist[v_node] > tmp: # Triangle inequality violated
|
|
274
305
|
return True
|
|
275
306
|
v_node = u_node
|
|
276
307
|
if v_node == handle: # Completed full cycle
|
|
@@ -280,7 +311,7 @@ class NegCycleFinder(Generic[Node, Arc, Domain]):
|
|
|
280
311
|
def howard(
|
|
281
312
|
self,
|
|
282
313
|
dist: MutableMapping[Node, Domain],
|
|
283
|
-
get_weight: Callable[[Arc],
|
|
314
|
+
get_weight: Callable[[Arc], Any],
|
|
284
315
|
) -> Generator[Cycle, None, None]:
|
|
285
316
|
"""Main algorithm to find negative cycles using Howard's method.
|
|
286
317
|
|
|
@@ -44,11 +44,14 @@ inconsistencies in systems modeled as graphs.
|
|
|
44
44
|
"""
|
|
45
45
|
|
|
46
46
|
from fractions import Fraction
|
|
47
|
+
from math import floor
|
|
47
48
|
from typing import (
|
|
49
|
+
Any,
|
|
48
50
|
Callable,
|
|
49
51
|
Dict,
|
|
50
52
|
Generator,
|
|
51
53
|
Generic,
|
|
54
|
+
Iterable,
|
|
52
55
|
List,
|
|
53
56
|
Mapping,
|
|
54
57
|
MutableMapping,
|
|
@@ -66,6 +69,18 @@ Domain = TypeVar("Domain", int, Fraction, float) # Comparable Ring
|
|
|
66
69
|
Cycle = List[Arc] # List of Arcs
|
|
67
70
|
|
|
68
71
|
|
|
72
|
+
def _view_items(container: Iterable[Tuple[Node, Arc]]) -> Iterable[Tuple[Node, Arc]]:
|
|
73
|
+
"""Get a pair-iterable view of a neighbor container.
|
|
74
|
+
|
|
75
|
+
Mirrors C++ `_view_items` in digraphx-cpp/neg_cycle_q.hpp:
|
|
76
|
+
- For dict-like containers, use .items() to get (key, value) pairs
|
|
77
|
+
- For list-like containers, iterate directly (each element is a pair)
|
|
78
|
+
"""
|
|
79
|
+
if isinstance(container, dict):
|
|
80
|
+
return container.items()
|
|
81
|
+
return container
|
|
82
|
+
|
|
83
|
+
|
|
69
84
|
class NegCycleFinderQ(Generic[Node, Arc, Domain]):
|
|
70
85
|
"""Negative Cycle Finder with constraints by Howard's method
|
|
71
86
|
|
|
@@ -90,14 +105,14 @@ class NegCycleFinderQ(Generic[Node, Arc, Domain]):
|
|
|
90
105
|
# Successor dictionary: maps each node to (successor_node, connecting_edge)
|
|
91
106
|
succ: Dict[Node, Tuple[Node, Arc]]
|
|
92
107
|
|
|
93
|
-
def __init__(self, digraph: Mapping[Node,
|
|
108
|
+
def __init__(self, digraph: Mapping[Node, Iterable[Tuple[Node, Arc]]]) -> None:
|
|
94
109
|
"""Initialize the negative cycle finder with a directed graph.
|
|
95
110
|
|
|
96
111
|
Args:
|
|
97
112
|
digraph: A directed graph represented as a nested mapping:
|
|
98
113
|
- Outer keys: source nodes
|
|
99
|
-
- Inner
|
|
100
|
-
Example: {u:
|
|
114
|
+
- Inner values: iterables of (target_node, edge) pairs
|
|
115
|
+
Example: {u: [(v, edge_uv), (w, edge_uw)], v: [(u, edge_vu)]}
|
|
101
116
|
"""
|
|
102
117
|
self.digraph = digraph
|
|
103
118
|
self.pred: Dict[Node, Tuple[Node, Arc]] = {}
|
|
@@ -147,7 +162,7 @@ class NegCycleFinderQ(Generic[Node, Arc, Domain]):
|
|
|
147
162
|
def relax_pred(
|
|
148
163
|
self,
|
|
149
164
|
dist: MutableMapping[Node, Domain],
|
|
150
|
-
get_weight: Callable[[Arc],
|
|
165
|
+
get_weight: Callable[[Arc], Any],
|
|
151
166
|
update_ok: Callable[[Domain, Domain], bool],
|
|
152
167
|
) -> bool:
|
|
153
168
|
"""Perform predecessor relaxation step (Bellman-Ford style).
|
|
@@ -181,8 +196,11 @@ class NegCycleFinderQ(Generic[Node, Arc, Domain]):
|
|
|
181
196
|
"""
|
|
182
197
|
changed = False
|
|
183
198
|
for u_node, neighbors in self.digraph.items():
|
|
184
|
-
for v_node, edge in neighbors
|
|
185
|
-
|
|
199
|
+
for v_node, edge in _view_items(neighbors):
|
|
200
|
+
tmp = dist[u_node] + get_weight(edge)
|
|
201
|
+
if isinstance(dist[u_node], int) and isinstance(tmp, float):
|
|
202
|
+
tmp = int(floor(tmp))
|
|
203
|
+
distance = tmp
|
|
186
204
|
if dist[v_node] > distance and update_ok(dist[v_node], distance):
|
|
187
205
|
dist[v_node] = distance
|
|
188
206
|
self.pred[v_node] = (u_node, edge) # Update predecessor
|
|
@@ -192,7 +210,7 @@ class NegCycleFinderQ(Generic[Node, Arc, Domain]):
|
|
|
192
210
|
def relax_succ(
|
|
193
211
|
self,
|
|
194
212
|
dist: MutableMapping[Node, Domain],
|
|
195
|
-
get_weight: Callable[[Arc],
|
|
213
|
+
get_weight: Callable[[Arc], Any],
|
|
196
214
|
update_ok: Callable[[Domain, Domain], bool],
|
|
197
215
|
) -> bool:
|
|
198
216
|
"""Perform successor relaxation step (reverse Bellman-Ford style).
|
|
@@ -223,8 +241,11 @@ class NegCycleFinderQ(Generic[Node, Arc, Domain]):
|
|
|
223
241
|
"""
|
|
224
242
|
changed = False
|
|
225
243
|
for u_node, neighbors in self.digraph.items():
|
|
226
|
-
for v_node, edge in neighbors
|
|
227
|
-
|
|
244
|
+
for v_node, edge in _view_items(neighbors):
|
|
245
|
+
tmp = dist[v_node] - get_weight(edge)
|
|
246
|
+
if isinstance(dist[v_node], int) and isinstance(tmp, float):
|
|
247
|
+
tmp = int(floor(tmp))
|
|
248
|
+
distance = tmp
|
|
228
249
|
if dist[u_node] < distance and update_ok(dist[u_node], distance):
|
|
229
250
|
dist[u_node] = distance
|
|
230
251
|
self.succ[u_node] = (v_node, edge) # Update successor
|
|
@@ -234,7 +255,7 @@ class NegCycleFinderQ(Generic[Node, Arc, Domain]):
|
|
|
234
255
|
def howard_pred(
|
|
235
256
|
self,
|
|
236
257
|
dist: MutableMapping[Node, Domain],
|
|
237
|
-
get_weight: Callable[[Arc],
|
|
258
|
+
get_weight: Callable[[Arc], Any],
|
|
238
259
|
update_ok: Callable[[Domain, Domain], bool],
|
|
239
260
|
) -> Generator[Cycle, None, None]:
|
|
240
261
|
"""Find negative cycles using predecessor-based Howard's algorithm.
|
|
@@ -282,7 +303,7 @@ class NegCycleFinderQ(Generic[Node, Arc, Domain]):
|
|
|
282
303
|
def howard_succ(
|
|
283
304
|
self,
|
|
284
305
|
dist: MutableMapping[Node, Domain],
|
|
285
|
-
get_weight: Callable[[Arc],
|
|
306
|
+
get_weight: Callable[[Arc], Any],
|
|
286
307
|
update_ok: Callable[[Domain, Domain], bool],
|
|
287
308
|
) -> Generator[Cycle, None, None]:
|
|
288
309
|
"""Find negative cycles using successor-based Howard's algorithm.
|
|
@@ -363,7 +384,7 @@ class NegCycleFinderQ(Generic[Node, Arc, Domain]):
|
|
|
363
384
|
self,
|
|
364
385
|
handle: Node,
|
|
365
386
|
dist: MutableMapping[Node, Domain],
|
|
366
|
-
get_weight: Callable[[Arc],
|
|
387
|
+
get_weight: Callable[[Arc], Any],
|
|
367
388
|
) -> bool:
|
|
368
389
|
"""Verify if the cycle starting at handle is negative.
|
|
369
390
|
|
|
@@ -396,8 +417,11 @@ class NegCycleFinderQ(Generic[Node, Arc, Domain]):
|
|
|
396
417
|
# C-style do-while loop
|
|
397
418
|
while True:
|
|
398
419
|
u_node, edge = self.pred[v_node]
|
|
399
|
-
|
|
400
|
-
|
|
420
|
+
tmp = dist[u_node] + get_weight(edge)
|
|
421
|
+
if isinstance(dist[u_node], int) and isinstance(tmp, float):
|
|
422
|
+
tmp = int(floor(tmp))
|
|
423
|
+
if dist[v_node] > tmp: # Found negative cycle
|
|
424
|
+
return True
|
|
401
425
|
v_node = u_node
|
|
402
426
|
if v_node == handle: # Completed full cycle
|
|
403
427
|
break
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
.coveragerc
|
|
2
|
+
.gitignore
|
|
3
|
+
.isort.cfg
|
|
4
|
+
.pre-commit-config.yaml
|
|
5
|
+
.readthedocs.yml
|
|
6
|
+
AGENTS.md
|
|
7
|
+
AUTHORS.md
|
|
8
|
+
CHANGELOG.md
|
|
9
|
+
CONTRIBUTING.md
|
|
10
|
+
GEMINI.md
|
|
11
|
+
IFLOW.md
|
|
12
|
+
LICENSE
|
|
13
|
+
LICENSE.txt
|
|
14
|
+
README.md
|
|
15
|
+
coverage.json
|
|
16
|
+
environment.yml
|
|
17
|
+
mypy.ini
|
|
18
|
+
note.md
|
|
19
|
+
pyproject.toml
|
|
20
|
+
requirements.txt
|
|
21
|
+
setup.cfg
|
|
22
|
+
setup.py
|
|
23
|
+
tox.ini
|
|
24
|
+
.github/workflows/ci.bak
|
|
25
|
+
.github/workflows/jekyll-gh-pages.yml
|
|
26
|
+
.github/workflows/multi-platforms.yml
|
|
27
|
+
.github/workflows/python-app.yml
|
|
28
|
+
.github/workflows/python-publish.yml
|
|
29
|
+
.vscode/settings.json
|
|
30
|
+
docs/Makefile
|
|
31
|
+
docs/authors.md
|
|
32
|
+
docs/changelog.md
|
|
33
|
+
docs/conf.py
|
|
34
|
+
docs/contributing.md
|
|
35
|
+
docs/index.md
|
|
36
|
+
docs/license.md
|
|
37
|
+
docs/readme.md
|
|
38
|
+
docs/requirements.txt
|
|
39
|
+
docs/_static/.gitignore
|
|
40
|
+
experiments/plot_node_colormap.ipynb
|
|
41
|
+
experiments/plot_node_colormap.py
|
|
42
|
+
requirements/README.md
|
|
43
|
+
requirements/default.txt
|
|
44
|
+
requirements/doc.txt
|
|
45
|
+
requirements/test.txt
|
|
46
|
+
src/digraphx/__init__.py
|
|
47
|
+
src/digraphx/min_cycle_ratio.py
|
|
48
|
+
src/digraphx/min_parametric_q.py
|
|
49
|
+
src/digraphx/neg_cycle.py
|
|
50
|
+
src/digraphx/neg_cycle_q.py
|
|
51
|
+
src/digraphx/parametric.py
|
|
52
|
+
src/digraphx/py.typed
|
|
53
|
+
src/digraphx/tiny_digraph.py
|
|
54
|
+
src/digraphx.egg-info/PKG-INFO
|
|
55
|
+
src/digraphx.egg-info/SOURCES.txt
|
|
56
|
+
src/digraphx.egg-info/dependency_links.txt
|
|
57
|
+
src/digraphx.egg-info/not-zip-safe
|
|
58
|
+
src/digraphx.egg-info/requires.txt
|
|
59
|
+
src/digraphx.egg-info/top_level.txt
|
|
60
|
+
tests/__init__.py
|
|
61
|
+
tests/conftest.py
|
|
62
|
+
tests/test_cycle_ratio.py
|
|
63
|
+
tests/test_delay_padding_example_howard.py
|
|
64
|
+
tests/test_edge_cases.py
|
|
65
|
+
tests/test_howard_cycle_cancellation.py
|
|
66
|
+
tests/test_min_cycle_ratio_property.py
|
|
67
|
+
tests/test_min_parametric_q.py
|
|
68
|
+
tests/test_neg_cycle.py
|
|
69
|
+
tests/test_neg_cycle_q.py
|
|
70
|
+
tests/test_neg_cycle_q_simple_timing.py
|
|
71
|
+
tests/test_neg_cycle_simple_timing.py
|
|
72
|
+
tests/test_parametric.py
|
|
73
|
+
tests/test_stress.py
|
|
74
|
+
tests/test_timing_example.py
|
|
75
|
+
tests/test_timing_example_howard.py
|
|
76
|
+
tests/test_tiny_digraph.py
|
|
77
|
+
tests/test_tiny_digraph_property.py
|
|
@@ -83,7 +83,7 @@ def test_even1() -> None:
|
|
|
83
83
|
"v2": {"v1": {"type": "h", "delay": 4}, "v3": {"type": "p", "delay": 0}},
|
|
84
84
|
"v3": {"v1": {"type": "s", "delay": TCP - 3}},
|
|
85
85
|
}
|
|
86
|
-
dist: Dict[str, float] = {"v1": 0, "v2": 0, "v3": 0}
|
|
86
|
+
dist: Dict[str, float] = {"v1": 0.0, "v2": 0.0, "v3": 0.0}
|
|
87
87
|
beta, num_iter, _ = even(digraph, 10, dist)
|
|
88
88
|
assert num_iter < 10
|
|
89
89
|
assert beta == approx(1.5)
|
|
@@ -108,7 +108,14 @@ def test_even2() -> None:
|
|
|
108
108
|
"v4": {"v3": {"type": "s", "delay": TCP - 8}, "v5": {"type": "h", "delay": 3}},
|
|
109
109
|
"v5": {"v4": {"type": "s", "delay": TCP - 3}},
|
|
110
110
|
}
|
|
111
|
-
dist: Dict[str, float] = {
|
|
111
|
+
dist: Dict[str, float] = {
|
|
112
|
+
"v0": 0.0,
|
|
113
|
+
"v1": 0.0,
|
|
114
|
+
"v2": 0.0,
|
|
115
|
+
"v3": 0.0,
|
|
116
|
+
"v4": 0.0,
|
|
117
|
+
"v5": 0.0,
|
|
118
|
+
}
|
|
112
119
|
beta, num_iter, _ = even(digraph, 10, dist)
|
|
113
120
|
print("slack02(s) = {}".format(TCP - 7 + dist["v2"] - dist["v0"]))
|
|
114
121
|
print("slack02(h) = {}".format(6 + dist["v0"] - dist["v2"]))
|
|
@@ -11,7 +11,7 @@ from digraphx.neg_cycle import NegCycleFinder
|
|
|
11
11
|
from digraphx.tiny_digraph import DiGraphAdapter, TinyDiGraph
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
def test_empty_graph_neg_cycle():
|
|
14
|
+
def test_empty_graph_neg_cycle() -> None:
|
|
15
15
|
"""Test negative cycle finder on completely empty graph."""
|
|
16
16
|
graph = DiGraphAdapter()
|
|
17
17
|
dist: Dict[str, Fraction] = {}
|
|
@@ -21,7 +21,7 @@ def test_empty_graph_neg_cycle():
|
|
|
21
21
|
assert len(cycles) == 0
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
def test_single_node_no_edges():
|
|
24
|
+
def test_single_node_no_edges() -> None:
|
|
25
25
|
"""Test graph with single node and no edges."""
|
|
26
26
|
graph = DiGraphAdapter()
|
|
27
27
|
graph.add_node(0)
|
|
@@ -32,7 +32,7 @@ def test_single_node_no_edges():
|
|
|
32
32
|
assert len(cycles) == 0
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
def test_single_node_self_loop_negative():
|
|
35
|
+
def test_single_node_self_loop_negative() -> None:
|
|
36
36
|
"""Test single node with negative self-loop."""
|
|
37
37
|
graph = DiGraphAdapter()
|
|
38
38
|
graph.add_edge(0, 0, weight=-1)
|
|
@@ -43,7 +43,7 @@ def test_single_node_self_loop_negative():
|
|
|
43
43
|
assert len(cycles) > 0
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
def test_single_node_self_loop_positive():
|
|
46
|
+
def test_single_node_self_loop_positive() -> None:
|
|
47
47
|
"""Test single node with positive self-loop."""
|
|
48
48
|
graph = DiGraphAdapter()
|
|
49
49
|
graph.add_edge(0, 0, weight=5)
|
|
@@ -54,7 +54,7 @@ def test_single_node_self_loop_positive():
|
|
|
54
54
|
assert len(cycles) == 0
|
|
55
55
|
|
|
56
56
|
|
|
57
|
-
def test_disconnected_components():
|
|
57
|
+
def test_disconnected_components() -> None:
|
|
58
58
|
"""Test graph with multiple disconnected components."""
|
|
59
59
|
graph = DiGraphAdapter()
|
|
60
60
|
|
|
@@ -75,7 +75,7 @@ def test_disconnected_components():
|
|
|
75
75
|
assert len(cycles) > 0
|
|
76
76
|
|
|
77
77
|
|
|
78
|
-
def test_tiny_digraph_init_zero_nodes():
|
|
78
|
+
def test_tiny_digraph_init_zero_nodes() -> None:
|
|
79
79
|
"""Test TinyDiGraph initialization with zero nodes."""
|
|
80
80
|
graph = TinyDiGraph()
|
|
81
81
|
graph.init_nodes(0)
|
|
@@ -84,7 +84,7 @@ def test_tiny_digraph_init_zero_nodes():
|
|
|
84
84
|
assert graph.number_of_edges() == 0
|
|
85
85
|
|
|
86
86
|
|
|
87
|
-
def test_tiny_digraph_add_edge_before_init():
|
|
87
|
+
def test_tiny_digraph_add_edge_before_init() -> None:
|
|
88
88
|
"""Test adding edge before initializing nodes."""
|
|
89
89
|
graph = TinyDiGraph()
|
|
90
90
|
|
|
@@ -92,7 +92,7 @@ def test_tiny_digraph_add_edge_before_init():
|
|
|
92
92
|
graph.add_edge(0, 1, weight=1)
|
|
93
93
|
|
|
94
94
|
|
|
95
|
-
def test_non_numeric_weights():
|
|
95
|
+
def test_non_numeric_weights() -> None:
|
|
96
96
|
"""Test that algorithm works with non-numeric weight types."""
|
|
97
97
|
graph = DiGraphAdapter()
|
|
98
98
|
graph.add_edge("a", "b", weight=Fraction(1, 2))
|
|
@@ -105,7 +105,7 @@ def test_non_numeric_weights():
|
|
|
105
105
|
assert len(cycles) > 0
|
|
106
106
|
|
|
107
107
|
|
|
108
|
-
def test_mixed_node_types():
|
|
108
|
+
def test_mixed_node_types() -> None:
|
|
109
109
|
"""Test graph with mixed node types (strings and ints)."""
|
|
110
110
|
graph = DiGraphAdapter()
|
|
111
111
|
graph.add_edge("a", 0, weight=1)
|
|
@@ -118,7 +118,7 @@ def test_mixed_node_types():
|
|
|
118
118
|
assert len(cycles) > 0
|
|
119
119
|
|
|
120
120
|
|
|
121
|
-
def test_zero_weight_cycle():
|
|
121
|
+
def test_zero_weight_cycle() -> None:
|
|
122
122
|
"""Test cycle with all zero weights."""
|
|
123
123
|
graph = DiGraphAdapter()
|
|
124
124
|
graph.add_edge(0, 1, weight=0)
|
|
@@ -131,7 +131,7 @@ def test_zero_weight_cycle():
|
|
|
131
131
|
assert len(cycles) == 0 # Zero cycle is not negative
|
|
132
132
|
|
|
133
133
|
|
|
134
|
-
def test_very_large_weights():
|
|
134
|
+
def test_very_large_weights() -> None:
|
|
135
135
|
"""Test with very large weight values."""
|
|
136
136
|
graph = DiGraphAdapter()
|
|
137
137
|
large_weight = 10**15
|
|
@@ -145,7 +145,7 @@ def test_very_large_weights():
|
|
|
145
145
|
assert len(cycles) > 0
|
|
146
146
|
|
|
147
147
|
|
|
148
|
-
def test_digraph_adapter_mixed_edge_attributes():
|
|
148
|
+
def test_digraph_adapter_mixed_edge_attributes() -> None:
|
|
149
149
|
"""Test DiGraphAdapter with mixed edge attributes."""
|
|
150
150
|
graph = DiGraphAdapter()
|
|
151
151
|
graph.add_edge("a", "b", weight=5, cost=10, time=2, label="edge1")
|
|
@@ -157,7 +157,7 @@ def test_digraph_adapter_mixed_edge_attributes():
|
|
|
157
157
|
assert graph["a"]["b"]["label"] == "edge1"
|
|
158
158
|
|
|
159
159
|
|
|
160
|
-
def test_min_cycle_ratio_empty_graph():
|
|
160
|
+
def test_min_cycle_ratio_empty_graph() -> None:
|
|
161
161
|
"""Test minimum cycle ratio solver on empty graph."""
|
|
162
162
|
graph = DiGraphAdapter()
|
|
163
163
|
dist: Dict[str, Fraction] = {}
|
|
@@ -170,7 +170,7 @@ def test_min_cycle_ratio_empty_graph():
|
|
|
170
170
|
assert len(cycle) == 0
|
|
171
171
|
|
|
172
172
|
|
|
173
|
-
def test_min_cycle_ratio_single_node():
|
|
173
|
+
def test_min_cycle_ratio_single_node() -> None:
|
|
174
174
|
"""Test minimum cycle ratio solver on single node graph."""
|
|
175
175
|
graph = DiGraphAdapter()
|
|
176
176
|
graph.add_edge("a", "a", cost=5, time=1)
|
|
@@ -183,7 +183,7 @@ def test_min_cycle_ratio_single_node():
|
|
|
183
183
|
assert isinstance(ratio, (Fraction, float))
|
|
184
184
|
|
|
185
185
|
|
|
186
|
-
def test_invalid_weight_extraction():
|
|
186
|
+
def test_invalid_weight_extraction() -> None:
|
|
187
187
|
"""Test handling of edges with missing weight attributes."""
|
|
188
188
|
graph = DiGraphAdapter()
|
|
189
189
|
graph.add_edge("a", "b", weight=5)
|
|
@@ -195,7 +195,7 @@ def test_invalid_weight_extraction():
|
|
|
195
195
|
assert len(cycles) == 0
|
|
196
196
|
|
|
197
197
|
|
|
198
|
-
def test_directed_graph_orientation():
|
|
198
|
+
def test_directed_graph_orientation() -> None:
|
|
199
199
|
"""Test that algorithm respects edge direction."""
|
|
200
200
|
graph = DiGraphAdapter()
|
|
201
201
|
graph.add_edge(0, 1, weight=1)
|
|
@@ -208,7 +208,7 @@ def test_directed_graph_orientation():
|
|
|
208
208
|
assert len(cycles) > 0 # 0->1->2->0 forms negative cycle
|
|
209
209
|
|
|
210
210
|
|
|
211
|
-
def test_multiple_edges_same_nodes():
|
|
211
|
+
def test_multiple_edges_same_nodes() -> None:
|
|
212
212
|
"""Test graph with multiple edges between same node pair (parallel edges)."""
|
|
213
213
|
graph = DiGraphAdapter()
|
|
214
214
|
# NetworkX typically replaces parallel edges, but test behavior
|
|
@@ -221,7 +221,7 @@ def test_multiple_edges_same_nodes():
|
|
|
221
221
|
assert len(cycles) == 0 # Single edge can't form cycle
|
|
222
222
|
|
|
223
223
|
|
|
224
|
-
def test_floating_point_weights():
|
|
224
|
+
def test_floating_point_weights() -> None:
|
|
225
225
|
"""Test with floating point weights."""
|
|
226
226
|
graph = DiGraphAdapter()
|
|
227
227
|
graph.add_edge(0, 1, weight=0.1)
|
|
@@ -234,7 +234,7 @@ def test_floating_point_weights():
|
|
|
234
234
|
assert len(cycles) > 0
|
|
235
235
|
|
|
236
236
|
|
|
237
|
-
def test_negative_self_loop():
|
|
237
|
+
def test_negative_self_loop() -> None:
|
|
238
238
|
"""Test negative self-loop detection."""
|
|
239
239
|
graph = DiGraphAdapter()
|
|
240
240
|
graph.add_edge(0, 1, weight=1)
|
|
@@ -247,7 +247,7 @@ def test_negative_self_loop():
|
|
|
247
247
|
assert len(cycles) > 0
|
|
248
248
|
|
|
249
249
|
|
|
250
|
-
def test_no_path_between_components():
|
|
250
|
+
def test_no_path_between_components() -> None:
|
|
251
251
|
"""Test that negative cycle in one component is detected even when other components exist."""
|
|
252
252
|
graph = DiGraphAdapter()
|
|
253
253
|
# Component 1: no cycle
|
|
@@ -262,3 +262,64 @@ def test_no_path_between_components():
|
|
|
262
262
|
finder = NegCycleFinder(graph)
|
|
263
263
|
cycles = list(finder.howard(dist, lambda edge: edge.get("weight", 1)))
|
|
264
264
|
assert len(cycles) > 0
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
# ── Multi-edge (parallel edge) tests ─────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def test_multi_edge_list_of_lists_no_cycle() -> None:
|
|
271
|
+
"""Multi-edge graph using list-of-lists (like C++ test_neg_cycle_raw.cpp).
|
|
272
|
+
|
|
273
|
+
Two parallel edges from node 2 to node 0, none forming a negative cycle.
|
|
274
|
+
"""
|
|
275
|
+
digraph = {
|
|
276
|
+
0: [(1, 7.0), (2, 5.0)],
|
|
277
|
+
1: [(0, 0.0), (2, 3.0)],
|
|
278
|
+
2: [(1, 1.0), (0, 2.0), (0, 1.0)], # two parallel edges 2→0
|
|
279
|
+
}
|
|
280
|
+
dist = {0: 0.0, 1: 0.0, 2: 0.0}
|
|
281
|
+
finder: NegCycleFinder[int, float, float] = NegCycleFinder(digraph)
|
|
282
|
+
cycles = list(finder.howard(dist, lambda w: w))
|
|
283
|
+
assert len(cycles) == 0
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def test_multi_edge_list_of_lists_with_cycle() -> None:
|
|
287
|
+
"""Multi-edge graph with a negative cycle formed by parallel edges."""
|
|
288
|
+
digraph = {
|
|
289
|
+
0: [(1, 1.0)],
|
|
290
|
+
1: [(2, 1.0)],
|
|
291
|
+
2: [(0, -3.0), (0, 2.0)], # one negative, one positive parallel edge
|
|
292
|
+
}
|
|
293
|
+
dist = {0: 0.0, 1: 0.0, 2: 0.0}
|
|
294
|
+
finder: NegCycleFinder[int, float, float] = NegCycleFinder(digraph)
|
|
295
|
+
cycles = list(finder.howard(dist, lambda w: w))
|
|
296
|
+
assert len(cycles) > 0
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def test_multi_edge_q_variant() -> None:
|
|
300
|
+
"""NegCycleFinderQ with multi-edge graph (predecessor variant)."""
|
|
301
|
+
from digraphx.neg_cycle_q import NegCycleFinderQ
|
|
302
|
+
|
|
303
|
+
digraph = {
|
|
304
|
+
0: [(1, 7.0), (2, 5.0)],
|
|
305
|
+
1: [(0, 0.0), (2, 3.0)],
|
|
306
|
+
2: [(1, 1.0), (0, 2.0), (0, 1.0)], # two parallel edges 2→0
|
|
307
|
+
}
|
|
308
|
+
dist = {0: 0.0, 1: 0.0, 2: 0.0}
|
|
309
|
+
update_ok = lambda old, new: True
|
|
310
|
+
finder: NegCycleFinderQ[int, float, float] = NegCycleFinderQ(digraph)
|
|
311
|
+
cycles = list(finder.howard_pred(dist, lambda w: w, update_ok))
|
|
312
|
+
assert len(cycles) == 0
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def test_multi_edge_dict_backward_compat() -> None:
|
|
316
|
+
"""Dict-based graphs still work (backward compatibility)."""
|
|
317
|
+
digraph = {
|
|
318
|
+
0: {1: 7.0, 2: 5.0},
|
|
319
|
+
1: {0: 0.0, 2: 3.0},
|
|
320
|
+
2: {1: 1.0, 0: 2.0},
|
|
321
|
+
}
|
|
322
|
+
dist = {0: 0.0, 1: 0.0, 2: 0.0}
|
|
323
|
+
finder: NegCycleFinder[int, float, float] = NegCycleFinder(digraph)
|
|
324
|
+
cycles = list(finder.howard(dist, lambda w: w))
|
|
325
|
+
assert len(cycles) == 0
|
|
@@ -31,7 +31,7 @@ def test_even() -> None:
|
|
|
31
31
|
"v2": {"v3": TCP - 3.0, "v1": 2.0},
|
|
32
32
|
"v3": {"v1": TCP - 4.0, "v2": 3.0},
|
|
33
33
|
}
|
|
34
|
-
dist: Dict[str, float] = {"v1": 0, "v2": 0, "v3": 0}
|
|
34
|
+
dist: Dict[str, float] = {"v1": 0.0, "v2": 0.0, "v3": 0.0}
|
|
35
35
|
beta, num_iter = even(digraph, 10, dist)
|
|
36
36
|
assert num_iter < 4
|
|
37
37
|
assert beta == approx(1.0)
|
|
@@ -76,7 +76,7 @@ def test_prop() -> None:
|
|
|
76
76
|
"v2": {"cost": 3.0, "time": 1.5},
|
|
77
77
|
},
|
|
78
78
|
}
|
|
79
|
-
dist: Dict[str, float] = {"v1": 0, "v2": 0, "v3": 0}
|
|
79
|
+
dist: Dict[str, float] = {"v1": 0.0, "v2": 0.0, "v3": 0.0}
|
|
80
80
|
beta, num_iter = prop(digraph, 10, dist)
|
|
81
81
|
assert num_iter < 5
|
|
82
82
|
assert beta == approx(0.3448275862068966)
|
|
@@ -22,17 +22,17 @@ def has_negative_cycle(TCP: float, dist: Dict[str, float]) -> bool:
|
|
|
22
22
|
|
|
23
23
|
def test_simple_timing_example1() -> None:
|
|
24
24
|
"""Creates a test graph for timing tests."""
|
|
25
|
-
dist: Dict[str, float] = {"v1": 0, "v2": 0, "v3": 0}
|
|
25
|
+
dist: Dict[str, float] = {"v1": 0.0, "v2": 0.0, "v3": 0.0}
|
|
26
26
|
assert has_negative_cycle(4.0, dist) is False
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
def test_simple_timing_example2() -> None:
|
|
30
30
|
"""Creates a test graph for timing tests."""
|
|
31
|
-
dist: Dict[str, float] = {"v1": 0, "v2": 0, "v3": 0}
|
|
31
|
+
dist: Dict[str, float] = {"v1": 0.0, "v2": 0.0, "v3": 0.0}
|
|
32
32
|
assert has_negative_cycle(2.0, dist) is True
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
def test_simple_timing_example3() -> None:
|
|
36
|
-
dist: Dict[str, float] = {"v1": 0, "v2": 0, "v3": 0}
|
|
36
|
+
dist: Dict[str, float] = {"v1": 0.0, "v2": 0.0, "v3": 0.0}
|
|
37
37
|
assert has_negative_cycle(3.0, dist) is False
|
|
38
38
|
assert dist == {"v1": 0, "v2": 1, "v3": 1}
|