digraphx 0.4__tar.gz → 0.5__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.4 → digraphx-0.5}/.github/workflows/ci.bak +2 -2
- {digraphx-0.4 → digraphx-0.5}/.github/workflows/multi-platforms.yml +1 -2
- {digraphx-0.4 → digraphx-0.5}/.github/workflows/python-app.yml +2 -3
- {digraphx-0.4 → digraphx-0.5}/.gitignore +4 -0
- digraphx-0.5/CHANGELOG.md +29 -0
- {digraphx-0.4/src/digraphx.egg-info → digraphx-0.5}/PKG-INFO +3 -2
- {digraphx-0.4 → digraphx-0.5}/docs/conf.py +14 -0
- digraphx-0.5/docs/examples/plot_cycle_detection.py +32 -0
- digraphx-0.5/docs/examples/plot_tiny_digraph.py +33 -0
- digraphx-0.5/docs/figures_demo.md +25 -0
- {digraphx-0.4 → digraphx-0.5}/docs/index.md +1 -0
- digraphx-0.5/experimental/__init__.py +5 -0
- digraphx-0.5/experimental/spare_tsv.py +190 -0
- digraphx-0.5/experimental/test_spare_tsv.py +170 -0
- digraphx-0.5/experiments/experi-descent-new.py +79 -0
- digraphx-0.5/experiments/experi-descent-old.py +72 -0
- digraphx-0.5/experiments/experi-descent.py +79 -0
- digraphx-0.5/experiments/experi.py +37 -0
- {digraphx-0.4 → digraphx-0.5}/mypy.ini +0 -3
- {digraphx-0.4 → digraphx-0.5}/setup.cfg +2 -3
- {digraphx-0.4 → digraphx-0.5}/src/digraphx/__init__.py +0 -2
- digraphx-0.5/src/digraphx/csr_digraph.py +160 -0
- digraphx-0.5/src/digraphx/mcf.py +353 -0
- {digraphx-0.4 → digraphx-0.5}/src/digraphx/neg_cycle.py +47 -65
- {digraphx-0.4 → digraphx-0.5}/src/digraphx/neg_cycle_q.py +55 -62
- {digraphx-0.4 → digraphx-0.5}/src/digraphx/tiny_digraph.py +133 -6
- {digraphx-0.4 → digraphx-0.5/src/digraphx.egg-info}/PKG-INFO +3 -2
- {digraphx-0.4 → digraphx-0.5}/src/digraphx.egg-info/SOURCES.txt +19 -2
- {digraphx-0.4 → digraphx-0.5}/src/digraphx.egg-info/requires.txt +2 -1
- digraphx-0.5/src/digraphx.egg-info/scm_file_list.json +90 -0
- digraphx-0.5/src/digraphx.egg-info/scm_version.json +8 -0
- digraphx-0.5/tests/test_coverage_extras.py +51 -0
- digraphx-0.5/tests/test_csr_digraph.py +350 -0
- {digraphx-0.4 → digraphx-0.5}/tests/test_delay_padding_example_howard.py +19 -13
- {digraphx-0.4 → digraphx-0.5}/tests/test_edge_cases.py +56 -56
- digraphx-0.5/tests/test_mcf.py +86 -0
- digraphx-0.5/tests/test_min_cycle_ratio_extra.py +95 -0
- digraphx-0.5/tests/test_min_parametric_q_extra.py +97 -0
- digraphx-0.4/CHANGELOG.md +0 -7
- digraphx-0.4/IFLOW.md +0 -103
- digraphx-0.4/LICENSE +0 -21
- {digraphx-0.4 → digraphx-0.5}/.coveragerc +0 -0
- {digraphx-0.4 → digraphx-0.5}/.github/workflows/jekyll-gh-pages.yml +0 -0
- {digraphx-0.4 → digraphx-0.5}/.github/workflows/python-publish.yml +0 -0
- {digraphx-0.4 → digraphx-0.5}/.isort.cfg +0 -0
- {digraphx-0.4 → digraphx-0.5}/.pre-commit-config.yaml +0 -0
- {digraphx-0.4 → digraphx-0.5}/.readthedocs.yml +0 -0
- {digraphx-0.4 → digraphx-0.5}/.vscode/settings.json +0 -0
- {digraphx-0.4 → digraphx-0.5}/AGENTS.md +0 -0
- {digraphx-0.4 → digraphx-0.5}/AUTHORS.md +0 -0
- {digraphx-0.4 → digraphx-0.5}/CONTRIBUTING.md +0 -0
- {digraphx-0.4 → digraphx-0.5}/GEMINI.md +0 -0
- {digraphx-0.4 → digraphx-0.5}/LICENSE.txt +0 -0
- {digraphx-0.4 → digraphx-0.5}/README.md +0 -0
- {digraphx-0.4 → digraphx-0.5}/coverage.json +0 -0
- {digraphx-0.4 → digraphx-0.5}/docs/Makefile +0 -0
- {digraphx-0.4 → digraphx-0.5}/docs/_static/.gitignore +0 -0
- {digraphx-0.4 → digraphx-0.5}/docs/authors.md +0 -0
- {digraphx-0.4 → digraphx-0.5}/docs/changelog.md +0 -0
- {digraphx-0.4 → digraphx-0.5}/docs/contributing.md +0 -0
- {digraphx-0.4 → digraphx-0.5}/docs/license.md +0 -0
- {digraphx-0.4 → digraphx-0.5}/docs/readme.md +0 -0
- {digraphx-0.4 → digraphx-0.5}/docs/requirements.txt +0 -0
- {digraphx-0.4 → digraphx-0.5}/environment.yml +0 -0
- {digraphx-0.4 → digraphx-0.5}/experiments/plot_node_colormap.ipynb +0 -0
- {digraphx-0.4 → digraphx-0.5}/experiments/plot_node_colormap.py +0 -0
- {digraphx-0.4 → digraphx-0.5}/note.md +0 -0
- {digraphx-0.4 → digraphx-0.5}/pyproject.toml +0 -0
- {digraphx-0.4 → digraphx-0.5}/requirements/README.md +0 -0
- {digraphx-0.4 → digraphx-0.5}/requirements/default.txt +0 -0
- {digraphx-0.4 → digraphx-0.5}/requirements/doc.txt +0 -0
- {digraphx-0.4 → digraphx-0.5}/requirements/test.txt +0 -0
- {digraphx-0.4 → digraphx-0.5}/requirements.txt +0 -0
- {digraphx-0.4 → digraphx-0.5}/setup.py +0 -0
- {digraphx-0.4 → digraphx-0.5}/src/digraphx/min_cycle_ratio.py +0 -0
- {digraphx-0.4 → digraphx-0.5}/src/digraphx/min_parametric_q.py +0 -0
- {digraphx-0.4 → digraphx-0.5}/src/digraphx/parametric.py +0 -0
- {digraphx-0.4 → digraphx-0.5}/src/digraphx/py.typed +0 -0
- {digraphx-0.4 → digraphx-0.5}/src/digraphx.egg-info/dependency_links.txt +0 -0
- {digraphx-0.4 → digraphx-0.5}/src/digraphx.egg-info/not-zip-safe +0 -0
- {digraphx-0.4 → digraphx-0.5}/src/digraphx.egg-info/top_level.txt +0 -0
- {digraphx-0.4 → digraphx-0.5}/tests/__init__.py +0 -0
- {digraphx-0.4 → digraphx-0.5}/tests/conftest.py +0 -0
- {digraphx-0.4 → digraphx-0.5}/tests/test_cycle_ratio.py +0 -0
- {digraphx-0.4 → digraphx-0.5}/tests/test_howard_cycle_cancellation.py +0 -0
- {digraphx-0.4 → digraphx-0.5}/tests/test_min_cycle_ratio_property.py +0 -0
- {digraphx-0.4 → digraphx-0.5}/tests/test_min_parametric_q.py +0 -0
- {digraphx-0.4 → digraphx-0.5}/tests/test_neg_cycle.py +0 -0
- {digraphx-0.4 → digraphx-0.5}/tests/test_neg_cycle_q.py +0 -0
- {digraphx-0.4 → digraphx-0.5}/tests/test_neg_cycle_q_simple_timing.py +0 -0
- {digraphx-0.4 → digraphx-0.5}/tests/test_neg_cycle_simple_timing.py +0 -0
- {digraphx-0.4 → digraphx-0.5}/tests/test_parametric.py +0 -0
- {digraphx-0.4 → digraphx-0.5}/tests/test_stress.py +0 -0
- {digraphx-0.4 → digraphx-0.5}/tests/test_timing_example.py +0 -0
- {digraphx-0.4 → digraphx-0.5}/tests/test_timing_example_howard.py +0 -0
- {digraphx-0.4 → digraphx-0.5}/tests/test_tiny_digraph.py +0 -0
- {digraphx-0.4 → digraphx-0.5}/tests/test_tiny_digraph_property.py +0 -0
- {digraphx-0.4 → digraphx-0.5}/tox.ini +0 -0
|
@@ -60,8 +60,8 @@ jobs:
|
|
|
60
60
|
strategy:
|
|
61
61
|
matrix:
|
|
62
62
|
python:
|
|
63
|
-
- "3.
|
|
64
|
-
- "3.
|
|
63
|
+
- "3.10" # oldest Python supported by PSF
|
|
64
|
+
- "3.13" # newest Python that is stable
|
|
65
65
|
platform:
|
|
66
66
|
- ubuntu-latest
|
|
67
67
|
- macos-latest
|
|
@@ -8,7 +8,7 @@ jobs:
|
|
|
8
8
|
strategy:
|
|
9
9
|
matrix:
|
|
10
10
|
os: ["ubuntu-latest", "windows-latest"]
|
|
11
|
-
version: ["3.
|
|
11
|
+
version: ["3.10", "3.13"]
|
|
12
12
|
steps:
|
|
13
13
|
- uses: actions/checkout@v3
|
|
14
14
|
- uses: conda-incubator/setup-miniconda@v3
|
|
@@ -34,6 +34,5 @@ jobs:
|
|
|
34
34
|
conda install pip
|
|
35
35
|
conda install setuptools wheel setuptools_scm
|
|
36
36
|
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
|
37
|
-
pip install git+https://github.com/luk036/mywheel.git
|
|
38
37
|
pip install -e .
|
|
39
38
|
pytest
|
|
@@ -15,16 +15,15 @@ jobs:
|
|
|
15
15
|
|
|
16
16
|
steps:
|
|
17
17
|
- uses: actions/checkout@v2
|
|
18
|
-
- name: Set up Python 3.
|
|
18
|
+
- name: Set up Python 3.10
|
|
19
19
|
uses: actions/setup-python@v2
|
|
20
20
|
with:
|
|
21
|
-
python-version: 3.
|
|
21
|
+
python-version: "3.10"
|
|
22
22
|
- name: Install dependencies
|
|
23
23
|
run: |
|
|
24
24
|
python -m pip install --upgrade pip
|
|
25
25
|
pip install flake8 pytest
|
|
26
26
|
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
|
27
|
-
pip install git+https://github.com/luk036/mywheel.git
|
|
28
27
|
python setup.py develop
|
|
29
28
|
# - name: Lint with flake8
|
|
30
29
|
# run: |
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Version 0.5 (2026-07-16)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
- **MCF solver**: Added min-cost flow cycle-cancellation solver (`mcf.py`) and `NegCycleFinderQ` with `VertexFilter` for vertex-disjoint MCF constraint. (#582b1eb, #e6e248f)
|
|
7
|
+
- **CSRDiGraph**: Memory-efficient CSR-backed directed graph with `_CSRNeighbors` Mapping view — compact integer arrays instead of dicts. (#12465a3)
|
|
8
|
+
- **Bellman-Ford cycle detection**: Replaced Howard's algorithm with Bellman-Ford for negative cycle detection; fixed parallel edge bug. (#04da700)
|
|
9
|
+
- **spareTSV integration**: Added spare TSV network flow utilities and unified variable naming to `utx`/`vtx` (C++ convention). (#762008a)
|
|
10
|
+
- **Moved spare_tsv to experimental/**: Relocated matplotlib-dependent spare_tsv module out of main package to avoid CI failures when matplotlib is not installed. (#7367e6c)
|
|
11
|
+
|
|
12
|
+
### Performance
|
|
13
|
+
- **Lazy TinyDiGraph/CSRDiGraph**: Avoid pre-allocating 2N dicts per `init_nodes()` call (~464 MB saved for 1M nodes). (#12465a3)
|
|
14
|
+
- **Incremental residual updates**: Update only edges affected by cycle cancellation instead of rebuilding full residual each iteration. (#12465a3)
|
|
15
|
+
|
|
16
|
+
### Documentation
|
|
17
|
+
- **plot_directive and svgbob**: Enabled matplotlib plot_directive for auto-generated figures. Added TinyDiGraph and cycle detection example plots. Added svgbob diagram to TinyDiGraph class docstring. (#b64d424)
|
|
18
|
+
|
|
19
|
+
### Testing & Code Quality
|
|
20
|
+
- **Coverage raised 70%→95%**: Added extensive test coverage including MCF solver, CSRDiGraph, edge cases, and extra min-cycle-ratio tests. (#80da2a2, #9030d8a)
|
|
21
|
+
|
|
22
|
+
### Code Cleanup
|
|
23
|
+
- **Removed PyScaffold boilerplate**: Deleted `skeleton.py` and `test_skeleton.py`. (#9bdbdb1)
|
|
24
|
+
- **Dropped Python < 3.9 compat**: Removed `importlib-metadata` conditional dependency. (#9bdbdb1)
|
|
25
|
+
- **Config cleanup**: Removed dead entry points, unused mypy ignores, stale `IFLOW.md` and duplicate `LICENSE`. (#9bdbdb1)
|
|
26
|
+
|
|
27
|
+
### Build & CI
|
|
28
|
+
- **CI repair**: Fixed broken entry_points and remaining skeleton imports. (#91fd90e)
|
|
29
|
+
- **flake8 cleanup**: Removed unused imports and fixed formatting warnings. (#91d7141)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: digraphx
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5
|
|
4
4
|
Summary: Network Optimization Python Code
|
|
5
5
|
Home-page: https://github.com/luk036/digraphx
|
|
6
6
|
Author: Wai-Shing Luk
|
|
@@ -12,7 +12,8 @@ Classifier: Development Status :: 4 - Beta
|
|
|
12
12
|
Classifier: Programming Language :: Python
|
|
13
13
|
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
14
14
|
License-File: LICENSE.txt
|
|
15
|
-
Requires-Dist: importlib-metadata; python_version < "3.
|
|
15
|
+
Requires-Dist: importlib-metadata; python_version < "3.10"
|
|
16
|
+
Requires-Dist: mywheel>=0.3
|
|
16
17
|
Requires-Dist: networkx
|
|
17
18
|
Provides-Extra: testing
|
|
18
19
|
Requires-Dist: setuptools; extra == "testing"
|
|
@@ -83,6 +83,20 @@ extensions = [
|
|
|
83
83
|
"sphinx.ext.mathjax",
|
|
84
84
|
"sphinx.ext.napoleon",
|
|
85
85
|
"sphinxcontrib.svgbob",
|
|
86
|
+
"matplotlib.sphinxext.plot_directive",
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
# -- matplotlib.sphinxext.plot_directive configuration -----------------------
|
|
90
|
+
plot_html_show_source_link = True
|
|
91
|
+
plot_html_show_formats = False
|
|
92
|
+
plot_include_source = True
|
|
93
|
+
plot_working_directory = os.path.join(__location__, "examples")
|
|
94
|
+
plot_pre_code = [
|
|
95
|
+
"import matplotlib.pyplot as plt",
|
|
96
|
+
"import networkx as nx",
|
|
97
|
+
"import numpy as np",
|
|
98
|
+
"from digraphx.tiny_digraph import TinyDiGraph",
|
|
99
|
+
"plt.rcParams['figure.dpi'] = 100",
|
|
86
100
|
]
|
|
87
101
|
|
|
88
102
|
# Add any paths that contain templates here, relative to this directory.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Negative Cycle Detection
|
|
3
|
+
========================
|
|
4
|
+
|
|
5
|
+
Demonstrates negative cycle detection in a weighted directed graph.
|
|
6
|
+
"""
|
|
7
|
+
import matplotlib.pyplot as plt
|
|
8
|
+
import networkx as nx
|
|
9
|
+
|
|
10
|
+
g = nx.DiGraph()
|
|
11
|
+
g.add_edge(1, 2, w=2)
|
|
12
|
+
g.add_edge(2, 3, w=-3)
|
|
13
|
+
g.add_edge(3, 1, w=1)
|
|
14
|
+
g.add_edge(2, 4, w=1)
|
|
15
|
+
g.add_edge(4, 5, w=-1)
|
|
16
|
+
g.add_edge(5, 2, w=2)
|
|
17
|
+
|
|
18
|
+
plt.figure(figsize=(7, 5))
|
|
19
|
+
pos = nx.spring_layout(g, seed=42)
|
|
20
|
+
nx.draw(
|
|
21
|
+
g,
|
|
22
|
+
pos,
|
|
23
|
+
with_labels=True,
|
|
24
|
+
node_color="lightcoral",
|
|
25
|
+
node_size=600,
|
|
26
|
+
arrowsize=20,
|
|
27
|
+
font_weight="bold",
|
|
28
|
+
)
|
|
29
|
+
edge_labels = {(u, v): f"w={d['w']}" for u, v, d in g.edges(data=True)}
|
|
30
|
+
nx.draw_networkx_edge_labels(g, pos, edge_labels=edge_labels)
|
|
31
|
+
plt.title("Directed Graph with Potential Negative Cycles")
|
|
32
|
+
plt.grid(True, alpha=0.3)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TinyDiGraph visualization
|
|
3
|
+
=========================
|
|
4
|
+
|
|
5
|
+
Example of creating and visualizing a directed graph using TinyDiGraph.
|
|
6
|
+
"""
|
|
7
|
+
import matplotlib.pyplot as plt
|
|
8
|
+
import networkx as nx
|
|
9
|
+
|
|
10
|
+
from digraphx.tiny_digraph import TinyDiGraph
|
|
11
|
+
|
|
12
|
+
g = TinyDiGraph()
|
|
13
|
+
g.init_nodes(5)
|
|
14
|
+
g.add_edge(0, 1)
|
|
15
|
+
g.add_edge(1, 2)
|
|
16
|
+
g.add_edge(2, 3)
|
|
17
|
+
g.add_edge(3, 0)
|
|
18
|
+
g.add_edge(0, 4)
|
|
19
|
+
g.add_edge(4, 2)
|
|
20
|
+
|
|
21
|
+
plt.figure(figsize=(6, 5))
|
|
22
|
+
pos = nx.spring_layout(g, seed=42)
|
|
23
|
+
nx.draw(
|
|
24
|
+
g,
|
|
25
|
+
pos,
|
|
26
|
+
with_labels=True,
|
|
27
|
+
node_color="lightblue",
|
|
28
|
+
node_size=500,
|
|
29
|
+
arrowsize=20,
|
|
30
|
+
font_weight="bold",
|
|
31
|
+
)
|
|
32
|
+
plt.title("TinyDiGraph with 5 nodes and 6 edges")
|
|
33
|
+
plt.grid(True, alpha=0.3)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Figures Demo
|
|
2
|
+
|
|
3
|
+
Auto-generated figures demonstrating digraphx functionality.
|
|
4
|
+
|
|
5
|
+
## TinyDiGraph Example
|
|
6
|
+
|
|
7
|
+
```{plot} examples/plot_tiny_digraph.py
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Cycle Detection Example
|
|
11
|
+
|
|
12
|
+
```{plot} examples/plot_cycle_detection.py
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### Inline Plot Example
|
|
16
|
+
|
|
17
|
+
```{plot}
|
|
18
|
+
import matplotlib.pyplot as plt
|
|
19
|
+
import numpy as np
|
|
20
|
+
|
|
21
|
+
x = np.linspace(0, 2 * np.pi, 100)
|
|
22
|
+
plt.plot(x, np.sin(x))
|
|
23
|
+
plt.title("Simple Sine Wave")
|
|
24
|
+
plt.grid(True, alpha=0.3)
|
|
25
|
+
```
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Spare TSV network optimization utilities."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
import networkx as nx
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def vdc(n, base=2):
|
|
11
|
+
"""Van der Corput sequence."""
|
|
12
|
+
vdc, denom = 0.0, 1.0
|
|
13
|
+
while n:
|
|
14
|
+
denom *= base
|
|
15
|
+
n, remainder = divmod(n, base)
|
|
16
|
+
vdc += remainder / denom
|
|
17
|
+
return vdc
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def vdcorput(n, base=2):
|
|
21
|
+
"""Generate n Van der Corput vectors as a list."""
|
|
22
|
+
return [vdc(i, base) for i in range(n)]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def vdcorput_iter(n, base=2):
|
|
26
|
+
"""Generate n Van der Corput vectors lazily (generator).
|
|
27
|
+
|
|
28
|
+
Yields values one at a time instead of allocating a full list.
|
|
29
|
+
"""
|
|
30
|
+
for i in range(n):
|
|
31
|
+
yield vdc(i, base)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def formGraph(T, pos, mu, eta, seed=None):
|
|
35
|
+
"""Form N by N grid of nodes, perturb by mu and connect nodes within eta.
|
|
36
|
+
|
|
37
|
+
mu and eta are relative to 1/(N-1).
|
|
38
|
+
"""
|
|
39
|
+
if seed is not None:
|
|
40
|
+
np.random.seed(seed)
|
|
41
|
+
|
|
42
|
+
N = int(np.sqrt(T))
|
|
43
|
+
mu = mu / (N - 1)
|
|
44
|
+
eta = eta / (N - 1)
|
|
45
|
+
|
|
46
|
+
pos = dict(enumerate(pos))
|
|
47
|
+
n = len(pos)
|
|
48
|
+
|
|
49
|
+
gra = nx.random_geometric_graph(n, eta, pos=pos)
|
|
50
|
+
gra = nx.DiGraph(gra)
|
|
51
|
+
return gra
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def showPaths(gra, pos, N, edgeProbs=1.0, path=None, visibleNodes=None, guards=None):
|
|
55
|
+
"""Draw directed graph with optional path overlay.
|
|
56
|
+
|
|
57
|
+
Parameters
|
|
58
|
+
----------
|
|
59
|
+
gra : nx.DiGraph
|
|
60
|
+
Directed graph.
|
|
61
|
+
pos : dict
|
|
62
|
+
Node positions {node: (x, y)}.
|
|
63
|
+
N : int
|
|
64
|
+
Number of primal nodes (drawn cyan).
|
|
65
|
+
edgeProbs : float or list
|
|
66
|
+
Edge probability/weights for coloring.
|
|
67
|
+
path : list of edges
|
|
68
|
+
Edges to highlight in blue.
|
|
69
|
+
visibleNodes : list
|
|
70
|
+
Nodes to draw (default: all).
|
|
71
|
+
guards : list
|
|
72
|
+
Nodes to highlight with black dots.
|
|
73
|
+
"""
|
|
74
|
+
fig = plt.figure(figsize=(8, 6))
|
|
75
|
+
ax = fig.add_subplot(111, aspect="equal")
|
|
76
|
+
|
|
77
|
+
n = gra.number_of_nodes()
|
|
78
|
+
if visibleNodes is None:
|
|
79
|
+
visibleNodes = gra.nodes()
|
|
80
|
+
primalNodes = range(0, N)
|
|
81
|
+
spareNodes = range(N, n)
|
|
82
|
+
|
|
83
|
+
nx.draw_networkx_nodes(
|
|
84
|
+
gra, pos, nodelist=primalNodes, node_color="c", node_size=50, ax=ax
|
|
85
|
+
)
|
|
86
|
+
nx.draw_networkx_nodes(
|
|
87
|
+
gra, pos, nodelist=spareNodes, node_color="r", node_size=50, ax=ax
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if guards is not None:
|
|
91
|
+
nx.draw_networkx_nodes(
|
|
92
|
+
gra, pos, nodelist=guards, node_color=".0", node_size=100, ax=ax
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
alpha = 1.0 if path is None else 0.15
|
|
96
|
+
|
|
97
|
+
# only display edges between non-dummy nodes
|
|
98
|
+
edge_list = list(gra.edges())
|
|
99
|
+
visibleEdges = [
|
|
100
|
+
e
|
|
101
|
+
for e in edge_list
|
|
102
|
+
if e[0] in visibleNodes and e[1] in visibleNodes
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
if isinstance(edgeProbs, float):
|
|
106
|
+
edgeProbs = [edgeProbs] * gra.number_of_edges()
|
|
107
|
+
p = [edgeProbs[i] for i, e in enumerate(edge_list) if e in visibleEdges]
|
|
108
|
+
|
|
109
|
+
nx.draw_networkx_edges(
|
|
110
|
+
gra,
|
|
111
|
+
pos,
|
|
112
|
+
edge_color=p,
|
|
113
|
+
width=1,
|
|
114
|
+
edge_cmap=plt.cm.RdYlGn,
|
|
115
|
+
arrows=False,
|
|
116
|
+
edgelist=visibleEdges,
|
|
117
|
+
edge_vmin=0.0,
|
|
118
|
+
edge_vmax=1.0,
|
|
119
|
+
ax=ax,
|
|
120
|
+
alpha=alpha,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if path is not None:
|
|
124
|
+
nx.draw_networkx_edges(
|
|
125
|
+
gra,
|
|
126
|
+
pos,
|
|
127
|
+
edge_color="b",
|
|
128
|
+
width=1,
|
|
129
|
+
edge_cmap=plt.cm.RdYlGn,
|
|
130
|
+
edgelist=path,
|
|
131
|
+
arrows=True,
|
|
132
|
+
edge_vmin=0.0,
|
|
133
|
+
edge_vmax=1.0,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
ax.axis([-0.05, 1.05, -0.05, 1.05])
|
|
137
|
+
ax.axis("off")
|
|
138
|
+
|
|
139
|
+
return fig, ax
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def setup_network_flow(gra, pos, primal_count, capacity):
|
|
143
|
+
"""Configure graph for spare TSV network flow.
|
|
144
|
+
|
|
145
|
+
Sets edge weights (Euclidean distance * 100), assigns demand=-1 to
|
|
146
|
+
primal nodes and demand=0 to spare nodes, then adds a sink node
|
|
147
|
+
connected to all spare nodes.
|
|
148
|
+
|
|
149
|
+
Returns the sink node index.
|
|
150
|
+
"""
|
|
151
|
+
total = gra.number_of_nodes()
|
|
152
|
+
for u, v in gra.edges():
|
|
153
|
+
dx = pos[u][0] - pos[v][0]
|
|
154
|
+
dy = pos[u][1] - pos[v][1]
|
|
155
|
+
gra[u][v]["weight"] = int(math.sqrt(dx * dx + dy * dy) * 100)
|
|
156
|
+
gra[u][v]["capacity"] = capacity
|
|
157
|
+
|
|
158
|
+
for i in range(primal_count):
|
|
159
|
+
gra.nodes[i]["demand"] = -1
|
|
160
|
+
for i in range(primal_count, total):
|
|
161
|
+
gra.nodes[i]["demand"] = 0
|
|
162
|
+
|
|
163
|
+
sink = total
|
|
164
|
+
gra.add_node(sink, demand=primal_count)
|
|
165
|
+
for i in range(primal_count, total):
|
|
166
|
+
gra.add_edge(i, sink, capacity=capacity)
|
|
167
|
+
|
|
168
|
+
return sink
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def solve_network_flow(gra, sink_node):
|
|
172
|
+
"""Solve network flow and return path edges.
|
|
173
|
+
|
|
174
|
+
Returns (flow_cost, path_list) where path_list contains edges
|
|
175
|
+
with positive flow (excluding edges to the sink).
|
|
176
|
+
|
|
177
|
+
Returns None if the problem is infeasible.
|
|
178
|
+
"""
|
|
179
|
+
try:
|
|
180
|
+
flow_cost, flow_dict = nx.network_simplex(gra)
|
|
181
|
+
except nx.NetworkXUnfeasible:
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
path_list = [
|
|
185
|
+
(u, v)
|
|
186
|
+
for u in flow_dict
|
|
187
|
+
for v in flow_dict[u]
|
|
188
|
+
if flow_dict[u][v] > 0 and v != sink_node
|
|
189
|
+
]
|
|
190
|
+
return flow_cost, path_list
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Tests for spareTSV (experimental)."""
|
|
2
|
+
|
|
3
|
+
import networkx as nx
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from experimental.spare_tsv import (
|
|
7
|
+
formGraph,
|
|
8
|
+
setup_network_flow,
|
|
9
|
+
showPaths,
|
|
10
|
+
solve_network_flow,
|
|
11
|
+
vdc,
|
|
12
|
+
vdcorput,
|
|
13
|
+
vdcorput_iter,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _vdc(n, base=2):
|
|
18
|
+
v, denom = 0.0, 1.0
|
|
19
|
+
while n:
|
|
20
|
+
denom *= base
|
|
21
|
+
n, remainder = divmod(n, base)
|
|
22
|
+
v += remainder / denom
|
|
23
|
+
return v
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.fixture
|
|
27
|
+
def sample_positions():
|
|
28
|
+
"""Van der Corput positions (T=12, bases 2/3)."""
|
|
29
|
+
T = 12
|
|
30
|
+
xbase, ybase = 2, 3
|
|
31
|
+
x = [_vdc(i, xbase) for i in range(T)]
|
|
32
|
+
y = [_vdc(i, ybase) for i in range(T)]
|
|
33
|
+
return list(zip(x, y))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.fixture
|
|
37
|
+
def small_graph(sample_positions):
|
|
38
|
+
"""Small geometric graph for spareTSV tests."""
|
|
39
|
+
return formGraph(12, sample_positions, 0.12, 1.6, seed=5)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_vdc():
|
|
43
|
+
result = vdc(1, base=2)
|
|
44
|
+
assert result == 0.5
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_vdc_sequence_base_2():
|
|
48
|
+
vals = [vdc(i, base=2) for i in range(4)]
|
|
49
|
+
assert vals == [0.0, 0.5, 0.25, 0.75]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_vdc_sequence_base_3():
|
|
53
|
+
vals = [vdc(i, base=3) for i in range(3)]
|
|
54
|
+
assert vals == [0.0, 1 / 3, 2 / 3]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_vdcorput():
|
|
58
|
+
result = vdcorput(5, base=2)
|
|
59
|
+
assert len(result) == 5
|
|
60
|
+
assert all(0 <= x < 1 for x in result)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_vdcorput_iter():
|
|
64
|
+
result = list(vdcorput_iter(5, base=2))
|
|
65
|
+
assert len(result) == 5
|
|
66
|
+
assert result == vdcorput(5, base=2)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_formGraph(sample_positions):
|
|
70
|
+
gra = formGraph(12, sample_positions, 0.12, 1.6, seed=5)
|
|
71
|
+
assert gra.number_of_nodes() == 12
|
|
72
|
+
assert gra.is_directed()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_formGraph_reproducible(sample_positions):
|
|
76
|
+
g1 = formGraph(12, sample_positions, 0.12, 1.6, seed=42)
|
|
77
|
+
g2 = formGraph(12, sample_positions, 0.12, 1.6, seed=42)
|
|
78
|
+
assert g1.number_of_edges() == g2.number_of_edges()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_formGraph_no_seed(sample_positions):
|
|
82
|
+
gra = formGraph(12, sample_positions, 0.12, 1.6)
|
|
83
|
+
assert gra.number_of_nodes() == 12
|
|
84
|
+
assert gra.is_directed()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_showPaths(small_graph, sample_positions):
|
|
88
|
+
N = 9
|
|
89
|
+
pos = dict(enumerate(sample_positions))
|
|
90
|
+
fig, ax = showPaths(small_graph, pos, N)
|
|
91
|
+
assert fig is not None
|
|
92
|
+
assert ax is not None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_showPaths_with_options(small_graph, sample_positions):
|
|
96
|
+
N = 9
|
|
97
|
+
pos = dict(enumerate(sample_positions))
|
|
98
|
+
edges = list(small_graph.edges())[:3]
|
|
99
|
+
edge_probs = [0.5] * small_graph.number_of_edges()
|
|
100
|
+
vis = list(range(9))
|
|
101
|
+
fig, ax = showPaths(
|
|
102
|
+
small_graph,
|
|
103
|
+
pos,
|
|
104
|
+
N,
|
|
105
|
+
edgeProbs=edge_probs,
|
|
106
|
+
path=edges,
|
|
107
|
+
visibleNodes=vis,
|
|
108
|
+
guards=[0, 1],
|
|
109
|
+
)
|
|
110
|
+
assert fig is not None
|
|
111
|
+
assert ax is not None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_setup_network_flow(small_graph, sample_positions):
|
|
115
|
+
gra = small_graph.copy()
|
|
116
|
+
total = gra.number_of_nodes()
|
|
117
|
+
sink = setup_network_flow(gra, sample_positions, primal_count=9, capacity=4)
|
|
118
|
+
|
|
119
|
+
assert sink == total
|
|
120
|
+
assert gra.number_of_nodes() == total + 1
|
|
121
|
+
assert gra.has_node(sink)
|
|
122
|
+
assert gra.nodes[0]["demand"] == -1
|
|
123
|
+
assert gra.nodes[9]["demand"] == 0
|
|
124
|
+
assert gra.nodes[sink]["demand"] == 9
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_setup_network_flow_edge_attrs(small_graph, sample_positions):
|
|
128
|
+
gra = small_graph.copy()
|
|
129
|
+
setup_network_flow(gra, sample_positions, primal_count=9, capacity=4)
|
|
130
|
+
|
|
131
|
+
u, v = next(iter(gra.edges()))
|
|
132
|
+
assert "weight" in gra[u][v]
|
|
133
|
+
assert "capacity" in gra[u][v]
|
|
134
|
+
assert gra[u][v]["capacity"] == 4
|
|
135
|
+
assert isinstance(gra[u][v]["weight"], int)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_solve_network_flow(small_graph, sample_positions):
|
|
139
|
+
gra = small_graph.copy()
|
|
140
|
+
sink = setup_network_flow(gra, sample_positions, primal_count=9, capacity=4)
|
|
141
|
+
result = solve_network_flow(gra, sink)
|
|
142
|
+
|
|
143
|
+
assert result is not None
|
|
144
|
+
flow_cost, path_list = result
|
|
145
|
+
assert isinstance(path_list, list)
|
|
146
|
+
assert flow_cost >= 0
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_solve_network_flow_path_excludes_sink(small_graph, sample_positions):
|
|
150
|
+
gra = small_graph.copy()
|
|
151
|
+
sink = setup_network_flow(gra, sample_positions, primal_count=9, capacity=4)
|
|
152
|
+
result = solve_network_flow(gra, sink)
|
|
153
|
+
|
|
154
|
+
assert result is not None
|
|
155
|
+
_, path_list = result
|
|
156
|
+
for u, v in path_list:
|
|
157
|
+
assert v != sink
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_solve_network_flow_infeasible():
|
|
161
|
+
gra = nx.DiGraph()
|
|
162
|
+
gra.add_node(0, demand=-1)
|
|
163
|
+
gra.add_node(1, demand=0)
|
|
164
|
+
gra.add_edge(0, 1, weight=1, capacity=0)
|
|
165
|
+
sink = 2
|
|
166
|
+
gra.add_node(sink, demand=1)
|
|
167
|
+
gra.add_edge(1, sink, capacity=0)
|
|
168
|
+
|
|
169
|
+
result = solve_network_flow(gra, sink)
|
|
170
|
+
assert result is None
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Experiment: spare TSV network flow optimization using cycle-cancellation descent
|
|
2
|
+
|
|
3
|
+
import matplotlib
|
|
4
|
+
|
|
5
|
+
matplotlib.use("Agg")
|
|
6
|
+
import matplotlib.pyplot as plt # noqa: E402
|
|
7
|
+
|
|
8
|
+
from digraphx.mcf import cycle_canceling_mcf # noqa: E402
|
|
9
|
+
from experimental.spare_tsv import ( # noqa: E402
|
|
10
|
+
formGraph,
|
|
11
|
+
setup_network_flow,
|
|
12
|
+
showPaths,
|
|
13
|
+
vdcorput,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def nx_to_dict_graph(gra, sink):
|
|
18
|
+
"""Convert networkx graph to dict-of-dicts for cycle_canceling_mcf.
|
|
19
|
+
|
|
20
|
+
Extracts weight/capacity from edge data and demand from node data.
|
|
21
|
+
Excludes the sink node (added separately by setup logic).
|
|
22
|
+
"""
|
|
23
|
+
g = {}
|
|
24
|
+
demands = {}
|
|
25
|
+
for u in gra.nodes():
|
|
26
|
+
d = gra.nodes[u].get("demand", 0)
|
|
27
|
+
if d != 0 or u == sink:
|
|
28
|
+
demands[u] = d
|
|
29
|
+
for u, v, data in gra.edges(data=True):
|
|
30
|
+
if u not in g:
|
|
31
|
+
g[u] = {}
|
|
32
|
+
g[u][v] = {
|
|
33
|
+
"weight": data.get("weight", 0),
|
|
34
|
+
"capacity": data.get("capacity", 1),
|
|
35
|
+
}
|
|
36
|
+
return g, demands
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
N = 155
|
|
40
|
+
M = 40
|
|
41
|
+
r = 4
|
|
42
|
+
|
|
43
|
+
T = N + M
|
|
44
|
+
xbase = 2
|
|
45
|
+
ybase = 3
|
|
46
|
+
x = [i for i in vdcorput(T, xbase)]
|
|
47
|
+
y = [i for i in vdcorput(T, ybase)]
|
|
48
|
+
pos = list(zip(x, y))
|
|
49
|
+
gra = formGraph(T, pos, 0.12, 1.6, seed=5)
|
|
50
|
+
pos2 = dict(enumerate(pos))
|
|
51
|
+
fig, ax = showPaths(gra, pos2, N)
|
|
52
|
+
fig.savefig("spareTSV-initial.svg")
|
|
53
|
+
plt.close(fig)
|
|
54
|
+
|
|
55
|
+
# Set up the network flow graph (weights, capacities, demands, sink)
|
|
56
|
+
sink = setup_network_flow(gra, pos, primal_count=N, capacity=r)
|
|
57
|
+
|
|
58
|
+
# Convert to dict-of-dicts format and solve via cycle-cancellation descent
|
|
59
|
+
g, demands = nx_to_dict_graph(gra, sink)
|
|
60
|
+
result = cycle_canceling_mcf(g, demands, sink=sink)
|
|
61
|
+
|
|
62
|
+
if result is None:
|
|
63
|
+
print("Solution Infeasible!")
|
|
64
|
+
else:
|
|
65
|
+
flow_cost, flow_dict = result
|
|
66
|
+
print(f"Flow cost: {flow_cost}")
|
|
67
|
+
|
|
68
|
+
# Extract path edges with positive flow, excluding sink
|
|
69
|
+
pathlist = [
|
|
70
|
+
(u, v)
|
|
71
|
+
for u in flow_dict
|
|
72
|
+
for v, f in flow_dict[u].items()
|
|
73
|
+
if f > 0 and v != sink
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
gra.remove_node(sink)
|
|
77
|
+
fig, ax = showPaths(gra, pos2, N, path=pathlist)
|
|
78
|
+
fig.savefig("spareTSV-solution.svg")
|
|
79
|
+
plt.close(fig)
|