Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Circuit design optimization with fcmaes-core and sindr

This standalone Rust tutorial puts a small-signal circuit simulator directly inside single-objective, multi-objective, and quality-diversity optimizers. It uses sindr 0.1.0-alpha.6 for AC modified-nodal analysis and fcmaes-core 0.1.3 for native parallel search.

The example is about optimization architecture and objective design, not production analog sign-off. Every evaluation follows the same auditable path:

The hot path has no Python callback. Python reads the recorded CSV files only to create the documentation figures.

Why this is a gradient-free problem

The controls span several decades, E12 controls are discrete, and the desired outputs are features of a sampled Bode curve: its peak, two threshold crossings, bandwidth, Q, and ripple. Those operations make a conventional end-to-end derivative unavailable even though each circuit solve is linear. The three formulations answer different design questions:

ModuleCircuitResult neededOptimizer
SO5-variable multiple-feedback band-passone filter near 10 kHz, Q=5parallel retry with CMA-ES, DE, and BiteOpt
MO8-variable fourth-order low-passa cutoff/ripple/area trade-off setconstrained MODE
QDdiscrete E12 band-pass catalogueone robust filter per frequency/gain nichebatch MAP-Elites

Resistors and capacitors are decoded logarithmically. Rebuilding the short sindr::Circuit for every candidate keeps worker state isolated and is small relative to the AC sweep.

Smooth feature extraction

Choosing the grid point with the largest gain produces a staircase objective. Changing a component can move the real response while the winning grid index stays fixed. More samples make the steps narrower but do not remove them.

The implementation fits a three-point parabola in log-frequency around the sampled maximum and linearly interpolates the two −3 dB crossings in (log(f), dB). The checked-in regression experiment varies R1 over 24 values: even the 201-point publication-grid arg-max has only two distinct centre frequencies, while the interpolated feature has 24.

Smoothness and numerical accuracy are separate requirements. A 41-point grid made the interpolated centre frequency smooth but biased Q by about 14% because its linearly interpolated bandwidth crossings were too widely spaced. The SO publication grid is therefore 201 points. A regression test compares both centre frequency and Q against an 801-point reference, with relative limits of 0.1% and 0.5% respectively.

This is more important than optimizer tuning: an optimizer can explore a smooth approximation of the physical response instead of quantization artifacts created by the measurement code.

Module 1: one tuned MFB band-pass

The decision vector u ∈ [0,1]⁵ decodes to three resistors in [100 Ω, 100 kΩ] and two capacitors in [10 pF, 1 µF]:

value = lower × (upper / lower)^u

The minimized scalar is dimensionless:

|log10(f0 / 10 kHz)|
  + 0.2 |log10(Q / 5)|
  + max(0, 1 - linear_peak_gain)

Each optimizer receives a requested budget of 6,000 evaluations split over six parallel retries. Population-based optimizers may complete a population beyond that request, so the manifest records both requested and actual work.

OptimizerActual evaluationsBest objectiveCentreQWall time
CMA-ES retry6,1382.675×10⁻⁴10,000.01 Hz5.01540.377 s
DE retry6,1271.577×10⁻⁶9,999.99 Hz5.00010.380 s
BiteOpt retry6,0002.501×10⁻⁶10,000.01 Hz5.00010.359 s

These are independent arms with equal requested budgets, not sequential polishing stages. The reported values are replayed from each retained design. The peak-gain penalty is inactive at these three high-gain winners, but remains part of the search objective so an attenuating response cannot win merely by matching frequency and Q.

Module 2: a constrained low-pass trade-off set

Two buffered second-order sections form a fourth-order low-pass with four resistors and four capacitors. MODE minimizes three independent values:

  1. |log10(f−3dB / 100 kHz)|;
  2. pass-band ripple through 80 kHz in dB;
  3. total capacitance in nF as an area/cost proxy.

The ripple measurement includes an interpolated sample at exactly 80 kHz and parabolically interpolates an interior pass-band extremum. The 201-point MO publication grid is tested against an 801-point reference for both cutoff and ripple; the relative cutoff limit is 0.1% and the absolute ripple limit is 0.01 dB.

The explicit quality constraint is feasible at <= 0:

peak_above_low_frequency_gain - 3 dB <= 0

The seed-42 publication run completed 8,192 evaluations and retained 128 feasible nondominated designs in 0.335 s. Pairwise Spearman correlations on that recorded front are −0.856 for cutoff error versus ripple, −0.433 for cutoff error versus capacitance, and +0.027 for ripple versus capacitance. Those values describe the observed trade-offs; they were not used as acceptance thresholds.

The constraint is not decorative: only 101 of the initial 128 population members were feasible. The full population was feasible by the next recorded checkpoint, and the final front’s least-slack point remained 1.127 dB inside the 3 dB limit.

The stars identify objective extremes and a scale-documented compromise. They are examples for inspection, not an extra preference hidden inside MODE.

Module 3: a robust E12 filter catalogue

This module deliberately extends continuous circuit tuning into manufacturability. Each coordinate is rounded and clamped to an index in an inclusive E12 table:

Component classRangeChoices
resistor100 Ω–100 kΩ37
capacitor10 pF–1 µF61

