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.
Files changed (175) hide show
  1. oscura/__init__.py +19 -19
  2. oscura/analyzers/__init__.py +2 -0
  3. oscura/analyzers/digital/extraction.py +2 -3
  4. oscura/analyzers/digital/quality.py +1 -1
  5. oscura/analyzers/digital/timing.py +1 -1
  6. oscura/analyzers/eye/__init__.py +5 -1
  7. oscura/analyzers/eye/generation.py +501 -0
  8. oscura/analyzers/jitter/__init__.py +6 -6
  9. oscura/analyzers/jitter/timing.py +419 -0
  10. oscura/analyzers/patterns/__init__.py +94 -0
  11. oscura/analyzers/patterns/reverse_engineering.py +991 -0
  12. oscura/analyzers/power/__init__.py +35 -12
  13. oscura/analyzers/power/basic.py +3 -3
  14. oscura/analyzers/power/soa.py +1 -1
  15. oscura/analyzers/power/switching.py +3 -3
  16. oscura/analyzers/signal_classification.py +529 -0
  17. oscura/analyzers/signal_integrity/sparams.py +3 -3
  18. oscura/analyzers/statistics/__init__.py +4 -0
  19. oscura/analyzers/statistics/basic.py +152 -0
  20. oscura/analyzers/statistics/correlation.py +47 -6
  21. oscura/analyzers/validation.py +1 -1
  22. oscura/analyzers/waveform/__init__.py +2 -0
  23. oscura/analyzers/waveform/measurements.py +329 -163
  24. oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
  25. oscura/analyzers/waveform/spectral.py +498 -54
  26. oscura/api/dsl/commands.py +15 -6
  27. oscura/api/server/templates/base.html +137 -146
  28. oscura/api/server/templates/export.html +84 -110
  29. oscura/api/server/templates/home.html +248 -267
  30. oscura/api/server/templates/protocols.html +44 -48
  31. oscura/api/server/templates/reports.html +27 -35
  32. oscura/api/server/templates/session_detail.html +68 -78
  33. oscura/api/server/templates/sessions.html +62 -72
  34. oscura/api/server/templates/waveforms.html +54 -64
  35. oscura/automotive/__init__.py +1 -1
  36. oscura/automotive/can/session.py +1 -1
  37. oscura/automotive/dbc/generator.py +638 -23
  38. oscura/automotive/dtc/data.json +102 -17
  39. oscura/automotive/uds/decoder.py +99 -6
  40. oscura/cli/analyze.py +8 -2
  41. oscura/cli/batch.py +36 -5
  42. oscura/cli/characterize.py +18 -4
  43. oscura/cli/export.py +47 -5
  44. oscura/cli/main.py +2 -0
  45. oscura/cli/onboarding/wizard.py +10 -6
  46. oscura/cli/pipeline.py +585 -0
  47. oscura/cli/visualize.py +6 -4
  48. oscura/convenience.py +400 -32
  49. oscura/core/config/loader.py +0 -1
  50. oscura/core/measurement_result.py +286 -0
  51. oscura/core/progress.py +1 -1
  52. oscura/core/schemas/device_mapping.json +8 -2
  53. oscura/core/schemas/packet_format.json +24 -4
  54. oscura/core/schemas/protocol_definition.json +12 -2
  55. oscura/core/types.py +300 -199
  56. oscura/correlation/multi_protocol.py +1 -1
  57. oscura/export/legacy/__init__.py +11 -0
  58. oscura/export/legacy/wav.py +75 -0
  59. oscura/exporters/__init__.py +19 -0
  60. oscura/exporters/wireshark.py +809 -0
  61. oscura/hardware/acquisition/file.py +5 -19
  62. oscura/hardware/acquisition/saleae.py +10 -10
  63. oscura/hardware/acquisition/socketcan.py +4 -6
  64. oscura/hardware/acquisition/synthetic.py +1 -5
  65. oscura/hardware/acquisition/visa.py +6 -6
  66. oscura/hardware/security/side_channel_detector.py +5 -508
  67. oscura/inference/message_format.py +686 -1
  68. oscura/jupyter/display.py +2 -2
  69. oscura/jupyter/magic.py +3 -3
  70. oscura/loaders/__init__.py +17 -12
  71. oscura/loaders/binary.py +1 -1
  72. oscura/loaders/chipwhisperer.py +1 -2
  73. oscura/loaders/configurable.py +1 -1
  74. oscura/loaders/csv_loader.py +2 -2
  75. oscura/loaders/hdf5_loader.py +1 -1
  76. oscura/loaders/lazy.py +6 -1
  77. oscura/loaders/mmap_loader.py +0 -1
  78. oscura/loaders/numpy_loader.py +8 -7
  79. oscura/loaders/preprocessing.py +3 -5
  80. oscura/loaders/rigol.py +21 -7
  81. oscura/loaders/sigrok.py +2 -5
  82. oscura/loaders/tdms.py +3 -2
  83. oscura/loaders/tektronix.py +38 -32
  84. oscura/loaders/tss.py +20 -27
  85. oscura/loaders/vcd.py +13 -8
  86. oscura/loaders/wav.py +1 -6
  87. oscura/pipeline/__init__.py +76 -0
  88. oscura/pipeline/handlers/__init__.py +165 -0
  89. oscura/pipeline/handlers/analyzers.py +1045 -0
  90. oscura/pipeline/handlers/decoders.py +899 -0
  91. oscura/pipeline/handlers/exporters.py +1103 -0
  92. oscura/pipeline/handlers/filters.py +891 -0
  93. oscura/pipeline/handlers/loaders.py +640 -0
  94. oscura/pipeline/handlers/transforms.py +768 -0
  95. oscura/reporting/__init__.py +88 -1
  96. oscura/reporting/automation.py +348 -0
  97. oscura/reporting/citations.py +374 -0
  98. oscura/reporting/core.py +54 -0
  99. oscura/reporting/formatting/__init__.py +11 -0
  100. oscura/reporting/formatting/measurements.py +320 -0
  101. oscura/reporting/html.py +57 -0
  102. oscura/reporting/interpretation.py +431 -0
  103. oscura/reporting/summary.py +329 -0
  104. oscura/reporting/templates/enhanced/protocol_re.html +504 -503
  105. oscura/reporting/visualization.py +542 -0
  106. oscura/side_channel/__init__.py +38 -57
  107. oscura/utils/builders/signal_builder.py +5 -5
  108. oscura/utils/comparison/compare.py +7 -9
  109. oscura/utils/comparison/golden.py +1 -1
  110. oscura/utils/filtering/convenience.py +2 -2
  111. oscura/utils/math/arithmetic.py +38 -62
  112. oscura/utils/math/interpolation.py +20 -20
  113. oscura/utils/pipeline/__init__.py +4 -17
  114. oscura/utils/progressive.py +1 -4
  115. oscura/utils/triggering/edge.py +1 -1
  116. oscura/utils/triggering/pattern.py +2 -2
  117. oscura/utils/triggering/pulse.py +2 -2
  118. oscura/utils/triggering/window.py +3 -3
  119. oscura/validation/hil_testing.py +11 -11
  120. oscura/visualization/__init__.py +47 -284
  121. oscura/visualization/batch.py +160 -0
  122. oscura/visualization/plot.py +542 -53
  123. oscura/visualization/styles.py +184 -318
  124. oscura/workflows/__init__.py +2 -0
  125. oscura/workflows/batch/advanced.py +1 -1
  126. oscura/workflows/batch/aggregate.py +7 -8
  127. oscura/workflows/complete_re.py +251 -23
  128. oscura/workflows/digital.py +27 -4
  129. oscura/workflows/multi_trace.py +136 -17
  130. oscura/workflows/waveform.py +788 -0
  131. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/METADATA +59 -79
  132. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/RECORD +135 -149
  133. oscura/side_channel/dpa.py +0 -1025
  134. oscura/utils/optimization/__init__.py +0 -19
  135. oscura/utils/optimization/parallel.py +0 -443
  136. oscura/utils/optimization/search.py +0 -532
  137. oscura/utils/pipeline/base.py +0 -338
  138. oscura/utils/pipeline/composition.py +0 -248
  139. oscura/utils/pipeline/parallel.py +0 -449
  140. oscura/utils/pipeline/pipeline.py +0 -375
  141. oscura/utils/search/__init__.py +0 -16
  142. oscura/utils/search/anomaly.py +0 -424
  143. oscura/utils/search/context.py +0 -294
  144. oscura/utils/search/pattern.py +0 -288
  145. oscura/utils/storage/__init__.py +0 -61
  146. oscura/utils/storage/database.py +0 -1166
  147. oscura/visualization/accessibility.py +0 -526
  148. oscura/visualization/annotations.py +0 -371
  149. oscura/visualization/axis_scaling.py +0 -305
  150. oscura/visualization/colors.py +0 -451
  151. oscura/visualization/digital.py +0 -436
  152. oscura/visualization/eye.py +0 -571
  153. oscura/visualization/histogram.py +0 -281
  154. oscura/visualization/interactive.py +0 -1035
  155. oscura/visualization/jitter.py +0 -1042
  156. oscura/visualization/keyboard.py +0 -394
  157. oscura/visualization/layout.py +0 -400
  158. oscura/visualization/optimization.py +0 -1079
  159. oscura/visualization/palettes.py +0 -446
  160. oscura/visualization/power.py +0 -508
  161. oscura/visualization/power_extended.py +0 -955
  162. oscura/visualization/presets.py +0 -469
  163. oscura/visualization/protocols.py +0 -1246
  164. oscura/visualization/render.py +0 -223
  165. oscura/visualization/rendering.py +0 -444
  166. oscura/visualization/reverse_engineering.py +0 -838
  167. oscura/visualization/signal_integrity.py +0 -989
  168. oscura/visualization/specialized.py +0 -643
  169. oscura/visualization/spectral.py +0 -1226
  170. oscura/visualization/thumbnails.py +0 -340
  171. oscura/visualization/time_axis.py +0 -351
  172. oscura/visualization/waveform.py +0 -454
  173. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/WHEEL +0 -0
  174. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/entry_points.txt +0 -0
  175. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/licenses/LICENSE +0 -0
