mcltspice/src/mcp_ltspice/asc_generator.py
Ryan Malloy c56ce918b4 Expand .asc schematic templates to 10 topologies, fix opamp subcircuit
Add 7 new graphical schematic templates (differential amp, buck converter,
LDO regulator, H-bridge, common emitter, Colpitts oscillator) and rewrite
inverting amp to actually include an op-amp instead of just passive components.

Fix UniversalOpamp2 subcircuit error: the .asy symbol defines SpiceModel as
"level2", so SYMATTR Value must be omitted to let the built-in model name
resolve. Previously emitting SYMATTR Value UniversalOpamp2 caused LTspice
to search for a non-existent subcircuit.

Fix wire-through-pin routing bugs: vertical wires crossing intermediate
opamp/source pins auto-connect at those pins, creating unintended shorts.
Rerouted V1-to-In+ paths to avoid crossing In- pins in non-inverting,
common-emitter, Colpitts, and differential amp templates.

Refactor generate_schematic tool from hardcoded if/elif to registry dispatch
via _ASC_TEMPLATES dict, matching the _TEMPLATES pattern for netlists.

All 10 templates verified: simulate with zero errors and zero NC nodes.
255 tests pass, source lint clean.
2026-02-11 00:46:13 -07:00

1260 lines
46 KiB
Python

