Coverage for src / tracekit / onboarding / tutorials.py: 98%
71 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"""Interactive tutorial system for TraceKit.
3This module provides step-by-step interactive tutorials for new users,
4covering common analysis workflows.
6 - Interactive tutorial system
7 - Step-by-step guidance
8 - Code examples with explanations
9 - Progress tracking
11Example:
12 >>> from tracekit.onboarding import run_tutorial
13 >>> run_tutorial("getting_started")
14 Welcome to TraceKit!
15 Step 1/5: Loading a trace file
16 ...
17"""
19from __future__ import annotations
21from dataclasses import dataclass, field
22from typing import TYPE_CHECKING
24if TYPE_CHECKING:
25 from collections.abc import Callable
28@dataclass
29class TutorialStep:
30 """A single step in a tutorial.
32 Attributes:
33 title: Step title
34 description: Detailed description with plain English explanation
35 code: Example code to run
36 expected_output: What the user should see
37 hints: Optional hints if stuck
38 """
40 title: str
41 description: str
42 code: str
43 expected_output: str = ""
44 hints: list[str] = field(default_factory=list)
45 validation_fn: Callable[..., bool] | None = None
48@dataclass
49class Tutorial:
50 """An interactive tutorial.
52 Attributes:
53 id: Unique tutorial identifier
54 title: Human-readable title
55 description: Tutorial overview
56 steps: List of tutorial steps
57 difficulty: beginner, intermediate, or advanced
58 """
60 id: str
61 title: str
62 description: str
63 steps: list[TutorialStep]
64 difficulty: str = "beginner"
67# Built-in tutorials
68TUTORIALS: dict[str, Tutorial] = {}
71def _register_getting_started() -> None:
72 """Register the getting started tutorial."""
73 steps = [
74 TutorialStep(
75 title="Loading a Trace File",
76 description="""
77TraceKit can load waveform data from many file formats.
78The simplest way is to use the load() function, which auto-detects the format.
80Think of a trace like a recording of an electrical signal over time -
81similar to how an audio file stores sound waves.
82""",
83 code="""
84import tracekit as tk
86# Load a waveform file (replace with your file path)
87trace = tk.load("signal.csv")
89# See basic info
90print(f"Loaded {len(trace.data)} samples")
91print(f"Sample rate: {trace.metadata.sample_rate} Hz")
92""",
93 expected_output="Loaded 10000 samples\nSample rate: 1000000.0 Hz",
94 hints=[
95 "Try loading a CSV file with two columns: time and voltage",
96 "Supported formats: .csv, .wfm, .npz, .hdf5, and more",
97 ],
98 ),
99 TutorialStep(
100 title="Making Basic Measurements",
101 description="""
102Once you have a trace, you can measure things like:
103- Rise time: How fast a signal goes from low to high
104- Frequency: How many times per second the signal repeats
105- Amplitude: The voltage difference between high and low
107These are the same measurements an oscilloscope would show you!
108""",
109 code="""
110import tracekit as tk
112trace = tk.load("signal.csv")
114# Measure rise time (10% to 90% transition)
115rt = tk.rise_time(trace)
116print(f"Rise time: {rt*1e9:.2f} nanoseconds")
118# Measure frequency
119freq = tk.frequency(trace)
120print(f"Frequency: {freq/1e6:.2f} MHz")
122# Get all measurements at once
123results = tk.measure(trace)
124for name, value in results.items():
125 print(f"{name}: {value}")
126""",
127 expected_output="Rise time: 2.50 nanoseconds\nFrequency: 10.00 MHz",
128 hints=[
129 "rise_time() measures 10%-90% transition by default",
130 "Use measure() to get all measurements in one call",
131 ],
132 ),
133 TutorialStep(
134 title="Spectral Analysis (Frequency Domain)",
135 description="""
136Spectral analysis shows you what frequencies are present in your signal.
137This is useful for:
138- Finding the main frequency of a clock signal
139- Detecting noise at specific frequencies
140- Measuring signal quality (THD, SNR)
142It's like looking at a music equalizer that shows bass, mid, and treble!
143""",
144 code="""
145import tracekit as tk
147trace = tk.load("signal.csv")
149# Compute FFT (Fast Fourier Transform)
150freq, magnitude = tk.fft(trace)
152# Find the dominant frequency
153import numpy as np
154peak_idx = np.argmax(magnitude)
155print(f"Dominant frequency: {freq[peak_idx]/1e6:.2f} MHz")
157# Measure signal quality
158thd_value = tk.thd(trace)
159snr_value = tk.snr(trace)
160print(f"THD: {thd_value:.1f} dB")
161print(f"SNR: {snr_value:.1f} dB")
162""",
163 expected_output="Dominant frequency: 10.00 MHz\nTHD: -45.2 dB\nSNR: 52.3 dB",
164 hints=[
165 "THD (Total Harmonic Distortion) should be negative in dB - more negative is better",
166 "SNR (Signal-to-Noise Ratio) should be positive - higher is better",
167 ],
168 ),
169 TutorialStep(
170 title="Protocol Decoding",
171 description="""
172If your signal is a digital communication protocol like UART, SPI, or I2C,
173TraceKit can decode it to show you the actual data being transmitted.
175Think of it like translating Morse code back into text!
176""",
177 code="""
178import tracekit as tk
180# Load a UART signal
181trace = tk.load("uart_signal.csv")
183# Decode UART (auto-detects baud rate!)
184from tracekit.analyzers.protocols import decode_uart
185packets = decode_uart(trace)
187# Show decoded bytes
188for pkt in packets[:5]: # First 5 packets
189 print(f"Time: {pkt.timestamp:.6f}s, Data: 0x{pkt.data:02X} ('{chr(pkt.data)}')")
190""",
191 expected_output="Time: 0.000001s, Data: 0x48 ('H')\nTime: 0.000086s, Data: 0x65 ('e')",
192 hints=[
193 "UART baud rate is auto-detected by default",
194 "Supported protocols: UART, SPI, I2C, CAN, and many more",
195 ],
196 ),
197 TutorialStep(
198 title="Auto-Discovery for Beginners",
199 description="""
200Not sure what your signal is? TraceKit can analyze it automatically!
202The characterize_signal() function examines your trace and tells you:
203- What type of signal it likely is
204- Key parameters (voltage, frequency, etc.)
205- Suggestions for further analysis
207It's like having an expert look at your signal and give you hints!
208""",
209 code="""
210import tracekit as tk
211from tracekit.discovery import characterize_signal
213trace = tk.load("mystery_signal.csv")
215# Auto-characterize the signal
216result = characterize_signal(trace)
218print(f"Signal type: {result.signal_type}")
219print(f"Confidence: {result.confidence:.1%}")
220print(f"Voltage range: {result.voltage_low:.2f}V to {result.voltage_high:.2f}V")
222if result.confidence >= 0.8:
223 print("High confidence - proceed with suggested analysis")
224else:
225 print("Consider alternatives:")
226 for alt in result.alternatives:
227 print(f" - {alt.signal_type}: {alt.confidence:.1%}")
228""",
229 expected_output="Signal type: digital\nConfidence: 94.0%",
230 hints=[
231 "Confidence >= 80% means high confidence in the detection",
232 "Low confidence? Check the alternatives for other possibilities",
233 ],
234 ),
235 ]
237 tutorial = Tutorial(
238 id="getting_started",
239 title="Getting Started with TraceKit",
240 description="""
241Welcome to TraceKit! This tutorial will teach you the basics of
242signal analysis in 5 easy steps:
2441. Loading trace files
2452. Making basic measurements
2463. Spectral analysis
2474. Protocol decoding
2485. Auto-discovery
250No prior signal analysis experience required!
251""",
252 steps=steps,
253 difficulty="beginner",
254 )
256 TUTORIALS[tutorial.id] = tutorial
259def _register_spectral_analysis() -> None:
260 """Register the spectral analysis tutorial."""
261 steps = [
262 TutorialStep(
263 title="Understanding FFT",
264 description="""
265The Fast Fourier Transform (FFT) converts a time-domain signal into
266its frequency components. Think of it as breaking a chord into individual notes.
267""",
268 code="""
269import tracekit as tk
270import numpy as np
272trace = tk.load("signal.csv")
273freq, mag = tk.fft(trace)
275# Magnitude is in dB (decibels)
276# 0 dB = full scale, -20 dB = 10x smaller, -40 dB = 100x smaller
277print(f"Frequency range: 0 to {freq[-1]/1e6:.1f} MHz")
278print(f"Peak magnitude: {np.max(mag):.1f} dB")
279""",
280 expected_output="Frequency range: 0 to 50.0 MHz\nPeak magnitude: -3.2 dB",
281 ),
282 TutorialStep(
283 title="Power Spectral Density",
284 description="""
285PSD shows power distribution across frequencies. Unlike FFT magnitude,
286PSD is normalized per Hz, making it easier to compare signals with
287different durations or sample rates.
288""",
289 code="""
290import tracekit as tk
292trace = tk.load("signal.csv")
293freq, psd = tk.psd(trace)
295# Find where most power is concentrated
296import numpy as np
297total_power = np.sum(psd)
298cumsum = np.cumsum(psd) / total_power
300# 90% of power is below this frequency
301idx_90 = np.searchsorted(cumsum, 0.9)
302print(f"90% of signal power below {freq[idx_90]/1e6:.1f} MHz")
303""",
304 expected_output="90% of signal power below 15.2 MHz",
305 ),
306 ]
308 tutorial = Tutorial(
309 id="spectral_analysis",
310 title="Spectral Analysis Deep Dive",
311 description="Learn advanced spectral analysis techniques.",
312 steps=steps,
313 difficulty="intermediate",
314 )
316 TUTORIALS[tutorial.id] = tutorial
319# Register built-in tutorials
320_register_getting_started()
321_register_spectral_analysis()
324def list_tutorials() -> list[dict[str, str]]:
325 """List all available tutorials.
327 Returns:
328 List of tutorial info dictionaries with id, title, difficulty
329 """
330 return [
331 {
332 "id": t.id,
333 "title": t.title,
334 "difficulty": t.difficulty,
335 "steps": len(t.steps), # type: ignore[dict-item]
336 }
337 for t in TUTORIALS.values()
338 ]
341def get_tutorial(tutorial_id: str) -> Tutorial | None:
342 """Get a tutorial by ID.
344 Args:
345 tutorial_id: Tutorial identifier
347 Returns:
348 Tutorial object or None if not found
349 """
350 return TUTORIALS.get(tutorial_id)
353def run_tutorial(tutorial_id: str, interactive: bool = True) -> None:
354 """Run an interactive tutorial.
356 Args:
357 tutorial_id: Tutorial to run (e.g., "getting_started")
358 interactive: If True, pause between steps for user input
360 Example:
361 >>> run_tutorial("getting_started")
362 """
363 tutorial = get_tutorial(tutorial_id)
364 if tutorial is None:
365 print(f"Tutorial '{tutorial_id}' not found.")
366 print("Available tutorials:")
367 for t in list_tutorials():
368 print(f" - {t['id']}: {t['title']}")
369 return
371 print("=" * 60)
372 print(f"Tutorial: {tutorial.title}")
373 print(f"Difficulty: {tutorial.difficulty}")
374 print("=" * 60)
375 print(tutorial.description)
376 print()
378 for i, step in enumerate(tutorial.steps, 1):
379 print(f"\n{'=' * 60}")
380 print(f"Step {i}/{len(tutorial.steps)}: {step.title}")
381 print("=" * 60)
382 print(step.description)
383 print("\nCode:")
384 print("-" * 40)
385 print(step.code)
386 print("-" * 40)
388 if step.expected_output: 388 ↛ 391line 388 didn't jump to line 391 because the condition on line 388 was always true
389 print(f"\nExpected output:\n{step.expected_output}")
391 if step.hints: 391 ↛ 396line 391 didn't jump to line 396 because the condition on line 391 was always true
392 print("\nHints:")
393 for hint in step.hints:
394 print(f" - {hint}")
396 if interactive:
397 input("\nPress Enter to continue...")
399 print("\n" + "=" * 60)
400 print("Tutorial Complete!")
401 print("=" * 60)
402 print("Next steps:")
403 print(" - Try the examples with your own data")
404 print(" - Run 'list_tutorials()' to see more tutorials")
405 print(" - Use 'get_help(function_name)' for detailed help")