digraphx 0.4__tar.gz → 0.6__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.
Files changed (101) hide show
  1. {digraphx-0.4 → digraphx-0.6}/.github/workflows/multi-platforms.yml +2 -3
  2. {digraphx-0.4 → digraphx-0.6}/.github/workflows/python-app.yml +5 -6
  3. {digraphx-0.4 → digraphx-0.6}/.gitignore +4 -0
  4. digraphx-0.6/CHANGELOG.md +51 -0
  5. {digraphx-0.4/src/digraphx.egg-info → digraphx-0.6}/PKG-INFO +3 -2
  6. {digraphx-0.4 → digraphx-0.6}/docs/conf.py +14 -0
  7. digraphx-0.6/docs/examples/plot_cycle_detection.py +32 -0
  8. digraphx-0.6/docs/examples/plot_tiny_digraph.py +33 -0
  9. digraphx-0.6/docs/figures_demo.md +25 -0
  10. {digraphx-0.4 → digraphx-0.6}/docs/index.md +1 -0
  11. {digraphx-0.4 → digraphx-0.6}/docs/requirements.txt +1 -0
  12. digraphx-0.6/experimental/__init__.py +5 -0
  13. digraphx-0.6/experimental/spare_tsv.py +188 -0
  14. digraphx-0.6/experimental/test_spare_tsv.py +170 -0
  15. digraphx-0.6/experiments/experi-descent-new.py +79 -0
  16. digraphx-0.6/experiments/experi-descent-old.py +72 -0
  17. digraphx-0.6/experiments/experi-descent.py +79 -0
  18. digraphx-0.6/experiments/experi.py +37 -0
  19. {digraphx-0.4 → digraphx-0.6}/mypy.ini +3 -8
  20. {digraphx-0.4 → digraphx-0.6}/setup.cfg +2 -3
  21. digraphx-0.6/src/digraphx/__init__.py +36 -0
  22. digraphx-0.6/src/digraphx/_cycle_base.py +180 -0
  23. digraphx-0.6/src/digraphx/_parametric_base.py +101 -0
  24. digraphx-0.6/src/digraphx/csr_digraph.py +165 -0
  25. digraphx-0.6/src/digraphx/mcf.py +335 -0
  26. {digraphx-0.4 → digraphx-0.6}/src/digraphx/min_parametric_q.py +14 -46
  27. {digraphx-0.4 → digraphx-0.6}/src/digraphx/neg_cycle.py +23 -102
  28. {digraphx-0.4 → digraphx-0.6}/src/digraphx/neg_cycle_q.py +30 -115
  29. {digraphx-0.4 → digraphx-0.6}/src/digraphx/parametric.py +3 -36
  30. {digraphx-0.4 → digraphx-0.6}/src/digraphx/tiny_digraph.py +133 -25
  31. {digraphx-0.4 → digraphx-0.6/src/digraphx.egg-info}/PKG-INFO +3 -2
  32. {digraphx-0.4 → digraphx-0.6}/src/digraphx.egg-info/SOURCES.txt +21 -3
  33. {digraphx-0.4 → digraphx-0.6}/src/digraphx.egg-info/requires.txt +2 -1
  34. digraphx-0.6/src/digraphx.egg-info/scm_file_list.json +91 -0
  35. digraphx-0.6/src/digraphx.egg-info/scm_version.json +8 -0
  36. digraphx-0.6/tests/test_coverage_extras.py +546 -0
  37. digraphx-0.6/tests/test_csr_digraph.py +350 -0
  38. {digraphx-0.4 → digraphx-0.6}/tests/test_delay_padding_example_howard.py +19 -13
  39. {digraphx-0.4 → digraphx-0.6}/tests/test_edge_cases.py +56 -56
  40. digraphx-0.6/tests/test_mcf.py +86 -0
  41. digraphx-0.6/tests/test_min_cycle_ratio_extra.py +97 -0
  42. digraphx-0.6/tests/test_min_parametric_q_extra.py +105 -0
  43. digraphx-0.4/.github/workflows/ci.bak +0 -128
  44. digraphx-0.4/CHANGELOG.md +0 -7
  45. digraphx-0.4/IFLOW.md +0 -103
  46. digraphx-0.4/LICENSE +0 -21
  47. digraphx-0.4/src/digraphx/__init__.py +0 -16
  48. {digraphx-0.4 → digraphx-0.6}/.coveragerc +0 -0
  49. {digraphx-0.4 → digraphx-0.6}/.github/workflows/jekyll-gh-pages.yml +0 -0
  50. {digraphx-0.4 → digraphx-0.6}/.github/workflows/python-publish.yml +0 -0
  51. {digraphx-0.4 → digraphx-0.6}/.isort.cfg +0 -0
  52. {digraphx-0.4 → digraphx-0.6}/.pre-commit-config.yaml +0 -0
  53. {digraphx-0.4 → digraphx-0.6}/.readthedocs.yml +0 -0
  54. {digraphx-0.4 → digraphx-0.6}/.vscode/settings.json +0 -0
  55. {digraphx-0.4 → digraphx-0.6}/AGENTS.md +0 -0
  56. {digraphx-0.4 → digraphx-0.6}/AUTHORS.md +0 -0
  57. {digraphx-0.4 → digraphx-0.6}/CONTRIBUTING.md +0 -0
  58. {digraphx-0.4 → digraphx-0.6}/GEMINI.md +0 -0
  59. {digraphx-0.4 → digraphx-0.6}/LICENSE.txt +0 -0
  60. {digraphx-0.4 → digraphx-0.6}/README.md +0 -0
  61. {digraphx-0.4 → digraphx-0.6}/coverage.json +0 -0
  62. {digraphx-0.4 → digraphx-0.6}/docs/Makefile +0 -0
  63. {digraphx-0.4 → digraphx-0.6}/docs/_static/.gitignore +0 -0
  64. {digraphx-0.4 → digraphx-0.6}/docs/authors.md +0 -0
  65. {digraphx-0.4 → digraphx-0.6}/docs/changelog.md +0 -0
  66. {digraphx-0.4 → digraphx-0.6}/docs/contributing.md +0 -0
  67. {digraphx-0.4 → digraphx-0.6}/docs/license.md +0 -0
  68. {digraphx-0.4 → digraphx-0.6}/docs/readme.md +0 -0
  69. {digraphx-0.4 → digraphx-0.6}/environment.yml +0 -0
  70. {digraphx-0.4 → digraphx-0.6}/experiments/plot_node_colormap.ipynb +0 -0
  71. {digraphx-0.4 → digraphx-0.6}/experiments/plot_node_colormap.py +0 -0
  72. {digraphx-0.4 → digraphx-0.6}/note.md +0 -0
  73. {digraphx-0.4 → digraphx-0.6}/pyproject.toml +0 -0
  74. {digraphx-0.4 → digraphx-0.6}/requirements/README.md +0 -0
  75. {digraphx-0.4 → digraphx-0.6}/requirements/default.txt +0 -0
  76. {digraphx-0.4 → digraphx-0.6}/requirements/doc.txt +0 -0
  77. {digraphx-0.4 → digraphx-0.6}/requirements/test.txt +0 -0
  78. {digraphx-0.4 → digraphx-0.6}/requirements.txt +0 -0
  79. {digraphx-0.4 → digraphx-0.6}/setup.py +0 -0
  80. {digraphx-0.4 → digraphx-0.6}/src/digraphx/min_cycle_ratio.py +0 -0
  81. {digraphx-0.4 → digraphx-0.6}/src/digraphx/py.typed +0 -0
  82. {digraphx-0.4 → digraphx-0.6}/src/digraphx.egg-info/dependency_links.txt +0 -0
  83. {digraphx-0.4 → digraphx-0.6}/src/digraphx.egg-info/not-zip-safe +0 -0
  84. {digraphx-0.4 → digraphx-0.6}/src/digraphx.egg-info/top_level.txt +0 -0
  85. {digraphx-0.4 → digraphx-0.6}/tests/__init__.py +0 -0
  86. {digraphx-0.4 → digraphx-0.6}/tests/conftest.py +0 -0
  87. {digraphx-0.4 → digraphx-0.6}/tests/test_cycle_ratio.py +0 -0
  88. {digraphx-0.4 → digraphx-0.6}/tests/test_howard_cycle_cancellation.py +0 -0
  89. {digraphx-0.4 → digraphx-0.6}/tests/test_min_cycle_ratio_property.py +0 -0
  90. {digraphx-0.4 → digraphx-0.6}/tests/test_min_parametric_q.py +0 -0
  91. {digraphx-0.4 → digraphx-0.6}/tests/test_neg_cycle.py +0 -0
  92. {digraphx-0.4 → digraphx-0.6}/tests/test_neg_cycle_q.py +0 -0
  93. {digraphx-0.4 → digraphx-0.6}/tests/test_neg_cycle_q_simple_timing.py +0 -0
  94. {digraphx-0.4 → digraphx-0.6}/tests/test_neg_cycle_simple_timing.py +0 -0
  95. {digraphx-0.4 → digraphx-0.6}/tests/test_parametric.py +0 -0
  96. {digraphx-0.4 → digraphx-0.6}/tests/test_stress.py +0 -0
  97. {digraphx-0.4 → digraphx-0.6}/tests/test_timing_example.py +0 -0
  98. {digraphx-0.4 → digraphx-0.6}/tests/test_timing_example_howard.py +0 -0
  99. {digraphx-0.4 → digraphx-0.6}/tests/test_tiny_digraph.py +0 -0
  100. {digraphx-0.4 → digraphx-0.6}/tests/test_tiny_digraph_property.py +0 -0
  101. {digraphx-0.4 → digraphx-0.6}/tox.ini +0 -0
