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.
This commit is contained in:
Ryan Malloy 2026-02-11 00:46:13 -07:00
parent 1afa4f112b
commit c56ce918b4
2 changed files with 1042 additions and 89 deletions

View File

@ -14,13 +14,53 @@ 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
],
}
@ -184,6 +224,7 @@ class AscSchematic:
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:
@ -320,59 +361,899 @@ def generate_voltage_divider(
def generate_inverting_amp(
rin: str = "10k",
rf: str = "100k",
opamp_model: str = "UniversalOpamp2",
) -> AscSchematic:
"""Generate an inverting op-amp amplifier schematic.
Topology::
V1 --[Rin]--> inv(-) --[Rf]--> out
non-inv(+) --> GND
V1 --[Rin]--> In-(inv) ---[Rf]--- out
In+(non-inv) --> GND
Supply: Vpos=+15V, Vneg=-15V
The gain is ``-Rf/Rin``.
This template uses the netlist (.cir) builder approach for the
op-amp since op-amp symbols have complex multi-pin layouts. The
generated .asc is a placeholder showing the RC input network.
Args:
rin: Input resistor value
rf: Feedback resistor value
opamp_model: Op-amp symbol name (default ``UniversalOpamp2``,
the built-in ideal op-amp that needs no external model file)
Returns:
An ``AscSchematic`` ready to ``.save()`` or ``.render()``.
"""
sch = AscSchematic(sheet_w=1200, sheet_h=880)
oa_sym = "OpAmps/UniversalOpamp2"
# For the inverting amp, use a vertical layout with known-good pin positions.
# V1 at (80, 160) R0: pin+ = (80, 176), pin- = (80, 256)
# Rin at (160, 160) R0: pinA = (176, 176), pinB = (176, 256)
# Rf at (320, 160) R0: pinA = (336, 176), pinB = (336, 256)
# 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)
v1p = pin_position("voltage", 0, 80, 160)
v1n = pin_position("voltage", 1, 80, 160)
rin_a = pin_position("res", 0, 160, 160)
rin_b = pin_position("res", 1, 160, 160)
rf_a = pin_position("res", 0, 320, 160)
rf_b = pin_position("res", 1, 320, 160)
# Signal source V1 at (80, 256)
v1p = pin_position("voltage", 0, 80, 256) # (80, 272)
v1n = pin_position("voltage", 1, 80, 256) # (80, 352)
# V1+ → Rin top
sch.add_wire(*v1p, *rin_a)
# Rin bottom → Rf top (inverting node)
sch.add_wire(*rin_b, *rf_a)
# 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)
sch.add_component("voltage", "V1", "AC 1", 80, 160)
sch.add_component("res", "Rin", rin, 160, 160)
sch.add_component("res", "Rf", rf, 320, 160)
# 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(*rf_b)
sch.add_net_label("inv", *rin_b)
sch.add_net_label("out", *rf_a)
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, 320)
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

View File

