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
@@ -0,0 +1,329 @@
1
+ """Executive summary and key findings generation.
2
+
3
+ This module provides automated generation of executive summaries,
4
+ measurement summaries, and key findings identification for reports.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from typing import Any
11
+
12
+ from oscura.reporting.interpretation import MeasurementInterpretation, QualityLevel
13
+
14
+
15
+ @dataclass
16
+ class ExecutiveSummarySection:
17
+ """A section in an executive summary.
18
+
19
+ Attributes:
20
+ title: Section title.
21
+ content: Section content.
22
+ bullet_points: List of key bullet points.
23
+ priority: Priority level (1=highest).
24
+ """
25
+
26
+ title: str
27
+ content: str = ""
28
+ bullet_points: list[str] = field(default_factory=list)
29
+ priority: int = 1
30
+
31
+
32
+ def generate_executive_summary(
33
+ measurements: dict[str, Any],
34
+ interpretations: dict[str, MeasurementInterpretation] | None = None,
35
+ max_findings: int = 5,
36
+ ) -> str:
37
+ """Generate executive summary from measurements.
38
+
39
+ Args:
40
+ measurements: Dictionary of measurement results.
41
+ interpretations: Optional interpretations of measurements.
42
+ max_findings: Maximum number of key findings to include.
43
+
44
+ Returns:
45
+ Executive summary text.
46
+
47
+ Example:
48
+ >>> measurements = {"snr": 45.2, "thd": -60.5, "bandwidth": 1e9}
49
+ >>> summary = generate_executive_summary(measurements)
50
+ >>> "Executive Summary" in summary or len(summary) > 0
51
+ True
52
+ """
53
+ sections = []
54
+
55
+ # Overall status
56
+ status_section = _generate_status_section(measurements, interpretations)
57
+ sections.append(status_section)
58
+
59
+ # Key findings
60
+ findings_section = _generate_findings_section(measurements, interpretations, max_findings)
61
+ sections.append(findings_section)
62
+
63
+ # Recommendations
64
+ if interpretations:
65
+ rec_section = _generate_recommendations_section(interpretations)
66
+ if rec_section.bullet_points:
67
+ sections.append(rec_section)
68
+
69
+ # Format as text
70
+ lines = ["# Executive Summary", ""]
71
+
72
+ for section in sections:
73
+ lines.append(f"## {section.title}")
74
+ lines.append("")
75
+
76
+ if section.content:
77
+ lines.append(section.content)
78
+ lines.append("")
79
+
80
+ if section.bullet_points:
81
+ for point in section.bullet_points:
82
+ lines.append(f"- {point}")
83
+ lines.append("")
84
+
85
+ return "\n".join(lines)
86
+
87
+
88
+ def _generate_status_section(
89
+ measurements: dict[str, Any],
90
+ interpretations: dict[str, MeasurementInterpretation] | None,
91
+ ) -> ExecutiveSummarySection:
92
+ """Generate overall status section."""
93
+ total = len(measurements)
94
+
95
+ if interpretations:
96
+ excellent = sum(1 for i in interpretations.values() if i.quality == QualityLevel.EXCELLENT)
97
+ good = sum(1 for i in interpretations.values() if i.quality == QualityLevel.GOOD)
98
+ acceptable = sum(
99
+ 1 for i in interpretations.values() if i.quality == QualityLevel.ACCEPTABLE
100
+ )
101
+ marginal = sum(1 for i in interpretations.values() if i.quality == QualityLevel.MARGINAL)
102
+ poor = sum(1 for i in interpretations.values() if i.quality == QualityLevel.POOR)
103
+ failed = sum(1 for i in interpretations.values() if i.quality == QualityLevel.FAILED)
104
+
105
+ if failed > 0:
106
+ overall = "CRITICAL"
107
+ content = f"{failed} of {total} measurements failed critical requirements."
108
+ elif marginal > total / 2:
109
+ overall = "MARGINAL"
110
+ content = f"{marginal} of {total} measurements are marginal."
111
+ elif excellent + good > total * 0.7:
112
+ overall = "GOOD"
113
+ content = f"Signal quality is good: {excellent + good} of {total} measurements are excellent or good."
114
+ else:
115
+ overall = "ACCEPTABLE"
116
+ content = f"{acceptable} of {total} measurements are acceptable."
117
+
118
+ bullet_points = [
119
+ f"Excellent: {excellent}",
120
+ f"Good: {good}",
121
+ f"Acceptable: {acceptable}",
122
+ f"Marginal: {marginal}",
123
+ f"Poor: {poor}",
124
+ f"Failed: {failed}",
125
+ ]
126
+ else:
127
+ overall = "COMPLETE"
128
+ content = f"Analysis complete with {total} measurements."
129
+ bullet_points = []
130
+
131
+ return ExecutiveSummarySection(
132
+ title=f"Overall Status: {overall}",
133
+ content=content,
134
+ bullet_points=bullet_points,
135
+ priority=1,
136
+ )
137
+
138
+
139
+ def _generate_findings_section(
140
+ measurements: dict[str, Any],
141
+ interpretations: dict[str, MeasurementInterpretation] | None,
142
+ max_findings: int,
143
+ ) -> ExecutiveSummarySection:
144
+ """Generate key findings section."""
145
+ findings = identify_key_findings(measurements, interpretations, max_findings)
146
+
147
+ return ExecutiveSummarySection(
148
+ title="Key Findings",
149
+ content="",
150
+ bullet_points=findings,
151
+ priority=2,
152
+ )
153
+
154
+
155
+ def _generate_recommendations_section(
156
+ interpretations: dict[str, MeasurementInterpretation],
157
+ ) -> ExecutiveSummarySection:
158
+ """Generate recommendations section."""
159
+ all_recommendations = []
160
+
161
+ for interp in interpretations.values():
162
+ all_recommendations.extend(interp.recommendations)
163
+
164
+ # Deduplicate
165
+ unique_recs = list(dict.fromkeys(all_recommendations))
166
+
167
+ return ExecutiveSummarySection(
168
+ title="Recommendations",
169
+ content="",
170
+ bullet_points=unique_recs[:5], # Top 5
171
+ priority=3,
172
+ )
173
+
174
+
175
+ def summarize_measurements(
176
+ measurements: dict[str, Any],
177
+ group_by: str | None = None,
178
+ ) -> dict[str, Any]:
179
+ """Summarize measurements with statistics.
180
+
181
+ Args:
182
+ measurements: Dictionary of measurements.
183
+ group_by: Optional grouping key (e.g., "domain", "type").
184
+
185
+ Returns:
186
+ Summary dictionary with statistics.
187
+
188
+ Example:
189
+ >>> measurements = {"snr": 45.2, "thd": -60.5, "bandwidth": 1e9}
190
+ >>> summary = summarize_measurements(measurements)
191
+ >>> summary["count"]
192
+ 3
193
+ """
194
+ numeric_values = [v for v in measurements.values() if isinstance(v, int | float)]
195
+
196
+ summary: dict[str, Any] = {
197
+ "count": len(measurements),
198
+ "numeric_count": len(numeric_values),
199
+ }
200
+
201
+ if numeric_values:
202
+ import numpy as np
203
+
204
+ summary.update(
205
+ {
206
+ "mean": float(np.mean(numeric_values)),
207
+ "median": float(np.median(numeric_values)),
208
+ "std": float(np.std(numeric_values)),
209
+ "min": float(np.min(numeric_values)),
210
+ "max": float(np.max(numeric_values)),
211
+ }
212
+ )
213
+
214
+ return summary
215
+
216
+
217
+ def identify_key_findings(
218
+ measurements: dict[str, Any],
219
+ interpretations: dict[str, MeasurementInterpretation] | None = None,
220
+ max_findings: int = 5,
221
+ ) -> list[str]:
222
+ """Automatically identify key findings from measurements.
223
+
224
+ Args:
225
+ measurements: Dictionary of measurements.
226
+ interpretations: Optional interpretations.
227
+ max_findings: Maximum number of findings to return.
228
+
229
+ Returns:
230
+ List of key finding strings.
231
+
232
+ Example:
233
+ >>> measurements = {"snr": 45.2, "bandwidth": 1e9}
234
+ >>> findings = identify_key_findings(measurements, max_findings=3)
235
+ >>> len(findings) <= 3
236
+ True
237
+ """
238
+ findings = []
239
+
240
+ # Check for exceptional values
241
+ if interpretations:
242
+ # Failed or poor quality
243
+ failed = [
244
+ name
245
+ for name, interp in interpretations.items()
246
+ if interp.quality in (QualityLevel.FAILED, QualityLevel.POOR)
247
+ ]
248
+ if failed:
249
+ findings.append(f"Critical: {len(failed)} measurements failed or poor quality")
250
+
251
+ # Excellent quality
252
+ excellent = [
253
+ name
254
+ for name, interp in interpretations.items()
255
+ if interp.quality == QualityLevel.EXCELLENT
256
+ ]
257
+ if excellent:
258
+ findings.append(f"{len(excellent)} measurements show excellent performance")
259
+
260
+ # Domain-specific findings
261
+ if "snr" in measurements:
262
+ snr = measurements["snr"]
263
+ if isinstance(snr, int | float):
264
+ if snr > 60:
265
+ findings.append(f"Excellent SNR: {snr:.1f} dB")
266
+ elif snr < 20:
267
+ findings.append(f"Low SNR: {snr:.1f} dB - noise mitigation recommended")
268
+
269
+ if "bandwidth" in measurements:
270
+ bw = measurements["bandwidth"]
271
+ if isinstance(bw, int | float):
272
+ if bw > 1e9:
273
+ findings.append(f"Wide bandwidth: {bw / 1e9:.2f} GHz")
274
+
275
+ if "jitter" in measurements or "rms_jitter" in measurements:
276
+ jitter_key = "rms_jitter" if "rms_jitter" in measurements else "jitter"
277
+ jitter = measurements[jitter_key]
278
+ if isinstance(jitter, int | float):
279
+ jitter_ps = jitter * 1e12
280
+ if jitter_ps < 10:
281
+ findings.append("Excellent timing: RMS jitter < 10 ps")
282
+ elif jitter_ps > 200:
283
+ findings.append(f"High jitter: {jitter_ps:.1f} ps - investigate timing issues")
284
+
285
+ # Limit to max_findings
286
+ return findings[:max_findings]
287
+
288
+
289
+ def recommendations_from_findings(
290
+ measurements: dict[str, Any],
291
+ interpretations: dict[str, MeasurementInterpretation] | None = None,
292
+ ) -> list[str]:
293
+ """Generate actionable recommendations from findings.
294
+
295
+ Args:
296
+ measurements: Dictionary of measurements.
297
+ interpretations: Optional interpretations.
298
+
299
+ Returns:
300
+ List of recommendation strings.
301
+
302
+ Example:
303
+ >>> from oscura.reporting.interpretation import interpret_measurement
304
+ >>> measurements = {"snr": 15.5}
305
+ >>> interpretations = {"snr": interpret_measurement("snr", 15.5, "dB")}
306
+ >>> recs = recommendations_from_findings(measurements, interpretations)
307
+ >>> len(recs) > 0
308
+ True
309
+ """
310
+ recommendations = []
311
+
312
+ if interpretations:
313
+ # Collect all recommendations from interpretations
314
+ for interp in interpretations.values():
315
+ recommendations.extend(interp.recommendations)
316
+
317
+ # Add domain-specific recommendations
318
+ if "snr" in measurements:
319
+ snr = measurements["snr"]
320
+ if isinstance(snr, int | float) and snr < 30:
321
+ recommendations.append("Investigate noise sources and consider filtering")
322
+
323
+ if "bandwidth" in measurements:
324
+ bw = measurements["bandwidth"]
325
+ if isinstance(bw, int | float) and bw < 100e6:
326
+ recommendations.append("Verify signal path bandwidth requirements")
327
+
328
+ # Deduplicate and return
329
+ return list(dict.fromkeys(recommendations))