Source code for jitxlib.symbols.crystal.crystal

"""
Crystal symbols for JITX Standard Library

This module provides crystal/resonator symbol definitions with optional case pins.
"""

from __future__ import annotations
import math
from dataclasses import dataclass, replace
from typing import TYPE_CHECKING, cast

from jitx.shapes import Shape
from jitx.shapes.primitive import Polyline
from jitx.symbol import Direction, Pin
from jitx.units import percent, Quantity

from ..label import LabelConfigurable, LabelledSymbol

if TYPE_CHECKING:
    from ..context import SymbolStyleContext


# Crystal constants
DEF_CRYSTAL_PITCH = 2.0
DEF_CRYSTAL_LINE_WIDTH = 0.05
DEF_RESONATOR_LINE_LEN = 100 * percent  # 100% of body width
DEF_RESONATOR_LINE_OFFSET = 0.1
DEF_CRYSTAL_BODY = (0.9, 0.3)  # (width, height)


def _resolve_percentage(value: float | Quantity, reference: float) -> float:
    """Resolve a value that could be absolute or percentage."""
    if isinstance(value, Quantity):
        if str(value.units) == "percent":
            return value.magnitude * reference / 100.0
        return float(value.magnitude)
    return float(value)


[docs] @dataclass class CrystalConfig(LabelConfigurable): """ Configuration for crystal symbols Defines the geometric and visual parameters for crystal/resonator symbols. """ pitch: float = DEF_CRYSTAL_PITCH """Distance between pin points""" line_width: float = DEF_CRYSTAL_LINE_WIDTH """Width of the crystal lines""" resonator_line_len: float = DEF_RESONATOR_LINE_LEN """Length of resonator lines (absolute or percentage of body width)""" resonator_line_offset: float = DEF_RESONATOR_LINE_OFFSET """Offset from body to resonator lines""" crystal_body: tuple[float, float] = DEF_CRYSTAL_BODY """Body dimensions as (width, height)""" crystal_case: tuple[float, float] | None = None """Optional case dimensions as (width, height), None for no case""" case_ports: int = 0 """Number of case connection pins (0 for none)"""
[docs] class CrystalSymbol[T: CrystalConfig](LabelledSymbol): """ Crystal/resonator symbol with graphics and pins. The crystal symbol consists of a rectangular body with resonator lines on either side, and optional case outline with case pins. Pins: 'p[1]', 'p[2]' (non-polarized), optional 'case[n]' pins """ config: T front_porch: Shape[Polyline] back_porch: Shape[Polyline] resonator_line_top: Shape[Polyline] resonator_line_bottom: Shape[Polyline] body: Shape[Polyline] case_outline: tuple[Shape[Polyline], Shape[Polyline]] | None p: dict[int, Pin] case: dict[int, Pin] | None def _symbol_style_config(self, context: SymbolStyleContext | None = None) -> T: """Symbol style config for this crystal symbol.""" if context is None: config = CrystalConfig() else: config = context.crystal_config return cast(T, config) def _lookup_config( self, config: T | None = None, context: SymbolStyleContext | None = None ) -> T: """Lookup the config for this symbol.""" if config is None: return self._symbol_style_config(context) return config def __init__(self, config: T | None = None, **kwargs): """ Initialize crystal symbol. Args: config: Config object, or None to use defaults **kwargs: Individual parameters to override defaults """ from ..context import SymbolStyleContext context = SymbolStyleContext.get() config = self._lookup_config(config, context) self.config = replace(config, **kwargs) self._build_crystal_glyphs() self._build_pins() self._build_labels(ref=Direction.Right, value=Direction.Right) def _build_crystal_glyphs(self) -> None: """Build the crystal body, resonator lines, and porch lines.""" p2 = self.pitch / 2.0 body_w, body_h = self.crystal_body body_w2 = body_w / 2.0 body_h2 = body_h / 2.0 # Resolve resonator line length res_len = _resolve_percentage(self.resonator_line_len, body_w) res_len_2 = res_len / 2.0 # Calculate porch end (where resonator line starts) porch_end = body_h2 + self.resonator_line_offset # Validate that total width fits within pitch total_width = (2 * self.resonator_line_offset) + self.line_width + body_h if total_width >= self.pitch: raise ValueError( f"Crystal body and resonator lines ({total_width}) exceed pitch ({self.pitch})" ) # Front porch (top side) self.front_porch = Polyline(self.line_width, [(0.0, p2), (0.0, porch_end)]) # Top resonator line self.resonator_line_top = Polyline( self.line_width, [(-res_len_2, porch_end), (res_len_2, porch_end)] ) # Crystal body (rectangle outline) body_pts = [ (-body_w2, body_h2), (body_w2, body_h2), (body_w2, -body_h2), (-body_w2, -body_h2), (-body_w2, body_h2), ] self.body = Polyline(self.line_width, body_pts) # Bottom resonator line self.resonator_line_bottom = Polyline( self.line_width, [(-res_len_2, -porch_end), (res_len_2, -porch_end)] ) # Back porch (bottom side) self.back_porch = Polyline(self.line_width, [(0.0, -porch_end), (0.0, -p2)]) # Optional case outline if self.crystal_case is not None: self._build_case_outline() else: self.case_outline = None def _build_case_outline(self) -> None: """Build the optional case outline (two vertical C-shaped lines).""" if self.crystal_case is None: self.case_outline = None return case_w, case_h = self.crystal_case case_w2 = case_w / 2.0 case_h2 = case_h / 2.0 body_w2 = self.crystal_body[0] / 2.0 # Calculate case position (outside the body) case_x = body_w2 + self.resonator_line_offset + (case_w / 2.0) # Left case line (C-shape facing right) left_case_pts = [ (-case_x, case_h2), (-case_x - case_w2, case_h2), (-case_x - case_w2, -case_h2), (-case_x, -case_h2), ] # Right case line (C-shape facing left) right_case_pts = [ (case_x, case_h2), (case_x + case_w2, case_h2), (case_x + case_w2, -case_h2), (case_x, -case_h2), ] self.case_outline = ( Polyline(self.line_width, left_case_pts), Polyline(self.line_width, right_case_pts), ) def _build_pins(self) -> None: """Build 'p[1]', 'p[2]' pins and optional case pins.""" w = math.floor(self.pitch / 2) self.p = { 1: Pin(at=(0, w), direction=Direction.Up), 2: Pin(at=(0, -w), direction=Direction.Down), } # Build case pins if specified if self.case_ports > 0: self._build_case_pins() else: self.case = None def _build_case_pins(self) -> None: """Build case pins on the left side.""" if self.case_ports <= 0: self.case = None return body_w2 = self.crystal_body[0] / 2.0 case_x = -(body_w2 + 1.0) # Position to the left of body if self.case_ports == 1: # Single case pin at center self.case = {1: Pin(at=(math.floor(case_x), 0), direction=Direction.Left)} else: # Multiple case pins distributed vertically pin_pitch = 1.0 pin_span = (self.case_ports - 1) * pin_pitch start_y = pin_span / 2.0 self.case = {} for i in range(self.case_ports): y = start_y - (i * pin_pitch) self.case[i + 1] = Pin( at=(math.floor(case_x), math.floor(y)), direction=Direction.Left ) @property def pitch(self) -> float: """See :attr:`~.CrystalConfig.pitch`.""" return self.config.pitch @property def line_width(self) -> float: """See :attr:`~.CrystalConfig.line_width`.""" return self.config.line_width @property def resonator_line_len(self) -> float: """See :attr:`~.CrystalConfig.resonator_line_len`.""" return self.config.resonator_line_len @property def resonator_line_offset(self) -> float: """See :attr:`~.CrystalConfig.resonator_line_offset`.""" return self.config.resonator_line_offset @property def crystal_body(self) -> tuple[float, float]: """See :attr:`~.CrystalConfig.crystal_body`.""" return self.config.crystal_body @property def crystal_case(self) -> tuple[float, float] | None: """See :attr:`~.CrystalConfig.crystal_case`.""" return self.config.crystal_case @property def case_ports(self) -> int: """See :attr:`~.CrystalConfig.case_ports`.""" return self.config.case_ports @property def label_config(self) -> LabelConfigurable: """Configuration object that provides label configuration""" return self.config