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
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
"""Eye diagram generation from serial data.
|
|
2
|
+
|
|
3
|
+
This module generates eye diagrams by folding waveform data
|
|
4
|
+
at the unit interval boundary.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> from oscura.analyzers.eye.diagram import generate_eye
|
|
8
|
+
>>> eye = generate_eye(trace, unit_interval=1e-9)
|
|
9
|
+
>>> print(f"Eye diagram: {eye.n_traces} traces, {eye.samples_per_ui} samples/UI")
|
|
10
|
+
|
|
11
|
+
References:
|
|
12
|
+
IEEE 802.3: Ethernet Physical Layer Specifications
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
|
|
22
|
+
from oscura.core.exceptions import AnalysisError, InsufficientDataError
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from numpy.typing import NDArray
|
|
26
|
+
|
|
27
|
+
from oscura.core.types import WaveformTrace
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class EyeDiagram:
|
|
32
|
+
"""Eye diagram data structure.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
data: 2D array of eye traces (n_traces x samples_per_ui).
|
|
36
|
+
time_axis: Time axis in UI (0.0 to 2.0 for 2-UI eye).
|
|
37
|
+
unit_interval: Unit interval in seconds.
|
|
38
|
+
samples_per_ui: Number of samples per unit interval.
|
|
39
|
+
n_traces: Number of overlaid traces.
|
|
40
|
+
sample_rate: Original sample rate in Hz.
|
|
41
|
+
histogram: Optional 2D histogram (voltage x time bins).
|
|
42
|
+
voltage_bins: Bin edges for voltage axis.
|
|
43
|
+
time_bins: Bin edges for time axis.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
data: NDArray[np.float64]
|
|
47
|
+
time_axis: NDArray[np.float64]
|
|
48
|
+
unit_interval: float
|
|
49
|
+
samples_per_ui: int
|
|
50
|
+
n_traces: int
|
|
51
|
+
sample_rate: float
|
|
52
|
+
histogram: NDArray[np.float64] | None = None
|
|
53
|
+
voltage_bins: NDArray[np.float64] | None = None
|
|
54
|
+
time_bins: NDArray[np.float64] | None = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def generate_eye(
|
|
58
|
+
trace: WaveformTrace,
|
|
59
|
+
unit_interval: float,
|
|
60
|
+
*,
|
|
61
|
+
n_ui: int = 2,
|
|
62
|
+
trigger_level: float = 0.5,
|
|
63
|
+
trigger_edge: str = "rising",
|
|
64
|
+
max_traces: int | None = None,
|
|
65
|
+
generate_histogram: bool = True,
|
|
66
|
+
histogram_bins: tuple[int, int] = (100, 100),
|
|
67
|
+
) -> EyeDiagram:
|
|
68
|
+
"""Generate eye diagram from waveform data.
|
|
69
|
+
|
|
70
|
+
Folds the waveform at unit interval boundaries to create
|
|
71
|
+
an overlaid eye pattern for signal quality analysis.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
trace: Input waveform trace.
|
|
75
|
+
unit_interval: Unit interval (bit period) in seconds.
|
|
76
|
+
n_ui: Number of unit intervals to display (1 or 2).
|
|
77
|
+
trigger_level: Trigger level as fraction of amplitude.
|
|
78
|
+
trigger_edge: Trigger on "rising" or "falling" edges.
|
|
79
|
+
max_traces: Maximum number of traces to include.
|
|
80
|
+
generate_histogram: Generate 2D histogram for persistence.
|
|
81
|
+
histogram_bins: (voltage_bins, time_bins) for histogram.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
EyeDiagram with overlaid traces and optional histogram.
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
AnalysisError: If unit interval is too short.
|
|
88
|
+
InsufficientDataError: If not enough data for eye generation.
|
|
89
|
+
|
|
90
|
+
Example:
|
|
91
|
+
>>> eye = generate_eye(trace, unit_interval=1e-9)
|
|
92
|
+
>>> print(f"Generated {eye.n_traces} traces")
|
|
93
|
+
|
|
94
|
+
References:
|
|
95
|
+
OIF CEI: Common Electrical I/O Eye Diagram Methodology
|
|
96
|
+
"""
|
|
97
|
+
data = trace.data
|
|
98
|
+
sample_rate = trace.metadata.sample_rate
|
|
99
|
+
|
|
100
|
+
samples_per_ui = _validate_unit_interval(unit_interval, sample_rate)
|
|
101
|
+
total_ui_samples = samples_per_ui * n_ui
|
|
102
|
+
_validate_data_length(len(data), total_ui_samples)
|
|
103
|
+
|
|
104
|
+
trigger_indices = _find_trigger_points(data, trigger_level, trigger_edge)
|
|
105
|
+
eye_traces = _extract_eye_traces(
|
|
106
|
+
data, trigger_indices, samples_per_ui, total_ui_samples, max_traces
|
|
107
|
+
)
|
|
108
|
+
eye_data = np.array(eye_traces, dtype=np.float64)
|
|
109
|
+
time_axis = np.linspace(0, n_ui, total_ui_samples, endpoint=False)
|
|
110
|
+
|
|
111
|
+
histogram, voltage_bins, time_bins = _generate_histogram_if_requested(
|
|
112
|
+
eye_data, time_axis, n_ui, generate_histogram, histogram_bins
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return EyeDiagram(
|
|
116
|
+
data=eye_data,
|
|
117
|
+
time_axis=time_axis,
|
|
118
|
+
unit_interval=unit_interval,
|
|
119
|
+
samples_per_ui=samples_per_ui,
|
|
120
|
+
n_traces=len(eye_traces),
|
|
121
|
+
sample_rate=sample_rate,
|
|
122
|
+
histogram=histogram,
|
|
123
|
+
voltage_bins=voltage_bins,
|
|
124
|
+
time_bins=time_bins,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _validate_unit_interval(unit_interval: float, sample_rate: float) -> int:
|
|
129
|
+
"""Validate unit interval and calculate samples per UI."""
|
|
130
|
+
samples_per_ui = round(unit_interval * sample_rate)
|
|
131
|
+
if samples_per_ui < 4:
|
|
132
|
+
raise AnalysisError(
|
|
133
|
+
f"Unit interval too short: {samples_per_ui} samples/UI. Need at least 4 samples per UI."
|
|
134
|
+
)
|
|
135
|
+
return samples_per_ui
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _validate_data_length(n_samples: int, total_ui_samples: int) -> None:
|
|
139
|
+
"""Validate that we have enough data for eye generation."""
|
|
140
|
+
if n_samples < total_ui_samples * 2:
|
|
141
|
+
raise InsufficientDataError(
|
|
142
|
+
f"Need at least {total_ui_samples * 2} samples for eye diagram",
|
|
143
|
+
required=total_ui_samples * 2,
|
|
144
|
+
available=n_samples,
|
|
145
|
+
analysis_type="eye_diagram_generation",
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _find_trigger_points(
|
|
150
|
+
data: NDArray[np.float64],
|
|
151
|
+
trigger_level: float,
|
|
152
|
+
trigger_edge: str,
|
|
153
|
+
) -> NDArray[np.intp]:
|
|
154
|
+
"""Find trigger points in the data."""
|
|
155
|
+
low = np.percentile(data, 10)
|
|
156
|
+
high = np.percentile(data, 90)
|
|
157
|
+
threshold = low + trigger_level * (high - low)
|
|
158
|
+
|
|
159
|
+
if trigger_edge == "rising":
|
|
160
|
+
trigger_mask = (data[:-1] < threshold) & (data[1:] >= threshold)
|
|
161
|
+
else:
|
|
162
|
+
trigger_mask = (data[:-1] >= threshold) & (data[1:] < threshold)
|
|
163
|
+
|
|
164
|
+
trigger_indices = np.where(trigger_mask)[0]
|
|
165
|
+
|
|
166
|
+
if len(trigger_indices) < 2:
|
|
167
|
+
raise InsufficientDataError(
|
|
168
|
+
"Not enough trigger events for eye diagram",
|
|
169
|
+
required=2,
|
|
170
|
+
available=len(trigger_indices),
|
|
171
|
+
analysis_type="eye_diagram_generation",
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
return trigger_indices
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _extract_eye_traces(
|
|
178
|
+
data: NDArray[np.float64],
|
|
179
|
+
trigger_indices: NDArray[np.intp],
|
|
180
|
+
samples_per_ui: int,
|
|
181
|
+
total_ui_samples: int,
|
|
182
|
+
max_traces: int | None,
|
|
183
|
+
) -> list[NDArray[np.float64]]:
|
|
184
|
+
"""Extract eye traces from data using trigger points."""
|
|
185
|
+
eye_traces = []
|
|
186
|
+
half_ui = samples_per_ui // 2
|
|
187
|
+
n_samples = len(data)
|
|
188
|
+
|
|
189
|
+
for trig_idx in trigger_indices:
|
|
190
|
+
start_idx = trig_idx - half_ui
|
|
191
|
+
end_idx = start_idx + total_ui_samples
|
|
192
|
+
|
|
193
|
+
if start_idx >= 0 and end_idx <= n_samples:
|
|
194
|
+
eye_traces.append(data[start_idx:end_idx])
|
|
195
|
+
|
|
196
|
+
if max_traces is not None and len(eye_traces) >= max_traces:
|
|
197
|
+
break
|
|
198
|
+
|
|
199
|
+
if len(eye_traces) == 0:
|
|
200
|
+
raise InsufficientDataError(
|
|
201
|
+
"Could not extract any complete eye traces",
|
|
202
|
+
required=1,
|
|
203
|
+
available=0,
|
|
204
|
+
analysis_type="eye_diagram_generation",
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
return eye_traces
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _generate_histogram_if_requested(
|
|
211
|
+
eye_data: NDArray[np.float64],
|
|
212
|
+
time_axis: NDArray[np.float64],
|
|
213
|
+
n_ui: int,
|
|
214
|
+
generate_histogram: bool,
|
|
215
|
+
histogram_bins: tuple[int, int],
|
|
216
|
+
) -> tuple[NDArray[np.float64] | None, NDArray[np.float64] | None, NDArray[np.float64] | None]:
|
|
217
|
+
"""Generate 2D histogram if requested."""
|
|
218
|
+
if not generate_histogram:
|
|
219
|
+
return None, None, None
|
|
220
|
+
|
|
221
|
+
all_voltages = eye_data.flatten()
|
|
222
|
+
all_times = np.tile(time_axis, len(eye_data))
|
|
223
|
+
|
|
224
|
+
voltage_range = (np.min(all_voltages), np.max(all_voltages))
|
|
225
|
+
time_range = (0, n_ui)
|
|
226
|
+
|
|
227
|
+
histogram, voltage_edges, time_edges = np.histogram2d(
|
|
228
|
+
all_voltages,
|
|
229
|
+
all_times,
|
|
230
|
+
bins=histogram_bins,
|
|
231
|
+
range=[voltage_range, time_range],
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
return histogram, voltage_edges, time_edges
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def generate_eye_from_edges(
|
|
238
|
+
trace: WaveformTrace,
|
|
239
|
+
edge_timestamps: NDArray[np.float64],
|
|
240
|
+
*,
|
|
241
|
+
n_ui: int = 2,
|
|
242
|
+
samples_per_ui: int = 100,
|
|
243
|
+
max_traces: int | None = None,
|
|
244
|
+
) -> EyeDiagram:
|
|
245
|
+
"""Generate eye diagram using recovered clock edges.
|
|
246
|
+
|
|
247
|
+
Uses pre-recovered clock edges for triggering, which can provide
|
|
248
|
+
more accurate alignment than threshold-based triggering.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
trace: Input waveform trace.
|
|
252
|
+
edge_timestamps: Array of clock edge timestamps in seconds.
|
|
253
|
+
n_ui: Number of unit intervals to display.
|
|
254
|
+
samples_per_ui: Samples per UI in resampled eye.
|
|
255
|
+
max_traces: Maximum traces to include.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
EyeDiagram with overlaid traces.
|
|
259
|
+
|
|
260
|
+
Raises:
|
|
261
|
+
InsufficientDataError: If not enough edge timestamps or traces.
|
|
262
|
+
|
|
263
|
+
Example:
|
|
264
|
+
>>> edges = recover_clock_edges(trace)
|
|
265
|
+
>>> eye = generate_eye_from_edges(trace, edges)
|
|
266
|
+
"""
|
|
267
|
+
data = trace.data
|
|
268
|
+
sample_rate = trace.metadata.sample_rate
|
|
269
|
+
|
|
270
|
+
if len(edge_timestamps) < 3:
|
|
271
|
+
raise InsufficientDataError(
|
|
272
|
+
"Need at least 3 edge timestamps",
|
|
273
|
+
required=3,
|
|
274
|
+
available=len(edge_timestamps),
|
|
275
|
+
analysis_type="eye_diagram_generation",
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# Calculate unit interval from edges
|
|
279
|
+
periods = np.diff(edge_timestamps)
|
|
280
|
+
unit_interval = float(np.median(periods))
|
|
281
|
+
|
|
282
|
+
# Create time vector for original data
|
|
283
|
+
original_time = np.arange(len(data)) / sample_rate
|
|
284
|
+
|
|
285
|
+
# Extract and resample traces around each edge
|
|
286
|
+
eye_traces = []
|
|
287
|
+
total_samples = samples_per_ui * n_ui
|
|
288
|
+
half_ui = unit_interval / 2
|
|
289
|
+
|
|
290
|
+
for edge_time in edge_timestamps:
|
|
291
|
+
# Define window around edge
|
|
292
|
+
start_time = edge_time - half_ui
|
|
293
|
+
end_time = start_time + unit_interval * n_ui
|
|
294
|
+
|
|
295
|
+
if start_time < 0 or end_time > original_time[-1]:
|
|
296
|
+
continue
|
|
297
|
+
|
|
298
|
+
# Find samples within window
|
|
299
|
+
mask = (original_time >= start_time) & (original_time <= end_time)
|
|
300
|
+
window_time = original_time[mask] - start_time
|
|
301
|
+
window_data = data[mask]
|
|
302
|
+
|
|
303
|
+
if len(window_data) < 4:
|
|
304
|
+
continue
|
|
305
|
+
|
|
306
|
+
# Resample to consistent samples_per_ui
|
|
307
|
+
resample_time = np.linspace(0, unit_interval * n_ui, total_samples)
|
|
308
|
+
resampled = np.interp(resample_time, window_time, window_data)
|
|
309
|
+
|
|
310
|
+
eye_traces.append(resampled)
|
|
311
|
+
|
|
312
|
+
if max_traces is not None and len(eye_traces) >= max_traces:
|
|
313
|
+
break
|
|
314
|
+
|
|
315
|
+
if len(eye_traces) == 0:
|
|
316
|
+
raise InsufficientDataError(
|
|
317
|
+
"Could not extract any eye traces",
|
|
318
|
+
required=1,
|
|
319
|
+
available=0,
|
|
320
|
+
analysis_type="eye_diagram_generation",
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
eye_data = np.array(eye_traces, dtype=np.float64)
|
|
324
|
+
time_axis = np.linspace(0, n_ui, total_samples, endpoint=False)
|
|
325
|
+
|
|
326
|
+
return EyeDiagram(
|
|
327
|
+
data=eye_data,
|
|
328
|
+
time_axis=time_axis,
|
|
329
|
+
unit_interval=unit_interval,
|
|
330
|
+
samples_per_ui=samples_per_ui,
|
|
331
|
+
n_traces=len(eye_traces),
|
|
332
|
+
sample_rate=sample_rate,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _calculate_trigger_threshold(data: NDArray[np.float64], trigger_fraction: float) -> float:
|
|
337
|
+
"""Calculate trigger threshold from data amplitude.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
data: Eye diagram data.
|
|
341
|
+
trigger_fraction: Trigger level as fraction of amplitude.
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
Threshold value.
|
|
345
|
+
"""
|
|
346
|
+
low = np.percentile(data, 10)
|
|
347
|
+
high = np.percentile(data, 90)
|
|
348
|
+
amplitude_range = high - low
|
|
349
|
+
threshold: float = float(low + trigger_fraction * amplitude_range)
|
|
350
|
+
return threshold
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _find_trace_crossings(data: NDArray[np.float64], threshold: float) -> list[int]:
|
|
354
|
+
"""Find crossing indices for all traces.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
data: Eye diagram trace data (n_traces x samples_per_trace).
|
|
358
|
+
threshold: Crossing threshold.
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
List of crossing indices for traces with crossings.
|
|
362
|
+
"""
|
|
363
|
+
n_traces, _samples_per_trace = data.shape
|
|
364
|
+
crossing_indices = []
|
|
365
|
+
|
|
366
|
+
for trace_idx in range(n_traces):
|
|
367
|
+
trace = data[trace_idx, :]
|
|
368
|
+
crossings = np.where((trace[:-1] < threshold) & (trace[1:] >= threshold))[0]
|
|
369
|
+
|
|
370
|
+
if len(crossings) > 0:
|
|
371
|
+
crossing_indices.append(crossings[0])
|
|
372
|
+
|
|
373
|
+
return crossing_indices
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _align_traces_to_target(
|
|
377
|
+
data: NDArray[np.float64], threshold: float, target_crossing: int
|
|
378
|
+
) -> NDArray[np.float64]:
|
|
379
|
+
"""Align all traces to target crossing position.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
data: Eye diagram trace data.
|
|
383
|
+
threshold: Crossing threshold.
|
|
384
|
+
target_crossing: Target crossing position.
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
Aligned trace data.
|
|
388
|
+
"""
|
|
389
|
+
n_traces, _samples_per_trace = data.shape
|
|
390
|
+
aligned_data = np.zeros_like(data)
|
|
391
|
+
|
|
392
|
+
for trace_idx in range(n_traces):
|
|
393
|
+
trace = data[trace_idx, :]
|
|
394
|
+
crossings = np.where((trace[:-1] < threshold) & (trace[1:] >= threshold))[0]
|
|
395
|
+
|
|
396
|
+
if len(crossings) > 0:
|
|
397
|
+
crossing = crossings[0]
|
|
398
|
+
shift = target_crossing - crossing
|
|
399
|
+
|
|
400
|
+
if shift != 0:
|
|
401
|
+
aligned_data[trace_idx, :] = np.roll(trace, shift)
|
|
402
|
+
else:
|
|
403
|
+
aligned_data[trace_idx, :] = trace
|
|
404
|
+
else:
|
|
405
|
+
aligned_data[trace_idx, :] = trace
|
|
406
|
+
|
|
407
|
+
return aligned_data
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _apply_symmetric_centering(data: NDArray[np.float64]) -> NDArray[np.float64]:
|
|
411
|
+
"""Apply symmetric amplitude centering if enabled.
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
data: Aligned trace data.
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
Symmetrically centered data.
|
|
418
|
+
"""
|
|
419
|
+
max_abs = np.max(np.abs(data))
|
|
420
|
+
if max_abs > 0:
|
|
421
|
+
data = data - np.mean(data)
|
|
422
|
+
return data
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def auto_center_eye_diagram(
|
|
426
|
+
eye: EyeDiagram,
|
|
427
|
+
*,
|
|
428
|
+
trigger_fraction: float = 0.5,
|
|
429
|
+
symmetric_range: bool = True,
|
|
430
|
+
) -> EyeDiagram:
|
|
431
|
+
"""Auto-center eye diagram on optimal crossing point.
|
|
432
|
+
|
|
433
|
+
Automatically centers eye diagrams on the optimal trigger point
|
|
434
|
+
and scales amplitude for maximum eye opening visibility with
|
|
435
|
+
symmetric vertical centering.
|
|
436
|
+
|
|
437
|
+
Args:
|
|
438
|
+
eye: Input EyeDiagram to center.
|
|
439
|
+
trigger_fraction: Trigger level as fraction of amplitude (default 0.5 = 50%).
|
|
440
|
+
symmetric_range: Use symmetric amplitude range ±max(abs(signal)).
|
|
441
|
+
|
|
442
|
+
Returns:
|
|
443
|
+
Centered EyeDiagram with adjusted data.
|
|
444
|
+
|
|
445
|
+
Raises:
|
|
446
|
+
ValueError: If trigger_fraction is not in [0, 1].
|
|
447
|
+
|
|
448
|
+
Example:
|
|
449
|
+
>>> eye = generate_eye(trace, unit_interval=1e-9)
|
|
450
|
+
>>> centered = auto_center_eye_diagram(eye)
|
|
451
|
+
>>> # Centered at 50% crossing with symmetric amplitude
|
|
452
|
+
|
|
453
|
+
References:
|
|
454
|
+
VIS-021: Eye Diagram Auto-Centering
|
|
455
|
+
"""
|
|
456
|
+
if not 0 <= trigger_fraction <= 1:
|
|
457
|
+
raise ValueError(f"trigger_fraction must be in [0, 1], got {trigger_fraction}")
|
|
458
|
+
|
|
459
|
+
# Setup: calculate threshold and find crossings
|
|
460
|
+
data = eye.data
|
|
461
|
+
threshold = _calculate_trigger_threshold(data, trigger_fraction)
|
|
462
|
+
crossing_indices = _find_trace_crossings(data, threshold)
|
|
463
|
+
|
|
464
|
+
if len(crossing_indices) == 0:
|
|
465
|
+
import warnings
|
|
466
|
+
|
|
467
|
+
warnings.warn(
|
|
468
|
+
"No crossing points found, cannot auto-center eye diagram",
|
|
469
|
+
UserWarning,
|
|
470
|
+
stacklevel=2,
|
|
471
|
+
)
|
|
472
|
+
return eye
|
|
473
|
+
|
|
474
|
+
# Processing: align traces to target crossing point
|
|
475
|
+
_n_traces, samples_per_trace = data.shape
|
|
476
|
+
target_crossing = samples_per_trace // 2
|
|
477
|
+
aligned_data = _align_traces_to_target(data, threshold, target_crossing)
|
|
478
|
+
|
|
479
|
+
# Result building: apply symmetric centering and create result
|
|
480
|
+
if symmetric_range:
|
|
481
|
+
aligned_data = _apply_symmetric_centering(aligned_data)
|
|
482
|
+
|
|
483
|
+
return EyeDiagram(
|
|
484
|
+
data=aligned_data,
|
|
485
|
+
time_axis=eye.time_axis,
|
|
486
|
+
unit_interval=eye.unit_interval,
|
|
487
|
+
samples_per_ui=eye.samples_per_ui,
|
|
488
|
+
n_traces=eye.n_traces,
|
|
489
|
+
sample_rate=eye.sample_rate,
|
|
490
|
+
histogram=None,
|
|
491
|
+
voltage_bins=None,
|
|
492
|
+
time_bins=None,
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
__all__ = [
|
|
497
|
+
"EyeDiagram",
|
|
498
|
+
"auto_center_eye_diagram",
|
|
499
|
+
"generate_eye",
|
|
500
|
+
"generate_eye_from_edges",
|
|
501
|
+
]
|
|
@@ -40,7 +40,12 @@ from oscura.analyzers.jitter.decomposition import (
|
|
|
40
40
|
extract_pj,
|
|
41
41
|
extract_rj,
|
|
42
42
|
)
|
|
43
|
-
from oscura.analyzers.jitter.
|
|
43
|
+
from oscura.analyzers.jitter.spectrum import (
|
|
44
|
+
JitterSpectrumResult,
|
|
45
|
+
identify_periodic_components,
|
|
46
|
+
jitter_spectrum,
|
|
47
|
+
)
|
|
48
|
+
from oscura.analyzers.jitter.timing import (
|
|
44
49
|
CycleJitterResult,
|
|
45
50
|
DutyCycleDistortionResult,
|
|
46
51
|
cycle_to_cycle_jitter,
|
|
@@ -48,11 +53,6 @@ from oscura.analyzers.jitter.measurements import (
|
|
|
48
53
|
period_jitter,
|
|
49
54
|
tie_from_edges,
|
|
50
55
|
)
|
|
51
|
-
from oscura.analyzers.jitter.spectrum import (
|
|
52
|
-
JitterSpectrumResult,
|
|
53
|
-
identify_periodic_components,
|
|
54
|
-
jitter_spectrum,
|
|
55
|
-
)
|
|
56
56
|
|
|
57
57
|
__all__ = [
|
|
58
58
|
"BathtubCurveResult",
|