oscura 0.3.0__py3-none-any.whl → 0.5.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 (59) hide show
  1. oscura/__init__.py +1 -7
  2. oscura/acquisition/__init__.py +147 -0
  3. oscura/acquisition/file.py +255 -0
  4. oscura/acquisition/hardware.py +186 -0
  5. oscura/acquisition/saleae.py +340 -0
  6. oscura/acquisition/socketcan.py +315 -0
  7. oscura/acquisition/streaming.py +38 -0
  8. oscura/acquisition/synthetic.py +229 -0
  9. oscura/acquisition/visa.py +376 -0
  10. oscura/analyzers/__init__.py +3 -0
  11. oscura/analyzers/digital/__init__.py +48 -0
  12. oscura/analyzers/digital/clock.py +9 -1
  13. oscura/analyzers/digital/edges.py +1 -1
  14. oscura/analyzers/digital/extraction.py +195 -0
  15. oscura/analyzers/digital/ic_database.py +498 -0
  16. oscura/analyzers/digital/timing.py +41 -11
  17. oscura/analyzers/digital/timing_paths.py +339 -0
  18. oscura/analyzers/digital/vintage.py +377 -0
  19. oscura/analyzers/digital/vintage_result.py +148 -0
  20. oscura/analyzers/protocols/__init__.py +22 -1
  21. oscura/analyzers/protocols/parallel_bus.py +449 -0
  22. oscura/analyzers/side_channel/__init__.py +52 -0
  23. oscura/analyzers/side_channel/power.py +690 -0
  24. oscura/analyzers/side_channel/timing.py +369 -0
  25. oscura/analyzers/signal_integrity/sparams.py +1 -1
  26. oscura/automotive/__init__.py +4 -2
  27. oscura/automotive/can/patterns.py +3 -1
  28. oscura/automotive/can/session.py +277 -78
  29. oscura/automotive/can/state_machine.py +5 -2
  30. oscura/builders/__init__.py +9 -11
  31. oscura/builders/signal_builder.py +99 -191
  32. oscura/core/exceptions.py +5 -1
  33. oscura/export/__init__.py +12 -0
  34. oscura/export/wavedrom.py +430 -0
  35. oscura/exporters/json_export.py +47 -0
  36. oscura/exporters/vintage_logic_csv.py +247 -0
  37. oscura/loaders/__init__.py +1 -0
  38. oscura/loaders/chipwhisperer.py +393 -0
  39. oscura/loaders/touchstone.py +1 -1
  40. oscura/reporting/__init__.py +7 -0
  41. oscura/reporting/vintage_logic_report.py +523 -0
  42. oscura/session/session.py +54 -46
  43. oscura/sessions/__init__.py +70 -0
  44. oscura/sessions/base.py +323 -0
  45. oscura/sessions/blackbox.py +640 -0
  46. oscura/sessions/generic.py +189 -0
  47. oscura/utils/autodetect.py +5 -1
  48. oscura/visualization/digital_advanced.py +718 -0
  49. oscura/visualization/figure_manager.py +156 -0
  50. {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/METADATA +86 -5
  51. {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/RECORD +54 -33
  52. oscura/automotive/dtc/data.json +0 -2763
  53. oscura/schemas/bus_configuration.json +0 -322
  54. oscura/schemas/device_mapping.json +0 -182
  55. oscura/schemas/packet_format.json +0 -418
  56. oscura/schemas/protocol_definition.json +0 -363
  57. {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/WHEEL +0 -0
  58. {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/entry_points.txt +0 -0
  59. {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -5,7 +5,7 @@ composition of signals for test data generation, demos, and protocol testing.
5
5
 
6
6
  Example:
7
7
  >>> from oscura import SignalBuilder
8
- >>> signal = (SignalBuilder()
8
+ >>> trace = (SignalBuilder()
9
9
  ... .sample_rate(10e6)
10
10
  ... .duration(0.01)
11
11
  ... .add_sine(frequency=1000, amplitude=1.0)
@@ -16,12 +16,11 @@ The builder supports:
16
16
  - Analog waveforms (sine, square, triangle, chirp, multitone)
17
17
  - Protocol signals (UART, SPI, I2C, CAN)
18
18
  - Noise and impairments (gaussian, pink, jitter, quantization)
19
- - Multi-channel signals
19
+ - Multi-channel signals (use build_channels() to get all channels)
20
20
  """
21
21
 
22
22
  from __future__ import annotations
23
23
 
24
- from dataclasses import dataclass, field
25
24
  from pathlib import Path
26
25
  from typing import Any, Literal
27
26
 
@@ -31,167 +30,6 @@ from scipy import signal as scipy_signal
31
30
  from oscura.core.types import TraceMetadata, WaveformTrace
32
31
 
33
32
 
34
- @dataclass
35
- class SignalMetadata:
36
- """Metadata for generated signals.
37
-
38
- Attributes:
39
- sample_rate: Sample rate in Hz.
40
- duration: Signal duration in seconds.
41
- channel_names: List of channel names.
42
- description: Human-readable description.
43
- generator: Name of generator that created this signal.
44
- parameters: Dictionary of generation parameters.
45
- """
46
-
47
- sample_rate: float
48
- duration: float
49
- channel_names: list[str] = field(default_factory=lambda: ["ch1"])
50
- description: str = ""
51
- generator: str = "SignalBuilder"
52
- parameters: dict[str, Any] = field(default_factory=dict)
53
-
54
-
55
- @dataclass
56
- class GeneratedSignal:
57
- """Container for generated signal data.
58
-
59
- Attributes:
60
- data: Dictionary mapping channel names to signal arrays.
61
- metadata: Signal metadata.
62
- """
63
-
64
- data: dict[str, np.ndarray[Any, np.dtype[np.float64]]]
65
- metadata: SignalMetadata
66
- _time: np.ndarray[Any, np.dtype[np.float64]] | None = field(default=None, repr=False)
67
-
68
- @property
69
- def time(self) -> np.ndarray[Any, np.dtype[np.float64]]:
70
- """Get time array, computing if necessary."""
71
- if self._time is None:
72
- n_samples = len(next(iter(self.data.values())))
73
- self._time = np.arange(n_samples) / self.metadata.sample_rate
74
- return self._time
75
-
76
- @property
77
- def num_channels(self) -> int:
78
- """Number of channels in signal."""
79
- return len(self.data)
80
-
81
- @property
82
- def num_samples(self) -> int:
83
- """Number of samples per channel."""
84
- return len(next(iter(self.data.values())))
85
-
86
- def get_channel(self, name: str) -> np.ndarray[Any, np.dtype[np.float64]]:
87
- """Get signal data for a specific channel.
88
-
89
- Args:
90
- name: Channel name.
91
-
92
- Returns:
93
- Signal array for the channel.
94
-
95
- Raises:
96
- KeyError: If channel name not found.
97
- """
98
- if name not in self.data:
99
- available = list(self.data.keys())
100
- raise KeyError(f"Channel '{name}' not found. Available: {available}")
101
- return self.data[name]
102
-
103
- def to_trace(self, channel: str | None = None) -> WaveformTrace:
104
- """Convert to WaveformTrace for Oscura analysis.
105
-
106
- Args:
107
- channel: Channel name to convert. If None, uses first channel.
108
-
109
- Returns:
110
- WaveformTrace instance ready for analysis.
111
- """
112
- if channel is None:
113
- channel = self.metadata.channel_names[0]
114
-
115
- data = self.get_channel(channel)
116
- trace_meta = TraceMetadata(
117
- sample_rate=self.metadata.sample_rate,
118
- channel_name=channel,
119
- )
120
- return WaveformTrace(data=data, metadata=trace_meta)
121
-
122
- def save_npz(self, path: Path | str) -> None:
123
- """Save signal to NPZ format.
124
-
125
- Args:
126
- path: Output file path.
127
- """
128
- path = Path(path)
129
- path.parent.mkdir(parents=True, exist_ok=True)
130
-
131
- save_dict: dict[str, Any] = {
132
- "sample_rate": self.metadata.sample_rate,
133
- "duration": self.metadata.duration,
134
- "channel_names": np.array(self.metadata.channel_names),
135
- "description": self.metadata.description,
136
- "generator": self.metadata.generator,
137
- }
138
-
139
- # Add channel data
140
- for name, data in self.data.items():
141
- save_dict[name] = data
142
-
143
- # Add parameters as JSON-serializable
144
- for key, value in self.metadata.parameters.items():
145
- if isinstance(value, (int, float, str, bool)):
146
- save_dict[f"param_{key}"] = value
147
-
148
- np.savez_compressed(path, **save_dict)
149
-
150
- @classmethod
151
- def load_npz(cls, path: Path | str) -> GeneratedSignal:
152
- """Load signal from NPZ format.
153
-
154
- Args:
155
- path: Input file path.
156
-
157
- Returns:
158
- GeneratedSignal instance.
159
- """
160
- path = Path(path)
161
- loaded = np.load(path, allow_pickle=True)
162
-
163
- sample_rate = float(loaded["sample_rate"])
164
- duration = float(loaded["duration"])
165
- channel_names = list(loaded.get("channel_names", ["ch1"]))
166
- description = str(loaded.get("description", ""))
167
- generator = str(loaded.get("generator", "unknown"))
168
-
169
- # Extract channel data
170
- data = {}
171
- for name in channel_names:
172
- if name in loaded:
173
- data[name] = loaded[name]
174
-
175
- # Extract parameters
176
- parameters: dict[str, Any] = {}
177
- for key in loaded.files:
178
- if key.startswith("param_"):
179
- param_name = key[6:] # Remove "param_" prefix
180
- value = loaded[key]
181
- parameters[param_name] = value.item() if value.ndim == 0 else value
182
-
183
- metadata = SignalMetadata(
184
- sample_rate=sample_rate,
185
- duration=duration,
186
- channel_names=channel_names,
187
- description=description,
188
- generator=generator,
189
- parameters=parameters,
190
- )
191
-
192
- return cls(data=data, metadata=metadata)
193
-
194
-
195
33
  class SignalBuilder:
196
34
  """Fluent builder for composable signal generation.
197
35
 
@@ -200,7 +38,7 @@ class SignalBuilder:
200
38
 
201
39
  Example:
202
40
  >>> # Simple sine wave with noise
203
- >>> signal = (SignalBuilder()
41
+ >>> trace = (SignalBuilder()
204
42
  ... .sample_rate(1e6)
205
43
  ... .duration(0.01)
206
44
  ... .add_sine(frequency=1000, amplitude=1.0)
@@ -208,7 +46,7 @@ class SignalBuilder:
208
46
  ... .build())
209
47
  >>>
210
48
  >>> # UART signal with realistic characteristics
211
- >>> uart_signal = (SignalBuilder()
49
+ >>> uart_trace = (SignalBuilder()
212
50
  ... .sample_rate(10e6)
213
51
  ... .add_uart(baud_rate=115200, data=b"Hello Oscura!", config="8N1")
214
52
  ... .add_noise(snr_db=30)
@@ -1050,14 +888,34 @@ class SignalBuilder:
1050
888
 
1051
889
  # ========== Build Methods ==========
1052
890
 
1053
- def build(self) -> GeneratedSignal:
1054
- """Build and return signal.
891
+ def build(self, channel: str | None = None) -> WaveformTrace:
892
+ """Build and return signal as WaveformTrace.
893
+
894
+ This is the primary build method that returns a WaveformTrace ready
895
+ for analysis with Oscura. For multi-channel signals, specify which
896
+ channel to return, or use build_channels() to get all channels.
897
+
898
+ Args:
899
+ channel: Channel name to build. If None, uses first channel.
900
+ For single-channel signals, this parameter is optional.
1055
901
 
1056
902
  Returns:
1057
- GeneratedSignal containing all channels and metadata.
903
+ WaveformTrace ready for Oscura analysis.
1058
904
 
1059
905
  Raises:
1060
- ValueError: If no signals have been added.
906
+ ValueError: If no signals have been added or channel doesn't exist.
907
+
908
+ Example:
909
+ >>> builder = SignalBuilder(sample_rate=1e6).add_sine(1000)
910
+ >>> trace = builder.build()
911
+ >>> print(f"Generated {len(trace.data)} samples")
912
+
913
+ >>> # Multi-channel
914
+ >>> builder = SignalBuilder(sample_rate=1e6)
915
+ >>> builder.add_sine(1000, channel="sig")
916
+ >>> builder.add_square(500, channel="clk")
917
+ >>> sig_trace = builder.build(channel="sig")
918
+ >>> clk_trace = builder.build(channel="clk")
1061
919
  """
1062
920
  if not self._channels:
1063
921
  raise ValueError("No signals added. Call add_* methods before build().")
@@ -1068,44 +926,94 @@ class SignalBuilder:
1068
926
  if len(signal) < max_len:
1069
927
  self._channels[name] = np.pad(signal, (0, max_len - len(signal)), mode="edge")
1070
928
 
1071
- # Calculate actual duration from signal length
1072
- actual_duration = max_len / self._sample_rate
929
+ # Determine which channel to return
930
+ if channel is None:
931
+ # Use first channel
932
+ channel = next(iter(self._channels.keys()))
933
+ elif channel not in self._channels:
934
+ available = list(self._channels.keys())
935
+ raise ValueError(f"Channel '{channel}' not found. Available: {available}")
936
+
937
+ # Get channel data
938
+ data = self._channels[channel]
1073
939
 
1074
- metadata = SignalMetadata(
940
+ # Build TraceMetadata
941
+ trace_metadata = TraceMetadata(
1075
942
  sample_rate=self._sample_rate,
1076
- duration=actual_duration,
1077
- channel_names=list(self._channels.keys()),
1078
- description=self._description,
1079
- generator="SignalBuilder",
1080
- parameters=self._parameters,
943
+ channel_name=channel,
1081
944
  )
1082
945
 
1083
- return GeneratedSignal(data=self._channels.copy(), metadata=metadata)
946
+ return WaveformTrace(data=data, metadata=trace_metadata)
1084
947
 
1085
- def build_trace(self, channel: str | None = None) -> WaveformTrace:
1086
- """Build and return as WaveformTrace for direct use with Oscura.
948
+ def build_channels(self) -> dict[str, WaveformTrace]:
949
+ """Build and return all channels as dictionary of WaveformTraces.
1087
950
 
1088
- Args:
1089
- channel: Channel to return as trace. If None, uses first channel.
951
+ Use this method when you need all channels from a multi-channel
952
+ signal generation.
1090
953
 
1091
954
  Returns:
1092
- WaveformTrace ready for Oscura analysis.
955
+ Dictionary mapping channel names to WaveformTrace objects.
956
+
957
+ Raises:
958
+ ValueError: If no signals have been added.
959
+
960
+ Example:
961
+ >>> builder = SignalBuilder(sample_rate=1e6)
962
+ >>> builder.add_sine(1000, channel="sig")
963
+ >>> builder.add_square(500, channel="clk")
964
+ >>> channels = builder.build_channels()
965
+ >>> print(f"Generated {len(channels)} channels")
966
+ >>> sig_trace = channels["sig"]
967
+ >>> clk_trace = channels["clk"]
1093
968
  """
1094
- signal = self.build()
1095
- return signal.to_trace(channel)
969
+ if not self._channels:
970
+ raise ValueError("No signals added. Call add_* methods before build().")
971
+
972
+ # Ensure all channels have same length (pad if necessary)
973
+ max_len = max(len(s) for s in self._channels.values())
974
+ for name, signal in self._channels.items():
975
+ if len(signal) < max_len:
976
+ self._channels[name] = np.pad(signal, (0, max_len - len(signal)), mode="edge")
977
+
978
+ # Build WaveformTrace for each channel
979
+ traces: dict[str, WaveformTrace] = {}
980
+ for channel_name, data in self._channels.items():
981
+ trace_metadata = TraceMetadata(
982
+ sample_rate=self._sample_rate,
983
+ channel_name=channel_name,
984
+ )
985
+ traces[channel_name] = WaveformTrace(data=data, metadata=trace_metadata)
986
+
987
+ return traces
1096
988
 
1097
- def save_npz(self, path: Path | str) -> GeneratedSignal:
989
+ def save_npz(self, path: Path | str, channel: str | None = None) -> WaveformTrace:
1098
990
  """Build and save signal to NPZ format.
1099
991
 
1100
992
  Args:
1101
993
  path: Output file path.
994
+ channel: Channel to save (for multi-channel signals).
1102
995
 
1103
996
  Returns:
1104
- GeneratedSignal that was saved.
997
+ WaveformTrace that was saved.
998
+
999
+ Example:
1000
+ >>> builder = SignalBuilder().sample_rate(1e6).add_sine(1000)
1001
+ >>> trace = builder.save_npz("signal.npz")
1105
1002
  """
1106
- signal = self.build()
1107
- signal.save_npz(path)
1108
- return signal
1003
+ trace = self.build(channel=channel)
1004
+
1005
+ # Save as NPZ using numpy
1006
+ path = Path(path)
1007
+ path.parent.mkdir(parents=True, exist_ok=True)
1008
+
1009
+ np.savez_compressed(
1010
+ path,
1011
+ data=trace.data,
1012
+ sample_rate=trace.metadata.sample_rate,
1013
+ channel_name=trace.metadata.channel_name or "ch1",
1014
+ )
1015
+
1016
+ return trace
1109
1017
 
1110
1018
  # ========== Internal Methods ==========
1111
1019
 
oscura/core/exceptions.py CHANGED
@@ -283,6 +283,7 @@ class InsufficientDataError(AnalysisError):
283
283
  required: int | None = None,
284
284
  available: int | None = None,
285
285
  analysis_type: str | None = None,
286
+ fix_hint: str | None = None,
286
287
  ) -> None:
287
288
  """Initialize InsufficientDataError.
288
289
 
@@ -291,6 +292,7 @@ class InsufficientDataError(AnalysisError):
291
292
  required: Minimum number of samples or features required.
292
293
  available: Actual number available.
293
294
  analysis_type: Type of analysis that failed.
295
+ fix_hint: Optional custom fix suggestion (overrides default).
294
296
  """
295
297
  self.required = required
296
298
  self.available = available
@@ -301,7 +303,9 @@ class InsufficientDataError(AnalysisError):
301
303
  elif required is not None:
302
304
  details = f"Minimum required: {required}"
303
305
 
304
- fix_hint = "Acquire more data or reduce analysis window."
306
+ # Use default fix hint if not provided
307
+ if fix_hint is None:
308
+ fix_hint = "Acquire more data or reduce analysis window."
305
309
 
306
310
  super().__init__(
307
311
  message,
oscura/export/__init__.py CHANGED
@@ -19,7 +19,19 @@ Example:
19
19
 
20
20
  # Import main exports
21
21
  from . import wireshark
22
+ from .wavedrom import (
23
+ WaveDromBuilder,
24
+ WaveDromEdge,
25
+ WaveDromSignal,
26
+ export_wavedrom,
27
+ from_digital_trace,
28
+ )
22
29
 
23
30
  __all__ = [
31
+ "WaveDromBuilder",
32
+ "WaveDromEdge",
33
+ "WaveDromSignal",
34
+ "export_wavedrom",
35
+ "from_digital_trace",
24
36
  "wireshark",
25
37
  ]