Source code for sycan.components.basic.vswitch

"""Voltage-controlled switch (SPICE ``S``).

A smooth resistive switch::

    R(V_c) = R_off + (R_on - R_off) · ½ · (1 + tanh((V_c - V_t) / V_h))

with ``V_c = V(nc_plus) - V(nc_minus)``.  ``V_t`` is the threshold
voltage and ``V_h`` is the half-width of the transition (smaller →
sharper switch, but harder for Newton's method to close on).

* **DC**: the conductance ``G(V_c) = 1/R(V_c)`` is stamped through
  ``stamp_nonlinear`` so V_c can come from the operating point.
* **AC**: linearised around an operating point ``V_c_op``.  Two terms
  drop out — a small-signal conductance between ``n_plus``/``n_minus``
  *and* a transconductance into ``nc_plus``/``nc_minus`` from the
  voltage drop across the switch.  When the switch is hard on or hard
  off, the cross term is negligible and the switch behaves like a
  static resistor.
"""
from __future__ import annotations

from dataclasses import dataclass, field
from typing import ClassVar, Optional

from sycan import cas as cas

from sycan.mna import Component, NoiseSpec, StampContext


[docs] @dataclass class VSwitch(Component): """Voltage-controlled smooth switch. Parameters ---------- name, n_plus, n_minus Switched terminals. nc_plus, nc_minus Control terminals. R_on, R_off Resistances in the closed and open states. V_t Control-voltage threshold (midpoint of the transition). V_h Transition half-width. V_c_op Operating-point control voltage used for AC linearisation. Defaults to a per-instance symbol ``V_c_op_<name>`` when ``None``. """ name: str n_plus: str n_minus: str nc_plus: str nc_minus: str R_on: cas.Expr = field(default=1) R_off: cas.Expr = field(default_factory=lambda: cas.Float("1e9")) V_t: cas.Expr = field(default=0) V_h: cas.Expr = field(default_factory=lambda: cas.Float("0.1")) V_c_op: Optional[cas.Expr] = field(default=None, kw_only=True) include_noise: NoiseSpec = field(default=None, kw_only=True) ports: ClassVar[tuple[str, ...]] = ( "n_plus", "n_minus", "nc_plus", "nc_minus" ) has_nonlinear: ClassVar[bool] = True SUPPORTED_NOISE: ClassVar[frozenset[str]] = frozenset() def __post_init__(self) -> None: self.R_on = cas.sympify(self.R_on) self.R_off = cas.sympify(self.R_off) self.V_t = cas.sympify(self.V_t) self.V_h = cas.sympify(self.V_h) if self.V_c_op is None: self.V_c_op = cas.Symbol(f"V_c_op_{self.name}") else: self.V_c_op = cas.sympify(self.V_c_op) self.include_noise = self._normalize_noise(self.include_noise) def _R_of(self, V_c: cas.Expr) -> cas.Expr: half_open = (1 + cas.tanh((V_c - self.V_t) / self.V_h)) / 2 return self.R_off + (self.R_on - self.R_off) * half_open
[docs] def stamp(self, ctx: StampContext) -> None: if ctx.mode != "ac": return # AC small-signal: g0 between (n_plus, n_minus) plus a control # transconductance gm = ∂I/∂V_c · sign that maps V_c to current # through the switch. I = (V+ - V-) / R(V_c). i, j = ctx.n(self.n_plus), ctx.n(self.n_minus) ci, cj = ctx.n(self.nc_plus), ctx.n(self.nc_minus) _vc = cas.Dummy("vc") R_sym = self._R_of(_vc) # Operating-point conductance. g0 = (1 / R_sym).subs(_vc, self.V_c_op) # Linear conductance term between switched terminals. if i >= 0: ctx.A[i, i] += g0 if j >= 0: ctx.A[j, j] += g0 if i >= 0 and j >= 0: ctx.A[i, j] -= g0 ctx.A[j, i] -= g0
# Cross-coupling: dG/dV_c times the operating-point voltage drop # across the switch is zero in the AC small-signal sense (the # operating-point V_DS is constant), but the small-signal control # voltage v_c modulates the conductance, producing a current # ∂(G · V_DS_op)/∂V_c · v_c in series with the switch path. # Without an explicit V_DS_op this contribution is zero — leave # it for users who supply it. (Most use cases drive the switch # hard on or hard off, where g0 dominates.)
[docs] def stamp_nonlinear(self, ctx: StampContext) -> None: if ctx.mode != "dc": return assert ctx.x is not None and ctx.residuals is not None i_idx = ctx.n(self.n_plus) j_idx = ctx.n(self.n_minus) ci_idx = ctx.n(self.nc_plus) cj_idx = ctx.n(self.nc_minus) V_i = ctx.x[i_idx] if i_idx >= 0 else cas.Integer(0) V_j = ctx.x[j_idx] if j_idx >= 0 else cas.Integer(0) V_ci = ctx.x[ci_idx] if ci_idx >= 0 else cas.Integer(0) V_cj = ctx.x[cj_idx] if cj_idx >= 0 else cas.Integer(0) V_c = V_ci - V_cj I = (V_i - V_j) / self._R_of(V_c) if i_idx >= 0: ctx.residuals[i_idx] += I if j_idx >= 0: ctx.residuals[j_idx] -= I