oscura 0.6.0__py3-none-any.whl → 0.8.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 (38) hide show
  1. oscura/__init__.py +1 -1
  2. oscura/analyzers/eye/__init__.py +5 -1
  3. oscura/analyzers/eye/generation.py +501 -0
  4. oscura/analyzers/jitter/__init__.py +6 -6
  5. oscura/analyzers/jitter/timing.py +419 -0
  6. oscura/analyzers/patterns/__init__.py +28 -0
  7. oscura/analyzers/patterns/reverse_engineering.py +991 -0
  8. oscura/analyzers/power/__init__.py +35 -12
  9. oscura/analyzers/statistics/__init__.py +4 -0
  10. oscura/analyzers/statistics/basic.py +149 -0
  11. oscura/analyzers/statistics/correlation.py +47 -6
  12. oscura/analyzers/waveform/__init__.py +2 -0
  13. oscura/analyzers/waveform/measurements.py +145 -23
  14. oscura/analyzers/waveform/spectral.py +361 -8
  15. oscura/automotive/__init__.py +1 -1
  16. oscura/core/config/loader.py +0 -1
  17. oscura/core/types.py +108 -0
  18. oscura/loaders/__init__.py +12 -4
  19. oscura/loaders/tss.py +456 -0
  20. oscura/reporting/__init__.py +88 -1
  21. oscura/reporting/automation.py +348 -0
  22. oscura/reporting/citations.py +374 -0
  23. oscura/reporting/core.py +54 -0
  24. oscura/reporting/formatting/__init__.py +11 -0
  25. oscura/reporting/formatting/measurements.py +279 -0
  26. oscura/reporting/html.py +57 -0
  27. oscura/reporting/interpretation.py +431 -0
  28. oscura/reporting/summary.py +329 -0
  29. oscura/reporting/visualization.py +542 -0
  30. oscura/visualization/__init__.py +2 -1
  31. oscura/visualization/batch.py +521 -0
  32. oscura/workflows/__init__.py +2 -0
  33. oscura/workflows/waveform.py +783 -0
  34. {oscura-0.6.0.dist-info → oscura-0.8.0.dist-info}/METADATA +37 -19
  35. {oscura-0.6.0.dist-info → oscura-0.8.0.dist-info}/RECORD +38 -26
  36. {oscura-0.6.0.dist-info → oscura-0.8.0.dist-info}/WHEEL +0 -0
  37. {oscura-0.6.0.dist-info → oscura-0.8.0.dist-info}/entry_points.txt +0 -0
  38. {oscura-0.6.0.dist-info → oscura-0.8.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))