Coverage for src / tracekit / analyzers / power / switching.py: 95%

159 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-11 23:04 +0000

1"""Switching loss analysis for TraceKit. 

2 

3Provides switching loss calculations for power semiconductor devices 

4including MOSFETs, IGBTs, and diodes. 

5 

6 

7Example: 

8 >>> from tracekit.analyzers.power.switching import switching_loss 

9 >>> losses = switching_loss(v_ds_trace, i_d_trace) 

10 >>> print(f"Turn-on: {losses['e_on']*1e6:.2f} uJ") 

11 >>> print(f"Turn-off: {losses['e_off']*1e6:.2f} uJ") 

12""" 

13 

14from __future__ import annotations 

15 

16from dataclasses import dataclass 

17from typing import TYPE_CHECKING, Any, Literal 

18 

19import numpy as np 

20 

21from tracekit.analyzers.power.basic import instantaneous_power 

22 

23if TYPE_CHECKING: 

24 from numpy.typing import NDArray 

25 

26 from tracekit.core.types import WaveformTrace 

27 

28 

29@dataclass 

30class SwitchingEvent: 

31 """Information about a switching transition. 

32 

33 Attributes: 

34 start_time: Start time of transition (seconds). 

35 end_time: End time of transition (seconds). 

36 duration: Transition duration (seconds). 

37 energy: Energy dissipated during transition (Joules). 

38 peak_power: Peak power during transition (Watts). 

39 event_type: "turn_on" or "turn_off". 

40 """ 

41 

42 start_time: float 

43 end_time: float 

44 duration: float 

45 energy: float 

46 peak_power: float 

47 event_type: Literal["turn_on", "turn_off"] 

48 

49 

50def switching_loss( 

51 voltage: WaveformTrace, 

52 current: WaveformTrace, 

53 *, 

54 v_threshold: float | None = None, 

55 i_threshold: float | None = None, 

56) -> dict[str, Any]: 

57 """Calculate switching losses for a power device. 

58 

59 Analyzes voltage and current waveforms to find switching transitions 

60 and calculate turn-on and turn-off energy losses. 

61 

62 Args: 

63 voltage: Drain-source (or collector-emitter) voltage trace. 

64 current: Drain (or collector) current trace. 

65 v_threshold: Voltage threshold for on/off detection. 

66 If None, uses 10% of peak voltage. 

67 i_threshold: Current threshold for on/off detection. 

68 If None, uses 10% of peak current. 

69 

70 Returns: 

71 Dictionary with: 

72 - e_on: Turn-on energy per event (Joules) 

73 - e_off: Turn-off energy per event (Joules) 

74 - e_total: Total switching energy per cycle (Joules) 

75 - p_sw: Switching power at estimated frequency (Watts) 

76 - events: List of SwitchingEvent objects 

77 - n_turn_on: Number of turn-on events 

78 - n_turn_off: Number of turn-off events 

79 

80 Example: 

81 >>> losses = switching_loss(v_ds, i_d) 

82 >>> print(f"E_on: {losses['e_on']*1e6:.2f} uJ") 

83 >>> print(f"E_off: {losses['e_off']*1e6:.2f} uJ") 

84 >>> print(f"Switching power @ 100kHz: {losses['p_sw']*100e3:.2f} W") 

85 

86 References: 

87 Infineon Application Note AN-9010 

88 """ 

89 # Calculate instantaneous power 

90 power = instantaneous_power(voltage, current) 

91 

92 # Ensure i_data matches v_data length (handle mismatched array sizes) 

93 min_len = min(len(voltage.data), len(current.data)) 

94 v_data = voltage.data[:min_len] 

95 i_data = current.data[:min_len] 

96 p_data = power.data[:min_len] 

97 sample_period = power.metadata.time_base 

98 

99 # Auto-detect thresholds if not provided 

100 if v_threshold is None: 

101 v_threshold = 0.1 * float(np.max(np.abs(v_data))) 

102 if i_threshold is None: 

103 i_threshold = 0.1 * float(np.max(np.abs(i_data))) 

104 

105 # Add hysteresis to prevent false transitions due to ringing (Schmitt trigger) 

106 # Use 20% hysteresis band around thresholds 

107 hysteresis_factor = 0.2 

108 v_threshold_high = v_threshold * (1 + hysteresis_factor) 

109 v_threshold_low = v_threshold * (1 - hysteresis_factor) 

110 i_threshold_high = i_threshold * (1 + hysteresis_factor) 

111 i_threshold_low = i_threshold * (1 - hysteresis_factor) 

112 

113 # Find switching events 

114 events: list[SwitchingEvent] = [] 

115 

116 # Determine device state at each sample with hysteresis 

