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.
- oscura/__init__.py +19 -19
- oscura/analyzers/__init__.py +2 -0
- oscura/analyzers/digital/extraction.py +2 -3
- oscura/analyzers/digital/quality.py +1 -1
- oscura/analyzers/digital/timing.py +1 -1
- oscura/analyzers/eye/__init__.py +5 -1
- oscura/analyzers/eye/generation.py +501 -0
- oscura/analyzers/jitter/__init__.py +6 -6
- oscura/analyzers/jitter/timing.py +419 -0
- oscura/analyzers/patterns/__init__.py +94 -0
- oscura/analyzers/patterns/reverse_engineering.py +991 -0
- oscura/analyzers/power/__init__.py +35 -12
- oscura/analyzers/power/basic.py +3 -3
- oscura/analyzers/power/soa.py +1 -1
- oscura/analyzers/power/switching.py +3 -3
- oscura/analyzers/signal_classification.py +529 -0
- oscura/analyzers/signal_integrity/sparams.py +3 -3
- oscura/analyzers/statistics/__init__.py +4 -0
- oscura/analyzers/statistics/basic.py +152 -0
- oscura/analyzers/statistics/correlation.py +47 -6
- oscura/analyzers/validation.py +1 -1
- oscura/analyzers/waveform/__init__.py +2 -0
- oscura/analyzers/waveform/measurements.py +329 -163
- oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
- oscura/analyzers/waveform/spectral.py +498 -54
- oscura/api/dsl/commands.py +15 -6
- oscura/api/server/templates/base.html +137 -146
- oscura/api/server/templates/export.html +84 -110
- oscura/api/server/templates/home.html +248 -267
- oscura/api/server/templates/protocols.html +44 -48
- oscura/api/server/templates/reports.html +27 -35
- oscura/api/server/templates/session_detail.html +68 -78
- oscura/api/server/templates/sessions.html +62 -72
- oscura/api/server/templates/waveforms.html +54 -64
- oscura/automotive/__init__.py +1 -1
- oscura/automotive/can/session.py +1 -1
- oscura/automotive/dbc/generator.py +638 -23
- oscura/automotive/dtc/data.json +102 -17
- oscura/automotive/uds/decoder.py +99 -6
- oscura/cli/analyze.py +8 -2
- oscura/cli/batch.py +36 -5
- oscura/cli/characterize.py +18 -4
- oscura/cli/export.py +47 -5
- oscura/cli/main.py +2 -0
- oscura/cli/onboarding/wizard.py +10 -6
- oscura/cli/pipeline.py +585 -0
- oscura/cli/visualize.py +6 -4
- oscura/convenience.py +400 -32
- oscura/core/config/loader.py +0 -1
- oscura/core/measurement_result.py +286 -0
- oscura/core/progress.py +1 -1
- oscura/core/schemas/device_mapping.json +8 -2
- oscura/core/schemas/packet_format.json +24 -4
- oscura/core/schemas/protocol_definition.json +12 -2
- oscura/core/types.py +300 -199
- oscura/correlation/multi_protocol.py +1 -1
- oscura/export/legacy/__init__.py +11 -0
- oscura/export/legacy/wav.py +75 -0
- oscura/exporters/__init__.py +19 -0
- oscura/exporters/wireshark.py +809 -0
- oscura/hardware/acquisition/file.py +5 -19
- oscura/hardware/acquisition/saleae.py +10 -10
- oscura/hardware/acquisition/socketcan.py +4 -6
- oscura/hardware/acquisition/synthetic.py +1 -5
- oscura/hardware/acquisition/visa.py +6 -6
- oscura/hardware/security/side_channel_detector.py +5 -508
- oscura/inference/message_format.py +686 -1
- oscura/jupyter/display.py +2 -2
- oscura/jupyter/magic.py +3 -3
- oscura/loaders/__init__.py +17 -12
- oscura/loaders/binary.py +1 -1
- oscura/loaders/chipwhisperer.py +1 -2
- oscura/loaders/configurable.py +1 -1
- oscura/loaders/csv_loader.py +2 -2
- oscura/loaders/hdf5_loader.py +1 -1
- oscura/loaders/lazy.py +6 -1
- oscura/loaders/mmap_loader.py +0 -1
- oscura/loaders/numpy_loader.py +8 -7
- oscura/loaders/preprocessing.py +3 -5
- oscura/loaders/rigol.py +21 -7
- oscura/loaders/sigrok.py +2 -5
- oscura/loaders/tdms.py +3 -2
- oscura/loaders/tektronix.py +38 -32
- oscura/loaders/tss.py +20 -27
- oscura/loaders/vcd.py +13 -8
- oscura/loaders/wav.py +1 -6
- oscura/pipeline/__init__.py +76 -0
- oscura/pipeline/handlers/__init__.py +165 -0
- oscura/pipeline/handlers/analyzers.py +1045 -0
- oscura/pipeline/handlers/decoders.py +899 -0
- oscura/pipeline/handlers/exporters.py +1103 -0
- oscura/pipeline/handlers/filters.py +891 -0
- oscura/pipeline/handlers/loaders.py +640 -0
- oscura/pipeline/handlers/transforms.py +768 -0
- oscura/reporting/__init__.py +88 -1
- oscura/reporting/automation.py +348 -0
- oscura/reporting/citations.py +374 -0
- oscura/reporting/core.py +54 -0
- oscura/reporting/formatting/__init__.py +11 -0
- oscura/reporting/formatting/measurements.py +320 -0
- oscura/reporting/html.py +57 -0
- oscura/reporting/interpretation.py +431 -0
- oscura/reporting/summary.py +329 -0
- oscura/reporting/templates/enhanced/protocol_re.html +504 -503
- oscura/reporting/visualization.py +542 -0
- oscura/side_channel/__init__.py +38 -57
- oscura/utils/builders/signal_builder.py +5 -5
- oscura/utils/comparison/compare.py +7 -9
- oscura/utils/comparison/golden.py +1 -1
- oscura/utils/filtering/convenience.py +2 -2
- oscura/utils/math/arithmetic.py +38 -62
- oscura/utils/math/interpolation.py +20 -20
- oscura/utils/pipeline/__init__.py +4 -17
- oscura/utils/progressive.py +1 -4
- oscura/utils/triggering/edge.py +1 -1
- oscura/utils/triggering/pattern.py +2 -2
- oscura/utils/triggering/pulse.py +2 -2
- oscura/utils/triggering/window.py +3 -3
- oscura/validation/hil_testing.py +11 -11
- oscura/visualization/__init__.py +47 -284
- oscura/visualization/batch.py +160 -0
- oscura/visualization/plot.py +542 -53
- oscura/visualization/styles.py +184 -318
- oscura/workflows/__init__.py +2 -0
- oscura/workflows/batch/advanced.py +1 -1
- oscura/workflows/batch/aggregate.py +7 -8
- oscura/workflows/complete_re.py +251 -23
- oscura/workflows/digital.py +27 -4
- oscura/workflows/multi_trace.py +136 -17
- oscura/workflows/waveform.py +788 -0
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/METADATA +59 -79
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/RECORD +135 -149
- oscura/side_channel/dpa.py +0 -1025
- oscura/utils/optimization/__init__.py +0 -19
- oscura/utils/optimization/parallel.py +0 -443
- oscura/utils/optimization/search.py +0 -532
- oscura/utils/pipeline/base.py +0 -338
- oscura/utils/pipeline/composition.py +0 -248
- oscura/utils/pipeline/parallel.py +0 -449
- oscura/utils/pipeline/pipeline.py +0 -375
- oscura/utils/search/__init__.py +0 -16
- oscura/utils/search/anomaly.py +0 -424
- oscura/utils/search/context.py +0 -294
- oscura/utils/search/pattern.py +0 -288
- oscura/utils/storage/__init__.py +0 -61
- oscura/utils/storage/database.py +0 -1166
- oscura/visualization/accessibility.py +0 -526
- oscura/visualization/annotations.py +0 -371
- oscura/visualization/axis_scaling.py +0 -305
- oscura/visualization/colors.py +0 -451
- oscura/visualization/digital.py +0 -436
- oscura/visualization/eye.py +0 -571
- oscura/visualization/histogram.py +0 -281
- oscura/visualization/interactive.py +0 -1035
- oscura/visualization/jitter.py +0 -1042
- oscura/visualization/keyboard.py +0 -394
- oscura/visualization/layout.py +0 -400
- oscura/visualization/optimization.py +0 -1079
- oscura/visualization/palettes.py +0 -446
- oscura/visualization/power.py +0 -508
- oscura/visualization/power_extended.py +0 -955
- oscura/visualization/presets.py +0 -469
- oscura/visualization/protocols.py +0 -1246
- oscura/visualization/render.py +0 -223
- oscura/visualization/rendering.py +0 -444
- oscura/visualization/reverse_engineering.py +0 -838
- oscura/visualization/signal_integrity.py +0 -989
- oscura/visualization/specialized.py +0 -643
- oscura/visualization/spectral.py +0 -1226
- oscura/visualization/thumbnails.py +0 -340
- oscura/visualization/time_axis.py +0 -351
- oscura/visualization/waveform.py +0 -454
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/WHEEL +0 -0
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/entry_points.txt +0 -0
- {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))
|