Tail-Current Compatibility

tail_current-slot compatibility filter and pruning for enumerate_circuits().

Every topology has a tail_current slot, wired input_pair.tail -> net_tail <- tail_current.out. But only the 4 differential_pair_* input_pair variants actually reference their tail port from a device terminal (s/b: tail on the tail transistor, or t2: tail on the degenerated variants’ tail resistor). The fifth input_pair variant, inverter_based_input – two back-to-back CMOS inverters – is self-biased by design and never references tail, so for that variant net_tail is a floating, single-terminal node and tail_current is synthesized dead.

Without a filter, all 6 tail_current variants would be enumerated for every combination, but for inverter_based_input the choice between them makes zero difference to the assembled circuit – pure combinatorial duplication, plus dead tail_current_* devices and an unnecessarily “needed” bias rail 7 (tail_current.bias).

is_tail_current_compatible() collapses this duplication: for an input_pair that doesn’t reference tail, only the canonical CANONICAL_TAIL_CURRENT_VARIANT is allowed through. prune_tail_current() then empties that variant’s ports/devices for those combinations, so it contributes no devices and tail_current.bias is no longer “needed” (see required_rail_kinds()).

Both the filter and the prune are required together, exactly as with is_cmfb_compatible()/ prune_cmfb(): itertools.product enumerates all 6 tail_current variants for every combination before pruning runs. If prune_tail_current() ran unconditionally with no filter, all 6 choices for an inverter_based_input combination would each be pruned to the same empty tail_current_absent placeholder – and since pruning happens before the circuit name is computed, that would yield 6 SynthesizedCircuits with identical devices and identical names for what should be one circuit. is_tail_current_compatible() prevents this by rejecting all but CANONICAL_TAIL_CURRENT_VARIANT up front, so prune_tail_current() only ever empties that one canonical choice – 1 circuit per (inverter_based_input, load, bias_gen, ...) combination, not 6.

To extend: tag a new or edited input_pair variant’s tail-side device terminal(s) with tail to make it a genuine tail_current consumer – no code changes needed here. A new self-biased, no-tail input_pair variant is automatically treated like inverter_based_input.

circuitgenome.synthesizer.compatibility.tail_current.is_tail_current_compatible(variant_map)[source]

Return False if tail_current’s variant choice is irrelevant for this combination.

If input_pair references its tail port (all 4 differential_pair_* variants), every tail_current variant supplies a real bias current and remains distinct. For inverter_based_input – which is self-biased and never references tailtail_current.out drives nothing, so only CANONICAL_TAIL_CURRENT_VARIANT is allowed through – the other 5 variants would otherwise be enumerated as duplicate no-op circuits.

Parameters:

variant_map (dict[str, ModuleVariant])

Return type:

bool

circuitgenome.synthesizer.compatibility.tail_current.prune_tail_current(variant, input_pair)[source]

Return an empty placeholder if input_pair doesn’t consume tail_current.out.

If input_pair references its tail port, variant is returned unchanged. Otherwise, returns a copy of variant with no ports and no devices – it contributes nothing to the assembled circuit, net_tail is no longer left floating, and tail_current.bias is no longer “needed” by required_rail_kinds().

Parameters:
Return type:

ModuleVariant