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), validrange(x, lo, hi) — x, with a warning when it lies outside lo … hi |
| Complex → Real | real(z) imag(z) abs(z) mag(z) (= abs) phase(z) (degrees) phase_rad(z) (radians) unwrap(z) — the phase in degrees, continued across ±180° along the frequency axis instead of jumping by 360°; unwrap(p) does the same to a phase p already in degrees, and unwrap(x, "axis") names another axis |
| Slope | deriv(y) — the derivative of y along its axis, per unit of that axis: a swept oscillator's deriv(OSC1.Fosc) is Kvco. A value with several axes takes freq, or deriv(y, "axis") names one |
| Real,Real → Complex | polar(mag, phase_deg) — e.g. polar(0.1, 10) is 0.1∠10° |
| Network metrics | mu(S) mu_prime(S) K(S) delta_mag(S) max_gain(S) (dB) max_gain_lin(S) passivity(S), each also (S, in, out) for an N-port — S is an analysis's S cube, e.g. mu(SP1.S) |
| Noise | nf(S[, Γs][, port]) (dB) — the noise figure from port 1's Z0 or a source of reflection Γs, to port 2 or port; te(S[, Γs][, port]) (K) — the same as a noise temperature; rn(S) — Rn/R; ns_circle(S, NFdB[, npts]) — the constant-noise-figure circle as {freq, point} Γ points, or ns_circle(NFdB, NFmin, Sopt, rn[, npts]) from noise parameters. The S forms need a run with Noise=yes — the equations |
| Gain and stability circles | ga_circle(S, GdB[, npts]) — the constant available-gain circle on the source plane; gp_circle(S, GdB[, npts]) — the constant operating-gain circle on the load plane; stab_circle(S, "source"|"load"[, npts]) — a stability circle; stab_side(S, "source"|"load") — 1 when the inside is stable, −1 when the outside is. Two-ports; GdB is a number or one per frequency — the equations |
| Delay / match | group_delay(z) (seconds; or group_delay(S, in, out)) vswr(Γ) |
| Statistics over trials | mean_over std_over median_over pctl_over(x, p) skew_over kurt_over yield_over(cond) cpk(x, lo, hi) sigma_to(x, limit); histogram(x, bins) cdf(x) normq(x) yield_sens(pass, x, bins) — measurement functions over a Monte Carlo run's trials |
| Distributions | agauss(nom, dev[, k]) normal, σ = dev/k · gauss(nom, rel[, k]) normal, σ = nom·rel/k · aunif(nom, dev) uniform on nom ± dev · unif(nom, rel) uniform on nom·(1 ± rel) · limit(nom, dev) nom ± dev, equally likely — each is nom except in a Monte Carlo trial. limit(x, lo, hi) is a clamp. |
Units
A unit attaches in two places. At the assignment level (after the expression, or in the row's unit
column) it scales the value by a linear factor — L = L1 nH, Z = 50 Ohm, M = 0.5 pH. And glued to
a number inside the expression, with no space, it makes a unit literal — 10um + 1mil, 2.4GHz,
10V + 100mV — scaled to base SI as it is read. 2 GHz, with a space, is not a literal.
An expression holding a unit literal, or naming a variable declared with a unit, is already in base units,
so the assignment's unit is not applied to its result; a bare number added to, subtracted from or
compared with such a term takes that unit instead (w + 5 in mil is 5 mil more than w), and a bare
number that multiplies or divides stays a number (2*w). See Units.
| 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 mOhm kOhm MOhm GOhm TOhm |
| Length | metre mm um cm nm mil in inch — m is the prefix milli, never the metre |
| Angle | deg rad |
They are logarithmic, not linear scale factors, so they are functions — dB(...),
dBm(...) — never a unit literal: 0dBm is refused.
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.