` + `if constexpr`
- Python: base class + Template Method `_update_core`
- Rust: `impl_search_space!` macro
**3๏ธโฃ Template Method: LMI oracles** ๐ฏ
- Three oracles, one assess skeleton
]
.pull-right[
**Part 2 โ State, Strategy & Facade** ๐
**4๏ธโฃ `OptimQState`** โ the retry-flag loop
- C++ / Rust / Python, one state machine
**5๏ธโฃ `RoundRobin`** โ the `idx += 1` idiom
- 8 duplicated counters โ one helper
**6๏ธโฃ Facade + Factory** ๐ญ
- `LMIProblem`, `make_lmi_oracle` / `make_lmi0_oracle`
**7๏ธโฃ lmi-solver family** ๐ฆ
- `factor_impl` + `PivotPolicy`
**8๏ธโฃ Lessons Learned** ๐
]
---
### ๐ค Why Refactoring Again?
Part 1 refactored *physdes & LDS*; Part 2 covered *netlistx & ckpttn*. This session applies the same recipe to **two new families** โ the algorithm libraries โ with a harder constraint:
.pull-left[
**New this time** โจ
- ๐งฌ **One algorithm, three languages** โ the exact same refactor must work in C++, Rust, and Python
- ๐ **"Don't change the public API"** โ the hard constraint
- ๐ **Cross-repo consistency** โ siblings must end up looking the same
- ๐งช **Regression-pinned** โ iteration counts locked in tests
]
.pull-right[
**Same discipline** โ
- ๐ฏ Name the pattern *before* touching code
- ๐ Baseline tests before any edit
- ๐ฆ Every gate green after every commit
- ๐งน Cleanup dead code first (P1)
]
---
### ๐ The Two Polyglot Families
Six siblings, two missions:
.pull-left[
.mermaid[
graph LR
ECPP["๐งฎ ellalgo-cpp\nC++ header-only"] --> E["๐๏ธ Ellipsoid Method\nsearch space + cutting plane"]
EPY["๐ ellalgo\nPython package"] --> E
ERS["๐ฆ ellalgo-rs\nRust crate"] --> E
style ECPP fill:#c8e6c9,stroke:#2e7d32,color:#2e3440,stroke-width:3px
style EPY fill:#fff9c4,stroke:#f57f17,color:#2e3440,stroke-width:3px
style ERS fill:#e3f2fd,stroke:#1565c0,color:#2e3440,stroke-width:3px
style E fill:#e8eaf6,stroke:#283593,color:#2e3440,stroke-width:3px
]
]
.pull-right[
.mermaid[
graph LR
LCPP["๐งฎ lmi-solver-cpp\nC++ LDLT oracles"] --> L["๐ฆ LMI Solver\noracle + factorization"]
LRS["๐ฆ lmi-solver-rs\nRust crate"] --> L
LPY["๐ ellalgo/oracles\nPython LDLTMgr"] --> L
style LCPP fill:#c8e6c9,stroke:#2e7d32,color:#2e3440,stroke-width:3px
style LRS fill:#e3f2fd,stroke:#1565c0,color:#2e3440,stroke-width:3px
style LPY fill:#fff9c4,stroke:#f57f17,color:#2e3440,stroke-width:3px
style L fill:#fce4ec,stroke:#880e4f,color:#2e3440,stroke-width:3px
]
]
The refactors happen **sibling-by-sibling** โ each keeps every public API byte-for-byte compatible.
---
class: nord-light, middle, center
## ๐๏ธ Part 1 โ Strategy/Bridge: the search space
---
### ๐งฌ The Smell: 95% Duplicated Ellipsoid
`Ell` (classic Q-update) and `EllStable` (LDL^T) expose **the same public API** โ same constructors, same `xc` / `set_xc` / `tsq` / `copy`, same three `update_*` entry points:
```
Ell (ell.hpp) ~270 lines
EllStable (ell_stable.hpp) ~270 lines
โโโ identical constructors โ
โโโ identical xc/set_xc/tsq/copy โ
โโโ identical update_bias_cut
โ /update_central_cut
โ /update_q wrappers โ
โโโ DIFFERENT _update_core โ โ the strategy
```
- โ Only the private core update differs โ direct Q vs LDL^T
- ๐ฃ Fix a bug in one โ forget to fix the other
- ๐ Adding a feature means editing **both** classes
---
### ๐ญ The Fix: `EllBase` (C++)
Compile-time **Strategy** tag selects the update path:
```cpp
// ell_base.hpp โ the shared public API, written ONCE
template
class EllBase {
public:
using Vec = std::valarray;
using ArrayType = Arr;
template
auto update_bias_cut(const std::pair& cut) -> CutStatus {
return this->_update_core(cut, [this](Vec& grad, const T& beta) {
if constexpr (Stable) { // โ the strategy switch
return this->_mgr.update_stable_bias_cut(grad, beta);
} else {
return this->_mgr.update_bias_cut(grad, beta);
}
});
}
// ... update_central_cut, update_q identical pattern
};
```
`Ell` / `EllStable` become ~60-line thin subclasses. **Net โ200 lines** in the C++ family. โ
---
### ๐ Python: base class + Template Method
Python can't do `if constexpr` โ so the base class defines the **Template Method** hook:
```python
# ell_base.py โ shared public API, written ONCE
class EllBase(SearchSpace[ArrayType]):
def update_bias_cut(self, cut: Cut) -> CutStatus:
return self._update_core(cut, self.helper.calc_single_or_parallel)
# update_central_cut, update_q identical pattern
def _update_core(self, cut, cut_strategy): # โ the hook
raise NotImplementedError
```
```python
# ell.py โ ONE strategy
class Ell(EllBase[np.ndarray]):
def _update_core(self, cut, cut_strategy): # direct Q update
...
```
```python
# ell_stable.py โ the OTHER strategy
class EllStable(EllBase[np.ndarray]):
def _update_core(self, cut, cut_strategy): # LDL^T update
...
```
`Ell` / `EllStable` are still imported from their original modules โ **public API untouched**. โ
---
### ๐ฆ Rust: no inheritance โ a macro
Rust has no base classes โ so the identical `SearchSpace` impl is generated once by a macro:
.font-sm[
```rust
// ell_common.rs โ the macro body holds the shared impl ONCE,
// with a type metavariable standing in for Ell / EllStable
// ell.rs / ell_stable.rs โ the invocation (two lines total)
impl_search_space!(Ell);
impl_search_space!(EllStable);
// What the macro expands to for each type (written once in the macro):
impl crate::cutting_plane::SearchSpace for Ell {
type ArrayType = crate::arr::Arr;
fn xc(&self) -> &Self::ArrayType { &self.xc }
fn tsq(&self) -> f64 { self.tsq }
fn update_bias_cut(&mut self, cut: &(Self::ArrayType, T))
-> crate::cutting_plane::CutStatus
where T: crate::cutting_plane::UpdateByCutChoice {
let (grad, beta) = cut;
beta.update_bias_cut_by(self, grad)
}
// ... update_central_cut, update_q, set_xc
}
```
]
- โ
Same public `SearchSpace` behavior
- ๐งฑ Macro paths fully-qualified for hygiene
- ๐ The only hand-written difference: private `update_core`
---
### โ
P1 First: Cleanup Dead Code
Before any pattern, remove what's already dead:
- ๐ง Removed redundant `enum class CutStatus;` forward declarations (C++)
- ๐งน `calc_parallel_cut_fast_old` **kept** โ tested public API, not dead
- ๐ `CInfo` / `Options::verbose` / `CutStatus::Unknown` kept โ public surface
> Rule: **cleanup โ delete**. Only remove what is provably unreachable *and* not public.
---
class: nord-light, middle, center
## ๐ฏ Part 2 โ Template Method: the LMI oracles
---
### ๐ฏ The Smell: Three Identical `assess_feas`
`LmiOracle`, `Lmi0Oracle`, `LmiOldOracle` โ same pipeline, three copies:
```
assess_feas(x):
getA = ... โ varies: F0โฮฃ, ฮฃ, explicit matrix
if factor(getA) โ SPD? return None โ fixed
ep = witness() โ fixed
g[i] = sign * sym_quad(F_i) โ varies: +1 or โ1
pack cut โ return โ fixed
```
- โ ~30 lines duplicated ร 3
- ๐ฃ Sign bug in one oracle = subtle wrong cut in that family only
- ๐ Adding a 4th oracle = copy-paste again
---
### ๐ฏ C++: `LmiOracleBase` skeleton
The **Template Method** lives once; oracles supply `getA` + sign:
```cpp
// lmi_oracle_base.hpp
template
class LmiOracleBase {
protected:
std::unique_ptr cut = std::make_unique();
template
auto assess_impl(LDLT& mgr, const std::vector& F, const int sign,
const Arr036& x, Fn&& getA) -> Cut* {
if (mgr.factor(std::forward(getA))) return nullptr;
const auto ep = mgr.witness(); // call before sym_quad() !!!
Arr036 g{x};
for (auto i = 0U; i != n; ++i) g[i] = sign * mgr.sym_quad(F[i]);
this->cut->first = std::move(g);
this->cut->second = std::move(ep);
return this->cut.get();
}
};
```
Each oracle keeps its exact constructor + public members โ **`Lmi0Oracle::_mq` stays public**. โ
---
### ๐ Python: `LMIBase._assess`
Python mirrors it with a mixin-style base:
```python
# lmi_oracle_base.py
class LMIBase:
def _assess(self, get_elem, sign) -> Optional[Cut]:
if self.ldlt_mgr.factor(get_elem):
return None # feasible
ep = self.ldlt_mgr.witness()
g = np.array([sign * self.ldlt_mgr.sym_quad(Fk) for Fk in self.mat_f])
return g, ep
```
```python
# lmi0_oracle.py โ negative sign, no constant term
class LMI0Oracle(LMIBase):
def assess_feas(self, x):
def get_elem(i, j):
n = len(x)
return sum(self.mat_f[k][i, j] * x[k] for k in range(n))
return self._assess(get_elem, -1) # โ only the strategy differs
```
โ
Constructors, `assess_feas` signatures, and attributes unchanged.
---
### ๐ฆ Rust: a shared free function
Rust has no inheritance โ the skeleton becomes a `pub(crate)` function:
```rust
// lmi_oracle_base.rs
pub(crate) fn assess_feas_impl(
ldlt_mgr: &mut LDLTMgr,
mat_f: &[Arr],
xc: &Arr,
sign: f64,
get_elem: impl Fn(usize, usize) -> f64,
) -> Option<(Arr, f64)> {
if ldlt_mgr.factor(get_elem) { return None; }
let ep = ldlt_mgr.witness();
let mut g = Arr::new(n);
for k in 0..n { g[k] = sign * ldlt_mgr.sym_quad(&mat_f[k]); }
Some((g, ep))
}
```
```rust
// lmi_oracle.rs
let result = assess_feas_impl(&mut self.ldlt_mgr, &self.mat_f, xc, 1.0, getA);
result.map(|(g, ep)| (g, SingleCut(ep)))
```
One skeleton, three languages, **zero API drift**. ๐
---
class: nord-light, middle, center
## ๐ Part 3 โ State: the `optim_q` loop
---
### ๐ The Smell: A Retry Flag Scattered
`cutting_plane_optim_q` hand-rolled its state machine inline:
```python
x_best = None
retry = False
for niter in range(options.max_iters):
cut, x_q, gamma1, more_alt = omega.assess_optim_q(space_q.xc(), gamma, retry)
if gamma1 is not None:
gamma = gamma1
x_best = x_q
status = space_q.update_q(cut)
if status == CutStatus.Success: retry = False
elif status == CutStatus.NoSoln: return x_best, gamma, niter
elif status == CutStatus.NoEffect:
if not more_alt: return x_best, gamma, niter
retry = True
if space_q.tsq() < options.tolerance: return x_best, gamma, niter
```
- ๐ฃ Retry/termination logic spread across the loop โ easy to break
- ๐ Same pattern in 3 languages, subtly different return semantics
---
### ๐ The Fix: `OptimQState` (all three languages)
Extract the **State** machine โ one class, three ports:
```cpp
// C++ โ cutting_plane.hpp
template class OptimQState {
A x_best; bool retry = false;
public:
void on_shrunk(A x) { x_best = std::move(x); retry = false; }
auto on_update(CutStatus status, bool more_alt) -> Result {
switch (status) {
case Success: retry = false; return Continue;
case NoSoln: return NoSoln;
case NoEffect: return more_alt ? (retry = true, Continue) : NoMoreAlt;
case Unknown: return Continue;
}
}
};
```
---
```python
# Python โ cutting_plane.py
class OptimQState:
def on_shrunk(self, x_q): self.x_best = x_q
def on_update(self, status, more_alt) -> bool:
if status == CutStatus.Success: self.retry = False; return True
if status == CutStatus.NoSoln: return False
if status == CutStatus.NoEffect:
if not more_alt: return False
self.retry = True
return True
```
```rust
// Rust โ cutting_plane.rs
pub struct OptimQState { x_best: Option, retry: bool }
impl OptimQState {
pub fn on_shrunk(&mut self, x: A) { self.x_best = Some(x); self.retry = false; }
pub fn on_update(&mut self, status: &CutStatus, more_alt: bool) -> OptimQOutcome
{ ... }
}
```
โ ๏ธ Rust's `NoEffect+!more_alt` returns `niter` (not `max_iters`) โ **preserved exactly**. โ
---
class: nord-light, middle, center
## ๐ Part 4 โ Strategy: the round-robin idiom
---
### ๐ The Smell: `idx += 1; if idx == N`
The same round-robin counter appears **8+ times** across the family:
```cpp
this->idx += 1;
if (this->idx == 2) { this->idx = 0; } // round robin
```
```python
self.idx += 1
if self.idx == 2:
self.idx = 0 # round robin
```
```rust
self.idx += 1;
if self.idx == 2 { self.idx = 0; }
```
- ๐ `ProfitOracle` has 1, `LowpassOracle` has **3** (idx1/idx2/idx3)
- ๐ฃ Off-by-one when the wrap bound changes
---
### ๐ The Fix: `RoundRobin` helper
One small class, all three languages:
.pull-left[
.font-sm[
```cpp
// C++ โ round_robin.hpp
class RoundRobin {
std::size_t _lo, _hi, _cur;
public:
RoundRobin(std::size_t lo, std::size_t hi)
: _lo{lo}, _hi{hi}, _cur{hi - 1} {}
auto next() -> std::size_t {
if (++_cur == _hi) _cur = _lo;
return _cur;
}
};
```
```python
# Python โ round_robin.py
class RoundRobin:
def next(self) -> int:
self._cur += 1
if self._cur == self._hi:
self._cur = self._lo
return self._cur
```
]
]
.pull-right[
.font-sm[
```rust
// Rust โ round_robin.rs
// (clippy: rename next โ advance)
pub struct RoundRobin {
cur: i32, lo: i32, hi: i32
}
impl RoundRobin {
pub fn advance(&mut self) -> i32 {
self.cur += 1;
if self.cur == self.hi {
self.cur = self.lo;
}
self.cur
}
}
```
]
โ
`LowpassOracle` keeps its **public** `idx1/idx2/idx3` fields as mirrors; `ProfitOracle.idx` stays readable int. **No public API change.** ๐ซ
]
---
class: nord-light, middle, center
## ๐ญ Part 5 โ Facade + Factory
---
### ๐ญ The Fix: `LMIProblem` Facade + Factory
Users previously hand-wired the 3-step recipe every time:
```cpp
// Before: build oracle โ build space โ call driver
LmiOracle omega{3, F, B};
EllStable ellip{10.0, Vec{0.0, 0.0, 0.0}};
auto result = cutting_plane_feas(omega, ellip);
```
```cpp
// After: one facade call
auto problem = make_lmi_problem(3, F, B);
auto result = problem.solve_feas(10.0, Vec{0.0, 0.0, 0.0});
```
Plus uniform **Factory** entry points (additive, non-breaking):
```cpp
make_lmi_oracle(mat_f, mat_b) โ LmiOracle // lazy
make_lmi0_oracle(mat_f) โ Lmi0Oracle // compact
make_lmi_old_oracle(mat_f, mat_b) โ LmiOldOracle // explicit
```
---
```python
# Python โ same idea
problem = LMIProblem(mat_f, mat_b)
x, niter = problem.solve_feas(10.0, np.zeros(3))
```
```rust
// Rust โ same idea
let mut problem = LMIProblem::new(f, b);
let (x, niter) = problem.solve_feas(10.0, Arr::new(3), Options::default());
```
โ
Added new API only โ nothing removed, nothing renamed.
---
### ๐ญ Named Constructors
The confusing overloads get clear names:
```cpp
Ell::from_radii(val, xc) // per-axis radii (diagonal matrix)
Ell::from_alpha(alpha, xc) // scalar scaling factor
```
```python
# Python โ EllBase.from_radii / EllBase.from_alpha
```
```rust
// Rust already had new_with_scalar / from_covariance โ nothing to add
```
- ๐ง Self-documenting construction โ no more guessing which arg means what
- โ
Additive API, zero breakage
---
class: nord-light, middle, center
## ๐ฆ Part 6 โ lmi-solver family
---
### ๐ฆ The Smell: `factor` vs `factor_with_allow_semidefinite`
`lmi-solver-cpp` / `lmi-solver-rs` / `py/ellalgo` all duplicate the same LDL^T row-sweep:
```
factor(f) factor_with_allow_semidefinite(f)
start = 0 start = 0
for i in 0..n: for i in 0..n:
... row sweep ... ... row sweep ... โ ~40 lines identical
if d <= 0: stop if d < 0: stop
if d == 0: start = i+1 โ only difference
```
- โ ~40 duplicated lines ร 3 languages
- ๐ฃ The pivot policy is the *only* difference โ everything else is noise
---
### ๐ฆ The Fix: `factor_impl` + `PivotPolicy`
C++ and Rust use an enum policy; Python uses a bool โ same skeleton:
```cpp
// C++ โ ldlt_mgr.hpp
enum class PivotPolicy { Strict, AllowSemidefinite };
template auto factor(Fn&& f) -> bool {
return factor_impl(std::forward(f), PivotPolicy::Strict);
}
```
```rust
// Rust โ ldlt_mgr.rs
enum PivotPolicy { Strict, AllowSemidefinite }
fn factor_impl(&mut self, get_elem: F, policy: PivotPolicy) -> bool { ... }
```
```python
# Python โ ldlt_mgr.py
def _factor_impl(self, get_elem, allow_semidefinite: bool) -> bool: ...
def factor(self, get_elem): return self._factor_impl(get_elem, False)
def factor_with_allow_semidefinite(...): return self._factor_impl(get_elem, True)
```
โ
Public signatures, docstrings, and doctests untouched โ **zero behavior drift**.
---
### ๐ฆ Wait โ Where's the Python LMI Oracle Collapse?
`py/ellalgo` already had it from an earlier pass:
- โ
`LMIBase._assess` (Template Method) โ done
- โ
`RoundRobin` โ done
- โ
`OptimQState` โ done
So this pass only needed **`_factor_impl`** โ proof that the sibling-by-sibling recipe converges: once a family is clean, later passes are tiny. ๐ฏ
---
### ๐ Verification Discipline
Every refactor step was gated the same way:
| Gate | C++ | Rust | Python |
|------|-----|------|--------|
| Build | `cmake --build` ๐จ | `cargo build` ๐ | โ |
| Test | `ctest` ๐งช | `cargo test` ๐งช | `pytest` ๐งช |
| Lint | `-Werror` โ | `cargo clippy` ๐ | `flake8` ๐ |
| Format | `clang-format` ๐ | `cargo fmt` ๐ | `black + isort` ๐ |
| Docs | `doxygen` ๐ | `cargo doc -D warnings` ๐ | doctests ๐ |
- ๐ **Net line change**: C++ โ288, Rust โ50 (ellalgo) & โ20 (lmi), Python โ100+
- ๐ Every "except" was explicit (e.g. keep `calc_parallel_cut_fast_old`)
---
class: nord-light, middle, center
## ๐ Lessons Learned
---
### ๐ Lessons Learned
.pull-left[
**Patterns that carried across languages** ๐งฉ
- ๐๏ธ **Strategy/Bridge** โ `EllBase` / macro / base class
- ๐ฏ **Template Method** โ oracle skeleton everywhere
- ๐ **State** โ `OptimQState` (C++/Rust/Python)
- ๐ **Strategy** โ `RoundRobin`
- ๐ญ **Facade + Factory** โ `LMIProblem`, `make_lmi_*`
**Language idioms matter** ๐บ
- Rust: no inheritance โ macro/free fn/trait
- Python: no `if constexpr` โ Template Method hook
- C++: `if constexpr` / `virtual` โ both work
]
.pull-right[
**What actually helped** ๐ก
- ๐ **"Don't change the public API"** forces honest refactors
- ๐งช **Regression-pinned iteration counts** catch drift instantly
- ๐ **Clippy caught a real naming trap** โ `next()` vs `Iterator::next`
- ๐งน Cleanup-first (P1) makes the patterns visible
- ๐ **Duplicate-then-diverge** is the #1 smell to hunt
**The takeaway** ๐ฏ
- Find the *one* thing that varies โ extract it
- Keep the skeleton fixed, inject the strategy
]
---
### ๐ Resources
.pull-left[
**Patterns**
- ๐งฉ _Design Patterns_ โ Gamma, Helm, Johnson, Vlissides
- ๐ง _Refactoring_ โ Martin Fowler
- ๐ฆ _Rust Design Patterns_ โ rust-unofficial
**Repos**
- ๐งฎ github.com/luk036/ellalgo-cpp
- ๐ github.com/luk036/ellalgo
- ๐ฆ github.com/luk036/ellalgo-rs
- ๐ฆ github.com/luk036/lmi-solver-cpp ยท lmi-solver-rs
]
.pull-right[
**Try it** ๐ฏ
- Find one duplicated loop / switch in your codebase
- Name the pattern that removes it
- Extract the skeleton, inject the strategy
- **Do not change the public API** ๐
> "The joy of the ellipsoid method is that it never gives up โ neither should your refactor." ๐
]
---
count: false
class: nord-dark, middle, center
# ๐ Thank You!
### Refactoring with Design Patterns โ Part 3
@luk036 ๐ง๐ป โ Questions welcome ๐ฌ