@@ -20,6 +20,7 @@ from typing import TYPE_CHECKING, Any
20
20
 
21
21
  import numpy as np
22
22
 
23
+ from oscura.core.measurement_result import make_measurement
23
24
  from oscura.core.types import WaveformTrace
24
25
 
25
26
  if TYPE_CHECKING:
@@ -171,6 +172,75 @@ def weighted_mean(
171
172
  return float(np.average(data, weights=weights))
172
173
 
173
174
 
175
+ def weighted_std(
176
+ trace: WaveformTrace | NDArray[np.floating[Any]],
177
+ weights: NDArray[np.floating[Any]] | None = None,
178
+ *,
179
+ ddof: int = 0,
180
+ ) -> float:
181
+ """Compute weighted standard deviation.
182
+
183
+ Uses the reliability weights formula for weighted variance.
184
+
185
+ Args:
186
+ trace: Input trace or numpy array.
187
+ weights: Weight array (same length as data). If None, equal weights (unweighted std).
188
+ ddof: Delta degrees of freedom for bias correction (default 0).
189
+ - ddof=0: Maximum likelihood estimate (biased)
190
+ - ddof=1: Sample standard deviation (unbiased for normal distribution)
191
+
192
+ Returns:
193
+ Weighted standard deviation.
194
+
195
+ Raises:
196
+ ValueError: If weights and data have different lengths.
197
+ ValueError: If weights contain negative values.
198
+
199
+ Example:
200
+ >>> weights = np.linspace(0.5, 1.0, len(trace.data))
201
+ >>> wstd = weighted_std(trace, weights)
202
+ >>> print(f"Weighted std: {wstd:.6f}")
203
+
204
+ >>> # Sample standard deviation (Bessel's correction)
205
+ >>> wstd_unbiased = weighted_std(trace, weights, ddof=1)
206
+
207
+ References:
208
+ Wikipedia: Weighted arithmetic mean
209
+ https://en.wikipedia.org/wiki/Weighted_arithmetic_mean#Weighted_sample_variance
210
+ """
211
+ data = trace.data if isinstance(trace, WaveformTrace) else trace
212
+
213
+ if weights is None:
214
+ return float(np.std(data, ddof=ddof))
215
+
216
+ if len(weights) != len(data):
217
+ raise ValueError(f"Weights and data must have same length: {len(weights)} != {len(data)}")
218
+
219
+ if np.any(weights < 0):
220
+ raise ValueError("Weights must be non-negative")
221
+
222
+ # Handle edge cases
223
+ if len(data) == 0:
224
+ return float("nan")
225
+
226
+ if len(data) == 1:
227
+ return 0.0
228
+
229
+ # Compute weighted mean
230
+ w_sum = np.sum(weights)
231
+ if w_sum <= 0:
232
+ return float("nan")
233
+
234
+ w_mean = np.sum(weights * data) / w_sum
235
+
236
+ # Compute weighted variance with bias correction
237
+ # Reliability weights formula: var = sum(w * (x - mean)^2) / (sum(w) - ddof)
238
+ weighted_sq_deviations = weights * (data - w_mean) ** 2
239
+ variance = np.sum(weighted_sq_deviations) / (w_sum - ddof) if w_sum > ddof else 0.0
240
+
241
+ return float(np.sqrt(max(0.0, variance)))
242
+
243
+
174
244
  def running_stats(
175
245
  trace: WaveformTrace | NDArray[np.floating[Any]],
176
246
  window_size: int,
@@ -253,11 +323,93 @@ def summary_stats(
253
323
  return basic
254
324
 
255
325
 
326
+ def measure(
327
+ trace: WaveformTrace | NDArray[np.floating[Any]],
328
+ *,
329
+ parameters: list[str] | None = None,
330
+ include_units: bool = True,
331
+ ) -> dict[str, Any]:
332
+ """Compute statistical measurements with consistent format.
333
+
334
+ Unified function matching the API pattern of waveform.measure() and spectral.measure().
335
+ Returns MeasurementResult format with applicability tracking and formatting.
336
+
337
+ Args:
338
+ trace: Input trace or numpy array.
339
+ parameters: List of measurement names to compute. If None, compute all.
340
+ Valid names: mean, variance, std, min, max, range, count, p1, p5, p25, p50, p75, p95, p99
341
+ include_units: If True, return MeasurementResult format. If False, return flat values.
342
+
343
+ Returns:
344
+ Dictionary mapping measurement names to MeasurementResults (if include_units=True)
345
+ or raw values (if include_units=False).
346
+
347
+ Example:
348
+ >>> from oscura.analyzers.statistics import measure
349
+ >>> results = measure(trace)
350
+ >>> if results['mean']['applicable']:
351
+ ... print(f"Mean: {results['mean']['display']}")
352
+
353
+ >>> # Get specific measurements only
354
+ >>> results = measure(trace, parameters=["mean", "std"])
355
+
356
+ >>> # Get flat values (legacy compatibility)
357
+ >>> results = measure(trace, include_units=False)
358
+ >>> mean_value = results["mean"] # Just the float
359
+ """
360
+ data = trace.data if isinstance(trace, WaveformTrace) else trace
361
+
362
+ # Define unit mappings for statistical measurements
363
+ # For generic signals we use voltage units, but this could be parameterized
364
+ unit_map = {
365
+ "mean": "V",
366
+ "variance": "V²",
367
+ "std": "V",
368
+ "min": "V",
369
+ "max": "V",
370
+ "range": "dimensionless",
371
+ "count": "samples",
372
+ "p1": "dimensionless",
373
+ "p5": "dimensionless",
374
+ "p25": "dimensionless",
375
+ "p50": "dimensionless",
376
+ "p75": "dimensionless",
377
+ "p95": "dimensionless",
378
+ "p99": "dimensionless",
379
+ }
380
+
381
+ # Get basic stats
382
+ basic = basic_stats(trace)
383
+
384
+ # Get percentiles
385
+ percentile_values = percentiles(data, [1, 5, 25, 50, 75, 95, 99])
386
+
387
+ # Combine into single dict
388
+ all_measurements = {**basic, **percentile_values}
389
+
390
+ # Select requested measurements or all
391
+ if parameters is not None:
392
+ all_measurements = {k: v for k, v in all_measurements.items() if k in parameters}
393
+
394
+ # Format results
395
+ if include_units:
396
+ results = {}
397
+ for name, value in all_measurements.items():
398
+ unit = unit_map.get(name, "")
399
+ # All statistical measurements are always applicable (never NaN from valid data)
400
+ results[name] = make_measurement(value, unit)
401
+ return results
402
+ else:
403
+ return all_measurements
404
+
405
+
256
406
  __all__ = [
257
407
  "basic_stats",
408
+ "measure",
258
409
  "percentiles",
259
410
  "quartiles",
260
411
  "running_stats",
261
412
  "summary_stats",
262
413
  "weighted_mean",
414
+ "weighted_std",
263
415
  ]
@@ -338,24 +338,48 @@ def cross_correlation(
338
338
  def correlation_coefficient(
339
339
  trace1: WaveformTrace | NDArray[np.floating[Any]],
340
340
  trace2: WaveformTrace | NDArray[np.floating[Any]],
341
+ *,
342
+ method: Literal["pearson", "spearman", "kendall"] = "pearson",
341
343
  ) -> float:
342
- """Compute Pearson correlation coefficient between two signals.
344
+ """Compute correlation coefficient between two signals.
343
345
 
344
- Simple measure of linear relationship between signals at zero lag.
346
+ Supports Pearson (linear), Spearman (monotonic), and Kendall (rank) correlations.
345
347
 
346
348
  Args:
347
349
  trace1: First input trace or numpy array.
348
350
  trace2: Second input trace or numpy array.
351
+ method: Correlation method to use:
352
+ - "pearson": Linear correlation (default, parametric)
353
+ - "spearman": Monotonic correlation (non-parametric, robust to outliers)
354
+ - "kendall": Rank correlation (non-parametric, tau-b coefficient)
349
355
 
350
356
  Returns:
351
357
  Correlation coefficient in range [-1, 1].
352
358
 
359
+ Raises:
360
+ ValueError: If method is not one of the supported types.
361
+
353
362
  Example:
363
+ >>> # Linear correlation (default)
354
364
  >>> r = correlation_coefficient(trace1, trace2)
355
- >>> print(f"Correlation: {r:.3f}")
365
+ >>> print(f"Pearson correlation: {r:.3f}")
366
+
367
+ >>> # Monotonic correlation (robust to outliers)
368
+ >>> rho = correlation_coefficient(trace1, trace2, method="spearman")
369
+ >>> print(f"Spearman correlation: {rho:.3f}")
370
+
371
+ >>> # Rank correlation (best for ordinal data)
372
+ >>> tau = correlation_coefficient(trace1, trace2, method="kendall")
373
+ >>> print(f"Kendall correlation: {tau:.3f}")
374
+
375
+ References:
376
+ Pearson, K. (1895). Correlation coefficient
377
+ Spearman, C. (1904). Rank correlation
378
+ Kendall, M. G. (1938). Tau rank correlation
356
379
  """
357
- data1 = trace1.data if isinstance(trace1, WaveformTrace) else trace1
380
+ from scipy import stats as sp_stats
358
381
 
382
+ data1 = trace1.data if isinstance(trace1, WaveformTrace) else trace1
359
383
  data2 = trace2.data if isinstance(trace2, WaveformTrace) else trace2
360
384
 
361
385
  # Ensure same length
@@ -363,8 +387,25 @@ def correlation_coefficient(
363
387
  data1 = data1[:n]
364
388
  data2 = data2[:n]
365
389
 
366
- # Compute correlation
367
- return float(np.corrcoef(data1, data2)[0, 1])
390
+ # Compute correlation based on method
391
+ if method == "pearson":
392
+ # Pearson linear correlation (parametric)
393
+ return float(np.corrcoef(data1, data2)[0, 1])
394
+
395
+ elif method == "spearman":
396
+ # Spearman rank correlation (non-parametric, monotonic)
397
+ corr, _p_value = sp_stats.spearmanr(data1, data2)
398
+ return float(corr)
399
+
400
+ elif method == "kendall":
401
+ # Kendall tau-b rank correlation (non-parametric)
402
+ corr, _p_value = sp_stats.kendalltau(data1, data2)
403
+ return float(corr)
404
+
405
+ else:
406
+ raise ValueError(
407
+ f"Unknown correlation method: {method}. Available: 'pearson', 'spearman', 'kendall'"
408
+ )
368
409
 
369
410
 
370
411
  def _extract_periodicity_data(
@@ -64,7 +64,7 @@ def is_suitable_for_frequency_measurement(trace: WaveformTrace) -> tuple[bool, s
64
64
 
65
65
  # Check period consistency (is it periodic?)
66
66
  if len(rising_edges) >= 3:
67
- edge_times = rising_edges * trace.metadata.time_base
67
+ edge_times = rising_edges / trace.metadata.sample_rate
68
68
  periods = np.diff(edge_times)
69
69
  period_cv = np.std(periods) / np.mean(periods) if np.mean(periods) > 0 else float("inf")
70
70
 
@@ -4,6 +4,7 @@ Provides timing and amplitude measurements for analog waveforms.
4
4
  """
5
5
 
6
6
  from oscura.analyzers.waveform.measurements import (
7
+ MEASUREMENT_METADATA,
7
8
  amplitude,
8
9
  duty_cycle,
9
10
  fall_time,
@@ -20,6 +21,7 @@ from oscura.analyzers.waveform.measurements import (
20
21
  )
21
22
 
22
23
  __all__ = [
24
+ "MEASUREMENT_METADATA",
23
25
  "amplitude",
24
26
  "duty_cycle",
25
27
  "fall_time",