Source code for circuitgenome.synthesizer.compatibility.output

"""
Output-cardinality compatibility filter for
:func:`~circuitgenome.synthesizer.synthesizer.enumerate_circuits`.

``load.in1``/``in2`` (the folding nodes fed by ``input_pair.out1``/``out2``)
and ``load.out``/``out1``/``out2`` (the load's actual output node(s)) are
wired to *separate* nets, and a topology template's ``output_type``
(``"single_ended"`` or ``"fully_differential"``) determines which of the
output-side ports get a net at all:

- ``load.in1``/``in2`` are always wired, in every topology.
- ``load.out1``/``out2`` are wired to ``net_loadout1``/``net_loadout2`` only
  in ``fully_differential`` topologies (sensed by ``cmfb``/``second_stage*``/
  ``comp*``); ``single_ended`` topologies never connect them.
- ``load.out``/``out2`` are wired to the stage's single output node only in
  ``single_ended`` topologies; ``fully_differential`` topologies never connect
  ``load.out``.

Two groups of ``load`` variants declare a *mandatory* port whose net
assignment depends on which of these rules applies:

- ``folded_cascode_load_*_input_single_output`` and
  ``telescopic_cascode_load_{pmos,nmos}`` declare ``out`` as
  ``role: output`` (mandatory) and ``out1``/``out2`` as ``role: optional``
  (unused -- those branches have no separate cascode-output device). In a
  ``fully_differential`` topology, ``out`` is never wired by
  ``slot_connections``, so the device terminal falls back to the
  internal-node naming and becomes a floating, disconnected node.
  -> ``output_cardinality: "single"``, compatible only with
  ``output_type: "single_ended"``.

- ``folded_cascode_load_*_input_differential_output`` declare ``out1``/
  ``out2`` as ``role: output`` (mandatory, distinct cascode-output nodes) and
  ``out`` as ``role: optional`` (unused). In a ``single_ended`` topology,
  ``out1``/``out2`` are never wired by ``slot_connections`` (no
  ``net_loadout1``/``net_loadout2`` net is defined there), so the cascode
  device whose drain is ``out1`` (or ``out2``) becomes a floating,
  disconnected node. -> ``output_cardinality: "differential"``, compatible
  only with ``output_type: "fully_differential"``.

``current_source_load_{pmos,nmos}`` carry the ``"differential"`` tag for an
*electrical* reason instead of a port-wiring one: their branch devices are
plain current sources gated by ``bias_cmfb``, so the load only has a defined
operating point when the CMFB loop drives that gate -- which only
``fully_differential`` topologies provide (issue #112; the untapped-branch
filter, :mod:`~circuitgenome.synthesizer.compatibility.load_branch`, would
independently reject them in ``single_ended`` topologies).

The other 6 ``load`` variants (resistor/active/current-source loads) declare
``out1``/``out2`` as ``alias_of: in1``/``in2`` and ``out`` as ``optional`` --
their devices reference only ``in1``/``in2``, and a net-merge pass (see
:mod:`~circuitgenome.synthesizer.net_aliasing`) collapses ``out1``/``out2``'s
assigned net back onto ``in1``/``in2``'s after assembly, regardless of
``output_type``. The 4 resistor/active loads are therefore untagged
(``output_cardinality`` is ``None``) and compatible with every topology.

To extend: tag new or edited ``load`` variants with
``output_cardinality: "single"`` / ``"differential"`` in
``opamp_modules.yaml`` -- no code changes needed here.
"""
from __future__ import annotations
from ..models import ModuleVariant, TopologyTemplate

_CARDINALITY_OUTPUT_TYPE = {
    "single": "single_ended",
    "differential": "fully_differential",
}


[docs] def is_output_type_compatible( topology: TopologyTemplate, variant_map: dict[str, ModuleVariant], ) -> bool: """Return ``False`` if ``load``'s ``output_cardinality`` (if set) doesn't match *topology*'s ``output_type``. ``load`` is the only slot with an ``output_cardinality`` tag; a load with no tag (``None``) is compatible with every topology. """ load = variant_map["load"] if load.output_cardinality is None: return True return topology.config.get("output_type") == _CARDINALITY_OUTPUT_TYPE[load.output_cardinality]