16. DSED benchmarks¶
This chapter is what backs the headline claim: DSED is up to 24× faster than PWL on real switched-mode converters. The whole chapter is "the numbers and what they mean" — no algorithms (those were chapters 11-14), no architecture (chapter 15).
The chapter is structured top-down:
- §16.1 — the single-converter headline (buck CCM, 24×)
- §16.2 — bridge-by-bridge per-step decomposition (where the 24× came from)
- §16.3 — geo-mean across 6 converter topologies (14.5×)
- §16.4 — comparison context (PWL chapter 8, what DSED adds)
- §16.5 — when DSED doesn't win (honest limits)
- §16.6 — reproducing the numbers
All measurements are on an Apple M1 (8-core, 16 GB), Pulsim built
in release mode (scikit-build-core with -O3 -DNDEBUG -flto=thin
+ -arch arm64), Python 3.13.
16.1 The headline: buck CCM, 5 ms, 100 kHz¶
import pulsim as p
V_in, D = 24.0, 0.5 # buck CCM 24V → 12V
L, C, R = 100e-6, 100e-6, 2.4 # standard CCM design
b = p.CircuitBuilder()
b.add_voltage_source("Vin", "in", "gnd", V_in)
b.add_switch("SW_HS", "in", "sw", 1e6, 1e-9)
b.add_switch("SW_LS", "sw", "gnd", 1e6, 1e-9)
b.add_inductor("L", "sw", "out", L)
b.add_capacitor("C", "out", "gnd", C)
b.add_resistor("R", "out", "gnd", R)
sf = p.NativePwm2Switch(T_sw=1e-5, D=D, n_switches=2)
# DSED — variable-step + event-driven + native everything
res = p.simulate(b, t_end=5e-3, engine='dsed', switch_fn=sf)
# → 2.21 ms wall-clock, 1007 steps, final v_C = 12.0000 V
# PWL — fixed-step trap (the previous default)
res = p.simulate(b, t_end=5e-3, dt=100e-9, switch_fn=sf)
# → 53.7 ms wall-clock, 50001 steps, final v_C = 12.0000 V
| Engine | Wall-clock | Steps | per-step | \(v_{C,\text{final}}\) | Speedup |
|---|---|---|---|---|---|
| PWL fixed-step trap | 53.73 ms | 50,001 | 1.07 µs | 12.0000 V | 1.0× (baseline) |
| DSED (Bridge.12) | 2.21 ms | 1,007 | 2.19 µs | 12.0000 V | 24.3× |
Both engines converge to the analytical \(D \cdot V_{\text{in}} = 12.0000\) V steady-state. The 24× comes from 50× fewer steps at 2× the per-step cost — exactly the structural prediction from chapter 11.
16.2 Per-step decomposition across bridges¶
The 24× headline took 4 bridges to land. Each bridge moved one piece of the per-step hot loop from Python to C++. Here's the full ladder:
| Bridge | Description | Per-step | Wall-clock | vs PWL |
|---|---|---|---|---|
| — | PWL (baseline) | 1.07 µs | 53.73 ms | 1.0× |
| 5 | Pure-Python scheduler + Python adapter | 60.8 µs | 61.26 ms | 0.85× |
| 10 | C++ scheduler + Python adapter | 22.2 µs | 22.40 ms | 2.3× |
| 11 | C++ scheduler + C++ adapter | 3.80 µs | 3.83 ms | 13.3× |
| 12 | + NativePwm2Switch (no GIL on gate edges) |
2.19 µs | 2.21 ms | 24.3× |
Where the time went at each bridge¶
Bridge.5 (pure Python, 60.8 µs/step):
- 6× adapter.rhs(t,x) — Python+numpy: ~30 µs
- Scheduler control flow (while, dict): ~10 µs
- PI controller + Hermite + Illinois: ~10 µs
- Misc: ~10 µs
Bridge.10 (C++ scheduler, Python adapter, 22.2 µs/step):
- 6× GIL-acquire + Python rhs() call: ~18 µs (bottleneck!)
- C++ scheduler control: ~0.5 µs
- numpy round-trip overhead: ~3 µs
- Misc: ~0.7 µs
Bridge.11 (full native adapter, 3.80 µs/step):
- C++ adapter.rhs(t,x) (pure C++ Eigen gemv): ~1.5 µs
- compute_*_b_extra pool walk: ~0.4 µs
- PI + Hermite + Illinois (all C++): ~0.5 µs
- PySwitchFnSSM::next_edge_after (Python): ~1.0 µs (residual bottleneck)
- Misc: ~0.4 µs
Bridge.12 (NativePwm2Switch, 2.19 µs/step):
- C++ adapter.rhs(t,x): ~1.5 µs
- compute_*_b_extra pool walk: ~0.4 µs
- PI + Hermite + Illinois: ~0.2 µs
- next_edge_after (now native): ~0.05 µs
- Misc: ~0.04 µs
Each bridge eliminated a different category of overhead. After Bridge.12, the per-step cost is dominated by actual numerical work (the gemv) — there's not much fat left to trim.
16.3 Geometric mean across 6 converter topologies¶
Buck CCM is the canonical benchmark. To show the speedup
generalises, the bench sweep at
scripts/bench_dsed_vs_pwl.py runs the same comparison on 6
representative topologies:
| Converter | DSED (ms) | PWL (ms) | Speedup | DSED steps | PWL steps |
|---|---|---|---|---|---|
| Buck CCM 24V→12V 100 kHz | 2.76 | 53.89 | 19.5× | 1,007 | 50,001 |
| Boost 12V→24V 100 kHz | 5.65 | 106.32 | 18.8× | 2,007 | 100,001 |
| Buck-boost 24V→−24V 100 kHz | 2.68 | 52.34 | 19.6× | 1,007 | 50,001 |
| Half-bridge + sine V_in (1 kHz) | 4.41 | 53.22 | 12.1× | 1,007 | 50,001 |
| Floating-cap RLC discharge (DC) | 0.73 | 9.26 | 12.7× | 435 | 10,001 |
| NPC split-bus 100 V (2 caps DC) | 0.53 | 4.40 | 8.3× | 507 | 5,001 |
Geometric mean: 14.46× (min 8.3×, max 19.6×).

