Skip to content

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×).

Speedup across 6 topologies + step-count ratio

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_extra walk 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:

  1. 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.
  2. 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 the test_dsed_native_pwm_matches_python_pwm bit-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.
  1. Chapter 11 — DSED motivation
  2. This chapter (skim §16.1, §16.5)
  3. Chapter 12 §12.4 — Schur complement (if you care about the math)
  4. Chapter 15 §15.4 — Bridge.12 detection trick (if you care about the impl)