Coverage for src / tracekit / config / thresholds.py: 99%
138 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"""Threshold configuration for voltage levels and logic families.
3This module provides threshold configuration for digital signal analysis
4including logic family definitions, threshold profiles, and per-analysis
5overrides.
6"""
8from __future__ import annotations
10import logging
11import os
12from dataclasses import dataclass, field
13from pathlib import Path
15import yaml
17from tracekit.config.schema import validate_against_schema
18from tracekit.core.exceptions import ConfigurationError
20logger = logging.getLogger(__name__)
23@dataclass
24class LogicFamily:
25 """Logic family voltage threshold definition.
27 Defines voltage thresholds per IEEE/JEDEC standards for digital
28 signal interpretation.
30 Attributes:
31 name: Logic family name (e.g., "TTL", "CMOS_3V3")
32 VIH: Input high voltage threshold (V)
33 VIL: Input low voltage threshold (V)
34 VOH: Output high voltage (V)
35 VOL: Output low voltage (V)
36 VCC: Supply voltage (V)
37 description: Human-readable description
38 temperature_range: Operating temperature range (min, max) in C
39 noise_margin_high: High state noise margin (V)
40 noise_margin_low: Low state noise margin (V)
41 source: Origin of definition (builtin, user, file path)
43 Example:
44 >>> ttl = LogicFamily(
45 ... name="TTL",
46 ... VIH=2.0, VIL=0.8,
47 ... VOH=2.4, VOL=0.4,
48 ... VCC=5.0
49 ... )
50 >>> print(f"TTL noise margin high: {ttl.noise_margin_high}V")
51 """
53 name: str
54 VIH: float # Input high threshold
55 VIL: float # Input low threshold
56 VOH: float # Output high level
57 VOL: float # Output low level
58 VCC: float = 5.0
59 description: str = ""
60 temperature_range: tuple[float, float] = field(default_factory=lambda: (0, 70))
61 noise_margin_high: float | None = None
62 noise_margin_low: float | None = None
63 source: str = "builtin"
65 def __post_init__(self) -> None:
66 """Validate thresholds and compute noise margins."""
67 # Validate threshold ordering
68 if self.VIH <= self.VIL:
69 raise ConfigurationError(
70 f"Invalid thresholds for {self.name}: VIH ({self.VIH}V) must be > VIL ({self.VIL}V)"
71 )
72 if self.VOH <= self.VOL:
73 raise ConfigurationError(
74 f"Invalid thresholds for {self.name}: VOH ({self.VOH}V) must be > VOL ({self.VOL}V)"
75 )
77 # Compute noise margins if not provided
78 if self.noise_margin_high is None:
79 self.noise_margin_high = self.VOH - self.VIH
80 if self.noise_margin_low is None:
81 self.noise_margin_low = self.VIL - self.VOL
83 def get_threshold(self, percent: float = 50.0) -> float:
84 """Get threshold voltage at given percentage between VIL and VIH.
86 Args:
87 percent: Percentage between VIL (0%) and VIH (100%)
89 Returns:
90 Threshold voltage
91 """
92 return self.VIL + (self.VIH - self.VIL) * (percent / 100.0)
94 def with_temperature_derating(
95 self, temperature: float, derating_factor: float = 0.002
96 ) -> LogicFamily:
97 """Create copy with temperature-derated thresholds.
99 Args:
100 temperature: Operating temperature in Celsius
101 derating_factor: Derating factor per degree C (default 0.2%/C)
103 Returns:
104 New LogicFamily with adjusted thresholds
105 """
106 # Simple linear derating from nominal 25C
107 delta_t = temperature - 25.0
108 factor = 1.0 - (delta_t * derating_factor)
110 return LogicFamily(
111 name=f"{self.name}@{temperature}C",
112 VIH=self.VIH * factor,
113 VIL=self.VIL * factor,
114 VOH=self.VOH * factor,
115 VOL=self.VOL * factor,
116 VCC=self.VCC,
117 description=f"{self.description} (derated for {temperature}C)",
118 temperature_range=self.temperature_range,
119 source=self.source,
120 )
123@dataclass
124class ThresholdProfile:
125 """Named threshold profile combining logic family with adjustments.
127 Profiles allow users to save and reuse threshold configurations
128 for specific analysis scenarios.
130 Attributes:
131 name: Profile name
132 base_family: Base logic family name
133 overrides: Override values for specific thresholds
134 tolerance: Tolerance percentage (0-100)
135 description: Profile description
137 Example:
138 >>> profile = ThresholdProfile(
139 ... name="strict_ttl",
140 ... base_family="TTL",
141 ... overrides={"VIH": 2.2},
142 ... tolerance=0
143 ... )
144 """
146 name: str
147 base_family: str = "TTL"
148 overrides: dict[str, float] = field(default_factory=dict)
149 tolerance: float = 0.0 # 0-100%
150 description: str = ""
152 def apply_to(self, family: LogicFamily) -> LogicFamily:
153 """Apply profile overrides to a logic family.
155 Args:
156 family: Base logic family
158 Returns:
159 New LogicFamily with overrides applied
160 """
161 # Apply tolerance
162 factor = 1.0 + (self.tolerance / 100.0)
164 return LogicFamily(
165 name=f"{family.name}_{self.name}",
166 VIH=self.overrides.get("VIH", family.VIH),
167 VIL=self.overrides.get("VIL", family.VIL),
168 VOH=self.overrides.get("VOH", family.VOH * factor),
169 VOL=self.overrides.get("VOL", family.VOL / factor),
170 VCC=self.overrides.get("VCC", family.VCC),
171 description=f"{family.description} with {self.name} profile",
172 temperature_range=family.temperature_range,
173 source="profile",
174 )
177class ThresholdRegistry:
178 """Registry for logic families and threshold profiles.
180 Manages built-in and user-defined logic families with support
181 for runtime overrides and profile switching.
183 Example:
184 >>> registry = ThresholdRegistry()
185 >>> ttl = registry.get_family("TTL")
186 >>> cmos = registry.get_family("CMOS_3V3")
187 >>> families = registry.list_families()
188 """
190 _instance: ThresholdRegistry | None = None
192 def __new__(cls) -> ThresholdRegistry:
193 """Ensure singleton instance."""
194 if cls._instance is None:
195 cls._instance = super().__new__(cls)
196 cls._instance._families: dict[str, LogicFamily] = {} # type: ignore[misc, attr-defined]
197 cls._instance._profiles: dict[str, ThresholdProfile] = {} # type: ignore[misc, attr-defined]
198 cls._instance._session_overrides: dict[str, float] = {} # type: ignore[misc, attr-defined]
199 cls._instance._register_builtins()
200 return cls._instance
202 def _register_builtins(self) -> None:
203 """Register built-in logic family definitions."""
204 builtins = [
205 # TTL
206 LogicFamily(
207 name="TTL",
208 VIH=2.0,
209 VIL=0.8,
210 VOH=2.4,
211 VOL=0.4,
212 VCC=5.0,
213 description="Standard TTL (74xx series)",
214 ),
215 # CMOS variants
216 LogicFamily(
217 name="CMOS_5V",
218 VIH=3.5,
219 VIL=1.5,
220 VOH=4.9,
221 VOL=0.1,
222 VCC=5.0,
223 description="CMOS 5V (74HCxx series)",
224 ),
225 LogicFamily(
226 name="LVTTL_3V3",
227 VIH=2.0,
228 VIL=0.8,
229 VOH=2.4,
230 VOL=0.4,
231 VCC=3.3,
232 description="Low Voltage TTL 3.3V",
233 ),
234 LogicFamily(
235 name="LVCMOS_3V3",
236 VIH=2.0,
237 VIL=0.7,
238 VOH=2.4,
239 VOL=0.4,
240 VCC=3.3,
241 description="Low Voltage CMOS 3.3V",
242 ),
243 LogicFamily(
244 name="LVCMOS_2V5",
245 VIH=1.7,
246 VIL=0.7,
247 VOH=2.0,
248 VOL=0.4,
249 VCC=2.5,
250 description="Low Voltage CMOS 2.5V",
251 ),
252 LogicFamily(
253 name="LVCMOS_1V8",
254 VIH=1.17,
255 VIL=0.63,
256 VOH=1.35,
257 VOL=0.45,
258 VCC=1.8,
259 description="Low Voltage CMOS 1.8V",
260 ),
261 LogicFamily(
262 name="LVCMOS_1V5",
263 VIH=0.975,
264 VIL=0.525,
265 VOH=1.125,
266 VOL=0.375,
267 VCC=1.5,
268 description="Low Voltage CMOS 1.5V",
269 ),
270 # ECL
271 LogicFamily(
272 name="ECL",
273 VIH=-0.9,
274 VIL=-1.7,
275 VOH=-0.9,
276 VOL=-1.75,
277 VCC=-5.2,
278 description="Emitter-Coupled Logic (ECL 10K)",
279 ),
280 ]
282 for family in builtins:
283 self._families[family.name] = family # type: ignore[attr-defined]
285 # Built-in profiles
286 builtin_profiles = [
287 ThresholdProfile(
288 name="strict",
289 base_family="TTL",
290 tolerance=0,
291 description="Exact specification values",
292 ),
293 ThresholdProfile(
294 name="relaxed",
295 base_family="TTL",
296 tolerance=20,
297 description="20% tolerance for real-world signals",
298 ),
299 ThresholdProfile(
300 name="auto",
301 base_family="TTL",
302 tolerance=10,
303 description="Auto-adjusted based on signal confidence",
304 ),
305 ]
307 for profile in builtin_profiles:
308 self._profiles[profile.name] = profile # type: ignore[attr-defined]
310 def get_family(self, name: str) -> LogicFamily:
311 """Get logic family by name.
313 Args:
314 name: Logic family name (case-insensitive)
316 Returns:
317 Logic family definition
319 Raises:
320 KeyError: If family not found
321 """
322 # Try exact match first
323 if name in self._families: # type: ignore[attr-defined]
324 family = self._families[name] # type: ignore[attr-defined]
325 # Try case-insensitive match
326 elif name.upper() in self._families: # type: ignore[attr-defined]
327 family = self._families[name.upper()] # type: ignore[attr-defined]
328 else:
329 available = list(self._families.keys()) # type: ignore[attr-defined]
330 raise KeyError(f"Logic family '{name}' not found. Available: {available}")
332 # Apply session overrides
333 if self._session_overrides: # type: ignore[attr-defined]
334 return LogicFamily(
335 name=family.name,
336 VIH=self._session_overrides.get("VIH", family.VIH), # type: ignore[attr-defined]
337 VIL=self._session_overrides.get("VIL", family.VIL), # type: ignore[attr-defined]
338 VOH=self._session_overrides.get("VOH", family.VOH), # type: ignore[attr-defined]
339 VOL=self._session_overrides.get("VOL", family.VOL), # type: ignore[attr-defined]
340 VCC=self._session_overrides.get("VCC", family.VCC), # type: ignore[attr-defined]
341 description=family.description,
342 temperature_range=family.temperature_range,
343 source="override",
344 )
346 return family # type: ignore[no-any-return]
348 def list_families(self) -> list[str]:
349 """List all available logic families.
351 Returns:
352 List of family names
353 """
354 return sorted(self._families.keys()) # type: ignore[attr-defined]
356 def register_family(self, family: LogicFamily, *, namespace: str = "user") -> None:
357 """Register custom logic family.
359 Args:
360 family: Logic family definition
361 namespace: Namespace prefix for custom families
363 Example:
364 >>> custom = LogicFamily(name="my_custom", VIH=2.5, VIL=1.0, VOH=3.0, VOL=0.5)
365 >>> registry.register_family(custom)
366 >>> # Available as "user.my_custom"
367 """
368 # Namespace custom families
369 if namespace and not family.name.startswith(f"{namespace}."): 369 ↛ 372line 369 didn't jump to line 372 because the condition on line 369 was always true
370 name = f"{namespace}.{family.name}"
371 else:
372 name = family.name
374 # Update family with new name
375 family = LogicFamily(
376 name=name,
377 VIH=family.VIH,
378 VIL=family.VIL,
379 VOH=family.VOH,
380 VOL=family.VOL,
381 VCC=family.VCC,
382 description=family.description,
383 temperature_range=family.temperature_range,
384 source=family.source,
385 )
387 self._families[name] = family # type: ignore[attr-defined]
388 logger.info(f"Registered custom logic family: {name}")
390 def set_threshold_override(self, **kwargs: float) -> None:
391 """Set session-level threshold overrides.
393 Overrides persist for session lifetime until reset.
395 Args:
396 **kwargs: Threshold overrides (VIH, VIL, VOH, VOL, VCC)
398 Raises:
399 ValueError: If invalid threshold key or value out of range.
401 Example:
402 >>> registry.set_threshold_override(VIH=2.5, VIL=0.7)
403 """
404 valid_keys = {"VIH", "VIL", "VOH", "VOL", "VCC"}
405 for key, value in kwargs.items():
406 if key not in valid_keys:
407 raise ValueError(f"Invalid threshold key: {key}. Valid: {valid_keys}")
408 if not 0.0 <= value <= 10.0:
409 raise ValueError(f"Threshold {key}={value}V out of range (0-10V)")
410 self._session_overrides[key] = value # type: ignore[attr-defined]
412 logger.info(f"Set threshold overrides: {kwargs}")
414 def reset_overrides(self) -> None:
415 """Reset session threshold overrides."""
416 self._session_overrides.clear() # type: ignore[attr-defined]
417 logger.info("Reset threshold overrides")
419 def get_profile(self, name: str) -> ThresholdProfile:
420 """Get threshold profile by name.
422 Args:
423 name: Profile name
425 Returns:
426 Threshold profile
428 Raises:
429 KeyError: If profile not found.
430 """
431 if name not in self._profiles: # type: ignore[attr-defined]
432 raise KeyError(f"Profile '{name}' not found. Available: {list(self._profiles.keys())}") # type: ignore[attr-defined]
433 return self._profiles[name] # type: ignore[no-any-return, attr-defined]
435 def apply_profile(self, name: str) -> LogicFamily:
436 """Apply a threshold profile.
438 Args:
439 name: Profile name
441 Returns:
442 Logic family with profile applied
443 """
444 profile = self.get_profile(name)
445 base_family = self.get_family(profile.base_family)
446 return profile.apply_to(base_family)
448 def save_profile(
449 self, name: str, base_family: str | None = None, path: str | Path | None = None
450 ) -> None:
451 """Save current settings as named profile.
453 Args:
454 name: Profile name
455 base_family: Base family name (default: "TTL")
456 path: Optional file path to save
458 Example:
459 >>> registry.set_threshold_override(VIH=2.5)
460 >>> registry.save_profile("my_profile")
461 """
462 profile = ThresholdProfile(
463 name=name,
464 base_family=base_family or "TTL",
465 overrides=dict(self._session_overrides), # type: ignore[attr-defined]
466 description=f"User profile {name}",
467 )
468 self._profiles[name] = profile # type: ignore[attr-defined]
470 if path:
471 path = Path(path)
472 data = {
473 "name": profile.name,
474 "base_family": profile.base_family,
475 "overrides": profile.overrides,
476 "tolerance": profile.tolerance,
477 "description": profile.description,
478 }
479 with open(path, "w", encoding="utf-8") as f:
480 yaml.dump(data, f)
481 logger.info(f"Saved profile to {path}")
484def load_logic_family(path: str | Path) -> LogicFamily:
485 """Load logic family from YAML/JSON file.
487 Args:
488 path: Path to file
490 Returns:
491 Loaded logic family
492 """
493 path = Path(path)
495 with open(path, encoding="utf-8") as f:
496 data = yaml.safe_load(f)
498 # Validate against schema
499 validate_against_schema(data, "logic_family")
501 return LogicFamily(
502 name=data["name"],
503 VIH=data["VIH"],
504 VIL=data["VIL"],
505 VOH=data["VOH"],
506 VOL=data["VOL"],
507 VCC=data.get("VCC", 5.0),
508 description=data.get("description", ""),
509 temperature_range=tuple(data.get("temperature_range", {}).values()) or (0, 70),
510 noise_margin_high=data.get("noise_margin_high"),
511 noise_margin_low=data.get("noise_margin_low"),
512 source=str(path),
513 )
516def get_threshold_registry() -> ThresholdRegistry:
517 """Get the global threshold registry.
519 Returns:
520 Global ThresholdRegistry instance
521 """
522 return ThresholdRegistry()
525def get_user_logic_families_dir() -> Path:
526 """Get user directory for custom logic families.
528 Returns:
529 Path to ~/.tracekit/logic_families/
530 """
531 home = Path.home()
532 xdg_config = os.environ.get("XDG_CONFIG_HOME")
533 base = Path(xdg_config) if xdg_config else home / ".config"
535 dir_path = base / "tracekit" / "logic_families"
536 dir_path.mkdir(parents=True, exist_ok=True)
537 return dir_path
540def load_user_logic_families() -> list[LogicFamily]:
541 """Load all user-defined logic families.
543 Returns:
544 List of loaded logic families
545 """
546 families = []
547 user_dir = get_user_logic_families_dir()
549 for file_path in user_dir.glob("*.yaml"):
550 try:
551 family = load_logic_family(file_path)
552 families.append(family)
553 except Exception as e:
554 logger.warning(f"Failed to load logic family from {file_path}: {e}")
556 return families
559__all__ = [
560 "LogicFamily",
561 "ThresholdProfile",
562 "ThresholdRegistry",
563 "get_threshold_registry",
564 "get_user_logic_families_dir",
565 "load_logic_family",
566 "load_user_logic_families",
567]