>
```
**Problem:** When multiple edges exist between the same pair of nodes, the node-pair representation loses which edge was traversed.
**Solution:** Always return **edge-weight cycles** `Vec<Weight>`. The weight carries the identity of the specific edge.
---
class: nord-light, middle, center
## ๐ Variable Name Alignment
---
### ๐ Unified Naming Convention
All three languages now use **identical variable names**:
.font-sm[
| Concept | C++ โก | Rust ๐ฆ | Python ๐ |
|---------|--------|---------|-----------|
| Source node | `utx` | `utx` | `utx` |
| Target node | `vtx` | `vtx` | `vtx` |
| Cycle traversal | `vtx` | `vtx` | `vtx` |
| Edge weight | `edge` / `w` | `w` | `edge` |
| Predecessor map | `_pred` | `pred` | `pred` |
| Successor map | `_succ` | `succ` | `succ` |
| Cycle handle | `handle` | `handle` | `handle` |
| Distance map | `dist` | `dist` | `dist` |
| Changed flag | `changed` | `changed` | `changed` |
]
---
```python
# ๐ Python
for utx, neighbors in self.digraph.items():
for vtx, edge in neighbors.items():
distance = dist[utx] + get_weight(edge)
```
```rust
// ๐ฆ Rust
for utx in self.graph.nodes() {
for (vtx, w) in self.graph.neighbors(utx) {
let distance = du + get_weight(&w);
```
```cpp
// โก C++
for (const auto& entry : this->_digraph) {
const auto& utx = _get_key(entry);
for (const auto& nbr_entry : nbrs) {
const auto& vtx = _get_key(nbr_entry);
```
---
class: nord-light, middle, center
## ๐ฐ Min-Cost Flow โ Cycle Canceling
---
### ๐ฐ Cycle-Canceling MCF Algorithm
The **spare TSV assignment problem** reduces to **min-cost flow**:
**Problem:**
- **Primal nodes** (demand = -1 each): need connection
- **Spare nodes** (demand = 0): available as intermediaries
- **Sink** (demand = +N): absorbs all flow
- **Edges** have weight (distance ร 100) and capacity (= 4)
**Algorithm โ Cycle Canceling Descent:**
$$ \text{Feasible flow} \rightarrow \text{Find negative cycles in residual} \rightarrow \text{Cancel them} \rightarrow \text{Repeat} $$
.mermaid[
graph LR
A["Feasible Flow\n(greedy BFS)"] --> B["Build Residual\nGraph"]
B --> C["Find Neg Cycles\n(Bellman-Ford)"]
C --> D{"Found?"}
D -- Yes --> E["Cancel cycle\n(update flow)"]
E --> F["Update residual\nedges"]
F --> C
D -- No --> G["โ
Optimal!"]
]
---
### ๐ฐ Residual Graph Construction
For each edge $(u,v)$ with flow $f$, capacity $c$, weight $w$:
.pull-left[
**Forward edge** (can send more flow):
$$ \text{cost} = w,\quad \text{capacity} = c - f $$
**Backward edge** (can cancel flow):
$$ \text{cost} = -w,\quad \text{capacity} = f $$
]
.pull-right[
.mermaid[
graph LR
u["u"] -- "f / c\nw" --> v["v"]
u -. "c-f\nw" .-> v
v -. "f\n-w" .-> u
]
]
When multiple edges collide at the same $(u,v)$, keep the one with **more negative cost**:
$$ \text{residual}[u][v] = \min_{\text{edges}} \text{cost} $$
---
### ๐ฐ Bellman-Ford Negative Cycle Detection
Runs $|V|$ passes. In the $V$-th pass, nodes whose distance still improves are part of a **negative cycle**.
$$ \text{dist}[v]^{k+1} = \min\!\left(\text{dist}[v]^k,\; \min_{(u,v)\in E}\!\big(\text{dist}[u]^k + \text{cost}(u,v)\big)\right) $$
**Cycle extraction โ trace back through predecessors:**
.mermaid[
graph LR
A["Node A\n(dist improves\nin pass V)"] --> B["Trace pred\nB โ C โ D โ B"]
B --> C["Found cycle\n(B โ C โ D โ B)"]
C --> D{"Sum of\ncosts < 0?"}
D -- Yes --> E["โ
Cancel cycle"]
D -- No --> F["โ Skip\n(not negative)"]
]
---
class: nord-light, middle, center
## ๐ spareTSV Integration
---
### ๐ From Python to Rust & C++
**Python source:** `spareTSV/spare_tsv.py` โ `digraphx/mcf.py`
**Algorithm:** `cycle_canceling_mcf`
**Ported to:**
| Language | File | Lines | Key Dependency |
|----------|------|-------|---------------|
| ๐ฆ **Rust** | `src/mcf.rs` | ~530 | `HashMap` only (zero deps) |
| โก **C++** | `include/digraphx/mcf.hpp` | ~460 | `absl::flat_hash_map` |
**5 functions ported identically:**
1. `find_feasible_flow` โ greedy BFS routing ๐งญ
2. `build_residual` โ residual graph construction ๐๏ธ
3. `find_all_neg_cycles_bf` โ Bellman-Ford cycle detection ๐
4. `update_residual_edge` โ local residual update ๐
5. `cycle_canceling_mcf` โ main MCF solver ๐ฐ
---
### ๐ Porting Challenges
**1. Graph representation:**
- ๐ Python: `dict` (insertion-ordered 3.7+)
- ๐ฆ Rust: `HashMap` (non-deterministic order)
- โก C++: `flat_hash_map` (non-deterministic order)
**2. Bellman-Ford edge tracking:**
Rust initially used a **second BF pass** to reconstruct predecessor edges, which found different edges than the first pass:
```rust
// โ
Single pass โ edge references stored during relaxation
let mut pred: HashMap = HashMap::new();
```
**3. Float truncation:**
Python `int(dist * 100)` truncates; Rust `.round()` rounds โ a **1-unit cost difference**! Fixed with `.trunc()`.
**4. Eta formula:**
$$ \text{Python: } \eta = \frac{1.6}{\lfloor\sqrt{T}\rfloor - 1},\quad
\text{Rust/C++ (init): } \eta = \frac{1.6}{\sqrt{T} - 1} $$
---
### ๐ Validation Against Actual spareTSV
**spareTSV fixture** (T=12, VdC bases (2,3), eta=1.6, seed=5):
.mermaid[
graph TD
0 -->|1| 9
1 -->|1| 10
2 -->|1| 10
3 -->|1| 9
4 -->|1| 10
5 -->|1| 11
6 -->|1| 9
7 -->|1| 11
8 -->|1| 10
9 -->|3| 12
10 -->|4| 12
11 -->|2| 12
]
**All four solvers โ cost = 264, identical flow** โญ
---
class: nord-light, middle, center
## โ
Cross-Validation Results
---
### โ
Exact Flow Match โ Small Fixture
**12 nodes** (9 primal + 3 spare + sink), **121 edges**, capacity=4
| Solver | Cost | Flow Match |
|--------|------|------------|
| ๐ Python (`network_simplex`) | **264** | โ
|
| ๐ Python (`cycle_canceling_mcf`) | **264** | โ
|
| ๐ฆ Rust (`cycle_canceling_mcf`) | **264** | โ
|
| โก C++ (`cycle_canceling_mcf`) | **264** | โ
|
$$
f(0 \to 9) = 1,\; f(1 \to 10) = 1,\; \dots,\; f(11 \to 12) = 2
$$
> ๐ฌ **Every edge, every unit of flow, every cost โ identical.** The cycle-canceling descent is **deterministic**: same BFS initial flow, same residual, same cycles.
---
### โ
Exact Cost Match โ Large Scale
**195 nodes** (155 primal + 40 spare + sink), **1684 edges**
| Solver | Cost | Time | vs Python |
|--------|------|------|-----------|
| ๐ Python (`cycle_canceling_mcf`) | **1108** | 12.36 s | 1ร |
| ๐ฆ Rust (`cycle_canceling_mcf`) | **1108** | **0.65 s** | **19ร faster** |
| โก C++ (`cycle_canceling_mcf`) | **1108** | **0.63 s** | **19.6ร faster** |
> ๐ฌ **Cost matches exactly.** The `eta` formula bug (wrong denominator) produced a different graph with cost 1109 โ caught by cross-validation!
---
class: nord-light, middle, center
## โก Benchmark Showdown
---
### โก MCF Benchmarks
**Hardware:** Windows x64, MSVC 2026 / Rust stable / Python 3.14
.font-sm[
| Benchmark | Nodes | Edges | ๐ Python | ๐ฆ Rust | โก C++ | Speedup |
|-----------|-------|-------|----------|---------|-------|---------|
| `mcf_chain_10` | 10 | 9 | 43.6 ยตs | **28.9 ยตs** | โ | **1.5ร** |
| `mcf_chain_100` | 100 | 99 | 2.55 ms | **824 ยตs** | โ | **3.1ร** |
| `mcf_neg_cycle` | 4 | 5 | 37.8 ยตs | **16.3 ยตs** | โ | **2.3ร** |
| `mcf_grid_50` | 50 | 147 | 430 ยตs | **224 ยตs** | โ | **1.9ร** |
| **`mcf_spare_tsv_195`** | **195** | **1684** | **12.36 s** | **0.65 s** | **0.63 s** | **19ร** |
]
**Observations:**
- Rust & C++ within **4%** of each other โ both **~19ร faster** than Python
- Speedup grows with problem size (amortized interpreter overhead)
---
### โก Full Benchmark Suite
Rust `criterion` benchmarks (optimized release, Windows x64):
.font-sm[
| Benchmark | Description | Time |
|-----------|-------------|------|
| `neg_cycle_small` | 3-node neg cycle | **4.0 ยตs** |
| `neg_cycle_medium` | 100-node chain | **16.4 ยตs** |
| `howard_ratio` | 500 edges, `Ratio` weights | **24.1 ยตs** |
| `parametric_solver` | 7-node parametric optimization | **12.8 ยตs** |
| `mcf_chain_10` | 10-node chain MCF | **28.9 ยตs** |
| `mcf_neg_cycle` | 4-node negative cycle MCF | **16.3 ยตs** |
| `mcf_grid_50` | 50-node grid MCF | **224 ยตs** |
| `mcf_spare_tsv_195` | **195-node spareTSV** | **647 ms** |
]
> ๐ **Rust & C++ within 4%** โ nanoseconds for micro-benchmarks, sub-second for large-scale.
---
### โก Test Suite Health
| Project | Tests | Status |
|---------|-------|--------|
| ๐ฆ **digraphx-rs** | **78** (53 unit + 6 integ + 19 doc) | โ
All pass |
| โก **digraphx-cpp** | **61** (55 old + 6 new) | โ
All pass |
| ๐ **digraphx** | **164** | โ
All pass |
| ๐ **spareTSV** | **14/15** (1 GUI failure) | โ
All algorithm tests pass |
---
class: nord-light, middle, center
## ๐ฏ Key Takeaways
---
### ๐ฏ What We Learned
.pull-left[
**1. ๐ Generator Pattern โ Unify Across Languages:**
- ๐ Python `yield`, โก C++ `co_yield`, ๐ฆ Rust `genawaiter::sync::Gen`
- Same `while !found && relax()` โ `for vtx in cycles:` โ `yield` pattern
- `genawaiter` with closure return-type annotation solves lifetime issues
**2. ๐ Naming Alignment:**
- All three now use `utx` / `vtx` consistently
- Edge-weight cycles only (no node-pair โ can't handle multi-edges)
]
.pull-right[
**3. ๐ง Algorithmic Gotchas:**
- Howard's algorithm **never converges** with negative cycles โ stop after first pass
- BF predecessor tracking in a **single pass** โ second pass finds different edges!
- Float rounding: `int()` vs `.round()` โ 1-unit cost difference
**4. โก Performance:**
- Rust & C++ within **4%** of each other, both **~19ร faster** than Python
- Speedup grows with problem size
]
---
### ๐ฏ Recommendations
**For future ports:**
- โ
Validate **flow output**, not just total cost
- โ
Match floating-point semantics exactly (`trunc` vs `round`)
- โ
Match formula structure exactly (`int(sqrt(T))` vs `sqrt(T)`)
- โ
Track algorithm state in a **single pass** (no second BF reconstruction)
**Generator strategy for Rust:**
- Use `genawaiter::sync::Gen` with `Pin>` + closure return-type annotation
- Zero external runtime โ works with any async executor
- Overhead is constant per generator creation; negligible for real workloads
**For users:**
- ๐ฆ **Rust** โ maximum performance with memory safety
- โก **C++** โ when integrating with existing abseil/infrastructure
- ๐ **Python** โ prototyping, then port to Rust/C++ for production
---
count: false
class: nord-dark, middle, center
# ๐ Thank You!
### *Questions?*
**Repositories:**
- ๐ฆ https://github.com/luk036/digraphx-rs
- โก https://github.com/luk036/digraphx-cpp
- ๐ https://github.com/luk036/digraphx
**Slides:** https://luk036.github.io/net_optim/digraphx-py-rs-cpp-remark.html
> ๐ก **"Make it work in Python, make it fast in Rust, make it robust in C++."**