Compatibility Filters¶
enumerate_circuits() forms the
Cartesian product of every slot’s candidate variants. Most of those
combinations are not circuits worth emitting: some assemble into an
electrically non-functional netlist (a node with no DC path, a stage that
cannot be biased), and others are exact duplicates that differ only in a
variant this combination never references. A set of small, pure-function
compatibility filters rejects those combinations before they are assembled,
one slot-level rule per filter.
Example
A self-biased inverter_based_input pair ignores its tail port, so
all six tail_current variants would produce the identical netlist.
This page explains the electrical why behind each filter and links its API.
Each filter section links its module inline, and the Per-module API
reference section collects the automodule docs.
Two types of filter¶
Pure filters expose a single is_*_compatible predicate that returns
False for a combination that should be dropped. Assembly is skipped;
nothing else changes.
Filter + prune pairs handle a subtler case: a slot whose variant choice is
irrelevant for the rest of the combination (its output drives nothing, or
its port is never referenced). Enumerating all of that slot’s variants would
produce N identical circuits. The is_*_compatible filter collapses the
choice to a single canonical variant, and the paired prune_* transform then
empties that variant’s ports and devices so it contributes no dead devices and
stops “needing” its bias rail
(see required_rail_kinds()).
The CMFB and tail-current
filters are the two filter + prune pairs.
Example
A load that does not consume bias_cmfb leaves the cmfb slot
driving nothing, so the resistive_sense_cmfb and dda_cmfb choices
would emit the same circuit — the filter keeps the canonical one and the
prune empties it.
Two kinds of check¶
Structural checks inspect actual device-terminal references — what a variant’s transistors and resistors really connect to — and need no metadata. They classify new variants automatically: the stage-interface, compensation-parity, and untapped-load-branch filters are structural.
Tag-based checks read a declared field from opamp_modules.yaml
(polarity, output_cardinality). Supporting a new variant is then a
one-line YAML tag, no code change: the polarity,
output-cardinality, and the load side of
the CMFB filter are tag-based.
The filters at a glance¶
Filter |
Shape |
Check |
Rejects / collapses |
|---|---|---|---|
filter |
tag |
Combinations whose |
|
filter |
structural |
Stage interfaces where the next stage’s required gate level falls outside the input pair’s reachable output window (unbiasable). |
|
filter |
structural |
Miller compensation wrapped around a non-inverting stage chain with gain (positive feedback, immeasurable AC response). |
|
filter |
tag |
|
|
filter |
structural |
Single-ended loads whose untapped branch node is left high-impedance between two series current sources (no DC definition). |
|
filter + prune |
tag |
The |
|
filter + prune |
structural |
The |
Where they run¶
The filters run in a fixed order inside
enumerate_circuits(), after the
variant product is formed and before the circuit is assembled: polarity →
stage-interface → compensation → output-cardinality → untapped-load-branch →
CMFB (filter, then prune) → tail-current (filter, then prune). The two prunes
must precede
construct_bias_generation()
so that emptied placeholders demand no bias rail. The synthesizer package
README and the Overview document the full
pipeline and the enumeration counts that these filters produce.
Polarity compatibility filter¶
This filter operates on the input_pair / load / tail_current
combination: a circuit only has a real DC current path if the three agree on
polarity. For example, differential_pair_nmos
draws current out of out1/out2 into the tail, so it needs a load
that sources current into out1/out2 from vdd and a
tail_current that sinks the tail node to gnd — pairing it with
active_load_nmos (which also sinks to gnd) or current_mirror_tail_pmos
(which also sources into the tail) leaves a node with no current path.
Each input_pair, load, and tail_current variant declares a
polarity field in opamp_modules.yaml: pmos_input, nmos_input,
or omitted for variants that work with either polarity
(inverter_based_input).
enumerate_circuits skips any combination where load’s or
tail_current’s polarity (if set) doesn’t match input_pair’s
(polarity).
Stage-interface compatibility filter¶
An amplification_stage variant is structurally unbiasable against the
first stage when the gate level its signal device (the transistor whose gate
is the in port) requires falls outside the input pair’s reachable output
window: an NMOS pair confines its output node to the upper part of the
supply range (its floor is the tail node, and vdd-referenced loads confine
it further), a PMOS pair mirrors that low — when the required level and
the window are disjoint, no sizing can establish the interface DC level
(mirror-type loads let the feedback loop drag the node to the boundary and
pin the pair in triode; range-limited loads rail outright).
The required level follows from the signal device’s source terminal: a
common-source stage (source on a supply) puts the gate one V_GS from that
supply, so it suits the opposite-polarity pair — an NMOS pair’s high output
suits a PMOS-gate CS stage, and vice versa.
enumerate_circuits therefore skips any combination where an
amplification_stage-category slot whose in net is one of the load’s
output nets (load.out/out1/out2) requires a pair type other than
the input_pair’s — that is the second (gm2) stage, the one wired directly
to the load output. The check is structural (which device gates in and
where its source sits — no YAML tags), so new amplification_stage variants
are classified automatically. The 3-stage templates’ third_stage slot
senses the second stage’s output instead — a wide-swing common-source node
that can meet either gate level — and is deliberately left unconstrained, as
are combinations using the untagged inverter_based_input (its output level
sits near mid-rail, reachable by either gate type). Source followers now live
in the output_stage category, wired after the gain stages (their in is
net_ampout, never a load-output net), so this filter does not apply to them
(second_stage).
Compensation parity filter¶
Every compensation variant couples its in port to its out port
through a capacitor (Miller family). Wired across a stage chain, that
coupling is negative feedback — pole splitting — only when the chain is
inverting; around a non-inverting chain with gain the same capacitor is
positive feedback, and the AC response develops a right-half-plane
character whose gain/GBW/PM cannot be measured (issue #114:
differential_ota_second_stage, two cascaded common-source stages,
measured PM 270–281°).
A chain’s parity is its number of common-source inversions — each
gate-to-drain hop inverts. enumerate_circuits skips only combinations
where a compensation slot wraps a chain whose total inversion count is a
positive even number. The check composes across slots: in the NMC
3-stage topologies comp1 wraps the second+third stage cascade, so two
common-source stages (non-inverting composite with gain) are rejected —
standard nested-Miller sign structure requires a non-inverting second
stage and an inverting output stage. The check is structural (device
terminal walks, no YAML tags), so new amplification_stage and
compensation variants are classified automatically; anything
unclassifiable imposes no constraint. Source followers (output_stage
slots in the buffered topologies) sit after the gain stages and outside the
compensation wrap, so they are not part of any chain this filter checks
(compensation).
Output-cardinality compatibility filter¶
Some load variants declare a mandatory output-side port that only one
output_type wires, so pairing them with the other kind of topology would
leave that port floating:
folded_cascode_load_*_input_single_outputandtelescopic_cascode_load_{pmos,nmos}declareoutas mandatory, which onlysingle_endedtopologies wire.folded_cascode_load_*_input_differential_outputdeclareout1/out2as mandatory, which onlyfully_differentialtopologies wire.
These loads carry an output_cardinality field in opamp_modules.yaml —
"single" or "differential" — and enumerate_circuits skips any
combination where it doesn’t match the topology’s output_type. Untagged
loads (output_cardinality: None, the resistor/active loads) work with
either output type. current_source_load_{pmos,nmos} also carry the
"differential" tag, for an electrical reason rather than 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)
(output).
Untapped-load-branch compatibility filter¶
In a single_ended topology only one of the first stage’s two branch nodes
is tapped (load.out/out2 → the stage-output net); the other
(load.in1/out1, net_diff1) is untapped — nothing outside the first
stage senses or drives it, so its DC voltage must be defined by the load
itself. current_source_load_* put a plain current source on that branch
(both gates on a single shared node, no diode connection), leaving the node
high-impedance between two series current sources — the load device on one
side, the input-pair half plus tail on the other. No sizing can absorb the
inevitable current mismatch, and one device always leaves saturation
(issue #112), so enumerate_circuits skips these combinations.
The check is structural (no YAML tags): the in1 node counts as DC-defined
when the load puts a diode-connected MOSFET on it (active_load_*), a
resistor touching it (resistor_load_*), or a MOSFET source terminal on it
(the cascode loads’ folding/cascode devices). Only the bare rail-referenced
current source fails all three.
fully_differential topologies tap both branches, so this filter does not
constrain them — defining the output common mode there is the CMFB loop’s job.
With the current module library the filter prunes nothing on its own:
current_source_load_* are already excluded from single-ended templates by
their output_cardinality tag, so it stands as the structural guard for any
future rail-gated load branch
(load_branch).
CMFB compatibility filter¶
fully_differential topologies have a cmfb slot, wired
cmfb.out -> net_cmfb_out -> load.bias_cmfb. Of the 14 load variants,
only the 4 tagged output_cardinality: "differential" declare
bias_cmfb as a real role: input consumer:
folded_cascode_load_*_input_differential_output (gating mn3/mn4
or mp1/mp2) and current_source_load_{pmos,nmos} (gating both
branch devices; issue #112). The other 8 declare it role: optional and
never reference it, so net_cmfb_out would drive nothing.
For a load whose output_cardinality isn’t "differential", only the
canonical resistive_sense_cmfb variant is allowed through – the
dda_cmfb choice would otherwise be enumerated as a duplicate no-op
circuit. That canonical variant is then pruned to an empty placeholder (no
ports, no devices), so it contributes no devices to the assembled circuit and
cmfb.bias is no longer counted as a needed bias rail. The
vcm_ref external port (statically present on every fully_differential
topology) is left unconnected for these circuits
(cmfb).
Tail-current compatibility filter¶
Every topology has a tail_current slot, wired input_pair.tail ->
net_tail <- tail_current.out. Of the 5 input_pair variants, only the 4
differential_pair_* variants reference their tail port from a device
terminal (s/b: tail on the tail transistor, or t2: tail on the
degenerated variants’ tail resistor). inverter_based_input – two
back-to-back CMOS inverters – is self-biased by design and never references
tail, so without this filter net_tail would be a floating,
single-terminal node and tail_current would drive nothing.
For an input_pair that doesn’t reference tail, only the canonical
current_mirror_tail_pmos variant is allowed through – the other 5
tail_current choices would otherwise be enumerated as duplicate no-op
circuits. That canonical variant is then pruned to an empty placeholder (no
ports, no devices), so it contributes no devices to the assembled circuit,
net_tail is no longer floating, and tail_current.bias is no longer
counted as a needed bias rail
(tail_current).
Example
inverter_based_input yields a single circuit per load instead of
six identical ones: whichever tail_current was chosen, the prune leaves
the same tail-less netlist.
Per-module API reference¶
Signatures and members for each filter module. The electrical rationale is in
the sections above; these pages are the API surface (import from the
compatibility subpackage, not the individual modules).