@ -20,9 +20,30 @@ import numpy as np
from fastmcp import FastMCP
from . import __version__
from .asc_generator import (
generate_buck_converter as generate_buck_converter_asc,
)
from .asc_generator import (
generate_colpitts_oscillator as generate_colpitts_asc,
)
from .asc_generator import (
generate_common_emitter_amp as generate_ce_amp_asc,
)
from .asc_generator import (
generate_differential_amp as generate_diff_amp_asc,
)
from .asc_generator import (
generate_h_bridge as generate_h_bridge_asc,
)
from .asc_generator import (
generate_inverting_amp,
)
from .asc_generator import (
generate_ldo_regulator as generate_ldo_asc,
)
from .asc_generator import (
generate_non_inverting_amp as generate_noninv_amp_asc,
)
from .asc_generator import (
generate_rc_lowpass as generate_rc_lowpass_asc,
)
@ -1107,70 +1128,118 @@ async def monte_carlo(
# ============================================================================
_ASC_TEMPLATES: dict[str, dict] = {
"rc_lowpass": {
"func": generate_rc_lowpass_asc,
"description": "RC lowpass filter with AC analysis",
"params": {"r": "1k", "c": "100n"},
},
"voltage_divider": {
"func": generate_voltage_divider_asc,
"description": "Resistive voltage divider with .op analysis",
"params": {"r1": "10k", "r2": "10k", "vin": "5"},
},
"inverting_amp": {
"func": generate_inverting_amp,
"description": "Inverting op-amp amplifier (gain = -Rf/Rin)",
"params": {"rin": "10k", "rf": "100k"},
},
"non_inverting_amp": {
"func": generate_noninv_amp_asc,
"description": "Non-inverting op-amp amplifier (gain = 1 + Rf/Rin)",
"params": {"rin": "10k", "rf": "100k"},
},
"common_emitter_amp": {
"func": generate_ce_amp_asc,
"description": "Common-emitter BJT amplifier with voltage divider bias",
"params": {
"rc": "2.2k", "rb1": "56k", "rb2": "12k", "re": "1k",
"cc_in": "10u", "cc_out": "10u", "ce": "47u",
"vcc": "12", "bjt_model": "2N2222",
},
},
"colpitts_oscillator": {
"func": generate_colpitts_asc,
"description": "Colpitts oscillator with LC tank and BJT",
"params": {
"ind": "1u", "c1": "100p", "c2": "100p",
"rb": "47k", "rc": "1k", "re": "470",
"vcc": "12", "bjt_model": "2N2222",
},
},
"differential_amp": {
"func": generate_diff_amp_asc,
"description": "Differential amplifier with op-amp",
"params": {"r1": "10k", "r2": "10k", "r3": "10k", "r4": "10k"},
},
"buck_converter": {
"func": generate_buck_converter_asc,
"description": "Buck (step-down) converter with NMOS switch",
"params": {
"ind": "10u", "c_out": "100u", "r_load": "10",
"v_in": "12", "duty_cycle": "0.5", "freq": "100k",
"mosfet_model": "IRF540N", "diode_model": "1N5819",
},
},
"ldo_regulator": {
"func": generate_ldo_asc,
"description": "LDO voltage regulator with PMOS pass transistor",
"params": {
"r1": "10k", "r2": "10k", "pass_transistor": "IRF9540N",
"v_in": "8", "v_ref": "2.5",
},
},
"h_bridge": {
"func": generate_h_bridge_asc,
"description": "H-bridge motor driver with 4 NMOS transistors",
"params": {"v_supply": "12", "r_load": "10", "mosfet_model": "IRF540N"},
},
}
@mcp.tool()
def generate_schematic(
template: str,
params: dict[str, str] | None = None,
output_path: str | None = None,
r: str | None = None,
c: str | None = None,
r1: str | None = None,
r2: str | None = None,
vin: str | None = None,
rin: str | None = None,
rf: str | None = None,
opamp_model: str | None = None,
) -> dict:
"""Generate an LTspice .asc schematic file from a template.
"""Generate an LTspice .asc graphical schematic file from a template.
Available templates and their parameters:
- "rc_lowpass": r (resistor, default "1k"), c (capacitor, default "100n")
- "voltage_divider": r1 (top, default "10k"), r2 (bottom, default "10k"),
vin (input voltage, default "5")
- "inverting_amp": rin (input R, default "10k"), rf (feedback R,
default "100k"), opamp_model (default "UniversalOpamp2")
Creates a ready-to-simulate .asc file with proper component placement,
wire routing, and simulation directives.
Available templates (use list_templates for full details):
- rc_lowpass, voltage_divider, inverting_amp, non_inverting_amp,
common_emitter_amp, colpitts_oscillator, differential_amp,
buck_converter, ldo_regulator, h_bridge
Args:
template: Template name
output_path: Where to save (None = auto in /tmp)
r: Resistor value (rc_lowpass)
c: Capacitor value (rc_lowpass)
r1: Top resistor (voltage_divider)
r2: Bottom resistor (voltage_divider)
vin: Input voltage (voltage_divider)
rin: Input resistor (inverting_amp)
rf: Feedback resistor (inverting_amp)
opamp_model: Op-amp model name (inverting_amp)
template: Template name (see list above)
params: Override default parameters as key-value pairs.
Use list_templates to see available parameters for each template.
output_path: Where to save the .asc file (None = auto in /tmp)
"""
if template == "rc_lowpass":
params: dict[str, str] = {}
if r is not None:
params["r"] = r
if c is not None:
params["c"] = c
sch = generate_rc_lowpass_asc(**params)
elif template == "voltage_divider":
params = {}
if r1 is not None:
params["r1"] = r1
if r2 is not None:
params["r2"] = r2
if vin is not None:
params["vin"] = vin
sch = generate_voltage_divider_asc(**params)
elif template == "inverting_amp":
params = {}
if rin is not None:
params["rin"] = rin
if rf is not None:
params["rf"] = rf
if opamp_model is not None:
params["opamp_model"] = opamp_model
sch = generate_inverting_amp(**params)
else:
if template not in _ASC_TEMPLATES:
names = ", ".join(sorted(_ASC_TEMPLATES))
return {"error": f"Unknown template '{template}'. Available: {names}"}
entry = _ASC_TEMPLATES[template]
call_params: dict[str, str | float] = {}
if params:
for k, v in params.items():
if k not in entry["params"]:
valid = ", ".join(sorted(entry["params"]))
return {
"error": f"Unknown template '{template}'. "
f"Available: rc_lowpass, voltage_divider, inverting_amp"
"error": f"Unknown param '{k}' for {template}. Valid: {valid}"
}
# duty_cycle needs float conversion for buck_converter
if k == "duty_cycle":
call_params[k] = float(v)
else:
call_params[k] = v
sch = entry["func"](**call_params)
if output_path is None:
output_path = str(Path(tempfile.gettempdir()) / f"{template}.asc")
@ -1179,6 +1248,7 @@ def generate_schematic(
return {
"success": True,
"output_path": str(saved),
"template": template,
"schematic_preview": sch.render()[:500],
}
@ -2044,9 +2114,11 @@ Approach 2 - Build from components:
4. Add simulation directives (.tran, .ac, .dc, .op, .tf)
5. Simulate and analyze
Approach 3 - Graphical schematic:
1. Use generate_schematic for supported topologies (rc_lowpass,
voltage_divider, inverting_amp)
Approach 3 - Graphical schematic (.asc file):
1. Use generate_schematic for any of 10 topologies: rc_lowpass,
voltage_divider, inverting_amp, non_inverting_amp, common_emitter_amp,
colpitts_oscillator, differential_amp, buck_converter, ldo_regulator,
h_bridge
2. The .asc file can be opened in LTspice GUI for editing
3. Simulate with the simulate tool