oscura 0.8.0__py3-none-any.whl → 0.11.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/__main__.py +4 -0
- 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/ml/signal_classifier.py +6 -0
- oscura/analyzers/patterns/__init__.py +66 -0
- 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/basic.py +10 -7
- oscura/analyzers/validation.py +1 -1
- oscura/analyzers/waveform/measurements.py +200 -156
- oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
- oscura/analyzers/waveform/spectral.py +182 -84
- 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 +17 -102
- oscura/automotive/flexray/fibex.py +9 -1
- 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/measurement_result.py +286 -0
- oscura/core/progress.py +1 -1
- oscura/core/schemas/device_mapping.json +2 -8
- oscura/core/schemas/packet_format.json +4 -24
- oscura/core/schemas/protocol_definition.json +2 -12
- oscura/core/types.py +232 -239
- 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/validation.py +17 -10
- 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/formatting/measurements.py +55 -14
- oscura/reporting/templates/enhanced/protocol_re.html +504 -503
- oscura/sessions/legacy.py +49 -1
- 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 +46 -284
- oscura/visualization/batch.py +72 -433
- oscura/visualization/plot.py +542 -53
- oscura/visualization/styles.py +184 -318
- oscura/workflows/batch/advanced.py +1 -1
- oscura/workflows/batch/aggregate.py +12 -9
- oscura/workflows/complete_re.py +251 -23
- oscura/workflows/digital.py +27 -4
- oscura/workflows/multi_trace.py +136 -17
- oscura/workflows/waveform.py +11 -6
- oscura-0.11.0.dist-info/METADATA +460 -0
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/RECORD +120 -145
- 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.8.0.dist-info/METADATA +0 -661
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/WHEEL +0 -0
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.8.0.dist-info → oscura-0.11.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,146 +151,139 @@ 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
|
-
|
|
218
|
-
|
|
219
|
-
@property
|
|
220
|
-
def time_vector(self) -> NDArray[np.float64]:
|
|
221
|
-
"""Time axis in seconds.
|
|
222
|
-
|
|
223
|
-
Computes a time vector starting from 0, with intervals
|
|
224
|
-
determined by the sample rate.
|
|
225
|
-
|
|
226
|
-
Returns:
|
|
227
|
-
Array of time values in seconds, same length as data.
|
|
228
|
-
"""
|
|
229
|
-
n_samples = len(self.data)
|
|
230
|
-
return np.arange(n_samples, dtype=np.float64) * self.metadata.time_base
|
|
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")
|
|
231
275
|
|
|
232
276
|
@property
|
|
233
277
|
def duration(self) -> float:
|
|
234
|
-
"""
|
|
235
|
-
|
|
236
|
-
Returns:
|
|
237
|
-
Duration from first to last sample in seconds.
|
|
238
|
-
"""
|
|
239
|
-
if len(self.data) == 0:
|
|
278
|
+
"""Duration of the trace in seconds (time from first to last sample)."""
|
|
279
|
+
if len(self.data) <= 1:
|
|
240
280
|
return 0.0
|
|
241
|
-
return (len(self.data) - 1)
|
|
281
|
+
return (len(self.data) - 1) / self.metadata.sample_rate
|
|
282
|
+
|
|
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
|
|
242
287
|
|
|
243
288
|
@property
|
|
244
289
|
def is_analog(self) -> bool:
|
|
@@ -280,84 +325,63 @@ class WaveformTrace:
|
|
|
280
325
|
"""Return number of samples in the trace."""
|
|
281
326
|
return len(self.data)
|
|
282
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
|
+
|
|
283
332
|
|
|
284
333
|
@dataclass
|
|
285
334
|
class DigitalTrace:
|
|
286
|
-
"""Digital
|
|
335
|
+
"""Digital logic trace with metadata.
|
|
287
336
|
|
|
288
|
-
|
|
289
|
-
|
|
337
|
+
Represents binary logic level data from a logic analyzer or
|
|
338
|
+
digital channel. Provides properties for signal type detection.
|
|
290
339
|
|
|
291
340
|
Attributes:
|
|
292
|
-
data:
|
|
293
|
-
metadata:
|
|
294
|
-
edges: Optional list of (timestamp, is_rising) tuples.
|
|
341
|
+
data: Boolean array representing logic levels.
|
|
342
|
+
metadata: Trace metadata (sample rate, channel, units).
|
|
295
343
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
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".
|
|
302
349
|
|
|
303
|
-
|
|
304
|
-
|
|
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
|
|
305
358
|
"""
|
|
306
359
|
|
|
307
360
|
data: NDArray[np.bool_]
|
|
308
361
|
metadata: TraceMetadata
|
|
309
|
-
edges: list[tuple[float, bool]] | None = None
|
|
310
362
|
|
|
311
363
|
def __post_init__(self) -> None:
|
|
312
|
-
"""Validate
|
|
364
|
+
"""Validate trace data after initialization."""
|
|
313
365
|
if not isinstance(self.data, np.ndarray):
|
|
314
|
-
raise TypeError(f"data must be
|
|
315
|
-
if self.data.dtype !=
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
"""Time axis in seconds.
|
|
322
|
-
|
|
323
|
-
Returns:
|
|
324
|
-
Array of time values in seconds, same length as data.
|
|
325
|
-
"""
|
|
326
|
-
n_samples = len(self.data)
|
|
327
|
-
return np.arange(n_samples, dtype=np.float64) * self.metadata.time_base
|
|
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")
|
|
328
373
|
|
|
329
374
|
@property
|
|
330
375
|
def duration(self) -> float:
|
|
331
|
-
"""
|
|
332
|
-
|
|
333
|
-
Returns:
|
|
334
|
-
Duration from first to last sample in seconds.
|
|
335
|
-
"""
|
|
336
|
-
if len(self.data) == 0:
|
|
376
|
+
"""Duration of the trace in seconds (time from first to last sample)."""
|
|
377
|
+
if len(self.data) <= 1:
|
|
337
378
|
return 0.0
|
|
338
|
-
return (len(self.data) - 1)
|
|
339
|
-
|
|
340
|
-
@property
|
|
341
|
-
def rising_edges(self) -> list[float]:
|
|
342
|
-
"""Timestamps of rising edges.
|
|
343
|
-
|
|
344
|
-
Returns:
|
|
345
|
-
List of timestamps where signal transitions from low to high.
|
|
346
|
-
"""
|
|
347
|
-
if self.edges is None:
|
|
348
|
-
return []
|
|
349
|
-
return [ts for ts, is_rising in self.edges if is_rising]
|
|
379
|
+
return (len(self.data) - 1) / self.metadata.sample_rate
|
|
350
380
|
|
|
351
381
|
@property
|
|
352
|
-
def
|
|
353
|
-
"""
|
|
354
|
-
|
|
355
|
-
Returns:
|
|
356
|
-
List of timestamps where signal transitions from high to low.
|
|
357
|
-
"""
|
|
358
|
-
if self.edges is None:
|
|
359
|
-
return []
|
|
360
|
-
return [ts for ts, is_rising in self.edges if not is_rising]
|
|
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
|
|
361
385
|
|
|
362
386
|
@property
|
|
363
387
|
def is_analog(self) -> bool:
|
|
@@ -399,99 +423,63 @@ class DigitalTrace:
|
|
|
399
423
|
"""Return number of samples in the trace."""
|
|
400
424
|
return len(self.data)
|
|
401
425
|
|
|
426
|
+
def __getitem__(self, key: int | slice) -> bool | NDArray[np.bool_]:
|
|
427
|
+
"""Get sample(s) by index."""
|
|
428
|
+
return self.data[key]
|
|
429
|
+
|
|
402
430
|
|
|
403
431
|
@dataclass
|
|
404
432
|
class IQTrace:
|
|
405
|
-
"""I/Q (
|
|
433
|
+
"""I/Q (complex) trace for RF/SDR applications.
|
|
406
434
|
|
|
407
|
-
|
|
408
|
-
|
|
435
|
+
Represents complex-valued I/Q data from software-defined radios
|
|
436
|
+
or RF measurement equipment. Provides properties for signal type detection.
|
|
409
437
|
|
|
410
438
|
Attributes:
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
metadata: Associated trace metadata.
|
|
439
|
+
data: Complex-valued I/Q data array.
|
|
440
|
+
metadata: Trace metadata (sample rate, channel, units).
|
|
414
441
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
>>> trace = IQTrace(i_data=i_data, q_data=q_data, metadata=TraceMetadata(sample_rate=1e6))
|
|
421
|
-
>>> print(f"Complex samples: {len(trace)}")
|
|
422
|
-
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".
|
|
423
447
|
|
|
424
|
-
|
|
425
|
-
|
|
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
|
|
426
456
|
"""
|
|
427
457
|
|
|
428
|
-
|
|
429
|
-
q_data: NDArray[np.floating[Any]]
|
|
458
|
+
data: NDArray[np.complexfloating[Any, Any]]
|
|
430
459
|
metadata: TraceMetadata
|
|
431
460
|
|
|
432
461
|
def __post_init__(self) -> None:
|
|
433
|
-
"""Validate
|
|
434
|
-
if not isinstance(self.
|
|
435
|
-
raise TypeError(f"
|
|
436
|
-
if not
|
|
437
|
-
raise TypeError(f"
|
|
438
|
-
if
|
|
439
|
-
raise ValueError(
|
|
440
|
-
|
|
441
|
-
)
|
|
442
|
-
# Convert to float64 if not already floating point
|
|
443
|
-
if not np.issubdtype(self.i_data.dtype, np.floating):
|
|
444
|
-
self.i_data = self.i_data.astype(np.float64)
|
|
445
|
-
if not np.issubdtype(self.q_data.dtype, np.floating):
|
|
446
|
-
self.q_data = self.q_data.astype(np.float64)
|
|
447
|
-
|
|
448
|
-
@property
|
|
449
|
-
def complex_data(self) -> NDArray[np.complex128]:
|
|
450
|
-
"""Return I/Q data as complex array.
|
|
451
|
-
|
|
452
|
-
Returns:
|
|
453
|
-
Complex array where real=I, imag=Q.
|
|
454
|
-
"""
|
|
455
|
-
return self.i_data + 1j * self.q_data
|
|
456
|
-
|
|
457
|
-
@property
|
|
458
|
-
def magnitude(self) -> NDArray[np.float64]:
|
|
459
|
-
"""Magnitude (amplitude) of the complex signal.
|
|
460
|
-
|
|
461
|
-
Returns:
|
|
462
|
-
Array of magnitude values sqrt(I² + Q²).
|
|
463
|
-
"""
|
|
464
|
-
return np.sqrt(self.i_data**2 + self.q_data**2)
|
|
465
|
-
|
|
466
|
-
@property
|
|
467
|
-
def phase(self) -> NDArray[np.float64]:
|
|
468
|
-
"""Phase angle of the complex signal in radians.
|
|
469
|
-
|
|
470
|
-
Returns:
|
|
471
|
-
Array of phase values atan2(Q, I).
|
|
472
|
-
"""
|
|
473
|
-
return np.arctan2(self.q_data, self.i_data)
|
|
474
|
-
|
|
475
|
-
@property
|
|
476
|
-
def time_vector(self) -> NDArray[np.float64]:
|
|
477
|
-
"""Time axis in seconds.
|
|
478
|
-
|
|
479
|
-
Returns:
|
|
480
|
-
Array of time values in seconds, same length as data.
|
|
481
|
-
"""
|
|
482
|
-
n_samples = len(self.i_data)
|
|
483
|
-
return np.arange(n_samples, dtype=np.float64) * self.metadata.time_base
|
|
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")
|
|
484
471
|
|
|
485
472
|
@property
|
|
486
473
|
def duration(self) -> float:
|
|
487
|
-
"""
|
|
488
|
-
|
|
489
|
-
Returns:
|
|
490
|
-
Duration from first to last sample in seconds.
|
|
491
|
-
"""
|
|
492
|
-
if len(self.i_data) == 0:
|
|
474
|
+
"""Duration of the trace in seconds (time from first to last sample)."""
|
|
475
|
+
if len(self.data) <= 1:
|
|
493
476
|
return 0.0
|
|
494
|
-
return (len(self.
|
|
477
|
+
return (len(self.data) - 1) / self.metadata.sample_rate
|
|
478
|
+
|
|
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
|
|
495
483
|
|
|
496
484
|
@property
|
|
497
485
|
def is_analog(self) -> bool:
|
|
@@ -531,22 +519,26 @@ class IQTrace:
|
|
|
531
519
|
|
|
532
520
|
def __len__(self) -> int:
|
|
533
521
|
"""Return number of samples in the trace."""
|
|
534
|
-
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]
|
|
535
527
|
|
|
536
528
|
|
|
537
529
|
@dataclass
|
|
538
530
|
class ProtocolPacket:
|
|
539
|
-
"""Decoded protocol packet
|
|
531
|
+
"""Decoded protocol packet with metadata.
|
|
540
532
|
|
|
541
|
-
Represents a decoded packet from
|
|
542
|
-
|
|
533
|
+
Represents a decoded packet/frame from protocol analysis
|
|
534
|
+
(UART, SPI, I2C, CAN, etc.).
|
|
543
535
|
|
|
544
536
|
Attributes:
|
|
545
|
-
timestamp:
|
|
546
|
-
protocol:
|
|
547
|
-
data:
|
|
548
|
-
annotations:
|
|
549
|
-
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).
|
|
550
542
|
end_timestamp: End time of the packet in seconds (optional).
|
|
551
543
|
|
|
552
544
|
Example:
|
|
@@ -609,6 +601,7 @@ __all__ = [
|
|
|
609
601
|
"CalibrationInfo",
|
|
610
602
|
"DigitalTrace",
|
|
611
603
|
"IQTrace",
|
|
604
|
+
"MeasurementResult",
|
|
612
605
|
"ProtocolPacket",
|
|
613
606
|
"Trace",
|
|
614
607
|
"TraceMetadata",
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Legacy export functions for backward compatibility.
|
|
2
|
+
|
|
3
|
+
This module provides backward-compatible export functions that wrap
|
|
4
|
+
the modern pipeline handler implementations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from oscura.export.legacy.wav import export_wav
|
|
10
|
+
|
|
11
|
+
__all__ = ["export_wav"]
|