Source code for circuitgenome.sizer.gmid.intent

"""Design intent for the gm/Id sizer, as an explicit three-level hierarchy.

In the gm/Id methodology the transconductance efficiency ``gm/Id`` of a device is
a **design choice** that selects its inversion region (strong ~5–8 /V, moderate
~10–16 /V, weak ~18–25 /V).  This module makes the *intent* behind those choices
explicit and layered, so the sizing flow reads top-down:

    Circuit intent (spec)            ── SizingSpec: gain, GBW, PM, swing, power …
            │                           (the *what*; lives in shared.models)

    Functional-block intent          ── BlockIntent: per building block, the role,
            │                           gm/Id region, L multiple and the *rationale*.

    Transistor intent                ── TransistorIntent: the block intent resolved
                                        onto each device (role, gm/Id, L, block, why).

The middle layer is the tunable one and the extension point: each functional
building block carries its own gm/Id region and a human-readable reason, and a
caller (or a future optimizer) can override a single block's region while the
rest fall back to the defaults below.

A functional building block is *not* the same as an FBR slot: one slot can hold
devices of different roles (e.g. a gain stage's signal driver **and** its
current-source load both live in the ``second_stage`` slot).  So a block is keyed
by ``(slot, role)`` via :func:`functional_block`, matching the design-intent
taxonomy (input stage, gain stage, active load, tail source, …).

Design-intent table (defaults):

.. list-table::
   :header-rows: 1
   :widths: 22 18 60

   * - Building block
     - gm/Id (1/V)
     - Design intent
   * - Input stage
     - solved
     - Diff voltage → current: high gm, low noise, matching
   * - Gain stage
     - solved
     - Increase voltage gain while keeping stability
   * - Output stage
     - solved
     - Drive load capacitance and provide slew current
   * - Active / stage load
     - 10 (strong-ish)
     - Mirror current accurately with high output resistance
   * - Tail current source
     - 10 (strong-ish)
     - Set bias current with saturation headroom and high rout
   * - Bias generator
     - 10 (strong-ish)
     - Generate stable, low-sensitivity bias currents
   * - CMFB
     - 10 (strong-ish)
     - Regulate the output common-mode voltage
   * - Cascode
     - 8 (strong)
     - Increase output resistance with a small Vdsat

"solved" = gm/Id is not a free knob for signal devices; it is set to
``gm_required / Id`` to meet the spec (see :meth:`GmIdModel.geometry_for`).
"""
from __future__ import annotations

from dataclasses import dataclass, field

from ..shared.device_model import CASCODE, CURRENT_SOURCE, SIGNAL
from ..shared.taxonomy import is_signal_device

# --- gm/Id inversion regions used as the block defaults (1/V) ----------------
_MODERATE = 14.0        # signal nominal (pre-geometry estimate); real gm/Id is solved
_STRONG = 10.0          # current source: headroom + output resistance
_STRONG_CASCODE = 8.0   # cascode: small Vdsat to preserve stacked headroom

# --- channel length as a multiple of L_min -----------------------------------
_L_SIGNAL = 2.0         # signal: balance gain and ft
_L_CS = 4.0             # current source: longer L → higher output resistance
_L_CASCODE = 3.0        # cascode


