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:
parent
1afa4f112b
commit
c56ce918b4
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user