117 # ON: low voltage, high current 

118 # OFF: high voltage, low current 

119 # Use hysteresis to avoid rapid state changes due to noise/ringing 

120 device_state = np.zeros(min_len, dtype=int) # 0=unknown, 1=on, 2=off 

121 current_state = 0 # Start in unknown state 

122 

123 for i in range(min_len): 

124 v = v_data[i] 

125 i_val = i_data[i] 

126 

127 # Determine next state based on current state and measurements 

128 if current_state == 1: # Currently ON 

129 # Stay ON unless voltage goes high (with hysteresis) 

130 if v > v_threshold_high: 

131 current_state = 2 # Transition to OFF 

132 elif current_state == 2: # Currently OFF 

133 # Stay OFF unless voltage goes low (with hysteresis) 

134 if v < v_threshold_low and i_val > i_threshold_low: 

135 current_state = 1 # Transition to ON 

136 else: # Unknown state - determine initial state 

137 if v < v_threshold_low and i_val > i_threshold_high: 

138 current_state = 1 # ON 

139 elif v > v_threshold_high and i_val < i_threshold_low: 

140 current_state = 2 # OFF 

141 

142 device_state[i] = current_state 

143 

144 device_on = device_state == 1 

145 device_off = device_state == 2 

146 

147 # Find transitions 

148 i = 0 

149 while i < len(device_on) - 1: 

150 # Look for turn-on: device was off, now turning on 

151 if device_off[i] and not device_off[i + 1]: 

152 # Find end of transition (device fully on) 

153 start_idx = i 

154 end_idx = start_idx + 1 

155 while end_idx < len(device_on) and not device_on[end_idx]: 155 ↛ 156line 155 didn't jump to line 156 because the condition on line 155 was never true

156 end_idx += 1 

157 

158 if end_idx < len(device_on): 158 ↛ 180line 158 didn't jump to line 180 because the condition on line 158 was always true

159 # Calculate transition energy (scipy for stable API) 

160 from scipy.integrate import trapezoid 

161 

162 transition_power = p_data[start_idx : end_idx + 1] 

163 e = float(trapezoid(transition_power, dx=sample_period)) 

164 peak_p = float(np.max(transition_power)) 

165 

166 events.append( 

167 SwitchingEvent( 

168 start_time=start_idx * sample_period, 

169 end_time=end_idx * sample_period, 

170 duration=(end_idx - start_idx) * sample_period, 

171 energy=e, 

172 peak_power=peak_p, 

173 event_type="turn_on", 

174 ) 

175 ) 

176 i = end_idx 

177 continue 

178 

179 # Look for turn-off: device was on, now turning off 

180 if device_on[i] and not device_on[i + 1]: 

181 start_idx = i 

182 end_idx = start_idx + 1 

183 while end_idx < len(device_off) and not device_off[end_idx]: 183 ↛ 184line 183 didn't jump to line 184 because the condition on line 183 was never true

184 end_idx += 1 

185 

186 if end_idx < len(device_off): 186 ↛ 206line 186 didn't jump to line 206 because the condition on line 186 was always true

187 from scipy.integrate import trapezoid 

188 

189 transition_power = p_data[start_idx : end_idx + 1] 

190 e = float(trapezoid(transition_power, dx=sample_period)) 

191 peak_p = float(np.max(transition_power)) 

192 

193 events.append( 

194 SwitchingEvent( 

195 start_time=start_idx * sample_period, 

196 end_time=end_idx * sample_period, 

197 duration=(end_idx - start_idx) * sample_period, 

198 energy=e, 

199 peak_power=peak_p, 

200 event_type="turn_off", 

201 ) 

202 ) 

203 i = end_idx 

204 continue 

205 

206 i += 1 

207 

208 # Calculate average energies 

209 turn_on_events = [e for e in events if e.event_type == "turn_on"] 

210 turn_off_events = [e for e in events if e.event_type == "turn_off"] 

211 

212 e_on = float(np.mean([e.energy for e in turn_on_events])) if turn_on_events else 0.0 

213 e_off = float(np.mean([e.energy for e in turn_off_events])) if turn_off_events else 0.0 

214 e_total = e_on + e_off 

215 

216 # Estimate switching frequency from event spacing 

217 if len(events) >= 2: 

218 event_times = [e.start_time for e in events] 

219 avg_period = float(np.mean(np.diff(event_times))) * 2 # Full cycle 

220 f_sw = 1.0 / avg_period if avg_period > 0 else 0.0 

221 else: 

222 f_sw = 0.0 

223 

