Coverage for src / tracekit / onboarding / help.py: 92%

102 statements  

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

1"""Context-sensitive help and command suggestions. 

2 

3This module provides plain English help, command suggestions based on 

4context, and result explanations for non-expert users. 

5 

6 

7Example: 

8 >>> from tracekit.onboarding import get_help, suggest_commands 

9 >>> get_help("rise_time") 

10 >>> suggest_commands(trace) 

11""" 

12 

13from __future__ import annotations 

14 

15from typing import Any 

16 

17# Plain English help database 

18HELP_DATABASE: dict[str, dict[str, str | list[str]]] = { 

19 "rise_time": { 

20 "summary": "Measures how quickly a signal transitions from low to high", 

21 "plain_english": """ 

22Rise time tells you how fast your signal can switch from OFF to ON 

23(or from a low voltage to a high voltage). It's measured between 

24the 10% and 90% points of the transition by default. 

25 

26In plain terms: A faster rise time means sharper edges on your signal, 

27which is important for high-speed digital circuits. 

28 

29Typical values: 

30- Slow logic (old TTL): 10-50 nanoseconds 

31- Fast logic (modern CMOS): 0.1-2 nanoseconds 

32- High-speed serial (USB, PCIe): 50-200 picoseconds 

33""", 

34 "when_to_use": [ 

35 "Characterizing digital buffer performance", 

36 "Checking if your driver is fast enough for your data rate", 

37 "Verifying signal integrity (slow rise = possible problems)", 

38 ], 

39 "related": ["fall_time", "slew_rate", "frequency"], 

40 }, 

41 "fall_time": { 

42 "summary": "Measures how quickly a signal transitions from high to low", 

43 "plain_english": """ 

44Fall time is the opposite of rise time - it measures how fast your 

45signal switches from ON to OFF (high voltage to low voltage). 

46 

47It's measured between the 90% and 10% points of the transition. 

48 

49Ideally, rise time and fall time should be similar. If they're very 

50different, it might indicate an issue with your circuit. 

51""", 

52 "when_to_use": [ 

53 "Checking symmetry of your driver", 

54 "Verifying output stage performance", 

55 "Diagnosing asymmetric signal issues", 

56 ], 

57 "related": ["rise_time", "slew_rate", "duty_cycle"], 

58 }, 

59 "frequency": { 

60 "summary": "Measures how many times per second your signal repeats", 

61 "plain_english": """ 

62Frequency tells you how fast your signal is cycling. It's measured 

63in Hertz (Hz), which means 'cycles per second'. 

64 

65Common scales: 

66- 1 kHz = 1,000 cycles/second (audio frequencies) 

67- 1 MHz = 1,000,000 cycles/second (radio, slow digital) 

68- 1 GHz = 1,000,000,000 cycles/second (fast digital, RF) 

69 

70TraceKit finds frequency by detecting repeated patterns in your signal. 

71""", 

72 "when_to_use": [ 

73 "Verifying clock frequency", 

74 "Checking oscillator output", 

75 "Measuring PWM frequency", 

76 ], 

77 "related": ["period", "duty_cycle", "fft"], 

78 }, 

79 "thd": { 

80 "summary": "Total Harmonic Distortion - measures signal purity", 

81 "plain_english": """ 

82THD tells you how 'clean' your signal is. A perfect sine wave has 

830% THD (or -infinity dB). Real signals have some distortion. 

84 

85THD in dB (decibels): 

86- -60 dB or lower: Excellent (high-quality audio) 

87- -40 to -60 dB: Good (typical electronics) 

88- -20 to -40 dB: Fair (some distortion visible) 

89- Above -20 dB: Poor (significant distortion) 

90 

91Note: THD is expressed as a negative number in dB. 

92More negative = less distortion = better signal. 

93""", 

94 "when_to_use": [ 

95 "Testing audio amplifier quality", 

96 "Verifying oscillator purity", 

97 "Characterizing ADC/DAC performance", 

98 ], 

99 "related": ["snr", "sinad", "enob"], 

100 }, 

101 "snr": { 

102 "summary": "Signal-to-Noise Ratio - measures how much signal vs noise", 

103 "plain_english": """ 

104SNR tells you how much of your signal is actual signal versus noise. 

105Higher SNR = cleaner signal with less interference. 

106 

107SNR in dB: 

108- 60+ dB: Excellent (barely any noise visible) 

109- 40-60 dB: Good (clean signal, some noise) 

110- 20-40 dB: Fair (visible noise) 

111- Below 20 dB: Poor (noisy signal) 

112 

113In practical terms: Every 6 dB is roughly doubling the signal level 

114relative to noise. 

115""", 

116 "when_to_use": [ 

117 "Evaluating measurement system quality", 

118 "Testing ADC performance", 

119 "Comparing different signal sources", 

120 ], 

121 "related": ["thd", "sinad", "enob"], 

122 }, 

123 "fft": { 

124 "summary": "Fast Fourier Transform - shows frequency content of signal", 

125 "plain_english": """ 

126FFT transforms your time-domain signal (voltage vs time) into the 

127frequency domain (power vs frequency). It's like an equalizer display 

128that shows what frequencies are present. 

129 

130Returns two arrays: 

131- frequencies: The x-axis values in Hz 

132- magnitudes: The strength at each frequency (usually in dB) 

133 

134Peaks in the FFT correspond to dominant frequencies in your signal. 

135A pure sine wave shows one peak. Square waves show peaks at odd 

136harmonics (1x, 3x, 5x, etc. of the fundamental). 

137""", 

138 "when_to_use": [ 

139 "Finding the frequency of an unknown signal", 

140 "Looking for interference at specific frequencies", 

141 "Analyzing modulated signals", 

142 ], 

143 "related": ["psd", "thd", "snr", "spectrogram"], 

144 }, 

145 "load": { 

146 "summary": "Load a trace file - TraceKit's starting point", 

147 "plain_english": """ 

148load() reads waveform data from a file. It auto-detects the format, 

149so you don't need to specify whether it's CSV, WFM, HDF5, etc. 

150 

151Returns a WaveformTrace or DigitalTrace object containing: 

152- data: The actual voltage/value samples 

153- metadata: Sample rate, channel info, etc. 

154- time_vector: Time axis (computed from sample rate) 

155 

156Supported formats: CSV, Tektronix WFM, Rigol WFM, NumPy NPZ, 

157HDF5, Sigrok sessions, VCD, TDMS, and more. 

158""", 

159 "when_to_use": [ 

160 "Starting any TraceKit analysis", 

161 "Loading oscilloscope captures", 

162 "Importing logic analyzer data", 

163 ], 

164 "related": ["get_supported_formats", "WaveformTrace", "DigitalTrace"], 

165 }, 

166 "measure": { 

167 "summary": "Run all standard measurements on a trace", 

168 "plain_english": """ 

169measure() is a convenience function that runs many common measurements 

170at once and returns them as a dictionary. 

171 

172It's like clicking 'Auto-Measure' on an oscilloscope. 

173 

174Measurements include: 

175- Timing: rise_time, fall_time, frequency, period, duty_cycle 

176- Amplitude: vpp, vmax, vmin, vmean, vrms 

177- Waveform quality: overshoot, undershoot 

178 

179Results are returned in a dictionary for easy access. 

180""", 

181 "when_to_use": [ 

182 "Quick signal characterization", 

183 "Getting an overview of signal properties", 

184 "When you're not sure which measurements you need", 

185 ], 

186 "related": ["rise_time", "frequency", "amplitude", "basic_stats"], 

187 }, 

188} 