Endpoint inclusion is why these counts are not simply “decades × 12.” MAP-Elites itself has no integer-mask argument, so discreteness belongs in the decoder. The archive stores the rounded indices and physical values needed to rebuild every elite.

The descriptors are observable behavior, not decision variables:

  • log10(f0 / Hz) from 2.0 to 6.5 (100 Hz–3.16 MHz);
  • peak gain from −60 to +40 dB.

Before optimization, the publication command samples 1,000 catalogue coordinates. Of those, 898 have an interior peak with both −3 dB crossings. That range study is exported in range_study.csv; it froze the rounded bounds above. During optimization, 189 otherwise valid responses outside the frozen box are counted and rejected rather than silently accumulated in boundary cells.

Quality is the population standard deviation of centre-frequency movement over 16 independent ±5% perturbations:

stddev(20 log10(f0,perturbed / f0,nominal))

One perturbation table is generated from the experiment seed and reused for every candidate. These common random numbers make repeated evaluations identical and prevent a design from winning because it received a lucky tolerance sample.

The 4,096-candidate search used 68,416 AC solves; the preceding range study used another 1,000, for 69,416 exact solves in the complete QD protocol. It filled 381 of 400 niches (95.2%) in 2.757 s for the optimization phase; 85 candidates had no valid band-pass features. The run manifest reports candidate calls, both solve counts, invalid responses, out-of-range descriptors, distinct decoded elites, and the frozen 20×20 grid.

An archive is useful because it returns a repertoire rather than one optimum. The six marked elites below are replayed directly from their recorded E12 component indices:

Parallelism and reproducibility

fcmaes-core owns candidate-level parallelism. A sindr AC sweep is serial, and every worker builds an isolated circuit. This avoids a nested thread pool and gives --workers one clear meaning. --workers 0 uses the available logical CPUs.

The publication evidence was recorded on 2026-07-27 on an AMD Ryzen 9 9950X (16 cores, 32 hardware threads) with:

cd tutorials/sindr-circuit-design
cargo run --release -- \
  --preset publication --mode all --workers 16 --seed 42 \
  --output results/publication

The three optimizer arms in the SO manifest ran concurrently only within each arm; their displayed wall times should not be added to MO/QD and interpreted as a cross-machine benchmark. Timings are reproducibility checks for this code and machine, not performance claims about other circuit simulators.

For a bounded functional check:

cargo run --release -- \
  --preset smoke --mode all --workers 2 --seed 42 --no-output

Run cargo run --release -- --help for individual modules, optimizer arms, budgets, frequency-grid size, Monte Carlo draws, and archive settings. The converged SO and MO studies use 201 AC points; the independently checked QD descriptors retain 41 points. --points deliberately overrides all three when investigating grid sensitivity.

Artifacts and figures

The publication bundle follows RESULT_SCHEMA.md. All objectives are minimized and constraints are feasible at <= 0.

results/publication/
  so/{run.json,best.csv,convergence.csv,feature_curve.csv,feature_smoothness.csv}
  mo/{run.json,pareto.csv,convergence.csv}
  qd/{run.json,range_study.csv,archive.csv,elites.csv,convergence.csv}

Native Rust writes full-precision JSON/CSV. The architecture diagram is a semantic SVG; every result plot is generated from the files above:

python plot_results.py --write
python plot_results.py --check

The repository-wide tutorials/python/render_all.py --check command calls the same tutorial-local renderer and compares every generated SVG byte for byte. That repository-wide comparison requires the Matplotlib version pinned in tutorials/python/requirements-lock.txt; use the tutorial-local check when working outside that environment.

Scope and simulator limitations

The dependency is pinned exactly because sindr is an alpha release. This tutorial relies on its documented AC analysis plus two implementation details of 0.1.0-alpha.6:

  • the op-amp is a high-gain linear VCVS; its rail fields do not model AC clipping or supply current;
  • AC analysis first obtains a DC operating point and then solves the linearized complex system at each frequency.

The exact crates.io fcmaes-core = "=0.1.3" dependency is also deliberate: the recorded tutorial validates the published optimizer/simulator pairing. The manifest retains a commented local-path override for testing changes to the working-tree optimizer core before updating that pin.

Consequently, the objectives do not claim saturation, headroom, slew rate, power consumption, nonlinear distortion, or production parasitics. Transient amplifier and switching problems were considered and intentionally excluded: the current transient interface does not provide the timestep and periodic steady-state control those objectives require. Temperature sweeps were also excluded because this RC catalogue specifies component tolerance, not junction temperature behavior.

The separate thevenin gate-driver tutorial follows that transient question without changing this tutorial’s simulator or small-signal scope. It adds explicit timestep convergence and an independent 49-design ngspice validation gate before publishing an optimization result.

If a later sindr version changes these contracts, rerun the feature tests, range study, and publication protocol before updating the exact version pin.

Test

cargo fmt --all -- --check
cargo clippy --all-targets -- -D warnings
cargo test
cargo run --release -- \
  --preset smoke --mode all --workers 2 --seed 42 --no-output
python plot_results.py --check

The unit tests cover decoding, exact E12 endpoints, analytic RC cutoff, interpolation smoothness, 201-to-801-point Q and ripple convergence, endpoint safety, deterministic common tolerance draws, range-study validity, and replayable SO, MODE, and QD smoke searches.