"""Programmatic LTspice .asc schematic file generation.
Generates graphical schematics (the .asc format LTspice uses for its GUI),
not just text netlists. Components are placed using absolute pin positions
derived from the .asy symbol files and rotated with the standard CCW
transform: R90 applies (px, py) → (-py, px) relative to the component origin.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from pathlib import Path
# LTspice grid spacing -- all coordinates should be multiples of this
GRID = 80
_SPICE_SUFFIXES = {
"T": 1e12, "G": 1e9, "MEG": 1e6, "K": 1e3,
"M": 1e-3, "U": 1e-6, "N": 1e-9, "P": 1e-12, "F": 1e-15,
}
def _parse_spice_value(value: str) -> float:
"""Convert a SPICE-style value string to a float (e.g., '100k' → 100000.0)."""
value = value.strip()
try:
return float(value)
except ValueError:
pass
upper = value.upper()
for suffix, mult in sorted(_SPICE_SUFFIXES.items(), key=lambda x: -len(x[0])):
if upper.endswith(suffix):
try:
return float(value[: len(value) - len(suffix)]) * mult
except ValueError:
continue
raise ValueError(f"Cannot parse SPICE value: {value!r}")
# Pin positions (relative to component origin) from LTspice .asy files.
# R0 orientation. Key = symbol name, value = list of (px, py) per pin.
_PIN_OFFSETS: dict[str, list[tuple[int, int]]] = {
# 2-pin passives & sources
"voltage": [(0, 16), (0, 96)], # pin+ , pin-
"res": [(16, 16), (16, 96)], # pinA , pinB
"cap": [(16, 0), (16, 64)], # pinA , pinB
"ind": [(16, 16), (16, 96)], # pinA , pinB (same body as res)
"diode": [(16, 0), (16, 64)], # anode(+) , cathode(-)
# 3-pin semiconductors
"npn": [(64, 0), (0, 48), (64, 96)], # C , B , E
"pnp": [(64, 0), (0, 48), (64, 96)], # C , B , E (same geometry)
"nmos": [(48, 0), (0, 80), (48, 96)], # D , G , S
"pmos": [(48, 0), (0, 80), (48, 96)], # D , G , S
# 4-pin MOSFETs (with body)
"nmos4": [(48, 0), (0, 80), (48, 96), (48, 48)], # D , G , S , B
"pmos4": [(48, 0), (0, 80), (48, 96), (48, 48)], # D , G , S , B
# 5-pin op-amp (note: negative offsets = pins extend left/above origin)
"OpAmps/UniversalOpamp2": [
(-32, 16), # In+ (non-inverting)
(-32, -16), # In- (inverting)
(0, -32), # V+ (positive supply)
(0, 32), # V- (negative supply)
(32, 0), # OUT
],
}
def _rotate(px: int, py: int, rotation: int) -> tuple[int, int]:
"""Apply LTspice rotation to a relative pin offset.
LTspice rotations are counterclockwise: (px, py) → (-py, px) for R90.
"""
if rotation == 0:
return px, py
if rotation == 90:
return -py, px
if rotation == 180:
return -px, -py
if rotation == 270:
return py, -px
return px, py
def pin_position(
symbol: str, pin_index: int, cx: int, cy: int, rotation: int = 0
) -> tuple[int, int]:
"""Compute absolute pin position for a component.
Args:
symbol: Base symbol name (``"res"``, ``"cap"``, ``"voltage"``, ...)
pin_index: 0 for first pin, 1 for second pin, etc.
cx: Component origin X
cy: Component origin Y
rotation: 0, 90, 180, or 270
Returns:
Absolute (x, y) of the pin.
"""
offsets = _PIN_OFFSETS.get(symbol, [(0, 0), (0, 80)])
px, py = offsets[pin_index % len(offsets)]
rx, ry = _rotate(px, py, rotation)
return cx + rx, cy + ry
@dataclass
class _WireEntry:
x1: int
y1: int
x2: int
y2: int
@dataclass
class _SymbolEntry:
symbol: str
name: str
value: str
x: int
y: int
rotation: int # 0, 90, 180, 270
windows: list[str] = field(default_factory=list)
@dataclass
class _FlagEntry:
x: int
y: int
name: str
@dataclass
class _TextEntry:
x: int
y: int
content: str
class AscSchematic:
"""Builder for LTspice .asc schematic files.
All ``add_*`` methods return ``self`` for chaining::
sch = (AscSchematic()
.add_component("res", "R1", "1k", 160, 176, rotation=90)
.add_wire(80, 176, 160, 176)
.add_ground(80, 256))
"""
def __init__(self, sheet_w: int = 880, sheet_h: int = 680) -> None:
self._sheet_w = sheet_w
self._sheet_h = sheet_h
self._wires: list[_WireEntry] = []
self._symbols: list[_SymbolEntry] = []
self._flags: list[_FlagEntry] = []
self._texts: list[_TextEntry] = []
# -- builder methods -----------------------------------------------------
def add_component(
self,
symbol: str,
name: str,
value: str,
x: int,
y: int,
rotation: int = 0,
) -> AscSchematic:
"""Place a component symbol.
Args:
symbol: LTspice symbol name (``res``, ``cap``, ``voltage``, etc.)
name: Instance name (``R1``, ``C1``, ``V1``, ...)
value: Component value (``1k``, ``100n``, ``AC 1``, ...)
x: X coordinate (should be on 80-pixel grid)
y: Y coordinate
rotation: 0, 90, 180, or 270 degrees
"""
windows: list[str] = []
# For resistors and inductors placed at R90, shift the WINDOW lines
# so labels sit neatly above/below the body
if rotation == 90 and symbol in ("res", "ind", "ind2"):
windows = [
"WINDOW 0 0 56 VBottom 2",
"WINDOW 3 32 56 VTop 2",
]
self._symbols.append(_SymbolEntry(symbol, name, value, x, y, rotation, windows))
return self
def add_wire(self, x1: int, y1: int, x2: int, y2: int) -> AscSchematic:
"""Add a wire segment between two points."""
self._wires.append(_WireEntry(x1, y1, x2, y2))
return self
def add_ground(self, x: int, y: int) -> AscSchematic:
"""Place a ground flag (net name ``0``)."""
self._flags.append(_FlagEntry(x, y, "0"))
return self
def add_net_label(self, name: str, x: int, y: int) -> AscSchematic:
"""Place a named net label (e.g., ``out``, ``vdd``)."""
self._flags.append(_FlagEntry(x, y, name))
return self
def add_directive(self, text: str, x: int, y: int) -> AscSchematic:
"""Add a SPICE directive (rendered with ``!`` prefix)."""
self._texts.append(_TextEntry(x, y, text))
return self
# -- output --------------------------------------------------------------
def render(self) -> str:
"""Render the schematic to an ``.asc`` format string."""
lines: list[str] = []
lines.append("Version 4")
lines.append(f"SHEET 1 {self._sheet_w} {self._sheet_h}")
for w in self._wires:
lines.append(f"WIRE {w.x1} {w.y1} {w.x2} {w.y2}")
for f in self._flags:
lines.append(f"FLAG {f.x} {f.y} {f.name}")
for s in self._symbols:
lines.append(f"SYMBOL {s.symbol} {s.x} {s.y} R{s.rotation}")
for win in s.windows:
lines.append(win)
lines.append(f"SYMATTR InstName {s.name}")
if s.value:
lines.append(f"SYMATTR Value {s.value}")
for t in self._texts:
lines.append(f"TEXT {t.x} {t.y} Left 2 !{t.content}")
return "\n".join(lines) + "\n"
def save(self, path: Path | str) -> Path:
"""Write the schematic to an ``.asc`` file on disk."""
path = Path(path)
path.write_text(self.render(), encoding="utf-8")
return path
# ============================================================================
# Layout helper functions -- auto-placed, ready-to-simulate schematics
# ============================================================================
def generate_rc_lowpass(r: str = "1k", c: str = "100n") -> AscSchematic:
"""Generate an RC lowpass filter schematic with AC analysis.
Signal flow (left to right)::
V1 --[R1]-- out --+
|
[C1]
|
GND
Args:
r: Resistor value (e.g., ``"1k"``, ``"4.7k"``)
c: Capacitor value (e.g., ``"100n"``, ``"10p"``)
Returns:
An ``AscSchematic`` ready to ``.save()`` or ``.render()``.
"""
sch = AscSchematic()
# Component origins (all on 16-pixel sub-grid for pin alignment)
# V1 at (80, 80) R0: pin+ = (80, 96), pin- = (80, 176)
# R1 at (160, 80) R0: pinA = (176, 96), pinB = (176, 176)
# C1 at (256, 176) R0: pinA = (272, 176), pinB = (272, 240)
v1p = pin_position("voltage", 0, 80, 80) # (80, 96)
v1n = pin_position("voltage", 1, 80, 80) # (80, 176)
r1a = pin_position("res", 0, 160, 80) # (176, 96)
r1b = pin_position("res", 1, 160, 80) # (176, 176)
c1a = pin_position("cap", 0, 256, 176) # (272, 176)
c1b = pin_position("cap", 1, 256, 176) # (272, 240)
# Wires
sch.add_wire(*v1p, *r1a) # V1+ to R1 top
sch.add_wire(*r1b, *c1a) # R1 bottom to C1 top (junction = "out")
# Components
sch.add_component("voltage", "V1", "AC 1", 80, 80)
sch.add_component("res", "R1", r, 160, 80)
sch.add_component("cap", "C1", c, 256, 176)
# Ground flags at V1- and C1 bottom
sch.add_ground(*v1n)
sch.add_ground(*c1b)
# Output net label at the R1-C1 junction
sch.add_net_label("out", *r1b)
# Simulation directive
sch.add_directive(".ac dec 100 1 1meg", 80, 296)
return sch
def generate_voltage_divider(
r1: str = "10k",
r2: str = "10k",
vin: str = "5",
) -> AscSchematic:
"""Generate a voltage divider schematic with operating-point analysis.
Topology (vertical)::
V1+ --[R1]-- out --[R2]-- GND
V1- = GND
Args:
r1: Top resistor value
r2: Bottom resistor value
vin: DC input voltage
Returns:
An ``AscSchematic`` ready to ``.save()`` or ``.render()``.
"""
sch = AscSchematic()
# Vertical layout: V1 on left, R1 and R2 stacked vertically on right
# V1 at (80, 80) R0: pin+ = (80, 96), pin- = (80, 176)
# R1 at (176, 0) R0: pinA = (192, 16), pinB = (192, 96)
# R2 at (176, 96) R0: pinA = (192, 112), pinB = (192, 192)
v1p = pin_position("voltage", 0, 80, 80) # (80, 96)
v1n = pin_position("voltage", 1, 80, 80) # (80, 176)
r1a = pin_position("res", 0, 176, 0) # (192, 16)
r1b = pin_position("res", 1, 176, 0) # (192, 96)
r2a = pin_position("res", 0, 176, 96) # (192, 112)
r2b = pin_position("res", 1, 176, 96) # (192, 192)
# Wires: V1+ to R1 top, R1 bottom to R2 top (junction = "out")
sch.add_wire(*v1p, *r1a) # need to connect (80,96) to (192,16)
# Route: V1+ up then right then down to R1 top
sch.add_wire(80, 96, 80, 16)
sch.add_wire(80, 16, 192, 16)
# R1 bottom to R2 top
sch.add_wire(*r1b, *r2a)
# Components
sch.add_component("voltage", "V1", vin, 80, 80)
sch.add_component("res", "R1", r1, 176, 0)
sch.add_component("res", "R2", r2, 176, 96)
# Ground
sch.add_ground(*v1n)
sch.add_ground(*r2b)
# Net label at R1-R2 junction
sch.add_net_label("out", *r1b)
# Directive
sch.add_directive(".op", 80, 240)
return sch
def generate_inverting_amp(
rin: str = "10k",
rf: str = "100k",
) -> AscSchematic:
"""Generate an inverting op-amp amplifier schematic.
Topology::
V1 --[Rin]--> In-(inv) ---[Rf]--- out
In+(non-inv) --> GND
Supply: Vpos=+15V, Vneg=-15V
The gain is ``-Rf/Rin``.
Args:
rin: Input resistor value
rf: Feedback resistor value
Returns:
An ``AscSchematic`` ready to ``.save()`` or ``.render()``.
"""
sch = AscSchematic(sheet_w=1200, sheet_h=880)
oa_sym = "OpAmps/UniversalOpamp2"
# Op-amp U1 at (512, 336) — same position as non-inverting amp
inp = pin_position(oa_sym, 0, 512, 336) # In+ = (480, 352)
inn = pin_position(oa_sym, 1, 512, 336) # In- = (480, 320)
vp = pin_position(oa_sym, 2, 512, 336) # V+ = (512, 304)
vn = pin_position(oa_sym, 3, 512, 336) # V- = (512, 368)
out = pin_position(oa_sym, 4, 512, 336) # OUT = (544, 336)
# Signal source V1 at (80, 256)
v1p = pin_position("voltage", 0, 80, 256) # (80, 272)
v1n = pin_position("voltage", 1, 80, 256) # (80, 352)
# Rin: horizontal (R90) between V1 and In-
# Origin (400, 304): pinA=(384,320), pinB=(304,320)
rin_a = pin_position("res", 0, 400, 304, 90) # (384, 320)
rin_b = pin_position("res", 1, 400, 304, 90) # (304, 320)
# Rf: horizontal (R90) above the op-amp for feedback
# Origin (560, 208): pinA=(544,224), pinB=(464,224)
rf_a = pin_position("res", 0, 560, 208, 90) # (544, 224)
rf_b = pin_position("res", 1, 560, 208, 90) # (464, 224)
# Supply: Vpos at (688, 176), Vneg at (688, 416)
vpos_p = pin_position("voltage", 0, 688, 176) # (688, 192)
vpos_n = pin_position("voltage", 1, 688, 176) # (688, 272)
vneg_p = pin_position("voltage", 0, 688, 416) # (688, 432)
vneg_n = pin_position("voltage", 1, 688, 416) # (688, 512)
# === WIRING ===
# V1+ to Rin pinB: route RIGHT first then down (avoids crossing V1-)
sch.add_wire(v1p[0], v1p[1], rin_b[0], v1p[1]) # (80,272) → (304,272)
sch.add_wire(rin_b[0], v1p[1], *rin_b) # (304,272) → (304,320)
# Rin pinA to In-
sch.add_wire(*rin_a, inn[0], inn[1]) # (384,320) → (480,320)
# Rf feedback: OUT up to Rf pinA, Rf pinB left and down to inv junction
sch.add_wire(out[0], out[1], *rf_a) # (544,336) → (544,224)
sch.add_wire(*rf_b, rin_a[0], rf_b[1]) # (464,224) → (384,224)
sch.add_wire(rin_a[0], rf_b[1], *rin_a) # (384,224) → (384,320)
# Supply wiring
sch.add_wire(vp[0], vp[1], vp[0], vpos_p[1]) # V+ up to (512,192)
sch.add_wire(vp[0], vpos_p[1], *vpos_p) # right to Vpos+
sch.add_wire(vn[0], vn[1], vn[0], vneg_n[1]) # V- down to (512,512)
sch.add_wire(vn[0], vneg_n[1], *vneg_n) # right to Vneg-
# === COMPONENTS ===
sch.add_component("voltage", "V1", "AC 1", 80, 256)
sch.add_component(oa_sym, "U1", "", 512, 336)
sch.add_component("res", "Rin", rin, 400, 304, rotation=90)
sch.add_component("res", "Rf", rf, 560, 208, rotation=90)
sch.add_component("voltage", "Vpos", "15", 688, 176)
sch.add_component("voltage", "Vneg", "15", 688, 416)
# === FLAGS ===
sch.add_ground(*v1n)
sch.add_ground(*inp) # In+ to GND (inverting configuration)
sch.add_ground(*vpos_n)
sch.add_ground(*vneg_p)
sch.add_net_label("out", *out)
sch.add_directive(".ac dec 100 1 1meg", 80, 560)
return sch
def generate_non_inverting_amp(
rin: str = "10k",
rf: str = "100k",
) -> AscSchematic:
"""Generate a non-inverting op-amp amplifier schematic.
Topology::
V1 --> In+
U1 --> out
GND --> Rin --> In-
^--- Rf --- out
Gain = 1 + Rf/Rin. Supply: +/-15V
Args:
rin: Input resistor (In- to GND)
rf: Feedback resistor (In- to out)
"""
sch = AscSchematic(sheet_w=1200, sheet_h=880)
oa_sym = "OpAmps/UniversalOpamp2"
# Op-amp U1 at (512, 336)
inp = pin_position(oa_sym, 0, 512, 336) # In+ = (480, 352)
inn = pin_position(oa_sym, 1, 512, 336) # In- = (480, 320)
vp = pin_position(oa_sym, 2, 512, 336) # V+ = (512, 304)
vn = pin_position(oa_sym, 3, 512, 336) # V- = (512, 368)
out = pin_position(oa_sym, 4, 512, 336) # OUT = (544, 336)
# Signal source V1 at (80, 256)
v1p = pin_position("voltage", 0, 80, 256) # (80, 272)
v1n = pin_position("voltage", 1, 80, 256) # (80, 352)
# Rin: In- node down to GND. Want pinA at In- junction (352, 320).
# res origin so that pinA = (352, 320): origin = (336, 304)
rin_a = pin_position("res", 0, 336, 304) # (352, 320)
rin_b = pin_position("res", 1, 336, 304) # (352, 400)
# Rf: horizontal (R90) above the op-amp for feedback path
# R90 res: pinA offset=(-16,16), pinB offset=(-96,16)
# Place at origin (560, 208) so: pinA=(544, 224), pinB=(464, 224)
rf_a = pin_position("res", 0, 560, 208, 90) # (544, 224)
rf_b = pin_position("res", 1, 560, 208, 90) # (464, 224)
# Supply: Vpos at (688, 176), Vneg at (688, 416)
vpos_p = pin_position("voltage", 0, 688, 176) # (688, 192) = vdd
vpos_n = pin_position("voltage", 1, 688, 176) # (688, 272)
vneg_p = pin_position("voltage", 0, 688, 416) # (688, 432)
vneg_n = pin_position("voltage", 1, 688, 416) # (688, 512) = vss
# === WIRING ===
# V1+ to In+: go RIGHT then DOWN then RIGHT to avoid crossing both
# V1- at (80,352) and In- at (480,320).
# Route: (80,272) → (448,272) → (448,352) → (480,352)
sch.add_wire(v1p[0], v1p[1], 448, v1p[1]) # (80,272) → (448,272) horiz
sch.add_wire(448, v1p[1], 448, inp[1]) # (448,272) → (448,352) vert
sch.add_wire(448, inp[1], inp[0], inp[1]) # (448,352) → (480,352) horiz
# In- to Rin junction
sch.add_wire(inn[0], inn[1], rin_a[0], rin_a[1]) # (480,320) → (352,320)
# Rf feedback: OUT up to Rf pinA, Rf pinB left and down to junction
sch.add_wire(out[0], out[1], rf_a[0], rf_a[1]) # (544,336) → (544,224) vertical
sch.add_wire(rf_b[0], rf_b[1], rin_a[0], rf_b[1]) # (464,224) → (352,224) horizontal
sch.add_wire(rin_a[0], rf_b[1], rin_a[0], rin_a[1]) # (352,224) → (352,320) vertical
# Supply wiring
sch.add_wire(vp[0], vp[1], vp[0], vpos_p[1]) # V+ up to (512,192)
sch.add_wire(vp[0], vpos_p[1], vpos_p[0], vpos_p[1]) # right to Vpos+
sch.add_wire(vn[0], vn[1], vn[0], vneg_n[1]) # V- down to (512,512)
sch.add_wire(vn[0], vneg_n[1], vneg_n[0], vneg_n[1]) # right to Vneg-
# === COMPONENTS ===
sch.add_component("voltage", "V1", "AC 1", 80, 256)
sch.add_component(oa_sym, "U1", "", 512, 336)
sch.add_component("res", "Rin", rin, 336, 304)
sch.add_component("res", "Rf", rf, 560, 208, rotation=90)
sch.add_component("voltage", "Vpos", "15", 688, 176)
sch.add_component("voltage", "Vneg", "15", 688, 416)
# === FLAGS ===
sch.add_ground(*v1n)
sch.add_ground(*rin_b)
sch.add_ground(*vpos_n)
sch.add_ground(*vneg_p)
sch.add_net_label("out", *out)
sch.add_directive(".ac dec 100 1 1meg", 80, 560)
return sch
def generate_common_emitter_amp(
rc: str = "2.2k",
rb1: str = "56k",
rb2: str = "12k",
re: str = "1k",
cc_in: str = "10u",
cc_out: str = "10u",
ce: str = "47u",
vcc: str = "12",
bjt_model: str = "2N2222",
) -> AscSchematic:
"""Generate a common-emitter amplifier schematic.
Topology::
Vcc --[RC]--> collector --[CC_out]--> out
Vcc --[RB1]--> base
in --[CC_in]--> base
base --[RB2]--> GND
emitter --[RE]--> GND
emitter --[CE]--> GND (bypass)
Args:
rc: Collector resistor
rb1: Base bias resistor (to Vcc)
rb2: Base bias resistor (to GND)
re: Emitter resistor
cc_in: Input coupling capacitor
cc_out: Output coupling capacitor
ce: Emitter bypass capacitor
vcc: Supply voltage
bjt_model: BJT model name
"""
sch = AscSchematic(sheet_w=1200, sheet_h=880)
# Q1 (NPN) at (400, 288): C=(464, 288), B=(400, 336), E=(464, 384)
qc = pin_position("npn", 0, 400, 288) # collector
qb = pin_position("npn", 1, 400, 288) # base
qe = pin_position("npn", 2, 400, 288) # emitter
# RC from Vcc rail to collector.
# Want pinB at collector (464, 288). res at (448, 192): pinB=(464, 288). Good.
rc_a = pin_position("res", 0, 448, 192) # (464, 208) = vcc rail
# RE from emitter to GND.
# Want pinA at emitter (464, 384). res at (448, 368): pinA=(464, 384).
re_b = pin_position("res", 1, 448, 368) # (464, 464) = GND
# RB1 from Vcc rail to base. Vertical, placed to the left of Q1.
# Want pinB at base Y (336). res at (240, 240): pinA=(256, 256), pinB=(256, 336)
rb1_a = pin_position("res", 0, 240, 240) # (256, 256)
rb1_b = pin_position("res", 1, 240, 240) # (256, 336)
# RB2 from base to GND.
# Want pinA at base Y (336). res at (240, 320): pinA=(256, 336), pinB=(256, 416)
rb2_b = pin_position("res", 1, 240, 320) # (256, 416)
# CC_in (input coupling cap) horizontal, connecting input to base.
# cap R90: pinA=origin+(-16,0)→(origin_x, origin_y+16)... wait let me recalculate.
# cap R90: pinA offset (16,0) → rotate90 → (0, 16), pinB offset (16,64) → (-64, 16)
# At origin (368, 320): pinA = (368, 336), pinB = (304, 336)
# pinA at (368, 336) close to base (400, 336). pinB at (304, 336).
# Wire from pinA (368, 336) to base (400, 336).
ccin_a = pin_position("cap", 0, 368, 320, 90) # (368, 336)
ccin_b = pin_position("cap", 1, 368, 320, 90) # (304, 336)
# CC_out (output coupling cap) to the right of collector.
# cap R90 at origin (560, 272): pinA = (560, 288), pinB = (496, 288)
# pinB near collector (464, 288), pinA extends right to output
# Actually pinB = (560-64, 288) = (496, 288). Wire from collector (464,288) to (496,288).
ccout_a = pin_position("cap", 0, 560, 272, 90) # (560, 288)
ccout_b = pin_position("cap", 1, 560, 272, 90) # (496, 288)
# CE (emitter bypass cap) parallel to RE.
# Place to the right of RE. cap at (528, 384): pinA=(544, 384), pinB=(544, 448)
ce_a = pin_position("cap", 0, 528, 384) # (544, 384)
ce_b = pin_position("cap", 1, 528, 384) # (544, 448)
# Vcc source at (80, 96): pin+=(80, 112), pin-=(80, 192)
vcc_p = pin_position("voltage", 0, 80, 96) # (80, 112)
vcc_n = pin_position("voltage", 1, 80, 96) # (80, 192)
# Vin source at (80, 288): pin+=(80, 304), pin-=(80, 384)
vin_p = pin_position("voltage", 0, 80, 288) # (80, 304)
vin_n = pin_position("voltage", 1, 80, 288) # (80, 384)
# === WIRING ===
# Vcc rail at y=208 — route RIGHT from Vcc+ first (avoid crossing Vcc- at 80,192)
vcc_y = rc_a[1] # 208
sch.add_wire(vcc_p[0], vcc_p[1], 160, vcc_p[1]) # (80,112) → (160,112)
sch.add_wire(160, vcc_p[1], 160, vcc_y) # (160,112) → (160,208)
sch.add_wire(160, vcc_y, rc_a[0], vcc_y) # (160,208) → (464,208) = RC top
sch.add_wire(rb1_a[0], rb1_a[1], rb1_a[0], vcc_y) # RB1 top up to rail
sch.add_wire(rb1_a[0], vcc_y, rc_a[0], vcc_y) # connect to rail
# RB1 bottom to base, RB2 top at same junction
sch.add_wire(rb1_b[0], rb1_b[1], qb[0], qb[1]) # RB1 bottom → base
# CC_in to base
sch.add_wire(ccin_a[0], ccin_a[1], qb[0], qb[1]) # CC_in right → base
# Input: Vin+ to CC_in left
sch.add_wire(vin_p[0], vin_p[1], ccin_b[0], ccin_b[1]) # Vin+ → CC_in left
# Collector to CC_out
sch.add_wire(qc[0], qc[1], ccout_b[0], ccout_b[1]) # collector → CC_out left
# Emitter to CE (bypass cap in parallel with RE)
sch.add_wire(qe[0], qe[1], ce_a[0], ce_a[1]) # emitter → CE top
# CE bottom to RE bottom (shared GND rail)
sch.add_wire(ce_b[0], ce_b[1], re_b[0], re_b[1]) # CE bot → RE bot
# === COMPONENTS ===
sch.add_component("npn", "Q1", bjt_model, 400, 288)
sch.add_component("res", "RC", rc, 448, 192)
sch.add_component("res", "RE", re, 448, 368)
sch.add_component("res", "RB1", rb1, 240, 240)
sch.add_component("res", "RB2", rb2, 240, 320)
sch.add_component("cap", "CC1", cc_in, 368, 320, rotation=90)
sch.add_component("cap", "CC2", cc_out, 560, 272, rotation=90)
sch.add_component("cap", "CE", ce, 528, 384)
sch.add_component("voltage", "Vcc", vcc, 80, 96)
sch.add_component("voltage", "Vin", "SINE(0 10m 1k)", 80, 288)
# === FLAGS ===
sch.add_ground(*vcc_n)
sch.add_ground(*vin_n)
sch.add_ground(*rb2_b)
sch.add_ground(*re_b)
sch.add_net_label("out", *ccout_a)
sch.add_directive(".tran 5m", 80, 528)
return sch
def generate_colpitts_oscillator(
ind: str = "1u",
c1: str = "100p",
c2: str = "100p",
rb: str = "47k",
rc: str = "1k",
re: str = "470",
vcc: str = "12",
bjt_model: str = "2N2222",
) -> AscSchematic:
"""Generate a Colpitts oscillator schematic.
Topology::
Vcc --[RC]--> collector --[L1]--> tank
Vcc --[RB]--> base
tank --[C1]--> base
tank --[C2]--> GND
emitter --[RE]--> GND
f ~ 1/(2*pi*sqrt(L*C1*C2/(C1+C2)))
"""
sch = AscSchematic(sheet_w=1200, sheet_h=880)
# Q1 NPN at (400, 288): C=(464,288), B=(400,336), E=(464,384)
qc = pin_position("npn", 0, 400, 288)
qb = pin_position("npn", 1, 400, 288)
# RC: Vcc to collector. res at (448, 192): pinA=(464,208), pinB=(464,288)
rc_a = pin_position("res", 0, 448, 192)
# RE: emitter to GND. res at (448, 368): pinA=(464,384), pinB=(464,464)
re_b = pin_position("res", 1, 448, 368)
# RB: Vcc to base. res at (240, 240): pinA=(256,256), pinB=(256,336)
rb_a = pin_position("res", 0, 240, 240)
rb_b = pin_position("res", 1, 240, 240)
# Vcc source at (80, 96): pin+=(80,112), pin-=(80,192)
vcc_p = pin_position("voltage", 0, 80, 96)
vcc_n = pin_position("voltage", 1, 80, 96)
# L1 horizontal (R90) from collector to tank node.
# ind R90 at origin (576, 272):
# pinA = origin+(-16,16) = (560, 288) — right/tank side
# pinB = origin+(-96,16) = (480, 288) — left/collector side
l1_a = pin_position("ind", 0, 576, 272, 90) # (560, 288) = tank
l1_b = pin_position("ind", 1, 576, 272, 90) # (480, 288) = near collector
# C1 from tank down to base (feedback).
# cap at (544, 288): pinA=(560, 288)=tank (same as l1_a), pinB=(560, 352)
c1_b = pin_position("cap", 1, 544, 288) # (560, 352)
# C2 from tank to GND.
# cap at (640, 288): pinA=(656, 288), pinB=(656, 352)
c2_a = pin_position("cap", 0, 640, 288) # (656, 288)
c2_b = pin_position("cap", 1, 640, 288) # (656, 352)
# === WIRING ===
vcc_y = rc_a[1] # 208
# Vcc rail: route RIGHT from Vcc+ first (avoid crossing Vcc- at 80,192)
sch.add_wire(vcc_p[0], vcc_p[1], 160, vcc_p[1]) # (80,112) → (160,112)
sch.add_wire(160, vcc_p[1], 160, vcc_y) # (160,112) → (160,208)
sch.add_wire(160, vcc_y, rc_a[0], vcc_y) # (160,208) → RC top
sch.add_wire(rb_a[0], rb_a[1], rb_a[0], vcc_y)
sch.add_wire(rb_a[0], vcc_y, rc_a[0], vcc_y)
# RB bottom to base
sch.add_wire(rb_b[0], rb_b[1], qb[0], qb[1])
# Collector to L1 left pin
sch.add_wire(qc[0], qc[1], l1_b[0], l1_b[1])
# Tank node: L1 right pin to C1 top (same point at 560,288)
# Wire from tank to C2 top
sch.add_wire(l1_a[0], l1_a[1], c2_a[0], c2_a[1])
# C1 bottom to base: L-route down then left
sch.add_wire(c1_b[0], c1_b[1], qb[0], c1_b[1]) # horizontal
sch.add_wire(qb[0], c1_b[1], qb[0], qb[1]) # vertical to base
# === COMPONENTS ===
sch.add_component("npn", "Q1", bjt_model, 400, 288)
sch.add_component("res", "RC", rc, 448, 192)
sch.add_component("res", "RE", re, 448, 368)
sch.add_component("res", "RB", rb, 240, 240)
sch.add_component("ind", "L1", ind, 576, 272, rotation=90)
sch.add_component("cap", "C1", c1, 544, 288)
sch.add_component("cap", "C2", c2, 640, 288)
sch.add_component("voltage", "Vcc", vcc, 80, 96)
# === FLAGS ===
sch.add_ground(*vcc_n)
sch.add_ground(*re_b)
sch.add_ground(*c2_b)
sch.add_net_label("out", *qc)
sch.add_directive(".tran 100u", 80, 528)
sch.add_directive(".ic V(out)=6", 80, 560)
return sch
def generate_differential_amp(
r1: str = "10k",
r2: str = "10k",
r3: str = "10k",
r4: str = "10k",
) -> AscSchematic:
"""Generate a differential amplifier schematic.
Topology::
V1 --[R1]--> inv(-) --[R2]--> out
V2 --[R3]--> non-inv(+)
non-inv(+) --[R4]--> GND
Supply: +/-15V
Vout = (R2/R1) * (V2 - V1) when R2/R1 = R4/R3
Args:
r1: Input resistor to inverting node
r2: Feedback resistor
r3: Input resistor to non-inverting node
r4: Non-inverting node to GND
"""
sch = AscSchematic(sheet_w=1200, sheet_h=880)
oa_sym = "OpAmps/UniversalOpamp2"
# U1 at (512, 336): In+=(480,352), In-=(480,320), V+=(512,304), V-=(512,368), OUT=(544,336)
inp = pin_position(oa_sym, 0, 512, 336) # (480, 352)
inn = pin_position(oa_sym, 1, 512, 336) # (480, 320)
vp = pin_position(oa_sym, 2, 512, 336) # (512, 304)
vn = pin_position(oa_sym, 3, 512, 336) # (512, 368)
out = pin_position(oa_sym, 4, 512, 336) # (544, 336)
# R1 horizontal (R90) from V1 to inv junction.
# R90 at (448, 304): pinA=(432,320), pinB=(352,320)
r1_a = pin_position("res", 0, 448, 304, 90) # (432, 320) near In-
r1_b = pin_position("res", 1, 448, 304, 90) # (352, 320) toward V1
# R2 horizontal (R90) above op-amp for feedback.
# R90 at (560, 208): pinA=(544,224), pinB=(464,224)
r2_a = pin_position("res", 0, 560, 208, 90) # (544, 224) near OUT
r2_b = pin_position("res", 1, 560, 208, 90) # (464, 224)
# R3 horizontal (R90) from V2 to noninv junction.
# R90 at (448, 336): pinA=(432,352), pinB=(352,352)
r3_a = pin_position("res", 0, 448, 336, 90) # (432, 352) near In+
r3_b = pin_position("res", 1, 448, 336, 90) # (352, 352) toward V2
# R4 vertical from noninv junction to GND.
# res at (336, 352): pinA=(352,368), pinB=(352,448)
r4_a = pin_position("res", 0, 336, 352) # (352, 368)
r4_b = pin_position("res", 1, 336, 352) # (352, 448)
# V1 at (80, 224): pin+=(80,240), pin-=(80,320)
v1p = pin_position("voltage", 0, 80, 224)
v1n = pin_position("voltage", 1, 80, 224)
# V2 at (80, 384): pin+=(80,400), pin-=(80,480)
v2p = pin_position("voltage", 0, 80, 384)
v2n = pin_position("voltage", 1, 80, 384)
# Supply: Vpos at (688, 176), Vneg at (688, 416)
vpos_p = pin_position("voltage", 0, 688, 176)
vpos_n = pin_position("voltage", 1, 688, 176)
vneg_p = pin_position("voltage", 0, 688, 416)
vneg_n = pin_position("voltage", 1, 688, 416)
# === WIRING ===
# Inv path: R1 pinA to In-
sch.add_wire(r1_a[0], r1_a[1], inn[0], inn[1])
# V1 to R1 pinB: route RIGHT first (avoid crossing V1- at 80,320)
sch.add_wire(v1p[0], v1p[1], r1_b[0], v1p[1]) # (80,240) → (352,240)
sch.add_wire(r1_b[0], v1p[1], r1_b[0], r1_b[1]) # (352,240) → (352,320)
# R2 feedback: OUT up to R2 pinA, R2 pinB left and down to inv junction
sch.add_wire(out[0], out[1], r2_a[0], r2_a[1]) # (544,336)→(544,224)
sch.add_wire(r2_b[0], r2_b[1], r1_a[0], r2_b[1]) # (464,224)→(432,224)
sch.add_wire(r1_a[0], r2_b[1], r1_a[0], r1_a[1]) # (432,224)→(432,320)
# Noninv path: R3 pinA to In+
sch.add_wire(r3_a[0], r3_a[1], inp[0], inp[1])
# V2 to R3 pinB: route (80,400) → (80,352) → (352,352)
sch.add_wire(v2p[0], v2p[1], v2p[0], r3_b[1])
sch.add_wire(v2p[0], r3_b[1], r3_b[0], r3_b[1])
# R4 from noninv junction to GND
sch.add_wire(r3_b[0], r3_b[1], r4_a[0], r4_a[1]) # (352,352)→(352,368)
# Supply wiring
sch.add_wire(vp[0], vp[1], vp[0], vpos_p[1])
sch.add_wire(vp[0], vpos_p[1], vpos_p[0], vpos_p[1])
sch.add_wire(vn[0], vn[1], vn[0], vneg_n[1])
sch.add_wire(vn[0], vneg_n[1], vneg_n[0], vneg_n[1])
# === COMPONENTS ===
sch.add_component("voltage", "V1", "AC 1", 80, 224)
sch.add_component("voltage", "V2", "AC 1", 80, 384)
sch.add_component(oa_sym, "U1", "", 512, 336)
sch.add_component("res", "R1", r1, 448, 304, rotation=90)
sch.add_component("res", "R2", r2, 560, 208, rotation=90)
sch.add_component("res", "R3", r3, 448, 336, rotation=90)
sch.add_component("res", "R4", r4, 336, 352)
sch.add_component("voltage", "Vpos", "15", 688, 176)
sch.add_component("voltage", "Vneg", "15", 688, 416)
# === FLAGS ===
sch.add_ground(*v1n)
sch.add_ground(*v2n)
sch.add_ground(*r4_b)
sch.add_ground(*vpos_n)
sch.add_ground(*vneg_p)
sch.add_net_label("out", *out)
sch.add_directive(".ac dec 100 1 1meg", 80, 560)
return sch
def generate_buck_converter(
ind: str = "10u",
c_out: str = "100u",
r_load: str = "10",
v_in: str = "12",
duty_cycle: float = 0.5,
freq: str = "100k",
mosfet_model: str = "IRF540N",
diode_model: str = "1N5819",
) -> AscSchematic:
"""Generate a buck (step-down) converter schematic.
Topology::
Vin+ ─── drain
M1 (NMOS switch)
source ── sw ──[L1]── out ──+
| |
[D1] [Cout] [Rload]
| | |
GND GND GND
Gate driven by PULSE source at specified frequency and duty cycle.
Args:
ind: Inductor value
c_out: Output capacitor
r_load: Load resistor
v_in: Input voltage
duty_cycle: Switching duty cycle (0.0-1.0)
freq: Switching frequency
mosfet_model: NMOS model name
diode_model: Freewheeling diode model
"""
sch = AscSchematic(sheet_w=1040, sheet_h=680)
# Compute PULSE timing
freq_hz = _parse_spice_value(freq)
period = 1.0 / freq_hz
t_on = period * duty_cycle
t_rise = period * 0.01
t_fall = t_rise
pulse_val = (
f"PULSE(0 {v_in} 0 {t_rise:.4g} {t_fall:.4g} {t_on:.4g} {period:.4g})"
)
# Vin at (80, 48): pin+=(80,64), pin-=(80,144)
vin_p = pin_position("voltage", 0, 80, 48)
vin_n = pin_position("voltage", 1, 80, 48)
# Vgate at (80, 256): pin+=(80,272), pin-=(80,352)
vg_p = pin_position("voltage", 0, 80, 256)
vg_n = pin_position("voltage", 1, 80, 256)
# NMOS at (256, 64): D=(304,64), G=(256,144), S=(304,160)
md = pin_position("nmos", 0, 256, 64) # drain
mg = pin_position("nmos", 1, 256, 64) # gate
ms = pin_position("nmos", 2, 256, 64) # source
# Diode R180 at (320, 224): cathode at sw (auto-connects), anode at GND
d_anode = pin_position("diode", 0, 320, 224, 180) # (304, 224)
# L1 R270 at (288, 176): pinA=(304,160)=sw (auto-connects), pinB=(384,160)=output
l1_b = pin_position("ind", 1, 288, 176, 270) # (384, 160)
# Cout at (368, 160): pinA=(384,160)=output, pinB=(384,224)
cout_b = pin_position("cap", 1, 368, 160) # (384, 224)
# Rload at (448, 144): pinA=(464,160), pinB=(464,240)
rl_a = pin_position("res", 0, 448, 144) # (464, 160)
rl_b = pin_position("res", 1, 448, 144) # (464, 240)
# === WIRING ===
sch.add_wire(vin_p[0], vin_p[1], md[0], md[1]) # Vin+ to drain
# Gate drive: Vgate+ → route to gate
sch.add_wire(vg_p[0], vg_p[1], 160, vg_p[1])
sch.add_wire(160, vg_p[1], 160, mg[1])
sch.add_wire(160, mg[1], mg[0], mg[1])
# Output to Rload
sch.add_wire(l1_b[0], l1_b[1], rl_a[0], rl_a[1])
# === COMPONENTS ===
sch.add_component("voltage", "Vin", v_in, 80, 48)
sch.add_component("voltage", "Vgate", pulse_val, 80, 256)
sch.add_component("nmos", "M1", mosfet_model, 256, 64)
sch.add_component("diode", "D1", diode_model, 320, 224, rotation=180)
sch.add_component("ind", "L1", ind, 288, 176, rotation=270)
sch.add_component("cap", "Cout", c_out, 368, 160)
sch.add_component("res", "Rload", r_load, 448, 144)
# === FLAGS ===
sch.add_ground(*vin_n)
sch.add_ground(*vg_n)
sch.add_ground(*d_anode)
sch.add_ground(*cout_b)
sch.add_ground(*rl_b)
sch.add_net_label("sw", *ms)
sch.add_net_label("out", *l1_b)
sch.add_directive(f".tran {period * 200:.4g}", 80, 420)
return sch
def generate_ldo_regulator(
r1: str = "10k",
r2: str = "10k",
pass_transistor: str = "IRF9540N",
v_in: str = "8",
v_ref: str = "2.5",
) -> AscSchematic:
"""Generate a simple LDO voltage regulator schematic.
Topology::
Vin ──[PMOS source]──[PMOS drain]── out ──+
gate |
| [R1] [Cout] [Rload]
[Op-amp OUT] | | |
+ = Vref fb GND GND
- = fb |
[R2]
|
GND
Vout = Vref * (1 + R1/R2)
Args:
r1: Top feedback resistor (out to fb)
r2: Bottom feedback resistor (fb to GND)
pass_transistor: PMOS model name
v_in: Input voltage
v_ref: Reference voltage
"""
sch = AscSchematic(sheet_w=1200, sheet_h=880)
oa_sym = "OpAmps/UniversalOpamp2"
# PMOS M1 at (400, 48): D=(448,48)=out, G=(400,128)=gate, S=(448,144)=vin
m_d = pin_position("pmos", 0, 400, 48) # drain (448, 48) = output
m_g = pin_position("pmos", 1, 400, 48) # gate (400, 128)
m_s = pin_position("pmos", 2, 400, 48) # source (448, 144) = vin
# Vin at (80, 48): pin+=(80,64), pin-=(80,144)
vin_p = pin_position("voltage", 0, 80, 48)
vin_n = pin_position("voltage", 1, 80, 48)
# Vref at (80, 336): pin+=(80,352), pin-=(80,432)
vref_p = pin_position("voltage", 0, 80, 336)
vref_n = pin_position("voltage", 1, 80, 336)
# Op-amp U1 at (304, 304): In+=(272,320), In-=(272,288), V+=(304,272),
# V-=(304,336), OUT=(336,304)
oa_inp = pin_position(oa_sym, 0, 304, 304) # (272, 320) = vref
oa_inn = pin_position(oa_sym, 1, 304, 304) # (272, 288) = fb
oa_vp = pin_position(oa_sym, 2, 304, 304) # (304, 272) = V+
oa_vn = pin_position(oa_sym, 3, 304, 304) # (304, 336) = V-
oa_out = pin_position(oa_sym, 4, 304, 304) # (336, 304) = gate drive
# R1 from output down to fb node. res at (560, 160): pinA=(576,176), pinB=(576,256)
r1_a = pin_position("res", 0, 560, 160) # (576, 176)
r1_b = pin_position("res", 1, 560, 160) # (576, 256) = fb
# R2 from fb to GND. res at (560, 256): pinA=(576,272), pinB=(576,352)
r2_a = pin_position("res", 0, 560, 256) # (576, 272) = fb
r2_b = pin_position("res", 1, 560, 256) # (576, 352) = GND
# Cout at (640, 48): pinA=(656,48)=out, pinB=(656,112)=GND
cout_a = pin_position("cap", 0, 640, 48) # (656, 48)
cout_b = pin_position("cap", 1, 640, 48) # (656, 112)
# Rload at (720, 48): pinA=(736,64), pinB=(736,144)
rl_a = pin_position("res", 0, 720, 48) # (736, 64)
rl_b = pin_position("res", 1, 720, 48) # (736, 144)
# === WIRING ===
# Vin to PMOS source: (80,64) right to (448,64), down to source (448,144)
sch.add_wire(vin_p[0], vin_p[1], m_s[0], vin_p[1]) # horizontal
sch.add_wire(m_s[0], vin_p[1], m_s[0], m_s[1]) # vertical to source
# PMOS drain (output) right to R1, Cout, Rload
# Drain at (448, 48). Wire right to Cout pinA (656, 48)
sch.add_wire(m_d[0], m_d[1], cout_a[0], cout_a[1])
# Continue to Rload: (656,48) → (736,48), down to pinA (736,64)
sch.add_wire(cout_a[0], cout_a[1], rl_a[0], cout_a[1])
sch.add_wire(rl_a[0], cout_a[1], rl_a[0], rl_a[1])
# R1 top: down from output rail. (576,48) → (576,176)
sch.add_wire(m_d[0], m_d[1], r1_a[0], m_d[1]) # (448,48)→(576,48)
sch.add_wire(r1_a[0], m_d[1], r1_a[0], r1_a[1]) # (576,48)→(576,176)
# Op-amp output to PMOS gate
sch.add_wire(oa_out[0], oa_out[1], m_g[0], oa_out[1]) # horizontal
sch.add_wire(m_g[0], oa_out[1], m_g[0], m_g[1]) # vertical to gate
# Vref to op-amp In+
sch.add_wire(vref_p[0], vref_p[1], vref_p[0], oa_inp[1])
sch.add_wire(vref_p[0], oa_inp[1], oa_inp[0], oa_inp[1])
# Feedback (fb) to op-amp In-: R1 bottom/R2 top junction at (576,256/272)
# Wire from fb junction to In- (272, 288)
sch.add_wire(r2_a[0], r2_a[1], r2_a[0], oa_inn[1]) # (576,272)→(576,288)
sch.add_wire(r2_a[0], oa_inn[1], oa_inn[0], oa_inn[1]) # →(272,288)
# Op-amp supply: V+ to Vin rail, V- to GND
sch.add_wire(oa_vp[0], oa_vp[1], oa_vp[0], vin_p[1]) # V+ up to y=64
sch.add_wire(oa_vp[0], vin_p[1], vin_p[0], vin_p[1]) # left to Vin+
# === COMPONENTS ===
sch.add_component("pmos", "M1", pass_transistor, 400, 48)
sch.add_component(oa_sym, "U1", "", 304, 304)
sch.add_component("voltage", "Vin", v_in, 80, 48)
sch.add_component("voltage", "Vref", v_ref, 80, 336)
sch.add_component("res", "R1", r1, 560, 160)
sch.add_component("res", "R2", r2, 560, 256)
sch.add_component("cap", "Cout", "10u", 640, 48)
sch.add_component("res", "Rload", "100", 720, 48)
# === FLAGS ===
sch.add_ground(*vin_n)
sch.add_ground(*vref_n)
sch.add_ground(*r2_b)
sch.add_ground(*cout_b)
sch.add_ground(*rl_b)
sch.add_ground(oa_vn[0], oa_vn[1])
sch.add_net_label("out", *m_d)
sch.add_net_label("fb", *r1_b)
sch.add_directive(".tran 10m", 80, 500)
return sch
def generate_h_bridge(
v_supply: str = "12",
r_load: str = "10",
mosfet_model: str = "IRF540N",
) -> AscSchematic:
"""Generate an H-bridge motor driver schematic.
Topology (two half-bridges with load between them)::
Vcc ──┬── M1_D M2_D ──┬── Vcc
M1(fwd) M2(rev)
outA ─┤── M1_S M2_S ──├─ outB
│ │
├──── [Rload] ──────┤
│ │
outA ─┤── M3_D M4_D ──├─ outB
M3(rev) M4(fwd)
GND ──┴── M3_S M4_S ──┴── GND
M1+M4 driven together (forward), M2+M3 driven together (reverse).
Complementary PULSE gate drives with dead time.
Args:
v_supply: Supply voltage
r_load: Load resistance
mosfet_model: NMOS model name (used for all 4 switches)
"""
sch = AscSchematic(sheet_w=1200, sheet_h=880)
# PULSE gate drive parameters
period = "1m"
t_on = "450u"
t_dead = "25u"
fwd_pulse = f"PULSE(0 {v_supply} {t_dead} 10n 10n {t_on} {period})"
rev_pulse = f"PULSE(0 {v_supply} 525u 10n 10n {t_on} {period})"
# --- Left column (A side) ---
# M1 (high-A) at (320, 48): D=(368,48), G=(320,128), S=(368,144)
m1_d = pin_position("nmos", 0, 320, 48) # (368, 48) = vcc
m1_g = pin_position("nmos", 1, 320, 48) # (320, 128) = gate_fwd
m1_s = pin_position("nmos", 2, 320, 48) # (368, 144) = outA
# M3 (low-A) at (320, 240): D=(368,240), G=(320,320), S=(368,336)
m3_d = pin_position("nmos", 0, 320, 240) # (368, 240) = outA
m3_g = pin_position("nmos", 1, 320, 240) # (320, 320) = gate_rev
m3_s = pin_position("nmos", 2, 320, 240) # (368, 336) = GND
# --- Right column (B side) ---
# M2 (high-B) at (560, 48): D=(608,48), G=(560,128), S=(608,144)
m2_d = pin_position("nmos", 0, 560, 48) # (608, 48) = vcc
m2_g = pin_position("nmos", 1, 560, 48) # (560, 128) = gate_rev
m2_s = pin_position("nmos", 2, 560, 48) # (608, 144) = outB
# M4 (low-B) at (560, 240): D=(608,240), G=(560,320), S=(608,336)
m4_d = pin_position("nmos", 0, 560, 240) # (608, 240) = outB
m4_g = pin_position("nmos", 1, 560, 240) # (560, 320) = gate_fwd
m4_s = pin_position("nmos", 2, 560, 240) # (608, 336) = GND
# Rload R90 between outA and outB at y=192
# R90 at (544, 176): pinA=(528,192), pinB=(448,192)
rl_a = pin_position("res", 0, 544, 176, 90) # (528, 192)
rl_b = pin_position("res", 1, 544, 176, 90) # (448, 192)
# Sources
# Vsupply at (80, 48): pin+=(80,64), pin-=(80,144)
vs_p = pin_position("voltage", 0, 80, 48)
vs_n = pin_position("voltage", 1, 80, 48)
# Vg_fwd at (80, 272): pin+=(80,288), pin-=(80,368)
vgf_p = pin_position("voltage", 0, 80, 272)
vgf_n = pin_position("voltage", 1, 80, 272)
# Vg_rev at (80, 432): pin+=(80,448), pin-=(80,528)
vgr_p = pin_position("voltage", 0, 80, 432)
vgr_n = pin_position("voltage", 1, 80, 432)
# === WIRING ===
# Vcc rail: Vsupply+ to M1_D to M2_D
sch.add_wire(vs_p[0], vs_p[1], vs_p[0], m1_d[1]) # (80,64)→(80,48)
sch.add_wire(vs_p[0], m1_d[1], m1_d[0], m1_d[1]) # (80,48)→(368,48)
sch.add_wire(m1_d[0], m1_d[1], m2_d[0], m2_d[1]) # (368,48)→(608,48)
# Left column vertical: M1_S → outA junction → M3_D
sch.add_wire(m1_s[0], m1_s[1], m1_s[0], 192) # (368,144)→(368,192)
sch.add_wire(m1_s[0], 192, m3_d[0], m3_d[1]) # (368,192)→(368,240)
# Right column vertical: M2_S → outB junction → M4_D
sch.add_wire(m2_s[0], m2_s[1], m2_s[0], 192) # (608,144)→(608,192)
sch.add_wire(m2_s[0], 192, m4_d[0], m4_d[1]) # (608,192)→(608,240)
# Rload connections
sch.add_wire(m1_s[0], 192, rl_b[0], rl_b[1]) # outA→Rload pinB
sch.add_wire(rl_a[0], rl_a[1], m2_s[0], 192) # Rload pinA→outB
# Gate connections via net labels (cleaner than long wires)
# gate_fwd: M1_G, M4_G, Vg_fwd+
# gate_rev: M2_G, M3_G, Vg_rev+
# === COMPONENTS ===
sch.add_component("voltage", "Vsupply", v_supply, 80, 48)
sch.add_component("voltage", "Vg_fwd", fwd_pulse, 80, 272)
sch.add_component("voltage", "Vg_rev", rev_pulse, 80, 432)
sch.add_component("nmos", "M1", mosfet_model, 320, 48)
sch.add_component("nmos", "M2", mosfet_model, 560, 48)
sch.add_component("nmos", "M3", mosfet_model, 320, 240)
sch.add_component("nmos", "M4", mosfet_model, 560, 240)
sch.add_component("res", "Rload", r_load, 544, 176, rotation=90)
# === FLAGS ===
sch.add_ground(*vs_n)
sch.add_ground(*vgf_n)
sch.add_ground(*vgr_n)
sch.add_ground(*m3_s)
sch.add_ground(*m4_s)
sch.add_net_label("gate_fwd", *m1_g)
sch.add_net_label("gate_fwd", *m4_g)
sch.add_net_label("gate_fwd", *vgf_p)
sch.add_net_label("gate_rev", *m2_g)
sch.add_net_label("gate_rev", *m3_g)
sch.add_net_label("gate_rev", *vgr_p)
sch.add_net_label("outA", *m1_s)
sch.add_net_label("outB", *m2_s)
sch.add_directive(".tran 5m", 80, 592)
return sch