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
@@ -52,9 +52,17 @@ from oscura.analyzers.digital.edges import (
52
52
  from oscura.analyzers.digital.extraction import (
53
53
  LOGIC_FAMILIES,
54
54
  detect_edges,
55
+ detect_logic_family,
56
+ detect_open_collector,
55
57
  get_logic_threshold,
56
58
  to_digital,
57
59
  )
60
+ from oscura.analyzers.digital.ic_database import (
61
+ IC_DATABASE,
62
+ ICTiming,
63
+ identify_ic,
64
+ validate_ic_timing,
65
+ )
58
66
  from oscura.analyzers.digital.quality import (
59
67
  Glitch,
60
68
  NoiseMarginResult,
@@ -97,13 +105,37 @@ from oscura.analyzers.digital.timing import (
97
105
  skew,
98
106
  slew_rate,
99
107
  )
108
+ from oscura.analyzers.digital.timing_paths import (
109
+ ICStage,
110
+ SetupHoldAnalysis,
111
+ TimingPathResult,
112
+ analyze_setup_hold,
113
+ analyze_timing_path,
114
+ calculate_timing_budget,
115
+ find_critical_paths,
116
+ )
117
+ from oscura.analyzers.digital.vintage import (
118
+ REPLACEMENT_DATABASE,
119
+ analyze_vintage_logic,
120
+ )
121
+ from oscura.analyzers.digital.vintage_result import (
122
+ BOMEntry,
123
+ ICIdentificationResult,
124
+ ModernReplacementIC,
125
+ VintageLogicAnalysisResult,
126
+ )
100
127
 
101
128
  __all__ = [
129
+ # IC Database
130
+ "IC_DATABASE",
102
131
  # Extraction
103
132
  "LOGIC_FAMILIES",
133
+ "REPLACEMENT_DATABASE",
104
134
  "AdaptiveThresholdResult",
105
135
  # Adaptive Thresholds (RE-THR-001)
106
136
  "AdaptiveThresholder",
137
+ # Vintage Logic Analysis
138
+ "BOMEntry",
107
139
  # Clock Recovery (DSP-002)
108
140
  "BaudRateResult",
109
141
  # Bus Decoding (DSP-003)
@@ -125,6 +157,10 @@ __all__ = [
125
157
  "EdgeTimingViolation",
126
158
  # Quality
127
159
  "Glitch",
160
+ "ICIdentificationResult",
161
+ "ICStage",
162
+ "ICTiming",
163
+ "ModernReplacementIC",
128
164
  # Multi-Level Logic (RE-THR-002)
129
165
  "MultiLevelDetector",
130
166
  "MultiLevelResult",
@@ -132,18 +168,25 @@ __all__ = [
132
168
  # Signal Quality (DSP-005)
133
169
  "NoiseMargins",
134
170
  "ParallelBusConfig",
171
+ "SetupHoldAnalysis",
135
172
  "SignalIntegrityReport",
136
173
  "SignalQualityAnalyzer",
137
174
  "SimpleQualityMetrics",
138
175
  "ThresholdConfig",
139
176
  "TimingConstraint",
177
+ "TimingPathResult",
140
178
  "TimingViolation",
141
179
  "TransitionMetrics",
180
+ "VintageLogicAnalysisResult",
142
181
  "Violation",
143
182
  "align_by_trigger",
183
+ "analyze_setup_hold",
144
184
  "analyze_signal_integrity",
185
+ "analyze_timing_path",
186
+ "analyze_vintage_logic",
145
187
  "apply_adaptive_threshold",
146
188
  "calculate_threshold_snr",
189
+ "calculate_timing_budget",
147
190
  "check_timing_constraints",
148
191
  "classify_edge_quality",
149
192
  "correlate_channels",
@@ -153,10 +196,14 @@ __all__ = [
153
196
  "detect_edges",
154
197
  "detect_edges_advanced",
155
198
  "detect_glitches",
199
+ "detect_logic_family",
156
200
  "detect_multi_level",
201
+ "detect_open_collector",
157
202
  "detect_violations",
203
+ "find_critical_paths",
158
204
  "get_logic_threshold",
159
205
  "hold_time",
206
+ "identify_ic",
160
207
  "interpolate_edge_time",
161
208
  "measure_clock_jitter",
162
209
  "measure_edge_timing",
@@ -174,4 +221,5 @@ __all__ = [
174
221
  "skew",
175
222
  "slew_rate",
176
223
  "to_digital",
224
+ "validate_ic_timing",
177
225
  ]
@@ -24,7 +24,6 @@ from dataclasses import dataclass
24
24
  from typing import TYPE_CHECKING, Any, ClassVar, Literal
25
25
 
26
26
  import numpy as np
27
- from scipy import signal
28
27
 
29
28
  from oscura.core.exceptions import InsufficientDataError, ValidationError
30
29
 
@@ -275,6 +274,9 @@ class ClockRecovery:
275
274
  # Use PLL tracking for robust recovery
276
275
  return self._pll_track(data_trace, freq)
277
276
  else:
277
+ # Lazy import to avoid loading scipy at module import time
278
+ from scipy import signal
279
+
278
280
  # Generate ideal square wave at detected frequency
279
281
  _period_samples = self.sample_rate / freq
280
282
  n_samples = len(data_trace)
@@ -498,6 +500,9 @@ class ClockRecovery:
498
500
  Raises:
499
501
  ValidationError: If sample rate is not set.
500
502
  """
503
+ # Lazy import to avoid loading scipy at module import time
504
+ from scipy import signal
505
+
501
506
  # Remove DC component
502
507
  trace_ac = trace - np.mean(trace)
503
508
 
@@ -539,6 +544,9 @@ class ClockRecovery:
539
544
  Raises:
540
545
  ValidationError: If no periodic pattern detected or sample rate not set.
541
546
  """
547
+ # Lazy import to avoid loading scipy at module import time
548
+ from scipy import signal
549
+
542
550
  # Remove mean
543
551
  trace_centered = trace - np.mean(trace)
544
552
 
@@ -143,7 +143,7 @@ def detect_edges(
143
143
  if len(trace) < 2:
144
144
  return []
145
145
 
146
- trace = np.asarray(trace)
146
+ trace = np.asarray(trace, dtype=np.float64)
147
147
 
148
148
  # Compute threshold if auto
149
149
  thresh_val: float
@@ -25,6 +25,7 @@ if TYPE_CHECKING:
25
25
  # Standard logic family threshold constants
26
26
  # Reference: Various IC manufacturer datasheets
27
27
  LOGIC_FAMILIES: dict[str, dict[str, float]] = {
28
+ # Modern logic families (5V)
28
29
  "TTL": {
29
30
  "VIL_max": 0.8, # Maximum input low voltage
30
31
  "VIH_min": 2.0, # Minimum input high voltage
@@ -74,6 +75,63 @@ LOGIC_FAMILIES: dict[str, dict[str, float]] = {
74
75
  "VOH_min": 1.1,
75
76
  "VCC": 1.2,
76
77
  },
78
+ # Vintage logic families (1960s-1970s)
79
+ "ECL": {
80
+ "VIL_max": -1.475, # ECL 10K series
81
+ "VIH_min": -1.105,
82
+ "VOL_max": -1.630, # Typical -1.63V
83
+ "VOH_min": -0.980, # Typical -0.98V
84
+ "VCC": 0.0, # Ground-referenced
85
+ "VEE": -5.2, # Negative supply
86
+ "differential": True, # Differential signaling
87
+ },
88
+ "ECL_100K": {
89
+ "VIL_max": -1.810, # ECL 100K series (faster)
90
+ "VIH_min": -1.620,
91
+ "VOL_max": -1.950,
92
+ "VOH_min": -1.650,
93
+ "VCC": 0.0,
94
+ "VEE": -5.2,
95
+ "differential": True,
96
+ },
97
+ "RTL": {
98
+ "VIL_max": 0.4, # Resistor-Transistor Logic
99
+ "VIH_min": 0.9,
100
+ "VOL_max": 0.2,
101
+ "VOH_min": 3.6, # Typical 3.6V high output
102
+ "VCC": 3.6, # 3.6V supply common
103
+ },
104
+ "DTL": {
105
+ "VIL_max": 0.5, # Diode-Transistor Logic
106
+ "VIH_min": 2.0,
107
+ "VOL_max": 0.4,
108
+ "VOH_min": 4.0, # Typical 4V high output
109
+ "VCC": 5.0,
110
+ },
111
+ "MOS": {
112
+ "VIL_max": -3.0, # P-channel MOS (negative logic)
113
+ "VIH_min": -10.0,
114
+ "VOL_max": -0.5,
115
+ "VOH_min": -11.5,
116
+ "VCC": 0.0, # Ground
117
+ "VDD": -12.0, # Negative supply
118
+ },
119
+ "PMOS": {
120
+ "VIL_max": -3.0, # PMOS (4000 series at -12V)
121
+ "VIH_min": -9.0,
122
+ "VOL_max": -0.5,
123
+ "VOH_min": -11.5,
124
+ "VCC": 0.0,
125
+ "VDD": -12.0,
126
+ },
127
+ "NMOS": {
128
+ "VIL_max": 1.5, # NMOS (positive logic, 12V)
129
+ "VIH_min": 8.0,
130
+ "VOL_max": 0.5,
131
+ "VOH_min": 11.5,
132
+ "VCC": 12.0,
133
+ "VSS": 0.0,
134
+ },
77
135
  }
78
136
 
79
137
 
@@ -405,9 +463,146 @@ def get_logic_threshold(
405
463
  raise ValueError(f"Unknown threshold_type: {threshold_type}")
406
464
 
407
465
 
466
+ def detect_logic_family(
467
+ trace: WaveformTrace,
468
+ *,
469
+ confidence_threshold: float = 0.8,
470
+ ) -> tuple[str, float]:
471
+ """Auto-detect logic family from signal voltage levels.
472
+
473
+ Analyzes the signal to determine which logic family it most likely represents
474
+ based on voltage levels.
475
+
476
+ Args:
477
+ trace: Input analog waveform trace.
478
+ confidence_threshold: Minimum confidence (0-1) to return a match.
479
+
480
+ Returns:
481
+ Tuple of (family_name, confidence_score).
482
+ Returns ("unknown", 0.0) if no match above threshold.
483
+
484
+ Example:
485
+ >>> family, conf = detect_logic_family(trace)
486
+ >>> print(f"Detected: {family} ({conf*100:.1f}% confidence)")
487
+ """
488
+ if len(trace.data) < 10:
489
+ return ("unknown", 0.0)
490
+
491
+ data = np.asarray(trace.data)
492
+
493
+ # Find voltage levels using percentiles
494
+ p10, p90 = np.percentile(data, [10, 90])
495
+ vlow = p10
496
+ vhigh = p90
497
+
498
+ # Calculate midpoint and swing
499
+ vmid = (vlow + vhigh) / 2.0
500
+ vswing = vhigh - vlow
501
+
502
+ if vswing < 0.1: # Insufficient signal
503
+ return ("unknown", 0.0)
504
+
505
+ # Score each logic family
506
+ scores: dict[str, float] = {}
507
+
508
+ for family_name, levels in LOGIC_FAMILIES.items():
509
+ # Check if voltage levels match this family
510
+ vil_max = levels["VIL_max"]
511
+ vih_min = levels["VIH_min"]
512
+ vcc = levels.get("VCC", 5.0)
513
+
514
+ # Expected midpoint for this family
515
+ expected_mid = (vil_max + vih_min) / 2.0
516
+
517
+ # Score based on:
518
+ # 1. Midpoint proximity
519
+ # 2. High level proximity to VOH
520
+ # 3. Low level proximity to VOL
521
+
522
+ mid_error = abs(vmid - expected_mid)
523
+ high_error = abs(vhigh - levels.get("VOH_min", vih_min))
524
+ low_error = abs(vlow - levels.get("VOL_max", vil_max))
525
+
526
+ # Normalize errors (handle VCC=0 for ECL/PMOS)
527
+ if vcc != 0:
528
+ mid_score = max(0, 1.0 - mid_error / abs(vcc))
529
+ high_score = max(0, 1.0 - high_error / abs(vcc))
530
+ low_score = max(0, 1.0 - low_error / abs(vcc))
531
+ else:
532
+ # For VCC=0 families (ECL), use voltage range instead
533
+ voltage_range = abs(vih_min - vil_max)
534
+ if voltage_range > 0:
535
+ mid_score = max(0, 1.0 - mid_error / voltage_range)
536
+ high_score = max(0, 1.0 - high_error / voltage_range)
537
+ low_score = max(0, 1.0 - low_error / voltage_range)
538
+ else:
539
+ mid_score = high_score = low_score = 0.0
540
+
541
+ # Combined score (weighted average)
542
+ total_score = mid_score * 0.5 + high_score * 0.25 + low_score * 0.25
543
+ scores[family_name] = total_score
544
+
545
+ # Find best match
546
+ if not scores:
547
+ return ("unknown", 0.0)
548
+
549
+ best_family = max(scores.items(), key=lambda x: x[1])
550
+
551
+ if best_family[1] < confidence_threshold:
552
+ return ("unknown", best_family[1])
553
+
554
+ return best_family
555
+
556
+
557
+ def detect_open_collector(
558
+ trace: WaveformTrace,
559
+ *,
560
+ asymmetry_threshold: float = 3.0,
561
+ ) -> tuple[bool, float]:
562
+ """Detect open-collector or open-drain output.
563
+
564
+ Open-collector outputs have slow rise times (limited by pull-up resistor)
565
+ and fast fall times (active transistor).
566
+
567
+ Args:
568
+ trace: Input analog waveform trace.
569
+ asymmetry_threshold: Minimum rise/fall ratio to indicate open-collector.
570
+
571
+ Returns:
572
+ Tuple of (is_open_collector, asymmetry_ratio).
573
+
574
+ Example:
575
+ >>> is_oc, ratio = detect_open_collector(trace)
576
+ >>> if is_oc:
577
+ ... print(f"Open-collector detected (rise/fall = {ratio:.1f})")
578
+ """
579
+ from oscura.analyzers.waveform.measurements import fall_time, rise_time
580
+
581
+ if len(trace.data) < 10:
582
+ return (False, 1.0)
583
+
584
+ # Measure rise and fall times
585
+ tr = rise_time(trace, ref_levels=(0.1, 0.9))
586
+ tf = fall_time(trace, ref_levels=(0.9, 0.1))
587
+
588
+ # Check for valid measurements
589
+ if np.isnan(tr) or np.isnan(tf) or tf == 0:
590
+ return (False, 1.0)
591
+
592
+ # Calculate asymmetry ratio
593
+ asymmetry = tr / tf
594
+
595
+ # Open-collector has slow rise, fast fall
596
+ is_oc = bool(asymmetry >= asymmetry_threshold)
597
+
598
+ return (is_oc, float(asymmetry))
599
+
600
+
408
601
  __all__ = [
409
602
  "LOGIC_FAMILIES",
410
603
  "detect_edges",
604
+ "detect_logic_family",
605
+ "detect_open_collector",
411
606
  "get_logic_threshold",
412
607
  "to_digital",
413
608
  ]