Getting started
===============
This page walks through installing sycan, building a circuit, and using
the four core solvers — :func:`~sycan.solve_dc`, :func:`~sycan.solve_ac`,
:func:`~sycan.solve_impedance`, and :func:`~sycan.solve_noise` — plus
the schematic renderer :func:`~sycan.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``:
.. code-block:: console
pip install sycan
With `uv `_:
.. code-block:: console
uv add sycan
The only hard runtime dependency is ``sympy`` — sycan reaches the CAS
through its own :mod:`sycan.cas` wrapper, so the underlying library can
be swapped via :func:`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 :func:`~sycan.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 :class:`~sycan.Circuit`, then add
components by name. Symbolic values come from :mod:`sycan.cas` (the
CAS proxy, sympy by default), so any parameter can be a free symbol or
a closed-form expression:
.. code-block:: python
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.** :func:`~sycan.parse` turns a SPICE
netlist into the same :class:`~sycan.Circuit`. This is the shortest path
when you already have a netlist or want to read one from a file:
.. code-block:: python
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 — :func:`~sycan.solve_dc`
--------------------------------------------
:func:`~sycan.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:
.. code-block:: python
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 :mod:`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 — :func:`~sycan.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:
.. code-block:: python
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
:mod:`sycan.polynomials`).
Port impedance — :func:`~sycan.solve_impedance`
-----------------------------------------------
To get the small-signal input or output impedance at a node, mark the
nodes of interest as ports and ask :func:`~sycan.solve_impedance`. The
``termination="auto"`` mode picks an appropriate excitation and
loading automatically:
.. code-block:: python
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 — :func:`~sycan.solve_noise`
--------------------------------------
Pass an ``output_node`` and any component that owns a noise source
(thermal, shot, flicker — any subclass of :class:`~sycan.NoiseSource`)
contributes its trans-impedance to the output PSD. The classic ``kT/C``
of an RC low-pass is one line:
.. code-block:: python
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 — :func:`~sycan.autodraw`
------------------------------------------
:func:`~sycan.autodraw` accepts the same circuit objects (or a SPICE
netlist string) and returns a self-contained SVG:
.. code-block:: python
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 :doc:`autodraw` for the full pipeline and tuning knobs.
What next
---------
* :doc:`autodraw` — the schematic-rendering pipeline, glyph loading,
and pattern-detect overrides.
* :doc:`api` — 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.