[docs] @dataclass(frozen=True) class BlockIntent: """Design intent for one functional building block (Level 2). :param role: the sizing role — ``SIGNAL`` (gm solved from the spec), ``CURRENT_SOURCE`` or ``CASCODE``. :param gm_id: target gm/Id in 1/V, or ``None`` when it is *solved* from the gm requirement (all signal blocks). This is the one free knob a caller or optimizer overrides per block. :param l_mult: channel length as a multiple of ``length.min``. :param rationale: why this region/length — surfaced in the sizing result so the choice is explainable, not implicit. """ role: str gm_id: float | None l_mult: float rationale: str
[docs] @dataclass(frozen=True) class TransistorIntent: """A block intent resolved onto a single device (Level 3). The per-device design intent that drives :meth:`GmIdModel.geometry_for`: the resolved ``role``/``gm_id``/``l_mult`` plus the functional ``block`` it came from and the ``rationale`` for reporting. """ ref: str block: str role: str gm_id: float | None l_mult: float rationale: str
# --- the default per-building-block intent registry -------------------------- DEFAULT_BLOCK_INTENTS: dict[str, BlockIntent] = { "input_stage": BlockIntent( SIGNAL, None, _L_SIGNAL, "Convert the differential input voltage to current with high gm, low " "noise and good matching. gm/Id is solved to meet GBW/gain (moderate " "inversion); a small L multiple balances gain and ft."), "gain_stage": BlockIntent( SIGNAL, None, _L_SIGNAL, "Increase voltage gain while maintaining stability. gm/Id is solved " "from the required gm; L favours a gain/ft balance."), "output_stage": BlockIntent( SIGNAL, None, _L_SIGNAL, "Drive the load capacitance and provide slew current. gm/Id is solved " "from the required gm."), "active_load": BlockIntent( CURRENT_SOURCE, _STRONG, _L_CS, "First-stage active current-mirror load: replicate current accurately " "with high output resistance. Strong-ish inversion (low gm/Id) for " "headroom and rout; a long L raises rout."), "stage_load": BlockIntent( CURRENT_SOURCE, _STRONG, _L_CS, "Current-source load of a gain/output stage: high output resistance " "for gain. Strong-ish inversion, long L."), "tail_current": BlockIntent( CURRENT_SOURCE, _STRONG, _L_CS, "Tail current source: set the input-pair bias current with adequate " "saturation headroom and high rout. A low gm/Id (strong inversion) " "maximises the headroom."), "bias_generator": BlockIntent( CURRENT_SOURCE, _STRONG, _L_CS, "Generate stable, low-sensitivity reference currents for the mirrors it " "drives. Strong inversion, long L to match those mirrors."), "cmfb": BlockIntent( CURRENT_SOURCE, _STRONG, _L_CS, "Common-mode feedback devices: regulate the output common-mode voltage. " "(Resistive-sense averaging is set separately by cmfb_sense_r.)"), "cascode": BlockIntent( CASCODE, _STRONG_CASCODE, _L_CASCODE, "Increase output resistance while preserving the stacked headroom. " "Strong inversion for a small Vdsat; a moderate L."), "current_source": BlockIntent( CURRENT_SOURCE, _STRONG, _L_CS, "Generic current source: accurate current with high output resistance. " "Strong-ish inversion, long L."), } # (slot, role) → functional-block name. Signal devices split by stage; every # other non-cascode device is a current source of some kind. _SIGNAL_BLOCK = { "input_pair": "input_stage", "second_stage": "gain_stage", "second_stage_p": "gain_stage", "second_stage_n": "gain_stage", "third_stage": "output_stage", "third_stage_p": "output_stage", "third_stage_n": "output_stage", # Source-follower output-buffer slots (the *_buffered_* topologies). "output_stage": "output_stage", "output_stage_p": "output_stage", "output_stage_n": "output_stage", } _CS_BLOCK = { "load": "active_load", "tail_current": "tail_current", "bias_gen": "bias_generator", "cmfb": "cmfb", "second_stage": "stage_load", "second_stage_p": "stage_load", "second_stage_n": "stage_load", "third_stage": "stage_load", "third_stage_p": "stage_load", "third_stage_n": "stage_load", "output_stage": "stage_load", "output_stage_p": "stage_load", "output_stage_n": "stage_load", }
[docs] def functional_block(slot: str, is_signal: bool, is_cascode: bool) -> str: """Functional-building-block name for a device in ``slot`` with its role. Signal takes precedence (a cascode is never a signal device), matching the role assignment the sizer used before this registry existed. """ if is_signal: return _SIGNAL_BLOCK.get(slot, "gain_stage") if is_cascode: return "cascode" return _CS_BLOCK.get(slot, "current_source")
[docs] def resolve_transistor_intents( all_transistors: dict[str, tuple], # ref → (Device, slot_name) cascode_refs: set[str], block_intents: dict[str, BlockIntent] = DEFAULT_BLOCK_INTENTS, ) -> dict[str, TransistorIntent]: """Resolve the block registry onto every device (Level 2 → Level 3).""" out: dict[str, TransistorIntent] = {} for ref, (device, slot) in all_transistors.items(): block = functional_block(slot, is_signal_device(device), ref in cascode_refs) bi = block_intents[block] out[ref] = TransistorIntent(ref=ref, block=block, role=bi.role, gm_id=bi.gm_id, l_mult=bi.l_mult, rationale=bi.rationale) return out
[docs] @dataclass(frozen=True) class GmIdIntent: """Circuit-wide gm/Id knobs that are *not* per building block. The per-block gm/Id and L choices live in ``block_intents`` (defaulting to :data:`DEFAULT_BLOCK_INTENTS`); the role-level fields here remain the fallback the device model uses for its pre-geometry ``gds`` estimate (via ``GmIdPolicy``), and the two resistor knobs configure the resistor network the block registry does not cover. To retune a single building block, pass a copy of ``block_intents`` with that entry replaced. :param block_intents: per-functional-block design intent (Level 2). :param signal_gm_id: nominal signal gm/Id for the pre-geometry gds estimate. :param current_source_gm_id: fallback current-source gm/Id. :param cascode_gm_id: fallback cascode gm/Id. :param signal_l_mult / current_source_l_mult / cascode_l_mult: fallback L multiples per role. :param degeneration_factor: ``gm1·R`` for a source-degenerated input pair — sets each degeneration resistor to ``factor/gm1`` and the effective input transconductance to ``gm1/(1+factor)`` (0 = no degeneration). :param cmfb_sense_r: resistance (Ω) of each resistive-sense CMFB averager resistor — large so it doesn't load the differential output. """ block_intents: dict[str, BlockIntent] = field( default_factory=lambda: dict(DEFAULT_BLOCK_INTENTS)) signal_gm_id: float = _MODERATE current_source_gm_id: float = _STRONG cascode_gm_id: float = _STRONG_CASCODE signal_l_mult: float = _L_SIGNAL current_source_l_mult: float = _L_CS cascode_l_mult: float = _L_CASCODE degeneration_factor: float = 0.5 # gm1·R for a degenerated input pair (0 = none) cmfb_sense_r: float = 1.0e6 # CMFB resistive-sense averager R (Ω, large)
DEFAULT_INTENT = GmIdIntent()