Synthesizer

Topology synthesis engine.

Enumerates every valid combination of module variants for a given topology template and assembles each combination into a SynthesizedCircuit.

The main entry points are:

  • synthesize() — high-level API that loads YAML configs and returns a list of circuits.

  • enumerate_circuits() — lower-level iterator over circuits for a single topology, useful when you want to stream results or supply custom configs.

circuitgenome.synthesizer.synthesizer.build_circuit(topology, variant_map, bias_legs=None)[source]

Assemble a single SynthesizedCircuit from variant_map, applying all cross-slot compatibility filters, pruning passes, and the bias-generation construction described in enumerate_circuits().

Returns None if variant_map is rejected by is_combination_valid(), is_second_stage_compatible(), is_compensation_compatible(), is_output_type_compatible(), is_load_branch_compatible(), is_cmfb_compatible(), or is_tail_current_compatible().

Parameters:
  • topology (TopologyTemplate) – The wiring template that defines slots and net connections.

  • variant_map (dict[str, ModuleVariant]) – One ModuleVariant per slot, keyed by slot name. Any bias_generation slot’s entry may be omitted – it is constructed from the other slots’ demands (see construct_bias_generation()) and overwrites whatever the caller supplied.

  • bias_legs (BiasLegLibrary | None) – Leg library for the bias construction; the built-in config/bias_legs.yaml when omitted.

Return type:

SynthesizedCircuit | None

circuitgenome.synthesizer.synthesizer.enumerate_circuits(topology, modules, config=None)[source]

Yield one SynthesizedCircuit for every valid combination of module variants in topology.