189 

190 

191def get_help(topic: str) -> str | None: 

192 """Get plain English help for a TraceKit function or concept. 

193 

194 Args: 

195 topic: Function name or concept to get help for 

196 

197 Returns: 

198 Formatted help text or None if topic not found 

199 

200 Example: 

201 >>> print(get_help("rise_time")) 

202 """ 

203 topic = topic.lower().strip() 

204 

205 if topic in HELP_DATABASE: 

206 entry = HELP_DATABASE[topic] 

207 output = [] 

208 output.append(f"Help: {topic}") 

209 output.append("=" * 50) 

210 output.append(f"\n{entry['summary']}\n") 

211 output.append(entry["plain_english"]) # type: ignore[arg-type] 

212 

213 if "when_to_use" in entry: 213 ↛ 218line 213 didn't jump to line 218 because the condition on line 213 was always true

214 output.append("\nWhen to use this:") 

215 for use in entry["when_to_use"]: 

216 output.append(f" - {use}") 

217 

218 if "related" in entry: 218 ↛ 221line 218 didn't jump to line 221 because the condition on line 218 was always true

219 output.append(f"\nRelated: {', '.join(entry['related'])}") 

220 

221 return "\n".join(output) 

222 

223 # Try to get docstring 

224 try: 

225 import tracekit as tk 

226 

227 if hasattr(tk, topic): 

228 func = getattr(tk, topic) 

229 if func.__doc__: 229 ↛ 234line 229 didn't jump to line 234 because the condition on line 229 was always true

230 return f"Help for {topic}:\n\n{func.__doc__}" 

231 except Exception: 

232 pass 

233 

234 return None 

235 

236 

237def suggest_commands(trace: Any = None, context: str | None = None) -> list[dict[str, str]]: 

