circuitRF Reference Guide

The Command Line

circuitRF runs without the GUI — not just its engines, but authoring, validation, resolution and drawing too. One executable, fifteen verbs — S-parameters, DC, harmonic balance, loadpull, loadpull pursuit, electromagnetic extraction, layout interchange, creating a workspace or a cell, importing a part, rendering a document as a picture, checking a design, explaining what it resolved to, reading a result back, an elaborated-netlist dump, and an MCP server. Every one of them answers --json. This chapter is the operational reference for all of them, including a worked EM run and a worked render, each from an empty folder.

Invoking it

The command-line driver is the same program as the GUI's Run button with the window taken off. It reads the same files, elaborates them with the same elaborator, runs the same engines, and evaluates the test bench's measure lines with the same evaluator.

$ circuitrf <verb> <file> [options]

From a source checkout there is no circuitrf on your path yet, so put dotnet run --project src/Cli -- wherever circuitrf appears:

$ dotnet run --project src/Cli -- sparam mycircuit.cnl --freq 1GHz:3GHz:50MHz

Run it with no arguments for the built-in help.

A file that works headless works when opened

This is the point of the command line being the same code rather than a second implementation. A .cnl that runs here runs when you open it in the workspace, and an EM setup run with em writes the byte-identical Touchstone the Simulate button writes. There is one elaborator, one set of engines, one measurement evaluator and one results-path convention behind both.

The verbs at a glance

Verb Takes Runs Writes
sparam .cnl or .csch The linear S-parameter engine over a frequency sweep A Touchstone .sNp, always
dc .cnl or .csch The nonlinear DC engine Node voltages and probe currents, to stdout
hb .cnl or .csch Harmonic balance, single- or multi-tone Spectra tables to stdout; -o .mat/.npy/.txt
lp .cnl or .csch Loadpull over the directive's Γ grid A per-Γ-point table; -o .mat/.npy/.txt/.spl/.lpcwave
lpp .cnl or .csch Loadpull pursuit — searches for the optima Optima + the follow-on grid; -o as hb; --out-grid writes a .gam
em .cem The EM kernel the setup resolves to A Touchstone .sNp and a grouped .npy, where Simulate writes them
rail .crail The same DC solve and via check railRF's Run button calls The ports, the ranked breakdown and the via check to stdout; -o .csv/.npy/.mat/.txt/.svg/.pdf
smith .csmith The same cascade evaluator the Smith Chart window walks on every edit The reading and the per-node table to stdout; -o .s1p for the load Γ, -o .svg/.pdf/.png for the chart
lvs a cell folder, a workspace, a .clay or a .csch The same comparison the LVS panel's Compare button calls Nothing — the report to stdout; -o report.txt
convert any layout format The same importer and exporter File ▸ Import/Export runs The layout in the format you asked for
new workspace a directory The same code File ▸ New Workspace runs A .cws and, unless you say otherwise, a copied technology
new cell a workspace + a name The same code New Cell runs A cell folder and one empty-but-valid file per view
import part a component file or folder The same code Import Component runs A cell folder holding the land patterns and the symbol
render a .csch, .csym, .clay or .cdd, a cell folder, or a workspace The same Skia renderers the editors draw every frame with A .svg, .pdf or .png, where -o says
check a workspace, a cell folder, or one document Every validator the application already uses Nothing — findings to stdout
explain the same Resolution only — no analysis Nothing — the walk and the answer, to stdout
read a result file, or one of circuitRF's own documents The same loaders the Data Display reads a file with Nothing — what the file holds, to stdout
netlist a .csch, a cell folder, or a workspace The same extraction Simulate performs A .cnl, or the netlist text to stdout
plot a result file Builds a one-plot data display and draws it A .svg, .pdf or .png, where -o says
find a directory Nothing — it reads documents Nothing — the workspaces, cells, views and analyses under it
reference nothing Nothing — it reads no file Nothing — the reference pages, and every netlist primitive with its terminals and parameters
elab .cnl or .csch Elaboration only, no analysis The elaborated netlist, to stdout
serve --root <dir> An MCP server for an external client Whatever the tool it is asked for writes

hb, lp and lpp all run the whole parametric sweep when one wraps the analysis — see naming the wrapper.

Every run verb takes a schematic as well as a netlist. Hand it a .csch and it extracts the netlist in memory first — the same extraction Simulate performs — so you do not have to write one out to run a design you drew. netlist is how you see what it will run.

Results on stdout, everything else on stderr

stdout is the result. stderr is everything else — progress, per-grid-point engine chatter, [circuitRF] notes, elaboration and engine warnings, device-worker logs.

That split is what makes the output pipeable while the terminal still shows a long run moving:

$ circuitrf lp hero3.cnl > table.txt

table.txt gets the loadpull table and nothing else; the per-drive-step [LP] lines and the convergence notes still scroll past on screen. Redirect 2&gt;/dev/null to silence them, or 2&gt;run.log to keep them.

Options every verb takes

Option What it does
--kits <dir> A folder of installed kits, so an externally-supplied device model (ExtDevice Provider=…) resolves headlessly the way opening a workspace resolves it in the GUI. Repeatable.
--json Put one JSON document on stdout and nothing else — see below. stderr is untouched.
--only a,b Narrow that document's result to these cubes.
--group g,h Narrow that document's result to these groups.
--at axis=value Narrow it to one point of an axis — --at freq=2GHz. The nearest grid point, and the document says which one it gave you.
--interp Make every --at interpolate between the two bracketing points instead. Never the default: it returns a number the run did not compute, and the document says so.
--range axis=lo:hi Keep a band of an axis — --range freq=1GHz:3GHz.
--result full\|summary summary returns the result's shape — group and cube names, units, axis lengths and extents — and no values at all.
--summary Report the informational notes as counts by severity instead of in full. Warnings and errors always travel in full, and stderr is untouched.

Frequencies are written as 1GHz, 100MHz, or bare Hz (1e9) anywhere a frequency is accepted.

Ask for the part you want, not the whole result

--only and --group narrow by cube name, which does nothing when the result has one cube. A 551-point two-port S-parameter run is about 173 kB of JSON; if the question is "what is S21 at 2 GHz", --at freq=2GHz --only S is a few hundred bytes. Every value carries its own unit — a bare 2 could be 2 Hz or 2 GHz — and an axis name nothing in the result has is refused, listing the ones that exist, rather than quietly handing you everything.

Every run returns result.shape whether or not it returns the values, so --result summary is how you find out what a run produced before deciding what to ask for.

An option a verb does not take is refused, never ignored

Every verb stops with unknown option '…' and exit 1 rather than dropping a flag it does not recognise. This matters more than it sounds: most verbs find their input file as the first argument that is not an option, so a silently dropped flag's value would be read as the file name — and a flag that carries an override, like --set, would simply not be applied, giving you a run that answers a different question with nothing to say so.


sparam — S-parameters

$ circuitrf sparam <file.cnl|.csch> [--freq start:stop:step] [-o out.sNp]
$ circuitrf sparam hero1.cnl --freq 1GHz:3GHz:1GHz -o hero1.s2p
S-parameter analysis: 3 points, 1–3 GHz
Wrote hero1.s2p
Option What it does
--freq start:stop:step Override the sweep. Omit it and the netlist's own sparam analysis is used, segments and all — which is almost always what you want, because it is the sweep the design was set up with.
-o, --output <path> Where the result goes, and its extension picks the format: .s1p….s99p for a Touchstone, or .npy / .mat / .txt for the cubes. Omitted, it is the input file with its extension changed to .sNp for the port count found.

There is no stdout table. The port count in the default extension comes from the network, so a circuit that grew a port writes .s3p without you editing the command. An extension naming no format this verb writes is refused, listing the ones it does — you never get a Touchstone under a name that says otherwise.

A run carrying WSProbes prints one line per probe after the S summary, and a run with NDF=yes on its directive prints the right-half-plane pole count:

$ circuitrf sparam amp.cnl
S-parameter analysis 'SP1': 2001 points, 0.5-3 GHz (1 segment(s))
NDF: 2 right-half-plane pole(s)  (net clockwise encirclement 1.989; NDF(100 GHz)=1 ∠ 1.8)
WSProbe P idx=1  H0(0.5 GHz)=11.597 ∠ -1.4  ZG(0.5 GHz)=10.482 ∠ 17.4
                 SM_Y0 min -18.1 dB @ 1.59125 GHz  SM_H0 min -19.8 dB @ 1.73375 GHz

--json carries the same under wsprobes and ndf. The margins are linear there and dB on the line, because dB is a display convention and a document should carry the number. A circuit that has WSProbes and no ports at all is a legitimate run — it writes every wsp cube and no S — and asking it for a Touchstone is refused, naming the spellings that do carry the result.

Ports with different reference impedances

A Touchstone file declares one reference impedance, and circuitRF writes port 1's on the option line. When the ports differ, the file also carries a header note listing each port's own impedance and saying that the data is referenced to those — and circuitRF reads that note back, so circuitrf read on the file reports the real per-port references rather than the option line repeated. Nothing is renormalized: the numbers are the ones the solve produced. If you want the per-port references in a form every tool reads, write .npy instead.

dc — the operating point

$ circuitrf dc <file.cnl|.csch>
$ circuitrf dc hero2.cnl
DC: converged in 3 iteration(s), residual 7.27E-16
Node voltages:
  0                                         0
  n_src                                     0
  n_gate                                -3.05
  n_drain                                  48

No options beyond the common ones. It prints the converged node voltages and any probe currents, and exits 2 if the solve did not converge — the operating point is the one thing every nonlinear analysis is built on, so a non-converged DC is a failed run, not a partial one.

hb — harmonic balance

$ circuitrf hb <file.cnl|.csch> [-a name] [--set var=expr] [-o out.npy]

The same verb runs single- and multi-tone — which it is comes from the netlist's directive, not from a flag.

$ circuitrf hb hero2.cnl --rows 6
HB 'HB1': f0=2 GHz, MaxHarm=4, tol=1E-06
Analysis: HB1   (hero2.cnl)
  Converged: yes (1 solve(s))
  Residual:  1.24E-09 (worst)
  Tones:     2 GHz
  V  [node:7 x harmonic:5]  (mag ∠deg)
                        0                     1                     2
    n_gate              3.05 ∠  180.0         0.029814 ∠    0.1     0
    n_drain             48 ∠    0.0           0.15004 ∠ -172.4      1.6824E-05 ∠ -179.8
    … 1 more row(s) — use --all or --rows N
Option What it does
-a, --analysis <name> Which analysis to run. Optional when the file declares one HB chain.
--set <var=expr> Override a global variable before elaboration. Repeatable.
--maxharm K Override MaxHarm.
--maxmix M Override MaxMixOrder (multi-tone only).
--tol t, --max-iter N Override the convergence tolerance and the iteration cap.
--rows N, --all How much of each printed table to show. Default is a truncated head.
--diag Engine convergence diagnostics, on stderr.
-o, --export <path> Export the results. The extension picks the format: .mat, .npy or .txt.

--set overrides the VARIABLE, not the number

--set Pavl_dbm=0 replaces the global variable in the test bench's own scope, then elaborates. So every expression derived from it re-derives — a bias that was written Vg = Vth + 0.2 follows a changed Vth, and a sweep computed from the variable sweeps the new values.

An override pushed at the engine instead would move one number and leave everything computed from it stale, which is why there is no such option.

Name the wrapper, or name nothing

When a parametric sweep wraps an analysis, the sweep is what runs. Naming the inner analysis with -a is promoted to its outermost enabled wrapper, and the promotion is announced:

[circuitRF] 'HB1' is the inner analysis of 'SW1' — running 'SW1' so the sweep axis is not lost.
Why it is promoted rather than obeyed

