Getting started

This page walks through installing sycan, building a circuit, and using the four core solvers — solve_dc(), solve_ac(), solve_impedance(), and solve_noise() — plus the schematic renderer autodraw(). The examples are small and can be pasted into a Python REPL in order.

Install

sycan needs Python ≥ 3.11 and SymPy. With pip:

pip install sycan

With uv:

uv add sycan

The only hard runtime dependency is sympy — sycan reaches the CAS through its own sycan.cas wrapper, so the underlying library can be swapped via sycan.cas.select_backend(). SymPy is the default; the optional symengine backend (pip install "sycan[symengine]", then export SYCAN_CAS_BACKEND=symengine before importing) is roughly 7–8× faster on AC and noise solves. See docs/BE_PORT_STATUS.md for backend coverage and docs/BE_BENCHMARK.md for the full benchmark. The schematic glyphs that autodraw() uses are bundled inside the wheel, so no post-install configuration is needed.

Try it without installing

The live REPL runs sycan entirely in your browser via Pyodide — no local install. The page ships preset examples (voltage divider, RC low-pass, CS amp, noise-cancelling LNA, …); click one and hit Run.

Build a circuit

There are two equivalent ways to describe a netlist:

1. The Python API. Start with a Circuit, then add components by name. Symbolic values come from sycan.cas (the CAS proxy, sympy by default), so any parameter can be a free symbol or a closed-form expression:

from sycan import cas as cas
from sycan import Circuit
from sycan.components.basic import Resistor, VoltageSource

Vin, R1_, R2_ = cas.symbols("Vin R1 R2", positive=True)

c = Circuit("voltage divider")
c.add(VoltageSource("V1", "in", "0", Vin))
c.add(Resistor("R1", "in", "out", R1_))
c.add(Resistor("R2", "out", "0", R2_))

Node "0" is always ground. Convenience methods (c.add_resistor, c.add_vsource, …) wrap the same constructors when you don’t need a reference to the component object.

2. A SPICE netlist string. parse() turns a SPICE netlist into the same Circuit. This is the shortest path when you already have a netlist or want to read one from a file:

from sycan import parse

c = parse("""voltage divider
V1 in 0 Vin
R1 in out R1
R2 out 0 R2
.end
""")

Both forms produce identical circuits — pick whichever reads better for the problem. The remaining examples mix the two interchangeably.

DC operating point — solve_dc()

solve_dc() returns a dict mapping each unknown (node voltages V(node) and source currents I(name)) to its closed-form expression. For the divider above:

from sycan import cas as cas
from sycan import solve_dc

sol = solve_dc(c)
V_out = sol[cas.Symbol("V(out)")]
print(cas.simplify(V_out))   # R2*Vin / (R1 + R2)

Linear circuits go through symbolic LU. When any component reports has_nonlinear (MOSFETs, BJTs, diodes), the solver instead calls the CAS solver (cas.solve, where sp is sycan.cas) on the full residual system — so transcendental operating points (sub-threshold MOSFETs, diode equations, …) come out as closed-form expressions when the backend can solve them.

AC transfer functions — solve_ac()

The AC solver returns the same shape of dict, but each value is a function of the Laplace variable s. Setting one of the source ac_value parameters to Vin (or any expression) gives the small-signal transfer function:

from sycan import cas as cas
from sycan import parse, solve_ac

c = parse("""RC low-pass
V1 in 0 AC Vin
R1 in out R
C1 out 0 C
.end
""")

sol = solve_ac(c)
H = sol[cas.Symbol("V(out)")] / cas.Symbol("Vin")
print(cas.simplify(H))   # 1 / (C*R*s + 1)

Pass your own s symbol if you want to share it with downstream analysis (e.g. polynomial filter prototypes from sycan.polynomials).

Port impedance — solve_impedance()

To get the small-signal input or output impedance at a node, mark the nodes of interest as ports and ask solve_impedance(). The termination="auto" mode picks an appropriate excitation and loading automatically:

from sycan import cas as cas
from sycan import Circuit, solve_impedance

mu_n, Cox, W, L, V_TH, lam, R_L = cas.symbols("mu_n Cox W L V_TH lam R_L")
VDD, V_GS_op, V_DS_op, C_gs = cas.symbols("VDD V_GS_op V_DS_op C_gs")

c = Circuit()
c.add_port("P_in",  "gate",  "0", "input")
c.add_port("P_out", "drain", "0", "output")
c.add_vsource("Vdd", "VDD", "0", value=VDD, ac_value=0)
c.add_resistor("RL", "VDD", "drain", R_L)
c.add_nmos_l1("M1", "drain", "gate", "0",
              mu_n=mu_n, Cox=Cox, W=W, L=L, V_TH=V_TH, lam=lam,
              C_gs=C_gs, V_GS_op=V_GS_op, V_DS_op=V_DS_op)

Z_in  = cas.simplify(solve_impedance(c, "P_in",  termination="auto"))
Z_out = cas.simplify(solve_impedance(c, "P_out", termination="auto"))
print(Z_in)    # 1 / (s*C_gs)
print(Z_out)   # R_L || r_o, in closed form

Noise PSD — solve_noise()

Pass an output_node and any component that owns a noise source (thermal, shot, flicker — any subclass of NoiseSource) contributes its trans-impedance to the output PSD. The classic kT/C of an RC low-pass is one line:

from sycan import cas as cas
from sycan import Circuit, T_kelvin, k_B, solve_noise
from sycan.components.basic import Capacitor, Resistor, VoltageSource

R, C, omega = cas.symbols("R C omega", positive=True)

c = Circuit("kT/C")
c.add(VoltageSource("V1", "in", "0", value=0, ac_value=0))
c.add(Resistor("R1", "in", "out", R, include_noise="thermal"))
c.add(Capacitor("C1", "out", "0", C))

S_total, per_source = solve_noise(c, "out", simplify=True)
S_omega = cas.simplify(S_total.subs(cas.Symbol("s"), cas.I * omega))
power = cas.integrate(S_omega, (omega, 0, cas.oo)) / (2 * cas.pi)
print(cas.simplify(power))   # k_B*T/C

The returned per_source dict maps each noise-source name to its individual PSD — handy when you want to pinpoint which device dominates the output noise.

Draw a schematic — autodraw()

autodraw() accepts the same circuit objects (or a SPICE netlist string) and returns a self-contained SVG:

from sycan import autodraw

svg = autodraw(c, filename="rc_lowpass.svg")
# svg is also returned as a string — useful in notebooks where you
# can call IPython.display.SVG(svg).

The placer wraps simulated annealing around the placement and routing pipeline; when wires would lie collinear on top of one another, it automatically retries with the next seed in a fixed sequence (up to max_retries times). See How autodraw works for the full pipeline and tuning knobs.

What next

  • How autodraw works — the schematic-rendering pipeline, glyph loading, and pattern-detect overrides.

  • API reference — full API reference with autosummary tables for every module.

  • The REPL — preset examples covering filters, low-noise amplifiers, voltage references, and S-parameter t-lines.