Tutorial 04 — Three-phase voltage-source inverter (SPWM)¶
Goal. Build a 6-switch 3-leg VSI driving a balanced RL load with sinusoidal PWM. Verify that the three line-to-line voltages have the correct 120° relative phase and equal magnitude.
Concepts introduced:
- 6-switch enumeration in SwitchStateMask (num_switches = 6 → 64 cache entries).
- make_three_phase_spwm_fn + ThreePhaseLegIndices (Layer 2 V8).
- Why the cache stays manageable even with many switches (only states reachable from the schedule are built).
Reference YAML: examples/three_phase_inverter.yaml.
Topology¶
V_dc ─┬── Q1_H ──┬── Q2_H ──┬── Q3_H ──
│ │ │
│ phase A │ phase B │ phase C
│ │ │
gnd ──┴── Q1_L ──┴── Q2_L ──┴── Q3_L ──
│ │ │
(phase outputs go to L-R load)
Three half-bridge legs. Each leg has a high-side and a low-side switch driven complementarily by an SPWM modulator with the appropriate 120° phase shift.
Load: a 3-phase wye-connected RL (5 Ω + 5 mH per phase, common neutral).
Python build¶
import pulsim as p
import math
V_DC = 600.0
F_CARRIER = 10e3 # PWM carrier
F_MOD = 60.0 # AC output fundamental
M = 0.85 # modulation index ∈ [0, 1]
b = p.CircuitBuilder()
b.add_voltage_source("Vdc", "dc_pos", "gnd", V_DC)
# 6 switches.
b.add_mosfet("Q1_H", "dc_pos", "a")
b.add_mosfet("Q1_L", "a", "gnd")
b.add_mosfet("Q2_H", "dc_pos", "b")
b.add_mosfet("Q2_L", "b", "gnd")
b.add_mosfet("Q3_H", "dc_pos", "c")
b.add_mosfet("Q3_L", "c", "gnd")
# Wye load (3 × (R + L) to neutral n).
for phase in ("a", "b", "c"):
b.add_resistor(f"R_{phase}", phase, f"{phase}_int", 5.0)
b.add_inductor(f"L_{phase}", f"{phase}_int", "n", 5e-3)
# Neutral pinned to gnd by a small resistor (numerical convenience).
b.add_resistor("R_n", "n", "gnd", 1e-3)
SPWM driver¶
legs = p.ThreePhaseLegIndices(
a_high=0, a_low=1,
b_high=2, b_low=3,
c_high=4, c_low=5,
)
spwm = p.make_three_phase_spwm_fn(
leg_indices=legs,
carrier_frequency=F_CARRIER,
modulating_frequency=F_MOD,
modulation_index=M,
phase=0.0,
)
The helper emits a SwitchStateMask(6) that:
- Compares a triangular carrier at F_CARRIER with three sinusoidal modulators at F_MOD, each phase-shifted by 120°.
- Drives each leg's HS-LS pair complementarily.
Run + analyse¶
res = p.simulate(b, t_end=100e-3, dt=2e-6, switch_fn=spwm)
a_idx = b.node_id_of("a")
b_idx = b.node_id_of("b")
c_idx = b.node_id_of("c")
v_ab = [s[a_idx] - s[b_idx] for s in res.states]
v_bc = [s[b_idx] - s[c_idx] for s in res.states]
v_ca = [s[c_idx] - s[a_idx] for s in res.states]
# After ~3 cycles, extract one full fundamental period.
import math
T_mod = 1.0 / F_MOD
k0 = int(50e-3 / 2e-6)
k1 = k0 + int(T_mod / 2e-6)
# Fundamental RMS via mean(v²)·2.
def rms(samples):
return math.sqrt(sum(v*v for v in samples) / len(samples))
print(f"V_ab fundamental RMS ≈ {rms(v_ab[k0:k1]):.1f} V (target ~{0.612 * M * V_DC:.0f} V)")
For M = 0.85 and V_DC = 600, expected fundamental line-to-line RMS is (√3/2) · M · V_DC / √2 ≈ 312 V (sinusoidal modulation, not third-harmonic-injected).
Cache size¶
This circuit has 6 switches → 64 possible masks. The cache is built lazily — only masks actually reached by the SPWM scheduler are factored, which is usually ~12-16 of the 64. You can check cache.dt() and the runtime stamping cost stays bounded.
Try this next¶
- Three-harmonic injection. Replace the pure-sine modulator with
sin + (1/6)·sin(3·), the classic technique to liftV_phase_maxfrom0.5 · V_DCto0.577 · V_DCwithout overmodulating. - Space-vector PWM. SVPWM is harder to express with
make_three_phase_spwm_fn; you can either write a customswitch_fnlambda OR build amake_combined_switch_fn(...)from per-leg helpers. See V10. - Field-oriented control. Drive a permanent-magnet synchronous motor model (via the saturable-inductor + transformer pair) and close the dq-loop in Python.
Going to Tutorial 05¶
Next we leave switching and look at a linear-regulator topology: a high-gain VCVS + pass MOSFET + RC feedback.