Running the inner analysis alone produces a converged, plausible, complete-looking result at one operating point — with the sweep axis silently missing. Nothing about it looks wrong. A frequency-swept loadpull has exactly this shape, which is why the rule is the same for every verb rather than something harmonic balance does on its own.

If more than one runnable chain exists, all their names are printed and the first runs; if none does, the message says whether the netlist declares no such analysis or declares one that is disabled.

Measurements

The measure lines on the test bench are evaluated exactly as the GUI evaluates them, and the results join the exported DataSet as named cubes. A measurement that fails to evaluate is reported on stderr and the run continues — one bad expression does not throw away a run that took minutes:

[circuitRF] measurement: Measurement 'Gain_dB': failed to evaluate 'Pout_dBm - Pavl_dbm':
                         Unresolved name 'Pout_dBm' in scope 'measurements'

lp — loadpull

$ circuitrf lp <file.cnl|.csch> [--grid grid.gam] [--pin start:step:max] [-o out.spl]

lp sweeps the load (or source) termination over the directive's Γ grid, runs a harmonic-balance drive ladder at each point, and reports the figures of merit.

$ circuitrf lp hero3.cnl --rows 8
Analysis: LP1   (hero3.cnl)
  Grid: 20 point(s) — 0 reached compression, 20 stopped at max drive
  Nothing reached compression — raise --pin's max (or the directive's PinMax).

      #  GammaLoad           ZLoad (ohm)           stop              Pavl     Pout      Gt     DE%    PAE%
      0  0.0000 ∠    0.0     50.00+j0.00           max drive        10.00    20.54   10.54    3.35    3.09
      1  0.2000 ∠    0.0     75.00+j0.00           max drive        10.00    22.31   12.31    5.03    4.76
      2  0.2000 ∠   90.0     46.15+j19.23          max drive        10.00    20.19   10.19    3.09    2.83
    … 12 more point(s) — use --all or --rows N
Option What it does
-a, --analysis <name> Which loadpull analysis to run.
--set <var=expr> Override a global variable before elaboration. Repeatable.
--grid <file.gam> Override the Γ grid the directive reads. Resolved against your working directory, not the netlist's.
--pin start:step:max Override the drive ladder, in dBm.
--compression dB Override the compression target.
--maxharm K, --tol t, --max-iter N Override the inner HB settings.
--rows N, --all --all dumps every cube instead of the summary table.
--diag Engine diagnostics, on stderr.
-o, --export <path> .mat, .npy, .txt — or .spl / .lpcwave, the loadpull interchange formats.

One row per Γ point, at the point that answers the question

A loadpull's raw cubes are [gridPoint × driveStep] — a 61-point grid driven up in 1 dB steps is a 61 × 30 table per figure of merit, and eight of those scroll a terminal without answering anything.

So the default table is one row per Γ grid point: where it was, how it stopped, and its FOMs at the last converged, non-tickle drive step — the compression point where the point compressed, the highest drive it managed otherwise. Reading a fixed drive index instead would mix compressed and uncompressed points in one column. --all still dumps everything.

A swept run prints one table per sweep point.

.spl and .lpcwave

-o out.spl writes the loadpull interchange format the Data Display reads back as a measured surface, so a headless run can produce a file the GUI opens. lp also runs the same post-processor a GUI run does, so the exported cubes carry the derived display metrics (Pout_dBm, Zin, IRL_dB, AMPM_deg) — a .npy written here and one written by the GUI carry the same cubes.

lpp — loadpull pursuit

$ circuitrf lpp <file.cnl|.csch> [--out-grid found.gam] [-o out.npy]

A pursuit searches for the max-power (MXP) and max-efficiency (MXE) terminations rather than reading a grid, then runs a follow-on loadpull over the terminations it recommends.

$ circuitrf lpp hero3B_at_compression.cnl
Analysis: LP1   (hero3B_at_compression.cnl)
  Pursuit optima:
  MXP (max power)            converged   Pout=40.625 dBm   Zload=80.48+j0.00   Zsource=50.00+j0.00
  MXE (max efficiency)       converged   Eff=69.617 %   Zload=140.31-j4.95   Zsource=50.00+j0.00
  21 termination(s) queried, 45 recommended termination(s)

  Grid: 45 point(s) — 45 reached compression

      #  GammaLoad           ZLoad (ohm)           stop              Pavl     Pout      Gt     DE%    PAE%
      0  0.2690 ∠    7.0     86.15+j6.11           compressed       26.00    40.56   14.56   67.08   64.74
      1  0.2030 ∠    9.6     74.80+j5.30           compressed       27.00    40.68   13.68   63.55   60.82

lpp takes every lp option except --grid, and adds --out-grid:

Option What it does
--out-grid <file.gam> Where the terminations the pursuit found are written, as a .gam you can feed back to lp. Resolved against your working directory.
The two grid options are refused, not ignored

--grid on lpp and --out-grid on lp each stop the run with a sentence naming the verb that owns them. A grid option silently doing nothing would be a run that answered a different question and said nothing about it.

A non-converged optimum is still printed, with its status. The engine publishes the last termination it looked at, and printing nothing there reads as "the search found nothing" when what actually happened is "nothing it tried reached compression".


em — electromagnetic extraction

$ circuitrf em <setup.cem> [-o out.sNp] [--workspace file.cws]

em is the only verb that does not take a .cnl. It takes a .cem EM setup — the document the EM Setup panel edits — and runs it: extracts the geometry from the layout the setup names, resolves the stackup, meshes, solves the frequency plan, de-embeds, and writes the results.

It needs no other arguments. Everything else it needs is already recorded in the files.

What an EM run takes

Four files, and three of them are things you already have if you have drawn a layout:

File What it supplies Where it comes from
.cem The setup: which layout, which analysis, the frequency plan, port impedances and types, mesh settings, solver switches File ▸ New ▸ EM Setup…, or the layout editor's EM button
.clay The artwork — the metal, and the port labels for a full-wave run The layout editor
.ctech The stackup: layer thicknesses, ε_r, tanδ, conductivity, which conductor is ground, and which drawing layers map onto what The technology editor, or one of the shipped starter technologies
.cws The workspace marker, carrying DefaultTechRef — the technology a layout uses when it does not name one itself Created with the workspace
Author the setup in the GUI; run it from the command line

The em verb runs a setup — it does not create or edit one, and it will not repair one. A setup with no ports, no technology or no signal conductor is refused with the sentence explaining what is missing. Build the .cem once in the EM Setup panel, where every control tells you as you type whether the run is blocked and why, then commit it beside the layout and run it headlessly from then on.

Both file references resolve by walking UP, and neither is a flag

A .cem names a layout; the layout names — or inherits — a technology. Neither reference is stored absolutely, and neither needs an argument:

The two walks start from different files, and that is deliberate. A .cem in one workspace may point at a layout in another, and that layout's layers have to be read by its technology, not by whichever workspace the setup happened to live in.

--workspace <file.cws> overrides the first walk, for a .cem being run from outside its own tree. It is never required.

The three resolutions are echoed on stderr before anything expensive starts, so you can see what the run is actually about to read:

[circuitRF] workspace: /work/amp/.cws
[circuitRF] layout: /work/amp/Line/layout/Line.clay
[circuitRF] technology: /work/amp/pcb.ctech

A worked example, from an empty folder

Here is a complete, minimal EM workspace — a single 20 mm × 2.9 mm microstrip line on a two-layer PCB technology, swept 1–10 GHz in 3 points. Four files:

amp/
├─ .cws                        the workspace marker, naming the default technology
├─ pcb.ctech                   the stackup
├─ line.cem                    the EM setup
└─ Line/
   └─ layout/
      └─ Line.clay             the artwork

The .cem is JSON, and this is all of it — every field not written takes its documented default:

{
  "FormatVersion": 1,
  "Name": "line",
  "LayoutRef": "Line/layout/Line.clay",
  "Frequency": {
    "StartExpr": "1", "StopExpr": "10", "NumPoints": 3,
    "Mode": "PointCount", "Kind": "Linear",
    "StartUnit": "GHz", "StopUnit": "GHz"
  },
  "Port1Z0Real": 50, "Port2Z0Real": 50
}

LayoutRef is workspace-relative — relative to the directory holding .cws, not to the .cem. The .cws supplies the technology:

{ "DefaultTechRef": "pcb.ctech" }

Nothing in the .cem names a technology, a kernel, a mesh or a port. The technology is inherited, the kernel is chosen from the geometry, the mesh settings are the engine's own defaults, and this structure's ports are the two ends of a uniform line by construction. Then:

$ circuitrf em amp/line.cem
[circuitRF] workspace: amp/.cws
[circuitRF] layout: amp/Line/layout/Line.clay
[circuitRF] technology: amp/pcb.ctech
[0] solving the cross-section
[3] solving the cross-section
note: Automatic chose "Uniform transmission line": this geometry is a uniform cross-section, which
      that analysis solves exactly and is about a thousand times cheaper than "Full-wave planar".
      Set Analysis to "Full-wave planar" if you want the full-wave answer anyway.
note: Dielectric interfaces truncated 20 substrate heights (32000 µm) beyond the outermost conductor
      on each side.
EM setup:  line
Kernel:    Quasi-static cross-section (CrossSection)
Points:    3
Wrote amp/results/line.s2p
Wrote amp/results/line_em.npy

Everything from EM setup: down is on stdout; the resolution lines, the progress and the notes are on stderr.

A full-wave run differs only in what the files say, not in how you invoke it: draw port labels in the layout with the layout editor's Port tool, set the setup's analysis to Planar (or leave it Auto and let the geometry decide), and run exactly the same command. It will take very much longer — a de-embedded full-wave point costs tens of seconds at the shipping mesh — which is why the progress lines exist.

Where the results go, and what -o moves

With no -o, the run writes exactly where the Simulate button writes: into the workspace's results/ folder. Two files come out, and they are not redundant:

File Holds
<name>.sNp S-parameters only — the artefact a schematic's SnP component references by path
<name>_em.npy The whole DataSet, including the per-kernel diagnostics group — Z_c, γ, ε_eff, RLGC for the cross-section kernel; the calibration residual and usability flags for the full-wave one
Why the default path is not the CLI's to choose

That results path is predictable by design, so a schematic's SnP reference stays valid across re-runs. A headless run that minted its own file name would orphan every one of them — so circuitrf em writes the same file Simulate does, and the acceptance test for the verb compares the two Touchstones byte for byte.

-o moves the Touchstone only. The .npy stays where it was, because it is the diagnostics record of the run rather than the deliverable:

$ circuitrf em amp/line.cem -o /tmp/mine.s2p
Wrote /tmp/mine.s2p
Wrote amp/results/line_em.npy

You do not have to get the extension right — the port count decides it, so a .s2p you typed for a structure that turned out to have four ports is written .s4p.

With no workspace above the .cem, results/ is created beside the .cem itself.

note, warning, error — three lists, kept apart

An EM run has three different things to say and they ask three different things of you, so they are printed under three labels rather than flattened into one stream:

Prefix Means
note: The run explaining itself — which kernel it chose and why, the mesh's own sentences, RLGC, the ports it found. Read these; they are the cheapest check that the tool is looking at the structure you think it is.
warning: Something to act on — a stale .sNp about to be replaced, a technology that resolved but failed validation.
error: Something you asked for and did not get — a results file that could not be written.

A refusal is a result

The EM engine declines geometry it cannot solve correctly rather than returning a plausible number. Each refusal carries a written explanation of what is wrong with this setup, and em prints that explanation rather than collapsing it into "EM failed":

[circuitRF] workspace: amp/.cws
warning: Layout file not found: amp/Line/layout/Missing.clay
No layout: The layout 'Line/layout/Missing.clay' could not be found, so there is no geometry to
analyse. Point this EM setup at a layout that exists.
Status Means Exit
Refused The extractor or the kernel declined this geometry — see what the engine refuses 1
No layout The layout reference did not resolve 1
Engine error The solve failed 1
Cancelled Stopped at a work boundary 130

