```
- ๐ฆ `DiGraph` is **duck-typed** container-of-containers: iterate
`(node โ (neighbour, edge))`. `Edge` = the stored *value* (we store `int`
costs directly).
- ๐งญ `dist` must be indexable by node โ we use `std::vector<Fraction>`
(integer-valued positions, read back via `.numerator()`).
- โ
No `TinyDiGraph` needed in C++: `create_flow_graph` returns a plain
adjacency; `apply_howard` wraps it in `mywheel::MapAdapter` for the solver.
---
class: nord-light, middle, center
## ๐งฑ Part 3: The Port
---
### ๐๏ธ Repo Layout & Conventions (netoptim-cpp style)
```
nnsplace-cpp/
โโโ include/nnsplace/
โ โโโ placement_cfg.hpp NnsConfig (+ validation)
โ โโโ placement.hpp create_flow_graph, NnsPlacer, HowardsCost
โ โโโ matching.hpp rectangular Hungarian assignment
โ โโโ readwrite.hpp read_json_edges ("edges" node-link reader)
โโโ include/fractions/ vendored patched pyfractions.hpp
โโโ source/ placement.cpp ยท matching.cpp ยท readwrite.cpp ยท placement_cfg.cpp
โโโ test/source/ doctest suite (main.cpp + test_placement.cpp)
โโโ standalone/source/ CLI parity runner
โโโ experiments/parity.py Python side-by-side runner
โโโ testcases/ p1.json, drawf.json, fix.json
โโโ xmake.lua + CMakeLists.txt
โโโ AGENTS.md + README.md
```
- Global-scope headers (no namespace wrapper), Doxygen comments, Google-ish
clang-format โ exactly like `netoptim-cpp` ๐.
- **Function bodies of more than 15 lines live in `source/*.cpp`** ๐งฑ; headers
only declare them. The one exception is the `hungarian` **template**, which
cannot move out of a header without explicit instantiation.
---
### ๐ `create_flow_graph`: Fidelity Matters
Python builds directed edges from every **ordered pair** `(v1, v2)` inside each
net, adding both directions โ unless the *target* is a pad:
```
for each net:
for v1 in net: # every ordered pair
for v2 in net:
if module_weight[v2] == 0: continue # pad can never be a target
add v1 โ v2 and v2 โ v1
```
We verified the consequences **empirically** on the Python runtime ๐ฌ:
- **self-loops** appear on every non-pad net member `(v, v)` ๐
- pads connect **bidirectionally** to their net, but never self-loop
- edge set is de-duplicated & sorted per node
C++ `create_flow_graph` reproduces this exactly โ the mock edge set matches
Python byte-for-byte โ๏ธ, and the `p1` flow graph has the Python-verified
**10 249 directed edges** ๐ฏ.
---
### โ๏ธ Wire Length & HPWL (integer-exact)
The linear (delta-scaled) cost model stays **integer-exact** ๐งฎ:
$${\color{green}\mathrm{cost}}(\ell, a)={\color{blue}\delta_a}\cdot\ell,\qquad
{\color{green}\mathrm{worst}}=\max_{(u,v)\in E}\ {\color{blue}\delta_x}\lvert\Delta x\rvert+{\color{orange}\delta_y}\lvert\Delta y\rvert$$
HPWL via hull intervals (one axis at a time) ๐ง:
$${\color{orange}\mathrm{hull}}(e)=\max_{i\in e} q_i-\min_{i\in e} q_i,\qquad
{\color{orange}\mathrm{HPWL}}=\sum_{e}\Bigl({\color{blue}\mathrm{hull}_x(e)}\cdot{\color{blue}\delta_x}+{\color{orange}\mathrm{hull}_y(e)}\cdot{\color{orange}\delta_y}\Bigr)$$
---
### ๐ฏ Legalization = Min-Weight Assignment
- A bucket of modules sharing one perpendicular line must spread onto distinct
slots ๐. Scoring a lateral move of module $v$ to coordinate $q$ is done in
$O(\log \deg)$ via sorted neighbours + prefix/suffix tables:
$${\color{blue}w}(q)=\max\Bigl({\color{blue}\delta_x} q+{\color{green}\mathrm{pref}}[t],\ \ {\color{green}\mathrm{suff}}[t]-{\color{blue}\delta_x} q\Bigr),\qquad
t=\#\{a: a\le q\}$$
- Each candidate slot gets weight ${\color{green}w_1}-{\color{green}w_0}$; the
placer solves
$${\color{red}\min}\sum_{v} {\color{green}w}\bigl(v,\ \mathrm{slot}(v)\bigr)$$
with our **rectangular Hungarian** (dummy rows leave spare slots free,
infeasible โ `nullopt`).
---
### ๐ Legalization Strategy: Window โ Fallback
.mermaid[
flowchart TB
L[legalize bucket] --> B[base graph: current pos, weight 0]
B --> LW[Local window r = 1..10]
LW --> M{matching feasible?}
M -- yes --> D[apply matches]
M -- "no - widen window" --> LW2[add radius ring r]
LW2 --> M
M -- "no - window exhausted" --> GS[Global slots fallback]
GS --> M2{matching feasible?}
M2 -- yes --> D
M2 -- no --> RAISE[RuntimeError]
style L fill:#fff3e0,stroke:#e65100,stroke-width:3px,color:#2e3440
style B fill:#e3f2fd,stroke:#1565c0,stroke-width:3px,color:#2e3440
style LW fill:#e3f2fd,stroke:#1565c0,stroke-width:3px,color:#2e3440
style GS fill:#e3f2fd,stroke:#1565c0,stroke-width:3px,color:#2e3440
style D fill:#c8e6c9,stroke:#2e7d32,stroke-width:3px,color:#2e3440
style RAISE fill:#ffcdd2,stroke:#c62828,stroke-width:3px,color:#2e3440
]
- Local ยฑradius window is the **cheap, quality-preserving** path ๐ช; the global
free-slot fallback guarantees success when the grid has capacity ๐ก๏ธ
(regression: 70 modules crowded on one line).
---
### ๐ I/O Pad Ring Assignment
- Each pad picks the nearest of the two **opposite edges**, respecting
per-edge capacity:
$${\color{blue} \mathrm{io\_limit}}=\Big\lceil \tfrac{\#\text{pads}}{2}\Big\rceil,\qquad
\text{position}\in\{{\color{orange}0},\,{\color{orange} \mathrm{grid}+1}\}$$
- Nearest-pad math (derived in the source comments) uses only **integer**
arithmetic with floor division โ and huge sentinels (~$10^{12}$), which is
why coordinates are `int64_t` ๐.
---
### ๐ The Per-Axis Optimization Loop
.mermaid[
flowchart LR
O[optimize] --> AX0["optimize_axis(0)"]
AX0 --> AX1["optimize_axis(1)"]
AX1 --> W{worst improved?}
W -- yes --> AX0
W -- no --> R[restore best memento & return]
subgraph A0 ["optimize_axis(axis)"]
H["apply_howard (axis)"] --> LM["legalize_modules (axis^1)"]
LM --> IO["choose_nearest_iopad"]
end
style O fill:#fff3e0,stroke:#e65100,stroke-width:3px,color:#2e3440
style AX0 fill:#e3f2fd,stroke:#1565c0,stroke-width:3px,color:#2e3440
style AX1 fill:#e3f2fd,stroke:#1565c0,stroke-width:3px,color:#2e3440
style W fill:#fff59d,stroke:#f9a825,stroke-width:3px,color:#2e3440
style R fill:#ffcdd2,stroke:#c62828,stroke-width:3px,color:#2e3440
style A0 fill:#f3e5f5,stroke:#8e24aa,stroke-width:2px,color:#2e3440
style H fill:#e3f2fd,stroke:#1565c0,stroke-width:3px,color:#2e3440
style LM fill:#e3f2fd,stroke:#1565c0,stroke-width:3px,color:#2e3440
style IO fill:#e3f2fd,stroke:#1565c0,stroke-width:3px,color:#2e3440
]
- `run()` repeats optimize + `io_assign` until no strict improvement,
**restoring the best saved state** each time (Memento ๐งพ).
---
class: nord-light, middle, center
## โ
Part 4: Verification
---
### ๐งช Test Suite (ported from Python)
- **31 doctest cases**, mirroring `test_placement.py`,
`test_placement_cfg.py`, `test_place.py`, `test_line_limit.py`:
config validation, flow-graph edge set, cost / cost_inv / worst wire length,
HPWL hulls, `line_cap_ratio` & `io_limit` caps, legalize fallback, and full
p1 / drawf placement runs with **legality + improvement** checks ๐งฏ.
- xmake gates on Windows: **MSVC `/W4 /WX`** โ zero warnings allowed ๐ซโ ๏ธ.
- Result: โ
```
test cases: 31 | 31 passed | 0 failed
assertions: 19315 | 19315 passed | Status: SUCCESS!
```
- The suite now asserts **strict worst-length improvement** โ the guard that
would have caught the snapshot bug below ๐.
---
### ๐ค The Parity Harness
- A CLI runner (`standalone/source/main.cpp`) + `experiments/parity.py` print
the *same* metrics (HPWL x/y, worst before/after, iterations) for the *same*
seed-831 flow:
```
xmake run nnsplace_standalone testcases/p1.json 50 50 40 2000 # C++
python experiments/parity.py testcases/p1.json 50 50 # Python
```
- Same file (`testcases/*.json`), same config, same pipeline โ side-by-side
comparison of the two implementations ๐.
---
### ๐ The Bug the Parity Check Found
- **Symptom**: C++ returned `iterations = 0` everywhere โ the placer never
improved ๐ค, while Python improved on every grid.
- **Root cause**: the C++ `optimize()`/`run()` snapshotted the state **once**
at entry ๐. Python re-creates its `PlacerState` **after every improving
iteration**; when the next iteration stalled, Python rolled back to the
*best improved* state โ C++ rolled back to the *original* one ๐ฅ.
- **Fix** โ re-snapshot in the loop:
```
worst0 = worst1
state = snapshot(place, count) # โ was missing
```
- โ
After the fix the C++ placer improves on every configuration and lands on
the **same final worst value** as Python (e.g. 32ร32: 1480 = 1480) ๐ฏ.
---
### ๐ Python vs C++ โ p1 (833 modules / 81 pads, seed 831)
| grid | Python worst โ | C++ worst โ | Py HPWLโ | C++ HPWLโ |
|------|----------------|-------------|----------|-----------|
| 32ร32 | 2240 โ **1480** | 2160 โ **1480** | 583 040 | 559 320 |
| 50ร50 | 2400 โ 1800 | 2440 โ 1600 | 653 160 | 644 120 |
| 100ร100 | 4240 โ 2640 | 4200 โ 2760 | 980 880 | 1 182 680 |
| 50ร50 cap | 2400 โ 1360 | 2440 โ 1520 | 605 560 | 651 560 |
| 100ร100 cap | 4240 โ 2480 | 4200 โ 2720 | 983 280 | 1 126 800 |
- Final **worst** values are close (identical on 32ร32); HPWL within a few % ๐
- Remaining spread = different RNG (Python `random` vs `std::mt19937`) and
Hungarian tie-breaking โ both placements are **legal** โ๏ธ
---
### ๐ผ๏ธ Placement Snapshots โ Initial (before)

Random initial placement on the core grid ๐ฒ โ pads still inside the core.
---
### ๐ผ๏ธ After One Howard Pass (x-axis)

Howard relaxes potentials along one axis โจ โ the first big wire-length drop.
---
### ๐ผ๏ธ After Legalization

Legalization spreads each bucket onto distinct slots โ no overlaps ๐งฉ.
---
### ๐ผ๏ธ Final Placement

Optimized & legal layout with I/O pads snapped to the ring ๐ฏ.
---
class: nord-light, middle, center
## ๐ Part 5: Release & Future Work
---
### ๐ฆ CMake โ Mirroring `netoptim-cpp`
- `CMakeLists.txt` follows the netoptim-cpp blueprint ๐งฌ:
CPM.cmake dependencies (`INSTALL_ONLY`), fmt/spdlog handling, abseil
resolution, `SPECIFIC_LIBS`, PackageProject install, doctest, Format.cmake.
- `NnsPlace` is a **compiled (static) target** now ๐: it PUBLIC-links
`${SPECIFIC_LIBS}` + abseil so the `.cpp` units compile and consumers
inherit the sibling includes ๐ (mirrors `netlistx-cpp`).
- Sibling packages are pulled **by tag** from GitHub
(`NetlistX` v1.1.7, `DiGraphX` v1.1.7, `Recti` v1.2.4, `MyWheel` v1.1.6,
`Py2Cpp` v1.6.4, `XNetwork` v1.7.7) ๐ท๏ธ.
- Lessons learned on Windows ๐ช:
- VS **18 2026** generator (not VS 2022 toolset) ๐ ๏ธ
- a static library target needs **`CXX_STANDARD 20`** set explicitly โ MSVC
otherwise defaults to C++17 and rejects `operator<=>` ๐ง
- abseil headers come from DiGraphX; include them via the linked targets,
not by hand ๐
Verified: configure โ build โ **ctest 100% passed** โ
---
### ๐งฑ From Header-Only to Headers + `.cpp`
A later clean-up pass moved every **function body longer than 15 lines** out of
the headers into `source/*.cpp` ๐๏ธ:
- `placement.cpp` โ flow graph + the big `NnsPlacer` methods (constructor,
`apply_howard`, `legalize`, I/O-pad logic, โฆ)
- `matching.cpp` โ Kuhn feasibility + `min_weight_full_matching`
- `readwrite.cpp` / `placement_cfg.cpp` โ JSON reader, validated constructor
Rules followed:
- bodies of **โค 15 lines stay inline** in the header (metrics, `run`/`optimize`,
small helpers) ๐
- the **`hungarian` template** stays in the header โ templates cannot move to a
`.cpp` without explicit instantiation ๐งฌ
Verified **before & after** โ byte-identical behaviour:
```
xmake โ build ok (no warnings) cmake build โ NnsPlace.lib + exes
xmake run โ 31/31 ยท 19315 assertions ctest โ 100% passed
standalone โ worst 2160 โ 1480, legal=yes (unchanged) โ
```
---
### ๐ฆ xmake + CMake, One Source of Truth
.mermaid[
flowchart LR
SRC[include/nnsplace/*.hpp] --> X[xmake.lua]
SRC --> C[CMakeLists.txt]
X --> XT[test_nnsplace]
X --> XS[nnsplace_standalone]
C --> CT[NnsPlaceTests]
C --> CS[NnsPlaceStandalone]
XT --> OK1[doctest 31/31 โ]
CT --> OK2[ctest 100% โ]
style SRC fill:#fff3e0,stroke:#e65100,stroke-width:3px,color:#2e3440
style X fill:#e3f2fd,stroke:#1565c0,stroke-width:3px,color:#2e3440
style C fill:#e3f2fd,stroke:#1565c0,stroke-width:3px,color:#2e3440
style XT fill:#c8e6c9,stroke:#2e7d32,stroke-width:3px,color:#2e3440
style CT fill:#c8e6c9,stroke:#2e7d32,stroke-width:3px,color:#2e3440
style XS fill:#c8e6c9,stroke:#2e7d32,stroke-width:3px,color:#2e3440
style CS fill:#c8e6c9,stroke:#2e7d32,stroke-width:3px,color:#2e3440
style OK1 fill:#c8e6c9,stroke:#2e7d32,stroke-width:3px,color:#2e3440
style OK2 fill:#c8e6c9,stroke:#2e7d32,stroke-width:3px,color:#2e3440
]
- xmake for **fast local iteration** โก; CMake/CPM for the **published** repo &
install (`find_package`) ๐ฆ.
---
### ๐งพ Deliberate Deviations (documented in AGENTS.md)
1. **`"edges"` reader**: netlistx-cpp expects `"links"`; we keep a small
`read_json_edges` in `include/nnsplace/readwrite.hpp` (Python testcases use
networkx `"edges"`) ๐งพ
2. **Vendored `pyfractions.hpp`**: zero-divisor-safe overflow guards ๐ฉน
3. **Integer module ids only** (C++ `SimpleNetlist`) ๐ฏ
4. **RNG**: `std::mt19937` with a fixed seed; tests assert legality +
improvement, never bit-exact worst values ๐ฒ
---
### ๐งญ What's Next?
- ๐ **Upstream** the `fractions-cpp` zero-divisor fix (drop the vendored copy)
- ๐ **GitHub Actions** workflows: Ubuntu / macOS / Windows + coverage
- ๐๏ธ **Doxygen** docs target + `GenerateDocs` deploy to GitHub Pages
- ๐ **Benchmarks** (nanobench) against the Python baseline
- ๐ง Optional: CMake `find_package` **install** test (`test_installed`)
- ๐งฎ Revisit pad-aware optimization (the Python `TODO`s stay TODO)
---
count: false
class: nord-dark, middle, center
# ๐ Q&A
### Ask me anything about the port ๐ค

---
count: false
class: nord-dark, middle, center
# ๐ Thank You!
### ๐ Porting `nnsplace` from Python to C++
Reuse the ecosystem ๐งฌ ยท Preserve semantics ๐ฌ ยท Verify against the reference ๐ค

Slides built with Remark.js ๐ ๏ธ | KaTeX ๐งฎ | Mermaid ๐ | Nord Theme ๐