"""
JFET transistor symbols for JITX Standard Library
This module provides JFET (Junction Field Effect Transistor) symbol definitions
for N-Channel and P-Channel transistors (always depletion mode).
"""
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 Arc, ArcPolyline, Polyline
from jitx.symbol import Direction, Pin
from ..arrow import Arrow, ArrowConfig, ArrowConfigurable
from ..label import LabelConfigurable, LabelledSymbol
from .fet import FETJunction
if TYPE_CHECKING:
from ..context import SymbolStyleContext
# JFET constants
DEF_JFET_PITCH = 3.0 # Drain to source distance
DEF_JFET_WIDTH = 2.0 # Distance from D/S line to gate pin
DEF_JFET_PORCH_WIDTH = 0.5
DEF_JFET_BASE_LINE = 2.4
DEF_JFET_SD_CHANNEL_WIDTH = 0.8 # Width of source-drain channel body
DEF_JFET_ARROW_OFFSET = 0.1 # 10% offset along gate line
DEF_JFET_OUTLINE = True
DEF_JFET_LINE_WIDTH = 0.1
[docs]
@dataclass
class JFETConfig(LabelConfigurable, ArrowConfigurable):
"""
Configuration for JFET symbols
Defines the geometric and visual parameters for JFET symbols.
JFETs are always depletion-mode devices (solid channel line).
"""
pitch: float = DEF_JFET_PITCH
"""Distance between drain and source pins"""
width: float = DEF_JFET_WIDTH
"""Distance from drain/source line to gate pin"""
porch_width: float = DEF_JFET_PORCH_WIDTH
"""Length of porch lines from pins to channel"""
base_line_length: float = DEF_JFET_BASE_LINE
"""Length of the channel structure"""
sd_channel_width: float = DEF_JFET_SD_CHANNEL_WIDTH
"""Width of the source-drain channel body"""
arrow_offset: float = DEF_JFET_ARROW_OFFSET
"""Offset of arrow along gate-to-channel line (as fraction)"""
outline: bool = DEF_JFET_OUTLINE
"""Whether to draw circle outline around transistor"""
line_width: float = DEF_JFET_LINE_WIDTH
"""Width of symbol lines"""
[docs]
class JFETSymbol[T: JFETConfig](LabelledSymbol):
"""
JFET symbol with graphics and pins.
The JFET symbol consists of:
- Drain and source channel lines
- Solid channel (JFETs are always depletion mode)
- Gate arrow pointing into or out of channel
- Optional circle outline
Pins: 'G' (gate), 'D' (drain), 'S' (source)
"""
config: T
junction_type: FETJunction
drain_line: Shape[Polyline]
source_line: Shape[Polyline]
drain_porch: Shape[Polyline]
source_porch: Shape[Polyline]
channel: Shape[Polyline]
gate_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 JFET symbol."""
if context is None:
config = JFETConfig()
else:
config = context.jfet_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,
config: T | None = None,
**kwargs,
):
"""
Initialize JFET symbol.
Args:
junction_type: N_CHANNEL or P_CHANNEL
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_jfet_glyphs()
self._build_pins()
self._build_labels(ref=Direction.Right, value=Direction.Right)
def _build_jfet_glyphs(self) -> None:
"""Build the JFET transistor glyphs following Stanza implementation."""
p2 = self.pitch / 2.0
porch = self.porch_width
base_len = self.base_line_length
lw = self.line_width
sd_len = self.sd_channel_width
# Calculate porch endpoints (same as FET)
drain_porch_y = p2 - porch
source_porch_y = -(p2 - porch)
# Drain and source horizontal lines (Stanza lines 137-144)
# line-ch from (0,0) to (-sd_len, 0), positioned at porch heights
self.drain_line = Polyline(lw, [(0.0, drain_porch_y), (-sd_len, drain_porch_y)])
self.source_line = Polyline(
lw, [(0.0, source_porch_y), (-sd_len, source_porch_y)]
)
# Porches (Stanza lines 146-152)
# 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)]
)
# Channel (Stanza lines 154-162) - always solid for JFET
base_x = -sd_len
self.channel = Polyline(
lw, [(base_x, base_len / 2.0), (base_x, -base_len / 2.0)]
)
# Gate arrow (Stanza lines 164-184)
# This is complex: shaft + arrow with offset
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.5, 0.5),
line_width=lw,
)
# Calculate gate pin Y position
gate_y_raw = p2 - porch
if self.junction_type == FETJunction.N_CHANNEL:
gate_y = -gate_y_raw
else:
gate_y = gate_y_raw
# Total length from channel to gate pin
total_len = abs(-self.width) - sd_len
offset = total_len * self.arrow_offset
shaft_len = total_len - offset
# Update arrow config with computed shaft length
arrow_config = replace(arrow_config, shaft_length=shaft_len)
if self.junction_type == FETJunction.N_CHANNEL:
# N-channel: at (-sd_len, gate_y), rotated 180° (pointing left toward gate)
self.gate_arrow = Arrow((-sd_len, gate_y), 180.0, arrow_config)
else:
# P-channel: at (-(sd_len + total_len), gate_y), pointing right (toward channel)
self.gate_arrow = Arrow((-(sd_len + total_len), gate_y), 0.0, arrow_config)
# 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:`~.JFETConfig.pitch`."""
return self.config.pitch
@property
def width(self) -> float:
"""See :attr:`~.JFETConfig.width`."""
return self.config.width
@property
def porch_width(self) -> float:
"""See :attr:`~.JFETConfig.porch_width`."""
return self.config.porch_width
@property
def base_line_length(self) -> float:
"""See :attr:`~.JFETConfig.base_line_length`."""
return self.config.base_line_length
@property
def sd_channel_width(self) -> float:
"""See :attr:`~.JFETConfig.sd_channel_width`."""
return self.config.sd_channel_width
@property
def arrow_offset(self) -> float:
"""See :attr:`~.JFETConfig.arrow_offset`."""
return self.config.arrow_offset
@property
def outline(self) -> bool:
"""See :attr:`~.JFETConfig.outline`."""
return self.config.outline
@property
def line_width(self) -> float:
"""See :attr:`~.JFETConfig.line_width`."""
return self.config.line_width
@property
def label_config(self) -> LabelConfigurable:
"""Configuration object that provides label configuration"""
return self.config