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 # LTspice grid spacing -- all coordinates should be multiples of this
GRID = 80 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. # Pin positions (relative to component origin) from LTspice .asy files.
# R0 orientation. Key = symbol name, value = list of (px, py) per pin. # R0 orientation. Key = symbol name, value = list of (px, py) per pin.
_PIN_OFFSETS: dict[str, list[tuple[int, int]]] = { _PIN_OFFSETS: dict[str, list[tuple[int, int]]] = {
# 2-pin passives & sources
"voltage": [(0, 16), (0, 96)], # pin+ , pin- "voltage": [(0, 16), (0, 96)], # pin+ , pin-
"res": [(16, 16), (16, 96)], # pinA , pinB "res": [(16, 16), (16, 96)], # pinA , pinB
"cap": [(16, 0), (16, 64)], # pinA , pinB "cap": [(16, 0), (16, 64)], # pinA , pinB
"ind": [(16, 16), (16, 96)], # pinA , pinB (same body as res) "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,7 +224,8 @@ class AscSchematic:
for win in s.windows: for win in s.windows:
lines.append(win) lines.append(win)
lines.append(f"SYMATTR InstName {s.name}") lines.append(f"SYMATTR InstName {s.name}")
lines.append(f"SYMATTR Value {s.value}") if s.value:
lines.append(f"SYMATTR Value {s.value}")
for t in self._texts: for t in self._texts:
lines.append(f"TEXT {t.x} {t.y} Left 2 !{t.content}") lines.append(f"TEXT {t.x} {t.y} Left 2 !{t.content}")
@ -320,59 +361,899 @@ def generate_voltage_divider(
def generate_inverting_amp( def generate_inverting_amp(
rin: str = "10k", rin: str = "10k",
rf: str = "100k", rf: str = "100k",
opamp_model: str = "UniversalOpamp2",
) -> AscSchematic: ) -> AscSchematic:
"""Generate an inverting op-amp amplifier schematic. """Generate an inverting op-amp amplifier schematic.
Topology:: Topology::
V1 --[Rin]--> inv(-) --[Rf]--> out V1 --[Rin]--> In-(inv) ---[Rf]--- out
non-inv(+) --> GND In+(non-inv) --> GND
Supply: Vpos=+15V, Vneg=-15V Supply: Vpos=+15V, Vneg=-15V
The gain is ``-Rf/Rin``. 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: Args:
rin: Input resistor value rin: Input resistor value
rf: Feedback 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: Returns:
An ``AscSchematic`` ready to ``.save()`` or ``.render()``. An ``AscSchematic`` ready to ``.save()`` or ``.render()``.
""" """
sch = AscSchematic(sheet_w=1200, sheet_h=880) 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. # Op-amp U1 at (512, 336) — same position as non-inverting amp
# V1 at (80, 160) R0: pin+ = (80, 176), pin- = (80, 256) inp = pin_position(oa_sym, 0, 512, 336) # In+ = (480, 352)
# Rin at (160, 160) R0: pinA = (176, 176), pinB = (176, 256) inn = pin_position(oa_sym, 1, 512, 336) # In- = (480, 320)
# Rf at (320, 160) R0: pinA = (336, 176), pinB = (336, 256) 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) # Signal source V1 at (80, 256)
v1n = pin_position("voltage", 1, 80, 160) v1p = pin_position("voltage", 0, 80, 256) # (80, 272)
rin_a = pin_position("res", 0, 160, 160) v1n = pin_position("voltage", 1, 80, 256) # (80, 352)
rin_b = pin_position("res", 1, 160, 160)
rf_a = pin_position("res", 0, 320, 160)
rf_b = pin_position("res", 1, 320, 160)
# V1+ → Rin top # Rin: horizontal (R90) between V1 and In-
sch.add_wire(*v1p, *rin_a) # Origin (400, 304): pinA=(384,320), pinB=(304,320)
# Rin bottom → Rf top (inverting node) rin_a = pin_position("res", 0, 400, 304, 90) # (384, 320)
sch.add_wire(*rin_b, *rf_a) rin_b = pin_position("res", 1, 400, 304, 90) # (304, 320)
sch.add_component("voltage", "V1", "AC 1", 80, 160) # Rf: horizontal (R90) above the op-amp for feedback
sch.add_component("res", "Rin", rin, 160, 160) # Origin (560, 208): pinA=(544,224), pinB=(464,224)
sch.add_component("res", "Rf", rf, 320, 160) 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(*v1n)
sch.add_ground(*rf_b) sch.add_ground(*inp) # In+ to GND (inverting configuration)
sch.add_net_label("inv", *rin_b) sch.add_ground(*vpos_n)
sch.add_net_label("out", *rf_a) 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 return sch

View File

@ -20,9 +20,30 @@ import numpy as np
from fastmcp import FastMCP from fastmcp import FastMCP
from . import __version__ 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 ( from .asc_generator import (
generate_inverting_amp, 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 ( from .asc_generator import (
generate_rc_lowpass as generate_rc_lowpass_asc, 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() @mcp.tool()
def generate_schematic( def generate_schematic(
template: str, template: str,
params: dict[str, str] | None = None,
output_path: 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: ) -> 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: Creates a ready-to-simulate .asc file with proper component placement,
- "rc_lowpass": r (resistor, default "1k"), c (capacitor, default "100n") wire routing, and simulation directives.
- "voltage_divider": r1 (top, default "10k"), r2 (bottom, default "10k"),
vin (input voltage, default "5") Available templates (use list_templates for full details):
- "inverting_amp": rin (input R, default "10k"), rf (feedback R, - rc_lowpass, voltage_divider, inverting_amp, non_inverting_amp,
default "100k"), opamp_model (default "UniversalOpamp2") common_emitter_amp, colpitts_oscillator, differential_amp,
buck_converter, ldo_regulator, h_bridge
Args: Args:
template: Template name template: Template name (see list above)
output_path: Where to save (None = auto in /tmp) params: Override default parameters as key-value pairs.
r: Resistor value (rc_lowpass) Use list_templates to see available parameters for each template.
c: Capacitor value (rc_lowpass) output_path: Where to save the .asc file (None = auto in /tmp)
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)
""" """
if template == "rc_lowpass": if template not in _ASC_TEMPLATES:
params: dict[str, str] = {} names = ", ".join(sorted(_ASC_TEMPLATES))
if r is not None: return {"error": f"Unknown template '{template}'. Available: {names}"}
params["r"] = r
if c is not None: entry = _ASC_TEMPLATES[template]
params["c"] = c call_params: dict[str, str | float] = {}
sch = generate_rc_lowpass_asc(**params)
elif template == "voltage_divider": if params:
params = {} for k, v in params.items():
if r1 is not None: if k not in entry["params"]:
params["r1"] = r1 valid = ", ".join(sorted(entry["params"]))
if r2 is not None: return {
params["r2"] = r2 "error": f"Unknown param '{k}' for {template}. Valid: {valid}"
if vin is not None: }
params["vin"] = vin # duty_cycle needs float conversion for buck_converter
sch = generate_voltage_divider_asc(**params) if k == "duty_cycle":
elif template == "inverting_amp": call_params[k] = float(v)
params = {} else:
if rin is not None: call_params[k] = v
params["rin"] = rin
if rf is not None: sch = entry["func"](**call_params)
params["rf"] = rf
if opamp_model is not None:
params["opamp_model"] = opamp_model
sch = generate_inverting_amp(**params)
else:
return {
"error": f"Unknown template '{template}'. "
f"Available: rc_lowpass, voltage_divider, inverting_amp"
}
if output_path is None: if output_path is None:
output_path = str(Path(tempfile.gettempdir()) / f"{template}.asc") output_path = str(Path(tempfile.gettempdir()) / f"{template}.asc")
@ -1179,6 +1248,7 @@ def generate_schematic(
return { return {
"success": True, "success": True,
"output_path": str(saved), "output_path": str(saved),
"template": template,
"schematic_preview": sch.render()[:500], "schematic_preview": sch.render()[:500],
} }
@ -2044,9 +2114,11 @@ Approach 2 - Build from components:
4. Add simulation directives (.tran, .ac, .dc, .op, .tf) 4. Add simulation directives (.tran, .ac, .dc, .op, .tf)
5. Simulate and analyze 5. Simulate and analyze
Approach 3 - Graphical schematic: Approach 3 - Graphical schematic (.asc file):
1. Use generate_schematic for supported topologies (rc_lowpass, 1. Use generate_schematic for any of 10 topologies: rc_lowpass,
voltage_divider, inverting_amp) 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 2. The .asc file can be opened in LTspice GUI for editing
3. Simulate with the simulate tool 3. Simulate with the simulate tool