mcltspice/src/mcp_ltspice/asc_generator.py
Ryan Malloy 9b418a06c5 Add SVG plotting, circuit tuning, 5 new templates, fix prompts
- SVG waveform plots (svg_plot.py): pure-SVG timeseries, Bode, spectrum
  generation with plot_waveform MCP tool — no matplotlib dependency
- Circuit tuning tool (tune_circuit): single-shot simulate → measure →
  compare targets → suggest adjustments workflow for iterative design
- 5 new circuit templates: Sallen-Key lowpass, boost converter,
  instrumentation amplifier, current mirror, transimpedance amplifier
  (both netlist and .asc schematic generators, 15 total templates)
- Fix all 6 prompts to return list[Message] per FastMCP 2.x spec
- Add ltspice://templates and ltspice://template/{name} resources
- Add troubleshoot_simulation prompt
- Integration tests for RC lowpass and non-inverting amp (2/4 pass;
  CE amp and Colpitts oscillator have pre-existing schematic bugs)
- 360 unit tests passing, ruff clean
2026-02-11 05:13:50 -07:00

1927 lines
72 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
def generate_sallen_key_lowpass(
r1: str = "10k",
r2: str = "10k",
c1: str = "10n",
c2: str = "10n",
) -> AscSchematic:
"""Generate a Sallen-Key lowpass filter schematic (unity gain).
Topology::
V1 --[R1]-- n1 --[R2]-- n2 (In+)
| --> U1 --> out
[C1]--> out In- --> out (unity gain)
|
[C2]
|
GND
The C1 connects n1 to out (feedback). C2 connects n2 to GND.
f_c = 1 / (2*pi*sqrt(R1*R2*C1*C2)). Supply: +/-15V.
Args:
r1: First series resistor
r2: Second series resistor
c1: Feedback capacitor (n1 to out)
c2: Shunt capacitor (n2 to GND)
"""
sch = AscSchematic(sheet_w=1200, sheet_h=880)
oa_sym = "OpAmps/UniversalOpamp2"
# Op-amp U1 at (576, 336)
inp = pin_position(oa_sym, 0, 576, 336) # In+ = (544, 352)
inn = pin_position(oa_sym, 1, 576, 336) # In- = (544, 320)
vp = pin_position(oa_sym, 2, 576, 336) # V+ = (576, 304)
vn = pin_position(oa_sym, 3, 576, 336) # V- = (576, 368)
out = pin_position(oa_sym, 4, 576, 336) # OUT = (608, 336)
# V1 at (80, 256): pin+=(80,272), pin-=(80,352)
v1p = pin_position("voltage", 0, 80, 256)
v1n = pin_position("voltage", 1, 80, 256)
# R1 horizontal (R90) at origin (256, 256): pinA=(240,272), pinB=(160,272)
r1_a = pin_position("res", 0, 256, 256, 90) # (240, 272) = n1 side
r1_b = pin_position("res", 1, 256, 256, 90) # (160, 272) = input side
# R2 horizontal (R90) at origin (416, 256): pinA=(400,272), pinB=(320,272)
r2_a = pin_position("res", 0, 416, 256, 90) # (400, 272) = n2 side
r2_b = pin_position("res", 1, 416, 256, 90) # (320, 272) = n1 side
# C1 vertical: connects n1 (junction R1_a/R2_b at x=272-ish) to out
# We route n1 down to C1, then C1 bottom goes right to out.
# C1 at (304, 336): pinA=(320, 336), pinB=(320, 400)
# Actually let's place C1 horizontal (R90) above the feedback path.
# Better: C1 as a horizontal cap (R90) from n1 to out.
# cap R90: pinA offset (16,0)-> (0,16), pinB offset (16,64)->(-64,16)
# At origin (560, 192): pinA=(560,208), pinB=(496,208)
c1_a = pin_position("cap", 0, 560, 192, 90) # (560, 208) = out side
c1_b = pin_position("cap", 1, 560, 192, 90) # (496, 208) = n1 side
# C2 vertical at (480, 352): pinA=(496,352), pinB=(496,416)
c2_a = pin_position("cap", 0, 480, 352) # (496, 352)
c2_b = pin_position("cap", 1, 480, 352) # (496, 416)
# Supply: Vpos at (752, 176), Vneg at (752, 416)
vpos_p = pin_position("voltage", 0, 752, 176) # (752, 192)
vpos_n = pin_position("voltage", 1, 752, 176) # (752, 272)
vneg_p = pin_position("voltage", 0, 752, 416) # (752, 432)
vneg_n = pin_position("voltage", 1, 752, 416) # (752, 512)
# === WIRING ===
# V1+ to R1 pinB
sch.add_wire(*v1p, *r1_b)
# R1 pinA to R2 pinB (n1 junction at roughly x=272..320)
# n1 junction: R1_a at (240,272), R2_b at (320,272) -- wire between
sch.add_wire(*r1_a, *r2_b)
# R2 pinA to In+ : (400,272) down to (400,352) then right to (544,352)
sch.add_wire(r2_a[0], r2_a[1], r2_a[0], inp[1])
sch.add_wire(r2_a[0], inp[1], inp[0], inp[1])
# n2 junction to C2: from R2_a/In+ junction at (400,352) left to C2 pinA (496,352)
# Actually, n2 is at the In+ side. C2 from n2 to GND.
# n2 is where R2 meets In+. Let's tap off at (496,352) for C2.
sch.add_wire(r2_a[0], inp[1], c2_a[0], c2_a[1])
# C1 feedback: n1 to out via C1
# n1 is at the R1_a / R2_b junction. Wire from n1 up to C1 pinB.
# n1 at y=272, C1 pinB at (496, 208)
# Route from (240,272) up to (240,208) then right to (496,208)
sch.add_wire(r1_a[0], r1_a[1], r1_a[0], c1_b[1])
sch.add_wire(r1_a[0], c1_b[1], c1_b[0], c1_b[1])
# C1 pinA to out: (560,208) down to out (608,336)
sch.add_wire(c1_a[0], c1_a[1], out[0], c1_a[1])
sch.add_wire(out[0], c1_a[1], out[0], out[1])
# Unity gain feedback: out to In-
# (608,336) up to (608,320) ... In- is at (544,320)
# Route: out (608,336) left to (544,336) up... no.
# Actually wire from In- (544,320) right and down to out (608,336).
# Route: (544,320) right to (620,320), down to (620,336), left to (608,336)
# Simpler: go below. (608,336) to (608,400), left to (544,400), up to (544,320)
sch.add_wire(out[0], out[1], out[0], 400)
sch.add_wire(out[0], 400, inn[0], 400)
sch.add_wire(inn[0], 400, inn[0], inn[1])
# 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, 256)
sch.add_component(oa_sym, "U1", "", 576, 336)
sch.add_component("res", "R1", r1, 256, 256, rotation=90)
sch.add_component("res", "R2", r2, 416, 256, rotation=90)
sch.add_component("cap", "C1", c1, 560, 192, rotation=90)
sch.add_component("cap", "C2", c2, 480, 352)
sch.add_component("voltage", "Vpos", "15", 752, 176)
sch.add_component("voltage", "Vneg", "15", 752, 416)
# === FLAGS ===
sch.add_ground(*v1n)
sch.add_ground(*c2_b)
sch.add_ground(*vpos_n)
sch.add_ground(*vneg_p)
sch.add_net_label("out", *out)
sch.add_net_label("n1", r1_a[0], r1_a[1])
sch.add_net_label("n2", r2_a[0], r2_a[1])
sch.add_directive(".ac dec 100 1 1meg", 80, 560)
return sch
def generate_boost_converter(
ind: str = "10u",
c_out: str = "100u",
r_load: str = "50",
v_in: str = "5",
duty_cycle: float = 0.5,
freq: str = "100k",
) -> AscSchematic:
"""Generate a boost (step-up) converter schematic.
Topology::
Vin --[L1]--> sw_node --[D1]--> out
| |
[MOSFET] [Cout] [Rload]
D=sw | |
G=gate GND GND
S=GND
Gate driven by PULSE at switching frequency and duty cycle.
Vout_ideal = Vin / (1 - 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
"""
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})"
)
mosfet_model = "IRF540N"
diode_model = "1N5819"
# 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)
# L1 horizontal (R90) at (192, 48):
# ind R90: pinA offset (16,16)->(-16,16), pinB offset (16,96)->(-96,16)
# At (192, 48): pinA = (176, 64), pinB = (96, 64)
# Actually re-derive: R90 transform: (px,py) -> (-py, px)
# pinA raw = (16,16) -> R90 -> (-16, 16). origin(192,48) + (-16,16) = (176, 64)
# pinB raw = (16,96) -> R90 -> (-96, 16). origin(192,48) + (-96,16) = (96, 64)
# So pinB is on the Vin side, pinA on the sw side. Good.
l1_a = pin_position("ind", 0, 192, 48, 90) # (176, 64) = sw side
l1_b = pin_position("ind", 1, 192, 48, 90) # (96, 64) = vin side
# NMOS at (224, 112): D=(272,112)=sw, G=(224,192), S=(272,208)=GND
# Actually let's position so drain lines up with L1 pinA.
# L1_a = (176, 64). NMOS drain should be at (176, something).
# nmos D offset = (48, 0). So origin = (176-48, y) = (128, y).
# Want drain at y ~ 112 range. Put origin at (128, 112): D=(176,112).
# Then wire from L1_a (176,64) down to D (176,112).
md = pin_position("nmos", 0, 128, 112) # drain (176, 112)
mg = pin_position("nmos", 1, 128, 112) # gate (128, 192)
ms = pin_position("nmos", 2, 128, 112) # source (176, 208)
# Diode from sw_node to output.
# sw_node is at L1_a (176,64). Diode horizontal.
# diode R90: anode offset (16,0)->(0,16), cathode offset (16,64)->(-64,16)
# At origin (320, 48): anode=(320,64), cathode=(256,64)
# We want anode at sw side, cathode at out side. But R90 has cathode to left.
# Use R270 instead: anode (16,0)->R270->(0,-16), cathode (16,64)->R270->(64,-16)
# At origin (240, 80): anode=(240,64), cathode=(304,64)
# Wire from sw (176,64) right to anode (240,64). Cathode (304,64) = output side.
d_anode = pin_position("diode", 0, 240, 80, 270) # (240, 64)
d_cathode = pin_position("diode", 1, 240, 80, 270) # (304, 64)
# Wire from L1_a (176,64) to sw junction, then to diode anode (240,64)
# And from diode cathode (304,64) to out
# Cout at (384, 64): pinA=(400,64), pinB=(400,128)
cout_a = pin_position("cap", 0, 384, 64) # (400, 64)
cout_b = pin_position("cap", 1, 384, 64) # (400, 128)
# Rload at (464, 48): pinA=(480,64), pinB=(480,144)
rl_a = pin_position("res", 0, 464, 48) # (480, 64)
rl_b = pin_position("res", 1, 464, 48) # (480, 144)
# === WIRING ===
# Vin+ to L1 pinB
sch.add_wire(*vin_p, *l1_b)
# L1 pinA (sw) to MOSFET drain
sch.add_wire(l1_a[0], l1_a[1], md[0], md[1])
# L1 pinA (sw) to diode anode
sch.add_wire(l1_a[0], l1_a[1], d_anode[0], d_anode[1])
# Diode cathode to Cout and Rload (output rail)
sch.add_wire(d_cathode[0], d_cathode[1], cout_a[0], cout_a[1])
sch.add_wire(cout_a[0], cout_a[1], rl_a[0], rl_a[1])
# Gate drive: Vgate+ to MOSFET gate
sch.add_wire(vg_p[0], vg_p[1], mg[0], vg_p[1])
sch.add_wire(mg[0], vg_p[1], mg[0], mg[1])
# === COMPONENTS ===
sch.add_component("voltage", "Vin", v_in, 80, 48)
sch.add_component("voltage", "Vgate", pulse_val, 80, 256)
sch.add_component("ind", "L1", ind, 192, 48, rotation=90)
sch.add_component("nmos", "M1", mosfet_model, 128, 112)
sch.add_component("diode", "D1", diode_model, 240, 80, rotation=270)
sch.add_component("cap", "Cout", c_out, 384, 64)
sch.add_component("res", "Rload", r_load, 464, 48)
# === FLAGS ===
sch.add_ground(*vin_n)
sch.add_ground(*vg_n)
sch.add_ground(*ms)
sch.add_ground(*cout_b)
sch.add_ground(*rl_b)
sch.add_net_label("sw", l1_a[0], l1_a[1])
sch.add_net_label("out", d_cathode[0], d_cathode[1])
sch.add_directive(f".tran {period * 200:.4g}", 80, 420)
return sch
def generate_instrumentation_amp(
r1: str = "10k",
r2: str = "10k",
r3: str = "10k",
r_gain: str = "10k",
) -> AscSchematic:
"""Generate a 3-opamp instrumentation amplifier schematic.
Stage 1 (input buffers with gain)::
Vin+ --> In+(X1) --> out1
Vin- --> In+(X2) --> out2
R1 from out1 to In-(X1), R1b from out2 to In-(X2)
Rgain between In-(X1) and In-(X2)
Stage 2 (difference amp)::
out1 --[R2]--> In-(X3) --[R3]--> Vout
out2 --[R2b]--> In+(X3)
In+(X3) --[R3b]--> GND
Gain = (1 + 2*R1/Rgain) * (R3/R2). Supply: +/-15V.
Args:
r1: Stage 1 feedback resistor
r2: Stage 2 input resistor
r3: Stage 2 feedback resistor
r_gain: Gain-setting resistor between X1 and X2 inverting inputs
"""
sch = AscSchematic(sheet_w=1600, sheet_h=1040)
oa_sym = "OpAmps/UniversalOpamp2"
# === STAGE 1 ===
# X1 at (480, 208)
x1_inp = pin_position(oa_sym, 0, 480, 208) # In+ = (448, 224)
x1_inn = pin_position(oa_sym, 1, 480, 208) # In- = (448, 192)
x1_vp = pin_position(oa_sym, 2, 480, 208) # V+ = (480, 176)
x1_vn = pin_position(oa_sym, 3, 480, 208) # V- = (480, 240)
x1_out = pin_position(oa_sym, 4, 480, 208) # OUT = (512, 208)
# X2 at (480, 480)
x2_inp = pin_position(oa_sym, 0, 480, 480) # In+ = (448, 496)
x2_inn = pin_position(oa_sym, 1, 480, 480) # In- = (448, 464)
x2_vp = pin_position(oa_sym, 2, 480, 480) # V+ = (480, 448)
x2_vn = pin_position(oa_sym, 3, 480, 480) # V- = (480, 512)
x2_out = pin_position(oa_sym, 4, 480, 480) # OUT = (512, 480)
# R1: feedback from X1 out to X1 In- (horizontal R90)
# Place at (576, 96): pinA=(560,112), pinB=(480,112)
# We want to connect out1 (512,208) up to (512,112) left to (560,112)=R1_a
# and R1_b (480,112) left to (448,112) down to In- (448,192)
r1_a = pin_position("res", 0, 576, 96, 90) # (560, 112)
r1_b = pin_position("res", 1, 576, 96, 90) # (480, 112)
# R1b: feedback from X2 out to X2 In- (horizontal R90)
# Place at (576, 544): pinA=(560,560), pinB=(480,560)
r1b_a = pin_position("res", 0, 576, 544, 90) # (560, 560)
r1b_b = pin_position("res", 1, 576, 544, 90) # (480, 560)
# Rgain: vertical between X1 In- junction and X2 In- junction
# X1 In- at y=192 area, X2 In- at y=464 area
# We'll connect to the R1/R1b feedback junctions.
# Place Rgain at (400, 208): pinA=(416,224), pinB=(416,304)
# Actually we need it to span from node_a (X1 In- junction) to node_b (X2 In- junction).
# The junctions are at the R1_b / R1b_b x positions.
# Let's use net labels for cleanliness: "node_a" at R1_b junction, "node_b" at R1b_b junction.
# Rgain vertical res at (336, 256): pinA=(352, 272), pinB=(352, 352)
rg_a = pin_position("res", 0, 336, 256) # (352, 272) = node_a side
rg_b = pin_position("res", 1, 336, 256) # (352, 352) = node_b side
# === STAGE 2 ===
# X3 at (880, 352)
x3_inp = pin_position(oa_sym, 0, 880, 352) # In+ = (848, 368)
x3_inn = pin_position(oa_sym, 1, 880, 352) # In- = (848, 336)
x3_vp = pin_position(oa_sym, 2, 880, 352) # V+ = (880, 320)
x3_vn = pin_position(oa_sym, 3, 880, 352) # V- = (880, 384)
x3_out = pin_position(oa_sym, 4, 880, 352) # OUT = (912, 352)
# R2: out1 to X3 In- (horizontal R90)
# Place at (768, 320): pinA=(752,336), pinB=(672,336)
r2_a = pin_position("res", 0, 768, 320, 90) # (752, 336) near X3 In-
r2_b = pin_position("res", 1, 768, 320, 90) # (672, 336) from out1
# R3: X3 In- to Vout (horizontal R90, above X3 for feedback)
# Place at (960, 240): pinA=(944, 256), pinB=(864, 256)
r3_a = pin_position("res", 0, 960, 240, 90) # (944, 256) near Vout
r3_b = pin_position("res", 1, 960, 240, 90) # (864, 256) near In-
# R2b: out2 to X3 In+ (horizontal R90)
# Place at (768, 352): pinA=(752,368), pinB=(672,368)
r2b_a = pin_position("res", 0, 768, 352, 90) # (752, 368) near X3 In+
r2b_b = pin_position("res", 1, 768, 352, 90) # (672, 368) from out2
# R3b: X3 In+ to GND (vertical)
# Place at (832, 416): pinA=(848,432), pinB=(848,512)
r3b_a = pin_position("res", 0, 832, 416) # (848, 432)
r3b_b = pin_position("res", 1, 832, 416) # (848, 512)
# Sources
# V1 at (80, 160): pin+=(80,176), pin-=(80,256)
v1p = pin_position("voltage", 0, 80, 160)
v1n = pin_position("voltage", 1, 80, 160)
# V2 at (80, 432): pin+=(80,448), pin-=(80,528)
v2p = pin_position("voltage", 0, 80, 432)
v2n = pin_position("voltage", 1, 80, 432)
# Supply: Vpos at (1056, 176), Vneg at (1056, 480)
vpos_p = pin_position("voltage", 0, 1056, 176)
vpos_n = pin_position("voltage", 1, 1056, 176)
vneg_p = pin_position("voltage", 0, 1056, 480)
vneg_n = pin_position("voltage", 1, 1056, 480)
# === WIRING ===
# V1+ to X1 In+
sch.add_wire(v1p[0], v1p[1], v1p[0], x1_inp[1])
sch.add_wire(v1p[0], x1_inp[1], x1_inp[0], x1_inp[1])
# V2+ to X2 In+
sch.add_wire(v2p[0], v2p[1], v2p[0], x2_inp[1])
sch.add_wire(v2p[0], x2_inp[1], x2_inp[0], x2_inp[1])
# R1 feedback: X1 out (512,208) up to (512,112), right to R1_a (560,112)
sch.add_wire(x1_out[0], x1_out[1], x1_out[0], r1_a[1])
sch.add_wire(x1_out[0], r1_a[1], r1_a[0], r1_a[1])
# R1_b (480,112) down to X1 In- (448,192): (480,112) left to (448,112), down to (448,192)
sch.add_wire(r1_b[0], r1_b[1], x1_inn[0], r1_b[1])
sch.add_wire(x1_inn[0], r1_b[1], x1_inn[0], x1_inn[1])
# R1b feedback: X2 out (512,480) down to (512,560), right to R1b_a (560,560)
sch.add_wire(x2_out[0], x2_out[1], x2_out[0], r1b_a[1])
sch.add_wire(x2_out[0], r1b_a[1], r1b_a[0], r1b_a[1])
# R1b_b (480,560) down to X2 In- (448,464): (480,560) left to (448,560), up to (448,464)
sch.add_wire(r1b_b[0], r1b_b[1], x2_inn[0], r1b_b[1])
sch.add_wire(x2_inn[0], r1b_b[1], x2_inn[0], x2_inn[1])
# Rgain: node_a (junction at X1 In-) to Rgain pinA, node_b (X2 In-) to Rgain pinB
# Route from X1 In- junction (448,192) left to (352,192), down to Rg_a (352,272)
sch.add_wire(x1_inn[0], x1_inn[1], rg_a[0], x1_inn[1])
sch.add_wire(rg_a[0], x1_inn[1], rg_a[0], rg_a[1])
# Route from X2 In- junction (448,464) left to (352,464), up to Rg_b (352,352)
sch.add_wire(x2_inn[0], x2_inn[1], rg_b[0], x2_inn[1])
sch.add_wire(rg_b[0], x2_inn[1], rg_b[0], rg_b[1])
# Stage 2 wiring
# out1 (512,208) to R2_b (672,336): route (512,208) right to (672,208), down to (672,336)
sch.add_wire(x1_out[0], x1_out[1], r2_b[0], x1_out[1])
sch.add_wire(r2_b[0], x1_out[1], r2_b[0], r2_b[1])
# R2_a (752,336) to X3 In- (848,336)
sch.add_wire(r2_a[0], r2_a[1], x3_inn[0], x3_inn[1])
# out2 (512,480) to R2b_b (672,368): route (512,480) right to (672,480), up to (672,368)
sch.add_wire(x2_out[0], x2_out[1], r2b_b[0], x2_out[1])
sch.add_wire(r2b_b[0], x2_out[1], r2b_b[0], r2b_b[1])
# R2b_a (752,368) to X3 In+ (848,368)
sch.add_wire(r2b_a[0], r2b_a[1], x3_inp[0], x3_inp[1])
# R3 feedback: X3 out (912,352) up to (912,256), left to R3_a (944,256)
sch.add_wire(x3_out[0], x3_out[1], x3_out[0], r3_a[1])
sch.add_wire(x3_out[0], r3_a[1], r3_a[0], r3_a[1])
# R3_b (864,256) down to X3 In- junction: (864,256) left to (848,256), down to (848,336)
sch.add_wire(r3_b[0], r3_b[1], x3_inn[0], r3_b[1])
sch.add_wire(x3_inn[0], r3_b[1], x3_inn[0], x3_inn[1])
# R3b: X3 In+ to GND via R3b
sch.add_wire(x3_inp[0], x3_inp[1], r3b_a[0], r3b_a[1])
# Supply wiring (use net labels for cleanliness with 3 opamps)
# Just wire X1, X2, X3 supply pins to net labels
# === COMPONENTS ===
sch.add_component("voltage", "V1", "AC 1", 80, 160)
sch.add_component("voltage", "V2", "0", 80, 432)
sch.add_component(oa_sym, "X1", "", 480, 208)
sch.add_component(oa_sym, "X2", "", 480, 480)
sch.add_component(oa_sym, "X3", "", 880, 352)
sch.add_component("res", "R1", r1, 576, 96, rotation=90)
sch.add_component("res", "R1b", r1, 576, 544, rotation=90)
sch.add_component("res", "Rgain", r_gain, 336, 256)
sch.add_component("res", "R2", r2, 768, 320, rotation=90)
sch.add_component("res", "R3", r3, 960, 240, rotation=90)
sch.add_component("res", "R2b", r2, 768, 352, rotation=90)
sch.add_component("res", "R3b", r3, 832, 416)
sch.add_component("voltage", "Vpos", "15", 1056, 176)
sch.add_component("voltage", "Vneg", "15", 1056, 480)
# === FLAGS ===
sch.add_ground(*v1n)
sch.add_ground(*v2n)
sch.add_ground(*r3b_b)
sch.add_ground(*vpos_n)
sch.add_ground(*vneg_p)
# Supply net labels for all three opamps
sch.add_net_label("vdd", *x1_vp)
sch.add_net_label("vdd", *x2_vp)
sch.add_net_label("vdd", *x3_vp)
sch.add_net_label("vdd", *vpos_p)
sch.add_net_label("vss", *x1_vn)
sch.add_net_label("vss", *x2_vn)
sch.add_net_label("vss", *x3_vn)
sch.add_net_label("vss", *vneg_n)
sch.add_net_label("vout", *x3_out)
sch.add_directive(".ac dec 100 1 1meg", 80, 640)
return sch
def generate_current_mirror(
r_ref: str = "10k",
r_load: str = "1k",
vcc: str = "12",
) -> AscSchematic:
"""Generate a BJT current mirror schematic.
Topology::
Vcc --[Rref]--> collector_Q1 = base_Q1 = base_Q2
emitter_Q1 = GND
Vcc --[Rload]--> collector_Q2
emitter_Q2 = GND
Q1 is diode-connected (collector tied to base).
I_ref ~ (Vcc - Vbe) / Rref, I_load ~ I_ref.
Args:
r_ref: Reference resistor
r_load: Load resistor
vcc: Supply voltage
"""
sch = AscSchematic(sheet_w=880, sheet_h=680)
bjt_model = "2N2222"
# Q1 (NPN, diode-connected) at (256, 288): C=(320,288), B=(256,336), E=(320,384)
q1c = pin_position("npn", 0, 256, 288) # collector (320, 288)
q1b = pin_position("npn", 1, 256, 288) # base (256, 336)
q1e = pin_position("npn", 2, 256, 288) # emitter (320, 384)
# Q2 (NPN) at (448, 288): C=(512,288), B=(448,336), E=(512,384)
q2c = pin_position("npn", 0, 448, 288) # collector (512, 288)
q2b = pin_position("npn", 1, 448, 288) # base (448, 336)
q2e = pin_position("npn", 2, 448, 288) # emitter (512, 384)
# Rref from Vcc to Q1 collector. res at (304, 192): pinA=(320,208)=vcc, pinB=(320,288)=Q1c
rref_a = pin_position("res", 0, 304, 192) # (320, 208)
# Rload from Vcc to Q2 collector. res at (496, 192): pinA=(512,208)=vcc, pinB=(512,288)=Q2c
rload_a = pin_position("res", 0, 496, 192) # (512, 208)
# 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)
# Vcc rail at y=208
vcc_y = rref_a[1] # 208
# === WIRING ===
# Vcc rail
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, rload_a[0], vcc_y) # (160,208)->(512,208) spans both
# Diode connection: Q1 collector to Q1 base
# Q1c = (320,288), Q1b = (256,336)
# Route: (320,288) left to (256,288) down to (256,336)
sch.add_wire(q1c[0], q1c[1], q1b[0], q1c[1])
sch.add_wire(q1b[0], q1c[1], q1b[0], q1b[1])
# Base connection: Q1 base to Q2 base
sch.add_wire(q1b[0], q1b[1], q2b[0], q2b[1])
# === COMPONENTS ===
sch.add_component("npn", "Q1", bjt_model, 256, 288)
sch.add_component("npn", "Q2", bjt_model, 448, 288)
sch.add_component("res", "Rref", r_ref, 304, 192)
sch.add_component("res", "Rload", r_load, 496, 192)
sch.add_component("voltage", "Vcc", vcc, 80, 96)
# === FLAGS ===
sch.add_ground(*vcc_n)
sch.add_ground(*q1e)
sch.add_ground(*q2e)
sch.add_net_label("mirror", *q1b)
sch.add_net_label("out", *q2c)
sch.add_directive(".op", 80, 448)
sch.add_directive(".tran 1m", 80, 480)
return sch
def generate_transimpedance_amp(
rf: str = "100k",
cf: str = "1p",
i_source: str = "1u",
) -> AscSchematic:
"""Generate a transimpedance amplifier (TIA) schematic.
Topology::
I1 (current src, AC) --> In- (inv)
--> U1 --> out
GND --> In+ (noninv)
Rf from In- to out (feedback)
Cf from In- to out (parallel with Rf, for stability)
Vout = -I_in * Rf (at low frequencies).
Bandwidth limited by Cf. Supply: +/-15V.
Args:
rf: Feedback resistor
cf: Feedback capacitor (for stability)
i_source: AC current source magnitude
"""
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)
# Current source I1 at (320, 224) (vertical, like voltage source)
# Using "voltage" pin positions since current source shares the same symbol geometry
# I1 at origin (320, 224): pin+ = (320, 240), pin- = (320, 320)
# Current flows from pin+ to pin- inside the source (conventional).
# We want current into In- node: connect pin+ to In- node, pin- to GND.
i1p = pin_position("voltage", 0, 320, 224) # (320, 240)
i1n = pin_position("voltage", 1, 320, 224) # (320, 320)
# Rf horizontal (R90) above op-amp for feedback
# At (560, 208): pinA=(544,224), pinB=(464,224)
rf_a = pin_position("res", 0, 560, 208, 90) # (544, 224) near out
rf_b = pin_position("res", 1, 560, 208, 90) # (464, 224) near In-
# Cf horizontal (R90) above Rf for parallel feedback
# At (560, 160): pinA=(544,176), pinB=(464,176)
# Actually cap R90: pinA offset (16,0)->(0,16), pinB offset (16,64)->(-64,16)
# At (560, 160): pinA=(560,176), pinB=(496,176)
cf_a = pin_position("cap", 0, 560, 160, 90) # (560, 176) near out
cf_b = pin_position("cap", 1, 560, 160, 90) # (496, 176) near In-
# 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 ===
# I1 pin+ (320,240) to In- node (480,320)
# Route: (320,240) right to (480,240), down to (480,320)
sch.add_wire(i1p[0], i1p[1], inn[0], i1p[1])
sch.add_wire(inn[0], i1p[1], inn[0], inn[1])
# Rf feedback: out up to Rf pinA, Rf pinB left and down to In- junction
sch.add_wire(out[0], out[1], rf_a[0], rf_a[1]) # (544,336)->(544,224)
sch.add_wire(rf_b[0], rf_b[1], rf_b[0], inn[1]) # (464,224)->(464,320)
sch.add_wire(rf_b[0], inn[1], inn[0], inn[1]) # (464,320)->(480,320)
# Cf feedback: parallel path
# Connect Cf pinA to Rf pinA column, Cf pinB to Rf pinB column
sch.add_wire(cf_a[0], cf_a[1], rf_a[0], cf_a[1]) # (560,176)->(544,176)
sch.add_wire(rf_a[0], cf_a[1], rf_a[0], rf_a[1]) # (544,176)->(544,224) vertical
sch.add_wire(cf_b[0], cf_b[1], rf_b[0], cf_b[1]) # (496,176)->(464,176)
sch.add_wire(rf_b[0], cf_b[1], rf_b[0], rf_b[1]) # (464,176)->(464,224) vertical
# 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", "I1", f"AC {i_source}", 320, 224)
sch.add_component(oa_sym, "U1", "", 512, 336)
sch.add_component("res", "Rf", rf, 560, 208, rotation=90)
sch.add_component("cap", "Cf", cf, 560, 160, rotation=90)
sch.add_component("voltage", "Vpos", "15", 688, 176)
sch.add_component("voltage", "Vneg", "15", 688, 416)
# === FLAGS ===
sch.add_ground(*i1n)
sch.add_ground(*inp) # In+ to GND
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