Variants parked with an unsupported: reason tag in the modules YAML are dropped from every slot’s candidate pool before the product is formed (currently inverter_based_input, issue #113, and differential_ota_second_stage, issue #114), unless config={"include_unsupported": True} – see the config parameter.

Variants tagged bias_infeasible: – functionally-correct wiring whose DC bias does not close on the default (low-voltage) spec class (currently stacked_cascode_current_mirror_tail_pmos/_nmos, issue #111) – are likewise dropped by default and kept only with config={"include_infeasible": True}. Unlike unsupported variants these build into a complete, valid netlist; they are retained for design-space exploration as correct-but-infeasible mutation seeds.

Combinations that mix incompatible polarity tags (see is_combination_valid()) are skipped – these would leave a shared node with no DC current path.

Combinations where a second_stage slot sensing the first stage’s output has a signal device of the same channel type as the input pair (see is_second_stage_compatible()) are also skipped – the first stage’s reachable output window and the second stage’s required gate level are disjoint, so no sizing can bias the interface (the third_stage slot senses a wide-swing node instead and is not constrained).

Combinations where a compensation slot wraps a non-inverting stage chain with gain – a positive even number of common-source inversions between the compensation’s in and out nets (see is_compensation_compatible()) – are also skipped: a Miller-family capacitor around such a chain is positive feedback, producing a right-half-plane AC response whose gain/GBW/PM cannot be measured (issue #114). Pure follower chains (zero inversions, no gain) are benign and stay allowed.

Combinations where load’s output_cardinality tag (if set) doesn’t match topology’s output_type (see is_output_type_compatible()) are also skipped – these would leave the load’s mandatory output port(s) (out for "single", out1/out2 for "differential") unconnected: only single_ended topologies define a net for load.out, and only fully_differential topologies define net_loadout1/net_loadout2 for load.out1/out2.

Combinations where a single_ended topology’s untapped first-stage branch node (load.in1, net_diff1) is left high-impedance by the load – a plain rail-referenced current source with no diode, resistor, or cascode connection to define its DC voltage (see is_load_branch_compatible()) – are also skipped: the node sits between two series current sources with no mechanism to absorb their mismatch, so no sizing can establish an operating point (issue #112).

After each slot’s ports are wired, a net-merge pass (see compute_alias_net_rename()/ apply_net_rename()) collapses load ports declared alias_of another load port (out1/ out2 on the 6 resistor/active/current-source loads) onto their target port’s net, restoring the shared in/out node those variants’ devices assume.

For topologies with a cmfb slot, combinations where load’s output_cardinality isn’t "differential" (i.e. cmfb.out would drive nothing) are restricted to the canonical cmfb variant (see is_cmfb_compatible()), and that variant is then pruned to an empty placeholder (see prune_cmfb()) so it contributes no devices and cmfb.bias is not counted as a needed bias rail.

Likewise, combinations where input_pair doesn’t reference its tail port (currently only inverter_based_input, which is self-biased and would otherwise leave net_tail floating) are restricted to the canonical tail_current variant (see is_tail_current_compatible()), and that variant is then pruned to an empty placeholder (see prune_tail_current()) so it contributes no devices and tail_current.bias is not counted as a needed bias rail.

The bias_generation slot is not part of the enumeration product: its variant is constructed per combination from what the other slots actually consume on each bias rail (see construct_bias_generation()) – one leg of the matching kind per consumed rail, so flavor mismatches, unused legs, and redundant rail-7 diodes cannot arise. out1.. out4 feed load’s bias inputs; out5/out6 feed second_stage/third_stage (shared across _p/_n instances in fully-differential topologies via the topology’s static wiring); out7 feeds tail_current (current-mirror / cascode-current-mirror variants only – resistor-tail variants declare bias as optional and need no rail); out8 feeds tail_current.bias_casc (the cascode tails’ wide-swing cascode-gate level). Each role’s rail is independent of the others, so load/second_stage/third_stage/tail_current never share a bias voltage. Any bias_generation entries in modules are ignored.

Parameters:
  • topology (TopologyTemplate) – The wiring template that defines slots and net connections.

  • modules (dict[str, list[ModuleVariant]]) – Module variant pool, keyed by category name. Typically the return value of load_modules().

  • config (dict | None) – Optional per-enumeration filter dictionary. Supported keys: include_unsupported (bool, default False) — when True, variants parked with an unsupported: reason tag (currently inverter_based_input, issue #113, and differential_ota_second_stage, issue #114) are enumerated anyway. Intended for round-trip tests and future un-parking work, not for design runs. include_infeasible (bool, default False) — when True, variants tagged bias_infeasible: (currently the stacked_cascode_current_mirror_tail_*, issue #111) are enumerated anyway. Intended for design-space exploration, which wants the full set of functionally- correct wirings including ones the default spec class cannot bias; not for acceptance-oriented design runs.

Raises:

ValueError – If a required module category (other than bias_generation) has no available variants (after dropping unsupported/bias_infeasible- tagged ones).

Return type:

Iterator[SynthesizedCircuit]

Example:

from circuitgenome.synthesizer.loader import load_modules, load_topologies
from circuitgenome.synthesizer.synthesizer import enumerate_circuits
from circuitgenome.synthesizer.netlist import to_flat_spice

modules = load_modules()
topology = next(t for t in load_topologies() if t.name == "one_stage_opamp")

for circuit in enumerate_circuits(topology, modules):
    print(to_flat_spice(circuit))
circuitgenome.synthesizer.synthesizer.synthesize(config=None, modules_path=None, topologies_path=None)[source]

Generate all op-amp circuits matching the given configuration.

Loads YAML definitions, applies filters, and returns a flat list of SynthesizedCircuit objects.

Parameters:
  • config (dict | None) –

    Optional filter dictionary. Supported keys:

    • topology (str) — exact topology name.

    • stages (int)1, 2, or 3.

    • output_type (str)"single_ended" or "fully_differential".

    • compensation_scheme (str) — for 3-stage topologies, "nested_miller" or "reversed_nested_miller".

  • modules_path (str | Path | None) – Path to a custom modules YAML file. Uses the built-in definitions when omitted.

  • topologies_path (str | Path | None) – Path to a custom topologies YAML file. Uses the built-in definitions when omitted.

Returns:

All synthesized circuits across every matching topology.

Return type:

list[SynthesizedCircuit]

Example:

from circuitgenome import synthesize
from circuitgenome.synthesizer import to_flat_spice

circuits = synthesize({"stages": 2, "output_type": "single_ended"})
print(f"{len(circuits)} circuits generated")
print(to_flat_spice(circuits[0]))