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
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 23:04 +0000
1"""Context-sensitive help and command suggestions.
3This module provides plain English help, command suggestions based on
4context, and result explanations for non-expert users.
7Example:
8 >>> from tracekit.onboarding import get_help, suggest_commands
9 >>> get_help("rise_time")
10 >>> suggest_commands(trace)
11"""
13from __future__ import annotations
15from typing import Any
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.
26In plain terms: A faster rise time means sharper edges on your signal,
27which is important for high-speed digital circuits.
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).
47It's measured between the 90% and 10% points of the transition.
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'.
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)
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.
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)
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.
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)
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.
130Returns two arrays:
131- frequencies: The x-axis values in Hz
132- magnitudes: The strength at each frequency (usually in dB)
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.
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)
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.
172It's like clicking 'Auto-Measure' on an oscilloscope.
174Measurements include:
175- Timing: rise_time, fall_time, frequency, period, duty_cycle
176- Amplitude: vpp, vmax, vmin, vmean, vrms
177- Waveform quality: overshoot, undershoot
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}
191def get_help(topic: str) -> str | None:
192 """Get plain English help for a TraceKit function or concept.
194 Args:
195 topic: Function name or concept to get help for
197 Returns:
198 Formatted help text or None if topic not found
200 Example:
201 >>> print(get_help("rise_time"))
202 """
203 topic = topic.lower().strip()
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]
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}")
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'])}")
221 return "\n".join(output)
223 # Try to get docstring
224 try:
225 import tracekit as tk
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
234 return None
237def suggest_commands(trace: Any = None, context: str | None = None) -> list[dict[str, str]]:
238 """Suggest next commands based on current context.
240 Args:
241 trace: Current trace object (if any)
242 context: Description of what user is trying to do
244 Returns:
245 List of suggested commands with descriptions
247 Example:
248 >>> suggestions = suggest_commands(trace)
249 >>> for s in suggestions:
250 ... print(f"{s['command']}: {s['description']}")
251 """
252 suggestions = []
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
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 )
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
285 data = trace.data
286 unique_levels = len(np.unique(np.round(data, 2)))
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 )
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 )
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 )
361 return suggestions
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.
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)
376 Returns:
377 Plain English explanation of the result
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 }
391 if measurement.lower() in explanations:
392 return explanations[measurement.lower()](value) # type: ignore[no-untyped-call]
394 # Generic explanation
395 return f"{measurement}: {value}"
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."
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."
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."
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."
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."
458def get_example(function_name: str) -> str | None:
459 """Get a code example for a function.
461 Args:
462 function_name: Name of the function
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 }
498 return examples.get(function_name.lower())