circuitRF Reference Guide

PDK Integration

Importing a process design kit and simulating the parts it supplies.

A kit is a read-only tree supplied by a foundry or a device maker. It typically holds symbol descriptions, netlists defining its parts, palette icons, model data files, and — usually in a separate package beside it — the compiled model library those netlists name but never define.

circuitRF reads a kit structurally. It does not carry a list of who makes what; nothing about a kit's identity is written into the product. Everything circuitRF knows about a kit it learned from the kit at run time.

What a kit contributes

Four things, and they arrive by four different routes:

What it is
Symbols Translated from the kit's own symbol descriptions into circuitRF symbols, in memory
Models The device equations — usually a compiled library the kit names but ships separately
Technology Layers, stackup and DRC rules, if the kit supplies them
Artwork Layout for its parts, often as PCells generated from parameters
A kit part is an ordinary cell reference

This is the load-bearing decision of the whole area. A cell reference is already the component whose artwork lives in an external file and resolves at render time — so placement, rendering, pin geometry, hit-testing, net extraction and the symbol editor all work on a kit part unchanged. There is no "external part" species, and there deliberately is not going to be one.

Three things a kit does not tell circuitRF, which it therefore works out and records:

OpenPDK

circuitRF supports the OpenPDK file structure — https://github.com/fossi-foundation/open-pdks. If you are building a kit yourself, or normalising a supplied one, that is the layout to follow: it is an open standard, it is what circuitRF's importer expects to find, and it keeps a kit portable between tools. PDK Authoring covers producing one.

Two kinds of open kit

An open kit can arrive in two forms, and each supplies a different half of a design.

Import both and a layout cell declaring the same model as a part merges onto that part's palette tile. Using a gdsfactory kit covers the second kind.

Importing one

File ▸ Manage Libraries… ▸ Add… (Ctrl/⌘ P), then pick the kit's folder.

An import writes nothing into your workspace

No symbols are copied in, no parameter interfaces, no icons. The workspace records a reference to the kit plus the decisions circuitRF made about it — the kit path, the provider name, the resolved model-library path per platform, the chosen variant defaults, and a translation version. Everything else is rebuilt in memory each time the workspace opens.

The principle is persist what was decided, rebuild what was translated. The decisions are tiny and carry no geometry; they are the difference between a workspace that opens the same way twice and one that quietly re-decides.

The import report

Every import ends in a report, success included — because the interesting result is almost never all-or-nothing. A kit typically holds parts circuitRF can place, drawings it cannot yet read, and files it does not recognise at all, and the difference is what you need to see.

Import PDK - AcmeRF GaAs-150 Imported /kits/AcmeRF-GaAs150/pdk AcmeRF GaAs-150 — imported /kits/AcmeRF-GaAs150/pdk Parts available for placement: 8 PHEMT 2×50 µm [Active] PHEMT 4×75 µm [Active] MIM capacitor [Passive] NiCr resistor [Passive] Spiral inductor [Passive] Through-substrate via [Interconnect] Microstrip line [Interconnect] Microstrip tee [Interconnect] Read: Layer technology ×1 12 layers, 9 design-rule checks Symbol library ×1 8 symbols, pins and bodies read Python PCell ×1 8 generators, parameters resolved Compiled model library ×1 runs in the device worker Subcircuit netlist ×1 8 cells, model cards resolved Notes: [Info] Corners: typical, slow, fast — typical is the kit's own default. → Choose one per test bench in the Analyses panel. [Info] All 8 parts carry both symbol and layout artwork. Import PDK - AcmeRF GaAs-150 Imported /kits/AcmeRF-GaAs150/pdk AcmeRF GaAs-150 — imported /kits/AcmeRF-GaAs150/pdk Parts available for placement: 8 PHEMT 2×50 µm [Active] PHEMT 4×75 µm [Active] MIM capacitor [Passive] NiCr resistor [Passive] Spiral inductor [Passive] Through-substrate via [Interconnect] Microstrip line [Interconnect] Microstrip tee [Interconnect] Read: Layer technology ×1 12 layers, 9 design-rule checks Symbol library ×1 8 symbols, pins and bodies read Python PCell ×1 8 generators, parameters resolved Compiled model library ×1 runs in the device worker Subcircuit netlist ×1 8 cells, model cards resolved Notes: [Info] Corners: typical, slow, fast — typical is the kit's own default. → Choose one per test bench in the Analyses panel. [Info] All 8 parts carry both symbol and layout artwork.
The report an import writes: what was read, what it holds, and the notes that go with it. The kit is invented - real kits are licensed and none is in this repository - but the report and the dialog rendering it are the application's own.