@@ -8,9 +8,9 @@ jobs:
8
8
  strategy:
9
9
  matrix:
10
10
  os: ["ubuntu-latest", "windows-latest"]
11
- version: ["3.9", "3.11"]
11
+ version: ["3.10", "3.13"]
12
12
  steps:
13
- - uses: actions/checkout@v3
13
+ - uses: actions/checkout@v4
14
14
  - uses: conda-incubator/setup-miniconda@v3
15
15
  with:
16
16
  activate-environment: test
@@ -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
@@ -14,17 +14,16 @@ jobs:
14
14
  runs-on: ubuntu-latest
15
15
 
16
16
  steps:
17
- - uses: actions/checkout@v2
18
- - name: Set up Python 3.9
19
- uses: actions/setup-python@v2
17
+ - uses: actions/checkout@v4
18
+ - name: Set up Python 3.10
19
+ uses: actions/setup-python@v5
20
20
  with:
21
- python-version: 3.9
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: |
@@ -38,7 +37,7 @@ jobs:
38
37
  - name: Codecov
39
38
  # You may pin to the exact commit or the version.
40
39
  # uses: codecov/codecov-action@29386c70ef20e286228c72b668a06fd0e8399192
41
- uses: codecov/codecov-action@v1.5.2
40
+ uses: codecov/codecov-action@v4
42
41
  with:
