Loss & Thermal Modelling¶
Pulsim reconstructs device losses from the simulated waveforms and turns them into junction temperatures through Foster/Cauer thermal networks — including several devices sharing one heatsink and the loss↔temperature feedback that decides whether a converter can run at a given power. This page explains how the model works and how to use it.
The whole stack lives in pulsim.losses and pulsim.thermal and is
re-exported at the flat pulsim namespace.
At a glance¶
| Layer | Function | What it gives you |
|---|---|---|
| Losses | device_loss_summary |
per-device conduction + switching + core loss from one sim |
| T_j (per device) | device_thermal_summary |
junction-temperature trace per device (Foster) |
| T_j (post-proc) | compute_temperature |
ΔT_j(t) from a power trace + Z_th |
| Shared heatsink | shared_heatsink_steady_state |
coupled T_j of N devices on one sink |
| Electro-thermal | electrothermal_steady_state |
self-consistent T_j with R_ds(T) feedback + runaway |
| Live co-sim | add_foster_network / add_shared_heatsink + observers |
T_j(t) solved alongside the circuit |
| Heatsink sizing | tim_resistance, convection_resistance |
R_th from geometry / airflow |
1. Losses — how it works¶
Conduction¶
By default conduction loss is reconstructed from the actual current
trace as a pure resistance, P = v·i = v²·g. Real IGBTs and diodes
follow an offset + slope characteristic V = V_f0 + r·I, whose
forward-voltage offset dominates at low current. Opt a device into it
with conduction_specs:
P = p.device_loss_summary(
b, res, switch_fn=sw,
conduction_specs={
"Q1": {"V_ce0": 0.9, "r_ce": 8e-3}, # IGBT (aliases V_f0/r_on too)
"D1": {"V_f0": 0.7, "r_d": 5e-3}, # diode
},
)
# each entry gains P_cond_model_avg = V_f0·|i| + r_on·i² (over the real i(t))
Switching¶
Switching loss is a PSIM/PLECS-style datasheet annotation — the ideal kernel switches emit no physical switching transient, so the energy is applied per detected edge:
switch_specs = {"Q1": {"E_on_ref": 0.9e-3, "E_off_ref": 0.6e-3,
"V_ref": 400.0, "I_ref": 20.0}} # linear
diode_specs = {"D1": {"E_rr_ref": 0.3e-3, "V_R_ref": 400.0}} # or {"Q_rr": …}
For better fidelity away from the reference point, give the nonlinear datasheet curve (interpolated at the actual switched current per event, linearly extrapolated beyond the table):
switch_specs = {"Q1": {
"E_on_curve": [(5, 0.4e-3), (10, 1.0e-3), (20, 2.6e-3)], # E_on vs I @ V_ref
"E_off_curve": [(5, 0.3e-3), (10, 0.8e-3), (20, 2.1e-3)],
"V_ref": 400.0}}
diode_specs = {"D1": {
"E_rr_curve": [(10, 0.5e-6), (50, 4e-6), (100, 12e-6)], # E_rr vs I_F @ V_R_ref
"V_R_ref": 400.0}}
Core loss¶
Inductor core loss uses Steinmetz / iGSE via core_loss_specs (material
catalog or an inline K/alpha/beta triplet) — see
device_loss_summary's docstring.
2. Junction temperature — Foster / Cauer¶
A device's transient thermal impedance Z_th(t) = Σ R_th_i·(1 − e^(−t/τ_i))
is a Foster RC ladder. Two ways to use it:
Post-processing — convolve a power trace with Z_th:
stages = [p.FosterStage(R_th_K_per_W=0.3, tau_s=5e-3),
p.FosterStage(R_th_K_per_W=0.5, tau_s=50e-3)]
T_j = p.compute_temperature(times, p_loss_trace, stages, T_amb_C=40.0)
End-to-end per device — wire the loss summary into Foster networks:
summary = p.device_thermal_summary(
b, res,
thermal_specs={"Q1": {"stages": stages}},
conduction_specs={"Q1": {"V_ce0": 0.9, "r_ce": 8e-3}},
switch_fn=sw, switch_specs=switch_specs,
T_ambient_C=40.0)
print(summary[0]["T_j_peak"])
p.CauerStage(R_th, C_th) + p.add_cauer_thermal_network(...) is the
physical (per-layer) parametrisation; p.fit_foster_from_zth(...) fits a
Foster ladder to a datasheet Z_th(t) curve.
3. Shared heatsink — coupling N devices¶
Several power devices on one heatsink are thermally coupled: their dissipations sum at the shared sink, so the sink temperature is driven by the total power and that rise lifts every junction together. A per-device-independent model misses this — exactly where it matters when pushing power up.
# A device = its junction→case ladder + a case→sink (TIM) resistance.
igbt = lambda n: p.HeatsinkDevice(
n, [p.FosterStage(R_th_K_per_W=0.3, tau_s=0.05)],
R_th_case_to_sink_K_per_W=0.2)
dio = lambda n: p.HeatsinkDevice(
n, [p.FosterStage(R_th_K_per_W=0.5, tau_s=0.03)],
R_th_case_to_sink_K_per_W=0.2)
devs = [igbt(f"Q{i}") for i in range(3)] + [dio(f"D{i}") for i in range(3)]
res = p.shared_heatsink_steady_state(
devs,
{**{f"Q{i}": 18.0 for i in range(3)}, **{f"D{i}": 6.0 for i in range(3)}},
R_th_sink_to_amb_K_per_W=0.5, T_amb_C=40.0)
print(res["T_sink_C"], res["devices"]["Q0"]["T_j_C"])
The maths is
— the \(\sum_i P_i\) is the coupling. For the transient version, build the
network into a CircuitBuilder with p.add_shared_heatsink(...) and
drive it with p.make_heatsink_observer(...) + p.simulate.
4. Electro-thermal feedback + runaway¶
Conduction loss climbs with junction temperature (R_ds(T) for MOSFETs, V_ce(T) for IGBTs), so the steady state is a self-consistent fixed point. A model with R_ds fixed at 25 °C reports an optimistic T_j and cannot detect thermal runaway.
q = p.TempCoLoss(P_cond_ref_W=15.0, P_sw_ref_W=6.0,
a_cond_per_C=0.006, a_sw_per_C=0.003) # tempcos from datasheet
r = p.electrothermal_steady_state(
devs, {name: q for name in (...)},
R_th_sink_to_amb_K_per_W=0.5, T_amb_C=60.0)
if r["runaway"]:
print("thermal runaway:", r["message"])
else:
print("T_j:", max(d["T_j_C"] for d in r["devices"].values()),
"feedback gain ρ:", r["feedback_gain"]) # margin = 1 − ρ
Because TempCoLoss is linear in T_j the fixed point is solved in closed
form, (I − M·K)·T_j = T_amb + M·(P₀ − K·T_ref), with feedback gain
G = M·K. ρ(G) < 1 is stable; ρ(G) ≥ 1 is runaway and is flagged
rather than returned as a plausible-but-wrong number. The live transient
counterpart is p.make_electrothermal_heatsink_observer(...).
5. Heatsink & TIM sizing¶
Turn geometry / airflow into the R_th values the API above consumes:
R_cs = p.tim_resistance(area_m2=2.5e-4, thickness_m=1e-4,
material="silicone_pad") # case → sink (TIM)
R_sa = p.convection_resistance(area_m2=0.03, airflow_m_per_s=3.0) # sink → ambient
p.TIM_CATALOG lists conductivities for common pads/greases/insulators.
The TIM resistance is exact physics (thickness/(k·area)); the
convection coefficient is a first-cut estimate — prefer the heatsink
datasheet's R_th-vs-airflow curve for a real design.
Worked example: an IPM at higher power¶
projects/inverters/pfc_vsi_drive/thermal_comparison.py applies the full
stack to a 6-IGBT + 6-diode inverter module and compares it to the
classic per-device-independent estimate. The takeaways:
- at the rated point the two agree within ~1 °C;
- pushing power up, the electro-thermal model runs hotter (the V_ce(T) feedback the fixed-coefficient model ignores): +8 °C at 4 kW, +20 °C at 5 kW — shaving ~500 W off the usable headroom before T_j,max;
- under degraded cooling the model detects thermal runaway (feedback gain ρ ≥ 1), which a fixed-coefficient model cannot.
Reference¶
- API details: docstrings of
device_loss_summary,device_thermal_summary,shared_heatsink_steady_state,electrothermal_steady_state,tim_resistance,convection_resistance. - Loss physics: Erickson & Maksimović, Fundamentals of Power Electronics Ch. 3; Infineon/ON-Semi switching-loss application notes.