oscura 0.3.0__py3-none-any.whl → 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. oscura/__init__.py +1 -7
  2. oscura/acquisition/__init__.py +147 -0
  3. oscura/acquisition/file.py +255 -0
  4. oscura/acquisition/hardware.py +186 -0
  5. oscura/acquisition/saleae.py +340 -0
  6. oscura/acquisition/socketcan.py +315 -0
  7. oscura/acquisition/streaming.py +38 -0
  8. oscura/acquisition/synthetic.py +229 -0
  9. oscura/acquisition/visa.py +376 -0
  10. oscura/analyzers/__init__.py +3 -0
  11. oscura/analyzers/digital/__init__.py +48 -0
  12. oscura/analyzers/digital/clock.py +9 -1
  13. oscura/analyzers/digital/edges.py +1 -1
  14. oscura/analyzers/digital/extraction.py +195 -0
  15. oscura/analyzers/digital/ic_database.py +498 -0
  16. oscura/analyzers/digital/timing.py +41 -11
  17. oscura/analyzers/digital/timing_paths.py +339 -0
  18. oscura/analyzers/digital/vintage.py +377 -0
  19. oscura/analyzers/digital/vintage_result.py +148 -0
  20. oscura/analyzers/protocols/__init__.py +22 -1
  21. oscura/analyzers/protocols/parallel_bus.py +449 -0
  22. oscura/analyzers/side_channel/__init__.py +52 -0
  23. oscura/analyzers/side_channel/power.py +690 -0
  24. oscura/analyzers/side_channel/timing.py +369 -0
  25. oscura/analyzers/signal_integrity/sparams.py +1 -1
  26. oscura/automotive/__init__.py +4 -2
  27. oscura/automotive/can/patterns.py +3 -1
  28. oscura/automotive/can/session.py +277 -78
  29. oscura/automotive/can/state_machine.py +5 -2
  30. oscura/builders/__init__.py +9 -11
  31. oscura/builders/signal_builder.py +99 -191
  32. oscura/core/exceptions.py +5 -1
  33. oscura/export/__init__.py +12 -0
  34. oscura/export/wavedrom.py +430 -0
  35. oscura/exporters/json_export.py +47 -0
  36. oscura/exporters/vintage_logic_csv.py +247 -0
  37. oscura/loaders/__init__.py +1 -0
  38. oscura/loaders/chipwhisperer.py +393 -0
  39. oscura/loaders/touchstone.py +1 -1
  40. oscura/reporting/__init__.py +7 -0
  41. oscura/reporting/vintage_logic_report.py +523 -0
  42. oscura/session/session.py +54 -46
  43. oscura/sessions/__init__.py +70 -0
  44. oscura/sessions/base.py +323 -0
  45. oscura/sessions/blackbox.py +640 -0
  46. oscura/sessions/generic.py +189 -0
  47. oscura/utils/autodetect.py +5 -1
  48. oscura/visualization/digital_advanced.py +718 -0
  49. oscura/visualization/figure_manager.py +156 -0
  50. {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/METADATA +86 -5
  51. {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/RECORD +54 -33
  52. oscura/automotive/dtc/data.json +0 -2763
  53. oscura/schemas/bus_configuration.json +0 -322
  54. oscura/schemas/device_mapping.json +0 -182
  55. oscura/schemas/packet_format.json +0 -418
  56. oscura/schemas/protocol_definition.json +0 -363
  57. {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/WHEEL +0 -0
  58. {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/entry_points.txt +0 -0
  59. {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,498 @@
1
+ """IC timing database for vintage and modern logic ICs.
2
+
3
+ This module provides timing specifications for common ICs to enable automatic
4
+ validation and identification.
5
+
6
+ Example:
7
+ >>> from oscura.analyzers.digital.ic_database import IC_DATABASE, identify_ic
8
+ >>> spec = IC_DATABASE["74LS74"]
9
+ >>> print(f"Setup time: {spec.timing['t_su']*1e9:.1f}ns")
10
+ >>>
11
+ >>> # Auto-identify IC from measurements
12
+ >>> ic_name, conf = identify_ic(measured_timings)
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from dataclasses import dataclass, field
18
+
19
+
20
+ @dataclass
21
+ class ICTiming:
22
+ """Timing specification for an IC.
23
+
24
+ Attributes:
25
+ part_number: IC part number (e.g., "74LS74").
26
+ description: Brief description of function.
27
+ family: Logic family (e.g., "TTL", "LS-TTL", "HC-CMOS").
28
+ vcc_nom: Nominal supply voltage.
29
+ vcc_range: Supply voltage range (min, max).
30
+ timing: Dictionary of timing parameters in seconds.
31
+ voltage_levels: Dictionary of voltage thresholds.
32
+ """
33
+
34
+ part_number: str
35
+ description: str
36
+ family: str
37
+ vcc_nom: float
38
+ vcc_range: tuple[float, float]
39
+ timing: dict[str, float] = field(default_factory=dict)
40
+ voltage_levels: dict[str, float] = field(default_factory=dict)
41
+
42
+
43
+ # Timing parameter definitions:
44
+ # t_pd: Propagation delay
45
+ # t_su: Setup time
46
+ # t_h: Hold time
47
+ # t_w: Pulse width (minimum)
48
+ # t_r: Rise time
49
+ # t_f: Fall time
50
+ # t_co: Clock-to-output delay
51
+
52
+ # 74xx TTL Series (Standard TTL - 1970s era)
53
+ IC_7400_STD = ICTiming(
54
+ part_number="7400",
55
+ description="Quad 2-input NAND gate",
56
+ family="TTL",
57
+ vcc_nom=5.0,
58
+ vcc_range=(4.75, 5.25),
59
+ timing={
60
+ "t_pd": 22e-9, # Typ 22ns, max 33ns
61
+ "t_r": 12e-9,
62
+ "t_f": 8e-9,
63
+ },
64
+ voltage_levels={
65
+ "VIL_max": 0.8,
66
+ "VIH_min": 2.0,
67
+ "VOL_max": 0.4,
68
+ "VOH_min": 2.4,
69
+ },
70
+ )
71
+
72
+ IC_7474_STD = ICTiming(
73
+ part_number="7474",
74
+ description="Dual D-type flip-flop",
75
+ family="TTL",
76
+ vcc_nom=5.0,
77
+ vcc_range=(4.75, 5.25),
78
+ timing={
79
+ "t_pd": 25e-9, # Clock-to-output
80
+ "t_su": 20e-9, # Setup time
81
+ "t_h": 5e-9, # Hold time
82
+ "t_w": 25e-9, # Minimum clock pulse width
83
+ "t_r": 12e-9,
84
+ "t_f": 8e-9,
85
+ },
86
+ voltage_levels={
87
+ "VIL_max": 0.8,
88
+ "VIH_min": 2.0,
89
+ "VOL_max": 0.4,
90
+ "VOH_min": 2.4,
91
+ },
92
+ )
93
+
94
+ # 74LSxx Low-Power Schottky TTL Series (1970s-1980s)
95
+ IC_74LS00 = ICTiming(
96
+ part_number="74LS00",
97
+ description="Quad 2-input NAND gate",
98
+ family="LS-TTL",
99
+ vcc_nom=5.0,
100
+ vcc_range=(4.75, 5.25),
101
+ timing={
102
+ "t_pd": 10e-9, # Typ 10ns, max 15ns
103
+ "t_r": 10e-9,
104
+ "t_f": 7e-9,
105
+ },
106
+ voltage_levels={
107
+ "VIL_max": 0.8,
108
+ "VIH_min": 2.0,
109
+ "VOL_max": 0.5,
110
+ "VOH_min": 2.7,
111
+ },
112
+ )
113
+
114
+ IC_74LS74 = ICTiming(
115
+ part_number="74LS74",
116
+ description="Dual D-type flip-flop",
117
+ family="LS-TTL",
118
+ vcc_nom=5.0,
119
+ vcc_range=(4.75, 5.25),
120
+ timing={
121
+ "t_pd": 25e-9, # Clock-to-output (typ), max 40ns
122
+ "t_su": 20e-9, # Setup time
123
+ "t_h": 5e-9, # Hold time
124
+ "t_w": 25e-9, # Min clock pulse width
125
+ "t_r": 10e-9,
126
+ "t_f": 7e-9,
127
+ },
128
+ voltage_levels={
129
+ "VIL_max": 0.8,
130
+ "VIH_min": 2.0,
131
+ "VOL_max": 0.5,
132
+ "VOH_min": 2.7,
133
+ },
134
+ )
135
+
136
+ IC_74LS244 = ICTiming(
137
+ part_number="74LS244",
138
+ description="Octal buffer/line driver",
139
+ family="LS-TTL",
140
+ vcc_nom=5.0,
141
+ vcc_range=(4.75, 5.25),
142
+ timing={
143
+ "t_pd": 12e-9, # Typ 12ns, max 18ns
144
+ "t_r": 7e-9,
145
+ "t_f": 5e-9,
146
+ },
147
+ voltage_levels={
148
+ "VIL_max": 0.8,
149
+ "VIH_min": 2.0,
150
+ "VOL_max": 0.5,
151
+ "VOH_min": 2.7,
152
+ },
153
+ )
154
+
155
+ IC_74LS245 = ICTiming(
156
+ part_number="74LS245",
157
+ description="Octal bus transceiver",
158
+ family="LS-TTL",
159
+ vcc_nom=5.0,
160
+ vcc_range=(4.75, 5.25),
161
+ timing={
162
+ "t_pd": 12e-9, # Typ 12ns, max 18ns
163
+ "t_r": 7e-9,
164
+ "t_f": 5e-9,
165
+ },
166
+ voltage_levels={
167
+ "VIL_max": 0.8,
168
+ "VIH_min": 2.0,
169
+ "VOL_max": 0.5,
170
+ "VOH_min": 2.7,
171
+ },
172
+ )
173
+
174
+ IC_74LS138 = ICTiming(
175
+ part_number="74LS138",
176
+ description="3-to-8 line decoder/demultiplexer",
177
+ family="LS-TTL",
178
+ vcc_nom=5.0,
179
+ vcc_range=(4.75, 5.25),
180
+ timing={
181
+ "t_pd": 21e-9, # Typ 21ns, max 41ns
182
+ "t_r": 10e-9,
183
+ "t_f": 7e-9,
184
+ },
185
+ voltage_levels={
186
+ "VIL_max": 0.8,
187
+ "VIH_min": 2.0,
188
+ "VOL_max": 0.5,
189
+ "VOH_min": 2.7,
190
+ },
191
+ )
192
+
193
+ IC_74LS273 = ICTiming(
194
+ part_number="74LS273",
195
+ description="Octal D-type flip-flop with clear",
196
+ family="LS-TTL",
197
+ vcc_nom=5.0,
198
+ vcc_range=(4.75, 5.25),
199
+ timing={
200
+ "t_pd": 20e-9, # Clock-to-output
201
+ "t_su": 20e-9, # Setup time
202
+ "t_h": 5e-9, # Hold time
203
+ "t_w": 20e-9, # Min clock pulse width
204
+ "t_r": 10e-9,
205
+ "t_f": 7e-9,
206
+ },
207
+ voltage_levels={
208
+ "VIL_max": 0.8,
209
+ "VIH_min": 2.0,
210
+ "VOL_max": 0.5,
211
+ "VOH_min": 2.7,
212
+ },
213
+ )
214
+
215
+ IC_74LS374 = ICTiming(
216
+ part_number="74LS374",
217
+ description="Octal D-type flip-flop with 3-state outputs",
218
+ family="LS-TTL",
219
+ vcc_nom=5.0,
220
+ vcc_range=(4.75, 5.25),
221
+ timing={
222
+ "t_pd": 20e-9, # Clock-to-output
223
+ "t_su": 20e-9, # Setup time
224
+ "t_h": 5e-9, # Hold time
225
+ "t_w": 20e-9, # Min clock pulse width
226
+ "t_pz": 18e-9, # Output enable to output
227
+ "t_pzh": 30e-9, # Output disable to high-Z
228
+ "t_r": 10e-9,
229
+ "t_f": 7e-9,
230
+ },
231
+ voltage_levels={
232
+ "VIL_max": 0.8,
233
+ "VIH_min": 2.0,
234
+ "VOL_max": 0.5,
235
+ "VOH_min": 2.7,
236
+ },
237
+ )
238
+
239
+ # 74HCxx High-Speed CMOS Series (1980s-present)
240
+ IC_74HC00 = ICTiming(
241
+ part_number="74HC00",
242
+ description="Quad 2-input NAND gate",
243
+ family="HC-CMOS",
244
+ vcc_nom=5.0,
245
+ vcc_range=(2.0, 6.0),
246
+ timing={
247
+ "t_pd": 8e-9, # At 5V, typ 8ns
248
+ "t_r": 6e-9,
249
+ "t_f": 6e-9,
250
+ },
251
+ voltage_levels={
252
+ "VIL_max": 1.35,
253
+ "VIH_min": 3.15,
254
+ "VOL_max": 0.33,
255
+ "VOH_min": 3.84,
256
+ },
257
+ )
258
+
259
+ IC_74HC74 = ICTiming(
260
+ part_number="74HC74",
261
+ description="Dual D-type flip-flop",
262
+ family="HC-CMOS",
263
+ vcc_nom=5.0,
264
+ vcc_range=(2.0, 6.0),
265
+ timing={
266
+ "t_pd": 16e-9, # Clock-to-output at 5V
267
+ "t_su": 14e-9, # Setup time
268
+ "t_h": 3e-9, # Hold time
269
+ "t_w": 14e-9, # Min clock pulse width
270
+ "t_r": 6e-9,
271
+ "t_f": 6e-9,
272
+ },
273
+ voltage_levels={
274
+ "VIL_max": 1.35,
275
+ "VIH_min": 3.15,
276
+ "VOL_max": 0.33,
277
+ "VOH_min": 3.84,
278
+ },
279
+ )
280
+
281
+ IC_74HC595 = ICTiming(
282
+ part_number="74HC595",
283
+ description="8-bit shift register with output latches",
284
+ family="HC-CMOS",
285
+ vcc_nom=5.0,
286
+ vcc_range=(2.0, 6.0),
287
+ timing={
288
+ "t_pd": 16e-9, # Clock-to-output
289
+ "t_su": 14e-9, # Setup time
290
+ "t_h": 3e-9, # Hold time
291
+ "t_w": 14e-9, # Min clock pulse width
292
+ "t_r": 6e-9,
293
+ "t_f": 6e-9,
294
+ },
295
+ voltage_levels={
296
+ "VIL_max": 1.35,
297
+ "VIH_min": 3.15,
298
+ "VOL_max": 0.33,
299
+ "VOH_min": 3.84,
300
+ },
301
+ )
302
+
303
+ # 4000 Series CMOS (1970s-1980s)
304
+ IC_4011 = ICTiming(
305
+ part_number="4011",
306
+ description="Quad 2-input NAND gate",
307
+ family="CMOS_5V",
308
+ vcc_nom=5.0,
309
+ vcc_range=(3.0, 18.0),
310
+ timing={
311
+ "t_pd": 90e-9, # At 5V, typ 90ns
312
+ "t_r": 60e-9,
313
+ "t_f": 60e-9,
314
+ },
315
+ voltage_levels={
316
+ "VIL_max": 1.5,
317
+ "VIH_min": 3.5,
318
+ "VOL_max": 0.05,
319
+ "VOH_min": 4.95,
320
+ },
321
+ )
322
+
323
+ IC_4013 = ICTiming(
324
+ part_number="4013",
325
+ description="Dual D-type flip-flop",
326
+ family="CMOS_5V",
327
+ vcc_nom=5.0,
328
+ vcc_range=(3.0, 18.0),
329
+ timing={
330
+ "t_pd": 140e-9, # Clock-to-output at 5V
331
+ "t_su": 60e-9, # Setup time
332
+ "t_h": 40e-9, # Hold time
333
+ "t_w": 100e-9, # Min clock pulse width
334
+ "t_r": 60e-9,
335
+ "t_f": 60e-9,
336
+ },
337
+ voltage_levels={
338
+ "VIL_max": 1.5,
339
+ "VIH_min": 3.5,
340
+ "VOL_max": 0.05,
341
+ "VOH_min": 4.95,
342
+ },
343
+ )
344
+
345
+ # Comprehensive IC database
346
+ IC_DATABASE: dict[str, ICTiming] = {
347
+ # Standard TTL
348
+ "7400": IC_7400_STD,
349
+ # LS-TTL (74LS series preferred over 74 for specificity)
350
+ "74LS00": IC_74LS00,
351
+ "74LS74": IC_74LS74,
352
+ "74LS138": IC_74LS138,
353
+ "74LS244": IC_74LS244,
354
+ "74LS245": IC_74LS245,
355
+ "74LS273": IC_74LS273,
356
+ "74LS374": IC_74LS374,
357
+ # HC-CMOS
358
+ "74HC00": IC_74HC00,
359
+ "74HC74": IC_74HC74,
360
+ "74HC595": IC_74HC595,
361
+ # 4000 series CMOS
362
+ "4011": IC_4011,
363
+ "4013": IC_4013,
364
+ }
365
+
366
+
367
+ def identify_ic(
368
+ measured_timings: dict[str, float],
369
+ *,
370
+ tolerance: float = 0.5,
371
+ min_confidence: float = 0.6,
372
+ ) -> tuple[str, float]:
373
+ """Identify IC from measured timing parameters.
374
+
375
+ Args:
376
+ measured_timings: Dictionary of measured timing values (e.g., {'t_pd': 25e-9}).
377
+ tolerance: Allowable deviation (0.0-1.0, 0.5 = 50% tolerance).
378
+ min_confidence: Minimum confidence score (0.0-1.0).
379
+
380
+ Returns:
381
+ Tuple of (ic_name, confidence_score).
382
+ Returns ("unknown", 0.0) if no match above min_confidence.
383
+
384
+ Example:
385
+ >>> timings = {'t_pd': 25e-9, 't_su': 20e-9, 't_h': 5e-9}
386
+ >>> ic, conf = identify_ic(timings)
387
+ >>> print(f"Identified: {ic} ({conf*100:.1f}% confidence)")
388
+ """
389
+ scores: dict[str, float] = {}
390
+
391
+ for ic_name, ic_spec in IC_DATABASE.items():
392
+ # Calculate match score for this IC
393
+ param_scores = []
394
+
395
+ for param, measured_value in measured_timings.items():
396
+ if param not in ic_spec.timing:
397
+ continue
398
+
399
+ spec_value = ic_spec.timing[param]
400
+
401
+ # Calculate relative error
402
+ if spec_value == 0:
403
+ continue
404
+
405
+ error = abs(measured_value - spec_value) / spec_value
406
+
407
+ # Score based on error (within tolerance gets high score)
408
+ if error <= tolerance:
409
+ param_score = 1.0 - (error / tolerance)
410
+ else:
411
+ param_score = 0.0
412
+
413
+ param_scores.append(param_score)
414
+
415
+ # Overall score is average of parameter scores
416
+ if param_scores:
417
+ scores[ic_name] = sum(param_scores) / len(param_scores)
418
+
419
+ # Find best match
420
+ if not scores:
421
+ return ("unknown", 0.0)
422
+
423
+ best_ic = max(scores.items(), key=lambda x: x[1])
424
+
425
+ if best_ic[1] < min_confidence:
426
+ return ("unknown", best_ic[1])
427
+
428
+ return best_ic
429
+
430
+
431
+ def validate_ic_timing(
432
+ ic_name: str,
433
+ measured_timings: dict[str, float],
434
+ *,
435
+ tolerance: float = 0.3,
436
+ ) -> dict[str, dict[str, float | bool | None]]:
437
+ """Validate measured timings against IC specification.
438
+
439
+ Args:
440
+ ic_name: IC part number (e.g., "74LS74").
441
+ measured_timings: Dictionary of measured timing values.
442
+ tolerance: Allowable deviation (0.0-1.0).
443
+
444
+ Returns:
445
+ Dictionary mapping parameter names to validation results:
446
+ {'t_pd': {'measured': 25e-9, 'spec': 25e-9, 'passes': True, 'error': 0.0}}
447
+
448
+ Raises:
449
+ KeyError: If IC not found in database.
450
+
451
+ Example:
452
+ >>> results = validate_ic_timing("74LS74", {'t_pd': 30e-9})
453
+ >>> if not results['t_pd']['passes']:
454
+ ... print(f"Propagation delay out of spec!")
455
+ """
456
+ if ic_name not in IC_DATABASE:
457
+ raise KeyError(f"IC '{ic_name}' not found in database")
458
+
459
+ ic_spec = IC_DATABASE[ic_name]
460
+ results: dict[str, dict[str, float | bool | None]] = {}
461
+
462
+ for param, measured_value in measured_timings.items():
463
+ if param not in ic_spec.timing:
464
+ results[param] = {
465
+ "measured": measured_value,
466
+ "spec": None,
467
+ "passes": None,
468
+ "error": None,
469
+ }
470
+ continue
471
+
472
+ spec_value = ic_spec.timing[param]
473
+
474
+ # Calculate relative error
475
+ if spec_value == 0:
476
+ error = 0.0
477
+ else:
478
+ error = abs(measured_value - spec_value) / spec_value
479
+
480
+ # Check if within tolerance
481
+ passes = error <= tolerance
482
+
483
+ results[param] = {
484
+ "measured": measured_value,
485
+ "spec": spec_value,
486
+ "passes": passes,
487
+ "error": error,
488
+ }
489
+
490
+ return results
491
+
492
+
493
+ __all__ = [
494
+ "IC_DATABASE",
495
+ "ICTiming",
496
+ "identify_ic",
497
+ "validate_ic_timing",
498
+ ]
@@ -642,20 +642,33 @@ def recover_clock_fft(
642
642
  Detects the dominant frequency component in the signal using
643
643
  FFT analysis, suitable for periodic digital signals.
644
644
 
645
+ **Best for**: Long signals (>64 samples) with clear periodicity.
646
+ **Not recommended for**: Short random data, aperiodic signals.
647
+ For short signals, use recover_clock_edge() instead.
648
+
645
649
  Args:
646
- trace: Input trace (analog or digital).
650
+ trace: Input trace (analog or digital). Should have at least
651
+ 4-5 cycles of the clock signal for reliable detection.
647
652
  min_freq: Minimum frequency to consider (Hz). Default: sample_rate/1000.
648
653
  max_freq: Maximum frequency to consider (Hz). Default: sample_rate/2.
649
654
 
650
655
  Returns:
651
656
  ClockRecoveryResult with recovered frequency and confidence.
657
+ Confidence < 0.5 indicates unreliable detection (warning issued).
652
658
 
653
659
  Raises:
654
- InsufficientDataError: If trace has fewer than 16 samples.
660
+ InsufficientDataError: If trace has fewer than 64 samples.
661
+ ValueError: If no frequency components found in specified range.
662
+
663
+ Warnings:
664
+ UserWarning: Issued when confidence < 0.5 (unreliable result).
655
665
 
656
666
  Example:
657
667
  >>> result = recover_clock_fft(trace)
658
- >>> print(f"Clock: {result.frequency / 1e6:.3f} MHz")
668
+ >>> if result.confidence > 0.7:
669
+ ... print(f"Clock: {result.frequency / 1e6:.3f} MHz")
670
+ >>> else:
671
+ ... print("Low confidence - try edge-based recovery")
659
672
 
660
673
  References:
661
674
  IEEE 1241-2010 Section 4.1
@@ -665,12 +678,17 @@ def recover_clock_fft(
665
678
  n = len(data)
666
679
  sample_rate = trace.metadata.sample_rate
667
680
 
668
- if n < 16:
681
+ # FFT requires sufficient samples for reliable frequency resolution
682
+ # Rule of thumb: At least 4-5 cycles of the signal for accurate peak detection
683
+ # With typical bit rates, this means ~100-200 samples minimum
684
+ min_samples = 64 # Increased from 16 for better frequency resolution
685
+ if n < min_samples:
669
686
  raise InsufficientDataError(
670
- "FFT clock recovery requires at least 16 samples",
671
- required=16,
687
+ f"FFT clock recovery requires at least {min_samples} samples for reliable frequency detection",
688
+ required=min_samples,
672
689
  available=n,
673
690
  analysis_type="clock_recovery_fft",
691
+ fix_hint="Use edge-based clock recovery for short signals or acquire more data",
674
692
  )
675
693
 
676
694
  # Set frequency range defaults
@@ -691,11 +709,11 @@ def recover_clock_fft(
691
709
  valid_indices = np.where(mask)[0]
692
710
 
693
711
  if len(valid_indices) == 0:
694
- return ClockRecoveryResult(
695
- frequency=np.nan,
696
- period=np.nan,
697
- method="fft",
698
- confidence=0.0,
712
+ # No valid frequencies in range - signal may be DC or out of range
713
+ raise ValueError(
714
+ f"No frequency components found in range [{min_freq:.0f} Hz, {max_freq:.0f} Hz]. "
715
+ f"Signal may be constant (DC) or frequency is outside specified range. "
716
+ f"Adjust min_freq/max_freq or check signal integrity."
699
717
  )
700
718
 
701
719
  # Find peak in valid range
@@ -721,6 +739,18 @@ def recover_clock_fft(
721
739
 
722
740
  period = 1.0 / peak_freq if peak_freq > 0 else np.nan
723
741
 
742
+ # Warn on low confidence results (may be unreliable)
743
+ if confidence < 0.5:
744
+ import warnings
745
+
746
+ warnings.warn(
747
+ f"FFT clock recovery has low confidence ({confidence:.2f}). "
748
+ f"Detected frequency: {peak_freq / 1e6:.3f} MHz. "
749
+ f"Consider using longer signal, edge-based recovery, or verifying signal periodicity.",
750
+ UserWarning,
751
+ stacklevel=2,
752
+ )
753
+
724
754
  return ClockRecoveryResult(
725
755
  frequency=float(peak_freq),
726
756
  period=float(period),