238 """Suggest next commands based on current context. 

239 

240 Args: 

241 trace: Current trace object (if any) 

242 context: Description of what user is trying to do 

243 

244 Returns: 

245 List of suggested commands with descriptions 

246 

247 Example: 

248 >>> suggestions = suggest_commands(trace) 

249 >>> for s in suggestions: 

250 ... print(f"{s['command']}: {s['description']}") 

251 """ 

252 suggestions = [] 

253 

254 if trace is None: 

255 # No trace loaded - suggest loading 

256 suggestions.append( 

257 { 

258 "command": "trace = load('file.csv')", 

259 "description": "Load a trace file to get started", 

260 "reason": "No trace loaded yet", 

261 } 

262 ) 

263 suggestions.append( 

264 { 

265 "command": "formats = get_supported_formats()", 

266 "description": "See what file formats are supported", 

267 "reason": "Helpful for knowing what files you can load", 

268 } 

269 ) 

270 return suggestions 

271 

272 # Trace is loaded - suggest measurements 

273 suggestions.append( 

274 { 

275 "command": "measure(trace)", 

276 "description": "Run all standard measurements", 

277 "reason": "Quick overview of signal properties", 

278 } 

279 ) 

280 

281 # Check if it looks like digital signal 

282 if hasattr(trace, "data"): 282 ↛ 322line 282 didn't jump to line 322 because the condition on line 282 was always true

283 import numpy as np 

284 

285 data = trace.data 

286 unique_levels = len(np.unique(np.round(data, 2))) 

287 

288 if unique_levels < 5: 

289 # Likely digital 

290 suggestions.append( 

291 { 

292 "command": "digital = to_digital(trace)", 

293 "description": "Convert to digital trace", 

294 "reason": "Signal appears to be digital (few voltage levels)", 

295 } 

296 ) 

297 suggestions.append( 

298 { 

299 "command": "characterize_signal(trace)", 

300 "description": "Auto-detect signal type and protocol", 

301 "reason": "May be a protocol like UART, SPI, I2C", 

302 } 

303 ) 

304 else: 

305 # Likely analog 

306 suggestions.append( 

307 { 

308 "command": "freq, mag = fft(trace)", 

309 "description": "Compute frequency spectrum", 

310 "reason": "See what frequencies are present", 

311 } 

312 ) 

313 suggestions.append( 

314 { 

315 "command": "thd(trace)", 

316 "description": "Measure Total Harmonic Distortion", 

317 "reason": "Check signal purity", 

318 } 

319 ) 

320 

321 # Always suggest filtering for noisy signals 

322 suggestions.append( 

323 { 

324 "command": "filtered = low_pass(trace, cutoff_hz)", 

325 "description": "Apply low-pass filter to remove noise", 

326 "reason": "Clean up high-frequency noise", 

327 } 

328 ) 

329 

330 # Context-specific suggestions 

331 if context: 

332 context_lower = context.lower() 

333 if "uart" in context_lower or "serial" in context_lower: 

334 suggestions.insert( 

335 0, 

336 { 

337 "command": "packets = decode_uart(trace)", 

338 "description": "Decode UART serial data", 

339 "reason": "You mentioned UART/serial", 

340 }, 

341 ) 

342 elif "spi" in context_lower: 

343 suggestions.insert( 

344 0, 

345 { 

346 "command": "packets = decode_spi(clk_trace, data_trace)", 

347 "description": "Decode SPI bus", 

348 "reason": "You mentioned SPI", 

349 }, 

350 ) 

351 elif "i2c" in context_lower: 351 ↛ 361line 351 didn't jump to line 361 because the condition on line 351 was always true

352 suggestions.insert( 

353 0, 

354 { 

355 "command": "packets = decode_i2c(scl_trace, sda_trace)", 

356 "description": "Decode I2C bus", 

357 "reason": "You mentioned I2C", 

358 }, 

359 ) 

360 

361 return suggestions 

362 

363 

364def explain_result( 

365 value: Any, 

366 measurement: str, 

367 context: dict[str, Any] | None = None, 

368) -> str: 

369 """Explain a measurement result in plain English. 

370 

371 Args: 

372 value: The measurement value 

373 measurement: Name of the measurement (e.g., "rise_time") 

374 context: Additional context (e.g., signal type, expected values) 

375 

376 Returns: 

377 Plain English explanation of the result 

378 

379 Example: 

380 >>> print(explain_result(2.5e-9, "rise_time")) 

381 "Your rise time is 2.5 nanoseconds, which is..." 

382 """ 