43
42
  # Repository upload token - get it from codecov.io. Required only for private repositories
44
43
  token: ${{ secrets.CODECOV_TOKEN }}
@@ -176,3 +176,7 @@ MANIFEST
176
176
 
177
177
  # my stuffs
178
178
  .hypothesis
179
+
180
+ # Cache dirs
181
+ .ruff_cache/
182
+ .benchmarks/
@@ -0,0 +1,51 @@
1
+ # Changelog
2
+
3
+ ## Version 0.6 (2026-09-04)
4
+
5
+ ### Features
6
+ - **Shared cycle/parametric bases**: Extracted `_cycle_base.py` (`find_cycle`, `relax_pred`, `relax_succ`, `cycle_list`, `is_negative`, `howard_search`) and `_parametric_base.py` (`_run_loop`) shared by the max/min parametric solvers. `NegCycleFinder`/`NegCycleFinderQ` now delegate to the shared skeleton, `MinParametricAPI` subclasses `ParametricAPI` (was a duplicate interface), and `mcf.py` reuses `_residual_edge`. Public API unchanged. (#1c60bde)
7
+ - **Public re-exports in `__init__.py`**: Fixes README quick-start imports such as `NegCycleFinder`, `TinyDiGraph`, `MinCycleRatioSolver`. (#1c60bde)
8
+
9
+ ### Bug Fixes
10
+ - **mypy type errors**: Resolved None guards, test generics and networkx config issues. (#13dc943)
11
+ - **RTD doc build**: Added matplotlib and numpy to `docs/requirements.txt`. (#1f49779)
12
+
13
+ ### Testing & Code Quality
14
+ - **Coverage raised 85%→99%**: Added coverage extras for MCF solver, TinyDiGraph and solvers. (#2cabc4e)
15
+ - **Style pass**: Reformatted list comprehensions and type annotations. (#2973013)
16
+
17
+ ### Code Cleanup
18
+ - **Removed AI slop**: Stripped boilerplate from docstrings and comments. (#be4456a)
19
+ - **Import tidy-up**: Dropped unused `NegCycleFinderQ` import in `min_parametric_q`, split multi-name `_cycle_base` imports, stripped stray blank lines in `tiny_digraph`. (#5e37f7c)
20
+
21
+ ### Build & CI
22
+ - **Removed stale `.bak` workflow**: Deleted `ci.bak`. (#5f50fe3)
23
+ - **Updated GitHub Actions**: checkout→v4, setup-python→v5, codecov-action→v4 to fix Codecov tokenless upload failure and Node 20 deprecation. (#5334192)
24
+
25
+ ## Version 0.5 (2026-07-16)
26
+
27
+ ### Features
28
+ - **MCF solver**: Added min-cost flow cycle-cancellation solver (`mcf.py`) and `NegCycleFinderQ` with `VertexFilter` for vertex-disjoint MCF constraint. (#582b1eb, #e6e248f)
29
+ - **CSRDiGraph**: Memory-efficient CSR-backed directed graph with `_CSRNeighbors` Mapping view — compact integer arrays instead of dicts. (#12465a3)
30
+ - **Bellman-Ford cycle detection**: Replaced Howard's algorithm with Bellman-Ford for negative cycle detection; fixed parallel edge bug. (#04da700)
31
+ - **spareTSV integration**: Added spare TSV network flow utilities and unified variable naming to `utx`/`vtx` (C++ convention). (#762008a)
32
+ - **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)
33
+
34
+ ### Performance
35
+ - **Lazy TinyDiGraph/CSRDiGraph**: Avoid pre-allocating 2N dicts per `init_nodes()` call (~464 MB saved for 1M nodes). (#12465a3)
36
+ - **Incremental residual updates**: Update only edges affected by cycle cancellation instead of rebuilding full residual each iteration. (#12465a3)
37
+
38
+ ### Documentation
39
+ - **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)
40
+
41
+ ### Testing & Code Quality
42
+ - **Coverage raised 70%→95%**: Added extensive test coverage including MCF solver, CSRDiGraph, edge cases, and extra min-cycle-ratio tests. (#80da2a2, #9030d8a)
43
+
44
+ ### Code Cleanup
45
+ - **Removed PyScaffold boilerplate**: Deleted `skeleton.py` and `test_skeleton.py`. (#9bdbdb1)
46
+ - **Dropped Python < 3.9 compat**: Removed `importlib-metadata` conditional dependency. (#9bdbdb1)
47
+ - **Config cleanup**: Removed dead entry points, unused mypy ignores, stale `IFLOW.md` and duplicate `LICENSE`. (#9bdbdb1)
48
+
49
+ ### Build & CI
50
+ - **CI repair**: Fixed broken entry_points and remaining skeleton imports. (#91fd90e)
51
+ - **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.4
3
+ Version: 0.6
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.9"
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
+ ```
@@ -22,6 +22,7 @@ Contributions & Help <contributing>
22
22
  License <license>
23
23
  Authors <authors>
24
24
  Changelog <changelog>
25
+ Figures Demo <figures_demo>
25
26
  Module Reference <api/modules>
26
27
  ```
27
28
 
@@ -6,3 +6,4 @@ git+https://github.com/luk036/mywheel.git
6
6
  myst-parser[linkify]
7
7
  sphinx>=3.2.1
8
8
  sphinxcontrib-svgbob
9
+ matplotlib
@@ -0,0 +1,5 @@
1
+ """Experimental / plotting utilities for digraphx.
2
+
3
+ This module contains experimental code including the spare TSV
4
+ network flow visualization utilities (requires matplotlib).
5
+ """
@@ -0,0 +1,188 @@
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 for e in edge_list if e[0] in visibleNodes and e[1] in visibleNodes
101
+ ]
102
+
103
+ if isinstance(edgeProbs, float):
104
+ edgeProbs = [edgeProbs] * gra.number_of_edges()
105
+ p = [edgeProbs[i] for i, e in enumerate(edge_list) if e in visibleEdges]
106
+
107
+ nx.draw_networkx_edges(
108
+ gra,
109
+ pos,
110
+ edge_color=p,
111
+ width=1,
112
+ edge_cmap=plt.cm.RdYlGn,
113
+ arrows=False,
114
+ edgelist=visibleEdges,
115
+ edge_vmin=0.0,
116
+ edge_vmax=1.0,
117
+ ax=ax,
118
+ alpha=alpha,
119
+ )
120
+
121
+ if path is not None:
122
+ nx.draw_networkx_edges(
123
+ gra,
124
+ pos,
125
+ edge_color="b",
126
+ width=1,
127
+ edge_cmap=plt.cm.RdYlGn,
128
+ edgelist=path,
129
+ arrows=True,
130
+ edge_vmin=0.0,
131
+ edge_vmax=1.0,
132
+ )
133
+
134
+ ax.axis([-0.05, 1.05, -0.05, 1.05])
135
+ ax.axis("off")
136
+
137
+ return fig, ax
138
+
139
+
140
+ def setup_network_flow(gra, pos, primal_count, capacity):
141
+ """Configure graph for spare TSV network flow.
142
+
143
+ Sets edge weights (Euclidean distance * 100), assigns demand=-1 to
144
+ primal nodes and demand=0 to spare nodes, then adds a sink node
145
+ connected to all spare nodes.
146
+
147
+ Returns the sink node index.
148
+ """
149
+ total = gra.number_of_nodes()
150
+ for u, v in gra.edges():
151
+ dx = pos[u][0] - pos[v][0]
152
+ dy = pos[u][1] - pos[v][1]
153
+ gra[u][v]["weight"] = int(math.sqrt(dx * dx + dy * dy) * 100)
154
+ gra[u][v]["capacity"] = capacity
155
+
156
+ for i in range(primal_count):
157
+ gra.nodes[i]["demand"] = -1
158
+ for i in range(primal_count, total):
159
+ gra.nodes[i]["demand"] = 0
160
+
161
+ sink = total
162
+ gra.add_node(sink, demand=primal_count)
163
+ for i in range(primal_count, total):
164
+ gra.add_edge(i, sink, capacity=capacity)
165
+
166
+ return sink
167
+
168
+
169
+ def solve_network_flow(gra, sink_node):
170
+ """Solve network flow and return path edges.
171
+
172
+ Returns (flow_cost, path_list) where path_list contains edges
173
+ with positive flow (excluding edges to the sink).
174
+
175
+ Returns None if the problem is infeasible.
176
+ """
177
+ try:
178
+ flow_cost, flow_dict = nx.network_simplex(gra)
179
+ except nx.NetworkXUnfeasible:
180
+ return None
181
+
182
+ path_list = [
183
+ (u, v)
184
+ for u in flow_dict
185
+ for v in flow_dict[u]
186
+ if flow_dict[u][v] > 0 and v != sink_node
187
+ ]
188
+ 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