"""
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