383 explanations = { 

384 "rise_time": lambda v: _explain_rise_time(v), 

385 "fall_time": lambda v: _explain_fall_time(v), 

386 "frequency": lambda v: _explain_frequency(v), 

387 "thd": lambda v: _explain_thd(v), 

388 "snr": lambda v: _explain_snr(v), 

389 } 

390 

391 if measurement.lower() in explanations: 

392 return explanations[measurement.lower()](value) # type: ignore[no-untyped-call] 

393 

394 # Generic explanation 

395 return f"{measurement}: {value}" 

396 

397 

398def _explain_rise_time(value: float) -> str: 

399 """Explain rise time result.""" 

400 if value < 1e-12: 

401 return f"Rise time: {value * 1e12:.2f} ps - Extremely fast! Sub-picosecond edge." 

402 elif value < 1e-9: 

403 return f"Rise time: {value * 1e12:.0f} ps - Very fast, typical of high-speed serial links." 

404 elif value < 10e-9: 

405 return f"Rise time: {value * 1e9:.2f} ns - Fast, suitable for most digital circuits." 

406 elif value < 100e-9: 406 ↛ 407line 406 didn't jump to line 407 because the condition on line 406 was never true

407 return f"Rise time: {value * 1e9:.1f} ns - Moderate, typical of standard logic." 

408 else: 

409 return f"Rise time: {value * 1e6:.2f} us - Slow, may limit data rate." 

410 

411 

412def _explain_fall_time(value: float) -> str: 

413 """Explain fall time result.""" 

414 if value < 1e-9: 414 ↛ 415line 414 didn't jump to line 415 because the condition on line 414 was never true

415 return f"Fall time: {value * 1e12:.0f} ps - Very fast falling edge." 

416 elif value < 10e-9: 416 ↛ 419line 416 didn't jump to line 419 because the condition on line 416 was always true

417 return f"Fall time: {value * 1e9:.2f} ns - Fast, good for digital circuits." 

418 else: 

419 return f"Fall time: {value * 1e9:.1f} ns - Relatively slow falling edge." 

420 

421 

422def _explain_frequency(value: float) -> str: 

423 """Explain frequency result.""" 

424 if value < 1e3: 

425 return f"Frequency: {value:.1f} Hz - Audio range or very slow signal." 

426 elif value < 1e6: 

427 return f"Frequency: {value / 1e3:.2f} kHz - Low frequency signal." 

428 elif value < 1e9: 

429 return f"Frequency: {value / 1e6:.2f} MHz - Radio/digital clock range." 

430 else: 

431 return f"Frequency: {value / 1e9:.3f} GHz - High-speed digital or RF." 

432 

433 

434def _explain_thd(value: float) -> str: 

435 """Explain THD result.""" 

436 if value < -60: 

437 return f"THD: {value:.1f} dB - Excellent! Very low distortion (high-fidelity)." 

438 elif value < -40: 

439 return f"THD: {value:.1f} dB - Good, typical for quality electronics." 

440 elif value < -20: 

441 return f"THD: {value:.1f} dB - Fair, some distortion present." 

442 else: 

443 return f"THD: {value:.1f} dB - Poor, significant distortion visible." 

444 

445 

446def _explain_snr(value: float) -> str: 

447 """Explain SNR result.""" 

448 if value > 60: 

449 return f"SNR: {value:.1f} dB - Excellent! Very clean signal." 

450 elif value > 40: 

451 return f"SNR: {value:.1f} dB - Good signal-to-noise ratio." 

452 elif value > 20: 

453 return f"SNR: {value:.1f} dB - Fair, some noise present." 

454 else: 

455 return f"SNR: {value:.1f} dB - Poor, noisy signal." 

456 

457 

458def get_example(function_name: str) -> str | None: 

459 """Get a code example for a function. 

460 

461 Args: 

462 function_name: Name of the function 

463 

464 Returns: 

465 Example code string or None 

466 """ 

467 examples = { 

468 "load": """ 

469# Load a trace file 

470import tracekit as tk 

471trace = tk.load("capture.csv") 

472print(f"Loaded {len(trace.data)} samples") 

473""", 

474 "rise_time": """ 

475# Measure rise time 

476import tracekit as tk 

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

478rt = tk.rise_time(trace) 

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

480""", 

481 "fft": """ 

482# Compute FFT spectrum 

483import tracekit as tk 

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

485freq, mag = tk.fft(trace) 

486print(f"Frequency resolution: {freq[1]:.2f} Hz") 

487""", 

488 "measure": """ 

489# Run all measurements 

490import tracekit as tk 

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

492results = tk.measure(trace) 

493for name, value in results.items(): 

494 print(f"{name}: {value}") 

495""", 

496 } 

497 

498 return examples.get(function_name.lower())