224 return { 

225 "e_on": e_on, 

226 "e_off": e_off, 

227 "e_total": e_total, 

228 "f_sw": f_sw, 

229 "p_sw": e_total * f_sw, # Switching power at this frequency 

230 "events": events, 

231 "n_turn_on": len(turn_on_events), 

232 "n_turn_off": len(turn_off_events), 

233 } 

234 

235 

236def switching_energy( 

237 voltage: WaveformTrace, 

238 current: WaveformTrace, 

239 start_time: float, 

240 end_time: float, 

241) -> float: 

242 """Calculate switching energy over a specific time window. 

243 

244 E = integral(V(t) * I(t) dt) from start_time to end_time 

245 

246 Args: 

247 voltage: Voltage trace. 

248 current: Current trace. 

249 start_time: Start of integration window (seconds). 

250 end_time: End of integration window (seconds). 

251 

252 Returns: 

253 Switching energy in Joules. 

254 

255 Example: 

256 >>> e = switching_energy(v_ds, i_d, start_time=1e-6, end_time=1.5e-6) 

257 >>> print(f"Switching energy: {e*1e9:.2f} nJ") 

258 """ 

259 power = instantaneous_power(voltage, current) 

260 sample_period = power.metadata.time_base 

261 time_vector = np.arange(len(power.data)) * sample_period 

262 

263 # Select time window 

264 mask = (time_vector >= start_time) & (time_vector <= end_time) 

265 window_power = power.data[mask] 

266 

267 # Use scipy for stable API across NumPy versions 

268 from scipy.integrate import trapezoid 

269 

270 return float(trapezoid(window_power, dx=sample_period)) 

271 

272 

273def turn_on_loss( 

274 voltage: WaveformTrace, 

275 current: WaveformTrace, 

276 *, 

277 v_threshold: float | None = None, 

278 i_threshold: float | None = None, 

279) -> float: 

280 """Calculate average turn-on energy loss. 

281 

282 Convenience function that returns just the turn-on energy. 

283 

284 Args: 

285 voltage: Drain-source voltage trace. 

286 current: Drain current trace. 

287 v_threshold: Voltage threshold for on/off detection. 

288 i_threshold: Current threshold for on/off detection. 

289 

290 Returns: 

291 Average turn-on energy in Joules. 

292 """ 

293 result = switching_loss(voltage, current, v_threshold=v_threshold, i_threshold=i_threshold) 

294 return float(result["e_on"]) 

295 

296 

297def turn_off_loss( 

298 voltage: WaveformTrace, 

299 current: WaveformTrace, 

300 *, 

301 v_threshold: float | None = None, 

302 i_threshold: float | None = None, 

303) -> float: 

304 """Calculate average turn-off energy loss. 

305 

306 Args: 

307 voltage: Drain-source voltage trace. 

308 current: Drain current trace. 

309 v_threshold: Voltage threshold for on/off detection. 

310 i_threshold: Current threshold for on/off detection. 

311 

312 Returns: 

313 Average turn-off energy in Joules. 

314 """ 

315 result = switching_loss(voltage, current, v_threshold=v_threshold, i_threshold=i_threshold) 

316 return float(result["e_off"]) 

317 

318 

319def total_switching_loss( 

320 voltage: WaveformTrace, 

321 current: WaveformTrace, 

322 frequency: float, 

323 *, 

324 v_threshold: float | None = None, 

325 i_threshold: float | None = None, 

326) -> float: 

327 """Calculate total switching power loss at given frequency. 

328 

329 P_sw = (E_on + E_off) * f_sw 

330 

331 Args: 

332 voltage: Drain-source voltage trace. 

333 current: Drain current trace. 

334 frequency: Switching frequency in Hz. 

335 v_threshold: Voltage threshold for on/off detection. 

336 i_threshold: Current threshold for on/off detection. 

337 

338 Returns: 

339 Switching power loss in Watts. 

340 

341 Example: 

342 >>> p_sw = total_switching_loss(v_ds, i_d, frequency=100e3) 

343 >>> print(f"Switching loss at 100kHz: {p_sw:.2f} W") 

344 """ 

345 result = switching_loss(voltage, current, v_threshold=v_threshold, i_threshold=i_threshold) 

346 return float(result["e_total"]) * frequency 

347 

348 

349def switching_frequency( 

350 voltage: WaveformTrace, 

351 *, 

352 threshold: float | None = None, 

353) -> float: 

354 """Estimate switching frequency from voltage waveform. 

355 

356 Args: 

357 voltage: Drain-source voltage trace. 

358 threshold: Voltage threshold for edge detection. 

359 

360 Returns: 

361 Estimated switching frequency in Hz. 

362 

363 Example: 

364 >>> f_sw = switching_frequency(v_ds) 

365 >>> print(f"Switching frequency: {f_sw/1e3:.1f} kHz") 

366 """ 

