Bridge to remote Cadence Virtuoso via Python API. TRIGGER when user mentions: Virtuoso, Maestro, ADE, CIW, SKILL, layout, schematic, cellview, OCEAN, or any Cadence EDA operation.
CRITICAL: Do NOT invent SKILL code or API calls from memory. Before writing any SKILL expression or calling any Python API function:
- Search
references/for the function name or keyword- Check
examples/for a working example of the same operation- Read the actual function signature (
help()for Python,references/*.mdfor SKILL)If the function is not documented in references or examples, it probably does not exist or has a different name. Never guess parameter names -- verify first.
You control a remote Cadence Virtuoso through virtuoso-bridge. Python runs locally; SKILL executes remotely in the Virtuoso CIW. SSH tunneling is automatic.
Local (Python) Remote (Virtuoso)
┌──────────────────┐ SSH tunnel ┌──────────────────┐
│ VirtuosoClient │ ────────────► │ CIW (SKILL) │
│ │ │ │
│ • schematic.* │ │ • dbCreateInst │
│ • layout.* │ │ • schCreateWire │
│ • execute_skill │ │ • mae* │
│ • load_il │ │ • dbOpenCellView │
└──────────────────┘ └──────────────────┘
| Level | When to use | Example |
|---|---|---|
| Python API | Schematic/layout editing — structured, safe | client.schematic.edit(lib, cell) |
| Inline SKILL | Maestro, CDF params, anything the API doesn't cover | client.execute_skill('maeRunSimulation()') |
| SKILL file | Bulk operations, complex loops | client.load_il("my_script.il") |
Always use the highest level that works. Drop to a lower level only when needed.
Never guess function names. If the function isn't in the examples below, read the relevant references/ file before writing the call. Fabricating a wrong name wastes time debugging in CIW.
| Domain | What it does | Python package | API docs |
|---|---|---|---|
| Schematic | Create/edit schematics, wire instances, add pins | client.schematic.* | references/schematic-python-api.md, references/schematic-skill-api.md |
| Layout | Create/edit layout, add shapes/vias/instances | client.layout.* | references/layout-python-api.md, references/layout-skill-api.md |
| Maestro | Read/write ADE Assembler config, run simulations | virtuoso_bridge.virtuoso.maestro | references/maestro-python-api.md, references/maestro-skill-api.md |
| Netlist (si) | Batch netlist generation without Maestro | simInitEnvWithArgs + si CLI | See "Batch Netlist (si)" section below |
| General | File transfer, screenshots, raw SKILL, .il loading | client.* | See below |
virtuoso-bridgeis a Python CLI. Useuv+ virtual environment — never install into the global Python.
uv venv .venv && source .venv/bin/activate # Windows: source .venv/Scripts/activate
uv pip install -e virtuoso-bridge-lite
All virtuoso-bridge CLI commands and Python scripts must run inside the activated venv.
.env — the bridge looks up .env in this order: --env FILE (CLI flag) → ./.env (project-local) → ~/.virtuoso-bridge/.env (user-level). If any of these exists, skip init. Only run virtuoso-bridge init when none exist — it creates ~/.virtuoso-bridge/.env by default (user-level, shared across projects).virtuoso-bridge start — starts the local bridge service and SSH tunnel.degraded — the user must load the setup script in Virtuoso CIW (the start output tells them exactly what to run).virtuoso-bridge status — verify everything is healthy before proceeding.virtuoso-bridge windows — list all open Virtuoso windows (num + name).virtuoso-bridge screenshot [ciw|current|N] — screenshot a window to output/. Default: CIW.examples/01_virtuoso/ — don't reinvent from scratch.client.open_window(lib, cell, view="layout") so the user sees what you're doing.from virtuoso_bridge import VirtuosoClient
client = VirtuosoClient.from_env()
client.execute_skill('...') # run SKILL expression
client.load_il("my_script.il") # upload + load .il file
client.upload_file(local_path, remote_path) # local → remote
client.download_file(remote_path, local_path) # remote → local
client.open_window(lib, cell, view="layout") # open GUI window
client.run_shell_command("ls /tmp/") # run shell on remote
client.list_windows() # list all open windows
client.screenshot(output="output", target="ciw") # screenshot a window
execute_skill() returns the result to Python but does not print anything in the CIW window. This is by design — the bridge is a programmatic API, not an interactive REPL.
# Return value only — CIW stays silent
r = client.execute_skill("1+2") # Python gets 3, CIW shows nothing
# To also display in CIW, use printf explicitly
r = client.execute_skill(r'let((v) v=1+2 printf("1+2 = %d\n" v) v)')
# Python gets 3, CIW shows "1+2 = 3"
Full example: examples/01_virtuoso/basic/00_ciw_output_vs_return.py
Sending multiple printf in a single execute_skill() loses newlines — the CIW concatenates everything on one line. To print multi-line text, write it as a Python multiline string and send one execute_skill() per line:
text = """\
========================================
Title goes here
========================================
First paragraph line one.
First paragraph line two.
Second paragraph.
========================================"""
for line in text.splitlines():
client.execute_skill('printf("' + line + '\\n")')
Constraints:
" or %, escape them (\\", %%) or use load_il() instead (see 03_load_il.py)IMPORTANT: Always write
.pyfiles, never usepython -c.python -c "..."has three layers of quoting (shell + Python + SKILL).\\neasily becomes\\\\n, causingprintfto silently produce no output. Always write code to a.pyfile and runpython script.py-- only two quoting layers (Python + SKILL), matching the examples.
Full example: examples/01_virtuoso/basic/02_ciw_print.py
Load on demand — each contains detailed API docs and edge-case guidance:
| File | Contents |
|---|---|
references/schematic-skill-api.md | Schematic SKILL API, terminal-aware helpers, CDF params |
references/schematic-python-api.md | SchematicEditor, SchematicOps, low-level builders |
references/layout-skill-api.md | Layout SKILL API, read/query, mosaic, layer control |
references/layout-python-api.md | LayoutEditor, LayoutOps, shape/via/instance creation |
references/maestro-skill-api.md | mae* SKILL functions, OCEAN, corners, known blockers |
references/maestro-python-api.md | Session, read_config (verbose 0/1/2), writer functions |
references/simulation-flow.md | Standard simulation flow — 8-step guide, pitfalls, optimization loops |
references/netlist.md | CDL/Spectre netlist formats, spiceIn import |
references/troubleshooting.md | Known gotchas, GUI blocking, CDF quirks, connection issues |
references/testbench-duplication.md | Clone a testbench cell (schematic+config+maestro) to a new name — same lib or cross-lib |
references/schematic-recreation.md | Recreate schematic from existing design (grid layout, diff pair conventions) |
references/batch-netlist-si.md | Generate netlists without Maestro using si batch translator |
Always check these before writing new code.
examples/01_virtuoso/basic/00_ciw_output_vs_return.py — CIW output vs Python return value (when CIW prints, when it doesn't)01_execute_skill.py — run arbitrary SKILL expressions02_ciw_print.py — print messages to CIW (one execute_skill per line)03_load_il.py — upload and load .il files04_list_library_cells.py — list libraries and cells05_multiline_skill.py — multi-line SKILL with comments, loops, procedures06_screenshot.py — capture layout/schematic screenshotsexamples/01_virtuoso/schematic/01a_create_rc_stepwise.py — create RC schematic via operations01b_create_rc_load_skill.py — create RC schematic via .il script02_read_connectivity.py — read instance connections and nets03_read_instance_params.py — read CDF instance parameters05_rename_instance.py — rename schematic instances06_delete_instance.py — delete instances07_delete_cell.py — delete cells from library08_import_cdl_cap_array.py — import CDL netlist via spiceIn (SSH)examples/01_virtuoso/layout/01_create_layout.py — create layout with rects, paths, instances02_add_polygon.py — add polygons03_add_via.py — add vias04_multilayer_routing.py — multi-layer routing05_bus_routing.py — bus routing06_read_layout.py — read layout shapes07–10 — delete/clear operationsexamples/01_virtuoso/maestro/01_read_focused_maestro.py — in-memory snapshot of the focused maestro (config + env + results + outputs + corners + variables)03_bg_open_read_close_maestro.py — background open → read config → close06a_rc_create.py — create RC schematic + Maestro setup06b_rc_simulate.py — run simulation06c_rc_read_results.py — read results, export waveforms, open GUI07_gui_session_lifecycle.py — GUI session lifecycle integration test (open/close edge cases)08_gui_open_snapshot_close.py — GUI open → snapshot artifacts → close (owns lifecycle)09_snapshot_with_metrics.py — snapshot the focused maestro to a timestamped directory (disk artifacts)ddGetObj(cellName) with a single argument returns nil — must iterate ddGetLibList():
r = client.execute_skill(f'''
let((result)
result = nil
foreach(lib ddGetLibList()
when(ddGetObj(lib~>name "{CELL}")
result = cons(lib~>name result)))
result)
''')
# r.output e.g. '("2025_FIA")'
No need for a separate script — inline in any workflow that needs to locate a cell before operating on it.
from virtuoso_bridge.virtuoso.schematic import (
schematic_create_inst_by_master_name as inst,
schematic_create_pin as pin,
)
with client.schematic.edit(LIB, CELL) as sch:
# 1. Place instances — sch.add() queues SKILL commands
sch.add(inst("tsmcN28", "pch_mac", "symbol", "MP0", 0, 1.5, "R0"))
sch.add(inst("tsmcN28", "nch_mac", "symbol", "MN0", 0, 0, "R0"))
# 2. Label MOS terminals with stubs — NOT manual add_wire
sch.add_net_label_to_transistor("MP0",
drain_net="OUT", gate_net="IN", source_net="VDD", body_net="VDD")
sch.add_net_label_to_transistor("MN0",
drain_net="OUT", gate_net="IN", source_net="VSS", body_net="VSS")
# 3. Add pins at circuit EDGE, not on terminals
sch.add(pin("IN", -1.0, 0.75, "R0", direction="input"))
sch.add(pin("OUT", -1.0, 0.25, "R0", direction="output"))
# schCheck + dbSave happen automatically on context exit
Key rules:
Use add_net_label_to_transistor for MOS D/G/S/B — it auto-detects stub direction. Never manually add_wire between terminals.
Pins go at the circuit edge, not on instance terminals. They connect via matching net names.
Delete before recreate — if the cell already exists, add_instance accumulates on top of old instances:
client.execute_skill(f'ddDeleteObj(ddGetObj("{LIB}" "{CELL}"))')
CDF parameters — two-step process:
Step 1: Set values with schHiReplace (Edit > Replace). Do NOT use param~>value = or dbSetq — they don't update display or derived params.
client.execute_skill(
'schHiReplace(?replaceAll t ?propName "cellName" ?condOp "==" '
'?propValue "pch_mac" ?newPropName "w" ?newPropValue "500n")')
Step 2: Trigger CDF callbacks with CCSinvokeCdfCallbacks to update derived parameters (finger_width, display annotations, etc.). Use ?order to run only the changed params — running all callbacks may fail on PDK-specific variables like mdlDir.
# Must load CCSinvokeCdfCallbacks.il first (one-time)
client.upload_file("reference/CCSinvokeCdfCallbacks.il", "/tmp/CCSinvokeCdfCallbacks.il")
client.execute_skill('load("/tmp/CCSinvokeCdfCallbacks.il")')
# Trigger only the callbacks you need
client.execute_skill('CCSinvokeCdfCallbacks(geGetEditCellView() ?order list("fingers"))')
Critical: PDK devices have nf as read-only. Use fingers instead:
# ✅ "fingers" is editable, "nf" is not
client.execute_skill(
'schHiReplace(?replaceAll t ?propName "cellName" ?condOp "==" '
'?propValue "pch_mac" ?newPropName "fingers" ?newPropValue "4")')
# ❌ schHiReplace(...?newPropName "nf" ...) → SCH-1725 "not editable"
Why two steps: schHiReplace changes the stored property but does NOT trigger CDF callbacks. Without callbacks, derived params (finger_width, m_ov_nf annotations) stay stale. CCSinvokeCdfCallbacks(?order ...) triggers only the specified callbacks, avoiding PDK errors from unrelated callbacks.
Or use the Python wrapper which handles both steps:
from virtuoso_bridge.virtuoso.schematic.params import set_instance_params
set_instance_params(client, "MP0", w="500n", l="30n", nf="4", m="2")
Always use the Python API functions below. Do NOT hand-write SKILL for reading.
from virtuoso_bridge import VirtuosoClient, decode_skill_output
client = VirtuosoClient.from_env()
LIB, CELL = "myLib", "myCell"
# 1. Schematic — default: topology only (no positions/geometry)
from virtuoso_bridge.virtuoso.schematic.reader import read_schematic
data = read_schematic(client, LIB, CELL, include_positions=False)
# data = {
# "instances": [{"name", "lib", "cell", "numInst", "view",
# "params": {...}, "terms": {...}}, ...],
# "nets": {"VN1": {"connections": ["M0.D", ...], "numBits": 1,
# "sigType": "signal", "isGlobal": false}, ...},
# "pins": {"VINP": {"direction": "input", "numBits": 1}, ...},
# "notes": [{"text": "...", ...}, ...]
# }
# With positions (only when you need xy/bBox, e.g. for layout-aware editing):
data_with_pos = read_schematic(client, LIB, CELL, include_positions=True)
# No CDF param filtering (return all 200+ PDK params):
raw = read_schematic(client, LIB, CELL, include_positions=False, param_filters=None)
# 2. Maestro — use open_session / read_config / close_session
from virtuoso_bridge.virtuoso.maestro import open_session, close_session, read_config
session = open_session(client, LIB, CELL) # maeOpenSetup (background, no GUI)
config = read_config(client, session) # dict of key -> (skill_expr, raw)
# config keys: maeGetSetup (tests), maeGetEnabledAnalysis, maeGetAnalysis:XXX,
# maeGetTestOutputs, variables, parameters, corners
close_session(client, session)
# IMPORTANT: Do NOT use deOpenCellView for maestro — it opens read-only and
# returns incomplete data. Always use open_session (= maeOpenSetup).
# 3. Netlist — generate from maestro session, download via SSH
session = open_session(client, LIB, CELL)
test = decode_skill_output(
client.execute_skill(f'car(maeGetSetup(?session "{session}"))').output)
client.execute_skill(
f'maeCreateNetlistForCorner("{test}" "Nominal" "/tmp/nl_{CELL}" ?session "{session}")')
client.download_file(f"/tmp/nl_{CELL}/netlist/input.scs", "output/netlist.scs")
close_session(client, session)
Follow this sequence exactly. Do not skip steps.
session = "fnxSession33" # from find_open_session() or maeGetSessions()
# 1. Set variables
client.execute_skill(f'maeSetVar("CL" "1p" ?session "{session}")')
# 2. Save before running — REQUIRED, skipping causes stale state
client.execute_skill(
f'maeSaveSetup(?lib "{LIB}" ?cell "{CELL}" ?view "maestro" ?session "{session}")')
# 3. Run (async — NEVER use ?waitUntilDone t, it deadlocks the event loop)
r = client.execute_skill(f'maeRunSimulation(?session "{session}")', timeout=30)
history = (r.output or "").strip('"')
# 4. Wait — blocks until simulation finishes (GUI mode only)
r = client.execute_skill("maeWaitUntilDone('All)", timeout=300)
# 5. Check for GUI dialog blockage — if wait returned empty/nil,
# a dialog is blocking CIW. Try dismissing it:
if not r.output or r.output.strip() in ("", "nil"):
client.execute_skill("hiFormDone(hiGetCurrentForm())", timeout=5)
# If still stuck, user must manually dismiss the dialog in Virtuoso
# 6. Read results
client.execute_skill(f'maeOpenResults(?history "{history}")', timeout=15)
r = client.execute_skill(f'maeGetOutputValue("myOutput" "myTest")', timeout=30)
value = float(r.output) if r.output else None
client.execute_skill("maeCloseResults()", timeout=10)
Apply these rules whenever you read or export any maestro output (scalar or waveform):
History binding is mandatory
history returned by maeRunSimulation().history explicitly to result readers/exporters (for example, read_results(..., history=history) and export_waveform(..., history=history)).Remote filename must be unique per export
/tmp/vb_wave_xxx.txt path./tmp/vb_wave_<history>_<timestamp>_<nonce>.txt.Bind results directory to the same history before ocnPrint
maeOpenResults(?history ...), verify the resolved resultsDir contains /<history>/.In optimization loops: add maeSaveSetup and dialog-recovery in every iteration. GUI dialogs ("Specify history name", "No analyses enabled") block the entire SKILL channel — all subsequent execute_skill calls will timeout until the dialog is dismissed.
Debug with screenshots: if simulation appears stuck or results are unexpected, capture the Maestro window to see its current state:
client.execute_skill('''
hiWindowSaveImage(
?target hiGetCurrentWindow()
?path "/tmp/debug_maestro.png"
?format "png"
?toplevel t
)
''')
client.download_file("/tmp/debug_maestro.png", "output/debug_maestro.png")
This reveals dialog boxes, error messages, or unexpected variable values that are invisible through the SKILL channel alone.
Symptom: maeGetOutputValue("bandwidth(...)" testName) returns nil, but maeGetOutputValue("Noise_rms_out" testName) returns a value.
Root Cause: The PSF directory contains no actual waveform data files. Check with:
# SSH to remote and check PSF directory
ssh zhangz@zhangz-wei "ls /server_local_ssd/.../Interactive.N/psf/<test>/psf/"
# Expected: .raw, simdata, spectre.log files
# Actual: only spectre.out, variables_file (NO waveform data!)
Why this happens:
Noise_rms_out) to the RDBCheck the RDB directly:
ssh zhangz@zhangz-wei "sqlite3 .../Interactive.N.rdb 'SELECT * FROM resultValue'"
# Returns rows like:
# 1|7|0.000469 -> Noise_rms_out scalar (saved)
# 1|8|wave -> VF(/VOUT)/VF(/VSIN) is a waveform reference, not saved!
Solution: Enable "save all" option before running simulation:
client.execute_skill(f'maeSetEnvOption("{test}" ?option "save" ?value "all")')
client.execute_skill('maeSaveSetup()')
When Maestro OCEAN functions fail (due to missing PSF waveform data), parse the .log file:
def read_maestro_results_from_log(client, LIB, CELL, history):
"""Read simulation results from the log file - most reliable method."""
# Determine base path - adjust for your project structure
base = f"/home/zhangz/TSMC28N/2025_LLM_AGENT/{LIB}"
log_path = f"{base}/{CELL}/maestro/results/maestro/{history}.log"
client.download_file(log_path, "/tmp/sim.log")
# Parse tab-separated format: "expression\t\tvalue"
results = {}
with open("/tmp/sim.log") as f:
for line in f:
if "\t\t" in line:
parts = line.rstrip().split("\t\t")
if len(parts) >= 2:
name = parts[0].strip()
value = parts[1].strip()
# Skip header lines
if name and value and "corner" not in name.lower():
results[name] = value
return results
# Full workflow:
from virtuoso_bridge import VirtuosoClient
from virtuoso_bridge.virtuoso.maestro import open_gui_session, run_and_wait, close_gui_session
client = VirtuosoClient.from_env()
LIB, CELL = "PLAYGROUND_AMP", "TB_AMP_5T_D2S_DC_AC"
session = open_gui_session(client, LIB, CELL) # GUI mode required for results
history, _ = run_and_wait(client, session=session, timeout=300)
h = history.strip('"')
results = read_maestro_results_from_log(client, LIB, CELL, h)
print(results)
# {'bandwidth(...)': '1.64M', 'dB20(...)': '10.93', ...}
close_gui_session(client, session, save=False)
Log format in the file:
bandwidth(abs((VF("/VOUT") / VF("/VSIN"))) 3 "low") 1.64M
dB20(value(abs((VF("/VOUT") / VF("/VSIN"))) 10000)) 10.93
value(abs((VF("/VOUT") / VF("/VSIN"))) 10000) 3.519
Noise_rms_out 469u
When execute_skill() times out, possible causes:
| Cause | Symptom | Fix |
|---|---|---|
| Modal dialog | GUI popup blocking CIW | virtuoso-bridge dismiss-dialog |
| Long operation | Simulation or netlist running | Wait, or use ?waitUntilDone nil |
| CIW input prompt | CIW waiting for typed input | dismiss-dialog (sends Enter) |
| Bridge disconnected | All calls fail immediately | virtuoso-bridge restart |
Dialog recovery (bypasses SKILL, uses X11 directly):
# Find and dismiss all blocking Virtuoso dialogs
virtuoso-bridge dismiss-dialog
# From Python
client.dismiss_dialog()
Uses xwininfo to find virtuoso-owned dialog windows and XTestFakeKeyEvent to send Enter. Works even when the SKILL channel is completely stuck.
Prevention: Always dbSave(cv) before hiCloseWindow(win). Never use ?waitUntilDone t in simulation calls. Add dialog-recovery in simulation loops (see "Run a simulation" section).
.scs netlist and wants to run it directly.