Figure 16.1 — DSED Bridge.12 vs PWL Bridge.13 wall-clock speedup
across 6 converter topologies (left), and the structural reason
(step-count ratio, right). Dashed line: geometric-mean 14.5×.
Regenerable from _figures/fig161_dsed_topology_sweep.py.
These numbers were measured with the Bridge.12 native-PWM path through DSED and the Bridge.13 native-PWM-aware path through PWL — both engines run at their respective optima.
Reading the spread¶
- Best case (~19×): switched DC-DC converters at high f_sw. Buck/boost/buck-boost are exactly the algorithm's sweet spot: smooth between gate edges, few modes (2 per phase), no event prediction surprises.
- Mid (~12×): time-varying source overlays + non-switched
decay. The half-bridge + sine V_in pays the
compute_sine_b_extrawalk per step (chapter 15 §15.7 — Bridge.14 candidate). Floating-cap RLC decay has no switching events at all → DSED's adaptive step grows aggressively (435 steps for 1 ms of pure exponential decay) but per-step cost is the same. - Worst (~8×): rank-deficient floating-cap stack. NPC's 2-cap split has eigenvalue near zero (cap voltages can drift among each other with no resistive path between midpoints). The adaptive step shrinks to control the near-zero mode → 507 steps for 5 ms, smaller "leverage" over PWL's fixed dt.
The honest summary: DSED wins on every topology tested, but the speedup band is 8× – 20× depending on how event-rich / smooth / rank-structured the system is.
16.4 Context: where DSED sits vs the rest of Pulsim¶
Chapter 4 (PWL cache) gave the PWL engine its 10-50× speedup over the v1 stamping-per-step codebase. Chapter 7 (path-based partial refactor) gave the PWL engine another 2-4× on parameter sweeps and multi-mask workloads. DSED gives another 14× on top of that on the converters where it applies.
The stack:
v1 codebase (stamp per step)
↓ ~50× from chapter 4 (PwlStateSpaceCache)
PWL engine v2 (cached LU per mask)
↓ ~2-4× from chapter 7 (path-based refactor)
PWL engine v2 + rank-1
↓ ~14× from this chapter (DSED engine)
DSED engine v1.6.0
Multiplying through: DSED is ~1400× faster than v1's stamping-per-step kernel on the converters where all three optimisations apply (switched, simple-mode-graph, LTI per mask).
The three optimisations are structurally orthogonal:
- Chapter 4: pre-build per-mask, eat the assembly cost once.
- Chapter 7: when masks change by 1 bit, update the LU instead of refactoring.
- This chapter: between mask changes, take adaptive-step ODE instead of fixed \(\Delta t\).
16.5 When DSED doesn't win¶
The chapter would be dishonest without a "where the algorithm breaks down" section.
Nonlinear devices (Shockley diodes, MOSFET V_th, saturable L)¶
DSED's state-space extractor (chapter 12) requires LTI per mask. Nonlinear devices need per-operating-point linearization that the PED engine doesn't model. Users with those circuits have two options:
- Use
pulsim.dsed.run_user_lti(system, switch_fn, x0, t_end, ...)with a hand-rolled LTI system (validated on buck DCM with body-diode commutation in Gate 3). This gives DSED-class speedup on the LTI portion plus user-controlled handling of the nonlinear bits — see the function's docstring for the 5-method system protocol. - Stay on
engine='pwl', which uses Newton refinement to handle nonlinearities (chapters 5-6).
Very-many-events-per-second converters¶
DSED's per-event cost (~5 µs to invalidate caches, swap mask, re-query stiffness) is small but non-zero. For a converter switching at 1 MHz with 4 modes per cycle:
| 1 MHz, 100 µs window | |
|---|---|
| Events | 400 |
| DSED per-event overhead | 400 × 5 µs = 2 ms |
| DSED integration cost (assuming 5 µs/step × 400 steps) | 2 ms |
| Total DSED | 4 ms |
| PWL at \(\Delta t = 10\) ns (1/100 of T_sw) | 100,000 × 1 µs = 100 ms |
| PWL | 100 ms |
| Ratio | 25× |
Even at 1 MHz, DSED wins. But at, say, 10 MHz with 4 modes per cycle the per-event overhead would start to dominate (~50% event, 50% integration). The 24× headline assumes typical 100-200 kHz PE switching.
Parameter sweeps + Monte Carlo¶
PWL has chapter 7's path-based partial refactor: when only one component value changes between sweep iterations, the LU update is O(√n) instead of a full refactor. DSED has no equivalent: every sweep iteration re-extracts the LTI state-space from scratch (cheap — but not free).
For a 10,000-point parameter sweep on a buck CCM, PWL with partial refactor wins. For a 100-point sweep over scenarios with significantly different switching schedules, DSED still wins.
Long transients on slow plants (RC ≫ T_sim)¶
If the simulation window is shorter than 1 RC time constant the state hardly moves. PWL's fixed \(\Delta t = T_{\text{sw}}/100\) still produces 50,000+ samples regardless. DSED's adaptive step grows huge on plateaus → way fewer points.
But on a 1-ms simulation of a converter with \(RC = 1\) ms (open loop transient just starting): DSED takes ~10 steps total. PWL takes 10,000. Speedup approaches 1000× — but it's a tiny absolute win because both engines finish in <1 ms.
Useful insight: the bigger speedups are on long simulations, not short ones. The 24× was on 5 ms. On a 100 ms window with the same buck CCM, the speedup widens to ~30×.
16.6 Reproducing the numbers¶
The bench script is at
scripts/bench_dsed_vs_pwl.py. Run from the repo root:
cd /path/to/pulsim
pip install --no-build-isolation -e . # ensures C++ is fresh
python3 scripts/bench_dsed_vs_pwl.py
Output is the table from §16.3 (within run-to-run noise of
~5%). The script uses time.perf_counter() with min over 5
runs to minimise GC and cache-warming noise.
Buck-CCM-only quick check:
import time, pulsim as p
b = p.CircuitBuilder()
b.add_voltage_source("Vin", "in", "gnd", 24.0)
b.add_switch("SW_HS", "in", "sw", 1e6, 1e-9)
b.add_switch("SW_LS", "sw", "gnd", 1e6, 1e-9)
b.add_inductor("L", "sw", "out", 100e-6)
b.add_capacitor("C", "out", "gnd", 100e-6)
b.add_resistor("R", "out", "gnd", 2.4)
sf = p.NativePwm2Switch(T_sw=1e-5, D=0.5, n_switches=2)
p.simulate(b, t_end=5e-3, engine='dsed', switch_fn=sf) # warm-up
p.simulate(b, t_end=5e-3, dt=100e-9, switch_fn=sf) # warm-up
t0 = time.perf_counter()
for _ in range(10):
res = p.simulate(b, t_end=5e-3, engine='dsed', switch_fn=sf)
print(f"DSED: {(time.perf_counter()-t0)/10*1000:.2f} ms")
t0 = time.perf_counter()
for _ in range(10):
res = p.simulate(b, t_end=5e-3, dt=100e-9, switch_fn=sf)
print(f"PWL: {(time.perf_counter()-t0)/10*1000:.2f} ms")
Expected on Apple M1 / Python 3.13 / release build: DSED ~2.2 ms, PWL ~52-54 ms, ratio ~24×.
16.7 Takeaways¶
- 24× wall-clock speedup on buck CCM — the headline number. Real, reproducible, defensible.
- 14.5× geo-mean across 6 topologies — the honest summary number. Lowest: 8.3× (NPC). Highest: 19.6× (buck-boost).
- The speedup comes from 50× fewer steps at only ~2× the per-step cost — the algorithm + native-stack combination pays for itself.
- Both engines produce bit-for-bit identical final states on the validation tests (12.0000 V buck CCM, balanced ±50 V NPC, etc.). The speedup is pure, no accuracy trade-off.
- DSED doesn't win on nonlinear-device circuits, very-many- events scenarios, or extremely short simulations. Honest scope in §16.5.
- DSED stacks on top of chapters 4 (PWL cache) and 7 (rank-1 refactor) — combined speedup vs v1 stamping is ~1400× on the converters where all three optimisations apply.
Further reading¶
- Reproducible bench script:
scripts/bench_dsed_vs_pwl.py - End-to-end test suite:
python/tests/test_dsed_end_to_end.py(14 tests including thetest_dsed_native_pwm_matches_python_pwmbit-for-bit-agreement check) - Chapter 8 (PWL benchmarks) — for the v1 → PWL → rank-1 step on the same converters.
- Design notes:
notes/DSED_BRIDGE_DESIGN.md— full bench-result history through every bridge.
Recommended reading order if you only have 20 minutes¶
- Chapter 11 — DSED motivation
- This chapter (skim §16.1, §16.5)
- Chapter 12 §12.4 — Schur complement (if you care about the math)
- Chapter 15 §15.4 — Bridge.12 detection trick (if you care about the impl)