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.
- oscura/__init__.py +1 -1
- oscura/analyzers/digital/__init__.py +48 -0
- oscura/analyzers/digital/extraction.py +195 -0
- oscura/analyzers/digital/ic_database.py +498 -0
- oscura/analyzers/digital/timing_paths.py +339 -0
- oscura/analyzers/digital/vintage.py +377 -0
- oscura/analyzers/digital/vintage_result.py +148 -0
- oscura/analyzers/protocols/__init__.py +22 -1
- oscura/analyzers/protocols/parallel_bus.py +449 -0
- oscura/automotive/__init__.py +1 -1
- oscura/export/__init__.py +12 -0
- oscura/export/wavedrom.py +430 -0
- oscura/exporters/json_export.py +47 -0
- oscura/exporters/vintage_logic_csv.py +247 -0
- oscura/reporting/__init__.py +7 -0
- oscura/reporting/vintage_logic_report.py +523 -0
- oscura/utils/autodetect.py +5 -1
- oscura/visualization/digital_advanced.py +718 -0
- oscura/visualization/figure_manager.py +156 -0
- {oscura-0.4.0.dist-info → oscura-0.5.0.dist-info}/METADATA +1 -1
- {oscura-0.4.0.dist-info → oscura-0.5.0.dist-info}/RECORD +24 -19
- oscura/automotive/dtc/data.json +0 -2763
- oscura/schemas/bus_configuration.json +0 -322
- oscura/schemas/device_mapping.json +0 -182
- oscura/schemas/packet_format.json +0 -418
- oscura/schemas/protocol_definition.json +0 -363
- {oscura-0.4.0.dist-info → oscura-0.5.0.dist-info}/WHEEL +0 -0
- {oscura-0.4.0.dist-info → oscura-0.5.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.4.0.dist-info → oscura-0.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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"\n\n"
|
|
515
|
+
|
|
516
|
+
return md
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
__all__ = [
|
|
520
|
+
"ReportMetadata",
|
|
521
|
+
"VintageLogicReport",
|
|
522
|
+
"generate_vintage_logic_report",
|
|
523
|
+
]
|
oscura/utils/autodetect.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|