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

1"""Interactive tutorial system for TraceKit. 

2 

3This module provides step-by-step interactive tutorials for new users, 

4covering common analysis workflows. 

5 

6 - Interactive tutorial system 

7 - Step-by-step guidance 

8 - Code examples with explanations 

9 - Progress tracking 

10 

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""" 

18 

19from __future__ import annotations 

20 

21from dataclasses import dataclass, field 

22from typing import TYPE_CHECKING 

23 

24if TYPE_CHECKING: 

25 from collections.abc import Callable 

26 

27 

28@dataclass 

29class TutorialStep: 

30 """A single step in a tutorial. 

31 

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 """ 

39 

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 

46 

47 

48@dataclass 

49class Tutorial: 

50 """An interactive tutorial. 

51 

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 """ 

59 

60 id: str 

61 title: str 

62 description: str 

63 steps: list[TutorialStep] 

64 difficulty: str = "beginner" 

65 

66 

67# Built-in tutorials 

68TUTORIALS: dict[str, Tutorial] = {} 

69 

70 

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. 

79 

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 

85 

86# Load a waveform file (replace with your file path) 

87trace = tk.load("signal.csv") 

88 

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 

106 

107These are the same measurements an oscilloscope would show you! 

108""", 

109 code=""" 

110import tracekit as tk 

111 

112trace = tk.load("signal.csv") 

113 

114# Measure rise time (10% to 90% transition) 

115rt = tk.rise_time(trace) 

116print(f"Rise time: {rt*1e9:.2f} nanoseconds") 

117 

118# Measure frequency 

119freq = tk.frequency(trace) 

120print(f"Frequency: {freq/1e6:.2f} MHz") 

121 

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) 

141 

142It's like looking at a music equalizer that shows bass, mid, and treble! 

143""", 

144 code=""" 

145import tracekit as tk 

146 

147trace = tk.load("signal.csv") 

148 

149# Compute FFT (Fast Fourier Transform) 

150freq, magnitude = tk.fft(trace) 

151 

152# Find the dominant frequency 

153import numpy as np 

154peak_idx = np.argmax(magnitude) 

155print(f"Dominant frequency: {freq[peak_idx]/1e6:.2f} MHz") 

156 

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. 

174 

175Think of it like translating Morse code back into text! 

176""", 

177 code=""" 

178import tracekit as tk 

179 

180# Load a UART signal 

181trace = tk.load("uart_signal.csv") 

182 

183# Decode UART (auto-detects baud rate!) 

184from tracekit.analyzers.protocols import decode_uart 

185packets = decode_uart(trace) 

186 

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! 

201 

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 

206 

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 

212 

213trace = tk.load("mystery_signal.csv") 

214 

215# Auto-characterize the signal 

216result = characterize_signal(trace) 

217 

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") 

221 

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 ] 

236 

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: 

243 

2441. Loading trace files 

2452. Making basic measurements 

2463. Spectral analysis 

2474. Protocol decoding 

2485. Auto-discovery 

249 

250No prior signal analysis experience required! 

251""", 

252 steps=steps, 

253 difficulty="beginner", 

254 ) 

255 

256 TUTORIALS[tutorial.id] = tutorial 

257 

258 

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 

271 

272trace = tk.load("signal.csv") 

273freq, mag = tk.fft(trace) 

274 

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 

291 

292trace = tk.load("signal.csv") 

293freq, psd = tk.psd(trace) 

294 

295# Find where most power is concentrated 

296import numpy as np 

297total_power = np.sum(psd) 

298cumsum = np.cumsum(psd) / total_power 

299 

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 ] 

307 

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 ) 

315 

316 TUTORIALS[tutorial.id] = tutorial 

317 

318 

319# Register built-in tutorials 

320_register_getting_started() 

321_register_spectral_analysis() 

322 

323 

324def list_tutorials() -> list[dict[str, str]]: 

325 """List all available tutorials. 

326 

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 ] 

339 

340 

341def get_tutorial(tutorial_id: str) -> Tutorial | None: 

342 """Get a tutorial by ID. 

343 

344 Args: 

345 tutorial_id: Tutorial identifier 

346 

347 Returns: 

348 Tutorial object or None if not found 

349 """ 

350 return TUTORIALS.get(tutorial_id) 

351 

352 

353def run_tutorial(tutorial_id: str, interactive: bool = True) -> None: 

354 """Run an interactive tutorial. 

355 

356 Args: 

357 tutorial_id: Tutorial to run (e.g., "getting_started") 

358 interactive: If True, pause between steps for user input 

359 

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 

370 

371 print("=" * 60) 

372 print(f"Tutorial: {tutorial.title}") 

373 print(f"Difficulty: {tutorial.difficulty}") 

374 print("=" * 60) 

375 print(tutorial.description) 

376 print() 

377 

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) 

387 

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}") 

390 

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}") 

395 

396 if interactive: 

397 input("\nPress Enter to continue...") 

398 

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")