The report groups what it found into read, recognised but not yet supported, and not recognised, and every finding carries the action that would close the gap. It is selectable text, so the whole thing can be pasted into a note or a bug report.

The kit in that figure is invented for the documentation — real kits are licensed and none ships with circuitRF — but the report and the dialog rendering it are the application's own.

A vendor delivery is often several part kits beside one shared package holding the compiled models, and discovery finds that package by adjacency. Once you reference a kit from somewhere else — a workspace folder, most obviously — that adjacency is gone and nothing on disk can recover it. So Add… also accepts a folder with no parts in it, when that folder holds the model libraries: adding the package directly is how you restore the link.

Where the parts go

Kit parts appear in the Library palette, ready to place. They do not appear in the Project Tree, because nothing is on disk — they are the kit's cells, not yours.

Place one and it behaves like any other cell reference: it draws its symbol, it has pins you can wire to, and its parameters are seeded from the interface the kit published. A part that ships several formulations arrives on the one that works, so your first Run answers rather than explains.

A welcome side effect of parts being virtual rather than on disk: placing one does not require a saved schematic. An ordinary cell reference is computed relative to the schematic's own folder, so it does; a kit part has no such need.

How a kit's models are evaluated

A kit's device models are usually compiled code — a shared library the vendor built, not equations you can read. circuitRF runs them in a separate worker process.

That is a deliberate design, and it buys three things: a model that crashes takes the worker with it and not your session; a model built for a different operating system can still be reached (below); and circuitRF learns every device type, parameter name, pin count and node role at run time from the library itself, rather than carrying a table that would go stale.

What to expect

Nothing to configure. The path is import kit → place part → set up an analysis → Run:

  1. Import reads the kit and, when the kit ships no provider manifest of its own — the ordinary case for an unmodified vendor kit — synthesises one, recording which library it found. It is written into your workspace as plain JSON, because anything chosen automatically should be visible and one line to correct.
  2. Placement seeds the instance's parameters from the published interface.
  3. Run starts the worker on demand. Opening a workspace starts no processes, and a kit your design never uses is never launched.

The first evaluation of a run is slower than the rest — the worker is starting, loading the library and interrogating it. After that it is a pipe.

Node roles are measured, not declared

Before a model is used, circuitRF probes it: it perturbs each node and compares the model's own analytic Jacobian against finite differences of its own currents. From that it learns, per node, whether the node is a free unknown, whether it is conductively coupled to its neighbours, and therefore whether it is electrical or thermal.

A thermal terminal needs a reference, and a floating one has no solution

An unconnected ordinary pin is fine — it simply gets its own net. A thermal pin is not: an open thermal terminal is a floating node fed by a constant current source, and it has no operating point at all. circuitRF supplies the reference the design did not, so an unconnected thermal pin does not stop a run. If you are modelling self-heating deliberately, build the thermal RC network yourself, between the thermal node and an ambient source — the models do not build it for you, and a non-zero thermal diagonal in the model is self-heating feedback, not a reference.

A model the kit's own simulator builds in