367 data = voltage.data 

368 sample_rate = voltage.metadata.sample_rate 

369 

370 if threshold is None: 

371 threshold = float((np.max(data) + np.min(data)) / 2) 

372 

373 # Find rising edges 

374 below = data < threshold 

375 above = data >= threshold 

376 rising = np.where(below[:-1] & above[1:])[0] 

377 

378 if len(rising) < 2: 

379 return 0.0 

380 

381 # Calculate average period 

382 periods = np.diff(rising) / sample_rate 

383 avg_period = float(np.mean(periods)) 

384 

385 return 1.0 / avg_period if avg_period > 0 else 0.0 

386 

387 

388def switching_times( 

389 voltage: WaveformTrace, 

390 current: WaveformTrace, 

391 *, 

392 v_threshold: float | None = None, 

393 i_threshold: float | None = None, 

394) -> dict[str, float]: 

395 """Measure switching times (tr, tf, ton, toff). 

396 

397 Args: 

398 voltage: Drain-source voltage trace. 

399 current: Drain current trace. 

400 v_threshold: Voltage threshold (10%-90% levels if None). 

401 i_threshold: Current threshold (10%-90% levels if None). 

402 

403 Returns: 

404 Dictionary with: 

405 - tr: Rise time (10%-90%) 

406 - tf: Fall time (90%-10%) 

407 - t_on: Turn-on delay time 

408 - t_off: Turn-off delay time 

409 """ 

410 # Ensure arrays match in length (handle mismatched array sizes) 

411 min_len = min(len(voltage.data), len(current.data)) 

412 v_data = voltage.data[:min_len] 

413 i_data = current.data[:min_len] 

414 sample_period = voltage.metadata.time_base 

415 

416 v_min, v_max = float(np.min(v_data)), float(np.max(v_data)) 

417 i_min, i_max = float(np.min(i_data)), float(np.max(i_data)) 

418 

419 v_10 = v_min + 0.1 * (v_max - v_min) 

420 v_90 = v_min + 0.9 * (v_max - v_min) 

421 i_10 = i_min + 0.1 * (i_max - i_min) 

422 i_90 = i_min + 0.9 * (i_max - i_min) 

423 

424 # Find voltage transitions 

425 def find_transition_time( 

426 data: NDArray[np.floating[Any]], low: float, high: float, rising: bool 

427 ) -> float: 

428 if rising: 

429 below_low = data < low 

430 start_idx_arr = np.where(below_low[:-1] & ~below_low[1:])[0] 

431 if len(start_idx_arr) == 0: 

432 return float(np.nan) 

433 start_idx = int(start_idx_arr[0]) 

434 remaining = data[start_idx:] 

435 above_mask = remaining > high 

436 if not np.any(above_mask): 436 ↛ 437line 436 didn't jump to line 437 because the condition on line 436 was never true

437 return float(np.nan) 

438 end_offset = int(np.argmax(above_mask)) 

439 return float(end_offset) * sample_period 

440 else: 

441 above_high = data > high 

442 start_idx_arr = np.where(above_high[:-1] & ~above_high[1:])[0] 

443 if len(start_idx_arr) == 0: 

444 return float(np.nan) 

445 start_idx = int(start_idx_arr[0]) 

446 remaining = data[start_idx:] 

447 below_mask = remaining < low 

448 if not np.any(below_mask): 448 ↛ 449line 448 didn't jump to line 449 because the condition on line 448 was never true

449 return float(np.nan) 

450 end_offset = int(np.argmax(below_mask)) 

451 return float(end_offset) * sample_period 

452 

453 # Voltage fall time (turn-on) 

454 tf_v = find_transition_time(v_data, v_10, v_90, rising=False) 

455 # Voltage rise time (turn-off) 

456 tr_v = find_transition_time(v_data, v_10, v_90, rising=True) 

457 # Current rise time (turn-on) 

458 tr_i = find_transition_time(i_data, i_10, i_90, rising=True) 

459 # Current fall time (turn-off) 

460 tf_i = find_transition_time(i_data, i_10, i_90, rising=False) 

461 

462 return { 

463 "tr": tr_v, # Voltage rise time (turn-off) 

464 "tf": tf_v, # Voltage fall time (turn-on) 

465 "tr_current": tr_i, # Current rise time (turn-on) 

466 "tf_current": tf_i, # Current fall time (turn-off) 

467 } 

468 

469 

470__all__ = [ 

471 "SwitchingEvent", 

472 "switching_energy", 

473 "switching_frequency", 

474 "switching_loss", 

475 "switching_times", 

476 "total_switching_loss", 

477 "turn_off_loss", 

478 "turn_on_loss", 

479]