oscura 0.5.0__py3-none-any.whl → 0.5.1__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 (34) hide show
  1. oscura/__init__.py +1 -1
  2. oscura/analyzers/digital/__init__.py +0 -48
  3. oscura/analyzers/digital/extraction.py +0 -195
  4. oscura/analyzers/protocols/__init__.py +1 -22
  5. oscura/automotive/__init__.py +1 -1
  6. oscura/automotive/dtc/data.json +2763 -0
  7. oscura/export/__init__.py +0 -12
  8. oscura/export/wireshark/README.md +15 -15
  9. oscura/exporters/json_export.py +0 -47
  10. oscura/inference/active_learning/README.md +7 -7
  11. oscura/pipeline/composition.py +10 -2
  12. oscura/reporting/__init__.py +0 -7
  13. oscura/reporting/templates/index.md +13 -13
  14. oscura/schemas/bus_configuration.json +322 -0
  15. oscura/schemas/device_mapping.json +182 -0
  16. oscura/schemas/packet_format.json +418 -0
  17. oscura/schemas/protocol_definition.json +363 -0
  18. oscura/utils/autodetect.py +1 -5
  19. oscura-0.5.1.dist-info/METADATA +583 -0
  20. {oscura-0.5.0.dist-info → oscura-0.5.1.dist-info}/RECORD +23 -28
  21. oscura/analyzers/digital/ic_database.py +0 -498
  22. oscura/analyzers/digital/timing_paths.py +0 -339
  23. oscura/analyzers/digital/vintage.py +0 -377
  24. oscura/analyzers/digital/vintage_result.py +0 -148
  25. oscura/analyzers/protocols/parallel_bus.py +0 -449
  26. oscura/export/wavedrom.py +0 -430
  27. oscura/exporters/vintage_logic_csv.py +0 -247
  28. oscura/reporting/vintage_logic_report.py +0 -523
  29. oscura/visualization/digital_advanced.py +0 -718
  30. oscura/visualization/figure_manager.py +0 -156
  31. oscura-0.5.0.dist-info/METADATA +0 -407
  32. {oscura-0.5.0.dist-info → oscura-0.5.1.dist-info}/WHEEL +0 -0
  33. {oscura-0.5.0.dist-info → oscura-0.5.1.dist-info}/entry_points.txt +0 -0
  34. {oscura-0.5.0.dist-info → oscura-0.5.1.dist-info}/licenses/LICENSE +0 -0