An open kit's model cards often name a model its own simulator builds in rather than a compiled one: a type word and a level, such as .model q1 npn level = 9. A kit ships no source for a built-in, because its simulator needs none. Where circuitRF implements that model itself, the card runs on circuitRF's own physics; where it does not, the device is refused by name, and so is any run that reaches it. The refusal quotes the declaration below with the card's own type and level filled in.

If you have a compiled Verilog-A model of the same thing — one you built once with a Verilog-A compiler, for instance — declare it as the card's stand-in in the kit's settings. Those are the Settings object of the kit's entry under PdkRefs in the workspace's .cws, or the kit's own device-provider.json:

"standIns": [
  { "model": "npn", "level": "9",
    "file": "/path/to/bipolar.osdi", "module": "bipolar_module",
    "parameters": { "dtemp": "trise", "vbe_max": null },
    "terminals": [1, 2, 3, 4, 5] }
]
Key What it says
model The card's type word, compared without case.
level The level the card must also state. Leave it out to cover every level of model; a declaration with a level wins over one without. The level that matched picks the model and is not passed to it.
file The compiled .osdi. A relative path resolves against the kit, as every path in these settings does.
module The module inside file that runs the card.
parameters Optional. Names on the card or the instance line that the module spells differently, each to the module's name; null drops a name the module has no use for. Any other name passes as written.
terminals Optional. For each of the module's terminals in its own order, the 1-based node of the element line it takes. Left out, the line's own node order is the module's.

The card's values then reach the module under the instance's own: the card is the model, the line is this device's geometry. A name the card states that the module does not declare is refused by name, all of them at once, rather than dropped — a dropped parameter would run a different model with nothing to say so. Map it to the module's name for it, or to null once you know it does not matter.

The device runs through the kit's compiled-model worker, so the kit's settings must name one. An import names it whenever the kit has compiled models of its own; for a kit with none, add an entry under workers with the osdi-worker command and the stand-in's file as its argument.

On macOS

Vendor model libraries usually ship Linux and Windows builds only. A Linux library cannot be loaded on macOS at all — that is a binary-format and OS-ABI mismatch, not an instruction-set one, so nothing at the library level bridges it. circuitRF ships a small virtual machine for exactly this, and starts it for you; there is nothing to install. Expect a few seconds of startup on the first run of a session.

When something goes wrong

A broken kit reference is a first-class, repairable state. A workspace whose kits are missing still opens; its kit parts draw as the "not found" placeholder. Your design is your data — a missing dependency degrades, it does not deny.

Reporting is one summary per kit, not one message per part — forty parts must not produce forty warnings. The details go to a log file in the workspace, and the Messages pane carries the summary with a clickable path to it.

What to check first, in order:

  1. Is the kit still where the workspace thinks it is? Manage Libraries… shows the stored path and whether it resolved. A kit is normally outside the workspace, so an absolute path is the common case and a moved kit is the common failure.
  2. Was the model library found? The resolved library path per platform is recorded per kit. If the shared model package was never referenced — see Importing one — the parts will draw and place but will not simulate.
  3. Run Validate PDK (below). It tells you whether the kit still holds the parts your design placed.
  4. Read the Messages pane for the worker's own words. A refused evaluation usually explains itself only in the worker's own error output, and that output is attached to the failure.

A model that stamps cleanly but will not converge is worth a specific mention: every number is finite, nothing errors, and the solve simply does not settle. That is the signature of a misread node role — worth reporting with the kit's details rather than tuning tolerances against.

Managing kits

Manage PDKs Kits this workspace references AcmeRF GaAs-150 — 8 parts · outside this workspace /kits/AcmeRF-GaAs150 This kit lives outside the workspace, so sharing the workspace does not carry it — a colleague will need their own copy and will repair the reference here. Add… Remove Reveal Validate Close Manage PDKs Kits this workspace references AcmeRF GaAs-150 — 8 parts · outside this workspace /kits/AcmeRF-GaAs150 This kit lives outside the workspace, so sharing the workspace does not carry it — a colleague will need their own copy and will repair the reference here. Add… Remove Reveal Validate Close
Manage Libraries: the workspace's kit and vendor-library references, what each one resolved to, how many parts it loaded, and the Add / Remove / Reveal / Validate actions.

File ▸ Manage Libraries… (Ctrl/⌘ P) lists every library the workspace references — each kit, and each vendor part library — with its kind, its name, stored path, whether it resolved and its part count.

Action What it does
Add… Folder picker. The folder is classified by what it holds — readable SPICE subcircuits and no kit structure is a vendor library, anything else is imported as a kit — so you are never asked which it is
Remove Drops the reference, after warning how many placed parts it will leave unresolved
Reveal Opens the kit folder in your file manager
Validate Runs the check below on the selected kit

Every action reports in the dialog and posts to the Messages pane — the dialog gets dismissed, and the record of what you added, removed or validated should outlive it.

Validate PDK

Re-reads the kit and reports drift, not just breakage: a part your design placed that the kit no longer offers, or a recorded translation version that no longer matches the reader.

It reports what it checked as well as what was wrong — parts offered, placed parts checked, problems, notes. "No problems found" on its own cannot be told apart from a check that did nothing, and that is the one thing a validation must never be ambiguous about. A kit that could not be read at all reports −1 parts offered rather than 0, because "offers nothing" and "could not be read" are different answers to different questions.

It checks your placed parts against a fresh read of the kit, never against whatever happens to be loaded in this session. The question is whether the kit still holds the part — conflating that with whether this session managed to load it turns "your kit changed" into "something went wrong at startup".

Why a translation version exists

Symbol pins are snapped to the connection grid when a kit's symbols are translated. If that reader ever changes — a scale fix, a snap fix, anything touching pin placement — re-deriving symbols would move pins, and wires attached to them would silently disconnect. The recorded translation version is what catches that: a mismatch is reported rather than re-derived underneath your wiring.

Vendor part libraries

A component manufacturer's simulation library — a folder of plain-text SPICE files, one small subcircuit per capacitor or inductor — can be referenced from a workspace and searched by value, size and description.

File ▸ Add Vendor Library… asks for the library's folder, reads it, and shows what it found before anything is added: the number of parts, the folders that group them, and any files it could not read. The name it offers is the folder's own; placed parts reach the library by that name. The Messages pane gets one line saying what was added.

Referenced where it is, never copied

A vendor library stays in the folder you downloaded it to. The workspace records its name and path, nothing else. To move it, add the folder again under the same name: every placed part resolves again and no schematic changes.

Only the static, plain-text edition can be read. Manufacturers publish their libraries in several editions. The ones with bias- and temperature-dependent models are encrypted for one simulator each, and circuitRF does not read them. A file like that is counted, not read, and the library says once how many it found. A folder holding nothing readable is refused with the same sentence. If you need one bias or temperature condition, export that condition from the manufacturer as a plain static model and place it as an ordinary SpiceModel.

Each part is listed from the comment header above its subcircuit (description, value, frequency range, temperature and bias). The circuit itself is read only when a run needs it. Indexing happens in the background each time the workspace opens, so opening never waits for it.

In the palette, each library is a sub-menu of the Type menu following its own folders, and choosing it lists its parts with value-aware search; see A vendor library's parts. Add Vendor Library… is at the bottom of that menu too.

Use in new workspaces. Every library you add is remembered for you, and File ▸ New Workspace references the ones ticked New workspaces in Manage Libraries. They are ticked by default. If a workspace opens and one of its libraries is missing, the Messages pane says so once per library. When you have that library somewhere else under the same name, the message offers Repair.

Validate on a vendor library re-reads it and reports parts indexed, files it could not read, placed parts checked, and any placed part the library no longer holds.

