diff --git a/src/mcp_ltspice/asc_generator.py b/src/mcp_ltspice/asc_generator.py index 1c7d4d3..42bcf8c 100644 --- a/src/mcp_ltspice/asc_generator.py +++ b/src/mcp_ltspice/asc_generator.py @@ -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,7 +224,8 @@ class AscSchematic: for win in s.windows: lines.append(win) 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: lines.append(f"TEXT {t.x} {t.y} Left 2 !{t.content}") @@ -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 diff --git a/src/mcp_ltspice/server.py b/src/mcp_ltspice/server.py index 032becd..07881fb 100644 --- a/src/mcp_ltspice/server.py +++ b/src/mcp_ltspice/server.py @@ -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: - return { - "error": f"Unknown template '{template}'. " - f"Available: rc_lowpass, voltage_divider, inverting_amp" - } + 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 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