Skip to content

gdsfactory/circulax

Repository files navigation

Circulax

logo

A differentiable circuit simulator built on JAX. Define netlists, run transient / DC / AC / harmonic-balance analysis, and differentiate through the solver for gradient-based optimization and inverse design. Circulax aims to be flexible multi-disciplined circuit simulator offering a similar interface to the linear s-parameter solver SAX.

Read the Documentation here

Installation

pip install circulax

Quickstart

Simulate an underdamped LCR circuit in the time domain:

LCR transient animation

import jax, jax.numpy as jnp
from circulax import compile_circuit
from circulax.components.electronic import Capacitor, Inductor, Resistor, VoltageSource

jax.config.update("jax_enable_x64", True)

net_dict = {
    "instances": {
        "GND": {"component": "ground"},
        "V1":  {"component": "source_voltage", "settings": {"V": 1.0, "delay": 0.25e-9}},
        "R1":  {"component": "resistor",        "settings": {"R": 10.0}},
        "C1":  {"component": "capacitor",       "settings": {"C": 1e-11}},
        "L1":  {"component": "inductor",        "settings": {"L": 5e-9}},
    },
    "connections": {
        "GND,p1": ("V1,p2", "C1,p2"),
        "V1,p1": "R1,p1",  "R1,p2": "L1,p1",  "L1,p2": "C1,p1",
    },
}

models = {
    "resistor": Resistor, "capacitor": Capacitor,
    "inductor": Inductor, "source_voltage": VoltageSource, "ground": lambda: 0,
}

circuit = compile_circuit(net_dict, models)
y_op    = circuit.dc()

sol = circuit.transient(
    t0=0.0, t1=3e-9, dt0=3e-12, y0=y_op,
    saveat=jnp.linspace(0, 3e-9, 500),
    max_steps=100_000,
)

v_cap = circuit.port(sol.ys, "C1,p1")  # capacitor voltage over time

Common analyses use the same compiled object:

op = circuit.dc()
op_sweep = circuit.dc(params={"R1.R": 20.0, "wavelength_nm": 1310.0})
S = circuit.ac(ports=["C1,p1"], freqs=jnp.logspace(6, 10, 101), y_dc=op)
y_time, y_freq = circuit.hb(freq=1e6, harmonics=5, y0=op)
v_out = circuit.port(op, "C1,p1")

Parameter keys like "R1.R" update one instance. Keys without a dot, such as "wavelength_nm", are broadcast to every component that declares the parameter.

Defining Components

Components are plain Python functions — no boilerplate, no subclassing:

from circulax.components.base_component import component, Signals, States

@component(ports=("p1", "p2"))
def Resistor(signals: Signals, s: States, R: float = 1e3):
    i = (signals.p1 - signals.p2) / R
    return {"p1": i, "p2": -i}, {}          # (currents, charges)

@component(ports=("p1", "p2"))
def Capacitor(signals: Signals, s: States, C: float = 1e-12):
    q = C * (signals.p1 - signals.p2)
    return {}, {"p1": q, "p2": -q}          # dq/dt becomes current automatically

Non-linear opto-electronic components are just as simple — the Jacobian is computed automatically via Automatic Differentiation:

@component(ports=("optical_in", "anode", "cathode"))
def Photodetector(signals: Signals, s: States,
                  responsivity: float = 0.8, dark_current: float = 1e-9):
    optical_power = jnp.abs(signals.optical_in) ** 2           # non-linear
    i_photo = responsivity * optical_power + dark_current
    i_reflect = -0.01 * signals.optical_in                     # small back-reflection
    return {"optical_in": i_reflect, "anode": i_photo, "cathode": -i_photo}, {}

Existing SAX models plug in directly — reuse your photonic PDK as-is:

import sax
from circulax.s_transforms import sax_component

Straight = sax_component(sax.models.straight)   # that's it — ready to simulate

Features

  • Transient — implicit ODE stepping via Diffrax; handles stiff circuits.
  • DC operating point — Newton-Raphson root-finding via Optimistix.
  • Harmonic Balance — periodic steady state directly in the frequency domain.
  • AC sweep — linearise at DC op-point, sweep frequency, return S-parameters.
  • OSDI compact models — load OpenVAF-compiled Verilog-A models through bosdi.
  • Automatic differentiation — differentiate through the solver for gradient-based inverse design.
  • Hardware-agnostic — CPU, GPU, or TPU with no code changes.
  • Mixed-domain — electronic and photonic circuits in a single netlist.

Comparison to SPICE

Circulax is a SPICE-like simulator but built with modern tooling so users can easily create their own models in a language they know.

SPICE circulax
Model definition Verilog-A / hardcoded C++ Python functions
Derivatives Hardcoded or compiler-generated Automatic differentiation
Solver Fixed/heuristic stepping Adaptive ODE (Diffrax)
Hardware CPU-only CPU / GPU / TPU

Inverse Design via Back-propagation

Because the entire solver is written in JAX, gradients flow end-to-end from a loss function back through the simulation and into component parameters. Use jax.grad and standard optimizers to automatically tune circuit designs — the cost is one forward + one backward pass regardless of parameter count.

See the Inverse Design guide for a comparison with finite differences and worked examples.


Copyright © 2026 Chris Daunt — Apache-2.0

About

Circulax is a differentiable circuit simulation framework built on JAX, Optimistix and Diffrax

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors