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
oscura/jupyter/display.py CHANGED
@@ -103,8 +103,8 @@ class TraceDisplay:
103
103
  rows.append(("Sample Rate", rate_str))
104
104
 
105
105
  # Channel name
106
- if hasattr(meta, "channel_name") and meta.channel_name:
107
- rows.append(("Channel", meta.channel_name))
106
+ if hasattr(meta, "channel") and meta.channel:
107
+ rows.append(("Channel", meta.channel))
108
108
 
109
109
  # Source file
110
110
  if hasattr(meta, "source_file") and meta.source_file:
oscura/jupyter/magic.py CHANGED
@@ -82,7 +82,7 @@ except ImportError:
82
82
 
83
83
 
84
84
  @magics_class
85
- class OscuraMagics(Magics):
85
+ class OscuraMagics(Magics): # type: ignore[misc]
86
86
  """IPython magics for Oscura analysis.
87
87
 
88
88
  Provides convenient shortcuts for loading traces, running measurements,
@@ -232,8 +232,8 @@ class OscuraMagics(Magics):
232
232
  meta = trace.metadata
233
233
  if hasattr(meta, "sample_rate"):
234
234
  info["sample_rate"] = meta.sample_rate
235
- if hasattr(meta, "channel_name"):
236
- info["channel"] = meta.channel_name
235
+ if hasattr(meta, "channel"):
236
+ info["channel"] = meta.channel
237
237
 
238
238
  for key, value in info.items():
239
239
  print(f"{key}: {value}")
@@ -290,6 +290,10 @@ def load(
290
290
  return _dispatch_loader(loader_name, path, channel=channel, **kwargs)
291
291
 
292
292
 
293
+ # Alias for auto-detection (backward compatibility)
294
+ load_auto = load
295
+
296
+
293
297
  def _load_wfm_auto(
294
298
  path: Path,
295
299
  *,
@@ -400,8 +404,8 @@ def load_all_channels(
400
404
  else:
401
405
  # For other formats, try loading as single channel
402
406
  trace = load(path, format=format)
403
- channel_name = getattr(trace.metadata, "channel_name", None) or "ch1"
404
- return {channel_name: trace}
407
+ channel = getattr(trace.metadata, "channel", None) or "ch1"
408
+ return {channel: trace}
405
409
 
406
410
 
407
411
  def _load_all_channels_tektronix(
@@ -464,9 +468,9 @@ def _read_tektronix_file(path: Path) -> Any:
464
468
  except ImportError:
465
469
  # Fall back to single channel loading
466
470
  trace = load(path, format="tektronix")
467
- channel_name = getattr(trace.metadata, "channel_name", None) or "ch1"
471
+ channel = getattr(trace.metadata, "channel", None) or "ch1"
468
472
  # Return a dict-like object to maintain compatibility
469
- return {"__fallback__": {channel_name: trace}}
473
+ return {"__fallback__": {channel: trace}}
470
474
 
471
475
  try:
472
476
  return tm_data_types.read_file(str(path))
@@ -502,14 +506,14 @@ def _extract_analog_waveforms(
502
506
  sample_rate = 1.0 / x_increment if x_increment > 0 else 1e6
503
507
  vertical_scale = getattr(awfm, "y_scale", None)
504
508
  vertical_offset = getattr(awfm, "y_offset", None)
505
- channel_name = getattr(awfm, "name", f"CH{i + 1}")
509
+ channel = getattr(awfm, "name", f"CH{i + 1}")
506
510
 
507
511
  trace = _build_waveform_trace(
508
512
  data=data,
509
513
  sample_rate=sample_rate,
510
514
  vertical_scale=vertical_scale,
511
515
  vertical_offset=vertical_offset,
512
- channel_name=channel_name,
516
+ channel=channel,
513
517
  path=path,
514
518
  wfm=awfm,
515
519
  )
@@ -557,23 +561,23 @@ def _extract_direct_waveform(
557
561
  from oscura.loaders.tektronix import _load_digital_waveform
558
562
 
559
563
  trace = _load_digital_waveform(wfm, path, 0)
560
- channel_name = trace.metadata.channel_name or "d1"
561
- channels[channel_name.lower()] = trace
564
+ channel = trace.metadata.channel or "d1"
565
+ channels[channel.lower()] = trace
562
566
 
563
567
  elif wfm_type == "IQWaveform" or (
564
568
  hasattr(wfm, "i_axis_values") and hasattr(wfm, "q_axis_values")
565
569
  ):
566
570
  # IQWaveform format (RF/SDR data)
567
571
  loaded_trace = load(path, format="tektronix")
568
- channel_name = loaded_trace.metadata.channel_name or "iq1"
569
- channels[channel_name.lower()] = loaded_trace
572
+ channel = loaded_trace.metadata.channel or "iq1"
573
+ channels[channel.lower()] = loaded_trace
570
574
 
571
575
  elif hasattr(wfm, "y_axis_values") or hasattr(wfm, "y_data"):
572
576
  # Direct analog waveform
573
577
  loaded_trace = load(path, format="tektronix")
574
578
  # Add both analog and digital traces to channels
575
- channel_name = loaded_trace.metadata.channel_name or "ch1"
576
- channels[channel_name.lower()] = loaded_trace
579
+ channel = loaded_trace.metadata.channel or "ch1"
580
+ channels[channel.lower()] = loaded_trace
577
581
 
578
582
 
579
583
  def get_supported_formats() -> list[str]:
@@ -649,6 +653,7 @@ __all__ = [
649
653
  "hdf5",
650
654
  "load",
651
655
  "load_all_channels",
656
+ "load_auto",
652
657
  "load_binary",
653
658
  "load_binary_packets",
654
659
  "load_lazy",
oscura/loaders/binary.py CHANGED
@@ -81,8 +81,8 @@ def load_binary(
81
81
  # Create metadata
82
82
  metadata = TraceMetadata(
83
83
  sample_rate=sample_rate,
84
+ channel=f"Channel {channel}",
84
85
  source_file=str(path),
85
- channel_name=f"Channel {channel}",
86
86
  )
87
87
 
88
88
  return WaveformTrace(data=data.astype(np.float64), metadata=metadata)
@@ -399,8 +399,7 @@ def to_waveform_trace(
399
399
 
400
400
  metadata = TraceMetadata(
401
401
  sample_rate=traceset.sample_rate,
402
- source_file=str(traceset.metadata.get("source_file", "")) if traceset.metadata else "",
403
- channel_name=f"trace_{trace_index}",
402
+ channel=f"trace_{trace_index}",
404
403
  )
405
404
 
406
405
  return WaveformTrace(
@@ -1351,7 +1351,7 @@ def extract_channels(
1351
1351
  # Create metadata with configurable sample rate
1352
1352
  metadata = TraceMetadata(
1353
1353
  sample_rate=effective_sample_rate,
1354
- channel_name=ch_name,
1354
+ channel=ch_name,
1355
1355
  )
1356
1356
 
1357
1357
  traces[ch_name] = DigitalTrace(data=data, metadata=metadata)
@@ -208,8 +208,8 @@ def _load_with_pandas(
208
208
  # Create metadata and trace
209
209
  metadata = TraceMetadata(
210
210
  sample_rate=detected_sample_rate,
211
+ channel=voltage_col_name or "CH1",
211
212
  source_file=str(path),
212
- channel_name=voltage_col_name or "CH1",
213
213
  )
214
214
 
215
215
  return WaveformTrace(data=np.asarray(voltage_data, dtype=np.float64), metadata=metadata)
@@ -429,8 +429,8 @@ def _load_basic(
429
429
  # Create metadata and trace
430
430
  metadata = TraceMetadata(
431
431
  sample_rate=detected_sample_rate,
432
+ channel=channel_name,
432
433
  source_file=str(path),
433
- channel_name=channel_name,
434
434
  )
435
435
 
436
436
  return WaveformTrace(data=np.array(voltage_data, dtype=np.float64), metadata=metadata)
@@ -413,8 +413,8 @@ def _build_metadata(
413
413
  sample_rate=float(detected_sample_rate),
414
414
  vertical_scale=float(vertical_scale) if vertical_scale else None,
415
415
  vertical_offset=float(vertical_offset) if vertical_offset else None,
416
+ channel=str(channel_name),
416
417
  source_file=str(file_path),
417
- channel_name=str(channel_name),
418
418
  )
419
419
 
420
420
 
oscura/loaders/lazy.py CHANGED
@@ -219,9 +219,14 @@ class LazyWaveformTrace:
219
219
  """
220
220
  from oscura.core.types import TraceMetadata, WaveformTrace
221
221
 
222
+ # Handle API migration: channel_name -> channel
223
+ metadata_dict = self._metadata.copy()
224
+ if "channel_name" in metadata_dict:
225
+ metadata_dict["channel"] = metadata_dict.pop("channel_name")
226
+
222
227
  metadata = TraceMetadata(
223
228
  sample_rate=self._sample_rate,
224
- **self._metadata,
229
+ **metadata_dict,
225
230
  )
226
231
  return WaveformTrace(data=self.data, metadata=metadata)
227
232
 
@@ -308,7 +308,6 @@ class MmapWaveformTrace:
308
308
 
309
309
  metadata = TraceMetadata(
310
310
  sample_rate=self._sample_rate,
311
- source_file=str(self._file_path),
312
311
  **self._metadata,
313
312
  )
314
313
 
@@ -173,8 +173,8 @@ def _build_npz_metadata(
173
173
  sample_rate=float(final_sample_rate),
174
174
  vertical_scale=float(detected_vertical_scale) if detected_vertical_scale else None,
175
175
  vertical_offset=float(detected_vertical_offset) if detected_vertical_offset else None,
176
+ channel=_get_channel_name(npz, channel),
176
177
  source_file=str(path),
177
- channel_name=_get_channel_name(npz, channel),
178
178
  )
