Source code for circuitgenome.synthesizer.compatibility.load_branch
"""
Untapped-load-branch compatibility filter for
:func:`~circuitgenome.synthesizer.synthesizer.enumerate_circuits`.
In every ``single_ended`` topology only one of the first stage's two branch
nodes is tapped: ``load.out``/``out2`` land on the stage-output net (inside
the feedback loop, or at least observed by the next stage), while
``load.in1``/``out1`` (``net_diff1``) is *untapped* -- nothing outside the
first stage senses or drives it. That node's DC voltage must therefore be
defined by the load itself. A load branch that is a plain, rail-referenced
current source (issue #112: ``current_source_load_*`` -- gate on a bias rail,
no diode connection) leaves the untapped node **high-impedance between two
series current sources**: the load device on one side, the input-pair half
plus tail on the other. Any mismatch between the two currents -- and there is
always some -- collapses the node until one device leaves saturation; no
sizing can fix it, because there is no mechanism (diode, resistor, CMFB, or
feedback) to absorb the difference. This is the textbook reason plain
current-source loads are only used with common-mode control.
The check is *structural* (actual device terminal references, no YAML tags,
same approach as :mod:`~circuitgenome.synthesizer.compatibility.second_stage`):
the untapped branch node (port ``in1``) counts as DC-defined when the load
contains
- a diode-connected MOSFET on it (``g == d == in1`` -- the mirror loads'
reference side, ``active_load_*``),
- a resistor touching it (``resistor_load_*``), or
- a MOSFET *source* terminal on it (the cascode loads' folding/cascode
devices: the device's gate rail pins the node one ``V_GS`` away).
Loads whose devices never put a MOSFET drain on ``in1`` are not constrained
(no current is forced into the node, so nothing needs to absorb a mismatch).
``fully_differential`` topologies tap both branches (``net_loadout1``/
``net_loadout2``) and are out of scope here -- there the common-mode
definition is the CMFB loop's job (see
:mod:`~circuitgenome.synthesizer.compatibility.cmfb`).
To extend: nothing to tag -- a new ``load`` variant is classified by what its
devices connect to ``in1``, and a new topology is covered by its
``output_type`` (``single_ended`` templates leave ``in1``'s net untapped by
the wiring convention documented in ``config/opamp_topologies.yaml``).
"""
from __future__ import annotations
from ..models import ModuleVariant, TopologyTemplate
#: The load port wired to the untapped branch node in ``single_ended``
#: topologies (``net_diff1``; ``out1`` aliases it on the non-cascode loads).
_UNTAPPED_BRANCH_PORT = "in1"
_MOS_TYPES = ("nmos", "pmos")
[docs]
def untapped_branch_is_dc_defined(load: ModuleVariant) -> bool:
"""Return ``True`` if *load* defines the DC voltage of its untapped
branch node (port ``in1``), or forces no current into it at all -- see
the module docstring for the electrical rationale.
"""
drain_on_node = False
for dev in load.devices:
terms = dev.terminals
if dev.type == "resistor":
if _UNTAPPED_BRANCH_PORT in (terms.get("t1"), terms.get("t2")):
return True
elif dev.type in _MOS_TYPES:
if terms.get("s") == _UNTAPPED_BRANCH_PORT:
return True # cascode/folding device rides the node
if terms.get("d") == _UNTAPPED_BRANCH_PORT:
if terms.get("g") == _UNTAPPED_BRANCH_PORT:
return True # diode-connected reference side
drain_on_node = True
return not drain_on_node
[docs]
def is_load_branch_compatible(
topology: TopologyTemplate,
variant_map: dict[str, ModuleVariant],
) -> bool:
"""Return ``False`` if *topology* leaves the ``load``'s ``in1`` branch
node untapped (``output_type: single_ended``) and the load doesn't
define its DC voltage (see the module docstring for the electrical
rationale).
"""
if topology.config.get("output_type") != "single_ended":
return True
return untapped_branch_is_dc_defined(variant_map["load"])