rail — power integrity, headless

$ circuitrf rail <board.crail> [--rail NAME] [--accurate] [-o out.{csv,npy,mat,txt,svg,pdf}]

rail runs a .crail — the document the railRF window edits. It resolves the artwork and the stackup, extracts the copper, solves the rail at DC, checks the vias and prints the answer. Every number comes out of the same call the window's Run button makes, and every pixel of an .svg or .pdf report out of the same renderer the window draws with, so a report produced on a build machine is the one you would have got by pressing the button.

Omitting --rail runs them all, in dependency order — the same shape hb and lp have for a wrapped sweep, and for the same reason: a downstream rail solved on its own would start its source from a nominal instead of from the upstream answer.

It can also take the .clay, the cell folder or the workspace the .crail sits in, and find it.

What it reads, and what walks up to it

Like em, the references are walk-ups rather than flags. The .crail names its artwork; the artwork names — or inherits from its workspace — a technology.

File What it supplies
.crail The rails, their references and extents, the sources, the loads and their currents, the parts, the targets, the band and the aggressors
.clay (or a Gerber set) The copper being measured
.ctech The stackup: conductor thicknesses and conductivities, the dielectrics between them, and the via entry's plated-wall thickness
.crlib The part library the decoupling resolves against

Options

Option Meaning
--rail <name> Which rail. Omitting it runs every one.
--fast (default) / --accurate The two readings of the copper. Fast is the default, as in the window.
--source REFDES.PIN=<model> Repeatable. 3.7V,50mOhm,10nH — any subset, in any order, each field identified by its unit or by a v=/r=/l= key — or a Touchstone file. A row for the same anchor is replaced, not added beside it.
--load REFDES.PIN[=<current>] Repeatable. The current may be left out: that makes it an observation port.
--target-drop, --target-z, --mask [PORT=]<file> The target forms. A mask is per observation port; one that lands on no port is refused, because a mask nobody applied reads on the report exactly like one that was honoured.
--aggressor NAME=<freq>[xN] Repeatable. x and × both spell the harmonic count.
--reference <layer>, --extent as-imported\|filled\|infinite The return conductor, and how far it is taken to extend.
--rows N, --all How much of the ranked breakdown to print.
-o out.… .csv for the tables, .npy/.mat/.txt through the usual exporter, .svg/.pdf for the report page.

An anchor is REFDES, REFDES.PIN, or @x,y in DBU. Headless it is always the coordinate form: a .crail names no placement file, so there is nothing to resolve a refdes against — see what is not wired up yet.

Values carry units, through the same table the expression engine uses, so a spelling that works in a .cnl works here. A bare number is base SI, which is what every number in a .crail already is.

What is refused, and what is not

Unstated Answer
The reference layer Refused, naming --reference. railRF never infers one.
The technology Refused. Copper priced with no thickness and no conductivity produces numbers that look exactly like numbers with physics behind them.
The Excellon coordinate format, on a Gerber import Refused — the same sentence convert gives.
The via plating thickness Not refused. It is a setting, and every flag says which basis produced its limit.
A load's current Not refused. It is an observation port, it contributes nothing to the DC solve, and the report lists it as observed. Refusing it — or defaulting it to zero — would make not added and added with no current indistinguishable.

-o out.sNp is refused: Z(f) at the observation ports is the frequency answer, this verb answers DC, and a Touchstone holding the DC point repeated would look like a measurement. --set is refused too, naming the flags that do state those quantities: a .crail declares no variables, so a --set accepted here would be silently dropped.

Every export says which model produced it

A file read six months later has no status strip beside it, so every format carries the model (Fast or Accuracy), the reference extent, the copper temperature, how many parts are modelled from a file, how many have no bias curve, and whether any ESR fell back to a class default — which makes a derived peak height indicative rather than measured.

lvs — does the artwork implement the drawing?

$ circuitrf lvs <path> [--no-reduce] [--flat] [--flatten-cell NAME] [--testbench]
  [--recognize] [--set var=expr] [--severity warning|error] [-o report.txt]

lvs compares a cell's layout against its schematic and reports every device, net, terminal and value the two disagree about. It answers the one question a headless client cannot answer any other way: the design was drawn twice, and do the two drawings say the same thing? An agent that authored a .clay cannot look at the screen.

Every finding comes out of the same call the LVS panel's Compare button makes, so a design that passes on a build machine passes when somebody opens it.

The path may be a cell folder (the default unit — its primary schematic against its primary layout), a workspace (every cell holding both views), a .clay or a .csch (each finds its sibling in the cell folder that holds it). A cell holding only one of the two views is reported and skipped, not failed: that is the ordinary state of a design being drawn.

Option Meaning
--no-reduce Compare object for object. By default parallel and series R/C/L collapse first, and every finding un-reduces to the objects you drew.
--flat / --flatten-cell <name> Flatten the whole hierarchy, or one named sub-cell (repeatable). Every use is reported, so a design that quietly flattens everything is visible.
--testbench Compare a bench as drawn rather than the cell it instantiates.
--recognize Also read devices out of bare copper, through the technology's own DeviceRules deck. Off by default, and off for a process that declares no rules.
--set var=expr Set a global before the schematic elaborates, exactly as a run verb does.
--severity warning\|error What makes the exit code non-zero. Default error.
-o report.txt The only thing this verb ever writes. With no -o it writes nothing at all, so it runs on a read-only tree and on a workspace another process has open.

Exit 0 when nothing at or above --severity was found, 1 otherwise, 130 on a cancellation. A run holding warnings and no errors exits 0 and still reports every one.

With --json, every finding travels with a stable lvs. id and typed arguments — the layer, the coordinate, the two values, the marker box — so a caller reads what it needs without parsing the sentence apart. It honours waivers and never creates one: waiving is a reasoned act with a sentence attached and belongs beside the thing being waived.

It is not folded into check, deliberately. check has to stay cheap enough to call after every edit; an LVS on a real board is seconds rather than milliseconds, and a check that had become slow is a check people stop running. What check does carry is the terminal-map validation, which is cheap and static.

See the LVS chapter for what the findings mean and a worked example on the shipped example workspace.

smith — a matching network, headless

$ circuitrf smith <match.csmith> [--at <freq>] [-o out.{s1p,svg,pdf,png}]

smith evaluates a .csmith — the document the Smith Chart window edits. It walks the cascade from the generator to the load at the design frequency, prints what the window's status strip states, and then prints the walk one node at a time.

That table is the reason to run this rather than open the window. The reading answers is it matched; the table answers where did it stop being matched, which is the question you have when the answer is no — and it is what you diff between two revisions of a network.

$ circuitrf smith Lmatch.csmith
L-match demo — Smith chart, Z0 50 Ω
  design frequency   2 GHz
  generator          10 − j10
  load               17.81 + j20.38
  Γ                  0.538 ∠131°
  VSWR               3.329
  mismatch           1.48 dB

  node  element                          Z (Ω)                    Γ
     0  (generator)                       10 − j10                0.6778 ∠-157°
     1  L1                                10 + j17.65             0.6991 ∠140°
     2  C1                                17.81 + j20.38          0.538 ∠131°

Where it evaluates

--at moves the design frequency for this run only — the document is not touched. Write the unit and it is honoured (2.4 GHz, 900 MHz, 1.9e9); a bare number is hertz.

Outside the generator table's span it refuses, with the span in the sentence. The generator impedance is interpolated between the table's rows and never extrapolated past them, so a frequency the table cannot answer for has no answer at all. The one exception is a single-row table: one row is one impedance, flat, and every frequency is legal against it.

There is no --sweep. The swept band is always walked, across the generator table's own span, so there is nothing to switch on — and a single-row table has no band, because one row is one impedance and a band needs two ends.

What it writes

-o You get
out.s1p The load reflection coefficient as Touchstone: the whole band, or one point at the design frequency when the generator table states a single frequency and there is no band. Referenced to the chart's own Z₀.
out.svg, out.pdf, out.png The chart — the trajectories, the constant-Q arcs, the swept band, the load points and their frequency labels, the conjugate-match targets, your overlays and your markers. The same picture the window's Copy chart puts on the clipboard.

.s2p and the rest are refused: what this verb has is the load, which is one reflection coefficient, and a two-port built from it would be three quarters invented.

The picture takes the same options plot takes — --size, --scale/--dpi, --background, --dark.

What it will not guess

--set is refused. A .csmith states every element value as a number, with no expressions and no variables, so there is nothing for an override to replace — and a flag accepted and then dropped means the run answered a different question than the one you asked. --at moves the design frequency; anything else is a change to the document, which you make by writing it.

An overlay that does not resolve is a warning and not a refusal: reference material that is missing must not take the work down with it, so the chart still draws everything that did resolve and the warning names what did not.

convert — layout interchange

$ circuitrf convert <input> -o <output> [options]

Reads a layout in any format circuitRF understands and writes it in any other. It is the same reader and the same writer File ▸ Import and File ▸ Export run — see Interchange for what each format can and cannot carry — so a conversion here and the same conversion through the GUI produce the same bytes.

Format Named by As input As output
circuitRF layout .clay the file a folder of cells plus a .ctech
GDSII .gds, .gdsii, .gds2 ✓ ✓
DXF .dxf ✓ ✓
Gerber + Excellon a folder, or one Gerber/drill file ✓ a folder
Board .kicad_pcb ✓ ✓

Every ordered pair works — DXF to Gerber, Gerber to board, GDSII to DXF, board to GDSII, and the rest. There is no privileged direction and no hub format you have to route through by hand: a conversion is an import followed by an export, and convert does both.

Formats are read off the paths. A folder means Gerber; a file with no telling extension is classified by its content, through the same classifier the Gerber import uses. --from and --to override that, and --to is required when the output is a folder, since a folder could be either Gerber or .clay.

Examples

Board file out to a fab house as artwork plus drill:

$ circuitrf convert board.kicad_pcb -o fab/ --to gerber

A folder of Gerbers back to a board file:

$ circuitrf convert fab/ -o recovered.kicad_pcb

A mechanical drawing straight to artwork — no board tool in the middle:

$ circuitrf convert outline.dxf -o gerbers/ --to gerber

A mask set to a drawing your mechanical engineer can open, at the DXF version their tool wants:

$ circuitrf convert mmic.gds -o mmic.dxf --dxf-version AC1015

Bring a board in as editable circuitRF cells and keep the technology it declared:

$ circuitrf convert board.kicad_pcb -o cells/ --to clay
A clay target is a folder, not a file

An import writes one cell folder per structure the source holds, plus the technology beside them — so -o cells/ is the shape, and -o cells/board.clay is refused with the folder spelling in the message. Point it at a folder and look inside: the .clay is at cells/<Cell>/layout/<Cell>.clay, which is where every other circuitRF tool expects a layout view to be.

One cell out of a GDSII library that holds many:

$ circuitrf convert lib.gds --list-cells
$ circuitrf convert lib.gds -o coupler.dxf --cell COUPLER

Convert a directory of drawings in one line:

