Source code for jitxlib.symbols.transistor.bjt

"""
BJT transistor symbols for JITX Standard Library

This module provides BJT (Bipolar Junction Transistor) symbol definitions
for NPN and PNP transistors.
"""

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

from jitx.shapes import Shape
from jitx.shapes.primitive import Arc, ArcPolyline, Polyline
from jitx.symbol import Direction, Pin

from ..arrow import Arrow, ArrowConfig, ArrowConfigurable
from ..label import LabelConfigurable, LabelledSymbol

if TYPE_CHECKING:
    from ..context import SymbolStyleContext


# BJT constants
DEF_BJT_PITCH = 3.0  # Collector to emitter distance
DEF_BJT_WIDTH = 2.0  # Distance from C/E line to base pin
DEF_BJT_PORCH_WIDTH = 0.25
DEF_BJT_BASE_LINE = 2.3
DEF_BJT_OUTLINE = True
DEF_BJT_LINE_WIDTH = 0.1
DEF_BJT_PIN_LENGTH = 2.0
DEF_BJT_PAD_REF_SIZE = 0.75


[docs] class BJTJunction(Enum): """ BJT junction types Defines the junction type for bipolar junction transistors. """ NPN = "npn" PNP = "pnp"
[docs] @dataclass class BJTConfig(LabelConfigurable, ArrowConfigurable): """ Configuration for BJT symbols Defines the geometric and visual parameters for BJT transistor symbols. """ pitch: float = DEF_BJT_PITCH """Distance between collector and emitter pins""" width: float = DEF_BJT_WIDTH """Distance from collector/emitter line to base pin""" porch_width: float = DEF_BJT_PORCH_WIDTH """Length of porch lines from pins to angled lines""" base_line_length: float = DEF_BJT_BASE_LINE """Length of the vertical base line""" outline: bool = DEF_BJT_OUTLINE """Whether to draw circle outline around transistor""" line_width: float = DEF_BJT_LINE_WIDTH """Width of symbol lines"""
def _compute_circle_from_points( p1: tuple[float, float], p2: tuple[float, float], p3: tuple[float, float], ) -> tuple[tuple[float, float], float]: """ Compute center and radius of circle passing through 3 points. Uses the circumcircle formula. """ x1, y1 = p1 x2, y2 = p2 x3, y3 = p3 # Calculate the determinant A = x1 * (y2 - y3) - y1 * (x2 - x3) + x2 * y3 - x3 * y2 if abs(A) < 1e-10: raise ValueError("Points are collinear, cannot compute circle") # Calculate center coordinates B = ( (x1 * x1 + y1 * y1) * (y3 - y2) + (x2 * x2 + y2 * y2) * (y1 - y3) + (x3 * x3 + y3 * y3) * (y2 - y1) ) C = ( (x1 * x1 + y1 * y1) * (x2 - x3) + (x2 * x2 + y2 * y2) * (x3 - x1) + (x3 * x3 + y3 * y3) * (x1 - x2) ) cx = -B / (2 * A) cy = -C / (2 * A) # Calculate radius radius = math.sqrt((cx - x1) ** 2 + (cy - y1) ** 2) return ((cx, cy), radius)
[docs] class BJTSymbol[T: BJTConfig](LabelledSymbol): """ BJT transistor symbol with graphics and pins. The BJT symbol consists of: - Emitter line with arrow (direction indicates NPN/PNP) - Collector line - Base line (vertical) - Optional circle outline Pins: 'B' (base), 'C' (collector), 'E' (emitter) """ config: T junction_type: BJTJunction emitter_arrow: Arrow collector_line: Shape[Polyline] top_porch: Shape[Polyline] bottom_porch: Shape[Polyline] base_line: Shape[Polyline] base_conn: Shape[Polyline] outline_circle: Shape[ArcPolyline] | None B: Pin # Base C: Pin # Collector E: Pin # Emitter def _symbol_style_config(self, context: SymbolStyleContext | None = None) -> T: """Symbol style config for this BJT symbol.""" if context is None: config = BJTConfig() else: config = context.bjt_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, junction_type: BJTJunction = BJTJunction.NPN, config: T | None = None, **kwargs, ): """ Initialize BJT symbol. Args: junction_type: NPN or PNP transistor type 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.junction_type = junction_type self._build_bjt_glyphs() self._build_pins() self._build_labels(ref=Direction.Right, value=Direction.Right) def _build_bjt_glyphs(self) -> None: """Build the BJT transistor glyphs following Stanza implementation exactly.""" p2 = self.pitch / 2.0 porch = self.porch_width base_len = self.base_line_length lw = self.line_width # Get arrow configuration (Stanza line 113) arrow_config = self.config.get_arrow_config() if arrow_config is None: from ..arrow import ArrowStyle arrow_config = ArrowConfig( style=ArrowStyle.OPEN_ARROW, shaft_length=1.0, head_dims=(0.3, 0.5), line_width=lw, ) # Key measurements (Stanza lines 117-120) a_offset = p2 - porch a_len = arrow_config.shaft_length angle = 45.0 angle_rad = math.radians(angle) # Construct emitter arrow using Transform (Stanza lines 121-128) # Arrow is constructed from (0,0) to (a_len, 0), then rotated and translated if self.junction_type == BJTJunction.NPN: # loc(0.0, -a_offset) * loc(0.0, 0.0, 180-angle) * arrow # Arrow at (0, -a_offset), rotated 135° self.emitter_arrow = Arrow((0.0, -a_offset), 180.0 - angle, arrow_config) else: # PNP needs offset adjustment due to rotation # adjust = loc(-(a_len*sin(angle_rad)), -(a_len*cos(angle_rad))) # adjust * loc(0.0, a_offset) * loc(0.0, 0.0, angle) * arrow offset_x = -(a_len * math.sin(angle_rad)) offset_y = -(a_len * math.cos(angle_rad)) self.emitter_arrow = Arrow( (offset_x, a_offset + offset_y), angle, arrow_config ) # Construct collector line using Transform (Stanza lines 131-138) # Line from (0,0) to (a_len, 0), rotated and translated if self.junction_type == BJTJunction.NPN: # loc(0.0, a_offset) * loc(0.0, 0.0, -(180-angle)) * line-c # Line at (0, a_offset), rotated -135° c_angle_rad = math.radians(-(180.0 - angle)) end_x = a_len * math.cos(c_angle_rad) end_y = a_len * math.sin(c_angle_rad) self.collector_line = Polyline( lw, [(0.0, a_offset), (end_x, a_offset + end_y)] ) else: # loc(0.0, -a_offset) * loc(0.0, 0.0, 180-angle) * line-c # Line at (0, -a_offset), rotated 135° c_angle_rad = math.radians(180.0 - angle) end_x = a_len * math.cos(c_angle_rad) end_y = a_len * math.sin(c_angle_rad) self.collector_line = Polyline( lw, [(0.0, -a_offset), (end_x, -a_offset + end_y)] ) # Construct porches (Stanza lines 140-150) # Extend porches beyond pins to ensure visual connection porch_extension = 0.4 # Top porch: from porch endpoint up to pin (and slightly beyond) self.top_porch = Polyline(lw, [(0.0, p2 - porch), (0.0, p2 + porch_extension)]) # Bottom porch: from porch endpoint down to pin (and slightly beyond) self.bottom_porch = Polyline( lw, [(0.0, -p2 + porch), (0.0, -p2 - porch_extension)] ) # Base line (Stanza lines 153-155) base_x = -(a_len * math.cos(angle_rad)) self.base_line = Polyline( lw, [(base_x, base_len / 2.0), (base_x, -base_len / 2.0)] ) # Base connection (Stanza line 158) self.base_conn = Polyline(lw, [(base_x, 0.0), (-self.width, 0.0)]) # Optional outline circle if self.outline: self._build_outline_circle() else: self.outline_circle = None def _build_outline_circle(self) -> None: """Build the optional circle outline around the transistor. Uses a centered circle that encompasses the symbol bounds. """ p2 = self.pitch / 2.0 # Center point: midway between base pin and drain/source pins center_x = -self.width / 2.0 center_y = 0.0 # Radius to farthest pin radius_to_base = abs(center_x - (-self.width)) radius_to_ce = math.sqrt(center_x**2 + p2**2) radius = max(radius_to_base, radius_to_ce) # Add margin for visual clearance radius *= 1.2 # Create circle with Arc at the calculated center position self.outline_circle = ArcPolyline( self.line_width, [Arc((center_x, center_y), radius, 0.0, 360.0)] ) def _build_pins(self) -> None: """Build 'B', 'C', 'E' symbol pins.""" p2 = self.pitch / 2.0 # Pin positions using exact float values to match porch geometry # GridPoint type hint expects int, but JITX accepts float (like Stanza) self.B = Pin(at=(-self.width, 0), direction=Direction.Left) # type: ignore if self.junction_type == BJTJunction.NPN: # NPN: Collector at top, Emitter at bottom self.C = Pin(at=(0, p2), direction=Direction.Up) # type: ignore self.E = Pin(at=(0, -p2), direction=Direction.Down) # type: ignore else: # PNP: Emitter at top, Collector at bottom self.E = Pin(at=(0, p2), direction=Direction.Up) # type: ignore self.C = Pin(at=(0, -p2), direction=Direction.Down) # type: ignore @property def pitch(self) -> float: """See :attr:`~.BJTConfig.pitch`.""" return self.config.pitch @property def width(self) -> float: """See :attr:`~.BJTConfig.width`.""" return self.config.width @property def porch_width(self) -> float: """See :attr:`~.BJTConfig.porch_width`.""" return self.config.porch_width @property def base_line_length(self) -> float: """See :attr:`~.BJTConfig.base_line_length`.""" return self.config.base_line_length @property def outline(self) -> bool: """See :attr:`~.BJTConfig.outline`.""" return self.config.outline @property def line_width(self) -> float: """See :attr:`~.BJTConfig.line_width`.""" return self.config.line_width @property def label_config(self) -> LabelConfigurable: """Configuration object that provides label configuration""" return self.config