Source code for jitxlib.symbols.transistor.fet

"""
FET transistor symbols for JITX Standard Library

This module provides MOSFET symbol definitions for N-Channel and P-Channel
transistors in Enhancement and Depletion modes.
"""

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


# FET constants
DEF_FET_PITCH = 3.0  # Drain to source distance
DEF_FET_WIDTH = 2.0  # Distance from D/S line to gate pin
DEF_FET_PORCH_WIDTH = 0.5
DEF_FET_BASE_LINE = 2.3
DEF_FET_OUTLINE = True
DEF_FET_LINE_WIDTH = 0.1
DEF_FET_CHANNEL_GAP = 0.15  # Gap between channel segments for enhancement mode


[docs] class FETJunction(Enum): """ FET junction (channel) types Defines the channel type for field effect transistors. """ N_CHANNEL = "n_channel" P_CHANNEL = "p_channel"
[docs] class FETMode(Enum): """ FET operating modes Defines the operating mode for MOSFETs (enhancement or depletion). """ ENHANCEMENT = "enhancement" DEPLETION = "depletion"
[docs] @dataclass class FETConfig(LabelConfigurable, ArrowConfigurable): """ Configuration for FET symbols Defines the geometric and visual parameters for MOSFET symbols. """ pitch: float = DEF_FET_PITCH """Distance between drain and source pins""" width: float = DEF_FET_WIDTH """Distance from drain/source line to gate pin""" porch_width: float = DEF_FET_PORCH_WIDTH """Length of porch lines from pins to channel""" base_line_length: float = DEF_FET_BASE_LINE """Length of the channel structure""" outline: bool = DEF_FET_OUTLINE """Whether to draw circle outline around transistor""" line_width: float = DEF_FET_LINE_WIDTH """Width of symbol lines""" channel_gap: float = DEF_FET_CHANNEL_GAP """Gap between channel segments for enhancement mode"""
[docs] class FETSymbol[T: FETConfig](LabelledSymbol): """ MOSFET symbol with graphics and pins. The FET symbol consists of: - Drain and source channel lines - Gate structure (solid or dashed based on mode) - Body arrow (direction indicates N/P channel) - Body-to-source connection - Optional circle outline Pins: 'G' (gate), 'D' (drain), 'S' (source) """ config: T junction_type: FETJunction mode_type: FETMode drain_line: Shape[Polyline] source_line: Shape[Polyline] drain_porch: Shape[Polyline] source_porch: Shape[Polyline] body_source_conn: Shape[Polyline] channel: Shape[Polyline] | list[Shape[Polyline]] gate_line: Shape[Polyline] gate_conn: Shape[Polyline] body_arrow: Arrow outline_circle: Shape[ArcPolyline] | None G: Pin # Gate D: Pin # Drain S: Pin # Source def _symbol_style_config(self, context: SymbolStyleContext | None = None) -> T: """Symbol style config for this FET symbol.""" if context is None: config = FETConfig() else: config = context.fet_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: FETJunction = FETJunction.N_CHANNEL, mode_type: FETMode = FETMode.ENHANCEMENT, config: T | None = None, **kwargs, ): """ Initialize FET symbol. Args: junction_type: N_CHANNEL or P_CHANNEL mode_type: ENHANCEMENT or DEPLETION 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.mode_type = mode_type self._build_fet_glyphs() self._build_pins() self._build_labels(ref=Direction.Right, value=Direction.Right) def _build_fet_glyphs(self) -> None: """Build the FET transistor glyphs following Stanza implementation.""" p2 = self.pitch / 2.0 porch = self.porch_width base_len = self.base_line_length lw = self.line_width # Get arrow configuration for body arrow 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=0.8, head_dims=(0.3, 0.5), line_width=lw, ) a_len = arrow_config.shaft_length # Body arrow (Stanza lines 126-130) if self.junction_type == FETJunction.N_CHANNEL: # N-channel: arrow at (-a_len, 0), pointing right (0°) self.body_arrow = Arrow((-a_len, 0.0), 0.0, arrow_config) else: # P-channel: arrow at (0, 0), rotated 180° (pointing left) self.body_arrow = Arrow((0.0, 0.0), 180.0, arrow_config) # Calculate porch endpoints (Stanza lines 132-134) # Pin positions from FET-pin-positions drain_porch_y = p2 - porch source_porch_y = -(p2 - porch) # Drain and source horizontal lines (Stanza lines 137-143) # line-ch from (0,0) to (-a_len, 0), positioned at porch heights self.drain_line = Polyline(lw, [(0.0, drain_porch_y), (-a_len, drain_porch_y)]) self.source_line = Polyline( lw, [(0.0, source_porch_y), (-a_len, source_porch_y)] ) # Porches (Stanza lines 145-151) # Drain porch: from drain_porch_y up to drain pin (and slightly beyond) porch_extension = 0.4 self.drain_porch = Polyline( lw, [(0.0, drain_porch_y), (0.0, p2 + porch_extension)] ) # Source porch: from source_porch_y down to source pin (and slightly beyond) self.source_porch = Polyline( lw, [(0.0, source_porch_y), (0.0, -p2 - porch_extension)] ) # Body-to-source line (Stanza lines 153-160) inv_porch_len = abs(source_porch_y) if self.junction_type == FETJunction.N_CHANNEL: # N-channel: vertical line from (0,0) down, rotated 180° self.body_source_conn = Polyline(lw, [(0.0, 0.0), (0.0, -inv_porch_len)]) else: # P-channel: vertical line from (0,0) up self.body_source_conn = Polyline(lw, [(0.0, 0.0), (0.0, inv_porch_len)]) # Channel structure (Stanza lines 163-180) base_x = -a_len if self.mode_type == FETMode.DEPLETION: # Solid channel line self.channel = Polyline( lw, [(base_x, base_len / 2.0), (base_x, -base_len / 2.0)] ) else: # Enhancement mode: three dashed segments (Stanza lines 168-175) # Dash from (0,0) to (0, dash_len) positioned at various Y locations gap = 0.25 dash_len = (base_len - (2.0 * gap)) / 3.0 d2 = dash_len / 2.0 # Three equal-length segments with gaps between them segments = [] # Top segment: starts at (d2 + gap), goes UP by dash_len segments.append( Polyline(lw, [(base_x, d2 + gap), (base_x, d2 + gap + dash_len)]) ) # Middle segment: starts at -d2, goes UP by dash_len segments.append(Polyline(lw, [(base_x, -d2), (base_x, -d2 + dash_len)])) # Bottom segment: starts at -(3*d2 + gap), goes UP by dash_len segments.append( Polyline( lw, [ (base_x, -(3.0 * d2 + gap)), (base_x, -(3.0 * d2 + gap) + dash_len), ], ) ) self.channel = segments # Gate line (Stanza lines 182-186) gate_x = base_x - 0.3 gate_len = 2.0 * inv_porch_len g2 = gate_len / 2.0 self.gate_line = Polyline(lw, [(gate_x, g2), (gate_x, -g2)]) # Gate connection (Stanza lines 188-195) if self.junction_type == FETJunction.N_CHANNEL: gate_conn_y = -g2 else: gate_conn_y = g2 self.gate_conn = Polyline( lw, [(-self.width, gate_conn_y), (gate_x, gate_conn_y)] ) # 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.""" p2 = self.pitch / 2.0 # Center between gate pin and drain/source pins center_x = -self.width / 2.0 center_y = 0.0 # Radius to farthest pin porch = self.porch_width gate_y_raw = p2 - porch if self.junction_type == FETJunction.N_CHANNEL: gate_y = -gate_y_raw else: gate_y = gate_y_raw radius_to_gate = math.sqrt( (center_x - (-self.width)) ** 2 + (center_y - gate_y) ** 2 ) radius_to_ds = math.sqrt(center_x**2 + p2**2) radius = max(radius_to_gate, radius_to_ds) # 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 'G', 'D', 'S' symbol pins.""" p2 = self.pitch / 2.0 porch = self.porch_width # Gate pin position depends on channel type (matches FET-pin-positions) gate_y_raw = p2 - porch if self.junction_type == FETJunction.N_CHANNEL: gate_y = -gate_y_raw else: gate_y = gate_y_raw # Pin positions using exact float values to match porch geometry # GridPoint type hint expects int, but JITX accepts float (like Stanza) self.G = Pin(at=(-self.width, gate_y), direction=Direction.Left) # type: ignore if self.junction_type == FETJunction.N_CHANNEL: # N-channel: Drain at top, Source at bottom self.D = Pin(at=(0, p2), direction=Direction.Up) # type: ignore self.S = Pin(at=(0, -p2), direction=Direction.Down) # type: ignore else: # P-channel: Source at top, Drain at bottom self.S = Pin(at=(0, p2), direction=Direction.Up) # type: ignore self.D = Pin(at=(0, -p2), direction=Direction.Down) # type: ignore @property def pitch(self) -> float: """See :attr:`~.FETConfig.pitch`.""" return self.config.pitch @property def width(self) -> float: """See :attr:`~.FETConfig.width`.""" return self.config.width @property def porch_width(self) -> float: """See :attr:`~.FETConfig.porch_width`.""" return self.config.porch_width @property def base_line_length(self) -> float: """See :attr:`~.FETConfig.base_line_length`.""" return self.config.base_line_length @property def outline(self) -> bool: """See :attr:`~.FETConfig.outline`.""" return self.config.outline @property def line_width(self) -> float: """See :attr:`~.FETConfig.line_width`.""" return self.config.line_width @property def channel_gap(self) -> float: """See :attr:`~.FETConfig.channel_gap`.""" return self.config.channel_gap @property def label_config(self) -> LabelConfigurable: """Configuration object that provides label configuration""" return self.config