Coverage for src / tracekit / cli / characterize.py: 99%
80 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 23:04 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 23:04 +0000
1"""TraceKit Characterize Command implementing CLI-002.
3Provides CLI for buffer/signal characterization with automatic logic family
4detection and optional reference comparison.
7Example:
8 $ tracekit characterize 74hc04_output.wfm
9 $ tracekit characterize signal.wfm --logic-family CMOS_3V3
10 $ tracekit characterize signal.wfm --compare reference.wfm --output html
11"""
13from __future__ import annotations
15import logging
16from pathlib import Path
17from typing import TYPE_CHECKING, Any
19import click
21from tracekit.cli.main import format_output
23if TYPE_CHECKING:
24 from tracekit.core.types import WaveformTrace
26logger = logging.getLogger("tracekit.cli.characterize")
29@click.command() # type: ignore[misc]
30@click.argument("file", type=click.Path(exists=True)) # type: ignore[misc]
31@click.option( # type: ignore[misc]
32 "--type",
33 "analysis_type",
34 type=click.Choice(["buffer", "signal", "power"], case_sensitive=False),
35 default="buffer",
36 help="Type of characterization to perform.",
37)
38@click.option( # type: ignore[misc]
39 "--logic-family",
40 type=click.Choice(
41 ["TTL", "CMOS", "CMOS_3V3", "CMOS_5V", "LVTTL", "LVCMOS", "auto"],
42 case_sensitive=False,
43 ),
44 default="auto",
45 help="Logic family for buffer characterization (default: auto-detect).",
46)
47@click.option( # type: ignore[misc]
48 "--compare",
49 type=click.Path(exists=True),
50 default=None,
51 help="Reference file for comparison analysis.",
52)
53@click.option( # type: ignore[misc]
54 "--output",
55 type=click.Choice(["json", "csv", "html", "table"], case_sensitive=False),
56 default="table",
57 help="Output format (default: table).",
58)
59@click.option( # type: ignore[misc]
60 "--save-report",
61 type=click.Path(),
62 default=None,
63 help="Save HTML report to file.",
64)
65@click.pass_context # type: ignore[misc]
66def characterize(
67 ctx: click.Context,
68 file: str,
69 analysis_type: str,
70 logic_family: str,
71 compare: str | None,
72 output: str,
73 save_report: str | None,
74) -> None:
75 """Characterize buffer, signal, or power measurements.
77 Analyzes a waveform file and extracts timing, quality, and performance
78 characteristics. Supports automatic logic family detection and optional
79 comparison to a reference signal.
81 Args:
82 ctx: Click context object.
83 file: Path to waveform file to characterize.
84 analysis_type: Type of characterization (buffer, signal, power).
85 logic_family: Logic family for buffer characterization.
86 compare: Path to reference file for comparison analysis.
87 output: Output format (json, csv, html, table).
88 save_report: Path to save HTML report file.
90 Raises:
91 Exception: If characterization fails or file cannot be loaded.
93 Examples:
95 \b
96 # Simple buffer characterization
97 $ tracekit characterize 74hc04_output.wfm
99 \b
100 # Full characterization with reference
101 $ tracekit characterize signal.wfm \\
102 --logic-family CMOS_3V3 \\
103 --compare golden_reference.wfm \\
104 --save-report report.html
106 \b
107 # Power analysis
108 $ tracekit characterize power_rail.wfm --type power --output json
109 """
110 verbose = ctx.obj.get("verbose", 0)
112 if verbose:
113 logger.info(f"Characterizing: {file}")
114 logger.info(f"Analysis type: {analysis_type}")
115 logger.info(f"Logic family: {logic_family}")
117 try:
118 # Import here to avoid circular imports
119 from tracekit.loaders import load
121 # Load the main trace
122 logger.debug(f"Loading trace from {file}")
123 trace = load(file)
125 # Load reference trace if provided
126 reference_trace: WaveformTrace | None = None
127 if compare:
128 logger.debug(f"Loading reference trace from {compare}")
129 reference_trace = load(compare) # type: ignore[assignment]
131 # Perform characterization based on type
132 results = _perform_characterization(
133 trace=trace,
134 reference_trace=reference_trace,
135 analysis_type=analysis_type,
136 logic_family=logic_family,
137 )
139 # Add metadata
140 results["file"] = str(Path(file).name)
141 if compare:
142 results["reference_file"] = str(Path(compare).name)
144 # Generate HTML report if requested
145 if save_report:
146 html_content = format_output(results, "html")
147 with open(save_report, "w") as f:
148 f.write(html_content)
149 logger.info(f"Report saved to {save_report}")
151 # Output results
152 formatted = format_output(results, output)
153 click.echo(formatted)
155 except Exception as e:
156 logger.error(f"Characterization failed: {e}")
157 if verbose > 1:
158 raise
159 click.echo(f"Error: {e}", err=True)
160 ctx.exit(1)
163def _perform_characterization(
164 trace: Any,
165 reference_trace: Any | None,
166 analysis_type: str,
167 logic_family: str,
168) -> dict[str, Any]:
169 """Perform characterization analysis.
171 Calls actual TraceKit analysis functions based on the analysis type.
173 Args:
174 trace: Main trace to analyze.
175 reference_trace: Optional reference trace for comparison.
176 analysis_type: Type of analysis ('buffer', 'signal', 'power').
177 logic_family: Logic family for digital analysis.
179 Returns:
180 Dictionary of analysis results.
181 """
182 import numpy as np
184 from tracekit.analyzers.waveform.measurements import (
185 fall_time,
186 overshoot,
187 rise_time,
188 undershoot,
189 )
190 from tracekit.comparison.compare import compare_traces, similarity_score
191 from tracekit.inference import detect_logic_family
193 sample_rate = trace.metadata.sample_rate
194 data = trace.data
196 results: dict[str, Any] = {
197 "analysis_type": analysis_type,
198 "logic_family": logic_family,
199 "sample_rate": f"{sample_rate / 1e6:.1f} MHz",
200 "samples": len(data),
201 "duration": f"{len(data) / sample_rate * 1e3:.3f} ms",
202 }
204 if analysis_type == "buffer":
205 # Buffer characterization using actual workflow (WRK-001)
206 # Functions expect WaveformTrace, not ndarray
207 rt = rise_time(trace)
208 ft = fall_time(trace)
209 os_pct = overshoot(trace)
210 us_pct = undershoot(trace)
212 results.update(
213 {
214 "rise_time": f"{rt * 1e9:.2f} ns" if not np.isnan(rt) else "N/A",
215 "fall_time": f"{ft * 1e9:.2f} ns" if not np.isnan(ft) else "N/A",
216 "overshoot": f"{os_pct:.1f} %" if not np.isnan(os_pct) else "N/A",
217 "undershoot": f"{us_pct:.1f} %" if not np.isnan(us_pct) else "N/A",
218 "status": "PASS",
219 }
220 )
222 # Logic family detection
223 if logic_family == "auto":
224 detected = detect_logic_family(trace)
225 # detect_logic_family returns dict with 'primary' key
226 primary = detected.get("primary", {})
227 results["logic_family_detected"] = primary.get("name", "unknown")
228 results["confidence"] = f"{primary.get('confidence', 0) * 100:.0f}%"
229 else:
230 results["logic_family_detected"] = logic_family
232 elif analysis_type == "signal":
233 # Signal analysis
234 results.update(
235 {
236 "amplitude": f"{float(data.max() - data.min()):.3f} V",
237 "peak_to_peak": f"{float(data.max() - data.min()):.3f} V",
238 "mean": f"{float(data.mean()):.3f} V",
239 "rms": f"{float(np.sqrt((data**2).mean())):.3f} V",
240 }
241 )
243 elif analysis_type == "power": 243 ↛ 260line 243 didn't jump to line 260 because the condition on line 243 was always true
244 # Power analysis - compute power statistics directly from voltage
245 # Assume voltage trace, compute power (P = V^2/R, assume R=1 for relative)
246 power_data = data**2
247 avg_pwr = float(np.mean(power_data))
248 peak_pwr = float(np.max(power_data))
249 total_energy = float(np.sum(power_data) / sample_rate)
251 results.update(
252 {
253 "average_power": f"{avg_pwr * 1e3:.3f} mW",
254 "peak_power": f"{peak_pwr * 1e3:.3f} mW",
255 "energy": f"{total_energy * 1e6:.3f} uJ",
256 }
257 )
259 # Add comparison results if reference provided
260 if reference_trace is not None:
261 ref_data = reference_trace.data
262 # similarity_score expects WaveformTrace objects
263 sim = similarity_score(trace, reference_trace)
264 # compare_traces returns ComparisonResult dataclass
265 comparison_result = compare_traces(trace, reference_trace)
267 results["comparison"] = {
268 "correlation": f"{comparison_result.correlation:.4f}",
269 "amplitude_difference": f"{abs(float(data.mean()) - float(ref_data.mean())):.3f} V",
270 "similarity": f"{sim * 100:.1f}%",
271 }
273 return results