A library can also hold model folders: parts drawn in circuitRF and packaged with every file they need. Their format is Model Library Format. Add Vendor Library… takes a folder of model folders, or a folder mixing them with SPICE files, the same way. Each model folder is one part. Its part.cpart gives the description, value, frequency range and case size that a SPICE part's header gives, so search, the palette and the Parameter Editor treat both kinds alike. Indexing reads that one file per model and nothing else in the folder. A .lib inside a model's models/ folder belongs to that model and is never listed as a part. A model folder whose part.cpart cannot be read is named in the library's summary, and the rest of the library is still indexed.

A placed model runs its netlist. It is a cell instance whose reference is vendormodel://<library>/<model folder>. A run reads the model's .cnl once, however many instances place it, and never reads its schematic. Its parameters are the model's cell parameters, edited in the Parameter Editor like any cell's, starting at the defaults its part.cpart states. A model's frequency range is checked as a SPICE part's is, with one difference: a DC operating point, a harmonic-balance run or a sweep from 0 Hz is outside the range unless the range starts at 0 Hz. A model built on S-parameters measured from 100 MHz has no exact DC answer, which a network of R, L and C has.

Validate and circuitrf check check every model folder too. A model that ships its schematic must also have a netlist that is exactly what that schematic extracts to. If it is not, that is an error: the part runs its netlist, so a difference means it does not run what its schematic shows.

A placed part

