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,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