Expressions
One expression language runs everywhere a value can be computed: global variables, component parameters, SDD device equations, and measurements. Anywhere you can type a number, you can type an expression — `2*pi*freq0`, `polar(0.1, 35)`, `if(Vg > Vth, gm*Vg, 0)`.
Where expressions are used
| Consumer | What it evaluates | Result |
|---|---|---|
| Global variables (VAR) | a testbench variable | Real or Complex |
| Cell parameters | a parameter default or an instance override | Real or Complex |
| SDD device equations | i = f(v), q = f(v) over port voltages | Real (and its derivatives) |
| Measurements | a figure of merit over result cubes | Real or Complex |
circuitRF parses an expression once into a tree and evaluates it against a scope — it never does text substitution. That makes it fast (an SDD equation is parsed once, evaluated thousands of times) and correct (no fragile longest-name-first variable replacement).
Values: Real, Complex, Bool, String
Every value carries a kind:
Real — a plain number. Component values like
R,L,Care Real (50stays50, not50+j0).Complex — appears as soon as the imaginary unit
jenters, or from an operation that is mathematically complex (e.g.sqrtof a negative). Impedances resolve here. Write imaginary values withj*:j*4,2 + j*3.Bool — the result of a comparison or logical operator; used only as a condition in
if/?:. A parameter that resolves to Bool is an error (a condition isn't a component value).String — a double-quoted literal (
"spline","path/to/x.s2p"). Storage only — no string operators. Used for genuinely textual parameters (the SnP block'sFile,InterpMode, …).
< <= > >= require Real operands (complex numbers are unordered); == /
!= work on Real and Complex. Real ∘ Complex promotes to Complex.
Operators & precedence
Lowest-binding (evaluated last) to highest-binding (tightest):
| Operators | Notes |
|---|---|
?: (ternary) | cond ? a : b — same as if(cond, a, b) |
|| | logical OR |
&& | logical AND |
== != | equality (Real or Complex) |
< <= > >= | ordering (Real only) |
+ - (binary) | |
* / | |
+ - ! (unary prefix) | |
^ (power) | binds tighter than unary minus: -2^2 == -4; right-assoc: 2^3^2 == 2^(3^2) |
function call, ( ), atoms | highest |
Constants & functions
Constants: j = (0,1), pi, e. These (and all function names) are reserved — a variable
may not shadow them.
| Group | Functions |
|---|---|
| Trig | sin cos tan asin acos atan atan2(y,x) |
| Hyperbolic | sinh cosh tanh |
| Exp / log / power | exp log (natural) log10 sqrt pow(x,y) abs |
| Misc | min(a,b) max(a,b) sign(x), if(cond,then,else) |
| Complex → Real | real(z) imag(z) abs(z) mag(z) (= abs) phase(z) (degrees) phase_rad(z) (radians) |
| Real,Real → Complex | polar(mag, phase_deg) — e.g. polar(0.1, 10) is 0.1∠10° |
Units
A unit attaches at the assignment level (after the expression), and scales the value by a
linear factor — L = L1 nH, Z = 50 Ohm, M = 0.5 pH. Units are not part of the expression
grammar.
| Domain | Units |
|---|---|
| SI prefixes | T G M k m u n p f (1e12 … 1e-15) |
| Frequency | Hz kHz MHz GHz THz |
| Inductance | H mH uH nH pH fH |
| Capacitance | F mF uF nF pF fF |
| Resistance | Ohm kOhm MOhm |
| Length | m mm um mil |
| Angle | deg rad |
They are logarithmic, not linear scale factors, so they are functions — dB(...),
dBm(...) — never a trailing unit on a value.
Variables, scope & cell parameters
Global variables (authored in a VAR block) are visible everywhere. Cell parameters pass top-down: an instance binds overrides in the parent scope; the cell evaluates its own defaults and component values in its own scope, then passes its scope down to its sub-cells. A name resolves to the local cell variable/parameter first, then the global — a cell never sees a parent's locals or a sibling's, which is what keeps a cell's meaning independent of where it's placed.
freq is reserved
Lowercase freq is the simulator's current stamping frequency (Hz), injected when the engine
evaluates a frequency-dependent value (a Z_Port impedance, an SDD H[w]). You can't
name a variable freq. It is distinct from a source's capital-Freq tone parameter.
User-defined functions
Define a function whose body is an expression in the same language, with any number of parameters:
gm(vgs, vth) = beta * tanh(vgs - vth)
A call binds arguments positionally into a fresh scope whose parent is the definition's scope (so a user function sees globals, not the caller's locals). User functions compose with built-ins and with each other, and are subject to the same cycle detection.
Cycle detection & errors
circuitRF detects and rejects dependency cycles across the whole graph — global variables,
cell-parameter defaults, and overrides — and reports the chain (e.g. a → b → a) rather than
hanging. Every error names the offending text, never a silent zero or NaN:
Cycle — the dependency chain.
Unresolved name — the name and the scope it was sought in.
Type error — Bool where a number is required; ordering on a complex operand; a non-Bool
ifcondition.Arity / unknown function — wrong argument count or an undefined function.
Domain —
log(0),sqrtof a negative in a real SDD context, division by zero (reported with context).
SDD device equations operate on real time-domain voltages and produce real current/charge, so j
is disallowed there and sqrt/log of a negative is a domain error rather than a
promotion to Complex. circuitRF differentiates SDD equations automatically (forward-mode AD) to get the
solver's conductance/capacitance — including through an if, where it differentiates the branch
that is taken. For tough convergence prefer soft switching (tanh) over a hard if.
See The SDD.
See also: Measurements · The SDD ·
VAR component. Full design: docs/design/expressions.md.