{
...
for (_, vrj) in vrsc.iter().enumerate().filter(|t| t.0 != i) {
suppress_old(..., vrj);
if autocorr { // โ the only difference
let vrjn = Vector2::new(-vrj.x_, 1.0) / vrj.y_;
suppress_old(..., &vrjn);
}
}
...
}
```
- ๐ 4 Bairstow jobs โ 2 ยท โ40 lines ยท all gates green
- ๐ค ...but the `if autocorr` lives **inside** the per-root, per-iteration loop
---
### ๐จ Why the Flag Is Wrong
The check runs **once per neighbor, per factor, per iteration** โ the hottest loop in the solver:
.font-sm[
| Cost | Why |
|------|-----|
| โก Branch in the hot loop | evaluated m ร (mโ1) ร niter times โ even when never needed |
| ๐ Coupled evolution | even & autocorr must change in lockstep forever |
| ๐ง False strategy | `autocorr` is NOT a mode โ no call changes it at runtime |
]
The reciprocal image it suppresses:
$$ \hat{v}_j = \bigl(-v_{j,x},\; 1\bigr) \big/ v_{j,y} $$
> The flag dedup parameterized the **variation point** with a bool โ the very thing Strategy exists to avoid.
---
### ๐ฏ The Correct Cut: Compile-time vs Runtime
.mermaid[
graph TD
V["๐ What varies?"] --> R["๐ runtime\nmode: st / mt / atomic"]
V --> C["๐ท๏ธ compile-time\nalgorithm variant: even / autocorr"]
R --> F["SolveMode enum\n(facade) โ flag is fine"]
C --> F2["separate functions\nโ a bool is wrong"]
style V fill:#e8eaf6,stroke:#283593,color:#2e3440,stroke-width:3px
style R fill:#fff9c4,stroke:#f57f17,color:#2e3440,stroke-width:3px
style C fill:#ffcdd2,stroke:#c62828,color:#2e3440,stroke-width:3px
style F fill:#c8e6c9,stroke:#2e7d32,color:#2e3440,stroke-width:3px
style F2 fill:#c8e6c9,stroke:#2e7d32,color:#2e3440,stroke-width:3px
]
- โ
**Runtime-varying** โ parameter/enum is honest (the facade's `SolveMode`)
- โ **Compile-time property** โ two functions/types, never a bool
- The branch cost is paid on **every call**, but the answer never changes
---
### ๐ The Reference Never Had This Problem
ginger-cpp kept two step structs:
```cpp
struct even_bairstow_step { // plain suppression
for (const auto jdx : neighbors) {
suppress_old(vA, vA1, vri, get(jdx));
}
};
struct autocorr_bairstow_step { // reciprocal suppression, always on
for (const auto jdx : neighbors) {
const auto vrj = get(jdx);
suppress_old(vA, vA1, vri, vrj);
const auto vrjn = Vector2(-vrj.x(), 1.0) / vrj.y();
suppress_old(vA, vA1, vri, vrjn); // reciprocal โ no flag
}
const auto vrin = Vector2(-vri.x(), 1.0) / vri.y();
suppress_old(vA, vA1, vri, vrin);
};
```
- ๐๏ธ templated `Get/Set/Neighbors` serves all three policies
- ๐ฏ duplicated body is the price โ the hot path stays straight-line
---
### ๐ฆ Rust: Split the Jobs Back
```
pbairstow_job(coeffs, i, vri, converged, vrsc, autocorr: bool)
โโ pbairstow_even_job(coeffs, i, vri, converged, vrsc)
โโ pbairstow_autocorr_job(coeffs, i, vri, converged, vrsc)
pbairstow_atomic_job(coeffs, i, buffer, autocorr: bool)
โโ pbairstow_even_atomic_job(coeffs, i, buffer)
โโ pbairstow_autocorr_atomic_job(coeffs, i, buffer)
```
- The 6 public wrappers just pass the right job โ **public API unchanged**
- Bodies restored from the pre-dedup commit ยท **+92 / โ38**
- `cargo test`: 156 โ
ยท clippy: only the pre-existing `ptr_arg` exceptions
---
### ๐ Python: Split the Steps Back
```
_bairstow_step(coeffs, degree, i, vri, vrs, robin, autocorr, tol_ind)
โโ _bairstow_even_step(coeffs, degree, i, vri, vrs, robin, tol_ind)
โโ _bairstow_autocorr_step(coeffs, degree, i, vri, vrs, robin, tol_ind)
```
The shared Gauss-Seidel loop becomes a **policy over the step**:
```python
_bairstow_solve(coeffs, vrs, options, step) # loop: no flag anywhere
pbairstow_even โ _bairstow_solve(..., _bairstow_even_step)
pbairstow_autocorr โ _bairstow_solve(..., _bairstow_autocorr_step)
```
- **+70 / โ27** ยท `pytest`: 86 โ
ยท coverage 97% (rootfinding 100%)
- Same shape as C++ and Rust โ **one architecture, three languages**
---
class: nord-light, middle, center
## ๐ฌ Part 2 โ The Trade-off Lens
How to judge any refactor: what it bought, what it cost, and how to tell.
---
### ๐ฌ Bought / Paid / Verdict
Every pattern is a trade. Name all three sides *before* touching code:
.font-sm[
| Side | Question |
|------|----------|
| โ
**Bought** | what duplication or indirection was removed? |
| โ **Paid** | what seam, branch, or API surface was added? |
| โ๏ธ **Verdict** | does the seam hold โ or does it leak? |
]
**The deciding factors** ๐
- ๐ **Frequency** โ a flag checked once per call is configuration; per inner-loop-iteration is a branch
- ๐งญ **Where the seam is** โ the seam is the design; a flag at the seam is the symptom
- ๐ **Currency** โ line counts are the metric to distrust; seam quality is the metric to inspect
---
class: nord-light, middle, center
## โ๏ธ Part 3 โ The Catalogue
The Part 4 patterns, judged through the lens.
---
### ๐ Strategy โ execution policies
The per-root job is one Newton step; the policy owns the loop:
$$ v_i^{(k+1)} = v_i^{(k)} - \delta\bigl(A, v_i^{(k)}, A_1\bigr) $$
.mermaid[
graph LR
S["๐ฏ per-root job\none Newton step"] --> P1["๐ถ sequential\nGauss-Seidel"]
S --> P2["๐ jacobi_mt\nsnapshot + rayon"]
S --> P3["โ๏ธ atomic\ndecoupled buffer"]
style S fill:#e8eaf6,stroke:#283593,color:#2e3440,stroke-width:3px
style P1 fill:#c8e6c9,stroke:#2e7d32,color:#2e3440,stroke-width:3px
style P2 fill:#fff9c4,stroke:#f57f17,color:#2e3440,stroke-width:3px
style P3 fill:#e3f2fd,stroke:#1565c0,color:#2e3440,stroke-width:3px
]
.font-sm[
| Bought โ
| Paid โ |
|----------|---------|
| one loop per mode | the **job contract** is now the bottleneck for new modes |
| per-root math written once | Rust: atomic uses a *different* job shape โ two job families |
| bug fixed in one place | the `from` parameter exists only to preserve pinned counts |
]
- ๐ฏ **The seam is the design** โ and the flag was the symptom of the seam
---
### ๐ฏ Template Method โ `scan_constraints`
The skeleton owns the traversal; each band injects only its check:
.font-sm[
```cpp
template
auto scan_constraints(const Arr& mat, std::size_t& idx, std::size_t count,
const Arr& x, Check&& check) -> std::optional {
for (auto i = 0U; i != count; ++i) {
if (idx == count) { idx = 0; } // round robin, ONCE
const auto k = idx++;
const auto v = dot_row(mat, k, x);
if (auto cut = check(k, v)) { return cut; }
}
return std::nullopt;
}
```
]
.font-sm[
| Bought โ
| Paid โ |
|----------|---------|
| round-robin idiom written once | skeleton **fixes the traversal** โ round-robin, one `dot_row` per row |
| all bands share bug fixes | control flow inverted into **stateful closures** (`fmax`, `imax`, โฆ) |
]
- โ
Right trade **only because** passband / stopband / nonredundant share the round-robin invariant
---
### ๐ญ Facade โ `solve_*` auto-dispatch
```rust
pub fn solve_pbairstow_even(..., mode: SolveMode) -> (usize, bool) {
match mode {
SolveMode::Automatic =>
if should_parallelize(vrs.len()) { pbairstow_even_mt(..) }
else { pbairstow_even(..) },
...
}
}
```
.font-sm[
| Bought โ
| Paid โ |
|----------|---------|
| one entry point per solver | API **surface grew** โ 12 functions stay + 4 facades |
| "when to go parallel" in one place | `PARALLEL_THRESHOLD` is a **hidden global policy** |
| callers can't get the mode wrong | `solve_aberth_autocorr` ignores `mode` โ a no-op arm |
]
- โ๏ธ **Centralized policy vs explicit determinism** โ `Automatic` surrenders pinned counts to a global threshold
---
### ๐๏ธ Dedup โ Three Instances, Three Verdicts
.font-sm[
| Instance | What was merged | Trade | Verdict |
|----------|-----------------|-------|---------|
| `roots_from_quadratic` | two identical statics | cross-module dependency | โ
worth it |
| `aberth_job` / `aberth_job2` | byte-identical closures | trivial generalization | โ
worth it |
| `pbairstow_job(autocorr)` | even + autocorr | **branch in hot loop** ยท coupled evolution | โ reverted |
]
- ๐ฏ Dedup is only free when the shared part is truly invariant
- ๐จ Here the shared part was the *loop body*, but the difference was *inside* the loop
---
### ๐ข Constants & Registries
.font-sm[
| Pattern | Bought โ
| Paid โ |
|---------|----------|---------|
| named constants | magic numbers named & greppable | "same constant, three names" = **three copies** โ drift silently |
| strategy registries | dispatch as data; unknown keys error | **indirection** + runtime typos; for sets of 2โ3 the if/else was fine |
]
> YAGNI cuts both ways: registries pay off only for open-ended sets
---
class: nord-light, middle, center
## ๐ Part 4 โ Retro-Analysis
Parts 1โ5 through the lens: the series' own flags.
---
### ๐ The Selection Spectrum
Every mechanism the series used, from compile-time to runtime-bool:
.font-sm[
| Mechanism | Where | Checked at | Verdict |
|-----------|-------|-----------|---------|
| `if constexpr (Stable)` | `EllBase` (P3) | compile time | โ
right |
| two step structs | `autocorr_bairstow_step` (P4โP5) | compile time | โ
right |
| macro codegen | `impl_search_space!` (P3) | compile time | โ
right |
| hook injection | `find_concave_point` (P1) | once per call | โ
right |
| bool at base case | `has_triangle_base_case` (P1) | once per recursion | ๐ก tolerable |
| enum in the loop | `PivotPolicy` (P3) | once per row | ๐ borderline |
| **bool in the loop** | **`pbairstow_job(autocorr)`** (P4) | **per neighbor per iteration** | โ **reverted** |
]
- ๐ฏ The revert wasn't an anomaly โ the series was converging on this rule
- ๐ **Frequency decides**: once per call is configuration; per inner-loop-iteration is a branch
- ๐ฌ refactor5 (digraphx ยท netoptim) re-runs this trade twice โ next slide
---
### ๐ refactor5: The Flag-Heaviest Deck
The digraphx & netoptim session extends the spectrum with five more mechanisms:
.font-sm[
| Mechanism | Where | Checked at | Verdict |
|-----------|-------|-----------|---------|
| template `update_ok` lambda | C++ `relax_pred` | compile time, per edge | โ
right |
| **`&dyn Fn` gate** | Rust `relax_pred_core` | **vtable per edge** | ๐ the Rust leak |
| `direction: str` | Python `howard_search` | once per call | ๐ก tolerable |
| `verify: bool` | Python `howard_search` | once per found cycle | ๐ก tolerable |
| `minimize` / `pick_one_only` | `_run_loop` | per sweep iteration | ๐ borderline |
| `alternate_direction` | `_run_loop` | per sweep (runtime state) | โ
defensible |
]
- ๐ **The always-true lambda** โ the unconstrained finder now pays a per-edge gate; Rust dispatches it through `dyn`, C++ inlines it
- ๐ **Two bools in `_run_loop`** โ Max/Min solvers are distinct algorithms; only `alternate_direction` is genuine runtime state
- ๐ฏ The series is self-consistent in its inconsistency: every deck extracts "the one thing that varies" โ as a function, template, `dyn` closure, string, or bool
---
### ๐ The Flag's Cousin: `PivotPolicy` (Part 3)
`factor_impl` threads a policy through the **per-row LDL^T sweep** โ same shape as the autocorr flag:
```cpp
template
auto factor_impl(Fn&& f, PivotPolicy policy) -> bool {
for (std::size_t i = 0; i != n; ++i) {
// ... 40 lines of identical row sweep ...
if (d < 0) { return true; }
if (d == 0 && policy == PivotPolicy::AllowSemidefinite) {
start = i + 1; // โ the only difference, per row
}
}
return false;
}
```
Why it survived:
.font-sm[
| Advantage | Why it matters |
|-----------|----------------|
| ๐ฆ bigger dedup | ~40 lines ร 3 languages |
| โก cheaper predicate | one `d == 0` per row โ not per neighbor |
| ๐ท๏ธ an enum, not a bool | self-documenting at the call site |
]
โ ๏ธ **Still Part-5's standard:** the policy is compile-time-fixed per entry point. If the sweep becomes hot โ specialize.
---
### ๐ก When a Bool Is Fine (Part 1)
`rpolygon_cut` merged three recursive decomposers โ the real differences were **hooks**, only one edge case stayed a flag:
```python
def rpolygon_cut_recur(v1, lst, is_anticlockwise, rdll,
find_concave_point, has_triangle_base_case):
if has_triangle_base_case and len(lst) == 3: # โ once per recursion
return [v1] # not per neighbor
vcurr = find_concave_point(v1, ...) # โ the real hooks
```
- ๐ข checked once per **recursive call**
- ๐งญ guards a base-case *specialization*, not the per-root math
- ๐ the three real differences were **functions**, not flags
> **Frequency ร semantics**: a bool guarding a base case is configuration; a bool in the Newton loop is a branch.
---
### ๐ง Seams That Leak: Mirrors, Quirks, Cruft
"Don't change the public API" left seams open across Parts 1โ3:
.font-sm[
| Seam | Pattern | The leak |
|------|---------|----------|
| `idx1/idx2/idx3` public fields (P3) | `RoundRobin` | helper + public mirrors must stay in sync |
| `NoEffect + !more_alt โ niter` (P3) | `OptimQState` | a historical quirk preserved *exactly* |
| `from: 0` vs `from: 1` (P4) | `sequential_run` | count quirks threaded through every call |
| `has_triangle_base_case` (P1) | `rpolygon_cut` | a 4th "hook" that is a bool, not a function |
]
- ๐ฏ Compatibility is bought with surface โ every preserved quirk is that bill
- ๐ An abstraction that absorbs a historical accident makes it **permanent**
---
### ๐ Line-Currency Patterns โ the "Wins" That Weren't
.font-sm[
| Pattern | Claimed win | The unmeasured cost |
|---------|------------|---------------------|
| ๐ญ `make_reader` (P2) | "open/closed โ zero edits to dispatch" | the factory is still an explicit `match` โ the switch moved |
| ๐งฐ parity Builder (P1) | Rust Builder "for parity" | `..Default::default()` already was one โ pure surface |
| โก 166 ร `#[inline]` (P2) | "inline the one-liners" | "test profile identical" โ **no measured speedup** |
| ๐ PEP 562 `__getattr__` (P2) | split without breaking imports | moved API invisible to IDE/mypy โ analysis tax |
]
- ๐งพ Justified by line counts or mechanical rules, not by seams
- ๐ "test profile identical" is the tell: no behavior, no benchmark
---
class: nord-light, middle, center
## ๐ฏ Part 5 โ Closing
The rules, the homework, the resources.
---
### ๐ Lessons Learned
.pull-left[
**What actually helped** ๐ก
- ๐ท๏ธ Name the pattern *before* touching code โ we named "Strategy" wrong
- ๐งช Exact iteration counts catch drift the moment it happens
- ๐ A sibling reference (ginger-cpp) settles design arguments
- ๐ Retro-analysis โ Parts 1โ5 already had the full spectrum
- ๐ Reverting is cheap **when the halves are semantically separate**
]
.pull-right[
**The corrected rule** ๐ฏ
- If the "one thing that varies" never varies at runtime โ **two functions, not a flag**
- Extract the skeleton, inject the check โ never a bool in the loop
- Frequency decides: once per call is config; per-iteration is a branch
- Judge by **seams**, not line counts
]
---
### ๐ฏ Try It
- ๐ Find a `bool` parameter in your core loop
- โ Does it change at runtime? Or is it a property of the algorithm?
- ๐๏ธ If compile-time โ split into two functions, let the caller choose
- ๐ Keep the public API unchanged โ the wrapper picks the variant
- ๐ Judge the result by its seams, not its line count
> "Every root converges โ the question is only how you schedule the sweeps.
> And whether your flag belongs inside the loop." ๐
---
### ๐ Resources
.pull-left[
**Patterns**
- ๐งฉ _Design Patterns_ โ Gamma, Helm, Johnson, Vlissides
- ๐ง _Refactoring_ โ Martin Fowler
- โ๏ธ _A Philosophy of Software Design_ โ Ousterhout
**Repos**
- ๐งฎ github.com/luk036/ginger-cpp
- ๐ github.com/luk036/ginger
- ๐ฆ github.com/luk036/ginger-rs
]
.pull-right[
**This series**
- ๐งฉ Part 1: physdes ยท LDS
- ๐๏ธ Part 2: netlistx ยท ckpttn
- ๐ฌ Part 3: ellalgo ยท lmi-solver
- โ๏ธ Part 4: ginger ยท multiplierless
- ๐งฉ Part 5: digraphx ยท netoptim
- โ๏ธ **Part 6 (this talk): trade-offs + retro-analysis**
]
---
count: false
class: nord-dark, middle, center
# ๐ Thank You!
### Design is a Trade-off โ Part 6
@luk036 ๐จโ๐ป โ Questions welcome ๐ฌ