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
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 23:04 +0000
1"""Switching loss analysis for TraceKit.
3Provides switching loss calculations for power semiconductor devices
4including MOSFETs, IGBTs, and diodes.
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"""
14from __future__ import annotations
16from dataclasses import dataclass
17from typing import TYPE_CHECKING, Any, Literal
19import numpy as np
21from tracekit.analyzers.power.basic import instantaneous_power
23if TYPE_CHECKING:
24 from numpy.typing import NDArray
26 from tracekit.core.types import WaveformTrace
29@dataclass
30class SwitchingEvent:
31 """Information about a switching transition.
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 """
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"]
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.
59 Analyzes voltage and current waveforms to find switching transitions
60 and calculate turn-on and turn-off energy losses.
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.
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
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")
86 References:
87 Infineon Application Note AN-9010
88 """
89 # Calculate instantaneous power
90 power = instantaneous_power(voltage, current)
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
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)))
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)
113 # Find switching events
114 events: list[SwitchingEvent] = []
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
123 for i in range(min_len):
124 v = v_data[i]
125 i_val = i_data[i]
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
142 device_state[i] = current_state
144 device_on = device_state == 1
145 device_off = device_state == 2
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
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
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))
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
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
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
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))
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
206 i += 1
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"]
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
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
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 }
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.
244 E = integral(V(t) * I(t) dt) from start_time to end_time
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).
252 Returns:
253 Switching energy in Joules.
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
263 # Select time window
264 mask = (time_vector >= start_time) & (time_vector <= end_time)
265 window_power = power.data[mask]
267 # Use scipy for stable API across NumPy versions
268 from scipy.integrate import trapezoid
270 return float(trapezoid(window_power, dx=sample_period))
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.
282 Convenience function that returns just the turn-on energy.
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.
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"])
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.
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.
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"])
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.
329 P_sw = (E_on + E_off) * f_sw
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.
338 Returns:
339 Switching power loss in Watts.
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
349def switching_frequency(
350 voltage: WaveformTrace,
351 *,
352 threshold: float | None = None,
353) -> float:
354 """Estimate switching frequency from voltage waveform.
356 Args:
357 voltage: Drain-source voltage trace.
358 threshold: Voltage threshold for edge detection.
360 Returns:
361 Estimated switching frequency in Hz.
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
370 if threshold is None:
371 threshold = float((np.max(data) + np.min(data)) / 2)
373 # Find rising edges
374 below = data < threshold
375 above = data >= threshold
376 rising = np.where(below[:-1] & above[1:])[0]
378 if len(rising) < 2:
379 return 0.0
381 # Calculate average period
382 periods = np.diff(rising) / sample_rate
383 avg_period = float(np.mean(periods))
385 return 1.0 / avg_period if avg_period > 0 else 0.0
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).
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).
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
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))
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)
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
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)
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 }
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]