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,431 @@
|
|
|
1
|
+
"""Measurement interpretation and quality assessment for reports.
|
|
2
|
+
|
|
3
|
+
This module provides intelligent interpretation of measurement results,
|
|
4
|
+
generating findings, quality scores, and compliance checks against
|
|
5
|
+
IEEE standards.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class QualityLevel(Enum):
|
|
16
|
+
"""Signal or measurement quality level."""
|
|
17
|
+
|
|
18
|
+
EXCELLENT = "excellent"
|
|
19
|
+
GOOD = "good"
|
|
20
|
+
ACCEPTABLE = "acceptable"
|
|
21
|
+
MARGINAL = "marginal"
|
|
22
|
+
POOR = "poor"
|
|
23
|
+
FAILED = "failed"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ComplianceStatus(Enum):
|
|
27
|
+
"""Compliance status against standards."""
|
|
28
|
+
|
|
29
|
+
COMPLIANT = "compliant"
|
|
30
|
+
NON_COMPLIANT = "non_compliant"
|
|
31
|
+
MARGINAL = "marginal"
|
|
32
|
+
NOT_APPLICABLE = "not_applicable"
|
|
33
|
+
UNKNOWN = "unknown"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class MeasurementInterpretation:
|
|
38
|
+
"""Interpretation of a measurement result.
|
|
39
|
+
|
|
40
|
+
Attributes:
|
|
41
|
+
measurement_name: Name of the measurement.
|
|
42
|
+
value: Measured value.
|
|
43
|
+
units: Measurement units.
|
|
44
|
+
interpretation: Human-readable interpretation.
|
|
45
|
+
quality: Quality assessment.
|
|
46
|
+
recommendations: List of recommendations.
|
|
47
|
+
standard_reference: IEEE standard reference if applicable.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
measurement_name: str
|
|
51
|
+
value: float | str
|
|
52
|
+
units: str = ""
|
|
53
|
+
interpretation: str = ""
|
|
54
|
+
quality: QualityLevel = QualityLevel.ACCEPTABLE
|
|
55
|
+
recommendations: list[str] = field(default_factory=list)
|
|
56
|
+
standard_reference: str | None = None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class Finding:
|
|
61
|
+
"""A finding from measurement analysis.
|
|
62
|
+
|
|
63
|
+
Attributes:
|
|
64
|
+
title: Finding title.
|
|
65
|
+
description: Detailed description.
|
|
66
|
+
severity: Severity level (info, warning, critical).
|
|
67
|
+
measurements: Related measurements.
|
|
68
|
+
recommendation: Recommended action.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
title: str
|
|
72
|
+
description: str
|
|
73
|
+
severity: str = "info" # info, warning, critical
|
|
74
|
+
measurements: list[str] = field(default_factory=list)
|
|
75
|
+
recommendation: str = ""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def interpret_measurement(
|
|
79
|
+
name: str,
|
|
80
|
+
value: float | str,
|
|
81
|
+
units: str = "",
|
|
82
|
+
spec_min: float | None = None,
|
|
83
|
+
spec_max: float | None = None,
|
|
84
|
+
context: dict[str, Any] | None = None,
|
|
85
|
+
) -> MeasurementInterpretation:
|
|
86
|
+
"""Interpret a measurement and provide context.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
name: Measurement name (e.g., "rise_time", "snr").
|
|
90
|
+
value: Measured value.
|
|
91
|
+
units: Measurement units.
|
|
92
|
+
spec_min: Minimum specification (if applicable).
|
|
93
|
+
spec_max: Maximum specification (if applicable).
|
|
94
|
+
context: Additional context dictionary.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
MeasurementInterpretation object with analysis.
|
|
98
|
+
|
|
99
|
+
Example:
|
|
100
|
+
>>> interp = interpret_measurement("rise_time", 2.5e-9, "s", spec_max=5e-9)
|
|
101
|
+
>>> interp.quality
|
|
102
|
+
<QualityLevel.GOOD: 'good'>
|
|
103
|
+
>>> "fast" in interp.interpretation.lower()
|
|
104
|
+
True
|
|
105
|
+
"""
|
|
106
|
+
if isinstance(value, str):
|
|
107
|
+
return MeasurementInterpretation(
|
|
108
|
+
measurement_name=name,
|
|
109
|
+
value=value,
|
|
110
|
+
units=units,
|
|
111
|
+
interpretation="Non-numeric measurement",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
interpretation = ""
|
|
115
|
+
quality = QualityLevel.ACCEPTABLE
|
|
116
|
+
recommendations: list[str] = []
|
|
117
|
+
|
|
118
|
+
# Rise time interpretation
|
|
119
|
+
if "rise" in name.lower() or "fall" in name.lower():
|
|
120
|
+
if value < 1e-9:
|
|
121
|
+
interpretation = "Very fast transition time, indicating high bandwidth signal path"
|
|
122
|
+
quality = QualityLevel.EXCELLENT
|
|
123
|
+
elif value < 10e-9:
|
|
124
|
+
interpretation = "Fast transition time, suitable for high-speed digital signals"
|
|
125
|
+
quality = QualityLevel.GOOD
|
|
126
|
+
elif value < 100e-9:
|
|
127
|
+
interpretation = "Moderate transition time, adequate for standard digital signals"
|
|
128
|
+
quality = QualityLevel.ACCEPTABLE
|
|
129
|
+
else:
|
|
130
|
+
interpretation = "Slow transition time, may limit signal bandwidth"
|
|
131
|
+
quality = QualityLevel.MARGINAL
|
|
132
|
+
recommendations.append("Consider improving signal path bandwidth")
|
|
133
|
+
|
|
134
|
+
# SNR interpretation
|
|
135
|
+
elif "snr" in name.lower():
|
|
136
|
+
if value > 60:
|
|
137
|
+
interpretation = "Excellent signal-to-noise ratio, minimal noise impact"
|
|
138
|
+
quality = QualityLevel.EXCELLENT
|
|
139
|
+
elif value > 40:
|
|
140
|
+
interpretation = "Good signal-to-noise ratio, acceptable for most applications"
|
|
141
|
+
quality = QualityLevel.GOOD
|
|
142
|
+
elif value > 20:
|
|
143
|
+
interpretation = "Moderate SNR, noise may affect precision measurements"
|
|
144
|
+
quality = QualityLevel.ACCEPTABLE
|
|
145
|
+
recommendations.append("Consider noise reduction techniques")
|
|
146
|
+
else:
|
|
147
|
+
interpretation = "Poor SNR, signal quality compromised by noise"
|
|
148
|
+
quality = QualityLevel.POOR
|
|
149
|
+
recommendations.extend(
|
|
150
|
+
["Investigate noise sources", "Consider signal averaging or filtering"]
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Jitter interpretation
|
|
154
|
+
elif "jitter" in name.lower():
|
|
155
|
+
# Assuming jitter in seconds
|
|
156
|
+
jitter_ps = value * 1e12 # Convert to picoseconds
|
|
157
|
+
if jitter_ps < 10:
|
|
158
|
+
interpretation = "Very low jitter, excellent timing stability"
|
|
159
|
+
quality = QualityLevel.EXCELLENT
|
|
160
|
+
elif jitter_ps < 50:
|
|
161
|
+
interpretation = "Low jitter, good timing performance"
|
|
162
|
+
quality = QualityLevel.GOOD
|
|
163
|
+
elif jitter_ps < 200:
|
|
164
|
+
interpretation = "Moderate jitter, acceptable for most applications"
|
|
165
|
+
quality = QualityLevel.ACCEPTABLE
|
|
166
|
+
else:
|
|
167
|
+
interpretation = "High jitter, timing stability may be compromised"
|
|
168
|
+
quality = QualityLevel.MARGINAL
|
|
169
|
+
recommendations.append("Investigate jitter sources (clock, power, crosstalk)")
|
|
170
|
+
|
|
171
|
+
# Bandwidth interpretation
|
|
172
|
+
elif "bandwidth" in name.lower():
|
|
173
|
+
if value > 1e9:
|
|
174
|
+
interpretation = (
|
|
175
|
+
f"Wide bandwidth ({value / 1e9:.1f} GHz), suitable for high-frequency signals"
|
|
176
|
+
)
|
|
177
|
+
quality = QualityLevel.EXCELLENT
|
|
178
|
+
elif value > 100e6:
|
|
179
|
+
interpretation = (
|
|
180
|
+
f"Good bandwidth ({value / 1e6:.0f} MHz), adequate for most applications"
|
|
181
|
+
)
|
|
182
|
+
quality = QualityLevel.GOOD
|
|
183
|
+
elif value > 10e6:
|
|
184
|
+
interpretation = f"Moderate bandwidth ({value / 1e6:.1f} MHz)"
|
|
185
|
+
quality = QualityLevel.ACCEPTABLE
|
|
186
|
+
else:
|
|
187
|
+
interpretation = f"Limited bandwidth ({value / 1e6:.2f} MHz)"
|
|
188
|
+
quality = QualityLevel.MARGINAL
|
|
189
|
+
|
|
190
|
+
# Generic interpretation based on specs
|
|
191
|
+
elif spec_min is not None or spec_max is not None:
|
|
192
|
+
if spec_min is not None and value < spec_min:
|
|
193
|
+
interpretation = f"Below minimum specification ({spec_min} {units})"
|
|
194
|
+
quality = QualityLevel.FAILED
|
|
195
|
+
recommendations.append("Value does not meet specification")
|
|
196
|
+
elif spec_max is not None and value > spec_max:
|
|
197
|
+
interpretation = f"Above maximum specification ({spec_max} {units})"
|
|
198
|
+
quality = QualityLevel.FAILED
|
|
199
|
+
recommendations.append("Value exceeds specification limit")
|
|
200
|
+
else:
|
|
201
|
+
# Within spec - check margin
|
|
202
|
+
if spec_min is not None and spec_max is not None:
|
|
203
|
+
range_val = spec_max - spec_min
|
|
204
|
+
margin_low = (value - spec_min) / range_val if range_val > 0 else 0
|
|
205
|
+
margin_high = (spec_max - value) / range_val if range_val > 0 else 0
|
|
206
|
+
min_margin = min(margin_low, margin_high)
|
|
207
|
+
|
|
208
|
+
if min_margin > 0.3:
|
|
209
|
+
interpretation = "Well within specification with good margin"
|
|
210
|
+
quality = QualityLevel.GOOD
|
|
211
|
+
elif min_margin > 0.1:
|
|
212
|
+
interpretation = "Within specification with adequate margin"
|
|
213
|
+
quality = QualityLevel.ACCEPTABLE
|
|
214
|
+
else:
|
|
215
|
+
interpretation = "Within specification but marginal"
|
|
216
|
+
quality = QualityLevel.MARGINAL
|
|
217
|
+
recommendations.append("Low margin to specification limits")
|
|
218
|
+
else:
|
|
219
|
+
interpretation = "Within specification"
|
|
220
|
+
quality = QualityLevel.ACCEPTABLE
|
|
221
|
+
|
|
222
|
+
else:
|
|
223
|
+
interpretation = f"Measured value: {value} {units}"
|
|
224
|
+
quality = QualityLevel.ACCEPTABLE
|
|
225
|
+
|
|
226
|
+
return MeasurementInterpretation(
|
|
227
|
+
measurement_name=name,
|
|
228
|
+
value=value,
|
|
229
|
+
units=units,
|
|
230
|
+
interpretation=interpretation,
|
|
231
|
+
quality=quality,
|
|
232
|
+
recommendations=recommendations,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def generate_finding(
|
|
237
|
+
title: str,
|
|
238
|
+
measurements: dict[str, Any],
|
|
239
|
+
threshold: float | None = None,
|
|
240
|
+
severity: str = "info",
|
|
241
|
+
) -> Finding:
|
|
242
|
+
"""Generate a finding from measurement analysis.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
title: Finding title.
|
|
246
|
+
measurements: Dictionary of measurements.
|
|
247
|
+
threshold: Threshold for determining severity.
|
|
248
|
+
severity: Override severity level.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Finding object.
|
|
252
|
+
|
|
253
|
+
Example:
|
|
254
|
+
>>> measurements = {"snr": 25.5, "thd": -60.2}
|
|
255
|
+
>>> finding = generate_finding("Signal Quality Assessment", measurements)
|
|
256
|
+
>>> finding.title
|
|
257
|
+
'Signal Quality Assessment'
|
|
258
|
+
"""
|
|
259
|
+
description_parts = []
|
|
260
|
+
|
|
261
|
+
for name, value in measurements.items():
|
|
262
|
+
if isinstance(value, float):
|
|
263
|
+
description_parts.append(f"{name}: {value:.3f}")
|
|
264
|
+
else:
|
|
265
|
+
description_parts.append(f"{name}: {value}")
|
|
266
|
+
|
|
267
|
+
description = "\n".join(description_parts)
|
|
268
|
+
|
|
269
|
+
recommendation = ""
|
|
270
|
+
if severity == "warning":
|
|
271
|
+
recommendation = "Review measurements and verify against specifications"
|
|
272
|
+
elif severity == "critical":
|
|
273
|
+
recommendation = "Immediate attention required - critical parameter out of range"
|
|
274
|
+
|
|
275
|
+
return Finding(
|
|
276
|
+
title=title,
|
|
277
|
+
description=description,
|
|
278
|
+
severity=severity,
|
|
279
|
+
measurements=list(measurements.keys()),
|
|
280
|
+
recommendation=recommendation,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def quality_score(
|
|
285
|
+
measurements: dict[str, float],
|
|
286
|
+
weights: dict[str, float] | None = None,
|
|
287
|
+
) -> tuple[float, QualityLevel]:
|
|
288
|
+
"""Calculate overall quality score from measurements.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
measurements: Dictionary of measurement names to values (0-100 scale).
|
|
292
|
+
weights: Optional weights for each measurement.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Tuple of (score, quality_level) where score is 0-100.
|
|
296
|
+
|
|
297
|
+
Example:
|
|
298
|
+
>>> measurements = {"snr": 45, "bandwidth": 80, "jitter": 60}
|
|
299
|
+
>>> score, level = quality_score(measurements)
|
|
300
|
+
>>> 0 <= score <= 100
|
|
301
|
+
True
|
|
302
|
+
>>> level in QualityLevel
|
|
303
|
+
True
|
|
304
|
+
"""
|
|
305
|
+
if not measurements:
|
|
306
|
+
return 0.0, QualityLevel.POOR
|
|
307
|
+
|
|
308
|
+
if weights is None:
|
|
309
|
+
weights = dict.fromkeys(measurements, 1.0)
|
|
310
|
+
|
|
311
|
+
total_weight = sum(weights.get(name, 1.0) for name in measurements)
|
|
312
|
+
weighted_sum = sum(value * weights.get(name, 1.0) for name, value in measurements.items())
|
|
313
|
+
|
|
314
|
+
score = weighted_sum / total_weight if total_weight > 0 else 0.0
|
|
315
|
+
|
|
316
|
+
# Determine quality level
|
|
317
|
+
if score >= 90:
|
|
318
|
+
level = QualityLevel.EXCELLENT
|
|
319
|
+
elif score >= 75:
|
|
320
|
+
level = QualityLevel.GOOD
|
|
321
|
+
elif score >= 60:
|
|
322
|
+
level = QualityLevel.ACCEPTABLE
|
|
323
|
+
elif score >= 40:
|
|
324
|
+
level = QualityLevel.MARGINAL
|
|
325
|
+
else:
|
|
326
|
+
level = QualityLevel.POOR
|
|
327
|
+
|
|
328
|
+
return score, level
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def compliance_check(
|
|
332
|
+
measurement_name: str,
|
|
333
|
+
value: float,
|
|
334
|
+
standard_id: str,
|
|
335
|
+
limits: dict[str, float] | None = None,
|
|
336
|
+
) -> tuple[ComplianceStatus, str]:
|
|
337
|
+
"""Check measurement compliance against IEEE standard.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
measurement_name: Name of measurement.
|
|
341
|
+
value: Measured value.
|
|
342
|
+
standard_id: IEEE standard ID (e.g., "181", "1241").
|
|
343
|
+
limits: Optional dict with "min" and/or "max" keys.
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
Tuple of (compliance_status, explanation).
|
|
347
|
+
|
|
348
|
+
Example:
|
|
349
|
+
>>> status, msg = compliance_check("rise_time", 2.5e-9, "181", {"max": 5e-9})
|
|
350
|
+
>>> status
|
|
351
|
+
<ComplianceStatus.COMPLIANT: 'compliant'>
|
|
352
|
+
>>> "compliant" in msg.lower()
|
|
353
|
+
True
|
|
354
|
+
"""
|
|
355
|
+
if limits is None:
|
|
356
|
+
return ComplianceStatus.NOT_APPLICABLE, f"No limits defined for {measurement_name}"
|
|
357
|
+
|
|
358
|
+
min_limit = limits.get("min")
|
|
359
|
+
max_limit = limits.get("max")
|
|
360
|
+
|
|
361
|
+
# Check limits
|
|
362
|
+
if min_limit is not None and value < min_limit:
|
|
363
|
+
explanation = (
|
|
364
|
+
f"{measurement_name} = {value:.3e} is below minimum limit {min_limit:.3e} "
|
|
365
|
+
f"(IEEE {standard_id})"
|
|
366
|
+
)
|
|
367
|
+
return ComplianceStatus.NON_COMPLIANT, explanation
|
|
368
|
+
|
|
369
|
+
if max_limit is not None and value > max_limit:
|
|
370
|
+
explanation = (
|
|
371
|
+
f"{measurement_name} = {value:.3e} exceeds maximum limit {max_limit:.3e} "
|
|
372
|
+
f"(IEEE {standard_id})"
|
|
373
|
+
)
|
|
374
|
+
return ComplianceStatus.NON_COMPLIANT, explanation
|
|
375
|
+
|
|
376
|
+
# Within limits - check margin
|
|
377
|
+
margin_pct = 100.0
|
|
378
|
+
if min_limit is not None and max_limit is not None:
|
|
379
|
+
range_val = max_limit - min_limit
|
|
380
|
+
margin_low = ((value - min_limit) / range_val * 100) if range_val > 0 else 100
|
|
381
|
+
margin_high = ((max_limit - value) / range_val * 100) if range_val > 0 else 100
|
|
382
|
+
margin_pct = min(margin_low, margin_high)
|
|
383
|
+
|
|
384
|
+
if margin_pct < 10:
|
|
385
|
+
explanation = (
|
|
386
|
+
f"{measurement_name} = {value:.3e} is compliant but marginal "
|
|
387
|
+
f"({margin_pct:.1f}% margin to IEEE {standard_id} limits)"
|
|
388
|
+
)
|
|
389
|
+
return ComplianceStatus.MARGINAL, explanation
|
|
390
|
+
|
|
391
|
+
explanation = (
|
|
392
|
+
f"{measurement_name} = {value:.3e} is compliant with IEEE {standard_id} "
|
|
393
|
+
f"({margin_pct:.1f}% margin)"
|
|
394
|
+
)
|
|
395
|
+
return ComplianceStatus.COMPLIANT, explanation
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def interpret_results_batch(
|
|
399
|
+
results: dict[str, dict[str, Any]],
|
|
400
|
+
) -> dict[str, MeasurementInterpretation]:
|
|
401
|
+
"""Interpret multiple measurement results.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
results: Dictionary mapping measurement names to result dicts.
|
|
405
|
+
Each result dict should have "value", "units", and optionally
|
|
406
|
+
"spec_min" and "spec_max" keys.
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
Dictionary mapping measurement names to interpretations.
|
|
410
|
+
|
|
411
|
+
Example:
|
|
412
|
+
>>> results = {
|
|
413
|
+
... "rise_time": {"value": 2.5e-9, "units": "s", "spec_max": 5e-9},
|
|
414
|
+
... "snr": {"value": 45.2, "units": "dB"}
|
|
415
|
+
... }
|
|
416
|
+
>>> interps = interpret_results_batch(results)
|
|
417
|
+
>>> len(interps)
|
|
418
|
+
2
|
|
419
|
+
"""
|
|
420
|
+
interpretations = {}
|
|
421
|
+
|
|
422
|
+
for name, result in results.items():
|
|
423
|
+
value = result.get("value", 0.0)
|
|
424
|
+
units = result.get("units", "")
|
|
425
|
+
spec_min = result.get("spec_min")
|
|
426
|
+
spec_max = result.get("spec_max")
|
|
427
|
+
|
|
428
|
+
interp = interpret_measurement(name, value, units, spec_min, spec_max)
|
|
429
|
+
interpretations[name] = interp
|
|
430
|
+
|
|
431
|
+
return interpretations
|