$ for f in dxf/*.dxf; do circuitrf convert "$f" -o "gds/$(basename "${f%.dxf}").gds"; done

Options

Option What it does
-o, --output <path> The file to write — or the folder, for gerber and clay; a file-shaped path there is refused. Required.
--from <fmt>, --to <fmt> clay, gdsii, dxf, gerber, board. Say it when the path does not.
--cell <name> Which cell to export, when the source holds several.
--list-cells Report what the input holds and write nothing.
--name <stem> What to call the written Gerber file set. Default: the cell's name.
--tech <file.ctech> The technology to convert against, instead of the one the layout resolves.
--workspace <file.cws> The workspace a .clay's references resolve against. Default: the nearest one above it.
--keep-cells <dir> Keep the cells the import produced instead of discarding them.
--dbu <n> Database units per micron for an imported design. Default 1000 — one DBU is one nanometre.
--dxf-version <v> AC1015 (R2000), AC1018 (R2004), AC1032 (R2018, the default).
--dxf-units <n> The $INSUNITS value for a DXF that declares none.
--drill-units <mm or inch> Excellon coordinate units, when the file does not say. Applies to every drill file in the set.
--drill-format <int>:<dec> Excellon digit counts, e.g. 2:4. Applies to every drill file in the set.
--drill-zeros <leading or trailing> Excellon zero suppression. Applies to every drill file in the set.
--accept-inferred-drill-format Take each drill file's own inference rather than refusing.
--open-archives Look inside an archive when a Gerber folder holds no artwork of its own. It is unpacked to a temporary folder, imported from there, and deleted again; nothing is added to the folder you named. Without this flag, such a folder is a refusal that names the flag.

Which cell gets exported

A GDSII library, a DXF drawing and a board file can all hold more than one cell, and an export writes one design. Unless --cell says otherwise, convert takes the source's own idea of the top: the GDSII structure nothing else instances, DXF's model space (the drawing itself, not a BLOCK definition), the board rather than one of its footprints. A Gerber set is always one flat cell. When the source genuinely has no unambiguous top, the conversion stops and tells you to name one — --list-cells prints the choices.

The technology, and why it matters here

An import brings a layer table with it, and in the GUI those layers land on the technology your workspace already has open. Headless there is no open workspace, so convert writes a .ctech of its own from what the file declared, exactly as File ▸ Import ▸ Gerber does. That is what keeps layer names, colours and Gerber file suffixes alive across a conversion instead of leaving every layer a bare number.

Two consequences worth knowing:

GDSII is the one exception, and it is the format's own doing. GDSII identifies a layer by a number, not a name, so an import has nothing to name it with: the numbers come through exactly, the names do not. Convert from GDSII with --tech pointing at the technology those numbers belong to and the names come back.

When it refuses

A drill file that does not state its format is a refusal, not a guess

Many Excellon files do not say whether their coordinates are inches or millimetres, or whether leading or trailing zeros are suppressed — and leading versus trailing differ by four orders of magnitude on identical text. The GUI asks you. There is nobody to ask here, so the conversion stops, prints what it inferred and the evidence behind it — including whether the holes land inside the artwork's own outline — and names the flags that answer it. Accept the inference with --accept-inferred-drill-format, or state it outright with --drill-units, --drill-format and --drill-zeros.

A --drill-* flag settles the whole set, not the first file. A drill flag is a statement about the run — one exporter wrote the .drl and the .rou next to it in one format — so it applies to every drill file the conversion reads, and the refusal is printed once rather than once per file. --accept-inferred-drill-format works the same way, with one difference worth knowing: it accepts each file's own inference rather than forcing the first file's format onto the rest.

$ circuitrf convert fab/ -o board.kicad_pcb --drill-units mm --drill-format 3:4 --drill-zeros leading

Reach for the flags less often than you might expect: a file that writes every coordinate at its full width — same number of digits throughout, leading zeros intact — states its own format by doing so, and the conversion reads it off the coordinates and says as much. The flags are for the files that leave a genuine question, and the note printed for every drill file names which parts of its format were declared, which were inferred, and from what.

It also stops, rather than guessing, when a design instantiates cells drawn against a different technology and the layer mapping needs confirming; when coordinates overflow GDSII's 32-bit range; and when the source holds several cells and none of them is an unambiguous top. Every refusal exits 1 and writes nothing at all.

Everything short of a refusal is a note on stderr, counted and named: labels flattened to geometry, curves turned into polygons, holes keyholed, bitmaps dropped, unresolved instance references, layers with no mapping in the target format. stdout carries only the paths written, one per line, so a script can consume them directly:

$ circuitrf convert board.kicad_pcb -o fab/ --to gerber 2> convert.log | zip -j fab.zip -@

new — a workspace or a cell

$ circuitrf new workspace <dir> [--name N] [--tech <id>|none]
$ circuitrf new cell <workspace> <cellName> [--views schematic,symbol,layout]

These create the first correct document — the thing that is awkward to write by hand because the folder structure and the primacy files have to be right before anything will open it.

They are not a second implementation. new workspace calls the same function File ▸ New Workspace calls, and new cell the same one New Cell calls, so a tree created here is a tree the application created.

$ circuitrf new workspace ~/designs/Amp --tech pcb-4layer_FR-4_62mil_1oz
/home/you/designs/Amp
/home/you/designs/Amp/.cws
/home/you/designs/Amp/tech/pcb-4layer_FR-4_62mil_1oz.ctech

$ circuitrf new cell ~/designs/Amp Stage1 --views schematic,symbol
/home/you/designs/Amp/Stage1
/home/you/designs/Amp/Stage1/schematic/Stage1.csch
/home/you/designs/Amp/Stage1/symbol/Stage1.csym

The paths it creates are the result, on stdout, because what you do next is almost always read or rewrite one of them.

Every default is the dialog's

Whatever the GUI's dialog pre-selects, the verb selects with no flag: --tech opens on the same technology the New Workspace combo box opens on (--tech none is its "None" row), and --views defaults to schematic, which is what New Cell creates. Anything the dialog would have asked is a refusal that names the flag answering it — never a guess.

There are deliberately no per-primitive edit verbs. There is no place-instance and no set-parameter: once a document exists, the way to change it is to write it. Every format circuitRF owns is readable, versioned JSON — see File formats — and that file is the interface.

`new` is one verb with a noun

new workspace and new cell are two nouns of one verb, not two verbs. It reads better and, more to the point, the number of top-level verbs is a cost every reader of --help pays.


import part — a footprint and its symbol

$ circuitrf import part <file-or-folder> --into <workspace> [--cell N] [--variant V]
                                            [--list-parts] [--tech f.ctech] [--add-layers]

The same code Import Component runs: it reads a downloaded component — a land pattern, a symbol, and the pin-to-pad map that joins them — and writes it into your workspace as one cell.

$ circuitrf import part downloads/SOT-23.zip --into ~/designs/Amp --list-parts
SOT-23-3
SOT-23-5

$ circuitrf import part downloads/SOT-23.zip --into ~/designs/Amp --cell SOT-23-3
/home/you/designs/Amp/SOT-23-3
/home/you/designs/Amp/SOT-23-3/layout/SOT-23-3.clay
/home/you/designs/Amp/SOT-23-3/symbol/SOT-23-3.csym

A source holding several parts is refused with them listed, never resolved by taking the first.

Layers the technology does not have are reported, and nothing is written, unless you pass --add-layers. The GUI's own install is session-only and writes nothing to disk either, so this is the same behaviour and not a headless restriction.


render — a picture of a document

$ circuitrf render <path> -o <out.svg|.pdf|.png> [options]

Turns a schematic, a symbol, a layout or a data display into a file you can look at, put in a report, or diff between two commits. Every pixel comes out of the renderers the editors draw each frame with — the same Skia that produces the picture on the canvas — so the file is what the window would have shown, not an approximation of it.

One verb over every document kind, inferred from the path exactly as check and explain infer it. There is no render-schematic.

Path Resolved by
a .csch, .csym, .clay or .cdd directly — including one in a folder with no workspace above it
a cell folder --view, or the one view it holds. More than one is a refusal listing them
a workspace --cell <name> — rendering "the workspace" is not a picture of anything
$ circuitrf render ~/designs/Amp/Stage1/layout/Stage1.clay -o stage1.png
Wrote stage1.png (1600x1200 device-pixels, 11,540 bytes)
  4 of 4 shape(s) drawn, 12 vertices emitted, 7 draw call(s)

-o is required and its extension picks the format — .svg, .pdf or .png; --format overrides it. There is no picture on stdout: stdout is the result, and --json has to be able to co-exist with the write.

A document with no workspace above it renders

It resolves no technology, draws on the generated fallback palette exactly as the layout editor does with an unresolved technology, and says so as a note — not a warning and never a refusal. A .clay a converter just handed you has no workspace by construction, and treating that as a problem would teach you to skip the warnings that are problems.

The viewport, and the unit rule

$ circuitrf render <path> -o out.png --fit [--margin 0.10]
$ circuitrf render <path> -o out.png --window x0,y0,x1,y1
$ circuitrf render <path> -o out.png --center x,y --span w

The three are refused together rather than ordered. A precedence nobody stated is an invention, so --fit --window … stops rather than quietly picking one.

On a layout, every coordinate carries a unit — zero included

--window 0,0,500,300 is refused. It could mean database units, micrometres or millimetres, and those are three pictures six orders of magnitude apart — all of which come back looking entirely reasonable. Write --window 0um,0um,500um,300um; the suffixes are nm, um (or µm, or u), mm, mil and in. The refusal prints the same number spelled the two ways you most plausibly meant, plus the document's own display unit when it is neither.

A schematic or a symbol takes bare numbers, because its coordinates really are dimensionless design units — and --json says "unit": "design-units" rather than leaving you to assume metres.

You do not have to write one from scratch. explain --extents prints a ready-made --window line for the whole document and for each layer, in exactly this spelling. Copy it; there is no conversion to get wrong.

A window of a different shape from the page is letterboxed, never cropped and never stretched. You get the whole region you asked for, with bars; the resolved window is in the --json document with letterboxed beside it. Silently giving you less of a region than you asked for is not something you could notice from the picture.

--fit frames the box that gets painted, not the box that is stored. A label's stored extent is its anchor point, an EM port paints a width bar and an arrow past its own geometry, and an instance's extent resolves through the cell it places. On a symbol the two genuinely differ: pin names are drawn in pixels, at a size with a floor, so they have no world extent until a page size is chosen — a fitted symbol page is therefore wider than the box explain --extents reports, which is the box you want when you are sizing a --window yourself.

Size, and what `--detail` costs

Option What it does
--size WxH Device pixels for .png, points for .svg/.pdf. Default 1600x1200.
--scale n Raster multiplier — 2 is "@2x". .png only; a refusal on a vector format, which has no pixels to multiply.
--dpi n The same number spelled relative to 96 dpi. Refused together with --scale.
--detail full, screen, or a pixel budget. How much geometry comes out. Layout only.

--detail full is the default, and it is not free. full turns every level-of-detail tier off, so what is stored is what is drawn; screen engages the tiers exactly as a canvas does at that zoom, which is the picture a person actually looks at. Measured on a real six-layer board — 3,284 shapes and 764,032 vertices — framed whole at 1600x1200:

Format --detail full --detail screen
.svg 23.5 MB, 1.44 s 6.2 MB, 0.36 s
.pdf 9.6 MB, 1.22 s 2.6 MB, 0.51 s
.png 1.4 MB, 0.55 s 1.0 MB, 0.35 s

A vector file stores every vertex, so full is nearly four times the SVG for a picture that cannot show the difference at that zoom. A raster barely moves, because its size is set by its pixels and not by the geometry behind them.

So: --detail screen when you want the picture. --detail full when you want the geometry — a plot you will zoom into, an SVG something downstream will read the paths out of, a diff between two revisions of the artwork. A pixel budget (--detail 0.5) sits between them; --json reports the toleranceDbu it actually resolved to, which is bucketed by octave and so is rarely the number you asked for.

The verb prints the vertex count and the file size it produced, and --json carries both, so you find this out from the answer rather than from a 23 MB file.

Layers and colour

$ circuitrf render Stage1.clay -o top.png --layers "Top Copper,Silk Top"
$ circuitrf render Stage1.clay -o nosilk.png --hide-layers "Silk Top,Silk Bottom"

Layout only, mutually exclusive, and the default is every layer the resolved technology marks visible — which is what the editor honours, not "all layers regardless".

Ask explain --layers before render --layers

A technology's layer list and the layers a document draws on are different sets, and only the second one puts anything in the picture. explain --layers gives you both at once: every layer the technology defines, with how many shapes this document has on it.

A name that is neither in the technology nor drawn on by the document is a refusal that lists the real ones. It is not a silent skip, because a misspelled layer and a genuinely empty layer produce the same picture and you would have no way to tell which you were looking at. A layer the document draws on that the technology does not define — ordinary after an import — is accepted under the generated L<layer>/<datatype> name explain --layers prints for it, and is excluded like any other when you name a different one.

Framing on some layers and drawing all of them

A fit frames what it draws. So hiding a layer takes it out of the framing as well as out of the picture — which is usually what you want, and occasionally not. The case that bites is an imported board: the drill-map fabrication drawing sits far outside the board outline, and framed with everything else it shrinks the board to a corner of the page.

$ circuitrf render board.clay -o board.png --fit-layers "Top Copper,Bottom Copper"

--fit-layers frames on those and draws everything — so the drill map is still there, it simply falls outside the frame. It is refused with --window and --center/--span, which state the frame outright, and refused when the layers you named draw nothing, because framing on nothing is not a page. With --json each layer row says whether the frame was taken from it.

Recolouring a layer for one picture

$ circuitrf render board.clay -o copper.png \
      --layers "L1 Copper,L2 Copper,L3 Copper" \
      --layer-colors "L1 Copper=#e04030,L2 Copper=#30a050,L3 Copper=#3060e0"

A Gerber import gives every copper layer nearly the same colour, so a copper overlay comes out unreadable. --layer-colors says how a layer draws for this render only — nothing is written to the technology, because changing a design to change a picture of it is not a fix. The flag can be repeated, and one entry can carry several comma-separated pairs.

Colours are #rgb, #rrggbb or #rrggbbaa. The eight-digit form sets the layer's fill opacity, which is the alpha the renderer actually paints a fill through — a layer's own colour alpha is not read at all, so an override that only set it would look applied and change nothing. Layer names are the ones explain --layers prints, including the generated L<layer>/<datatype> names an import produces; a name that is not one of them, or a value that is not a colour, is a refusal rather than a silent skip. --json reports each layer's color and fillOpacity as drawn, which is how you check an override landed — a colour change is the one thing a picture alone cannot confirm.

Option What it does
--theme A theme name, or the path to a .ccolor file. Default: the workspace's own recorded theme, else the shipped one. A name that resolves to nothing is a refusal listing where it looked.
--variant light or dark. Default light.
--background opaque or transparent. Default opaque.
--grid Draw the grid. Off by default, as every export is.
--no-rulers Leave the rulers out.

Rulers are on by default and everything else is off. A ruler is document content — it is in the .clay and the layout editor treats it that way — so an export that dropped it would be showing you a different document. Selection chrome, handles, the marquee, PCell pin overlays, snap glyphs and the EM/DRC overlays are all views of editor state rather than of the file, and none of them can appear here at all.

A data display — the same verb, a different anatomy

$ circuitrf render Amp.cdd -o amp.pdf [--data run.npy]... [--tab name|n] [--plot n] [--all-tabs]

A .cdd is the fourth document kind, and the one that is not a drawing: it holds no data. Its traces name a source — a file, or the sentinel meaning "whatever this display has selected" — and every curve is re-resolved from a file on disk each time it opens.

A source that does not resolve is a refusal, never an empty plot

This is the rule the whole .cdd path is arranged around. An empty plot is a valid picture: it draws, it exports cleanly, and it looks exactly like a measurement that genuinely came back empty. So every source the pages you asked for reference is resolved before anything is drawn, and one that cannot be is a refusal naming --data.

--data may be repeated and binds in order; the first one satisfies the document's own selected source. A --data that binds nothing is a refusal too — handing over last week's run must not silently get you a picture drawn from whatever was lying beside the document.

A reference is looked for beside the .cdd, then under the nearest ancestor workspace's results/. --tab takes a name or a 1-based number (a tab literally called 2 beats the second tab), --plot a 1-based number within it, and the default is the tab the display opens on. --all-tabs is PDF's alone — a PDF is a multi-page format and SVG and PNG are not, and inventing out-1.svg, out-2.svg from one -o would be this tool naming your files for you. On those it is a refusal naming --tab.

--size replaces the page and nothing else about the composition changes. The default is the 792×612 pt landscape page with 36 pt margins, and the plots, axis label strips and any marker info boxes you dragged are fitted to it as one group — so a box you moved lands in the file where it sits on screen. The options that describe a drawing — --window, --center, --span, --fit, --layers, --hide-layers, --detail, --view, --cell, --grid, --no-rulers — are each a refusal naming themselves, because a display has no world coordinates and no layers, and a --window that silently did nothing would give you a full picture you believed was a crop.

With --json, result.render.dataDisplay names every source and the file it actually resolved to, and which of --data or the document bound it. That is the part the picture cannot tell you: "the plot is empty" and "the plot read the wrong run" look identical.

A worked example, from an empty folder

Two commands to set it up, three questions, one picture. The point of the sequence is that the three questions are what make the last command writable: you cannot name a layer or size a window without first asking what the document has.

$ mkdir work && cd work
$ circuitrf new workspace Amp --tech pcb-2layer_FR-4_70mil_1oz
$ circuitrf new cell Amp Stage1 --views layout

That is a correct, empty layout. Give it some artwork — a 20 mm line on the top copper, a ground plane under it, and a label on the top silk:

{
  "FormatVersion": 1,
  "DbuPerMicron": 1000,
  "DisplayUnit": "Um",
  "Shapes": [
    { "$type": "Rect",  "Layer": { "Layer": 1, "Datatype": 0 },
      "X1": 0, "Y1": 0, "X2": 20000000, "Y2": 2900000 },
    { "$type": "Rect",  "Layer": { "Layer": 2, "Datatype": 0 },
      "X1": 2000000, "Y1": -3000000, "X2": 18000000, "Y2": -1000000 },
    { "$type": "Label", "Layer": { "Layer": 5, "Datatype": 0 },
      "X": 400000, "Y": 3400000, "Text": "STAGE 1", "Height": 500000 }
  ]
}

Which cells are in here, and which views does each have?

$ circuitrf explain Amp --cells
Amp  (workspace)
  workspace    /home/you/work/Amp/.cws
               via nearest ancestor .cws
  cells: 1
    Stage1               /home/you/work/Amp/Stage1
      schematic  no-view                (none)
      symbol     no-view                (none)
      layout     sole-file              Stage1.clay

What may I ask for with --layers? The technology defines eight; this document uses three.

$ circuitrf explain Amp/Stage1/layout/Stage1.clay --layers
  layers       PCB 2-Layer FR-4 (70mil, 1oz) — 9 defined, 3 used
    Top Copper           1/0      #c87a3e  solid       purpose=drawing   1 shape(s)
    Bottom Copper        2/0      #8a5028  solid       purpose=drawing   1 shape(s)
    Soldermask Top       3/0      #1e6b3c  solid       purpose=drawing   0 shape(s)
    …
    Silk Top             5/0      #f2f2f2  solid       purpose=drawing   1 shape(s)
    …

How big is it, so I can write a window? In base SI, with the unit and the scale named.

$ circuitrf explain Amp/Stage1/layout/Stage1.clay --extents
  extents      0 -0.003 .. 0.02 0.00376719  (0.02 x 0.00676719 m, scale 1E-06)
               --window 0um,-3000um,20000um,3767.188um
    Top Copper           0 0 .. 0.02 0.0029   --window 0um,0um,20000um,2900um
    Bottom Copper        0.002 -0.003 .. 0.018 -0.001   --window 2000um,-3000um,18000um,-1000um
    Silk Top             0.0004 0.00338437 .. 0.00234306 0.00376719   --window 400um,3384.375um,2343.062um,3767.188um

The line is 20 mm long and 2.9 mm wide, on Top Copper — and the --window line beside each box is already in the spelling the next command takes. Now the left 6 mm of it, that layer only:

$ circuitrf render Amp --cell Stage1 --layers "Top Copper" \
                        --window 0um,0um,6000um,3000um -o stage1-top.png
Wrote stage1-top.png (1600x1200 device-pixels, 10,399 bytes)
  1 of 2 shape(s) drawn, 4 vertices emitted, 2 draw call(s)

Three options and one verb, not five commands — because all three questions are the one question explain already exists for: what did circuitRF decide?


check — is it well formed, does it resolve, is it sound?

$ circuitrf check <path> [--recursive] [--severity warning|error]

Point it at a workspace, a cell folder, or one document. It runs no analysis and it writes nothing, which is what makes it cheap enough to call after every edit — and safe to run on a read-only tree, or on a workspace you have open in the GUI at the same time.

$ circuitrf check ~/designs/Amp
Amp/Stage1/schematic/Stage1.csch
  error   two labels name one physical net: 'vout' and 'out'
Amp/Stage1/layout/Stage1.clay
  warning no technology resolves for this layout
5 document(s): 1 error, 1 warning

Every finding comes from a validator the application already uses — the same view-file check, the same primacy rule, the same technology walk-up, the same net extractor, the same elaborator, the same DRC engine. A rule that lived only in check would be a rule the application does not enforce, and a design would pass here and be refused the moment somebody opened it.

Warnings are reported and still exit 0

--severity decides the exit code, and it defaults to error. Warnings are always printed — a check that hid them to keep the exit code clean would make the exit code useless. Two states are warnings on purpose: a cell folder holding several views with none named primary, and a layout that resolves no technology. Both are normal.

The kind of document is inferred from the path, exactly as convert infers a format. A GDSII or Gerber file is named as interchange rather than called unreadable — it is simply not validated, because there is nothing to validate it against.

Checking a Touchstone file

$ circuitrf check part.s2p
note: part.s2p: 2-port, 401 points, 1 MHz to 1 GHz, reference 50 Ω.
note: part.s2p: causality not evaluated — the frequency grid is not uniformly spaced.
1 document(s) checked: 0 error(s), 0 warning(s), 2 note(s).

An .sNp is data rather than a design, and that changes the severity rule in one place. Passivity, reciprocity and causality are warnings and never errors, each carrying the measured number and the frequency it occurred at: nothing in a Touchstone file says what the part is, so an amplifier is supposed to have gain and a circulator is supposed to be non-reciprocal. Only the unambiguous defects — unreadable, a port count that contradicts the file's own name, a frequency axis that is not sorted, a reference impedance no renormalisation can use — are errors.

A folder walk deliberately skips Touchstone files. A kit directory holds hundreds of them, and measuring passivity and causality across all of them would bury a workspace's own findings under notes about parts you did not author. Naming the file is what checks it.

What each finding means, and the limits of the causality measurement, are on the Derived Metrics page.


explain — what did circuitRF decide?

$ circuitrf explain <path> [--expr "<expression>"] [--set var=expr]
                            [--analysis [<name>]] [--ref <relative-ref>]
                            [--cells [--all]] [--layers] [--extents] [--view <name>]
                            [--footprints]

check answers "is something wrong". explain answers the question that is not a failure: which technology did this layout get, which analysis would actually run, what does this expression evaluate to here, what does this cell reference point at, what cells and layers are in here, and how big is it?

Six questions, and they are refused together rather than ordered — one per run. A precedence nobody stated is an invention, and it would answer a question you did not ask while looking like it answered the one you did.

It reports the walk as well as the answer, and that is the useful half. Resolution in circuitRF is a series of walk-ups — a document's ancestor workspace, a layout's technology, a .cem's two independent references — and which one produced an answer is exactly what you cannot see from the file.

$ circuitrf explain Amp.cem
workspace   from Amp.cem            → /home/you/designs/Amp/.cws        (nearest ancestor .cws)
layout      from Amp.cem            → Amp/Line/layout/Line.clay         (workspace-relative)
workspace   from Line.clay          → /home/you/parts/.cws              (nearest ancestor .cws)
technology  from /home/you/parts    → parts/tech/pcb-2layer.ctech       (the .cws DefaultTechRef)

Two different workspaces there, and that is legitimate: a .cem in one workspace may point at a layout in another, and that layout's layers must be read by its technology.

`--analysis` — which chain would run

$ circuitrf explain pa.cnl --analysis
SWEEP1   parametric_sweep   enabled  runnable  root   → dispatched by hb
  chain: SWEEP1 → HB1
  sweep: Pavl over 1.0000e+09 … 3.0000e+09 step 5.0000e+08 Hz  (stated GHz, scale 1e9)
HB1      hb                 enabled  runnable         → promoted to SWEEP1

A sweep is reported in base SI with its unit and its scale. Reading a mark without its scale has already produced a run at 2 Hz that looked entirely normal.

runnable is two claims, not one: the chain reaches an enabled analysis, and every reference the analysis names resolves — the tuner instances, the inner analysis a sweep wraps, the swept variable, and the variables a tone expression reads. When one does not, the chain is not runnable and the report says which:

$ circuitrf explain pa.cnl --analysis
  analyses:
    LP1              loadpull-pursuit   LP1                          not-runnable
      unresolved: LoadTuner=NoSuchTuner: no instance of that name in this document. Its Tuners are: Load.
      unresolved: SourceTuner=AlsoMissing: no instance of that name in this document. Its Tuners are: Load.
Changed in 1.0

runnable used to mean only the first half, so the analysis above was reported runnable and dispatched by lpp while naming two tuners that do not exist. Whether a thing will run is the question this verb exists to answer, and a caller that acts on an optimistic yes gets its refusal later, about something it has already been told is fine.

What explain still does not claim is anything that would need a solve — "this bench has no bias source" is a property of the solved circuit, and this verb solves nothing. It reports what it can establish and stays quiet about the rest.

`explain` on a Touchstone file — what IS this part?

$ circuitrf explain part.s2p
part.s2p  (touchstone)
  ports        2
  sweep        401 points, 1 MHz to 1 GHz, non-uniform
  reference impedance 50 Ω
  SRF (shunt-through)     22.507906 MHz
  |Z| min (shunt-through) |Z| = 5.057 mΩ at 22.387211 MHz, ESR 5 mΩ
  SRF (series-through)    (nothing)
  |Z| min (series-through) |Z| = 796.177 Ω at 1 GHz, ESR 1.268 Ω

The self-resonance and the impedance floor are what a decoupling capacitor is chosen on, and before this verb the only way to see them was to build a schematic around the file and plot it.

Every applicable fixture is reported side by side rather than one being chosen. Nothing in the file records how the part was measured, and seeing all the readings is the fastest way to identify an unlabelled one — above, the series reading finds no resonance at all and puts the floor five orders of magnitude out, so the part is plainly a shunt-mounted capacitor. The equations behind each reading are on the Derived Metrics page.

`--expr` — evaluate in the design's own scope

$ circuitrf explain pa.cnl --expr "Zopt*2" --set Zopt=12.5
Zopt*2 = 25   (real)

Through the one expression engine, in the design's own resolved scope — never by substitution — with --set applied first exactly as a run verb applies it. The kind is reported, never coerced.

`--ref` — where does this reference land

$ circuitrf explain Stage1.csch --ref ../parts/SOT-23-3
../parts/SOT-23-3 → /home/you/designs/parts/SOT-23-3   resolved
                     outside this workspace: no

Where resolution fails, that is the answer — a sentence naming what was looked for and where it was looked. You are usually running this verb precisely because something did not resolve.

`--cells` — what cells are in here?

$ circuitrf explain ~/designs/Amp --cells
Amp  (workspace)
  workspace    /home/you/designs/Amp/.cws
               via nearest ancestor .cws
  cells: 2
    Balun                /home/you/designs/Amp/Balun
      schematic  named-present          Balun.csch   of 2: alt.csch, Balun.csch
      symbol     no-view                (none)
      layout     missing-named-primary  Balun_rev3.clay   of 2: Balun.clay, Balun_rev2.clay
    Stage1               /home/you/designs/Amp/Stage1
      schematic  sole-file              Stage1.csch
      symbol     sole-file              Stage1.csym
      layout     sole-file              Stage1.clay

It reports the resolution, not a directory listing, and that is the whole reason to run it rather than ls. The interesting rows are the ones where the answer is not obvious, and there are five states, never collapsed into fewer:

State Means
sole-file One file in the sub-folder; it is primary by being the only one.
named-present Several files, and the cell names one of them.
missing-named-primary Several files, and the cell names one that is not there. A flat contradiction, and the state you most need to see — the name it looked for is printed.
no-primary Several files and none named. Not an error; nothing has chosen yet.
no-view The sub-folder is empty, or there is none.

Each is listed with its state — never omitted, and never quietly resolved to the alphabetically first file, which is the answer that would look right and be wrong.

Point it at a cell folder and you get that one cell in the same shape, which is what makes it compose with render: ask what views a cell has, then render one. --all includes generated cells, which are hidden by default exactly as the project tree hides them. This is the same enumeration render --cell resolves through, so a cell listed here is a cell that verb can draw.

`--layers` — what may I ask for?

$ circuitrf explain Stage1.clay --layers
  technology   /home/you/designs/Amp/tech/pcb-2layer_FR-4_70mil_1oz.ctech
               via the workspace's DefaultTechRef — the layout states none
  layers       PCB 2-Layer FR-4 (70mil, 1oz) — 9 defined, 3 used
    Top Copper           1/0      #c87a3e  solid       purpose=drawing   1 shape(s)
    Bottom Copper        2/0      #8a5028  solid       purpose=drawing   1 shape(s)
    Soldermask Top       3/0      #1e6b3c  solid       purpose=drawing   0 shape(s)
    Silk Top             5/0      #f2f2f2  solid       purpose=drawing   1 shape(s)
    …

Two different sets, side by side, and the second is the one that draws anything. The technology's layer table is what render --layers will accept; the shape count is what this document actually has on each. A layer with a colour, a purpose and 0 shapes is a name you may pass that will produce nothing.

The count walks the hierarchy: a layer used only inside a placed sub-cell counts, and an array placement counts its shapes once per element. It is deliberately not render --json's counters.shapesDrawn, which counts only the top-level shapes one frame issued a draw call for.

Two things it cannot tell you, both worth knowing:

`--footprints` — what artwork does each part state?

$ circuitrf explain Board1/schematic/Board1.csch --footprints
  footprints: 3
    C1             smt:0402@N             builtin   2 pad(s) / 2 port(s)
                   via the built-in case table, because the value starts with 'smt:' — generated on demand, not read from a file
                   → 0402 (metric 1005)   1.00 x 0.50 mm, density N (nominal) — generated
                   against technology PCB 2-Layer FR-4 (70mil, 1oz)
    U1             ../../Widget9          cell      9 pad(s) / 9 port(s)
                   via a path, resolved against the document's own folder
                   → /work/Board/Widget9/layout/Widget9.clay
    S4P1           smt:0402@N             builtin   2 pad(s) / 4 port(s)   MISMATCH
                   via the built-in case table, because the value starts with 'smt:' — generated on demand, not read from a file

What it states, what that resolved to, and how it got there. A footprint resolves one of two ways, decided by the first four characters: smt: is a built-in case size, which does not exist as a file and is generated on demand; anything else is a path, resolved against the schematic's own folder exactly as a cell reference is. Which of the two produced the answer is printed, because that is the half you cannot work out from the result.

The technology is named for a built-in and only for a built-in. A generated land pattern picks its copper, soldermask and silkscreen by role out of whatever technology is in force — and the shipped technologies disagree about every layer key, so which technology that is is part of what the artwork will be. A cell you imported or drew already has its artwork on disk on keys of its own, and printing a technology beside it would suggest it was about to be re-resolved.

Pads and ports are on the same line, and a mismatch says so. A land pattern with two pads under a four-port part is a design error that Update Layout reports and refuses to place; two numbers in two places is how that goes unread.

It reads the schematic, not the netlist

Footprint is artwork, not a value, and it is dropped before parameters are resolved — it never reaches the simulator. Asking the netlist about it would report every design as stating none.

`--extents` — how big is it?

$ circuitrf explain Stage1.clay --extents
  extents      0 -0.003 .. 0.02 0.00376719  (0.02 x 0.00676719 m, scale 1E-06)
               --window 0um,-3000um,20000um,3767.188um
    Top Copper           0 0 .. 0.02 0.0029   --window 0um,0um,20000um,2900um
    Bottom Copper        0.002 -0.003 .. 0.018 -0.001   --window 2000um,-3000um,18000um,-1000um
    Silk Top             0.0004 0.00338437 .. 0.00234306 0.00376719   --window 400um,3384.375um,2343.062um,3767.188um

In base SI, with the unit and the scale named — the rule --analysis already follows, for the reason it follows it: reading a mark without its scale has already produced a run at 2 Hz that looked entirely normal. A schematic or a symbol reports design-units at scale 1, because its coordinates are dimensionless and dressing them up in metres would be a lie.

This is the same function render --fit frames on, which is why you can size a --window from it and get exactly the region you expected. The per-layer boxes are the document's own shapes; an instance's extent arrives as one box for the whole placement.

The --window line is meant to be pasted

Every box comes with the same numbers written the way render --window takes them — in this document's own display unit, whole document and per layer, and in --json as a window field beside the coordinates. The metres above are deliberately not what you type: a layout coordinate carries a unit there, and m is not one of the suffixes it reads. Copy the --window line and you get exactly that box; there is no conversion to do and no chance of being three orders of magnitude out.

So framing on one layer is a copy rather than a calculation — and if you want to frame on one layer while still drawing the others, that is render --fit-layers.

An empty document says so rather than reporting a zero box, because a zero box is a point at the origin and that is a different fact. On a symbol, or on a layout carrying a fixed-size ruler, the report adds a note: the fit adds room for marks that are drawn in pixels — a pin's name, a ruler's readout — which have no world extent until a page size is chosen. Those marks are why a fitted symbol page is wider than the box reported here.


read — a result, or a document, back

$ circuitrf read <path> [--only a,b] [--group g] [--json]

The inverse of a run verb. A .npy or a Touchstone file is loaded back into cubes — through the same two loaders the Data Display's source library reads a file with — and one of circuitRF's own documents comes back as its own bytes, unchanged.

$ circuitrf read results/Amp_em.npy
results/Amp_em.npy  (npy)
  group (default):
    S                        Complex  freq[201] Hz x i[2] port x j[2] port
    Z0                       Complex  port[2] port
  group planar:
    MeshCells                Real     cell[1544]

It writes nothing, and it takes one file at a time. For "what is in this workspace", use check or explain; for a GDSII or a Gerber set, use convert.

With --json, --only and --group narrow what comes back — which matters, because reading a 20,000-point swept loadpull in full is the expensive direction:

$ circuitrf read hero3.npy --only Pout_dBm --json

netlist — the netlist a schematic runs as

$ circuitrf netlist <path.csch | cell-folder | workspace --cell N> [-o out.cnl]

The extraction Simulate performs, as a file you can read. It takes a .csch, a cell folder, or a workspace with --cell — the same three inputs render takes, resolved the same way.

$ circuitrf netlist Example_SParam_LC.csch
; extracted from Example_SParam_LC

Port:Term1  n1  0  Num=1  Z=50 Ohm
L:L1  n1  n2  L=2 nH
C:C1  n2  0  C=0.8 pF
Port:Term2  n2  0  Num=2  Z=50 Ohm

analysis SP1 type=sparam start="1" startUnit=GHz stop="5" stopUnit=GHz npts=201

Without -o the netlist goes to stdout, so circuitrf netlist Stage1.csch &gt; stage1.cnl is the file and nothing else. With -o it is written there, and the path is what goes to stdout.

This is the netlist a run actually consumes — the same bytes

You do not need it to simulate a drawing: every run verb takes a .csch directly and extracts it in memory. What this verb is for is seeing that extraction — and checking a .cnl you wrote by hand against what circuitRF produces for the equivalent schematic. Both go through one function, so the file here is not merely equivalent to what a run reads, it is byte for byte the same text.

-o takes a .cnl and refuses any other extension — there is one format here. A .cnl input is refused too: passing it through the reader and the writer would hand back a file that is not the one you gave (comments gone, directives reordered) and call it an extraction.


plot — a picture of a result

$ circuitrf plot <result.npy|.sNp> -o <out.svg|.pdf|.png> --trace <spec> [--trace <spec>]…

One plot, one axis pair, without writing a data display first.

$ circuitrf plot lc.s2p -o match.png \
    --trace cube=S,i=1,j=1,y=db --trace cube=S,i=2,j=1,y=db \
    --title "LC lowpass" --ylabel dB --x 1:5 --y -40:5
Wrote match.png (792x612 device-pixels, 19,785 bytes)
  1 plot(s) on 1 page(s), 1 data source(s)

A trace spec is comma-separated key=value:

Key What it means
cube Which cube. Required. It is the same shorthand the trace card's spec box takes, so S, S[:,2,1], Pout and mag(V[:,"X1.drain"]) all work.
i, j The port numbers of a matrix cube — i=2,j=1 is S21. Refused together with a bracketed slice: they are the convenience over writing one.
y db, db10, db20, mag, phase, real, imag or conj.
axis left (the default) or right.
cut An antenna pattern cut: a bearing in degrees pins the cube's phi axis and sweeps theta; all keeps every phi as a curve family. The verb prints the φ it landed on.
port The port number on a cube's port axis — not an index. A port the run does not hold is refused, listing the ones it does.
freq Pins a freq axis to its nearest sample. Takes an SI suffix: 2.45G.
probe A WSProbe label. Given, it turns a cube=<analysis>.wsp trace into a probe metric.
metric Which of the reference document's quantities — H0, 1/Y0, ZG, SM_Y0, LGa, SMenv, … See The WSProbe. Some take with=, set=, z0=, side=, gi= or the envelope's grid keys.
circuitrf plot amp.npy -o margin.svg --trace cube=SP1.wsp,probe=GATE,metric=SM_Y0,y=db

Case is load-bearing in that notation and is not folded away — LGF is one probe's forward synthetic-circulator loop gain and LGf is a probe pair's feedback-as-synthetic-FET loop gain. A name whose canonical form is shared by two quantities resolves only when it is spelled exactly.

circuitrf read <result> lists the cubes a file holds and their axes, which is where the names come from. A cube the file does not hold is refused, listing the ones it does.

Option What it does
-o <path> Required. Its extension picks the format: .svg, .pdf or .png.
--type rect\|smith\|polar\|table Default rect.
--freq-unit Hz\|kHz\|MHz\|GHz Default GHz. It is also the unit --x is read in.
--title, --xlabel, --ylabel, --y2label Custom labels. Omitted, the plot labels itself.
--x lo:hi, --y lo:hi, --y2 lo:hi Axis windows. An axis you leave out autoscales. Refused on a Smith or Polar chart, whose window is the complex plane.
--size WxH, --scale, --dpi The page. Default 792×612 points — the same page File ▸ Export writes. --scale/--dpi are .png only.
--variant light\|dark, --background opaque\|transparent As render.
--radial linear\|db How a polar plot's radius is read. db makes it an antenna pattern plot. Refused on any other --type.
--db-floor <dB> The centre of a db plot, relative to the outer ring. Default −40.
--db-ring <dB> Ring spacing. Default 10.
--db-ref peak\|<dB> The outer ring: the data's own peak (normalised, the default) or an absolute level.
--db-unit <text> What the radial numbers are in — dBi, dB(W/sr).
--trace …,ref=<dBm> Read a dBm level (TrpDbm, PeakEirpDbm) against this conducted input power instead of the one the run published. An exact dB shift — no re-run. Inert on anything that is not a level.
--whole-plane Draw each cut= as one trace spanning −θmax … +θmax, by fetching the φ + 180° half alongside it — instead of the default two traces per cut. Needs --type polar --radial db.
--angle-labels Print the bearing every 30° outside the disc, with a spoke to each, the way an antenna-range plot is drawn. Polar only, either radial mode. The disc shrinks to make room rather than the numbers overprinting the outer ring.
--write-cdd <path> Also write the data display this drew.
circuitrf plot run.npy -o eplane.svg --type polar --radial db --db-unit "dB(W/sr)" \
  --trace cube=farfield.U,cut=0,port=1,y=db10

# both principal planes, one trace each, with bearings around the rim
circuitrf plot run.npy -o cuts.svg --type polar --radial db --whole-plane --angle-labels \
  --trace cube=farfield.U,freq=5.8e9,cut=0,port=1,y=db10 \
  --trace cube=farfield.U,freq=5.8e9,cut=90,port=1,y=db10
A pattern plot says what it is

On a --radial db plot the outer ring is a reference and the centre is a floor, and the picture states which reference it is using — a 0 dB peak with no reference is not a result. Values below the floor are drawn AT the floor, never dropped: a gap in a pattern trace reads as a null in the antenna, and a real null and a clipped value must not look the same. 0° is at the top and angles increase clockwise, and the note under the plot says what the θ range covers — with an infinite ground plane there is no field below the horizon, so a pattern occupying part of the disc is the model saying so rather than a drawing fault.

--write-cdd is how you go further

This verb draws one plot with one axis pair. Everything else a data display can do — several plots on a page, tabs, markers, contours, a summary table — is still done by writing a .cdd and calling render. --write-cdd hands you the document this verb built, which is a correct starting point to edit rather than a blank page; the picture it draws and the picture render draws from that file are byte for byte the same, because they are the same code.

A plot with no --trace is refused rather than drawn. An empty plot is a valid picture that exports cleanly and looks exactly like a measurement that came back empty.


find — what is in this folder?

$ circuitrf find <root> [--depth n] [--no-analyses]

The workspaces under a directory, their cells, each cell's views, and the analyses each cell declares.

$ circuitrf find ./projects
/home/you/projects  (1 workspace(s), 2 cell(s), depth 4)
  Amp  /home/you/projects/Amp  [tech tech/generic-mmic.ctech]
    LC                       schematic: LC.csch
                             analyses: SP1
    Stage1                   schematic: Stage1.csch, layout: Stage1.clay
                             analyses: HB1, SWEEP1

It reads what the other verbs read, so a cell listed here is the cell render would draw and the analyses are the ones hb would dispatch. It writes nothing, so it runs on a read-only tree and on a workspace the application has open.

Option What it does
--depth n How many levels below the root a workspace is looked for. Default 4, at most 12.
--no-analyses Skip the analyses. Each one costs an extraction, which adds up on a large tree.
The walk is bounded, and it says when it stopped

A listing that quietly gave up reads as "the workspace is not here". So when the walk hits --depth with directories still below it, it says so — a warning on stderr and truncated: true in the --json document. Raise --depth and ask again. A directory symbolic link is never followed, so nothing outside the root you named can appear in the answer.


reference — what may I write?

$ circuitrf reference
$ circuitrf reference netlist
$ circuitrf reference components
$ circuitrf reference components MLIN
$ circuitrf reference analyses
$ circuitrf reference analyses sparam
$ circuitrf reference data-display
$ circuitrf reference technology

They are, in order: the list of topics with what each costs; one page as text; every netlist primitive; just that one; every analysis directive with every key it takes; just that one; and the two document formats nothing else here creates for you.

check tells you that what you wrote is wrong. explain tells you what circuitRF made of it. This one tells you what you are allowed to write in the first place — the primitive type names, how many nets each takes and in what order, what its parameters are called and what they default to.

It reads no file, runs nothing and writes nothing. There is no path argument: it answers about no particular design, which is exactly why it is useful before one exists.

Why this matters more for a script than for you

A netlist naming a type that does not exist fails at elaboration and says so. A component given a plausible but wrong parameter name does not: the name is ignored, the parameter takes its default, and the run converges and produces a complete-looking answer to a different circuit. Getting the spelling from here rather than from memory is what avoids that.

The topics

With no arguments you get the list, with each topic's size — because reading is the expensive direction and a 4 kB page and an 84 kB one should not look alike:

$ circuitrf reference
Reference topics — circuitrf reference <topic>

  netlist            17.7 kB  The Netlist (.cnl) Format
  expressions         9.1 kB  Expressions
  units              10.9 kB  Units
  measurements        5.8 kB  Measurements
  pins-ports-terms    4.3 kB  Pins, Ports & Terms
  sdd                12.2 kB  The SDD (Symbolically-Defined Device)
  file-formats       11.6 kB  File Formats
  component-notes    74.2 kB  Components
  data-display       12.8 kB  The .cdd data-display format
  technology          6.1 kB  The .ctech technology format
  analyses            8.5 kB  Analysis directives
  components         91.3 kB  Component types

  circuitrf reference components <TYPE>   one primitive
  circuitrf reference analyses <TYPE>     one analysis directive

These are the same pages you are reading now, shipped inside the program so they are there on a machine that has no copy of this site. components is the generated catalogue — read from the live component registry every time you ask, so it cannot go stale — while component-notes is this site's Components page, which explains what each part is for. The catalogue answers "what may I write"; the page answers "what does it mean". Ask for both if you want both.

Asking for a topic prints it as its own Markdown, so it redirects cleanly:

$ circuitrf reference netlist > netlist.md

The component catalogue

$ circuitrf reference components MLIN
MLIN
  nets: 2
  terminals: 2  1 2
  order: The two terminals are interchangeable: the line is uniform, so neither end is the input.
  Mlin (MLIN) — Microstrip   search: MLIN, microstrip, microstrip line, line, hammerstad
    terminals: 2  1 2
    W                  2.9              mm     shown
    L                  10               mm     shown
    SignalLayer        -                -      -
    GroundReference    -                -      -

The columns are the parameter's name, its default expression, its unit, whether it shows on the schematic by default, and — where the registry has one — what it means.

terminals is the part you cannot get from a picture: the pins the symbol draws, in the order a netlist line writes their nets. That order is a contract the models themselves read, and it is not always the one you would guess — a MESFET is gate, drain, source while a JFET is drain, gate, source, and a diode is anode then cathode.

`nets` and `terminals` are two different numbers, and the first is the one to write to

How many nets the instance line binds is nets. How many pins the symbol draws is terminals. They are usually the same and they are not always: a Tuner draws one pin and its line takes two, because its reference terminal is implicit on the glyph. So do Port, Term, Vdc and IProbe; a 2-port SDD takes four nets, as ± pairs.

$ circuitrf reference components Tuner
Tuner
  nets: 2
  terminals: 1  1

Writing Tuner:T1 n_drain Z[1]=50 BiasTee=on Vbias=48 — one net — is now refused by name. It used to run: the bias tee delivered nothing, every diagnostic was clean, and the output sat at the engine's floor at every drive point.

Some counts are not fixed, and they say so

An SDD's, a Z_Port's and an SnP's follow a port-count parameter; a Verilog-A model's follows Pins; an ideal switch's follows Throws; a wBond's follows the arrays it places. Those report the rule rather than a number:

$ circuitrf reference components SDD
SDD
  nets: SddPortCount=N binds 2N nets.
  terminals: set by NumPorts — at NumPorts=2: 1+ 1- 2+ 2-

Note that the two lines name different parameters, and that is not a mistake: the parameter panel calls it NumPorts, and a .cnl instance line spells it SddPortCount. A number printed where the honest answer is "it depends" is worse than no number at all.

Analysis directives

$ circuitrf reference analyses sparam
sparam   also: sp, s_param, sparameter, s_parameters
  bare words: log
  type                   required       The analysis kind. One of the tokens this page lists.
  enabled                true           false skips the analysis at run time without deleting it.
  start                  required       First frequency. Bare number in Hz unless a unit follows or Unit=/startUnit= is given.
  stop                   required       Last frequency, same spelling rules as start.
  step                   1e8            Step size. Mutually exclusive with npts; npts wins when both are given.
  npts                   -              Point count. Selects the point-count sweep mode.
  Unit                   -              Sets startUnit, stopUnit and stepUnit at once. Any one of those given individually overrides it.
  ...

Every analysis type= token, every other spelling of it that is accepted, and every key it may carry with its default and whether it is required. It is read from the same table the .cnl reader validates against, so a key that is not on this page is a key the reader refuses — it is not ignored, and it has not been since the netlist contract landed.

The two formats nothing creates for you

circuitrf reference data-display and circuitrf reference technology describe the .cdd and .ctech files field by field, generated from the types their readers actually deserialise into, each with a minimal example that was written out and run. They are here because those are the two formats you may have to write by hand: new workspace copies a technology in for you, but nothing creates a data display.

A few entries carry a note instead of a clean answer, and the note is the useful part. GND, VAR, MEAS and Pin are schematic elements the netlist extractor consumes rather than components you can place in a .cnl. Chain, ExtDevice, SemiC, Short, Term, V_nTone and I_nTone are the other way round — writable in a .cnl, with no palette tile and so no declared defaults.

Unknown names are refused with the real list, never guessed at:

$ circuitrf reference components MLINE
No primitive type 'MLINE'. Types: Amp, Atten, BJT_NPN, BJT_PNP, Balun, Bead, C, Chain, …

With --json the whole catalogue comes back structured — type token, terminals, and every parameter with its default, unit, dimension and visibility:

$ circuitrf reference components --json | jq -r '.result.reference.components[].type'

elab — the elaborated netlist

$ circuitrf elab <file.cnl|.csch>

Elaborates and stops: flattens the hierarchy, resolves every parameter and expression top-down, and numbers the nodes, then prints the elaborated netlist — the exact thing the engines consume. No analysis runs.

This is the debugging verb. When a value is not what you expected, elab is where you find out whether the expression resolved to something different from what you meant, or resolved correctly and the analysis is doing something else.

--json — one machine-readable document

Every verb takes --json, spelled that way everywhere. It changes exactly one thing: stdout carries a single JSON document and nothing else.

stderr is untouched — progress, [circuitRF] notes, warnings and refusal sentences stream exactly as they always did — so a script watching stderr cannot tell whether the flag was passed.

$ circuitrf sparam amp.cnl --json | jq '.outputs[].path'
"amp.s2p"

One schema serves every verb:

{ "circuitrf": { "version": …, "verb": … },
  "input":     { "path": …, "analysis": … },
  "status":    "ok" | "not-converged" | "failed",
  "exitCode":  0 | 1 | 2 | 130,
  "outputs":     [ { "kind": …, "path": … }, … ],
  "diagnostics": [ { "id": …, "severity": …, "message": …, "arguments": { … } }, … ],
  "result":      { … } }

Every cube says what its numbers are in

A cube's unit is always present and never empty. SI symbols as they are written — Hz, V, A, W, Ohm, F, H, K, m, s — and dB and dBm likewise; % for a ratio already scaled to a percentage, 1 for one that is not, index for a flag or a count, and unknown where circuitRF cannot say. A measure line you wrote yourself has whatever unit your expression has, and saying unknown is an answer where a guess would not be.

Why this is worth a field of its own

One loadpull-pursuit result carried Efficiency reading 65.84 and MXE_Eff reading 0.7087 at the same operating point — the cube in percent, the scalar as a fraction. Reading the scalar and formatting it as a percentage gives 0.7%; multiplying the cube by 100 gives 6584%. Both are one plausible line of code, and until the unit was carried there was nothing anywhere to say which was which. Nothing was rescaled to fix it: the numbers are what the engine computed, and the document now says what they are.

--summary: the notes as counts

Some verbs are deliberately talkative. The Gerber import names every inference it made as an inference, which is exactly what lets you decide whether to trust the result — but it is not what you want back from convert --list-cells, whose answer is one cell name.

--summary reports the informational notes as counts by severity and adds a diagnosticSummary block saying how many were left out. Warnings and errors are never collapsed, and stderr still carries everything, so nothing is hidden: what you stop paying for is thirty notes describing inferences that all went fine.


serve — the MCP server

$ circuitrf serve --root <dir> [--kits <dir>]

This is circuitRF as an MCP server. It speaks the Model Context Protocol over its standard input and output — the stdio transport, newline-delimited JSON-RPC 2.0, with initialize, tools/list and tools/call — so any MCP client can discover what circuitRF can do and ask it to do it. Protocol versions 2025-06-18, 2025-03-26 and 2024-11-05 are accepted.

In practice that means an assistant, a design agent, a CI job or an automation harness can run a simulation, check a design, create a workspace or import a part without a human at a terminal. It is started by that program, not by you, and it ends when that program disconnects.

Point an MCP client at it

A client is configured with a command and its arguments. The command is the circuitRF executable, the arguments are serve --root <dir>, and --root is the only directory tree the server will read or write, and a path escaping it is refused rather than clamped (see What it will not do below). Nothing else about the host matters, because the transport is the process's own stdin and stdout.

Thirteen tools, and each is a verb you already have:

Tool Runs
run sparam, dc, hb, lp, lpp or em, chosen by an argument
check check
explain explain, including --cells, --layers, --extents and --footprints
create new workspace or new cell
import import part or convert
render render — one tool over every document kind, as the verb is
read read
netlist netlist — the extraction a schematic runs as
plot plot — one picture from one result file. It takes attachImage too
find find — the workspaces, cells, views and analyses under a directory
history history checkpoint, list or restore — the correction nouns (rename, retitle, correct, review) are on the verb but not on this server
reference reference
batch The only tool with no verb behind it: it holds a restore-point batch open across several calls, which a process that exits after one command cannot

Every tool returns exactly the document --json writes, byte for byte, because the server calls the verb rather than re-implementing it. Nothing is reachable through the server that is not reachable from your own shell, and nothing is reachable from your shell that the server cannot do.

render and plot can hand the picture back, not just its path. Pass attachImage: true and the result carries the rendered file itself — a .png or .svg as image content, a .pdf as an embedded resource with its own type. It is off by default, because an image is expensive in a way a JSON document is not, and a client that only wanted the path should not pay for one.

Over 4 MB it hands back the path and says what to narrow

The file is still written and still named in outputs; only the attachment is withheld, and the answer says how large it came to and which argument would bring it under — --detail screen, --layers, --window, a smaller --size, or .png instead of a vector format. It is never truncated and never dropped in silence: half a PNG is not a smaller PNG, and a request that comes back empty with no explanation just gets sent again.

The number is the measured one. A whole six-layer board is 1.4 MB as a PNG and 23.5 MB as an undecimated SVG, so the cap admits every raster this verb plausibly produces and refuses exactly the case where you should have narrowed the render.

The attached bytes are the bytes on disk. The server attaches the file the verb wrote; it never draws a second time, and it never converts one format into another to make it attachable — that would be the adapter making a rendering decision, which is the one thing it does not do.

What it will not do

The reference surface is also published as protocol resources — one per topic, at circuitrf://reference/<topic>, each advertising its size so a client can decide what to spend. That is the cheaper channel: a resource costs a URI and a title until something reads it, where a tool description is carried for the whole session whether or not it is used. The reference tool exists alongside it because not every client shows resources to the model at all, and both return the same bytes.

A long run reports progress and can be cancelled — a client that asks for progress is sent it as the run moves, and a cancellation stops the run at a work boundary and returns exit code 130 having written nothing.

serve is the one verb whose stdout is not the result: it carries the protocol, so everything else — progress, notes, warnings, device-worker logs — goes to stderr, where the program that started it picks it up. For that reason it takes no --json of its own; every call through it already returns one.

$ circuitrf serve --root ~/designs 2> serve.log

Exit codes

Code Meaning
0 Ran, and produced something usable
1 Could not run — bad arguments, a missing file, no matching analysis, a refusal, an exception
2 Ran, but did not converge
130 Stopped — a run cancelled at a work boundary, by em's own stop or by a serve client's cancellation

2 is deliberately not the same test for every verb. hb and dc fail on any non-converged solve. A loadpull grid in which some points do not converge is a normal and useful result — the edge of a Γ grid routinely will not — so lp returns 2 only when every grid point failed, and lpp only when neither optimum converged and there is no follow-on grid. A rule that failed the whole run on one bad point would make the exit code useless in a script.

Scripting patterns

Keep the table, keep the log, and still see it run.

$ circuitrf lp hero3.cnl -o hero3.npy > hero3-table.txt 2> hero3-run.log

Sweep a variable the netlist already has, without editing the netlist:

$ for p in -10 -5 0 5; do circuitrf hb pa.cnl --set Pavl_dbm=$p -o pa_$p.npy; done

Fail a build on a regression, using the exit code:

$ circuitrf hb pa.cnl -o out.npy || echo "PA did not converge" >&2

Validate a whole workspace in CI, before anything is run:

$ circuitrf check ~/designs/Amp --severity error || exit 1

Author, validate and simulate with no display at any step — the whole loop:

$ circuitrf new workspace build/Amp
$ circuitrf new cell build/Amp Stage1
$ cat > build/Amp/Stage1/Stage1.cnl <<'EOF'
  … your netlist …
$ EOF
$ circuitrf check build/Amp || exit 1
$ circuitrf hb build/Amp/Stage1/Stage1.cnl -o out.npy

Pull one number out of a result, without parsing a table:

$ circuitrf read out.npy --only Pout_dBm --json | jq '.result.groups[""].Pout_dBm.values[-1]'

Ask why a file resolved the way it did, when a run used a technology you did not expect:

$ circuitrf explain Amp.cem

Put a picture of every cell into a build's artefacts, at the size a report reads at:

$ circuitrf explain ~/designs/Amp --cells --json \
    | jq -r '.result.explain.cells[].name' \
    | while read -r c; do
      circuitrf render ~/designs/Amp --cell "$c" --detail screen -o "artefacts/$c.png"
    done

Watch a layout change across two revisions, without opening either:

$ git show HEAD~1:Stage1/layout/Stage1.clay > Stage1/layout/before.clay
$ circuitrf render Stage1/layout/before.clay -o before.png --window 0um,0um,6000um,3000um
$ circuitrf render Stage1/layout/Stage1.clay -o after.png  --window 0um,0um,6000um,3000um

Two things make those two pictures comparable, and both are easy to lose. The explicit --window: --fit frames each file's own extents, so a change in size would move everything in the frame and the diff would be of the framing rather than of the artwork. And the older revision written inside the workspace: a .clay somewhere else resolves no technology and draws on the fallback palette, so the whole picture would change colour.

Re-extract every EM setup in a workspace after a technology edit — the layout and stackup references resolve themselves, so the loop needs nothing but the file names:

$ for f in em/*.cem; do circuitrf em "$f" || exit 1; done

Because each of those writes the same file Simulate writes, a schematic that references the extracted Touchstones picks the new results up with no further action.


See also: Simulations (what each analysis computes) · The netlist format · EM Setup · The layout editor · The Data Display · The MoM engine · Results & data export · Kits and external device models.