oscura 0.4.0__py3-none-any.whl → 0.5.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.
@@ -0,0 +1,523 @@
1
+ """Vintage logic analysis report generation.
2
+
3
+ This module provides comprehensive reporting capabilities for vintage logic
4
+ analysis results, including HTML, PDF, and Markdown formats.
5
+
6
+ Example:
7
+ >>> from oscura.reporting.vintage_logic_report import generate_vintage_logic_report
8
+ >>> report = generate_vintage_logic_report(result, traces, output_dir="./output")
9
+ >>> report.save_html("analysis.html")
10
+ >>> report.save_markdown("analysis.md")
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from dataclasses import dataclass, field
16
+ from datetime import datetime
17
+ from pathlib import Path
18
+ from typing import TYPE_CHECKING, Any
19
+
20
+ if TYPE_CHECKING:
21
+ from oscura.analyzers.digital.vintage_result import VintageLogicAnalysisResult
22
+ from oscura.core.types import DigitalTrace, WaveformTrace
23
+
24
+
25
+ @dataclass
26
+ class ReportMetadata:
27
+ """Report metadata.
28
+
29
+ Attributes:
30
+ title: Report title.
31
+ author: Report author.
32
+ timestamp: Report generation timestamp.
33
+ version: Report format version.
34
+ """
35
+
36
+ title: str
37
+ author: str = "Oscura Vintage Logic Analyzer"
38
+ timestamp: datetime = field(default_factory=datetime.now)
39
+ version: str = "1.0"
40
+
41
+
42
+ @dataclass
43
+ class VintageLogicReport:
44
+ """Vintage logic analysis report.
45
+
46
+ Attributes:
47
+ result: Analysis result object.
48
+ plots: Dictionary mapping plot names to file paths.
49
+ metadata: Report metadata.
50
+ """
51
+
52
+ result: VintageLogicAnalysisResult
53
+ plots: dict[str, Path]
54
+ metadata: ReportMetadata
55
+
56
+ def save_html(self, path: str | Path, **options: Any) -> Path:
57
+ """Generate comprehensive HTML report.
58
+
59
+ Args:
60
+ path: Output HTML file path.
61
+ **options: Additional options (currently unused, reserved for future).
62
+
63
+ Returns:
64
+ Path to saved HTML file.
65
+
66
+ Example:
67
+ >>> report.save_html("analysis.html")
68
+ """
69
+
70
+ path = Path(path)
71
+
72
+ # Generate HTML content
73
+ html = _generate_html_report(self)
74
+
75
+ # Write to file
76
+ path.write_text(html, encoding="utf-8")
77
+
78
+ return path
79
+
80
+ def save_markdown(self, path: str | Path) -> Path:
81
+ """Generate markdown summary.
82
+
83
+ Args:
84
+ path: Output markdown file path.
85
+
86
+ Returns:
87
+ Path to saved markdown file.
88
+
89
+ Example:
90
+ >>> report.save_markdown("analysis.md")
91
+ """
92
+ path = Path(path)
93
+
94
+ # Generate markdown content
95
+ md = _generate_markdown_report(self)
96
+
97
+ # Write to file
98
+ path.write_text(md, encoding="utf-8")
99
+
100
+ return path
101
+
102
+ def save_pdf(self, path: str | Path, **options: Any) -> Path:
103
+ """Generate PDF report.
104
+
105
+ Currently generates HTML first, then suggests using browser print
106
+ or external tools for PDF conversion.
107
+
108
+ Args:
109
+ path: Output PDF file path.
110
+ **options: Additional options.
111
+
112
+ Returns:
113
+ Path to saved PDF file (or HTML file with instructions).
114
+
115
+ Example:
116
+ >>> report.save_pdf("analysis.pdf")
117
+ """
118
+ path = Path(path)
119
+
120
+ # For now, save as HTML with instructions
121
+ html_path = path.with_suffix(".html")
122
+ self.save_html(html_path)
123
+
124
+ # Add note about PDF conversion
125
+ print(f"HTML report generated: {html_path}")
126
+ print("To convert to PDF:")
127
+ print(" 1. Open in browser and use Print > Save as PDF")
128
+ print(" 2. Use weasyprint: weasyprint {html_path} {path}")
129
+ print(" 3. Use wkhtmltopdf: wkhtmltopdf {html_path} {path}")
130
+
131
+ return html_path
132
+
133
+
134
+ def generate_vintage_logic_report(
135
+ result: VintageLogicAnalysisResult,
136
+ traces: dict[str, WaveformTrace | DigitalTrace],
137
+ *,
138
+ title: str | None = None,
139
+ include_plots: list[str] | None = None,
140
+ output_dir: Path | None = None,
141
+ ) -> VintageLogicReport:
142
+ """Generate complete vintage logic report.
143
+
144
+ Args:
145
+ result: Analysis result from analyze_vintage_logic().
146
+ traces: Original signal traces.
147
+ title: Report title. Defaults to auto-generated title.
148
+ include_plots: List of plot types to include (None = all).
149
+ output_dir: Directory for saving plot files. If None, uses temp directory.
150
+
151
+ Returns:
152
+ VintageLogicReport object ready for export.
153
+
154
+ Example:
155
+ >>> from oscura.analyzers.digital.vintage import analyze_vintage_logic
156
+ >>> result = analyze_vintage_logic(traces)
157
+ >>> report = generate_vintage_logic_report(result, traces, output_dir="./output")
158
+ >>> report.save_html("report.html")
159
+ """
160
+ from pathlib import Path
161
+ from tempfile import mkdtemp
162
+
163
+ from oscura.visualization.digital_advanced import generate_all_vintage_logic_plots
164
+
165
+ # Use temp directory if no output directory specified
166
+ if output_dir is None:
167
+ output_dir = Path(mkdtemp(prefix="oscura_vintage_"))
168
+ else:
169
+ output_dir = Path(output_dir)
170
+ output_dir.mkdir(parents=True, exist_ok=True)
171
+
172
+ # Generate default title if not provided
173
+ if title is None:
174
+ title = f"Vintage Logic Analysis - {result.detected_family}"
175
+
176
+ # Create metadata
177
+ metadata = ReportMetadata(title=title)
178
+
179
+ # Generate all plots
180
+ plot_objects = generate_all_vintage_logic_plots(
181
+ result,
182
+ traces,
183
+ output_dir=output_dir,
184
+ save_formats=["png"],
185
+ )
186
+
187
+ # Get plot paths from output directory
188
+ plot_paths: dict[str, Path] = {}
189
+ for plot_name in plot_objects:
190
+ plot_path = output_dir / f"{plot_name}.png"
191
+ if plot_path.exists():
192
+ plot_paths[plot_name] = plot_path
193
+
194
+ # Filter plots if specified
195
+ if include_plots:
196
+ plot_paths = {k: v for k, v in plot_paths.items() if k in include_plots}
197
+
198
+ return VintageLogicReport(
199
+ result=result,
200
+ plots=plot_paths,
201
+ metadata=metadata,
202
+ )
203
+
204
+
205
+ def _generate_html_report(report: VintageLogicReport) -> str:
206
+ """Generate HTML report content."""
207
+ result = report.result
208
+
209
+ # Embed images as base64
210
+ from base64 import b64encode
211
+
212
+ plot_html = []
213
+ for plot_name, plot_path in report.plots.items():
214
+ if plot_path.exists():
215
+ with plot_path.open("rb") as f:
216
+ img_data = b64encode(f.read()).decode("utf-8")
217
+ plot_html.append(
218
+ f'<div class="plot"><h3>{plot_name.replace("_", " ").title()}</h3>'
219
+ f'<img src="data:image/png;base64,{img_data}" alt="{plot_name}" /></div>'
220
+ )
221
+
222
+ plots_section = "\n".join(plot_html)
223
+
224
+ # Generate IC table
225
+ ic_rows = []
226
+ for ic in result.identified_ics:
227
+ validation_status = (
228
+ "PASS" if all(v.get("passes", True) for v in ic.validation.values()) else "FAIL"
229
+ )
230
+ status_class = "pass" if validation_status == "PASS" else "fail"
231
+
232
+ ic_rows.append(
233
+ f"<tr>"
234
+ f"<td>{ic.ic_name}</td>"
235
+ f"<td>{ic.confidence * 100:.1f}%</td>"
236
+ f"<td>{ic.family}</td>"
237
+ f'<td class="{status_class}">{validation_status}</td>'
238
+ f"</tr>"
239
+ )
240
+
241
+ ic_table = (
242
+ "<table><tr><th>IC</th><th>Confidence</th><th>Family</th><th>Validation</th></tr>\n"
243
+ + "\n".join(ic_rows)
244
+ + "</table>"
245
+ if ic_rows
246
+ else "<p>No ICs identified</p>"
247
+ )
248
+
249
+ # Generate BOM table
250
+ bom_rows = []
251
+ for entry in result.bom:
252
+ bom_rows.append(
253
+ f"<tr>"
254
+ f"<td>{entry.part_number}</td>"
255
+ f"<td>{entry.description}</td>"
256
+ f"<td>{entry.quantity}</td>"
257
+ f"<td>{entry.category}</td>"
258
+ f"<td>{entry.notes or ''}</td>"
259
+ f"</tr>"
260
+ )
261
+
262
+ bom_table = (
263
+ "<table><tr><th>Part Number</th><th>Description</th><th>Qty</th><th>Category</th><th>Notes</th></tr>\n"
264
+ + "\n".join(bom_rows)
265
+ + "</table>"
266
+ if bom_rows
267
+ else "<p>No BOM entries</p>"
268
+ )
269
+
270
+ # Generate timing measurements table
271
+ timing_rows = []
272
+ for param, value in result.timing_measurements.items():
273
+ timing_rows.append(f"<tr><td>{param}</td><td>{value * 1e9:.2f} ns</td></tr>")
274
+
275
+ timing_table = (
276
+ "<table><tr><th>Parameter</th><th>Value</th></tr>\n" + "\n".join(timing_rows) + "</table>"
277
+ if timing_rows
278
+ else "<p>No timing measurements</p>"
279
+ )
280
+
281
+ # Generate warnings
282
+ warnings_html = ""
283
+ if result.warnings:
284
+ warnings_list = "\n".join(f"<li>{w}</li>" for w in result.warnings)
285
+ warnings_html = f'<div class="warnings"><h2>Warnings</h2><ul>{warnings_list}</ul></div>'
286
+
287
+ html = f"""<!DOCTYPE html>
288
+ <html lang="en">
289
+ <head>
290
+ <meta charset="UTF-8">
291
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
292
+ <title>{report.metadata.title}</title>
293
+ <style>
294
+ body {{
295
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
296
+ line-height: 1.6;
297
+ color: #333;
298
+ max-width: 1200px;
299
+ margin: 0 auto;
300
+ padding: 20px;
301
+ background: #f5f5f5;
302
+ }}
303
+ .container {{
304
+ background: white;
305
+ padding: 30px;
306
+ border-radius: 8px;
307
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
308
+ }}
309
+ h1 {{
310
+ color: #2c3e50;
311
+ border-bottom: 3px solid #3498db;
312
+ padding-bottom: 10px;
313
+ }}
314
+ h2 {{
315
+ color: #34495e;
316
+ margin-top: 30px;
317
+ border-bottom: 2px solid #ecf0f1;
318
+ padding-bottom: 5px;
319
+ }}
320
+ h3 {{
321
+ color: #7f8c8d;
322
+ }}
323
+ table {{
324
+ width: 100%;
325
+ border-collapse: collapse;
326
+ margin: 20px 0;
327
+ }}
328
+ th, td {{
329
+ padding: 12px;
330
+ text-align: left;
331
+ border-bottom: 1px solid #ddd;
332
+ }}
333
+ th {{
334
+ background-color: #3498db;
335
+ color: white;
336
+ font-weight: bold;
337
+ }}
338
+ tr:hover {{
339
+ background-color: #f5f5f5;
340
+ }}
341
+ .plot {{
342
+ margin: 30px 0;
343
+ text-align: center;
344
+ }}
345
+ .plot img {{
346
+ max-width: 100%;
347
+ height: auto;
348
+ border: 1px solid #ddd;
349
+ border-radius: 4px;
350
+ padding: 5px;
351
+ }}
352
+ .summary-box {{
353
+ background: #ecf0f1;
354
+ padding: 20px;
355
+ border-radius: 4px;
356
+ margin: 20px 0;
357
+ }}
358
+ .summary-box strong {{
359
+ color: #2c3e50;
360
+ }}
361
+ .pass {{
362
+ color: #27ae60;
363
+ font-weight: bold;
364
+ }}
365
+ .fail {{
366
+ color: #e74c3c;
367
+ font-weight: bold;
368
+ }}
369
+ .warnings {{
370
+ background: #fff3cd;
371
+ border-left: 4px solid #ffc107;
372
+ padding: 15px;
373
+ margin: 20px 0;
374
+ }}
375
+ .warnings h2 {{
376
+ color: #856404;
377
+ margin-top: 0;
378
+ }}
379
+ .warnings ul {{
380
+ margin: 10px 0;
381
+ }}
382
+ .warnings li {{
383
+ color: #856404;
384
+ }}
385
+ .metadata {{
386
+ font-size: 0.9em;
387
+ color: #7f8c8d;
388
+ margin-bottom: 20px;
389
+ }}
390
+ </style>
391
+ </head>
392
+ <body>
393
+ <div class="container">
394
+ <h1>{report.metadata.title}</h1>
395
+
396
+ <div class="metadata">
397
+ <p>Generated: {report.metadata.timestamp.strftime("%Y-%m-%d %H:%M:%S")}</p>
398
+ <p>Analysis Date: {result.timestamp.strftime("%Y-%m-%d %H:%M:%S")}</p>
399
+ <p>Analysis Duration: {result.analysis_duration:.2f} seconds</p>
400
+ </div>
401
+
402
+ <div class="summary-box">
403
+ <h2>Summary</h2>
404
+ <p><strong>Detected Logic Family:</strong> {result.detected_family} ({result.family_confidence * 100:.1f}% confidence)</p>
405
+ <p><strong>ICs Identified:</strong> {len(result.identified_ics)}</p>
406
+ <p><strong>Timing Measurements:</strong> {len(result.timing_measurements)}</p>
407
+ <p><strong>Open-Collector Detected:</strong> {"Yes" if result.open_collector_detected else "No"}</p>
408
+ {f"<p><strong>Asymmetry Ratio:</strong> {result.asymmetry_ratio:.2f}</p>" if result.open_collector_detected else ""}
409
+ </div>
410
+
411
+ {warnings_html}
412
+
413
+ <h2>IC Identification</h2>
414
+ {ic_table}
415
+
416
+ <h2>Timing Measurements</h2>
417
+ {timing_table}
418
+
419
+ <h2>Bill of Materials</h2>
420
+ {bom_table}
421
+
422
+ <h2>Visualizations</h2>
423
+ {plots_section}
424
+ </div>
425
+ </body>
426
+ </html>"""
427
+
428
+ return html
429
+
430
+
431
+ def _generate_markdown_report(report: VintageLogicReport) -> str:
432
+ """Generate markdown report content."""
433
+ result = report.result
434
+
435
+ # Generate IC table
436
+ ic_rows = []
437
+ for ic in result.identified_ics:
438
+ validation_status = (
439
+ "PASS" if all(v.get("passes", True) for v in ic.validation.values()) else "FAIL"
440
+ )
441
+ ic_rows.append(
442
+ f"| {ic.ic_name} | {ic.confidence * 100:.1f}% | {ic.family} | {validation_status} |"
443
+ )
444
+
445
+ ic_table = (
446
+ "| IC | Confidence | Family | Validation |\n|---|---|---|---|\n" + "\n".join(ic_rows)
447
+ if ic_rows
448
+ else "*No ICs identified*"
449
+ )
450
+
451
+ # Generate BOM table
452
+ bom_rows = []
453
+ for entry in result.bom:
454
+ bom_rows.append(
455
+ f"| {entry.part_number} | {entry.description} | {entry.quantity} | {entry.category} | {entry.notes or ''} |"
456
+ )
457
+
458
+ bom_table = (
459
+ "| Part Number | Description | Qty | Category | Notes |\n|---|---|---|---|---|\n"
460
+ + "\n".join(bom_rows)
461
+ if bom_rows
462
+ else "*No BOM entries*"
463
+ )
464
+
465
+ # Generate warnings
466
+ warnings_md = ""
467
+ if result.warnings:
468
+ warnings_list = "\n".join(f"- {w}" for w in result.warnings)
469
+ warnings_md = f"\n## Warnings\n\n{warnings_list}\n"
470
+
471
+ md = f"""# {report.metadata.title}
472
+
473
+ **Generated:** {report.metadata.timestamp.strftime("%Y-%m-%d %H:%M:%S")}
474
+ **Analysis Date:** {result.timestamp.strftime("%Y-%m-%d %H:%M:%S")}
475
+ **Analysis Duration:** {result.analysis_duration:.2f} seconds
476
+
477
+ ## Summary
478
+
479
+ - **Detected Logic Family:** {result.detected_family} ({result.family_confidence * 100:.1f}% confidence)
480
+ - **ICs Identified:** {len(result.identified_ics)}
481
+ - **Timing Measurements:** {len(result.timing_measurements)}
482
+ - **Open-Collector Detected:** {"Yes" if result.open_collector_detected else "No"}
483
+ {"- **Asymmetry Ratio:** " + f"{result.asymmetry_ratio:.2f}" if result.open_collector_detected else ""}
484
+
485
+ {warnings_md}
486
+
487
+ ## IC Identification
488
+
489
+ {ic_table}
490
+
491
+ ## Timing Measurements
492
+
493
+ | Parameter | Value |
494
+ |---|---|
495
+ """
496
+
497
+ for param, value in result.timing_measurements.items():
498
+ md += f"| {param} | {value * 1e9:.2f} ns |\n"
499
+
500
+ if not result.timing_measurements:
501
+ md += "*No timing measurements*\n"
502
+
503
+ md += f"""
504
+ ## Bill of Materials
505
+
506
+ {bom_table}
507
+
508
+ ## Visualizations
509
+
510
+ """
511
+
512
+ for plot_name, plot_path in report.plots.items():
513
+ md += f"### {plot_name.replace('_', ' ').title()}\n\n"
514
+ md += f"![{plot_name}]({plot_path})\n\n"
515
+
516
+ return md
517
+
518
+
519
+ __all__ = [
520
+ "ReportMetadata",
521
+ "VintageLogicReport",
522
+ "generate_vintage_logic_report",
523
+ ]
@@ -317,7 +317,11 @@ def detect_logic_family(
317
317
  # Score based on how well levels match
318
318
  low_match = 1.0 - min(1.0, abs(v_low - vol) / 0.5)
319
319
  high_match = 1.0 - min(1.0, abs(v_high - voh) / 0.5)
320
- vcc_match = 1.0 - min(1.0, abs(v_cc_est - vcc) / vcc)
320
+ # Handle VCC=0 for families like ECL
321
+ if vcc != 0:
322
+ vcc_match = 1.0 - min(1.0, abs(v_cc_est - vcc) / abs(vcc))
323
+ else:
324
+ vcc_match = 1.0 if abs(v_cc_est) < 0.5 else 0.0
321
325
 
322
326
  score = (low_match + high_match + vcc_match) / 3
323
327