oscura/export/wavedrom.py DELETED
@@ -1,430 +0,0 @@
1
- """WaveDrom timing diagram generation.
2
-
3
- Creates WaveDrom JSON format timing diagrams from digital signals.
4
- WaveDrom format can be rendered as SVG/PNG using wavedrom-cli or online tools.
5
-
6
- Example:
7
- >>> from oscura.export.wavedrom import export_wavedrom, WaveDromBuilder
8
- >>> builder = WaveDromBuilder()
9
- >>> builder.add_clock("CLK", period=100e-9)
10
- >>> builder.add_signal("DATA", edges=[10e-9, 50e-9, 150e-9])
11
- >>> builder.add_arrow(10e-9, 40e-9, "t_su = 30ns")
12
- >>> json_output = builder.to_json()
13
- >>> export_wavedrom(json_output, "timing_diagram.json")
14
- """
15
-
16
- from __future__ import annotations
17
-
18
- import json
19
- from dataclasses import dataclass
20
- from pathlib import Path
21
- from typing import TYPE_CHECKING, Any, Literal
22
-
23
- import numpy as np
24
-
25
- if TYPE_CHECKING:
26
- from oscura.core.types import DigitalTrace, WaveformTrace
27
-
28
-
29
- @dataclass
30
- class WaveDromSignal:
31
- """A WaveDrom signal definition.
32
-
33
- Attributes:
34
- name: Signal name.
35
- wave: Wave string (WaveDrom format).
36
- data: Optional data labels.
37
- node: Optional node markers for arrows.
38
- """
39
-
40
- name: str
41
- wave: str
42
- data: list[str] | None = None
43
- node: str | None = None
44
-
45
-
46
- @dataclass
47
- class WaveDromEdge:
48
- """A WaveDrom edge/arrow annotation.
49
-
50
- Attributes:
51
- from_node: Source node name.
52
- to_node: Destination node name.
53
- label: Arrow label text.
54
- style: Arrow style.
55
- """
56
-
57
- from_node: str
58
- to_node: str
59
- label: str
60
- style: str = ""
61
-
62
-
63
- class WaveDromBuilder:
64
- """Builder for WaveDrom timing diagrams.
65
-
66
- Example:
67
- >>> builder = WaveDromBuilder(title="74LS74 Setup Time")
68
- >>> builder.add_clock("CLK", period=100e-9, start_time=0)
69
- >>> builder.add_signal("D", edges=[10e-9, 50e-9])
70
- >>> builder.add_arrow(10e-9, 40e-9, "t_su = 30ns")
71
- >>> json_str = builder.to_json()
72
- """
73
-
74
- def __init__(
75
- self,
76
- *,
77
- title: str | None = None,
78
- time_scale: float = 1e-9, # Default to nanoseconds
79
- ):
80
- """Initialize WaveDrom builder.
81
-
82
- Args:
83
- title: Optional diagram title.
84
- time_scale: Time scale for signal conversion (default 1ns).
85
- """
86
- self.title = title
87
- self.time_scale = time_scale
88
- self.signals: list[WaveDromSignal] = []
89
- self.edges: list[WaveDromEdge] = []
90
- self.config: dict[str, Any] = {"hscale": 2, "skin": "narrow"}
91
- self._time_offset = 0.0
92
- self._time_end = 0.0
93
-
94
- def add_clock(
95
- self,
96
- name: str,
97
- *,
98
- period: float,
99
- start_time: float = 0.0,
100
- duty_cycle: float = 0.5,
101
- initial_state: Literal["high", "low"] = "low",
102
- duration: float | None = None,
103
- ) -> None:
104
- """Add a clock signal.
105
-
106
- Args:
107
- name: Signal name.
108
- period: Clock period in seconds.
109
- start_time: Start time in seconds.
110
- duty_cycle: Duty cycle (0.0-1.0).
111
- initial_state: Initial clock state.
112
- duration: Optional duration (defaults to auto-calculated).
113
- """
114
- if duration is None:
115
- duration = max(self._time_end - start_time, period * 10)
116
-
117
- # Convert to time steps
118
- time_steps = int((start_time - self._time_offset) / self.time_scale)
119
- num_periods = int(duration / period)
120
-
121
- # Build wave string
122
- wave = "." * time_steps # Leading dots
123
-
124
- if initial_state == "low":
125
- wave += "p" * num_periods # pulsing clock
126
- else:
127
- wave += "n" * num_periods # inverted pulsing clock
128
-
129
- self.signals.append(WaveDromSignal(name=name, wave=wave))
130
- self._time_end = max(self._time_end, start_time + duration)
131
-
132
- def add_signal(
133
- self,
134
- name: str,
135
- *,
136
- edges: list[float] | None = None,
137
- wave_string: str | None = None,
138
- data: list[str] | None = None,
139
- nodes: str | None = None,
140
- ) -> None:
141
- """Add a digital signal.
142
-
143
- Args:
144
- name: Signal name.
145
- edges: List of edge timestamps (rising/falling alternating).
146
- wave_string: Direct WaveDrom wave string (overrides edges).
147
- data: Optional data labels.
148
- nodes: Optional node markers (e.g., "A.B.C").
149
- """
150
- if wave_string is not None:
151
- # Use direct wave string
152
- self.signals.append(WaveDromSignal(name=name, wave=wave_string, data=data, node=nodes))
153
- return
154
-
155
- if edges is None:
156
- raise ValueError("Must provide either edges or wave_string")
157
-
158
- # Convert edges to wave string
159
- wave = self._edges_to_wave(edges)
160
- self.signals.append(WaveDromSignal(name=name, wave=wave, data=data, node=nodes))
161
-
162
- def add_data_bus(
163
- self,
164
- name: str,
165
- *,
166
- transitions: list[tuple[float, str]],
167
- initial_value: str = "x",
168
- ) -> None:
169
- """Add a data bus with labeled values.
170
-
171
- Args:
172
- name: Bus name.
173
- transitions: List of (timestamp, value) tuples.
174
- initial_value: Initial bus value.
175
- """
176
- if not transitions:
177
- raise ValueError("Must provide at least one transition")
178
-
179
- # Sort by timestamp
180
- transitions_sorted = sorted(transitions, key=lambda x: x[0])
181
-
182
- # Build wave string and data labels
183
- wave = ""
184
- data: list[str] = []
185
- current_time = self._time_offset
186
-
187
- for timestamp, value in transitions_sorted:
188
- # Add stable periods
189
- steps = int((timestamp - current_time) / self.time_scale)
190
- if steps > 0:
191
- wave += "." * (steps - 1)
192
-
193
- # Add transition
194
- wave += "="
195
- data.append(value)
196
- current_time = timestamp
197
-
198
- self.signals.append(WaveDromSignal(name=name, wave=wave, data=data))
199
-
200
- def add_arrow(
201
- self,
202
- from_time: float,
203
- to_time: float,
204
- label: str,
205
- *,
206
- from_signal_idx: int = 0,
207
- to_signal_idx: int = 0,
208
- ) -> None:
209
- """Add an arrow annotation between two time points.
210
-
211
- Args:
212
- from_time: Start time in seconds.
213
- to_time: End time in seconds.
214
- label: Arrow label text.
215
- from_signal_idx: Source signal index.
216
- to_signal_idx: Destination signal index.
217
- """
218
- # Create node markers
219
- from_node = f"A{len(self.edges)}"
220
- to_node = f"B{len(self.edges)}"
221
-
222
- # Add nodes to signals (simplified - would need to track positions)
223
- self.edges.append(WaveDromEdge(from_node=from_node, to_node=to_node, label=label))
224
-
225
- def add_group(self, name: str, signals: list[WaveDromSignal]) -> None:
226
- """Add a group of signals.
227
-
228
- Args:
229
- name: Group name.
230
- signals: List of signals in group.
231
- """
232
- # WaveDrom groups are represented as nested lists
233
- # This is a simplified implementation
234
-
235
- def _edges_to_wave(self, edges: list[float]) -> str:
236
- """Convert edge timestamps to WaveDrom wave string.
237
-
238
- Args:
239
- edges: List of edge timestamps.
240
-
241
- Returns:
242
- WaveDrom wave string.
243
- """
244
- if not edges:
245
- return "0"
246
-
247
- wave = ""
248
- current_time = self._time_offset
249
- state = 0 # Start low
250
-
251
- for edge_time in sorted(edges):
252
- # Calculate steps to this edge
253
- steps = int((edge_time - current_time) / self.time_scale)
254
-
255
- # Add stable period
256
- if steps > 0:
257
- wave += "." * (steps - 1)
258
-
259
- # Add transition
260
- if state == 0:
261
- wave += "1" # Rising edge
262
- state = 1
263
- else:
264
- wave += "0" # Falling edge
265
- state = 0
266
-
267
- current_time = edge_time
268
-
269
- return wave if wave else "0"
270
-
271
- def to_dict(self) -> dict[str, Any]:
272
- """Export to WaveDrom dictionary.
273
-
274
- Returns:
275
- Dictionary in WaveDrom JSON format.
276
- """
277
- result: dict[str, Any] = {}
278
-
279
- if self.title:
280
- result["head"] = {"text": self.title}
281
-
282
- # Build signal list
283
- signal_list: list[dict[str, Any]] = []
284
- for sig in self.signals:
285
- sig_dict: dict[str, Any] = {"name": sig.name, "wave": sig.wave}
286
- if sig.data:
287
- sig_dict["data"] = sig.data
288
- if sig.node:
289
- sig_dict["node"] = sig.node
290
- signal_list.append(sig_dict)
291
-
292
- result["signal"] = signal_list
293
-
294
- # Add edges if any
295
- if self.edges:
296
- edge_list = [f"{e.from_node}{e.style}>{e.to_node} {e.label}" for e in self.edges]
297
- result["edge"] = edge_list
298
-
299
- # Add configuration
300
- if self.config:
301
- result["config"] = self.config
302
-
303
- return result
304
-
305
- def to_json(self, *, indent: int = 2) -> str:
306
- """Export to WaveDrom JSON string.
307
-
308
- Args:
309
- indent: JSON indentation level.
310
-
311
- Returns:
312
- JSON string.
313
- """
314
- return json.dumps(self.to_dict(), indent=indent)
315
-
316
- def save(self, filepath: str | Path) -> None:
317
- """Save to file.
318
-
319
- Args:
320
- filepath: Output file path.
321
- """
322
- filepath = Path(filepath)
323
- with filepath.open("w") as f:
324
- f.write(self.to_json())
325
-
326
-
327
- def from_digital_trace(
328
- trace: DigitalTrace,
329
- *,
330
- name: str = "signal",
331
- start_time: float = 0.0,
332
- duration: float | None = None,
333
- ) -> WaveDromSignal:
334
- """Create WaveDrom signal from DigitalTrace.
335
-
336
- Args:
337
- trace: Input digital trace.
338
- name: Signal name.
339
- start_time: Start time offset.
340
- duration: Optional duration limit.
341
-
342
- Returns:
343
- WaveDromSignal object.
344
- """
345
- # Extract edges from digital trace
346
- data = trace.data
347
- transitions = np.diff(data.astype(np.int8))
348
-
349
- edges: list[float] = []
350
- time_base = trace.metadata.time_base
351
-
352
- for i, trans in enumerate(transitions):
353
- if trans != 0:
354
- edges.append((i + 1) * time_base + start_time)
355
-
356
- # Limit duration if specified
357
- if duration is not None:
358
- edges = [e for e in edges if e < start_time + duration]
359
-
360
- # Build wave string
361
- builder = WaveDromBuilder(time_scale=time_base)
362
- builder.add_signal(name, edges=edges)
363
-
364
- return builder.signals[0]
365
-
366
-
367
- def export_wavedrom(
368
- signals: dict[str, WaveformTrace | DigitalTrace],
369
- filepath: str | Path,
370
- *,
371
- title: str | None = None,
372
- time_scale: float = 1e-9,
373
- annotations: list[tuple[float, float, str]] | None = None,
374
- ) -> None:
375
- """Export signals to WaveDrom JSON file.
376
-
377
- Args:
378
- signals: Dictionary mapping signal names to traces.
379
- filepath: Output file path.
380
- title: Optional diagram title.
381
- time_scale: Time scale for conversion (default 1ns).
382
- annotations: Optional list of (from_time, to_time, label) tuples.
383
-
384
- Example:
385
- >>> signals = {
386
- ... "CLK": clock_trace,
387
- ... "DATA": data_trace,
388
- ... "CS": cs_trace,
389
- ... }
390
- >>> export_wavedrom(signals, "timing.json", title="SPI Transaction")
391
- """
392
- builder = WaveDromBuilder(title=title, time_scale=time_scale)
393
-
394
- # Add signals
395
- for name, trace in signals.items():
396
- # Detect if clock-like
397
- from oscura.analyzers.waveform.measurements import frequency
398
- from oscura.core.types import WaveformTrace
399
-
400
- # frequency() only works with WaveformTrace, skip for DigitalTrace
401
- if isinstance(trace, WaveformTrace):
402
- freq = frequency(trace)
403
- else:
404
- freq = np.nan
405
- if not np.isnan(freq) and freq > 0:
406
- # Looks like a clock
407
- period = float(1.0 / freq)
408
- builder.add_clock(name, period=period)
409
- else:
410
- # Regular signal - extract edges
411
- from oscura.analyzers.digital import detect_edges
412
-
413
- edges = detect_edges(trace)
414
- builder.add_signal(name, edges=list(edges))
415
-
416
- # Add annotations
417
- if annotations:
418
- for from_t, to_t, label in annotations:
419
- builder.add_arrow(from_t, to_t, label)
420
-
421
- builder.save(filepath)
422
-
423
-
424
- __all__ = [
425
- "WaveDromBuilder",
426
- "WaveDromEdge",
427
- "WaveDromSignal",
428
- "export_wavedrom",
429
- "from_digital_trace",
430
- ]
@@ -1,247 +0,0 @@
1
- """CSV export functions for vintage logic analysis results.
2
-
3
- This module provides specialized CSV exporters for vintage logic analysis data,
4
- including timing measurements, IC identification, and bill of materials.
5
-
6
- Example:
7
- >>> from oscura.exporters.vintage_logic_csv import export_bom_csv
8
- >>> export_bom_csv(result, "bom.csv")
9
- """
10
-
11
- from __future__ import annotations
12
-
13
- import csv
14
- from pathlib import Path
15
- from typing import TYPE_CHECKING
16
-
17
- if TYPE_CHECKING:
18
- from oscura.analyzers.digital.vintage_result import VintageLogicAnalysisResult
19
-
20
-
21
- def export_timing_measurements_csv(
22
- result: VintageLogicAnalysisResult,
23
- path: str | Path,
24
- ) -> None:
25
- """Export timing measurements to CSV.
26
-
27
- Creates a CSV file with columns: parameter, measured_value_ns, measurement_type.
28
-
29
- Args:
30
- result: Vintage logic analysis result.
31
- path: Output CSV file path.
32
-
33
- Example:
34
- >>> export_timing_measurements_csv(result, "timing.csv")
35
- """
36
- path = Path(path)
37
-
38
- with path.open("w", newline="") as csvfile:
39
- writer = csv.writer(csvfile)
40
-
41
- # Write header
42
- writer.writerow(["parameter", "measured_value_ns", "measurement_type"])
43
-
44
- # Write timing measurements
45
- for param_name, value in result.timing_measurements.items():
46
- # Determine measurement type from parameter name
47
- if "_t_pd" in param_name:
48
- meas_type = "propagation_delay"
49
- elif "_t_su" in param_name:
50
- meas_type = "setup_time"
51
- elif "_t_h" in param_name:
52
- meas_type = "hold_time"
53
- elif "_t_w" in param_name:
54
- meas_type = "pulse_width"
55
- else:
56
- meas_type = "other"
57
-
58
- writer.writerow([param_name, f"{value * 1e9:.3f}", meas_type])
59
-
60
-
61
- def export_ic_identification_csv(
62
- result: VintageLogicAnalysisResult,
63
- path: str | Path,
64
- ) -> None:
65
- """Export IC identification results to CSV.
66
-
67
- Creates a CSV file with columns: ic_name, confidence, family, timing_params,
68
- validation_status.
69
-
70
- Args:
71
- result: Vintage logic analysis result.
72
- path: Output CSV file path.
73
-
74
- Example:
75
- >>> export_ic_identification_csv(result, "ic_identification.csv")
76
- """
77
- path = Path(path)
78
-
79
- with path.open("w", newline="") as csvfile:
80
- writer = csv.writer(csvfile)
81
-
82
- # Write header
83
- writer.writerow(
84
- [
85
- "ic_name",
86
- "confidence",
87
- "family",
88
- "t_pd_ns",
89
- "t_su_ns",
90
- "t_h_ns",
91
- "t_w_ns",
92
- "validation_status",
93
- ]
94
- )
95
-
96
- # Write IC identification results
97
- for ic_result in result.identified_ics:
98
- # Extract timing parameters
99
- t_pd = ic_result.timing_params.get("t_pd", 0) * 1e9
100
- t_su = ic_result.timing_params.get("t_su", 0) * 1e9
101
- t_h = ic_result.timing_params.get("t_h", 0) * 1e9
102
- t_w = ic_result.timing_params.get("t_w", 0) * 1e9
103
-
104
- # Determine validation status
105
- validation_failed = any(v.get("passes") is False for v in ic_result.validation.values())
106
- validation_status = "FAIL" if validation_failed else "PASS"
107
-
108
- writer.writerow(
109
- [
110
- ic_result.ic_name,
111
- f"{ic_result.confidence:.3f}",
112
- ic_result.family,
113
- f"{t_pd:.3f}" if t_pd > 0 else "",
114
- f"{t_su:.3f}" if t_su > 0 else "",
115
- f"{t_h:.3f}" if t_h > 0 else "",
116
- f"{t_w:.3f}" if t_w > 0 else "",
117
- validation_status,
118
- ]
119
- )
120
-
121
-
122
- def export_bom_csv(
123
- result: VintageLogicAnalysisResult,
124
- path: str | Path,
125
- ) -> None:
126
- """Export bill of materials to CSV.
127
-
128
- Creates a CSV file compatible with spreadsheet programs and procurement systems.
129
- Columns: part_number, description, quantity, category, notes.
130
-
131
- Args:
132
- result: Vintage logic analysis result.
133
- path: Output CSV file path.
134
-
135
- Example:
136
- >>> export_bom_csv(result, "bom.csv")
137
- """
138
- path = Path(path)
139
-
140
- with path.open("w", newline="") as csvfile:
141
- writer = csv.writer(csvfile)
142
-
143
- # Write header
144
- writer.writerow(["part_number", "description", "quantity", "category", "notes"])
145
-
146
- # Write BOM entries
147
- for entry in result.bom:
148
- writer.writerow(
149
- [
150
- entry.part_number,
151
- entry.description,
152
- entry.quantity,
153
- entry.category,
154
- entry.notes or "",
155
- ]
156
- )
157
-
158
-
159
- def export_voltage_levels_csv(
160
- result: VintageLogicAnalysisResult,
161
- path: str | Path,
162
- ) -> None:
163
- """Export voltage levels to CSV.
164
-
165
- Creates a CSV file with measured voltage levels for the detected logic family.
166
-
167
- Args:
168
- result: Vintage logic analysis result.
169
- path: Output CSV file path.
170
-
171
- Example:
172
- >>> export_voltage_levels_csv(result, "voltage_levels.csv")
173
- """
174
- path = Path(path)
175
-
176
- with path.open("w", newline="") as csvfile:
177
- writer = csv.writer(csvfile)
178
-
179
- # Write header
180
- writer.writerow(["parameter", "voltage_v", "logic_family"])
181
-
182
- # Write voltage levels
183
- for param, value in result.voltage_levels.items():
184
- writer.writerow([param, f"{value:.3f}", result.detected_family])
185
-
186
-
187
- def export_all_vintage_logic_csv(
188
- result: VintageLogicAnalysisResult,
189
- output_dir: str | Path,
190
- *,
191
- prefix: str = "",
192
- ) -> dict[str, Path]:
193
- """Export all vintage logic analysis data to CSV files.
194
-
195
- Convenience function that exports all data types to separate CSV files.
196
-
197
- Args:
198
- result: Vintage logic analysis result.
199
- output_dir: Output directory for CSV files.
200
- prefix: Optional prefix for file names.
201
-
202
- Returns:
203
- Dictionary mapping data type to output file path.
204
-
205
- Example:
206
- >>> paths = export_all_vintage_logic_csv(result, "./output", prefix="analysis_")
207
- >>> print(paths["bom"]) # PosixPath('./output/analysis_bom.csv')
208
- """
209
- output_dir = Path(output_dir)
210
- output_dir.mkdir(parents=True, exist_ok=True)
211
-
212
- paths: dict[str, Path] = {}
213
-
214
- # Export timing measurements
215
- if result.timing_measurements:
216
- timing_path = output_dir / f"{prefix}timing_measurements.csv"
217
- export_timing_measurements_csv(result, timing_path)
218
- paths["timing_measurements"] = timing_path
219
-
220
- # Export IC identification
221
- if result.identified_ics:
222
- ic_path = output_dir / f"{prefix}ic_identification.csv"
223
- export_ic_identification_csv(result, ic_path)
224
- paths["ic_identification"] = ic_path
225
-
226
- # Export BOM
227
- if result.bom:
228
- bom_path = output_dir / f"{prefix}bom.csv"
229
- export_bom_csv(result, bom_path)
230
- paths["bom"] = bom_path
231
-
232
- # Export voltage levels
233
- if result.voltage_levels:
234
- voltage_path = output_dir / f"{prefix}voltage_levels.csv"
235
- export_voltage_levels_csv(result, voltage_path)
236
- paths["voltage_levels"] = voltage_path
237
-
238
- return paths
239
-
240
-
241
- __all__ = [
242
- "export_all_vintage_logic_csv",
243
- "export_bom_csv",
244
- "export_ic_identification_csv",
245
- "export_timing_measurements_csv",
246
- "export_voltage_levels_csv",
247
- ]