oscura 0.7.0__py3-none-any.whl → 0.10.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.
- oscura/__init__.py +19 -19
- oscura/analyzers/__init__.py +2 -0
- oscura/analyzers/digital/extraction.py +2 -3
- oscura/analyzers/digital/quality.py +1 -1
- oscura/analyzers/digital/timing.py +1 -1
- oscura/analyzers/eye/__init__.py +5 -1
- oscura/analyzers/eye/generation.py +501 -0
- oscura/analyzers/jitter/__init__.py +6 -6
- oscura/analyzers/jitter/timing.py +419 -0
- oscura/analyzers/patterns/__init__.py +94 -0
- oscura/analyzers/patterns/reverse_engineering.py +991 -0
- oscura/analyzers/power/__init__.py +35 -12
- oscura/analyzers/power/basic.py +3 -3
- oscura/analyzers/power/soa.py +1 -1
- oscura/analyzers/power/switching.py +3 -3
- oscura/analyzers/signal_classification.py +529 -0
- oscura/analyzers/signal_integrity/sparams.py +3 -3
- oscura/analyzers/statistics/__init__.py +4 -0
- oscura/analyzers/statistics/basic.py +152 -0
- oscura/analyzers/statistics/correlation.py +47 -6
- oscura/analyzers/validation.py +1 -1
- oscura/analyzers/waveform/__init__.py +2 -0
- oscura/analyzers/waveform/measurements.py +329 -163
- oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
- oscura/analyzers/waveform/spectral.py +498 -54
- oscura/api/dsl/commands.py +15 -6
- oscura/api/server/templates/base.html +137 -146
- oscura/api/server/templates/export.html +84 -110
- oscura/api/server/templates/home.html +248 -267
- oscura/api/server/templates/protocols.html +44 -48
- oscura/api/server/templates/reports.html +27 -35
- oscura/api/server/templates/session_detail.html +68 -78
- oscura/api/server/templates/sessions.html +62 -72
- oscura/api/server/templates/waveforms.html +54 -64
- oscura/automotive/__init__.py +1 -1
- oscura/automotive/can/session.py +1 -1
- oscura/automotive/dbc/generator.py +638 -23
- oscura/automotive/dtc/data.json +102 -17
- oscura/automotive/uds/decoder.py +99 -6
- oscura/cli/analyze.py +8 -2
- oscura/cli/batch.py +36 -5
- oscura/cli/characterize.py +18 -4
- oscura/cli/export.py +47 -5
- oscura/cli/main.py +2 -0
- oscura/cli/onboarding/wizard.py +10 -6
- oscura/cli/pipeline.py +585 -0
- oscura/cli/visualize.py +6 -4
- oscura/convenience.py +400 -32
- oscura/core/config/loader.py +0 -1
- oscura/core/measurement_result.py +286 -0
- oscura/core/progress.py +1 -1
- oscura/core/schemas/device_mapping.json +8 -2
- oscura/core/schemas/packet_format.json +24 -4
- oscura/core/schemas/protocol_definition.json +12 -2
- oscura/core/types.py +300 -199
- oscura/correlation/multi_protocol.py +1 -1
- oscura/export/legacy/__init__.py +11 -0
- oscura/export/legacy/wav.py +75 -0
- oscura/exporters/__init__.py +19 -0
- oscura/exporters/wireshark.py +809 -0
- oscura/hardware/acquisition/file.py +5 -19
- oscura/hardware/acquisition/saleae.py +10 -10
- oscura/hardware/acquisition/socketcan.py +4 -6
- oscura/hardware/acquisition/synthetic.py +1 -5
- oscura/hardware/acquisition/visa.py +6 -6
- oscura/hardware/security/side_channel_detector.py +5 -508
- oscura/inference/message_format.py +686 -1
- oscura/jupyter/display.py +2 -2
- oscura/jupyter/magic.py +3 -3
- oscura/loaders/__init__.py +17 -12
- oscura/loaders/binary.py +1 -1
- oscura/loaders/chipwhisperer.py +1 -2
- oscura/loaders/configurable.py +1 -1
- oscura/loaders/csv_loader.py +2 -2
- oscura/loaders/hdf5_loader.py +1 -1
- oscura/loaders/lazy.py +6 -1
- oscura/loaders/mmap_loader.py +0 -1
- oscura/loaders/numpy_loader.py +8 -7
- oscura/loaders/preprocessing.py +3 -5
- oscura/loaders/rigol.py +21 -7
- oscura/loaders/sigrok.py +2 -5
- oscura/loaders/tdms.py +3 -2
- oscura/loaders/tektronix.py +38 -32
- oscura/loaders/tss.py +20 -27
- oscura/loaders/vcd.py +13 -8
- oscura/loaders/wav.py +1 -6
- oscura/pipeline/__init__.py +76 -0
- oscura/pipeline/handlers/__init__.py +165 -0
- oscura/pipeline/handlers/analyzers.py +1045 -0
- oscura/pipeline/handlers/decoders.py +899 -0
- oscura/pipeline/handlers/exporters.py +1103 -0
- oscura/pipeline/handlers/filters.py +891 -0
- oscura/pipeline/handlers/loaders.py +640 -0
- oscura/pipeline/handlers/transforms.py +768 -0
- oscura/reporting/__init__.py +88 -1
- oscura/reporting/automation.py +348 -0
- oscura/reporting/citations.py +374 -0
- oscura/reporting/core.py +54 -0
- oscura/reporting/formatting/__init__.py +11 -0
- oscura/reporting/formatting/measurements.py +320 -0
- oscura/reporting/html.py +57 -0
- oscura/reporting/interpretation.py +431 -0
- oscura/reporting/summary.py +329 -0
- oscura/reporting/templates/enhanced/protocol_re.html +504 -503
- oscura/reporting/visualization.py +542 -0
- oscura/side_channel/__init__.py +38 -57
- oscura/utils/builders/signal_builder.py +5 -5
- oscura/utils/comparison/compare.py +7 -9
- oscura/utils/comparison/golden.py +1 -1
- oscura/utils/filtering/convenience.py +2 -2
- oscura/utils/math/arithmetic.py +38 -62
- oscura/utils/math/interpolation.py +20 -20
- oscura/utils/pipeline/__init__.py +4 -17
- oscura/utils/progressive.py +1 -4
- oscura/utils/triggering/edge.py +1 -1
- oscura/utils/triggering/pattern.py +2 -2
- oscura/utils/triggering/pulse.py +2 -2
- oscura/utils/triggering/window.py +3 -3
- oscura/validation/hil_testing.py +11 -11
- oscura/visualization/__init__.py +47 -284
- oscura/visualization/batch.py +160 -0
- oscura/visualization/plot.py +542 -53
- oscura/visualization/styles.py +184 -318
- oscura/workflows/__init__.py +2 -0
- oscura/workflows/batch/advanced.py +1 -1
- oscura/workflows/batch/aggregate.py +7 -8
- oscura/workflows/complete_re.py +251 -23
- oscura/workflows/digital.py +27 -4
- oscura/workflows/multi_trace.py +136 -17
- oscura/workflows/waveform.py +788 -0
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/METADATA +59 -79
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/RECORD +135 -149
- oscura/side_channel/dpa.py +0 -1025
- oscura/utils/optimization/__init__.py +0 -19
- oscura/utils/optimization/parallel.py +0 -443
- oscura/utils/optimization/search.py +0 -532
- oscura/utils/pipeline/base.py +0 -338
- oscura/utils/pipeline/composition.py +0 -248
- oscura/utils/pipeline/parallel.py +0 -449
- oscura/utils/pipeline/pipeline.py +0 -375
- oscura/utils/search/__init__.py +0 -16
- oscura/utils/search/anomaly.py +0 -424
- oscura/utils/search/context.py +0 -294
- oscura/utils/search/pattern.py +0 -288
- oscura/utils/storage/__init__.py +0 -61
- oscura/utils/storage/database.py +0 -1166
- oscura/visualization/accessibility.py +0 -526
- oscura/visualization/annotations.py +0 -371
- oscura/visualization/axis_scaling.py +0 -305
- oscura/visualization/colors.py +0 -451
- oscura/visualization/digital.py +0 -436
- oscura/visualization/eye.py +0 -571
- oscura/visualization/histogram.py +0 -281
- oscura/visualization/interactive.py +0 -1035
- oscura/visualization/jitter.py +0 -1042
- oscura/visualization/keyboard.py +0 -394
- oscura/visualization/layout.py +0 -400
- oscura/visualization/optimization.py +0 -1079
- oscura/visualization/palettes.py +0 -446
- oscura/visualization/power.py +0 -508
- oscura/visualization/power_extended.py +0 -955
- oscura/visualization/presets.py +0 -469
- oscura/visualization/protocols.py +0 -1246
- oscura/visualization/render.py +0 -223
- oscura/visualization/rendering.py +0 -444
- oscura/visualization/reverse_engineering.py +0 -838
- oscura/visualization/signal_integrity.py +0 -989
- oscura/visualization/specialized.py +0 -643
- oscura/visualization/spectral.py +0 -1226
- oscura/visualization/thumbnails.py +0 -340
- oscura/visualization/time_axis.py +0 -351
- oscura/visualization/waveform.py +0 -454
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/WHEEL +0 -0
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/licenses/LICENSE +0 -0
oscura/core/types.py
CHANGED
|
@@ -9,12 +9,14 @@ Requirements addressed:
|
|
|
9
9
|
- CORE-003: DigitalTrace Data Class
|
|
10
10
|
- CORE-004: ProtocolPacket Data Class
|
|
11
11
|
- CORE-005: CalibrationInfo Data Class (regulatory compliance)
|
|
12
|
+
- CORE-006: MeasurementResult TypedDict (v0.9.0)
|
|
12
13
|
"""
|
|
13
14
|
|
|
14
15
|
from __future__ import annotations
|
|
15
16
|
|
|
16
17
|
from dataclasses import dataclass, field
|
|
17
|
-
from
|
|
18
|
+
from datetime import UTC, datetime
|
|
19
|
+
from typing import TYPE_CHECKING, Any, TypedDict
|
|
18
20
|
|
|
19
21
|
import numpy as np
|
|
20
22
|
|
|
@@ -24,6 +26,56 @@ if TYPE_CHECKING:
|
|
|
24
26
|
from numpy.typing import NDArray
|
|
25
27
|
|
|
26
28
|
|
|
29
|
+
class MeasurementResult(TypedDict, total=False):
|
|
30
|
+
"""Structured measurement result with metadata and applicability tracking.
|
|
31
|
+
|
|
32
|
+
This replaces raw float values to handle edge cases gracefully and provide
|
|
33
|
+
rich metadata for reporting and interpretation.
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
value: Measurement value (None if not applicable).
|
|
37
|
+
unit: Unit of measurement (e.g., "V", "Hz", "s", "dB", "%", "ratio").
|
|
38
|
+
applicable: Whether measurement is applicable to this signal type.
|
|
39
|
+
reason: Explanation if not applicable (e.g., "Aperiodic signal").
|
|
40
|
+
display: Human-readable formatted display string.
|
|
41
|
+
|
|
42
|
+
Example:
|
|
43
|
+
>>> # Applicable measurement
|
|
44
|
+
>>> freq_result: MeasurementResult = {
|
|
45
|
+
... "value": 1000.0,
|
|
46
|
+
... "unit": "Hz",
|
|
47
|
+
... "applicable": True,
|
|
48
|
+
... "reason": None,
|
|
49
|
+
... "display": "1.000 kHz"
|
|
50
|
+
... }
|
|
51
|
+
|
|
52
|
+
>>> # Inapplicable measurement (no NaN!)
|
|
53
|
+
>>> period_result: MeasurementResult = {
|
|
54
|
+
... "value": None,
|
|
55
|
+
... "unit": "s",
|
|
56
|
+
... "applicable": False,
|
|
57
|
+
... "reason": "Aperiodic signal (single impulse)",
|
|
58
|
+
... "display": "N/A"
|
|
59
|
+
... }
|
|
60
|
+
|
|
61
|
+
>>> # Access safely
|
|
62
|
+
>>> if period_result["applicable"]:
|
|
63
|
+
... print(f"Period: {period_result['display']}")
|
|
64
|
+
... else:
|
|
65
|
+
... print(f"Period: {period_result['display']} ({period_result['reason']})")
|
|
66
|
+
Period: N/A (Aperiodic signal (single impulse))
|
|
67
|
+
|
|
68
|
+
References:
|
|
69
|
+
API Improvement Recommendation #3 (v0.9.0)
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
value: float | None
|
|
73
|
+
unit: str
|
|
74
|
+
applicable: bool
|
|
75
|
+
reason: str | None
|
|
76
|
+
display: str
|
|
77
|
+
|
|
78
|
+
|
|
27
79
|
@dataclass
|
|
28
80
|
class CalibrationInfo:
|
|
29
81
|
"""Calibration and instrument provenance information.
|
|
@@ -99,346 +151,394 @@ class CalibrationInfo:
|
|
|
99
151
|
"""
|
|
100
152
|
if self.calibration_date is None or self.calibration_due_date is None:
|
|
101
153
|
return None
|
|
102
|
-
from datetime import datetime
|
|
103
154
|
|
|
104
|
-
|
|
155
|
+
from datetime import datetime
|
|
105
156
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
157
|
+
now = datetime.now(UTC)
|
|
158
|
+
# Ensure dates are timezone-aware for comparison
|
|
159
|
+
due_date = self.calibration_due_date
|
|
160
|
+
if due_date.tzinfo is None:
|
|
161
|
+
due_date = due_date.replace(tzinfo=UTC)
|
|
109
162
|
|
|
110
|
-
|
|
111
|
-
Human-readable summary of calibration traceability.
|
|
112
|
-
"""
|
|
113
|
-
parts = [f"Instrument: {self.instrument}"]
|
|
114
|
-
if self.serial_number:
|
|
115
|
-
parts.append(f"S/N: {self.serial_number}")
|
|
116
|
-
if self.calibration_date:
|
|
117
|
-
parts.append(f"Cal Date: {self.calibration_date.strftime('%Y-%m-%d')}")
|
|
118
|
-
if self.calibration_due_date:
|
|
119
|
-
parts.append(f"Due: {self.calibration_due_date.strftime('%Y-%m-%d')}")
|
|
120
|
-
if self.calibration_cert_number:
|
|
121
|
-
parts.append(f"Cert: {self.calibration_cert_number}")
|
|
122
|
-
return ", ".join(parts)
|
|
163
|
+
return now < due_date
|
|
123
164
|
|
|
124
165
|
|
|
125
166
|
@dataclass
|
|
126
167
|
class TraceMetadata:
|
|
127
|
-
"""Metadata
|
|
168
|
+
"""Metadata for waveform and digital traces.
|
|
128
169
|
|
|
129
|
-
|
|
130
|
-
|
|
170
|
+
Stores acquisition parameters, channel information, and optional
|
|
171
|
+
calibration data for oscilloscope/logic analyzer captures.
|
|
131
172
|
|
|
132
173
|
Attributes:
|
|
133
|
-
sample_rate: Sample rate in Hz
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
174
|
+
sample_rate: Sample rate in Hz.
|
|
175
|
+
start_time: Start time in seconds (relative to trigger).
|
|
176
|
+
channel: Channel name or number.
|
|
177
|
+
units: Physical units (e.g., "V", "A", "Pa").
|
|
178
|
+
calibration: Optional calibration and provenance information.
|
|
179
|
+
trigger_time: Trigger timestamp in seconds (optional).
|
|
180
|
+
coupling: Input coupling mode ("DC", "AC", "GND") (optional).
|
|
181
|
+
probe_attenuation: Probe attenuation factor (optional).
|
|
182
|
+
bandwidth_limit: Bandwidth limit in Hz (optional).
|
|
183
|
+
vertical_offset: Vertical offset in physical units (optional).
|
|
184
|
+
vertical_scale: Vertical scale in units/division (optional).
|
|
185
|
+
horizontal_scale: Horizontal scale in seconds/division (optional).
|
|
186
|
+
source_file: Source file path for loaded data (optional).
|
|
187
|
+
trigger_info: Additional trigger metadata as dict (optional).
|
|
188
|
+
acquisition_time: Timestamp when data was acquired (optional).
|
|
141
189
|
|
|
142
190
|
Example:
|
|
143
|
-
>>>
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
>>> from datetime import datetime
|
|
149
|
-
>>> cal = CalibrationInfo(
|
|
150
|
-
... instrument="Tektronix DPO7254C",
|
|
151
|
-
... calibration_date=datetime(2024, 12, 15)
|
|
191
|
+
>>> meta = TraceMetadata(
|
|
192
|
+
... sample_rate=1e9,
|
|
193
|
+
... start_time=-0.001,
|
|
194
|
+
... channel="CH1",
|
|
195
|
+
... units="V"
|
|
152
196
|
... )
|
|
153
|
-
>>>
|
|
154
|
-
|
|
155
|
-
Instrument: Tektronix DPO7254C, Cal Date: 2024-12-15
|
|
156
|
-
|
|
157
|
-
References:
|
|
158
|
-
IEEE 181-2011: Standard for Transitional Waveform Definitions
|
|
159
|
-
ISO/IEC 17025: General Requirements for Testing/Calibration Laboratories
|
|
197
|
+
>>> print(f"Sample rate: {meta.sample_rate/1e6:.1f} MS/s")
|
|
198
|
+
Sample rate: 1000.0 MS/s
|
|
160
199
|
"""
|
|
161
200
|
|
|
162
201
|
sample_rate: float
|
|
163
|
-
|
|
202
|
+
start_time: float = 0.0
|
|
203
|
+
channel: str = "CH1"
|
|
204
|
+
units: str = "V"
|
|
205
|
+
calibration: CalibrationInfo | None = None
|
|
206
|
+
trigger_time: float | None = None
|
|
207
|
+
coupling: str | None = None
|
|
208
|
+
probe_attenuation: float | None = None
|
|
209
|
+
bandwidth_limit: float | None = None
|
|
164
210
|
vertical_offset: float | None = None
|
|
165
|
-
|
|
166
|
-
|
|
211
|
+
vertical_scale: float | None = None
|
|
212
|
+
horizontal_scale: float | None = None
|
|
167
213
|
source_file: str | None = None
|
|
168
|
-
|
|
169
|
-
|
|
214
|
+
trigger_info: dict[str, Any] | None = None
|
|
215
|
+
acquisition_time: datetime | None = None
|
|
170
216
|
|
|
171
217
|
def __post_init__(self) -> None:
|
|
172
218
|
"""Validate metadata after initialization."""
|
|
173
219
|
if self.sample_rate <= 0:
|
|
174
220
|
raise ValueError(f"sample_rate must be positive, got {self.sample_rate}")
|
|
175
221
|
|
|
176
|
-
@property
|
|
177
|
-
def time_base(self) -> float:
|
|
178
|
-
"""Time between samples in seconds (derived from sample_rate).
|
|
179
|
-
|
|
180
|
-
Returns:
|
|
181
|
-
Time per sample in seconds (1 / sample_rate).
|
|
182
|
-
"""
|
|
183
|
-
return 1.0 / self.sample_rate
|
|
184
|
-
|
|
185
222
|
|
|
186
223
|
@dataclass
|
|
187
224
|
class WaveformTrace:
|
|
188
|
-
"""Analog waveform
|
|
225
|
+
"""Analog waveform trace with metadata.
|
|
189
226
|
|
|
190
|
-
|
|
191
|
-
|
|
227
|
+
Represents time-series voltage/current data from an oscilloscope or
|
|
228
|
+
similar instrument. Provides properties for signal type detection.
|
|
192
229
|
|
|
193
230
|
Attributes:
|
|
194
|
-
data: Waveform
|
|
195
|
-
metadata:
|
|
231
|
+
data: Waveform data array (voltage/current values).
|
|
232
|
+
metadata: Trace metadata (sample rate, channel, units).
|
|
233
|
+
processing_history: List of processing operations applied to this trace.
|
|
234
|
+
Each entry is a dict with keys: operation, params, timestamp.
|
|
235
|
+
Enables tracking the full processing chain for reproducibility.
|
|
236
|
+
|
|
237
|
+
Properties:
|
|
238
|
+
is_analog: Always True for WaveformTrace.
|
|
239
|
+
is_digital: Always False for WaveformTrace.
|
|
240
|
+
is_iq: Always False for WaveformTrace.
|
|
241
|
+
signal_type: Returns "analog".
|
|
196
242
|
|
|
197
243
|
Example:
|
|
198
244
|
>>> import numpy as np
|
|
199
|
-
>>>
|
|
200
|
-
>>>
|
|
201
|
-
>>>
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
245
|
+
>>> from datetime import datetime, UTC
|
|
246
|
+
>>> data = np.sin(2 * np.pi * 1000 * np.linspace(0, 0.001, 1000))
|
|
247
|
+
>>> meta = TraceMetadata(sample_rate=1e6, units="V")
|
|
248
|
+
>>> trace = WaveformTrace(data=data, metadata=meta)
|
|
249
|
+
>>> print(f"Signal type: {trace.signal_type}")
|
|
250
|
+
Signal type: analog
|
|
251
|
+
>>> print(f"Is analog: {trace.is_analog}")
|
|
252
|
+
Is analog: True
|
|
253
|
+
>>> # Track processing operations
|
|
254
|
+
>>> trace.processing_history.append({
|
|
255
|
+
... "operation": "low_pass",
|
|
256
|
+
... "params": {"cutoff_hz": 5000, "order": 4},
|
|
257
|
+
... "timestamp": datetime.now(UTC).isoformat()
|
|
258
|
+
... })
|
|
259
|
+
>>> print(f"Processing steps: {len(trace.processing_history)}")
|
|
260
|
+
Processing steps: 1
|
|
206
261
|
"""
|
|
207
262
|
|
|
208
263
|
data: NDArray[np.floating[Any]]
|
|
209
264
|
metadata: TraceMetadata
|
|
265
|
+
processing_history: list[dict[str, Any]] = field(default_factory=list)
|
|
210
266
|
|
|
211
267
|
def __post_init__(self) -> None:
|
|
212
|
-
"""Validate
|
|
268
|
+
"""Validate trace data after initialization."""
|
|
213
269
|
if not isinstance(self.data, np.ndarray):
|
|
214
|
-
raise TypeError(f"data must be
|
|
215
|
-
if
|
|
216
|
-
|
|
217
|
-
|
|
270
|
+
raise TypeError(f"data must be numpy array, got {type(self.data).__name__}")
|
|
271
|
+
if self.data.ndim != 1:
|
|
272
|
+
raise ValueError(f"data must be 1-D array, got shape {self.data.shape}")
|
|
273
|
+
if len(self.data) == 0:
|
|
274
|
+
raise ValueError("data array cannot be empty")
|
|
218
275
|
|
|
219
276
|
@property
|
|
220
|
-
def
|
|
221
|
-
"""
|
|
277
|
+
def duration(self) -> float:
|
|
278
|
+
"""Duration of the trace in seconds (time from first to last sample)."""
|
|
279
|
+
if len(self.data) <= 1:
|
|
280
|
+
return 0.0
|
|
281
|
+
return (len(self.data) - 1) / self.metadata.sample_rate
|
|
222
282
|
|
|
223
|
-
|
|
224
|
-
|
|
283
|
+
@property
|
|
284
|
+
def time(self) -> NDArray[np.floating[Any]]:
|
|
285
|
+
"""Time axis array for the trace."""
|
|
286
|
+
return np.arange(len(self.data)) / self.metadata.sample_rate + self.metadata.start_time
|
|
287
|
+
|
|
288
|
+
@property
|
|
289
|
+
def is_analog(self) -> bool:
|
|
290
|
+
"""Check if this is an analog signal trace.
|
|
225
291
|
|
|
226
292
|
Returns:
|
|
227
|
-
|
|
293
|
+
True for WaveformTrace (always analog).
|
|
228
294
|
"""
|
|
229
|
-
|
|
230
|
-
return np.arange(n_samples, dtype=np.float64) * self.metadata.time_base
|
|
295
|
+
return True
|
|
231
296
|
|
|
232
297
|
@property
|
|
233
|
-
def
|
|
234
|
-
"""
|
|
298
|
+
def is_digital(self) -> bool:
|
|
299
|
+
"""Check if this is a digital signal trace.
|
|
235
300
|
|
|
236
301
|
Returns:
|
|
237
|
-
|
|
302
|
+
False for WaveformTrace (always analog).
|
|
238
303
|
"""
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
304
|
+
return False
|
|
305
|
+
|
|
306
|
+
@property
|
|
307
|
+
def is_iq(self) -> bool:
|
|
308
|
+
"""Check if this is an I/Q signal trace.
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
False for WaveformTrace.
|
|
312
|
+
"""
|
|
313
|
+
return False
|
|
314
|
+
|
|
315
|
+
@property
|
|
316
|
+
def signal_type(self) -> str:
|
|
317
|
+
"""Get the signal type identifier.
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
"analog" for WaveformTrace.
|
|
321
|
+
"""
|
|
322
|
+
return "analog"
|
|
242
323
|
|
|
243
324
|
def __len__(self) -> int:
|
|
244
325
|
"""Return number of samples in the trace."""
|
|
245
326
|
return len(self.data)
|
|
246
327
|
|
|
328
|
+
def __getitem__(self, key: int | slice) -> float | NDArray[np.floating[Any]]:
|
|
329
|
+
"""Get sample(s) by index."""
|
|
330
|
+
return self.data[key]
|
|
331
|
+
|
|
247
332
|
|
|
248
333
|
@dataclass
|
|
249
334
|
class DigitalTrace:
|
|
250
|
-
"""Digital
|
|
335
|
+
"""Digital logic trace with metadata.
|
|
251
336
|
|
|
252
|
-
|
|
253
|
-
|
|
337
|
+
Represents binary logic level data from a logic analyzer or
|
|
338
|
+
digital channel. Provides properties for signal type detection.
|
|
254
339
|
|
|
255
340
|
Attributes:
|
|
256
|
-
data:
|
|
257
|
-
metadata:
|
|
258
|
-
edges: Optional list of (timestamp, is_rising) tuples.
|
|
341
|
+
data: Boolean array representing logic levels.
|
|
342
|
+
metadata: Trace metadata (sample rate, channel, units).
|
|
259
343
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
High samples: 2
|
|
344
|
+
Properties:
|
|
345
|
+
is_analog: Always False for DigitalTrace.
|
|
346
|
+
is_digital: Always True for DigitalTrace.
|
|
347
|
+
is_iq: Always False for DigitalTrace.
|
|
348
|
+
signal_type: Returns "digital".
|
|
266
349
|
|
|
267
|
-
|
|
268
|
-
|
|
350
|
+
Example:
|
|
351
|
+
>>> data = np.array([0, 0, 1, 1, 0, 1, 0, 0], dtype=bool)
|
|
352
|
+
>>> meta = TraceMetadata(sample_rate=1e6, units="logic")
|
|
353
|
+
>>> trace = DigitalTrace(data=data, metadata=meta)
|
|
354
|
+
>>> print(f"Signal type: {trace.signal_type}")
|
|
355
|
+
Signal type: digital
|
|
356
|
+
>>> print(f"Is digital: {trace.is_digital}")
|
|
357
|
+
Is digital: True
|
|
269
358
|
"""
|
|
270
359
|
|
|
271
360
|
data: NDArray[np.bool_]
|
|
272
361
|
metadata: TraceMetadata
|
|
273
|
-
edges: list[tuple[float, bool]] | None = None
|
|
274
362
|
|
|
275
363
|
def __post_init__(self) -> None:
|
|
276
|
-
"""Validate
|
|
364
|
+
"""Validate trace data after initialization."""
|
|
277
365
|
if not isinstance(self.data, np.ndarray):
|
|
278
|
-
raise TypeError(f"data must be
|
|
279
|
-
if self.data.dtype !=
|
|
280
|
-
|
|
281
|
-
|
|
366
|
+
raise TypeError(f"data must be numpy array, got {type(self.data).__name__}")
|
|
367
|
+
if self.data.dtype != bool:
|
|
368
|
+
raise TypeError(f"data must be boolean array, got dtype {self.data.dtype}")
|
|
369
|
+
if self.data.ndim != 1:
|
|
370
|
+
raise ValueError(f"data must be 1-D array, got shape {self.data.shape}")
|
|
371
|
+
if len(self.data) == 0:
|
|
372
|
+
raise ValueError("data array cannot be empty")
|
|
282
373
|
|
|
283
374
|
@property
|
|
284
|
-
def
|
|
285
|
-
"""
|
|
375
|
+
def duration(self) -> float:
|
|
376
|
+
"""Duration of the trace in seconds (time from first to last sample)."""
|
|
377
|
+
if len(self.data) <= 1:
|
|
378
|
+
return 0.0
|
|
379
|
+
return (len(self.data) - 1) / self.metadata.sample_rate
|
|
380
|
+
|
|
381
|
+
@property
|
|
382
|
+
def time(self) -> NDArray[np.floating[Any]]:
|
|
383
|
+
"""Time axis array for the trace."""
|
|
384
|
+
return np.arange(len(self.data)) / self.metadata.sample_rate + self.metadata.start_time
|
|
385
|
+
|
|
386
|
+
@property
|
|
387
|
+
def is_analog(self) -> bool:
|
|
388
|
+
"""Check if this is an analog signal trace.
|
|
286
389
|
|
|
287
390
|
Returns:
|
|
288
|
-
|
|
391
|
+
False for DigitalTrace (always digital).
|
|
289
392
|
"""
|
|
290
|
-
|
|
291
|
-
return np.arange(n_samples, dtype=np.float64) * self.metadata.time_base
|
|
393
|
+
return False
|
|
292
394
|
|
|
293
395
|
@property
|
|
294
|
-
def
|
|
295
|
-
"""
|
|
396
|
+
def is_digital(self) -> bool:
|
|
397
|
+
"""Check if this is a digital signal trace.
|
|
296
398
|
|
|
297
399
|
Returns:
|
|
298
|
-
|
|
400
|
+
True for DigitalTrace (always digital).
|
|
299
401
|
"""
|
|
300
|
-
|
|
301
|
-
return 0.0
|
|
302
|
-
return (len(self.data) - 1) * self.metadata.time_base
|
|
402
|
+
return True
|
|
303
403
|
|
|
304
404
|
@property
|
|
305
|
-
def
|
|
306
|
-
"""
|
|
405
|
+
def is_iq(self) -> bool:
|
|
406
|
+
"""Check if this is an I/Q signal trace.
|
|
307
407
|
|
|
308
408
|
Returns:
|
|
309
|
-
|
|
409
|
+
False for DigitalTrace.
|
|
310
410
|
"""
|
|
311
|
-
|
|
312
|
-
return []
|
|
313
|
-
return [ts for ts, is_rising in self.edges if is_rising]
|
|
411
|
+
return False
|
|
314
412
|
|
|
315
413
|
@property
|
|
316
|
-
def
|
|
317
|
-
"""
|
|
414
|
+
def signal_type(self) -> str:
|
|
415
|
+
"""Get the signal type identifier.
|
|
318
416
|
|
|
319
417
|
Returns:
|
|
320
|
-
|
|
418
|
+
"digital" for DigitalTrace.
|
|
321
419
|
"""
|
|
322
|
-
|
|
323
|
-
return []
|
|
324
|
-
return [ts for ts, is_rising in self.edges if not is_rising]
|
|
420
|
+
return "digital"
|
|
325
421
|
|
|
326
422
|
def __len__(self) -> int:
|
|
327
423
|
"""Return number of samples in the trace."""
|
|
328
424
|
return len(self.data)
|
|
329
425
|
|
|
426
|
+
def __getitem__(self, key: int | slice) -> bool | NDArray[np.bool_]:
|
|
427
|
+
"""Get sample(s) by index."""
|
|
428
|
+
return self.data[key]
|
|
429
|
+
|
|
330
430
|
|
|
331
431
|
@dataclass
|
|
332
432
|
class IQTrace:
|
|
333
|
-
"""I/Q (
|
|
433
|
+
"""I/Q (complex) trace for RF/SDR applications.
|
|
334
434
|
|
|
335
|
-
|
|
336
|
-
|
|
435
|
+
Represents complex-valued I/Q data from software-defined radios
|
|
436
|
+
or RF measurement equipment. Provides properties for signal type detection.
|
|
337
437
|
|
|
338
438
|
Attributes:
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
metadata: Associated trace metadata.
|
|
439
|
+
data: Complex-valued I/Q data array.
|
|
440
|
+
metadata: Trace metadata (sample rate, channel, units).
|
|
342
441
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
>>> trace = IQTrace(i_data=i_data, q_data=q_data, metadata=TraceMetadata(sample_rate=1e6))
|
|
349
|
-
>>> print(f"Complex samples: {len(trace)}")
|
|
350
|
-
Complex samples: 1000
|
|
442
|
+
Properties:
|
|
443
|
+
is_analog: Always False for IQTrace.
|
|
444
|
+
is_digital: Always False for IQTrace.
|
|
445
|
+
is_iq: Always True for IQTrace.
|
|
446
|
+
signal_type: Returns "iq".
|
|
351
447
|
|
|
352
|
-
|
|
353
|
-
|
|
448
|
+
Example:
|
|
449
|
+
>>> data = np.exp(1j * 2 * np.pi * np.linspace(0, 1, 100))
|
|
450
|
+
>>> meta = TraceMetadata(sample_rate=1e6, units="V")
|
|
451
|
+
>>> trace = IQTrace(data=data, metadata=meta)
|
|
452
|
+
>>> print(f"Signal type: {trace.signal_type}")
|
|
453
|
+
Signal type: iq
|
|
454
|
+
>>> print(f"Is I/Q: {trace.is_iq}")
|
|
455
|
+
Is I/Q: True
|
|
354
456
|
"""
|
|
355
457
|
|
|
356
|
-
|
|
357
|
-
q_data: NDArray[np.floating[Any]]
|
|
458
|
+
data: NDArray[np.complexfloating[Any, Any]]
|
|
358
459
|
metadata: TraceMetadata
|
|
359
460
|
|
|
360
461
|
def __post_init__(self) -> None:
|
|
361
|
-
"""Validate
|
|
362
|
-
if not isinstance(self.
|
|
363
|
-
raise TypeError(f"
|
|
364
|
-
if not
|
|
365
|
-
raise TypeError(f"
|
|
366
|
-
if
|
|
367
|
-
raise ValueError(
|
|
368
|
-
|
|
369
|
-
)
|
|
370
|
-
# Convert to float64 if not already floating point
|
|
371
|
-
if not np.issubdtype(self.i_data.dtype, np.floating):
|
|
372
|
-
self.i_data = self.i_data.astype(np.float64)
|
|
373
|
-
if not np.issubdtype(self.q_data.dtype, np.floating):
|
|
374
|
-
self.q_data = self.q_data.astype(np.float64)
|
|
462
|
+
"""Validate trace data after initialization."""
|
|
463
|
+
if not isinstance(self.data, np.ndarray):
|
|
464
|
+
raise TypeError(f"data must be numpy array, got {type(self.data).__name__}")
|
|
465
|
+
if not np.iscomplexobj(self.data):
|
|
466
|
+
raise TypeError(f"data must be complex array, got dtype {self.data.dtype}")
|
|
467
|
+
if self.data.ndim != 1:
|
|
468
|
+
raise ValueError(f"data must be 1-D array, got shape {self.data.shape}")
|
|
469
|
+
if len(self.data) == 0:
|
|
470
|
+
raise ValueError("data array cannot be empty")
|
|
375
471
|
|
|
376
472
|
@property
|
|
377
|
-
def
|
|
378
|
-
"""
|
|
473
|
+
def duration(self) -> float:
|
|
474
|
+
"""Duration of the trace in seconds (time from first to last sample)."""
|
|
475
|
+
if len(self.data) <= 1:
|
|
476
|
+
return 0.0
|
|
477
|
+
return (len(self.data) - 1) / self.metadata.sample_rate
|
|
379
478
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
"""
|
|
383
|
-
return self.
|
|
479
|
+
@property
|
|
480
|
+
def time(self) -> NDArray[np.floating[Any]]:
|
|
481
|
+
"""Time axis array for the trace."""
|
|
482
|
+
return np.arange(len(self.data)) / self.metadata.sample_rate + self.metadata.start_time
|
|
384
483
|
|
|
385
484
|
@property
|
|
386
|
-
def
|
|
387
|
-
"""
|
|
485
|
+
def is_analog(self) -> bool:
|
|
486
|
+
"""Check if this is an analog signal trace.
|
|
388
487
|
|
|
389
488
|
Returns:
|
|
390
|
-
|
|
489
|
+
False for IQTrace (complex I/Q data).
|
|
391
490
|
"""
|
|
392
|
-
return
|
|
491
|
+
return False
|
|
393
492
|
|
|
394
493
|
@property
|
|
395
|
-
def
|
|
396
|
-
"""
|
|
494
|
+
def is_digital(self) -> bool:
|
|
495
|
+
"""Check if this is a digital signal trace.
|
|
397
496
|
|
|
398
497
|
Returns:
|
|
399
|
-
|
|
498
|
+
False for IQTrace (complex I/Q data).
|
|
400
499
|
"""
|
|
401
|
-
return
|
|
500
|
+
return False
|
|
402
501
|
|
|
403
502
|
@property
|
|
404
|
-
def
|
|
405
|
-
"""
|
|
503
|
+
def is_iq(self) -> bool:
|
|
504
|
+
"""Check if this is an I/Q signal trace.
|
|
406
505
|
|
|
407
506
|
Returns:
|
|
408
|
-
|
|
507
|
+
True for IQTrace (always I/Q).
|
|
409
508
|
"""
|
|
410
|
-
|
|
411
|
-
return np.arange(n_samples, dtype=np.float64) * self.metadata.time_base
|
|
509
|
+
return True
|
|
412
510
|
|
|
413
511
|
@property
|
|
414
|
-
def
|
|
415
|
-
"""
|
|
512
|
+
def signal_type(self) -> str:
|
|
513
|
+
"""Get the signal type identifier.
|
|
416
514
|
|
|
417
515
|
Returns:
|
|
418
|
-
|
|
516
|
+
"iq" for IQTrace.
|
|
419
517
|
"""
|
|
420
|
-
|
|
421
|
-
return 0.0
|
|
422
|
-
return (len(self.i_data) - 1) * self.metadata.time_base
|
|
518
|
+
return "iq"
|
|
423
519
|
|
|
424
520
|
def __len__(self) -> int:
|
|
425
521
|
"""Return number of samples in the trace."""
|
|
426
|
-
return len(self.
|
|
522
|
+
return len(self.data)
|
|
523
|
+
|
|
524
|
+
def __getitem__(self, key: int | slice) -> complex | NDArray[np.complexfloating[Any, Any]]:
|
|
525
|
+
"""Get sample(s) by index."""
|
|
526
|
+
return self.data[key]
|
|
427
527
|
|
|
428
528
|
|
|
429
529
|
@dataclass
|
|
430
530
|
class ProtocolPacket:
|
|
431
|
-
"""Decoded protocol packet
|
|
531
|
+
"""Decoded protocol packet with metadata.
|
|
432
532
|
|
|
433
|
-
Represents a decoded packet from
|
|
434
|
-
|
|
533
|
+
Represents a decoded packet/frame from protocol analysis
|
|
534
|
+
(UART, SPI, I2C, CAN, etc.).
|
|
435
535
|
|
|
436
536
|
Attributes:
|
|
437
|
-
timestamp:
|
|
438
|
-
protocol:
|
|
439
|
-
data:
|
|
440
|
-
annotations:
|
|
441
|
-
errors: List of
|
|
537
|
+
timestamp: Packet timestamp in seconds.
|
|
538
|
+
protocol: Protocol name (e.g., "UART", "SPI", "I2C").
|
|
539
|
+
data: Raw packet data as bytes.
|
|
540
|
+
annotations: Protocol-specific annotations (e.g., address, command).
|
|
541
|
+
errors: List of decoding errors (empty if no errors).
|
|
442
542
|
end_timestamp: End time of the packet in seconds (optional).
|
|
443
543
|
|
|
444
544
|
Example:
|
|
@@ -501,6 +601,7 @@ __all__ = [
|
|
|
501
601
|
"CalibrationInfo",
|
|
502
602
|
"DigitalTrace",
|
|
503
603
|
"IQTrace",
|
|
604
|
+
"MeasurementResult",
|
|
504
605
|
"ProtocolPacket",
|
|
505
606
|
"Trace",
|
|
506
607
|
"TraceMetadata",
|