Source code for sycan.components.blocks.opamp

"""Op-amp models packaged as :class:`SubCircuit` instances.

Two flavours are offered:

* :class:`OPAMP` — ideal single-VCVS (infinite bandwidth, zero output
  impedance).
* :class:`OPAMP1` — first-order model with finite gain-bandwidth
  product and optional output impedance.

Higher-fidelity op-amp models (slew limit, input offset, etc.) can be
added as additional ``SubCircuit`` subclasses.
"""
from __future__ import annotations

from typing import Optional

from sycan import cas as cas

from sycan.mna import Value
from sycan.components.blocks.subcircuit import SubCircuit


def _make_opamp_body(
    A: cas.Expr,
    GBW: Optional[cas.Expr] = None,
    Z_out: Optional[cas.Expr] = None,
) -> "Circuit":
    """Build the internal circuit for a first-order op-amp model.

    Parameters
    ----------
    A
        DC open-loop voltage gain.
    GBW
        Gain-bandwidth product (Hz). If ``None`` the op-amp is ideal
        (infinite bandwidth, a single VCVS with gain ``A``).
    Z_out
        Output impedance (Ω). If ``None`` or 0 the output is driven
        directly.
    """
    from sycan.circuit import Circuit

    body = Circuit(name="OPAMP")

    if GBW is not None and GBW != cas.oo:
        # First-order: H(s) = A * ω_p / (s + ω_p) with ω_p = 2π·GBW / A.
        # DC gain = A, unity-gain freq = GBW.
        var = cas.Symbol("s")
        omega_p = 2 * cas.pi * GBW / A
        H = A * omega_p / (var + omega_p)
        body.add_transfer_function("df", "in_p", "in_n", "out_int", "0",
                                  H=H, var=var)
    else:
        body.add_vcvs("E1", "out_int", "0", "in_p", "in_n", A)

    if Z_out is not None and Z_out != 0:
        body.add_resistor("Rout", "out_int", "out", Z_out)
    else:
        body.add_vcvs("Eout", "out", "0", "out_int", "0", 1)

    return body


[docs] class OPAMP(SubCircuit): """Ideal single-VCVS op-amp. Parameters ---------- name Instance designator (e.g. ``"X1"``). in_p, in_n, out Parent-scope nodes wired to the non-inverting input, inverting input, and output pin respectively. A Open-loop voltage gain. Defaults to a per-instance symbol ``A_<name>`` so that ``A -> oo`` limits recover the ideal closed-loop behaviour. """ def __init__( self, name: str, in_p: str, in_n: str, out: str, A: Optional[cas.Expr] = None, ) -> None: if A is None: A = cas.Symbol(f"A_{name}") else: A = cas.sympify(A) body = _make_opamp_body(A=A, GBW=None, Z_out=None) super().__init__( name=name, body=body, port_map={"in_p": in_p, "in_n": in_n, "out": out}, ) self.A = A
[docs] class OPAMP1(SubCircuit): """First-order op-amp with finite gain-bandwidth product and output Z. Builds a single-pole dominant-pole model:: H(s) = A · ω_p / (s + ω_p) with ω_p = 2π·GBW / A followed by an optional series output impedance ``Z_out``. Parameters ---------- name Instance designator. in_p, in_n, out Parent-scope nodes. A DC open-loop gain (defaults to ``A_<name>``). GBW Gain-bandwidth product in Hz (default ``None`` = ideal, infinite bandwidth). Z_out Output impedance in Ω (default ``None`` = 0). """ def __init__( self, name: str, in_p: str, in_n: str, out: str, A: Optional[Value] = None, GBW: Optional[Value] = None, Z_out: Optional[Value] = None, ) -> None: if A is None: A = cas.Symbol(f"A_{name}") else: A = cas.sympify(A) if GBW is not None: GBW = cas.sympify(GBW) if Z_out is not None: Z_out = cas.sympify(Z_out) body = _make_opamp_body(A=A, GBW=GBW, Z_out=Z_out) super().__init__( name=name, body=body, port_map={"in_p": in_p, "in_n": in_n, "out": out}, ) self.A = A self.GBW = GBW self.Z_out = Z_out