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
@@ -18,6 +18,12 @@ from oscura.reporting.auto_report import (
18
18
  from oscura.reporting.auto_report import (
19
19
  generate_report as generate_auto_report,
20
20
  )
21
+ from oscura.reporting.automation import (
22
+ auto_interpret_results,
23
+ flag_anomalies,
24
+ identify_issues,
25
+ suggest_follow_up_analyses,
26
+ )
21
27
  from oscura.reporting.batch import (
22
28
  BatchReportResult,
23
29
  aggregate_batch_measurements,
@@ -30,6 +36,13 @@ from oscura.reporting.chart_selection import (
30
36
  get_axis_scaling,
31
37
  recommend_chart_with_reasoning,
32
38
  )
39
+ from oscura.reporting.citations import (
40
+ Citation,
41
+ CitationManager,
42
+ auto_cite_measurement,
43
+ get_standard_info,
44
+ list_available_standards,
45
+ )
33
46
  from oscura.reporting.comparison import (
34
47
  compare_waveforms,
35
48
  generate_comparison_report,
@@ -73,8 +86,12 @@ from oscura.reporting.export import (
73
86
  export_report,
74
87
  )
75
88
  from oscura.reporting.formatting import (
89
+ MeasurementFormatter,
76
90
  NumberFormatter,
91
+ convert_to_measurement_dict,
77
92
  format_margin,
93
+ format_measurement,
94
+ format_measurement_dict,
78
95
  format_pass_fail,
79
96
  format_value,
80
97
  format_with_context,
@@ -82,6 +99,7 @@ from oscura.reporting.formatting import (
82
99
  format_with_units,
83
100
  )
84
101
  from oscura.reporting.html import (
102
+ embed_plots,
85
103
  generate_html_report,
86
104
  save_html_report,
87
105
  )
@@ -89,6 +107,16 @@ from oscura.reporting.index import (
89
107
  IndexGenerator,
90
108
  TemplateEngine,
91
109
  )
110
+ from oscura.reporting.interpretation import (
111
+ ComplianceStatus,
112
+ MeasurementInterpretation,
113
+ QualityLevel,
114
+ compliance_check,
115
+ generate_finding,
116
+ interpret_measurement,
117
+ interpret_results_batch,
118
+ quality_score,
119
+ )
92
120
  from oscura.reporting.multichannel import (
93
121
  generate_multichannel_report,
94
122
  )
@@ -127,6 +155,12 @@ from oscura.reporting.standards import (
127
155
  format_executive_summary_html,
128
156
  generate_executive_summary,
129
157
  )
158
+ from oscura.reporting.summary import (
159
+ ExecutiveSummarySection,
160
+ identify_key_findings,
161
+ recommendations_from_findings,
162
+ summarize_measurements,
163
+ )
130
164
  from oscura.reporting.summary_generator import (
131
165
  Finding,
132
166
  Summary,
@@ -144,6 +178,10 @@ from oscura.reporting.template_system import (
144
178
  list_templates,
145
179
  load_template,
146
180
  )
181
+ from oscura.reporting.visualization import (
182
+ IEEEPlotGenerator,
183
+ PlotStyler,
184
+ )
147
185
 
148
186
  __all__ = [
149
187
  # Comprehensive Analysis Report API (CAR-001 through CAR-007)
@@ -160,30 +198,46 @@ __all__ = [
160
198
  "BatchReportResult",
161
199
  # Chart Selection (REPORT-028)
162
200
  "ChartType",
201
+ # Citations (NEW)
202
+ "Citation",
203
+ "CitationManager",
163
204
  # Standards (REPORT-001, REPORT-002, REPORT-004)
164
205
  "ColorScheme",
206
+ # Compliance (NEW)
207
+ "ComplianceStatus",
165
208
  "DataOutputConfig",
166
209
  "DomainConfig",
167
210
  # Enhanced Reports (Feature 6)
168
211
  "EnhancedReportConfig",
169
212
  "EnhancedReportGenerator",
170
213
  "ExecutiveSummary",
214
+ # Executive Summary (NEW)
215
+ "ExecutiveSummarySection",
171
216
  # Summary Generation
172
217
  "Finding",
173
218
  "FormatStandards",
219
+ # IEEE Visualization (NEW)
220
+ "IEEEPlotGenerator",
174
221
  "IndexGenerator",
175
222
  "InputType",
223
+ # Formatting (REPORT-026)
224
+ "MeasurementFormatter",
225
+ # Measurement Interpretation (NEW)
226
+ "MeasurementInterpretation",
176
227
  # Multi-format (REPORT-010)
177
228
  "MultiFormatRenderer",
178
- # Formatting (REPORT-026)
179
229
  "NumberFormatter",
180
230
  "OutputManager",
181
231
  # PPTX Export (REPORT-023)
182
232
  "PPTXPresentation",
183
233
  "PPTXSlide",
184
234
  "PlotGenerator",
235
+ # Plot Styling (NEW)
236
+ "PlotStyler",
185
237
  "ProgressCallback",
186
238
  "ProgressInfo",
239
+ # Quality Assessment (NEW)
240
+ "QualityLevel",
187
241
  # Core
188
242
  "Report",
189
243
  "ReportConfig",
@@ -199,12 +253,18 @@ __all__ = [
199
253
  "VisualEmphasis",
200
254
  "aggregate_batch_measurements",
201
255
  "analyze",
256
+ # Automation (NEW)
257
+ "auto_cite_measurement",
258
+ "auto_interpret_results",
202
259
  "auto_select_chart",
203
260
  # Export
204
261
  "batch_export_formats",
205
262
  "batch_report",
206
263
  # Comparison
207
264
  "compare_waveforms",
265
+ # Compliance (NEW)
266
+ "compliance_check",
267
+ "convert_to_measurement_dict",
208
268
  # Tables
209
269
  "create_comparison_table",
210
270
  # Sections
@@ -220,12 +280,18 @@ __all__ = [
220
280
  "create_violations_section",
221
281
  # Multi-format (REPORT-010)
222
282
  "detect_format_from_extension",
283
+ # HTML Generation
284
+ "embed_plots",
223
285
  "export_multiple_reports",
224
286
  "export_pptx",
225
287
  "export_report",
288
+ # Anomaly Detection (NEW)
289
+ "flag_anomalies",
226
290
  "format_batch_summary_table",
227
291
  "format_executive_summary_html",
228
292
  "format_margin",
293
+ "format_measurement",
294
+ "format_measurement_dict",
229
295
  "format_pass_fail",
230
296
  "format_value",
231
297
  "format_with_context",
@@ -235,6 +301,8 @@ __all__ = [
235
301
  "generate_batch_report",
236
302
  "generate_comparison_report",
237
303
  "generate_executive_summary",
304
+ # Findings (NEW)
305
+ "generate_finding",
238
306
  # HTML Generation
239
307
  "generate_html_report",
240
308
  # Multi-Channel
@@ -246,12 +314,31 @@ __all__ = [
246
314
  "generate_summary",
247
315
  "get_available_analyses",
248
316
  "get_axis_scaling",
317
+ # Citations (NEW)
318
+ "get_standard_info",
319
+ # Issue Identification (NEW)
320
+ "identify_issues",
321
+ # Key Findings (NEW)
322
+ "identify_key_findings",
323
+ # Interpretation (NEW)
324
+ "interpret_measurement",
325
+ "interpret_results_batch",
326
+ # Standards (NEW)
327
+ "list_available_standards",
249
328
  "list_templates",
250
329
  "load_template",
330
+ # Quality (NEW)
331
+ "quality_score",
251
332
  "recommend_chart_with_reasoning",
333
+ # Recommendations (NEW)
334
+ "recommendations_from_findings",
252
335
  "register_plot",
253
336
  # Multi-format (REPORT-010)
254
337
  "render_all_formats",
255
338
  "save_html_report",
256
339
  "save_pdf_report",
340
+ # Follow-up Analysis (NEW)
341
+ "suggest_follow_up_analyses",
342
+ # Summary (NEW)
343
+ "summarize_measurements",
257
344
  ]
@@ -0,0 +1,348 @@
1
+ """Automated analysis and anomaly detection for reports.
2
+
3
+ This module provides automated interpretation of results, anomaly flagging,
4
+ and intelligent recommendations generation.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ import numpy as np
12
+
13
+ from oscura.reporting.interpretation import MeasurementInterpretation, interpret_measurement
14
+ from oscura.reporting.summary import (
15
+ identify_key_findings,
16
+ recommendations_from_findings,
17
+ summarize_measurements,
18
+ )
19
+
20
+
21
+ def auto_interpret_results(results: dict[str, Any]) -> dict[str, MeasurementInterpretation]:
22
+ """Automatically interpret all results in a dictionary.
23
+
24
+ Args:
25
+ results: Dictionary of measurement results. Can be nested.
26
+
27
+ Returns:
28
+ Dictionary mapping measurement names to interpretations.
29
+
30
+ Example:
31
+ >>> results = {"snr": 45.2, "bandwidth": 1e9, "jitter": 50e-12}
32
+ >>> interpretations = auto_interpret_results(results)
33
+ >>> len(interpretations) >= 3
34
+ True
35
+ """
36
+ interpretations = {}
37
+
38
+ for key, value in results.items():
39
+ if isinstance(value, dict):
40
+ # Nested dictionary - recurse
41
+ if "value" in value:
42
+ # This is a measurement dict with value and metadata
43
+ meas_value = value["value"]
44
+ units = value.get("units", "")
45
+ spec_min = value.get("spec_min")
46
+ spec_max = value.get("spec_max")
47
+
48
+ interp = interpret_measurement(key, meas_value, units, spec_min, spec_max)
49
+ interpretations[key] = interp
50
+ else:
51
+ # Recurse into nested dict
52
+ nested = auto_interpret_results(value)
53
+ interpretations.update(nested)
54
+ elif isinstance(value, int | float):
55
+ # Simple numeric value
56
+ interp = interpret_measurement(key, value)
57
+ interpretations[key] = interp
58
+
59
+ return interpretations
60
+
61
+
62
+ def generate_summary(
63
+ results: dict[str, Any],
64
+ include_recommendations: bool = True,
65
+ ) -> dict[str, Any]:
66
+ """Generate comprehensive automated summary of results.
67
+
68
+ Args:
69
+ results: Dictionary of measurement results.
70
+ include_recommendations: Whether to include recommendations.
71
+
72
+ Returns:
73
+ Summary dictionary with statistics, findings, and recommendations.
74
+
75
+ Example:
76
+ >>> results = {"snr": 45.2, "thd": -60.5, "bandwidth": 1e9}
77
+ >>> summary = generate_summary(results)
78
+ >>> "statistics" in summary
79
+ True
80
+ >>> "key_findings" in summary
81
+ True
82
+ """
83
+ # Flatten results to simple dict
84
+ flat_results = _flatten_results(results)
85
+
86
+ # Generate statistics
87
+ statistics = summarize_measurements(flat_results)
88
+
89
+ # Interpret results
90
+ interpretations = auto_interpret_results(results)
91
+
92
+ # Identify key findings
93
+ key_findings = identify_key_findings(flat_results, interpretations, max_findings=10)
94
+
95
+ # Generate recommendations
96
+ recommendations = []
97
+ if include_recommendations:
98
+ recommendations = recommendations_from_findings(flat_results, interpretations)
99
+
100
+ # Flag anomalies
101
+ anomalies = flag_anomalies(flat_results)
102
+
103
+ return {
104
+ "statistics": statistics,
105
+ "key_findings": key_findings,
106
+ "recommendations": recommendations,
107
+ "anomalies": anomalies,
108
+ "interpretations": {k: v.__dict__ for k, v in interpretations.items()},
109
+ }
110
+
111
+
112
+ def flag_anomalies(
113
+ measurements: dict[str, Any],
114
+ threshold_std: float = 3.0,
115
+ ) -> list[dict[str, Any]]:
116
+ """Flag anomalous measurements using statistical analysis.
117
+
118
+ Args:
119
+ measurements: Dictionary of measurements.
120
+ threshold_std: Number of standard deviations for outlier detection.
121
+
122
+ Returns:
123
+ List of anomaly dictionaries with name, value, and reason.
124
+
125
+ Example:
126
+ >>> measurements = {"m1": 10, "m2": 11, "m3": 12, "m4": 100}
127
+ >>> anomalies = flag_anomalies(measurements, threshold_std=2.0)
128
+ >>> len(anomalies) >= 1
129
+ True
130
+ """
131
+ anomalies: list[dict[str, Any]] = []
132
+
133
+ # Extract numeric values
134
+ numeric_measurements = {k: v for k, v in measurements.items() if isinstance(v, int | float)}
135
+
136
+ if len(numeric_measurements) < 3:
137
+ # Not enough data for statistical analysis
138
+ return anomalies
139
+
140
+ values = np.array(list(numeric_measurements.values()))
141
+ mean = np.mean(values)
142
+ std = np.std(values)
143
+
144
+ if std == 0:
145
+ return anomalies
146
+
147
+ # Check each measurement
148
+ for name, value in numeric_measurements.items():
149
+ z_score = abs((value - mean) / std)
150
+
151
+ if z_score > threshold_std:
152
+ anomalies.append(
153
+ {
154
+ "name": name,
155
+ "value": value,
156
+ "z_score": float(z_score),
157
+ "reason": f"Value is {z_score:.1f} std devs from mean ({mean:.3e})",
158
+ "severity": "high" if z_score > 5 else "medium",
159
+ }
160
+ )
161
+
162
+ # Domain-specific anomaly checks
163
+ for name, value in measurements.items():
164
+ if not isinstance(value, int | float):
165
+ continue
166
+
167
+ # Negative SNR
168
+ if "snr" in name.lower() and value < 0:
169
+ anomalies.append(
170
+ {
171
+ "name": name,
172
+ "value": value,
173
+ "reason": "Negative SNR indicates signal weaker than noise",
174
+ "severity": "critical",
175
+ }
176
+ )
177
+
178
+ # Negative bandwidth
179
+ if "bandwidth" in name.lower() and value <= 0:
180
+ anomalies.append(
181
+ {
182
+ "name": name,
183
+ "value": value,
184
+ "reason": "Invalid negative or zero bandwidth",
185
+ "severity": "critical",
186
+ }
187
+ )
188
+
189
+ # Extremely high jitter (>1 second)
190
+ if "jitter" in name.lower() and value > 1.0:
191
+ anomalies.append(
192
+ {
193
+ "name": name,
194
+ "value": value,
195
+ "reason": "Unrealistically high jitter value (>1 second)",
196
+ "severity": "critical",
197
+ }
198
+ )
199
+
200
+ # Invalid power factor (must be -1 to +1)
201
+ if "power_factor" in name.lower() and abs(value) > 1.0:
202
+ anomalies.append(
203
+ {
204
+ "name": name,
205
+ "value": value,
206
+ "reason": "Power factor must be between -1 and +1",
207
+ "severity": "critical",
208
+ }
209
+ )
210
+
211
+ return anomalies
212
+
213
+
214
+ def suggest_follow_up_analyses(
215
+ measurements: dict[str, Any],
216
+ interpretations: dict[str, MeasurementInterpretation] | None = None,
217
+ ) -> list[str]:
218
+ """Suggest follow-up analyses based on measurement results.
219
+
220
+ Args:
221
+ measurements: Dictionary of measurements.
222
+ interpretations: Optional measurement interpretations.
223
+
224
+ Returns:
225
+ List of suggested analysis descriptions.
226
+
227
+ Example:
228
+ >>> measurements = {"snr": 15.5, "thd": -40}
229
+ >>> suggestions = suggest_follow_up_analyses(measurements)
230
+ >>> len(suggestions) > 0
231
+ True
232
+ """
233
+ suggestions = []
234
+
235
+ # Low SNR - suggest noise analysis
236
+ if "snr" in measurements:
237
+ snr = measurements["snr"]
238
+ if isinstance(snr, int | float) and snr < 30:
239
+ suggestions.append("Perform detailed noise analysis to identify noise sources")
240
+ suggestions.append("Analyze frequency spectrum for interference peaks")
241
+
242
+ # High THD - suggest harmonic analysis
243
+ if "thd" in measurements:
244
+ thd = measurements["thd"]
245
+ if isinstance(thd, int | float) and abs(thd) < 40: # THD typically negative dB
246
+ suggestions.append("Perform harmonic analysis to identify distortion sources")
247
+ suggestions.append("Check for clipping or saturation in signal path")
248
+
249
+ # Jitter present - suggest jitter decomposition
250
+ if any("jitter" in k.lower() for k in measurements):
251
+ suggestions.append("Perform jitter decomposition (RJ, DJ, PJ) for root cause analysis")
252
+ suggestions.append("Analyze clock quality and investigate timing sources")
253
+
254
+ # Power measurements - suggest power quality
255
+ if any("power" in k.lower() for k in measurements):
256
+ suggestions.append("Perform power quality analysis (harmonics, flicker, sags/swells)")
257
+
258
+ # Eye diagram quality issues
259
+ if interpretations:
260
+ poor_quality = [
261
+ k for k, v in interpretations.items() if v.quality.value in ("marginal", "poor")
262
+ ]
263
+ if poor_quality:
264
+ suggestions.append(
265
+ f"Investigate {len(poor_quality)} marginal/poor measurements: "
266
+ f"{', '.join(poor_quality[:3])}"
267
+ )
268
+
269
+ return suggestions
270
+
271
+
272
+ def identify_issues(
273
+ measurements: dict[str, Any],
274
+ anomalies: list[dict[str, Any]],
275
+ ) -> list[dict[str, Any]]:
276
+ """Identify issues from measurements and anomalies.
277
+
278
+ Args:
279
+ measurements: Dictionary of measurements.
280
+ anomalies: List of detected anomalies.
281
+
282
+ Returns:
283
+ List of issue dictionaries with severity and description.
284
+
285
+ Example:
286
+ >>> measurements = {"snr": 10}
287
+ >>> anomalies = [{"name": "snr", "value": 10, "severity": "high"}]
288
+ >>> issues = identify_issues(measurements, anomalies)
289
+ >>> len(issues) > 0
290
+ True
291
+ """
292
+ issues = []
293
+
294
+ # Add critical anomalies as issues
295
+ for anomaly in anomalies:
296
+ if anomaly.get("severity") == "critical":
297
+ issues.append(
298
+ {
299
+ "severity": "critical",
300
+ "measurement": anomaly["name"],
301
+ "description": anomaly["reason"],
302
+ "value": anomaly["value"],
303
+ }
304
+ )
305
+
306
+ # Domain-specific issue detection
307
+ if "snr" in measurements:
308
+ snr = measurements["snr"]
309
+ if isinstance(snr, int | float) and snr < 20:
310
+ issues.append(
311
+ {
312
+ "severity": "high",
313
+ "measurement": "snr",
314
+ "description": f"Low SNR ({snr:.1f} dB) may impact measurement accuracy",
315
+ "value": snr,
316
+ }
317
+ )
318
+
319
+ if "bandwidth" in measurements:
320
+ bw = measurements["bandwidth"]
321
+ if isinstance(bw, int | float) and bw < 10e6:
322
+ issues.append(
323
+ {
324
+ "severity": "medium",
325
+ "measurement": "bandwidth",
326
+ "description": f"Limited bandwidth ({bw / 1e6:.1f} MHz) may restrict signal fidelity",
327
+ "value": bw,
328
+ }
329
+ )
330
+
331
+ return issues
332
+
333
+
334
+ def _flatten_results(results: dict[str, Any]) -> dict[str, Any]:
335
+ """Flatten nested results dictionary."""
336
+ flat = {}
337
+
338
+ for key, value in results.items():
339
+ if isinstance(value, dict):
340
+ if "value" in value:
341
+ flat[key] = value["value"]
342
+ else:
343
+ nested = _flatten_results(value)
344
+ flat.update(nested)
345
+ elif isinstance(value, int | float | str):
346
+ flat[key] = value
347
+
348
+ return flat