"""Programmatic LTspice .asc schematic file generation. Generates graphical schematics (the .asc format LTspice uses for its GUI), not just text netlists. Components are placed using absolute pin positions derived from the .asy symbol files and rotated with the standard CCW transform: R90 applies (px, py) → (-py, px) relative to the component origin. """ from __future__ import annotations from dataclasses import dataclass, field from pathlib import Path # LTspice grid spacing -- all coordinates should be multiples of this GRID = 80 _SPICE_SUFFIXES = { "T": 1e12, "G": 1e9, "MEG": 1e6, "K": 1e3, "M": 1e-3, "U": 1e-6, "N": 1e-9, "P": 1e-12, "F": 1e-15, } def _parse_spice_value(value: str) -> float: """Convert a SPICE-style value string to a float (e.g., '100k' → 100000.0).""" value = value.strip() try: return float(value) except ValueError: pass upper = value.upper() for suffix, mult in sorted(_SPICE_SUFFIXES.items(), key=lambda x: -len(x[0])): if upper.endswith(suffix): try: return float(value[: len(value) - len(suffix)]) * mult except ValueError: continue raise ValueError(f"Cannot parse SPICE value: {value!r}") # Pin positions (relative to component origin) from LTspice .asy files. # R0 orientation. Key = symbol name, value = list of (px, py) per pin. _PIN_OFFSETS: dict[str, list[tuple[int, int]]] = { # 2-pin passives & sources "voltage": [(0, 16), (0, 96)], # pin+ , pin- "res": [(16, 16), (16, 96)], # pinA , pinB "cap": [(16, 0), (16, 64)], # pinA , pinB "ind": [(16, 16), (16, 96)], # pinA , pinB (same body as res) "diode": [(16, 0), (16, 64)], # anode(+) , cathode(-) # 3-pin semiconductors "npn": [(64, 0), (0, 48), (64, 96)], # C , B , E "pnp": [(64, 0), (0, 48), (64, 96)], # C , B , E (same geometry) "nmos": [(48, 0), (0, 80), (48, 96)], # D , G , S "pmos": [(48, 0), (0, 80), (48, 96)], # D , G , S # 4-pin MOSFETs (with body) "nmos4": [(48, 0), (0, 80), (48, 96), (48, 48)], # D , G , S , B "pmos4": [(48, 0), (0, 80), (48, 96), (48, 48)], # D , G , S , B # 5-pin op-amp (note: negative offsets = pins extend left/above origin) "OpAmps/UniversalOpamp2": [ (-32, 16), # In+ (non-inverting) (-32, -16), # In- (inverting) (0, -32), # V+ (positive supply) (0, 32), # V- (negative supply) (32, 0), # OUT ], } def _rotate(px: int, py: int, rotation: int) -> tuple[int, int]: """Apply LTspice rotation to a relative pin offset. LTspice rotations are counterclockwise: (px, py) → (-py, px) for R90. """ if rotation == 0: return px, py if rotation == 90: return -py, px if rotation == 180: return -px, -py if rotation == 270: return py, -px return px, py def pin_position( symbol: str, pin_index: int, cx: int, cy: int, rotation: int = 0 ) -> tuple[int, int]: """Compute absolute pin position for a component. Args: symbol: Base symbol name (``"res"``, ``"cap"``, ``"voltage"``, ...) pin_index: 0 for first pin, 1 for second pin, etc. cx: Component origin X cy: Component origin Y rotation: 0, 90, 180, or 270 Returns: Absolute (x, y) of the pin. """ offsets = _PIN_OFFSETS.get(symbol, [(0, 0), (0, 80)]) px, py = offsets[pin_index % len(offsets)] rx, ry = _rotate(px, py, rotation) return cx + rx, cy + ry @dataclass class _WireEntry: x1: int y1: int x2: int y2: int @dataclass class _SymbolEntry: symbol: str name: str value: str x: int y: int rotation: int # 0, 90, 180, 270 windows: list[str] = field(default_factory=list) @dataclass class _FlagEntry: x: int y: int name: str @dataclass class _TextEntry: x: int y: int content: str class AscSchematic: """Builder for LTspice .asc schematic files. All ``add_*`` methods return ``self`` for chaining:: sch = (AscSchematic() .add_component("res", "R1", "1k", 160, 176, rotation=90) .add_wire(80, 176, 160, 176) .add_ground(80, 256)) """ def __init__(self, sheet_w: int = 880, sheet_h: int = 680) -> None: self._sheet_w = sheet_w self._sheet_h = sheet_h self._wires: list[_WireEntry] = [] self._symbols: list[_SymbolEntry] = [] self._flags: list[_FlagEntry] = [] self._texts: list[_TextEntry] = [] # -- builder methods ----------------------------------------------------- def add_component( self, symbol: str, name: str, value: str, x: int, y: int, rotation: int = 0, ) -> AscSchematic: """Place a component symbol. Args: symbol: LTspice symbol name (``res``, ``cap``, ``voltage``, etc.) name: Instance name (``R1``, ``C1``, ``V1``, ...) value: Component value (``1k``, ``100n``, ``AC 1``, ...) x: X coordinate (should be on 80-pixel grid) y: Y coordinate rotation: 0, 90, 180, or 270 degrees """ windows: list[str] = [] # For resistors and inductors placed at R90, shift the WINDOW lines # so labels sit neatly above/below the body if rotation == 90 and symbol in ("res", "ind", "ind2"): windows = [ "WINDOW 0 0 56 VBottom 2", "WINDOW 3 32 56 VTop 2", ] self._symbols.append(_SymbolEntry(symbol, name, value, x, y, rotation, windows)) return self def add_wire(self, x1: int, y1: int, x2: int, y2: int) -> AscSchematic: """Add a wire segment between two points.""" self._wires.append(_WireEntry(x1, y1, x2, y2)) return self def add_ground(self, x: int, y: int) -> AscSchematic: """Place a ground flag (net name ``0``).""" self._flags.append(_FlagEntry(x, y, "0")) return self def add_net_label(self, name: str, x: int, y: int) -> AscSchematic: """Place a named net label (e.g., ``out``, ``vdd``).""" self._flags.append(_FlagEntry(x, y, name)) return self def add_directive(self, text: str, x: int, y: int) -> AscSchematic: """Add a SPICE directive (rendered with ``!`` prefix).""" self._texts.append(_TextEntry(x, y, text)) return self # -- output -------------------------------------------------------------- def render(self) -> str: """Render the schematic to an ``.asc`` format string.""" lines: list[str] = [] lines.append("Version 4") lines.append(f"SHEET 1 {self._sheet_w} {self._sheet_h}") for w in self._wires: lines.append(f"WIRE {w.x1} {w.y1} {w.x2} {w.y2}") for f in self._flags: lines.append(f"FLAG {f.x} {f.y} {f.name}") for s in self._symbols: lines.append(f"SYMBOL {s.symbol} {s.x} {s.y} R{s.rotation}") for win in s.windows: lines.append(win) lines.append(f"SYMATTR InstName {s.name}") if s.value: lines.append(f"SYMATTR Value {s.value}") for t in self._texts: lines.append(f"TEXT {t.x} {t.y} Left 2 !{t.content}") return "\n".join(lines) + "\n" def save(self, path: Path | str) -> Path: """Write the schematic to an ``.asc`` file on disk.""" path = Path(path) path.write_text(self.render(), encoding="utf-8") return path # ============================================================================ # Layout helper functions -- auto-placed, ready-to-simulate schematics # ============================================================================ def generate_rc_lowpass(r: str = "1k", c: str = "100n") -> AscSchematic: """Generate an RC lowpass filter schematic with AC analysis. Signal flow (left to right):: V1 --[R1]-- out --+ | [C1] | GND Args: r: Resistor value (e.g., ``"1k"``, ``"4.7k"``) c: Capacitor value (e.g., ``"100n"``, ``"10p"``) Returns: An ``AscSchematic`` ready to ``.save()`` or ``.render()``. """ sch = AscSchematic() # Component origins (all on 16-pixel sub-grid for pin alignment) # V1 at (80, 80) R0: pin+ = (80, 96), pin- = (80, 176) # R1 at (160, 80) R0: pinA = (176, 96), pinB = (176, 176) # C1 at (256, 176) R0: pinA = (272, 176), pinB = (272, 240) v1p = pin_position("voltage", 0, 80, 80) # (80, 96) v1n = pin_position("voltage", 1, 80, 80) # (80, 176) r1a = pin_position("res", 0, 160, 80) # (176, 96) r1b = pin_position("res", 1, 160, 80) # (176, 176) c1a = pin_position("cap", 0, 256, 176) # (272, 176) c1b = pin_position("cap", 1, 256, 176) # (272, 240) # Wires sch.add_wire(*v1p, *r1a) # V1+ to R1 top sch.add_wire(*r1b, *c1a) # R1 bottom to C1 top (junction = "out") # Components sch.add_component("voltage", "V1", "AC 1", 80, 80) sch.add_component("res", "R1", r, 160, 80) sch.add_component("cap", "C1", c, 256, 176) # Ground flags at V1- and C1 bottom sch.add_ground(*v1n) sch.add_ground(*c1b) # Output net label at the R1-C1 junction sch.add_net_label("out", *r1b) # Simulation directive sch.add_directive(".ac dec 100 1 1meg", 80, 296) return sch def generate_voltage_divider( r1: str = "10k", r2: str = "10k", vin: str = "5", ) -> AscSchematic: """Generate a voltage divider schematic with operating-point analysis. Topology (vertical):: V1+ --[R1]-- out --[R2]-- GND V1- = GND Args: r1: Top resistor value r2: Bottom resistor value vin: DC input voltage Returns: An ``AscSchematic`` ready to ``.save()`` or ``.render()``. """ sch = AscSchematic() # Vertical layout: V1 on left, R1 and R2 stacked vertically on right # V1 at (80, 80) R0: pin+ = (80, 96), pin- = (80, 176) # R1 at (176, 0) R0: pinA = (192, 16), pinB = (192, 96) # R2 at (176, 96) R0: pinA = (192, 112), pinB = (192, 192) v1p = pin_position("voltage", 0, 80, 80) # (80, 96) v1n = pin_position("voltage", 1, 80, 80) # (80, 176) r1a = pin_position("res", 0, 176, 0) # (192, 16) r1b = pin_position("res", 1, 176, 0) # (192, 96) r2a = pin_position("res", 0, 176, 96) # (192, 112) r2b = pin_position("res", 1, 176, 96) # (192, 192) # Wires: V1+ to R1 top, R1 bottom to R2 top (junction = "out") sch.add_wire(*v1p, *r1a) # need to connect (80,96) to (192,16) # Route: V1+ up then right then down to R1 top sch.add_wire(80, 96, 80, 16) sch.add_wire(80, 16, 192, 16) # R1 bottom to R2 top sch.add_wire(*r1b, *r2a) # Components sch.add_component("voltage", "V1", vin, 80, 80) sch.add_component("res", "R1", r1, 176, 0) sch.add_component("res", "R2", r2, 176, 96) # Ground sch.add_ground(*v1n) sch.add_ground(*r2b) # Net label at R1-R2 junction sch.add_net_label("out", *r1b) # Directive sch.add_directive(".op", 80, 240) return sch def generate_inverting_amp( rin: str = "10k", rf: str = "100k", ) -> AscSchematic: """Generate an inverting op-amp amplifier schematic. Topology:: V1 --[Rin]--> In-(inv) ---[Rf]--- out In+(non-inv) --> GND Supply: Vpos=+15V, Vneg=-15V The gain is ``-Rf/Rin``. Args: rin: Input resistor value rf: Feedback resistor value Returns: An ``AscSchematic`` ready to ``.save()`` or ``.render()``. """ sch = AscSchematic(sheet_w=1200, sheet_h=880) oa_sym = "OpAmps/UniversalOpamp2" # Op-amp U1 at (512, 336) — same position as non-inverting amp inp = pin_position(oa_sym, 0, 512, 336) # In+ = (480, 352) inn = pin_position(oa_sym, 1, 512, 336) # In- = (480, 320) vp = pin_position(oa_sym, 2, 512, 336) # V+ = (512, 304) vn = pin_position(oa_sym, 3, 512, 336) # V- = (512, 368) out = pin_position(oa_sym, 4, 512, 336) # OUT = (544, 336) # Signal source V1 at (80, 256) v1p = pin_position("voltage", 0, 80, 256) # (80, 272) v1n = pin_position("voltage", 1, 80, 256) # (80, 352) # Rin: horizontal (R90) between V1 and In- # Origin (400, 304): pinA=(384,320), pinB=(304,320) rin_a = pin_position("res", 0, 400, 304, 90) # (384, 320) rin_b = pin_position("res", 1, 400, 304, 90) # (304, 320) # Rf: horizontal (R90) above the op-amp for feedback # Origin (560, 208): pinA=(544,224), pinB=(464,224) rf_a = pin_position("res", 0, 560, 208, 90) # (544, 224) rf_b = pin_position("res", 1, 560, 208, 90) # (464, 224) # Supply: Vpos at (688, 176), Vneg at (688, 416) vpos_p = pin_position("voltage", 0, 688, 176) # (688, 192) vpos_n = pin_position("voltage", 1, 688, 176) # (688, 272) vneg_p = pin_position("voltage", 0, 688, 416) # (688, 432) vneg_n = pin_position("voltage", 1, 688, 416) # (688, 512) # === WIRING === # V1+ to Rin pinB: route RIGHT first then down (avoids crossing V1-) sch.add_wire(v1p[0], v1p[1], rin_b[0], v1p[1]) # (80,272) → (304,272) sch.add_wire(rin_b[0], v1p[1], *rin_b) # (304,272) → (304,320) # Rin pinA to In- sch.add_wire(*rin_a, inn[0], inn[1]) # (384,320) → (480,320) # Rf feedback: OUT up to Rf pinA, Rf pinB left and down to inv junction sch.add_wire(out[0], out[1], *rf_a) # (544,336) → (544,224) sch.add_wire(*rf_b, rin_a[0], rf_b[1]) # (464,224) → (384,224) sch.add_wire(rin_a[0], rf_b[1], *rin_a) # (384,224) → (384,320) # Supply wiring sch.add_wire(vp[0], vp[1], vp[0], vpos_p[1]) # V+ up to (512,192) sch.add_wire(vp[0], vpos_p[1], *vpos_p) # right to Vpos+ sch.add_wire(vn[0], vn[1], vn[0], vneg_n[1]) # V- down to (512,512) sch.add_wire(vn[0], vneg_n[1], *vneg_n) # right to Vneg- # === COMPONENTS === sch.add_component("voltage", "V1", "AC 1", 80, 256) sch.add_component(oa_sym, "U1", "", 512, 336) sch.add_component("res", "Rin", rin, 400, 304, rotation=90) sch.add_component("res", "Rf", rf, 560, 208, rotation=90) sch.add_component("voltage", "Vpos", "15", 688, 176) sch.add_component("voltage", "Vneg", "15", 688, 416) # === FLAGS === sch.add_ground(*v1n) sch.add_ground(*inp) # In+ to GND (inverting configuration) sch.add_ground(*vpos_n) sch.add_ground(*vneg_p) sch.add_net_label("out", *out) sch.add_directive(".ac dec 100 1 1meg", 80, 560) return sch def generate_non_inverting_amp( rin: str = "10k", rf: str = "100k", ) -> AscSchematic: """Generate a non-inverting op-amp amplifier schematic. Topology:: V1 --> In+ U1 --> out GND --> Rin --> In- ^--- Rf --- out Gain = 1 + Rf/Rin. Supply: +/-15V Args: rin: Input resistor (In- to GND) rf: Feedback resistor (In- to out) """ sch = AscSchematic(sheet_w=1200, sheet_h=880) oa_sym = "OpAmps/UniversalOpamp2" # Op-amp U1 at (512, 336) inp = pin_position(oa_sym, 0, 512, 336) # In+ = (480, 352) inn = pin_position(oa_sym, 1, 512, 336) # In- = (480, 320) vp = pin_position(oa_sym, 2, 512, 336) # V+ = (512, 304) vn = pin_position(oa_sym, 3, 512, 336) # V- = (512, 368) out = pin_position(oa_sym, 4, 512, 336) # OUT = (544, 336) # Signal source V1 at (80, 256) v1p = pin_position("voltage", 0, 80, 256) # (80, 272) v1n = pin_position("voltage", 1, 80, 256) # (80, 352) # Rin: In- node down to GND. Want pinA at In- junction (352, 320). # res origin so that pinA = (352, 320): origin = (336, 304) rin_a = pin_position("res", 0, 336, 304) # (352, 320) rin_b = pin_position("res", 1, 336, 304) # (352, 400) # Rf: horizontal (R90) above the op-amp for feedback path # R90 res: pinA offset=(-16,16), pinB offset=(-96,16) # Place at origin (560, 208) so: pinA=(544, 224), pinB=(464, 224) rf_a = pin_position("res", 0, 560, 208, 90) # (544, 224) rf_b = pin_position("res", 1, 560, 208, 90) # (464, 224) # Supply: Vpos at (688, 176), Vneg at (688, 416) vpos_p = pin_position("voltage", 0, 688, 176) # (688, 192) = vdd vpos_n = pin_position("voltage", 1, 688, 176) # (688, 272) vneg_p = pin_position("voltage", 0, 688, 416) # (688, 432) vneg_n = pin_position("voltage", 1, 688, 416) # (688, 512) = vss # === WIRING === # V1+ to In+: go RIGHT then DOWN then RIGHT to avoid crossing both # V1- at (80,352) and In- at (480,320). # Route: (80,272) → (448,272) → (448,352) → (480,352) sch.add_wire(v1p[0], v1p[1], 448, v1p[1]) # (80,272) → (448,272) horiz sch.add_wire(448, v1p[1], 448, inp[1]) # (448,272) → (448,352) vert sch.add_wire(448, inp[1], inp[0], inp[1]) # (448,352) → (480,352) horiz # In- to Rin junction sch.add_wire(inn[0], inn[1], rin_a[0], rin_a[1]) # (480,320) → (352,320) # Rf feedback: OUT up to Rf pinA, Rf pinB left and down to junction sch.add_wire(out[0], out[1], rf_a[0], rf_a[1]) # (544,336) → (544,224) vertical sch.add_wire(rf_b[0], rf_b[1], rin_a[0], rf_b[1]) # (464,224) → (352,224) horizontal sch.add_wire(rin_a[0], rf_b[1], rin_a[0], rin_a[1]) # (352,224) → (352,320) vertical # Supply wiring sch.add_wire(vp[0], vp[1], vp[0], vpos_p[1]) # V+ up to (512,192) sch.add_wire(vp[0], vpos_p[1], vpos_p[0], vpos_p[1]) # right to Vpos+ sch.add_wire(vn[0], vn[1], vn[0], vneg_n[1]) # V- down to (512,512) sch.add_wire(vn[0], vneg_n[1], vneg_n[0], vneg_n[1]) # right to Vneg- # === COMPONENTS === sch.add_component("voltage", "V1", "AC 1", 80, 256) sch.add_component(oa_sym, "U1", "", 512, 336) sch.add_component("res", "Rin", rin, 336, 304) sch.add_component("res", "Rf", rf, 560, 208, rotation=90) sch.add_component("voltage", "Vpos", "15", 688, 176) sch.add_component("voltage", "Vneg", "15", 688, 416) # === FLAGS === sch.add_ground(*v1n) sch.add_ground(*rin_b) sch.add_ground(*vpos_n) sch.add_ground(*vneg_p) sch.add_net_label("out", *out) sch.add_directive(".ac dec 100 1 1meg", 80, 560) return sch def generate_common_emitter_amp( rc: str = "2.2k", rb1: str = "56k", rb2: str = "12k", re: str = "1k", cc_in: str = "10u", cc_out: str = "10u", ce: str = "47u", vcc: str = "12", bjt_model: str = "2N2222", ) -> AscSchematic: """Generate a common-emitter amplifier schematic. Topology:: Vcc --[RC]--> collector --[CC_out]--> out Vcc --[RB1]--> base in --[CC_in]--> base base --[RB2]--> GND emitter --[RE]--> GND emitter --[CE]--> GND (bypass) Args: rc: Collector resistor rb1: Base bias resistor (to Vcc) rb2: Base bias resistor (to GND) re: Emitter resistor cc_in: Input coupling capacitor cc_out: Output coupling capacitor ce: Emitter bypass capacitor vcc: Supply voltage bjt_model: BJT model name """ sch = AscSchematic(sheet_w=1200, sheet_h=880) # Q1 (NPN) at (400, 288): C=(464, 288), B=(400, 336), E=(464, 384) qc = pin_position("npn", 0, 400, 288) # collector qb = pin_position("npn", 1, 400, 288) # base qe = pin_position("npn", 2, 400, 288) # emitter # RC from Vcc rail to collector. # Want pinB at collector (464, 288). res at (448, 192): pinB=(464, 288). Good. rc_a = pin_position("res", 0, 448, 192) # (464, 208) = vcc rail # RE from emitter to GND. # Want pinA at emitter (464, 384). res at (448, 368): pinA=(464, 384). re_b = pin_position("res", 1, 448, 368) # (464, 464) = GND # RB1 from Vcc rail to base. Vertical, placed to the left of Q1. # Want pinB at base Y (336). res at (240, 240): pinA=(256, 256), pinB=(256, 336) rb1_a = pin_position("res", 0, 240, 240) # (256, 256) rb1_b = pin_position("res", 1, 240, 240) # (256, 336) # RB2 from base to GND. # Want pinA at base Y (336). res at (240, 320): pinA=(256, 336), pinB=(256, 416) rb2_b = pin_position("res", 1, 240, 320) # (256, 416) # CC_in (input coupling cap) horizontal, connecting input to base. # cap R90: pinA=origin+(-16,0)→(origin_x, origin_y+16)... wait let me recalculate. # cap R90: pinA offset (16,0) → rotate90 → (0, 16), pinB offset (16,64) → (-64, 16) # At origin (368, 320): pinA = (368, 336), pinB = (304, 336) # pinA at (368, 336) close to base (400, 336). pinB at (304, 336). # Wire from pinA (368, 336) to base (400, 336). ccin_a = pin_position("cap", 0, 368, 320, 90) # (368, 336) ccin_b = pin_position("cap", 1, 368, 320, 90) # (304, 336) # CC_out (output coupling cap) to the right of collector. # cap R90 at origin (560, 272): pinA = (560, 288), pinB = (496, 288) # pinB near collector (464, 288), pinA extends right to output # Actually pinB = (560-64, 288) = (496, 288). Wire from collector (464,288) to (496,288). ccout_a = pin_position("cap", 0, 560, 272, 90) # (560, 288) ccout_b = pin_position("cap", 1, 560, 272, 90) # (496, 288) # CE (emitter bypass cap) parallel to RE. # Place to the right of RE. cap at (528, 384): pinA=(544, 384), pinB=(544, 448) ce_a = pin_position("cap", 0, 528, 384) # (544, 384) ce_b = pin_position("cap", 1, 528, 384) # (544, 448) # Vcc source at (80, 96): pin+=(80, 112), pin-=(80, 192) vcc_p = pin_position("voltage", 0, 80, 96) # (80, 112) vcc_n = pin_position("voltage", 1, 80, 96) # (80, 192) # Vin source at (80, 288): pin+=(80, 304), pin-=(80, 384) vin_p = pin_position("voltage", 0, 80, 288) # (80, 304) vin_n = pin_position("voltage", 1, 80, 288) # (80, 384) # === WIRING === # Vcc rail at y=208 — route RIGHT from Vcc+ first (avoid crossing Vcc- at 80,192) vcc_y = rc_a[1] # 208 sch.add_wire(vcc_p[0], vcc_p[1], 160, vcc_p[1]) # (80,112) → (160,112) sch.add_wire(160, vcc_p[1], 160, vcc_y) # (160,112) → (160,208) sch.add_wire(160, vcc_y, rc_a[0], vcc_y) # (160,208) → (464,208) = RC top sch.add_wire(rb1_a[0], rb1_a[1], rb1_a[0], vcc_y) # RB1 top up to rail sch.add_wire(rb1_a[0], vcc_y, rc_a[0], vcc_y) # connect to rail # RB1 bottom to base, RB2 top at same junction sch.add_wire(rb1_b[0], rb1_b[1], qb[0], qb[1]) # RB1 bottom → base # CC_in to base sch.add_wire(ccin_a[0], ccin_a[1], qb[0], qb[1]) # CC_in right → base # Input: Vin+ to CC_in left sch.add_wire(vin_p[0], vin_p[1], ccin_b[0], ccin_b[1]) # Vin+ → CC_in left # Collector to CC_out sch.add_wire(qc[0], qc[1], ccout_b[0], ccout_b[1]) # collector → CC_out left # Emitter to CE (bypass cap in parallel with RE) sch.add_wire(qe[0], qe[1], ce_a[0], ce_a[1]) # emitter → CE top # CE bottom to RE bottom (shared GND rail) sch.add_wire(ce_b[0], ce_b[1], re_b[0], re_b[1]) # CE bot → RE bot # === COMPONENTS === sch.add_component("npn", "Q1", bjt_model, 400, 288) sch.add_component("res", "RC", rc, 448, 192) sch.add_component("res", "RE", re, 448, 368) sch.add_component("res", "RB1", rb1, 240, 240) sch.add_component("res", "RB2", rb2, 240, 320) sch.add_component("cap", "CC1", cc_in, 368, 320, rotation=90) sch.add_component("cap", "CC2", cc_out, 560, 272, rotation=90) sch.add_component("cap", "CE", ce, 528, 384) sch.add_component("voltage", "Vcc", vcc, 80, 96) sch.add_component("voltage", "Vin", "SINE(0 10m 1k)", 80, 288) # === FLAGS === sch.add_ground(*vcc_n) sch.add_ground(*vin_n) sch.add_ground(*rb2_b) sch.add_ground(*re_b) sch.add_net_label("out", *ccout_a) sch.add_directive(".tran 5m", 80, 528) return sch def generate_colpitts_oscillator( ind: str = "1u", c1: str = "100p", c2: str = "100p", rb: str = "47k", rc: str = "1k", re: str = "470", vcc: str = "12", bjt_model: str = "2N2222", ) -> AscSchematic: """Generate a Colpitts oscillator schematic. Topology:: Vcc --[RC]--> collector --[L1]--> tank Vcc --[RB]--> base tank --[C1]--> base tank --[C2]--> GND emitter --[RE]--> GND f ~ 1/(2*pi*sqrt(L*C1*C2/(C1+C2))) """ sch = AscSchematic(sheet_w=1200, sheet_h=880) # Q1 NPN at (400, 288): C=(464,288), B=(400,336), E=(464,384) qc = pin_position("npn", 0, 400, 288) qb = pin_position("npn", 1, 400, 288) # RC: Vcc to collector. res at (448, 192): pinA=(464,208), pinB=(464,288) rc_a = pin_position("res", 0, 448, 192) # RE: emitter to GND. res at (448, 368): pinA=(464,384), pinB=(464,464) re_b = pin_position("res", 1, 448, 368) # RB: Vcc to base. res at (240, 240): pinA=(256,256), pinB=(256,336) rb_a = pin_position("res", 0, 240, 240) rb_b = pin_position("res", 1, 240, 240) # Vcc source at (80, 96): pin+=(80,112), pin-=(80,192) vcc_p = pin_position("voltage", 0, 80, 96) vcc_n = pin_position("voltage", 1, 80, 96) # L1 horizontal (R90) from collector to tank node. # ind R90 at origin (576, 272): # pinA = origin+(-16,16) = (560, 288) — right/tank side # pinB = origin+(-96,16) = (480, 288) — left/collector side l1_a = pin_position("ind", 0, 576, 272, 90) # (560, 288) = tank l1_b = pin_position("ind", 1, 576, 272, 90) # (480, 288) = near collector # C1 from tank down to base (feedback). # cap at (544, 288): pinA=(560, 288)=tank (same as l1_a), pinB=(560, 352) c1_b = pin_position("cap", 1, 544, 288) # (560, 352) # C2 from tank to GND. # cap at (640, 288): pinA=(656, 288), pinB=(656, 352) c2_a = pin_position("cap", 0, 640, 288) # (656, 288) c2_b = pin_position("cap", 1, 640, 288) # (656, 352) # === WIRING === vcc_y = rc_a[1] # 208 # Vcc rail: route RIGHT from Vcc+ first (avoid crossing Vcc- at 80,192) sch.add_wire(vcc_p[0], vcc_p[1], 160, vcc_p[1]) # (80,112) → (160,112) sch.add_wire(160, vcc_p[1], 160, vcc_y) # (160,112) → (160,208) sch.add_wire(160, vcc_y, rc_a[0], vcc_y) # (160,208) → RC top sch.add_wire(rb_a[0], rb_a[1], rb_a[0], vcc_y) sch.add_wire(rb_a[0], vcc_y, rc_a[0], vcc_y) # RB bottom to base sch.add_wire(rb_b[0], rb_b[1], qb[0], qb[1]) # Collector to L1 left pin sch.add_wire(qc[0], qc[1], l1_b[0], l1_b[1]) # Tank node: L1 right pin to C1 top (same point at 560,288) # Wire from tank to C2 top sch.add_wire(l1_a[0], l1_a[1], c2_a[0], c2_a[1]) # C1 bottom to base: L-route down then left sch.add_wire(c1_b[0], c1_b[1], qb[0], c1_b[1]) # horizontal sch.add_wire(qb[0], c1_b[1], qb[0], qb[1]) # vertical to base # === COMPONENTS === sch.add_component("npn", "Q1", bjt_model, 400, 288) sch.add_component("res", "RC", rc, 448, 192) sch.add_component("res", "RE", re, 448, 368) sch.add_component("res", "RB", rb, 240, 240) sch.add_component("ind", "L1", ind, 576, 272, rotation=90) sch.add_component("cap", "C1", c1, 544, 288) sch.add_component("cap", "C2", c2, 640, 288) sch.add_component("voltage", "Vcc", vcc, 80, 96) # === FLAGS === sch.add_ground(*vcc_n) sch.add_ground(*re_b) sch.add_ground(*c2_b) sch.add_net_label("out", *qc) sch.add_directive(".tran 100u", 80, 528) sch.add_directive(".ic V(out)=6", 80, 560) return sch def generate_differential_amp( r1: str = "10k", r2: str = "10k", r3: str = "10k", r4: str = "10k", ) -> AscSchematic: """Generate a differential amplifier schematic. Topology:: V1 --[R1]--> inv(-) --[R2]--> out V2 --[R3]--> non-inv(+) non-inv(+) --[R4]--> GND Supply: +/-15V Vout = (R2/R1) * (V2 - V1) when R2/R1 = R4/R3 Args: r1: Input resistor to inverting node r2: Feedback resistor r3: Input resistor to non-inverting node r4: Non-inverting node to GND """ sch = AscSchematic(sheet_w=1200, sheet_h=880) oa_sym = "OpAmps/UniversalOpamp2" # U1 at (512, 336): In+=(480,352), In-=(480,320), V+=(512,304), V-=(512,368), OUT=(544,336) inp = pin_position(oa_sym, 0, 512, 336) # (480, 352) inn = pin_position(oa_sym, 1, 512, 336) # (480, 320) vp = pin_position(oa_sym, 2, 512, 336) # (512, 304) vn = pin_position(oa_sym, 3, 512, 336) # (512, 368) out = pin_position(oa_sym, 4, 512, 336) # (544, 336) # R1 horizontal (R90) from V1 to inv junction. # R90 at (448, 304): pinA=(432,320), pinB=(352,320) r1_a = pin_position("res", 0, 448, 304, 90) # (432, 320) near In- r1_b = pin_position("res", 1, 448, 304, 90) # (352, 320) toward V1 # R2 horizontal (R90) above op-amp for feedback. # R90 at (560, 208): pinA=(544,224), pinB=(464,224) r2_a = pin_position("res", 0, 560, 208, 90) # (544, 224) near OUT r2_b = pin_position("res", 1, 560, 208, 90) # (464, 224) # R3 horizontal (R90) from V2 to noninv junction. # R90 at (448, 336): pinA=(432,352), pinB=(352,352) r3_a = pin_position("res", 0, 448, 336, 90) # (432, 352) near In+ r3_b = pin_position("res", 1, 448, 336, 90) # (352, 352) toward V2 # R4 vertical from noninv junction to GND. # res at (336, 352): pinA=(352,368), pinB=(352,448) r4_a = pin_position("res", 0, 336, 352) # (352, 368) r4_b = pin_position("res", 1, 336, 352) # (352, 448) # V1 at (80, 224): pin+=(80,240), pin-=(80,320) v1p = pin_position("voltage", 0, 80, 224) v1n = pin_position("voltage", 1, 80, 224) # V2 at (80, 384): pin+=(80,400), pin-=(80,480) v2p = pin_position("voltage", 0, 80, 384) v2n = pin_position("voltage", 1, 80, 384) # Supply: Vpos at (688, 176), Vneg at (688, 416) vpos_p = pin_position("voltage", 0, 688, 176) vpos_n = pin_position("voltage", 1, 688, 176) vneg_p = pin_position("voltage", 0, 688, 416) vneg_n = pin_position("voltage", 1, 688, 416) # === WIRING === # Inv path: R1 pinA to In- sch.add_wire(r1_a[0], r1_a[1], inn[0], inn[1]) # V1 to R1 pinB: route RIGHT first (avoid crossing V1- at 80,320) sch.add_wire(v1p[0], v1p[1], r1_b[0], v1p[1]) # (80,240) → (352,240) sch.add_wire(r1_b[0], v1p[1], r1_b[0], r1_b[1]) # (352,240) → (352,320) # R2 feedback: OUT up to R2 pinA, R2 pinB left and down to inv junction sch.add_wire(out[0], out[1], r2_a[0], r2_a[1]) # (544,336)→(544,224) sch.add_wire(r2_b[0], r2_b[1], r1_a[0], r2_b[1]) # (464,224)→(432,224) sch.add_wire(r1_a[0], r2_b[1], r1_a[0], r1_a[1]) # (432,224)→(432,320) # Noninv path: R3 pinA to In+ sch.add_wire(r3_a[0], r3_a[1], inp[0], inp[1]) # V2 to R3 pinB: route (80,400) → (80,352) → (352,352) sch.add_wire(v2p[0], v2p[1], v2p[0], r3_b[1]) sch.add_wire(v2p[0], r3_b[1], r3_b[0], r3_b[1]) # R4 from noninv junction to GND sch.add_wire(r3_b[0], r3_b[1], r4_a[0], r4_a[1]) # (352,352)→(352,368) # Supply wiring sch.add_wire(vp[0], vp[1], vp[0], vpos_p[1]) sch.add_wire(vp[0], vpos_p[1], vpos_p[0], vpos_p[1]) sch.add_wire(vn[0], vn[1], vn[0], vneg_n[1]) sch.add_wire(vn[0], vneg_n[1], vneg_n[0], vneg_n[1]) # === COMPONENTS === sch.add_component("voltage", "V1", "AC 1", 80, 224) sch.add_component("voltage", "V2", "AC 1", 80, 384) sch.add_component(oa_sym, "U1", "", 512, 336) sch.add_component("res", "R1", r1, 448, 304, rotation=90) sch.add_component("res", "R2", r2, 560, 208, rotation=90) sch.add_component("res", "R3", r3, 448, 336, rotation=90) sch.add_component("res", "R4", r4, 336, 352) sch.add_component("voltage", "Vpos", "15", 688, 176) sch.add_component("voltage", "Vneg", "15", 688, 416) # === FLAGS === sch.add_ground(*v1n) sch.add_ground(*v2n) sch.add_ground(*r4_b) sch.add_ground(*vpos_n) sch.add_ground(*vneg_p) sch.add_net_label("out", *out) sch.add_directive(".ac dec 100 1 1meg", 80, 560) return sch def generate_buck_converter( ind: str = "10u", c_out: str = "100u", r_load: str = "10", v_in: str = "12", duty_cycle: float = 0.5, freq: str = "100k", mosfet_model: str = "IRF540N", diode_model: str = "1N5819", ) -> AscSchematic: """Generate a buck (step-down) converter schematic. Topology:: Vin+ ─── drain M1 (NMOS switch) source ── sw ──[L1]── out ──+ | | [D1] [Cout] [Rload] | | | GND GND GND Gate driven by PULSE source at specified frequency and duty cycle. Args: ind: Inductor value c_out: Output capacitor r_load: Load resistor v_in: Input voltage duty_cycle: Switching duty cycle (0.0-1.0) freq: Switching frequency mosfet_model: NMOS model name diode_model: Freewheeling diode model """ sch = AscSchematic(sheet_w=1040, sheet_h=680) # Compute PULSE timing freq_hz = _parse_spice_value(freq) period = 1.0 / freq_hz t_on = period * duty_cycle t_rise = period * 0.01 t_fall = t_rise pulse_val = ( f"PULSE(0 {v_in} 0 {t_rise:.4g} {t_fall:.4g} {t_on:.4g} {period:.4g})" ) # Vin at (80, 48): pin+=(80,64), pin-=(80,144) vin_p = pin_position("voltage", 0, 80, 48) vin_n = pin_position("voltage", 1, 80, 48) # Vgate at (80, 256): pin+=(80,272), pin-=(80,352) vg_p = pin_position("voltage", 0, 80, 256) vg_n = pin_position("voltage", 1, 80, 256) # NMOS at (256, 64): D=(304,64), G=(256,144), S=(304,160) md = pin_position("nmos", 0, 256, 64) # drain mg = pin_position("nmos", 1, 256, 64) # gate ms = pin_position("nmos", 2, 256, 64) # source # Diode R180 at (320, 224): cathode at sw (auto-connects), anode at GND d_anode = pin_position("diode", 0, 320, 224, 180) # (304, 224) # L1 R270 at (288, 176): pinA=(304,160)=sw (auto-connects), pinB=(384,160)=output l1_b = pin_position("ind", 1, 288, 176, 270) # (384, 160) # Cout at (368, 160): pinA=(384,160)=output, pinB=(384,224) cout_b = pin_position("cap", 1, 368, 160) # (384, 224) # Rload at (448, 144): pinA=(464,160), pinB=(464,240) rl_a = pin_position("res", 0, 448, 144) # (464, 160) rl_b = pin_position("res", 1, 448, 144) # (464, 240) # === WIRING === sch.add_wire(vin_p[0], vin_p[1], md[0], md[1]) # Vin+ to drain # Gate drive: Vgate+ → route to gate sch.add_wire(vg_p[0], vg_p[1], 160, vg_p[1]) sch.add_wire(160, vg_p[1], 160, mg[1]) sch.add_wire(160, mg[1], mg[0], mg[1]) # Output to Rload sch.add_wire(l1_b[0], l1_b[1], rl_a[0], rl_a[1]) # === COMPONENTS === sch.add_component("voltage", "Vin", v_in, 80, 48) sch.add_component("voltage", "Vgate", pulse_val, 80, 256) sch.add_component("nmos", "M1", mosfet_model, 256, 64) sch.add_component("diode", "D1", diode_model, 320, 224, rotation=180) sch.add_component("ind", "L1", ind, 288, 176, rotation=270) sch.add_component("cap", "Cout", c_out, 368, 160) sch.add_component("res", "Rload", r_load, 448, 144) # === FLAGS === sch.add_ground(*vin_n) sch.add_ground(*vg_n) sch.add_ground(*d_anode) sch.add_ground(*cout_b) sch.add_ground(*rl_b) sch.add_net_label("sw", *ms) sch.add_net_label("out", *l1_b) sch.add_directive(f".tran {period * 200:.4g}", 80, 420) return sch def generate_ldo_regulator( r1: str = "10k", r2: str = "10k", pass_transistor: str = "IRF9540N", v_in: str = "8", v_ref: str = "2.5", ) -> AscSchematic: """Generate a simple LDO voltage regulator schematic. Topology:: Vin ──[PMOS source]──[PMOS drain]── out ──+ gate | | [R1] [Cout] [Rload] [Op-amp OUT] | | | + = Vref fb GND GND - = fb | [R2] | GND Vout = Vref * (1 + R1/R2) Args: r1: Top feedback resistor (out to fb) r2: Bottom feedback resistor (fb to GND) pass_transistor: PMOS model name v_in: Input voltage v_ref: Reference voltage """ sch = AscSchematic(sheet_w=1200, sheet_h=880) oa_sym = "OpAmps/UniversalOpamp2" # PMOS M1 at (400, 48): D=(448,48)=out, G=(400,128)=gate, S=(448,144)=vin m_d = pin_position("pmos", 0, 400, 48) # drain (448, 48) = output m_g = pin_position("pmos", 1, 400, 48) # gate (400, 128) m_s = pin_position("pmos", 2, 400, 48) # source (448, 144) = vin # Vin at (80, 48): pin+=(80,64), pin-=(80,144) vin_p = pin_position("voltage", 0, 80, 48) vin_n = pin_position("voltage", 1, 80, 48) # Vref at (80, 336): pin+=(80,352), pin-=(80,432) vref_p = pin_position("voltage", 0, 80, 336) vref_n = pin_position("voltage", 1, 80, 336) # Op-amp U1 at (304, 304): In+=(272,320), In-=(272,288), V+=(304,272), # V-=(304,336), OUT=(336,304) oa_inp = pin_position(oa_sym, 0, 304, 304) # (272, 320) = vref oa_inn = pin_position(oa_sym, 1, 304, 304) # (272, 288) = fb oa_vp = pin_position(oa_sym, 2, 304, 304) # (304, 272) = V+ oa_vn = pin_position(oa_sym, 3, 304, 304) # (304, 336) = V- oa_out = pin_position(oa_sym, 4, 304, 304) # (336, 304) = gate drive # R1 from output down to fb node. res at (560, 160): pinA=(576,176), pinB=(576,256) r1_a = pin_position("res", 0, 560, 160) # (576, 176) r1_b = pin_position("res", 1, 560, 160) # (576, 256) = fb # R2 from fb to GND. res at (560, 256): pinA=(576,272), pinB=(576,352) r2_a = pin_position("res", 0, 560, 256) # (576, 272) = fb r2_b = pin_position("res", 1, 560, 256) # (576, 352) = GND # Cout at (640, 48): pinA=(656,48)=out, pinB=(656,112)=GND cout_a = pin_position("cap", 0, 640, 48) # (656, 48) cout_b = pin_position("cap", 1, 640, 48) # (656, 112) # Rload at (720, 48): pinA=(736,64), pinB=(736,144) rl_a = pin_position("res", 0, 720, 48) # (736, 64) rl_b = pin_position("res", 1, 720, 48) # (736, 144) # === WIRING === # Vin to PMOS source: (80,64) right to (448,64), down to source (448,144) sch.add_wire(vin_p[0], vin_p[1], m_s[0], vin_p[1]) # horizontal sch.add_wire(m_s[0], vin_p[1], m_s[0], m_s[1]) # vertical to source # PMOS drain (output) right to R1, Cout, Rload # Drain at (448, 48). Wire right to Cout pinA (656, 48) sch.add_wire(m_d[0], m_d[1], cout_a[0], cout_a[1]) # Continue to Rload: (656,48) → (736,48), down to pinA (736,64) sch.add_wire(cout_a[0], cout_a[1], rl_a[0], cout_a[1]) sch.add_wire(rl_a[0], cout_a[1], rl_a[0], rl_a[1]) # R1 top: down from output rail. (576,48) → (576,176) sch.add_wire(m_d[0], m_d[1], r1_a[0], m_d[1]) # (448,48)→(576,48) sch.add_wire(r1_a[0], m_d[1], r1_a[0], r1_a[1]) # (576,48)→(576,176) # Op-amp output to PMOS gate sch.add_wire(oa_out[0], oa_out[1], m_g[0], oa_out[1]) # horizontal sch.add_wire(m_g[0], oa_out[1], m_g[0], m_g[1]) # vertical to gate # Vref to op-amp In+ sch.add_wire(vref_p[0], vref_p[1], vref_p[0], oa_inp[1]) sch.add_wire(vref_p[0], oa_inp[1], oa_inp[0], oa_inp[1]) # Feedback (fb) to op-amp In-: R1 bottom/R2 top junction at (576,256/272) # Wire from fb junction to In- (272, 288) sch.add_wire(r2_a[0], r2_a[1], r2_a[0], oa_inn[1]) # (576,272)→(576,288) sch.add_wire(r2_a[0], oa_inn[1], oa_inn[0], oa_inn[1]) # →(272,288) # Op-amp supply: V+ to Vin rail, V- to GND sch.add_wire(oa_vp[0], oa_vp[1], oa_vp[0], vin_p[1]) # V+ up to y=64 sch.add_wire(oa_vp[0], vin_p[1], vin_p[0], vin_p[1]) # left to Vin+ # === COMPONENTS === sch.add_component("pmos", "M1", pass_transistor, 400, 48) sch.add_component(oa_sym, "U1", "", 304, 304) sch.add_component("voltage", "Vin", v_in, 80, 48) sch.add_component("voltage", "Vref", v_ref, 80, 336) sch.add_component("res", "R1", r1, 560, 160) sch.add_component("res", "R2", r2, 560, 256) sch.add_component("cap", "Cout", "10u", 640, 48) sch.add_component("res", "Rload", "100", 720, 48) # === FLAGS === sch.add_ground(*vin_n) sch.add_ground(*vref_n) sch.add_ground(*r2_b) sch.add_ground(*cout_b) sch.add_ground(*rl_b) sch.add_ground(oa_vn[0], oa_vn[1]) sch.add_net_label("out", *m_d) sch.add_net_label("fb", *r1_b) sch.add_directive(".tran 10m", 80, 500) return sch def generate_h_bridge( v_supply: str = "12", r_load: str = "10", mosfet_model: str = "IRF540N", ) -> AscSchematic: """Generate an H-bridge motor driver schematic. Topology (two half-bridges with load between them):: Vcc ──┬── M1_D M2_D ──┬── Vcc M1(fwd) M2(rev) outA ─┤── M1_S M2_S ──├─ outB │ │ ├──── [Rload] ──────┤ │ │ outA ─┤── M3_D M4_D ──├─ outB M3(rev) M4(fwd) GND ──┴── M3_S M4_S ──┴── GND M1+M4 driven together (forward), M2+M3 driven together (reverse). Complementary PULSE gate drives with dead time. Args: v_supply: Supply voltage r_load: Load resistance mosfet_model: NMOS model name (used for all 4 switches) """ sch = AscSchematic(sheet_w=1200, sheet_h=880) # PULSE gate drive parameters period = "1m" t_on = "450u" t_dead = "25u" fwd_pulse = f"PULSE(0 {v_supply} {t_dead} 10n 10n {t_on} {period})" rev_pulse = f"PULSE(0 {v_supply} 525u 10n 10n {t_on} {period})" # --- Left column (A side) --- # M1 (high-A) at (320, 48): D=(368,48), G=(320,128), S=(368,144) m1_d = pin_position("nmos", 0, 320, 48) # (368, 48) = vcc m1_g = pin_position("nmos", 1, 320, 48) # (320, 128) = gate_fwd m1_s = pin_position("nmos", 2, 320, 48) # (368, 144) = outA # M3 (low-A) at (320, 240): D=(368,240), G=(320,320), S=(368,336) m3_d = pin_position("nmos", 0, 320, 240) # (368, 240) = outA m3_g = pin_position("nmos", 1, 320, 240) # (320, 320) = gate_rev m3_s = pin_position("nmos", 2, 320, 240) # (368, 336) = GND # --- Right column (B side) --- # M2 (high-B) at (560, 48): D=(608,48), G=(560,128), S=(608,144) m2_d = pin_position("nmos", 0, 560, 48) # (608, 48) = vcc m2_g = pin_position("nmos", 1, 560, 48) # (560, 128) = gate_rev m2_s = pin_position("nmos", 2, 560, 48) # (608, 144) = outB # M4 (low-B) at (560, 240): D=(608,240), G=(560,320), S=(608,336) m4_d = pin_position("nmos", 0, 560, 240) # (608, 240) = outB m4_g = pin_position("nmos", 1, 560, 240) # (560, 320) = gate_fwd m4_s = pin_position("nmos", 2, 560, 240) # (608, 336) = GND # Rload R90 between outA and outB at y=192 # R90 at (544, 176): pinA=(528,192), pinB=(448,192) rl_a = pin_position("res", 0, 544, 176, 90) # (528, 192) rl_b = pin_position("res", 1, 544, 176, 90) # (448, 192) # Sources # Vsupply at (80, 48): pin+=(80,64), pin-=(80,144) vs_p = pin_position("voltage", 0, 80, 48) vs_n = pin_position("voltage", 1, 80, 48) # Vg_fwd at (80, 272): pin+=(80,288), pin-=(80,368) vgf_p = pin_position("voltage", 0, 80, 272) vgf_n = pin_position("voltage", 1, 80, 272) # Vg_rev at (80, 432): pin+=(80,448), pin-=(80,528) vgr_p = pin_position("voltage", 0, 80, 432) vgr_n = pin_position("voltage", 1, 80, 432) # === WIRING === # Vcc rail: Vsupply+ to M1_D to M2_D sch.add_wire(vs_p[0], vs_p[1], vs_p[0], m1_d[1]) # (80,64)→(80,48) sch.add_wire(vs_p[0], m1_d[1], m1_d[0], m1_d[1]) # (80,48)→(368,48) sch.add_wire(m1_d[0], m1_d[1], m2_d[0], m2_d[1]) # (368,48)→(608,48) # Left column vertical: M1_S → outA junction → M3_D sch.add_wire(m1_s[0], m1_s[1], m1_s[0], 192) # (368,144)→(368,192) sch.add_wire(m1_s[0], 192, m3_d[0], m3_d[1]) # (368,192)→(368,240) # Right column vertical: M2_S → outB junction → M4_D sch.add_wire(m2_s[0], m2_s[1], m2_s[0], 192) # (608,144)→(608,192) sch.add_wire(m2_s[0], 192, m4_d[0], m4_d[1]) # (608,192)→(608,240) # Rload connections sch.add_wire(m1_s[0], 192, rl_b[0], rl_b[1]) # outA→Rload pinB sch.add_wire(rl_a[0], rl_a[1], m2_s[0], 192) # Rload pinA→outB # Gate connections via net labels (cleaner than long wires) # gate_fwd: M1_G, M4_G, Vg_fwd+ # gate_rev: M2_G, M3_G, Vg_rev+ # === COMPONENTS === sch.add_component("voltage", "Vsupply", v_supply, 80, 48) sch.add_component("voltage", "Vg_fwd", fwd_pulse, 80, 272) sch.add_component("voltage", "Vg_rev", rev_pulse, 80, 432) sch.add_component("nmos", "M1", mosfet_model, 320, 48) sch.add_component("nmos", "M2", mosfet_model, 560, 48) sch.add_component("nmos", "M3", mosfet_model, 320, 240) sch.add_component("nmos", "M4", mosfet_model, 560, 240) sch.add_component("res", "Rload", r_load, 544, 176, rotation=90) # === FLAGS === sch.add_ground(*vs_n) sch.add_ground(*vgf_n) sch.add_ground(*vgr_n) sch.add_ground(*m3_s) sch.add_ground(*m4_s) sch.add_net_label("gate_fwd", *m1_g) sch.add_net_label("gate_fwd", *m4_g) sch.add_net_label("gate_fwd", *vgf_p) sch.add_net_label("gate_rev", *m2_g) sch.add_net_label("gate_rev", *m3_g) sch.add_net_label("gate_rev", *vgr_p) sch.add_net_label("outA", *m1_s) sch.add_net_label("outB", *m2_s) sch.add_directive(".tran 5m", 80, 592) return sch def generate_sallen_key_lowpass( r1: str = "10k", r2: str = "10k", c1: str = "10n", c2: str = "10n", ) -> AscSchematic: """Generate a Sallen-Key lowpass filter schematic (unity gain). Topology:: V1 --[R1]-- n1 --[R2]-- n2 (In+) | --> U1 --> out [C1]--> out In- --> out (unity gain) | [C2] | GND The C1 connects n1 to out (feedback). C2 connects n2 to GND. f_c = 1 / (2*pi*sqrt(R1*R2*C1*C2)). Supply: +/-15V. Args: r1: First series resistor r2: Second series resistor c1: Feedback capacitor (n1 to out) c2: Shunt capacitor (n2 to GND) """ sch = AscSchematic(sheet_w=1200, sheet_h=880) oa_sym = "OpAmps/UniversalOpamp2" # Op-amp U1 at (576, 336) inp = pin_position(oa_sym, 0, 576, 336) # In+ = (544, 352) inn = pin_position(oa_sym, 1, 576, 336) # In- = (544, 320) vp = pin_position(oa_sym, 2, 576, 336) # V+ = (576, 304) vn = pin_position(oa_sym, 3, 576, 336) # V- = (576, 368) out = pin_position(oa_sym, 4, 576, 336) # OUT = (608, 336) # V1 at (80, 256): pin+=(80,272), pin-=(80,352) v1p = pin_position("voltage", 0, 80, 256) v1n = pin_position("voltage", 1, 80, 256) # R1 horizontal (R90) at origin (256, 256): pinA=(240,272), pinB=(160,272) r1_a = pin_position("res", 0, 256, 256, 90) # (240, 272) = n1 side r1_b = pin_position("res", 1, 256, 256, 90) # (160, 272) = input side # R2 horizontal (R90) at origin (416, 256): pinA=(400,272), pinB=(320,272) r2_a = pin_position("res", 0, 416, 256, 90) # (400, 272) = n2 side r2_b = pin_position("res", 1, 416, 256, 90) # (320, 272) = n1 side # C1 vertical: connects n1 (junction R1_a/R2_b at x=272-ish) to out # We route n1 down to C1, then C1 bottom goes right to out. # C1 at (304, 336): pinA=(320, 336), pinB=(320, 400) # Actually let's place C1 horizontal (R90) above the feedback path. # Better: C1 as a horizontal cap (R90) from n1 to out. # cap R90: pinA offset (16,0)-> (0,16), pinB offset (16,64)->(-64,16) # At origin (560, 192): pinA=(560,208), pinB=(496,208) c1_a = pin_position("cap", 0, 560, 192, 90) # (560, 208) = out side c1_b = pin_position("cap", 1, 560, 192, 90) # (496, 208) = n1 side # C2 vertical at (480, 352): pinA=(496,352), pinB=(496,416) c2_a = pin_position("cap", 0, 480, 352) # (496, 352) c2_b = pin_position("cap", 1, 480, 352) # (496, 416) # Supply: Vpos at (752, 176), Vneg at (752, 416) vpos_p = pin_position("voltage", 0, 752, 176) # (752, 192) vpos_n = pin_position("voltage", 1, 752, 176) # (752, 272) vneg_p = pin_position("voltage", 0, 752, 416) # (752, 432) vneg_n = pin_position("voltage", 1, 752, 416) # (752, 512) # === WIRING === # V1+ to R1 pinB sch.add_wire(*v1p, *r1_b) # R1 pinA to R2 pinB (n1 junction at roughly x=272..320) # n1 junction: R1_a at (240,272), R2_b at (320,272) -- wire between sch.add_wire(*r1_a, *r2_b) # R2 pinA to In+ : (400,272) down to (400,352) then right to (544,352) sch.add_wire(r2_a[0], r2_a[1], r2_a[0], inp[1]) sch.add_wire(r2_a[0], inp[1], inp[0], inp[1]) # n2 junction to C2: from R2_a/In+ junction at (400,352) left to C2 pinA (496,352) # Actually, n2 is at the In+ side. C2 from n2 to GND. # n2 is where R2 meets In+. Let's tap off at (496,352) for C2. sch.add_wire(r2_a[0], inp[1], c2_a[0], c2_a[1]) # C1 feedback: n1 to out via C1 # n1 is at the R1_a / R2_b junction. Wire from n1 up to C1 pinB. # n1 at y=272, C1 pinB at (496, 208) # Route from (240,272) up to (240,208) then right to (496,208) sch.add_wire(r1_a[0], r1_a[1], r1_a[0], c1_b[1]) sch.add_wire(r1_a[0], c1_b[1], c1_b[0], c1_b[1]) # C1 pinA to out: (560,208) down to out (608,336) sch.add_wire(c1_a[0], c1_a[1], out[0], c1_a[1]) sch.add_wire(out[0], c1_a[1], out[0], out[1]) # Unity gain feedback: out to In- # (608,336) up to (608,320) ... In- is at (544,320) # Route: out (608,336) left to (544,336) up... no. # Actually wire from In- (544,320) right and down to out (608,336). # Route: (544,320) right to (620,320), down to (620,336), left to (608,336) # Simpler: go below. (608,336) to (608,400), left to (544,400), up to (544,320) sch.add_wire(out[0], out[1], out[0], 400) sch.add_wire(out[0], 400, inn[0], 400) sch.add_wire(inn[0], 400, inn[0], inn[1]) # Supply wiring sch.add_wire(vp[0], vp[1], vp[0], vpos_p[1]) sch.add_wire(vp[0], vpos_p[1], vpos_p[0], vpos_p[1]) sch.add_wire(vn[0], vn[1], vn[0], vneg_n[1]) sch.add_wire(vn[0], vneg_n[1], vneg_n[0], vneg_n[1]) # === COMPONENTS === sch.add_component("voltage", "V1", "AC 1", 80, 256) sch.add_component(oa_sym, "U1", "", 576, 336) sch.add_component("res", "R1", r1, 256, 256, rotation=90) sch.add_component("res", "R2", r2, 416, 256, rotation=90) sch.add_component("cap", "C1", c1, 560, 192, rotation=90) sch.add_component("cap", "C2", c2, 480, 352) sch.add_component("voltage", "Vpos", "15", 752, 176) sch.add_component("voltage", "Vneg", "15", 752, 416) # === FLAGS === sch.add_ground(*v1n) sch.add_ground(*c2_b) sch.add_ground(*vpos_n) sch.add_ground(*vneg_p) sch.add_net_label("out", *out) sch.add_net_label("n1", r1_a[0], r1_a[1]) sch.add_net_label("n2", r2_a[0], r2_a[1]) sch.add_directive(".ac dec 100 1 1meg", 80, 560) return sch def generate_boost_converter( ind: str = "10u", c_out: str = "100u", r_load: str = "50", v_in: str = "5", duty_cycle: float = 0.5, freq: str = "100k", ) -> AscSchematic: """Generate a boost (step-up) converter schematic. Topology:: Vin --[L1]--> sw_node --[D1]--> out | | [MOSFET] [Cout] [Rload] D=sw | | G=gate GND GND S=GND Gate driven by PULSE at switching frequency and duty cycle. Vout_ideal = Vin / (1 - duty_cycle). Args: ind: Inductor value c_out: Output capacitor r_load: Load resistor v_in: Input voltage duty_cycle: Switching duty cycle (0.0-1.0) freq: Switching frequency """ sch = AscSchematic(sheet_w=1040, sheet_h=680) # Compute PULSE timing freq_hz = _parse_spice_value(freq) period = 1.0 / freq_hz t_on = period * duty_cycle t_rise = period * 0.01 t_fall = t_rise pulse_val = ( f"PULSE(0 {v_in} 0 {t_rise:.4g} {t_fall:.4g} {t_on:.4g} {period:.4g})" ) mosfet_model = "IRF540N" diode_model = "1N5819" # Vin at (80, 48): pin+=(80,64), pin-=(80,144) vin_p = pin_position("voltage", 0, 80, 48) vin_n = pin_position("voltage", 1, 80, 48) # Vgate at (80, 256): pin+=(80,272), pin-=(80,352) vg_p = pin_position("voltage", 0, 80, 256) vg_n = pin_position("voltage", 1, 80, 256) # L1 horizontal (R90) at (192, 48): # ind R90: pinA offset (16,16)->(-16,16), pinB offset (16,96)->(-96,16) # At (192, 48): pinA = (176, 64), pinB = (96, 64) # Actually re-derive: R90 transform: (px,py) -> (-py, px) # pinA raw = (16,16) -> R90 -> (-16, 16). origin(192,48) + (-16,16) = (176, 64) # pinB raw = (16,96) -> R90 -> (-96, 16). origin(192,48) + (-96,16) = (96, 64) # So pinB is on the Vin side, pinA on the sw side. Good. l1_a = pin_position("ind", 0, 192, 48, 90) # (176, 64) = sw side l1_b = pin_position("ind", 1, 192, 48, 90) # (96, 64) = vin side # NMOS at (224, 112): D=(272,112)=sw, G=(224,192), S=(272,208)=GND # Actually let's position so drain lines up with L1 pinA. # L1_a = (176, 64). NMOS drain should be at (176, something). # nmos D offset = (48, 0). So origin = (176-48, y) = (128, y). # Want drain at y ~ 112 range. Put origin at (128, 112): D=(176,112). # Then wire from L1_a (176,64) down to D (176,112). md = pin_position("nmos", 0, 128, 112) # drain (176, 112) mg = pin_position("nmos", 1, 128, 112) # gate (128, 192) ms = pin_position("nmos", 2, 128, 112) # source (176, 208) # Diode from sw_node to output. # sw_node is at L1_a (176,64). Diode horizontal. # diode R90: anode offset (16,0)->(0,16), cathode offset (16,64)->(-64,16) # At origin (320, 48): anode=(320,64), cathode=(256,64) # We want anode at sw side, cathode at out side. But R90 has cathode to left. # Use R270 instead: anode (16,0)->R270->(0,-16), cathode (16,64)->R270->(64,-16) # At origin (240, 80): anode=(240,64), cathode=(304,64) # Wire from sw (176,64) right to anode (240,64). Cathode (304,64) = output side. d_anode = pin_position("diode", 0, 240, 80, 270) # (240, 64) d_cathode = pin_position("diode", 1, 240, 80, 270) # (304, 64) # Wire from L1_a (176,64) to sw junction, then to diode anode (240,64) # And from diode cathode (304,64) to out # Cout at (384, 64): pinA=(400,64), pinB=(400,128) cout_a = pin_position("cap", 0, 384, 64) # (400, 64) cout_b = pin_position("cap", 1, 384, 64) # (400, 128) # Rload at (464, 48): pinA=(480,64), pinB=(480,144) rl_a = pin_position("res", 0, 464, 48) # (480, 64) rl_b = pin_position("res", 1, 464, 48) # (480, 144) # === WIRING === # Vin+ to L1 pinB sch.add_wire(*vin_p, *l1_b) # L1 pinA (sw) to MOSFET drain sch.add_wire(l1_a[0], l1_a[1], md[0], md[1]) # L1 pinA (sw) to diode anode sch.add_wire(l1_a[0], l1_a[1], d_anode[0], d_anode[1]) # Diode cathode to Cout and Rload (output rail) sch.add_wire(d_cathode[0], d_cathode[1], cout_a[0], cout_a[1]) sch.add_wire(cout_a[0], cout_a[1], rl_a[0], rl_a[1]) # Gate drive: Vgate+ to MOSFET gate sch.add_wire(vg_p[0], vg_p[1], mg[0], vg_p[1]) sch.add_wire(mg[0], vg_p[1], mg[0], mg[1]) # === COMPONENTS === sch.add_component("voltage", "Vin", v_in, 80, 48) sch.add_component("voltage", "Vgate", pulse_val, 80, 256) sch.add_component("ind", "L1", ind, 192, 48, rotation=90) sch.add_component("nmos", "M1", mosfet_model, 128, 112) sch.add_component("diode", "D1", diode_model, 240, 80, rotation=270) sch.add_component("cap", "Cout", c_out, 384, 64) sch.add_component("res", "Rload", r_load, 464, 48) # === FLAGS === sch.add_ground(*vin_n) sch.add_ground(*vg_n) sch.add_ground(*ms) sch.add_ground(*cout_b) sch.add_ground(*rl_b) sch.add_net_label("sw", l1_a[0], l1_a[1]) sch.add_net_label("out", d_cathode[0], d_cathode[1]) sch.add_directive(f".tran {period * 200:.4g}", 80, 420) return sch def generate_instrumentation_amp( r1: str = "10k", r2: str = "10k", r3: str = "10k", r_gain: str = "10k", ) -> AscSchematic: """Generate a 3-opamp instrumentation amplifier schematic. Stage 1 (input buffers with gain):: Vin+ --> In+(X1) --> out1 Vin- --> In+(X2) --> out2 R1 from out1 to In-(X1), R1b from out2 to In-(X2) Rgain between In-(X1) and In-(X2) Stage 2 (difference amp):: out1 --[R2]--> In-(X3) --[R3]--> Vout out2 --[R2b]--> In+(X3) In+(X3) --[R3b]--> GND Gain = (1 + 2*R1/Rgain) * (R3/R2). Supply: +/-15V. Args: r1: Stage 1 feedback resistor r2: Stage 2 input resistor r3: Stage 2 feedback resistor r_gain: Gain-setting resistor between X1 and X2 inverting inputs """ sch = AscSchematic(sheet_w=1600, sheet_h=1040) oa_sym = "OpAmps/UniversalOpamp2" # === STAGE 1 === # X1 at (480, 208) x1_inp = pin_position(oa_sym, 0, 480, 208) # In+ = (448, 224) x1_inn = pin_position(oa_sym, 1, 480, 208) # In- = (448, 192) x1_vp = pin_position(oa_sym, 2, 480, 208) # V+ = (480, 176) x1_vn = pin_position(oa_sym, 3, 480, 208) # V- = (480, 240) x1_out = pin_position(oa_sym, 4, 480, 208) # OUT = (512, 208) # X2 at (480, 480) x2_inp = pin_position(oa_sym, 0, 480, 480) # In+ = (448, 496) x2_inn = pin_position(oa_sym, 1, 480, 480) # In- = (448, 464) x2_vp = pin_position(oa_sym, 2, 480, 480) # V+ = (480, 448) x2_vn = pin_position(oa_sym, 3, 480, 480) # V- = (480, 512) x2_out = pin_position(oa_sym, 4, 480, 480) # OUT = (512, 480) # R1: feedback from X1 out to X1 In- (horizontal R90) # Place at (576, 96): pinA=(560,112), pinB=(480,112) # We want to connect out1 (512,208) up to (512,112) left to (560,112)=R1_a # and R1_b (480,112) left to (448,112) down to In- (448,192) r1_a = pin_position("res", 0, 576, 96, 90) # (560, 112) r1_b = pin_position("res", 1, 576, 96, 90) # (480, 112) # R1b: feedback from X2 out to X2 In- (horizontal R90) # Place at (576, 544): pinA=(560,560), pinB=(480,560) r1b_a = pin_position("res", 0, 576, 544, 90) # (560, 560) r1b_b = pin_position("res", 1, 576, 544, 90) # (480, 560) # Rgain: vertical between X1 In- junction and X2 In- junction # X1 In- at y=192 area, X2 In- at y=464 area # We'll connect to the R1/R1b feedback junctions. # Place Rgain at (400, 208): pinA=(416,224), pinB=(416,304) # Actually we need it to span from node_a (X1 In- junction) to node_b (X2 In- junction). # The junctions are at the R1_b / R1b_b x positions. # Let's use net labels for cleanliness: "node_a" at R1_b junction, "node_b" at R1b_b junction. # Rgain vertical res at (336, 256): pinA=(352, 272), pinB=(352, 352) rg_a = pin_position("res", 0, 336, 256) # (352, 272) = node_a side rg_b = pin_position("res", 1, 336, 256) # (352, 352) = node_b side # === STAGE 2 === # X3 at (880, 352) x3_inp = pin_position(oa_sym, 0, 880, 352) # In+ = (848, 368) x3_inn = pin_position(oa_sym, 1, 880, 352) # In- = (848, 336) x3_vp = pin_position(oa_sym, 2, 880, 352) # V+ = (880, 320) x3_vn = pin_position(oa_sym, 3, 880, 352) # V- = (880, 384) x3_out = pin_position(oa_sym, 4, 880, 352) # OUT = (912, 352) # R2: out1 to X3 In- (horizontal R90) # Place at (768, 320): pinA=(752,336), pinB=(672,336) r2_a = pin_position("res", 0, 768, 320, 90) # (752, 336) near X3 In- r2_b = pin_position("res", 1, 768, 320, 90) # (672, 336) from out1 # R3: X3 In- to Vout (horizontal R90, above X3 for feedback) # Place at (960, 240): pinA=(944, 256), pinB=(864, 256) r3_a = pin_position("res", 0, 960, 240, 90) # (944, 256) near Vout r3_b = pin_position("res", 1, 960, 240, 90) # (864, 256) near In- # R2b: out2 to X3 In+ (horizontal R90) # Place at (768, 352): pinA=(752,368), pinB=(672,368) r2b_a = pin_position("res", 0, 768, 352, 90) # (752, 368) near X3 In+ r2b_b = pin_position("res", 1, 768, 352, 90) # (672, 368) from out2 # R3b: X3 In+ to GND (vertical) # Place at (832, 416): pinA=(848,432), pinB=(848,512) r3b_a = pin_position("res", 0, 832, 416) # (848, 432) r3b_b = pin_position("res", 1, 832, 416) # (848, 512) # Sources # V1 at (80, 160): pin+=(80,176), pin-=(80,256) v1p = pin_position("voltage", 0, 80, 160) v1n = pin_position("voltage", 1, 80, 160) # V2 at (80, 432): pin+=(80,448), pin-=(80,528) v2p = pin_position("voltage", 0, 80, 432) v2n = pin_position("voltage", 1, 80, 432) # Supply: Vpos at (1056, 176), Vneg at (1056, 480) vpos_p = pin_position("voltage", 0, 1056, 176) vpos_n = pin_position("voltage", 1, 1056, 176) vneg_p = pin_position("voltage", 0, 1056, 480) vneg_n = pin_position("voltage", 1, 1056, 480) # === WIRING === # V1+ to X1 In+ sch.add_wire(v1p[0], v1p[1], v1p[0], x1_inp[1]) sch.add_wire(v1p[0], x1_inp[1], x1_inp[0], x1_inp[1]) # V2+ to X2 In+ sch.add_wire(v2p[0], v2p[1], v2p[0], x2_inp[1]) sch.add_wire(v2p[0], x2_inp[1], x2_inp[0], x2_inp[1]) # R1 feedback: X1 out (512,208) up to (512,112), right to R1_a (560,112) sch.add_wire(x1_out[0], x1_out[1], x1_out[0], r1_a[1]) sch.add_wire(x1_out[0], r1_a[1], r1_a[0], r1_a[1]) # R1_b (480,112) down to X1 In- (448,192): (480,112) left to (448,112), down to (448,192) sch.add_wire(r1_b[0], r1_b[1], x1_inn[0], r1_b[1]) sch.add_wire(x1_inn[0], r1_b[1], x1_inn[0], x1_inn[1]) # R1b feedback: X2 out (512,480) down to (512,560), right to R1b_a (560,560) sch.add_wire(x2_out[0], x2_out[1], x2_out[0], r1b_a[1]) sch.add_wire(x2_out[0], r1b_a[1], r1b_a[0], r1b_a[1]) # R1b_b (480,560) down to X2 In- (448,464): (480,560) left to (448,560), up to (448,464) sch.add_wire(r1b_b[0], r1b_b[1], x2_inn[0], r1b_b[1]) sch.add_wire(x2_inn[0], r1b_b[1], x2_inn[0], x2_inn[1]) # Rgain: node_a (junction at X1 In-) to Rgain pinA, node_b (X2 In-) to Rgain pinB # Route from X1 In- junction (448,192) left to (352,192), down to Rg_a (352,272) sch.add_wire(x1_inn[0], x1_inn[1], rg_a[0], x1_inn[1]) sch.add_wire(rg_a[0], x1_inn[1], rg_a[0], rg_a[1]) # Route from X2 In- junction (448,464) left to (352,464), up to Rg_b (352,352) sch.add_wire(x2_inn[0], x2_inn[1], rg_b[0], x2_inn[1]) sch.add_wire(rg_b[0], x2_inn[1], rg_b[0], rg_b[1]) # Stage 2 wiring # out1 (512,208) to R2_b (672,336): route (512,208) right to (672,208), down to (672,336) sch.add_wire(x1_out[0], x1_out[1], r2_b[0], x1_out[1]) sch.add_wire(r2_b[0], x1_out[1], r2_b[0], r2_b[1]) # R2_a (752,336) to X3 In- (848,336) sch.add_wire(r2_a[0], r2_a[1], x3_inn[0], x3_inn[1]) # out2 (512,480) to R2b_b (672,368): route (512,480) right to (672,480), up to (672,368) sch.add_wire(x2_out[0], x2_out[1], r2b_b[0], x2_out[1]) sch.add_wire(r2b_b[0], x2_out[1], r2b_b[0], r2b_b[1]) # R2b_a (752,368) to X3 In+ (848,368) sch.add_wire(r2b_a[0], r2b_a[1], x3_inp[0], x3_inp[1]) # R3 feedback: X3 out (912,352) up to (912,256), left to R3_a (944,256) sch.add_wire(x3_out[0], x3_out[1], x3_out[0], r3_a[1]) sch.add_wire(x3_out[0], r3_a[1], r3_a[0], r3_a[1]) # R3_b (864,256) down to X3 In- junction: (864,256) left to (848,256), down to (848,336) sch.add_wire(r3_b[0], r3_b[1], x3_inn[0], r3_b[1]) sch.add_wire(x3_inn[0], r3_b[1], x3_inn[0], x3_inn[1]) # R3b: X3 In+ to GND via R3b sch.add_wire(x3_inp[0], x3_inp[1], r3b_a[0], r3b_a[1]) # Supply wiring (use net labels for cleanliness with 3 opamps) # Just wire X1, X2, X3 supply pins to net labels # === COMPONENTS === sch.add_component("voltage", "V1", "AC 1", 80, 160) sch.add_component("voltage", "V2", "0", 80, 432) sch.add_component(oa_sym, "X1", "", 480, 208) sch.add_component(oa_sym, "X2", "", 480, 480) sch.add_component(oa_sym, "X3", "", 880, 352) sch.add_component("res", "R1", r1, 576, 96, rotation=90) sch.add_component("res", "R1b", r1, 576, 544, rotation=90) sch.add_component("res", "Rgain", r_gain, 336, 256) sch.add_component("res", "R2", r2, 768, 320, rotation=90) sch.add_component("res", "R3", r3, 960, 240, rotation=90) sch.add_component("res", "R2b", r2, 768, 352, rotation=90) sch.add_component("res", "R3b", r3, 832, 416) sch.add_component("voltage", "Vpos", "15", 1056, 176) sch.add_component("voltage", "Vneg", "15", 1056, 480) # === FLAGS === sch.add_ground(*v1n) sch.add_ground(*v2n) sch.add_ground(*r3b_b) sch.add_ground(*vpos_n) sch.add_ground(*vneg_p) # Supply net labels for all three opamps sch.add_net_label("vdd", *x1_vp) sch.add_net_label("vdd", *x2_vp) sch.add_net_label("vdd", *x3_vp) sch.add_net_label("vdd", *vpos_p) sch.add_net_label("vss", *x1_vn) sch.add_net_label("vss", *x2_vn) sch.add_net_label("vss", *x3_vn) sch.add_net_label("vss", *vneg_n) sch.add_net_label("vout", *x3_out) sch.add_directive(".ac dec 100 1 1meg", 80, 640) return sch def generate_current_mirror( r_ref: str = "10k", r_load: str = "1k", vcc: str = "12", ) -> AscSchematic: """Generate a BJT current mirror schematic. Topology:: Vcc --[Rref]--> collector_Q1 = base_Q1 = base_Q2 emitter_Q1 = GND Vcc --[Rload]--> collector_Q2 emitter_Q2 = GND Q1 is diode-connected (collector tied to base). I_ref ~ (Vcc - Vbe) / Rref, I_load ~ I_ref. Args: r_ref: Reference resistor r_load: Load resistor vcc: Supply voltage """ sch = AscSchematic(sheet_w=880, sheet_h=680) bjt_model = "2N2222" # Q1 (NPN, diode-connected) at (256, 288): C=(320,288), B=(256,336), E=(320,384) q1c = pin_position("npn", 0, 256, 288) # collector (320, 288) q1b = pin_position("npn", 1, 256, 288) # base (256, 336) q1e = pin_position("npn", 2, 256, 288) # emitter (320, 384) # Q2 (NPN) at (448, 288): C=(512,288), B=(448,336), E=(512,384) q2c = pin_position("npn", 0, 448, 288) # collector (512, 288) q2b = pin_position("npn", 1, 448, 288) # base (448, 336) q2e = pin_position("npn", 2, 448, 288) # emitter (512, 384) # Rref from Vcc to Q1 collector. res at (304, 192): pinA=(320,208)=vcc, pinB=(320,288)=Q1c rref_a = pin_position("res", 0, 304, 192) # (320, 208) # Rload from Vcc to Q2 collector. res at (496, 192): pinA=(512,208)=vcc, pinB=(512,288)=Q2c rload_a = pin_position("res", 0, 496, 192) # (512, 208) # Vcc source at (80, 96): pin+=(80,112), pin-=(80,192) vcc_p = pin_position("voltage", 0, 80, 96) vcc_n = pin_position("voltage", 1, 80, 96) # Vcc rail at y=208 vcc_y = rref_a[1] # 208 # === WIRING === # Vcc rail sch.add_wire(vcc_p[0], vcc_p[1], 160, vcc_p[1]) # (80,112)->(160,112) sch.add_wire(160, vcc_p[1], 160, vcc_y) # (160,112)->(160,208) sch.add_wire(160, vcc_y, rload_a[0], vcc_y) # (160,208)->(512,208) spans both # Diode connection: Q1 collector to Q1 base # Q1c = (320,288), Q1b = (256,336) # Route: (320,288) left to (256,288) down to (256,336) sch.add_wire(q1c[0], q1c[1], q1b[0], q1c[1]) sch.add_wire(q1b[0], q1c[1], q1b[0], q1b[1]) # Base connection: Q1 base to Q2 base sch.add_wire(q1b[0], q1b[1], q2b[0], q2b[1]) # === COMPONENTS === sch.add_component("npn", "Q1", bjt_model, 256, 288) sch.add_component("npn", "Q2", bjt_model, 448, 288) sch.add_component("res", "Rref", r_ref, 304, 192) sch.add_component("res", "Rload", r_load, 496, 192) sch.add_component("voltage", "Vcc", vcc, 80, 96) # === FLAGS === sch.add_ground(*vcc_n) sch.add_ground(*q1e) sch.add_ground(*q2e) sch.add_net_label("mirror", *q1b) sch.add_net_label("out", *q2c) sch.add_directive(".op", 80, 448) sch.add_directive(".tran 1m", 80, 480) return sch def generate_transimpedance_amp( rf: str = "100k", cf: str = "1p", i_source: str = "1u", ) -> AscSchematic: """Generate a transimpedance amplifier (TIA) schematic. Topology:: I1 (current src, AC) --> In- (inv) --> U1 --> out GND --> In+ (noninv) Rf from In- to out (feedback) Cf from In- to out (parallel with Rf, for stability) Vout = -I_in * Rf (at low frequencies). Bandwidth limited by Cf. Supply: +/-15V. Args: rf: Feedback resistor cf: Feedback capacitor (for stability) i_source: AC current source magnitude """ sch = AscSchematic(sheet_w=1200, sheet_h=880) oa_sym = "OpAmps/UniversalOpamp2" # Op-amp U1 at (512, 336) inp = pin_position(oa_sym, 0, 512, 336) # In+ = (480, 352) inn = pin_position(oa_sym, 1, 512, 336) # In- = (480, 320) vp = pin_position(oa_sym, 2, 512, 336) # V+ = (512, 304) vn = pin_position(oa_sym, 3, 512, 336) # V- = (512, 368) out = pin_position(oa_sym, 4, 512, 336) # OUT = (544, 336) # Current source I1 at (320, 224) (vertical, like voltage source) # Using "voltage" pin positions since current source shares the same symbol geometry # I1 at origin (320, 224): pin+ = (320, 240), pin- = (320, 320) # Current flows from pin+ to pin- inside the source (conventional). # We want current into In- node: connect pin+ to In- node, pin- to GND. i1p = pin_position("voltage", 0, 320, 224) # (320, 240) i1n = pin_position("voltage", 1, 320, 224) # (320, 320) # Rf horizontal (R90) above op-amp for feedback # At (560, 208): pinA=(544,224), pinB=(464,224) rf_a = pin_position("res", 0, 560, 208, 90) # (544, 224) near out rf_b = pin_position("res", 1, 560, 208, 90) # (464, 224) near In- # Cf horizontal (R90) above Rf for parallel feedback # At (560, 160): pinA=(544,176), pinB=(464,176) # Actually cap R90: pinA offset (16,0)->(0,16), pinB offset (16,64)->(-64,16) # At (560, 160): pinA=(560,176), pinB=(496,176) cf_a = pin_position("cap", 0, 560, 160, 90) # (560, 176) near out cf_b = pin_position("cap", 1, 560, 160, 90) # (496, 176) near In- # Supply: Vpos at (688, 176), Vneg at (688, 416) vpos_p = pin_position("voltage", 0, 688, 176) vpos_n = pin_position("voltage", 1, 688, 176) vneg_p = pin_position("voltage", 0, 688, 416) vneg_n = pin_position("voltage", 1, 688, 416) # === WIRING === # I1 pin+ (320,240) to In- node (480,320) # Route: (320,240) right to (480,240), down to (480,320) sch.add_wire(i1p[0], i1p[1], inn[0], i1p[1]) sch.add_wire(inn[0], i1p[1], inn[0], inn[1]) # Rf feedback: out up to Rf pinA, Rf pinB left and down to In- junction sch.add_wire(out[0], out[1], rf_a[0], rf_a[1]) # (544,336)->(544,224) sch.add_wire(rf_b[0], rf_b[1], rf_b[0], inn[1]) # (464,224)->(464,320) sch.add_wire(rf_b[0], inn[1], inn[0], inn[1]) # (464,320)->(480,320) # Cf feedback: parallel path # Connect Cf pinA to Rf pinA column, Cf pinB to Rf pinB column sch.add_wire(cf_a[0], cf_a[1], rf_a[0], cf_a[1]) # (560,176)->(544,176) sch.add_wire(rf_a[0], cf_a[1], rf_a[0], rf_a[1]) # (544,176)->(544,224) vertical sch.add_wire(cf_b[0], cf_b[1], rf_b[0], cf_b[1]) # (496,176)->(464,176) sch.add_wire(rf_b[0], cf_b[1], rf_b[0], rf_b[1]) # (464,176)->(464,224) vertical # Supply wiring sch.add_wire(vp[0], vp[1], vp[0], vpos_p[1]) sch.add_wire(vp[0], vpos_p[1], vpos_p[0], vpos_p[1]) sch.add_wire(vn[0], vn[1], vn[0], vneg_n[1]) sch.add_wire(vn[0], vneg_n[1], vneg_n[0], vneg_n[1]) # === COMPONENTS === sch.add_component("voltage", "I1", f"AC {i_source}", 320, 224) sch.add_component(oa_sym, "U1", "", 512, 336) sch.add_component("res", "Rf", rf, 560, 208, rotation=90) sch.add_component("cap", "Cf", cf, 560, 160, rotation=90) sch.add_component("voltage", "Vpos", "15", 688, 176) sch.add_component("voltage", "Vneg", "15", 688, 416) # === FLAGS === sch.add_ground(*i1n) sch.add_ground(*inp) # In+ to GND sch.add_ground(*vpos_n) sch.add_ground(*vneg_p) sch.add_net_label("out", *out) sch.add_directive(".ac dec 100 1 1meg", 80, 560) return sch