Selecting a placed vendor part shows its library and file (a model's folder), a Part field and the part's value, tolerance, frequency range and description in the Parameter Editor. The editor's title is the part number. The check boxes beside File and Part say whether each one is drawn on the schematic, as the check box on every other parameter does; File is off when a part is placed.

Either way the instance keeps its place, wires and labels. The footprint follows the part only when it came from the part: a footprint you chose yourself stays. One Undo puts both back. Selecting a part reads nothing from the library file.

The ? button opens the documents the library came with, if any, each in your system's own viewer. Some editions ship nothing but model files, and publish their notes beside the download instead. The menu then says the library came with no documents.

A run reads only the placed parts. Each part's circuit is read from its own few lines of its file, so a run placing three parts from a 15 MB file reads three small blocks. A part whose library is missing, or that the library no longer holds, is reported by name and left out of the run.

Frequency range. A part's model is valid over the range its library states. When a run's analyses go outside it, the run says so once per part number: "… is characterised from 100 MHz to 30 GHz; this run reaches 1 MHz." It is a warning in the Messages pane (on stderr from the command line); the run still completes. A DC operating point never warns.

Changing a placed model. A library part is somebody else's design, so it cannot be pushed into: Push In is not on its menu, toolbar button 18 is greyed and ⌘] / Ctrl+] only says why. Two commands on its right-click menu change it instead:

Sharing. Archive Workspace lists each referenced vendor library unticked, because a library is usually licensed for your own use. The recipient adds their own copy. Ticked, the library is copied into the archive and the archived workspace points at the copy.

Letting the optimizer choose the part

A placed vendor part can be an optimizer variable. What varies is the part itself, so each candidate is simulated with its own model, parasitics and self-resonance included. A real 10 pF part is not an ideal 10 pF capacitor, and the difference between the two is often why you placed a real part.

Beside Value ▼ / ▲ in the Parameter Editor are the toggles every parameter row has. Tune adds the part to the Tuning panel and Optimize adds it to the Optimizer, each with the parts of the same kind as its candidates. Click a toggle again to remove the part. It appears in those panels as C1.Part, showing its part number and value:

When every optimized value is a vendor part, Auto searches the candidates directly with Discrete, and the methods that move values continuously are greyed in the algorithm menu. With other variables too, the optimizer first puts an ideal capacitor or inductor in the part's place and moves its value continuously. It then tries the real candidates either side of that value, keeps the better one, and polishes the other variables with it. The status line shows both costs, the cost with ideal components and the cost with the real parts; the gap is what the parts' own parasitics do. A part with no C or L glyph has no ideal stand-in and is always searched on its candidates.

Push writes the chosen part to the instance as choosing it in the Part field would. The footprint follows only when it came from the part, and the whole Push is one Undo. The part itself is chosen, never drawn; to vary a real part in a Monte Carlo run, draw its value with its own tolerance (next section). The frequency-range warning is given once, for each chosen part whose range the analyses leave.

Headless, the same line is written tune C1.Part kind=same min=1 pF max=47 pF opt=1, or tune C1.Part parts="0603 C0G 50V" opt=1. circuitrf explain --tunables lists each part with its candidates, circuitrf check reports an entry with no candidate or too many, and circuitrf opt reports the chosen part for each entry.

A part's tolerance

A static model states no tolerance, but the part number does. Manufacturers spell a part's value as a short code (100 is 10 pF, R60 is 0.6 pF, 1N3 is 1.3 nH) and put a tolerance letter right after it: B ±0.1 pF, C ±0.25 pF, J ±5 %, K ±10 % and so on. circuitRF works out the value code from the value the header states, finds it in the part number, and reads the letter after it. A header that states a tolerance itself (Tolerance = ±5%) is used as written and the part number is not read.

Letter Capacitor Inductor
W ±0.05 pF ±0.05 nH
B ±0.1 pF ±0.1 nH
C ±0.25 pF ±0.2 nH
S — ±0.3 nH
D ±0.5 pF ±0.5 nH
F G H J K M ±1, 2, 3, 5, 10, 20 % the same

A letter in pF or nH counts only on a part of 10 pF / 10 nH or less whose value is at least the tolerance. When the answer is not certain the tolerance is not known, never guessed: the part number holds no value code followed by one of these letters, it spells the value code twice with different letters after it, or the letters of one file sit at more than one position in their part numbers (that file is reported once and none of its tolerances is used). Adding a library reports how many parts have a known tolerance: Tolerance known for 1,212 of 1,236 parts.

The Parameter Editor shows the tolerance on its own row under the value (Tolerance ±0.1 pF, or not known), and the part picker shows it after each part's value (10 pF ±5 %).

Drawing a part in a Monte Carlo run. The Yield toggle beside Tune and Optimize adds the part's value to the Yield panel, as C1.C for a capacitor or C1.L for an inductor, drawn from the part's own tolerance. It is greyed, with the reason in its tooltip, when the tolerance is not known. Headless the line is:

tune C1.C dist=gauss tol=part sigmas=3 trunc=3
tune L2.L dist=unif tol=part

tol=part follows the part. Choosing another part, stepping the value or applying an optimized part changes the tolerance with it, with no edit to the line. An explicit tol=0.05 pF still works. A draw scales every capacitance of a capacitor's model, or every inductance of an inductor's, by the drawn value over the stated one. The parasitics of the other kinds stay as they are, so the self-resonance moves as a real part's does. The value is drawn only: opt=1 or tune=1 on C1.C is an error, because the knob a real part has is C1.Part. When the optimizer chooses among parts and the value draws with tol=part, each candidate is drawn with its own tolerance, and a candidate whose tolerance is not known is left out and reported.

circuitrf check reports tol=part on a part whose tolerance is not known, naming the part and the reason. circuitrf find --parts prints the tolerance as a column, and circuitrf explain --tunables shows what tol=part resolves to.

Headless, circuitrf import library <folder> --into <workspace> adds one, circuitrf find <workspace> --parts "<query>" searches them, circuitrf check <workspace> reports a missing library or a vanished part as an error, and circuitrf explain <file> --ref vendorlib://… shows which workspace, library entry and folder a reference resolves through; see CLI.

Authoring a kit

PCells can be authored in Python — the generator, its parameters, its pins and its parameter handles all in one function in one file. That, the OpenPDK layout, and a complete worked example are in PDK Authoring.

Kits headless

A command-line run has no workspace to read the kit list from, so it takes the folders directly: --kits <dir>, repeatable, valid on every verb. That is what makes a design placing externally-supplied device models runnable in a script or a CI job.

See also: PDK Authoring · PCells · Compiled Verilog-A models · Technology · The command line.