179
179
 
180
180
 
@@ -444,12 +444,13 @@ def _get_channel_name(
444
444
  elif isinstance(channel, int):
445
445
  return f"CH{channel + 1}"
446
446
 
447
- # Try to find channel name in metadata
447
+ # Try to find channel name in metadata (support both old and new API)
448
448
  keys = list(npz.keys())
449
- if "channel_name" in keys:
450
- value = npz["channel_name"]
451
- # NPZ values are always ndarrays
452
- return str(value.item())
449
+ for key in ["channel", "channel_name"]:
450
+ if key in keys:
451
+ value = npz[key]
452
+ # NPZ values are always ndarrays
453
+ return str(value.item())
453
454
 
454
455
  return "CH1"
455
456
 
@@ -556,8 +557,8 @@ def load_raw_binary(
556
557
 
557
558
  metadata = TraceMetadata(
558
559
  sample_rate=sample_rate,
560
+ channel="RAW",
559
561
  source_file=str(path),
560
- channel_name="RAW",
561
562
  )
562
563
 
563
564
  return WaveformTrace(data=data, metadata=metadata)
@@ -293,13 +293,11 @@ def trim_idle(
293
293
  sample_rate=trace.metadata.sample_rate,
294
294
  vertical_scale=trace.metadata.vertical_scale,
295
295
  vertical_offset=trace.metadata.vertical_offset,
296
- acquisition_time=trace.metadata.acquisition_time,
297
- trigger_info=trace.metadata.trigger_info,
298
- source_file=trace.metadata.source_file,
299
- channel_name=trace.metadata.channel_name,
296
+ channel=trace.metadata.channel,
297
+ units=trace.metadata.units,
300
298
  )
301
299
 
302
- return DigitalTrace(data=trimmed_data, metadata=new_metadata, edges=None)
300
+ return DigitalTrace(data=trimmed_data, metadata=new_metadata)
303
301
 
304
302
  return trace
305
303
 
oscura/loaders/rigol.py CHANGED
@@ -26,7 +26,7 @@ if TYPE_CHECKING:
26
26
 
27
27
  # Try to import RigolWFM for full Rigol support
28
28
  try:
29
- import RigolWFM.wfm as rigol_wfm # type: ignore[import-untyped] # Optional third-party library
29
+ import RigolWFM.wfm as rigol_wfm # type: ignore[import-untyped,import-not-found] # Optional third-party library
30
30
 
31
31
  RIGOL_WFM_AVAILABLE = True
32
32
  except ImportError:
@@ -109,14 +109,28 @@ def _load_with_rigolwfm(
109
109
  _extract_rigol_channel_data(wfm, channel, str(path))
110
110
  )
111
111
 
112
+ # Extract trigger info if available
113
+ trigger_info = None
114
+ if (
115
+ hasattr(wfm, "trigger_level")
116
+ or hasattr(wfm, "trigger_mode")
117
+ or hasattr(wfm, "trigger_source")
118
+ ):
119
+ trigger_info = {}
120
+ if hasattr(wfm, "trigger_level"):
121
+ trigger_info["level"] = wfm.trigger_level
122
+ if hasattr(wfm, "trigger_mode"):
123
+ trigger_info["mode"] = wfm.trigger_mode
124
+ if hasattr(wfm, "trigger_source"):
125
+ trigger_info["source"] = wfm.trigger_source
126
+
112
127
  # Build metadata
113
128
  metadata = TraceMetadata(
114
129
  sample_rate=sample_rate,
115
130
  vertical_scale=vertical_scale,
116
131
  vertical_offset=vertical_offset,
117
- source_file=str(path),
118
- channel_name=channel_name,
119
- trigger_info=_extract_trigger_info(wfm),
132
+ channel=channel_name,
133
+ trigger_info=trigger_info,
120
134
  )
121
135
 
122
136
  return WaveformTrace(data=data, metadata=metadata)
@@ -200,7 +214,7 @@ def _extract_rigol_channel_data(
200
214
  file_path: File path for error messages.
201
215
 
202
216
  Returns:
203
- Tuple of (data, sample_rate, vertical_scale, vertical_offset, channel_name).
217
+ Tuple of (data, sample_rate, vertical_scale, vertical_offset, channel).
204
218
 
205
219
  Raises:
206
220
  FormatError: If no waveform data found.
@@ -212,7 +226,7 @@ def _extract_rigol_channel_data(
212
226
  sample_rate = wfm.sample_rate if hasattr(wfm, "sample_rate") else 1e6
213
227
  vertical_scale = ch.volts_per_div if hasattr(ch, "volts_per_div") else None
214
228
  vertical_offset = ch.volt_offset if hasattr(ch, "volt_offset") else None
215
- channel_name = f"CH{channel + 1}"
229
+ channel_name: str = f"CH{channel + 1}"
216
230
  return data, sample_rate, vertical_scale, vertical_offset, channel_name
217
231
 
218
232
  # Single channel format
@@ -302,7 +316,7 @@ def _load_basic(
302
316
  vertical_scale=vertical_scale,
303
317
  vertical_offset=vertical_offset,
304
318
  source_file=str(path),
305
- channel_name=f"CH{channel + 1}",
319
+ channel=f"CH{channel + 1}",
306
320
  )
307
321
 
308
322
  return WaveformTrace(data=data, metadata=metadata)
oscura/loaders/sigrok.py CHANGED
@@ -270,19 +270,16 @@ def _build_digital_trace(
270
270
  Returns:
271
271
  DigitalTrace object.
272
272
  """
273
- edges = _compute_edges(channel_data, sample_rate)
273
+ _compute_edges(channel_data, sample_rate)
274
274
 
275
275
  trace_metadata = TraceMetadata(
276
276
  sample_rate=sample_rate,
277
- source_file=str(path),
278
- channel_name=channel_name,
279
- trigger_info=metadata_dict.get("trigger"),
277
+ channel=channel_name,
280
278
  )
281
279
 
282
280
  return DigitalTrace(
283
281
  data=channel_data,
284
282
  metadata=trace_metadata,
285
- edges=edges,
286
283
  )
287
284
 
288
285
 
oscura/loaders/tdms.py CHANGED
@@ -230,14 +230,15 @@ def _build_tdms_metadata(
230
230
  sample_rate = _get_sample_rate(target_channel, target_group, tdms_file)
231
231
  vertical_scale = target_channel.properties.get("NI_Scale[0]_Linear_Slope")
232
232
  vertical_offset = target_channel.properties.get("NI_Scale[0]_Linear_Y_Intercept")
233
+ trigger_info = _extract_tdms_properties(target_channel)
233
234
 
234
235
  return TraceMetadata(
235
236
  sample_rate=sample_rate,
236
237
  vertical_scale=float(vertical_scale) if vertical_scale is not None else None,
237
238
  vertical_offset=float(vertical_offset) if vertical_offset is not None else None,
239
+ channel=target_channel.name,
240
+ trigger_info=trigger_info,
238
241
  source_file=str(path),
239
- channel_name=target_channel.name,
240
- trigger_info=_extract_tdms_properties(target_channel),
241
242
  )
242
243
 
243
244
 
@@ -20,7 +20,6 @@ Example:
20
20
 
21
21
  from __future__ import annotations
22
22
 
23
- import contextlib
24
23
  import logging
25
24
  from pathlib import Path
26
25
  from typing import TYPE_CHECKING, Any, Union
@@ -80,7 +79,7 @@ def load_tektronix_wfm(
80
79
  Example:
81
80
  >>> trace = load_tektronix_wfm("TEK00001.wfm")
82
81
  >>> print(f"Sample rate: {trace.metadata.sample_rate} Hz")
83
- >>> print(f"Channel: {trace.metadata.channel_name}")
82
+ >>> print(f"Channel: {trace.metadata.channel}")
84
83
 
85
84
  >>> # Check trace type
86
85
  >>> if isinstance(trace, DigitalTrace):
@@ -250,7 +249,7 @@ def _load_analog_waveforms_container(
250
249
  sample_rate = 1.0 / waveform.x_increment if waveform.x_increment > 0 else 1e6
251
250
  vertical_scale = getattr(waveform, "y_scale", None)
252
251
  vertical_offset = getattr(waveform, "y_offset", None)
253
- channel_name = getattr(waveform, "name", f"CH{channel + 1}")
252
+ channel_name: str = str(getattr(waveform, "name", None) or f"CH{channel + 1}")
254
253
 
255
254
  # Use original wfm for trigger info (need to get it from parent)
256
255
  return _build_waveform_trace(
@@ -258,7 +257,7 @@ def _load_analog_waveforms_container(
258
257
  sample_rate=sample_rate,
259
258
  vertical_scale=vertical_scale,
260
259
  vertical_offset=vertical_offset,
261
- channel_name=channel_name,
260
+ channel=channel_name,
262
261
  path=path,
263
262
  wfm=waveform,
264
263
  )
@@ -288,7 +287,7 @@ def _load_analog_waveform_direct(
288
287
  x_spacing = float(wfm.x_axis_spacing) if wfm.x_axis_spacing else 1e-6
289
288
  sample_rate = 1.0 / x_spacing if x_spacing > 0 else 1e6
290
289
  vertical_offset = y_offset
291
- channel_name = (
290
+ channel_name: str = (
292
291
  wfm.source_name if hasattr(wfm, "source_name") and wfm.source_name else f"CH{channel + 1}"
293
292
  )
294
293
 
@@ -297,7 +296,7 @@ def _load_analog_waveform_direct(
297
296
  sample_rate=sample_rate,
298
297
  vertical_scale=None,
299
298
  vertical_offset=vertical_offset,
300
- channel_name=channel_name,
299
+ channel=channel_name,
301
300
  path=path,
302
301
  wfm=wfm,
303
302
  )
@@ -321,14 +320,14 @@ def _load_legacy_y_data(
321
320
  sample_rate = 1.0 / x_increment if x_increment > 0 else 1e6
322
321
  vertical_scale = getattr(wfm, "y_scale", None)
323
322
  vertical_offset = getattr(wfm, "y_offset", None)
324
- channel_name = getattr(wfm, "name", "CH1")
323
+ channel = getattr(wfm, "name", "CH1")
325
324
 
326
325
  return _build_waveform_trace(
327
326
  data=data,
328
327
  sample_rate=sample_rate,
329
328
  vertical_scale=vertical_scale,
330
329
  vertical_offset=vertical_offset,
331
- channel_name=channel_name,
330
+ channel=channel,
332
331
  path=path,
333
332
  wfm=wfm,
334
333
  )
@@ -339,7 +338,7 @@ def _build_waveform_trace(
339
338
  sample_rate: float,
340
339
  vertical_scale: float | None,
341
340
  vertical_offset: float | None,
342
- channel_name: str,
341
+ channel: str,
343
342
  path: Path,
344
343
  wfm: Any,
345
344
  ) -> WaveformTrace:
@@ -350,27 +349,38 @@ def _build_waveform_trace(
350
349
  sample_rate: Sample rate in Hz.
351
350
  vertical_scale: Vertical scale in volts/div.
352
351
  vertical_offset: Vertical offset in volts.
353
- channel_name: Channel name.
352
+ channel: Channel name.
354
353
  path: Source file path.
355
354
  wfm: Original waveform object for trigger info extraction.
356
355
 
357
356
  Returns:
358
357
  Constructed WaveformTrace.
359
358
  """
359
+ # Extract trigger information
360
+ trigger_info = _extract_trigger_info(wfm)
361
+
360
362
  # Extract acquisition time if available
361
363
  acquisition_time = None
362
364
  if hasattr(wfm, "date_time"):
363
- with contextlib.suppress(ValueError, AttributeError):
364
- acquisition_time = wfm.date_time
365
+ try:
366
+ from datetime import datetime
367
+
368
+ # Handle both datetime objects and other formats
369
+ if isinstance(wfm.date_time, datetime):
370
+ acquisition_time = wfm.date_time
371
+ # Add additional parsing if needed for other formats
372
+ except (ValueError, AttributeError, TypeError):
373
+ # Silently ignore invalid or unparseable times
374
+ pass
365
375
 
366
376
  metadata = TraceMetadata(
367
377
  sample_rate=sample_rate,
368
378
  vertical_scale=vertical_scale,
369
379
  vertical_offset=vertical_offset,
370
- acquisition_time=acquisition_time,
380
+ channel=channel,
371
381
  source_file=str(path),
372
- channel_name=channel_name,
373
- trigger_info=_extract_trigger_info(wfm),
382
+ trigger_info=trigger_info,
383
+ acquisition_time=acquisition_time,
374
384
  )
375
385
 
376
386
  return WaveformTrace(data=data, metadata=metadata)
@@ -407,19 +417,15 @@ def _load_digital_waveform(
407
417
  sample_rate = _extract_sample_rate(wfm)
408
418
 
409
419
  # Extract channel name
410
- channel_name = _extract_channel_name(wfm, channel)
420
+ channel_name: str = _extract_channel(wfm, channel)
411
421
 
412
422
  # Build metadata
413
423
  metadata = TraceMetadata(
414
424
  sample_rate=sample_rate,
415
- source_file=str(path),
416
- channel_name=channel_name,
425
+ channel=channel_name,
417
426
  )
418
427
 
419
- # Extract edge information if available
420
- edges = _extract_edges(wfm)
421
-
422
- return DigitalTrace(data=data, metadata=metadata, edges=edges)
428
+ return DigitalTrace(data=data, metadata=metadata)
423
429
 
424
430
 
425
431
  def _extract_digital_samples(wfm: Any, path: Path) -> NDArray[np.bool_]:
@@ -465,7 +471,7 @@ def _extract_sample_rate(wfm: Any) -> float:
465
471
  return 1.0 / x_spacing if x_spacing > 0 else 1e6
466
472
 
467
473
 
468
- def _extract_channel_name(wfm: Any, channel: int) -> str:
474
+ def _extract_channel(wfm: Any, channel: int) -> str:
469
475
  """Extract channel name from waveform object."""
470
476
  # Try source_name first
471
477
  if hasattr(wfm, "source_name") and wfm.source_name:
@@ -534,18 +540,19 @@ def _load_iq_waveform(
534
540
  sample_rate = 1.0 / x_spacing if x_spacing > 0 else 1e6
535
541
 
536
542
  # Extract channel name
537
- channel_name = "IQ1"
543
+ channel = "IQ1"
538
544
  if hasattr(wfm, "source_name") and wfm.source_name:
539
- channel_name = wfm.source_name
545
+ channel = wfm.source_name
540
546
 
541
547
  # Build metadata
542
548
  metadata = TraceMetadata(
543
549
  sample_rate=sample_rate,
544
- source_file=str(path),
545
- channel_name=channel_name,
550
+ channel=channel,
546
551
  )
547
552
 
548
- return IQTrace(i_data=i_data, q_data=q_data, metadata=metadata)
553
+ # Create complex I/Q data
554
+ iq_data = i_data + 1j * q_data
555
+ return IQTrace(data=iq_data, metadata=metadata)
549
556
 
550
557
 
551
558
  def _load_basic(
@@ -641,14 +648,14 @@ def _parse_wfm003(
641
648
  # Extract metadata from header
642
649
  sample_rate = _extract_sample_interval(file_data, header_size)
643
650
  vertical_scale, vertical_offset = _extract_vertical_params(file_data, header_size)
644
- channel_name = f"CH{channel + 1}"
651
+ channel_name: str = f"CH{channel + 1}"
645
652
 
646
653
  metadata = TraceMetadata(
647
654
  sample_rate=sample_rate,
648
655
  vertical_scale=vertical_scale,
649
656
  vertical_offset=vertical_offset,
657
+ channel=channel_name,
650
658
  source_file=str(path),
651
- channel_name=channel_name,
652
659
  )
653
660
 
654
661
  return WaveformTrace(data=data, metadata=metadata)
@@ -797,8 +804,7 @@ def _parse_wfm_legacy(
797
804
  sample_rate=sample_rate,
798
805
  vertical_scale=vertical_scale,
799
806
  vertical_offset=vertical_offset,
800
- source_file=str(path),
801
- channel_name=f"CH{channel + 1}",
807
+ channel=f"CH{channel + 1}",
802
808
  )
803
809
 
804
810
  return WaveformTrace(data=data, metadata=metadata)