name: virtuoso description: "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."
Virtuoso Skill
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.
Mental Model
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 │
└──────────────────┘ └──────────────────┘
Three abstraction levels
| 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.
Four domains
| 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 |
| SKILL Finder | Search SKILL function names and get detailed docs | client.find_skill(), client.get_skill_more_info() |
references/skill-finder-python-api.md |
| General | File transfer, screenshots, raw SKILL, .il loading | client.* |
See below |
Before you start
Environment setup
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.
Connection sequence (follow in order)
- Check
.env— the bridge looks up.envin this order:--env FILE(CLI flag) → first parent.envthat looks like a Virtuoso Bridge config (VB_REMOTE_HOSTorVB_LOCAL_PORT) →~/.virtuoso-bridge/.env(user-level). If any of these exists, skipinit. Only runvirtuoso-bridge initwhen none exist — it creates~/.virtuoso-bridge/.env(user-level, shared across projects). If the user already told you their SSH target, prefervirtuoso-bridge init user@host [-J user@jump]to fill host/user/jump + port in one step; otherwise plainvirtuoso-bridge initwrites an empty template for them to edit. virtuoso-bridge start— starts the local bridge service and SSH tunnel.- If status is
degraded— the user must load the setup script in Virtuoso CIW (thestartoutput tells them exactly what to run). virtuoso-bridge status— verify everything ishealthybefore proceeding.virtuoso-bridge windows— list all open Virtuoso windows (num + name).virtuoso-bridge eval 'EXPR'— run a one-line SKILL expression from the shell and print the fullVirtuosoResultJSON.virtuoso-bridge eval --stdin— run multi-line SKILL from stdin; the CLI auto-wraps multiple forms inprogn(...)and returns the last form.virtuoso-bridge load FILE.il— run a.ilfile in the live Virtuoso session; uploads the file automatically in SSH mode.virtuoso-bridge screenshot [ciw|current|N] [-o DIR|FILE]— screenshot a window. Default target is CIW; default output is the user artifact screenshots directory.virtuoso-bridge snapshot -o <dir>— dump the currently-focused maestro window to<dir>/<YYYYMMDD_HHMMSS>__<lib>__<cell>/(state XMLs, SKILL probe output, per-point netlist + PSF results,.rdb). This is the default way to capture Maestro state — no Python required. Use the Python API (below) only inside a multi-step pipeline.
Then
- Check examples first:
examples/01_virtuoso/— don't reinvent from scratch. - Open the window:
client.open_window(lib, cell, view="layout")so the user sees what you're doing.
Client basics
Direct CLI SKILL execution
For quick checks and one-off SKILL files, prefer the CLI over writing a Python wrapper. It uses the same bridge connection and avoids shell/Python/SKILL triple-quoting problems.
# One-line expression -- full VirtuosoResult JSON on stdout
virtuoso-bridge eval 'getCurrentTime()'
# Multi-line SKILL -- auto-wrapped in progn when needed
virtuoso-bridge eval --stdin <<'EOF'
let((libs)
libs = mapcar(lambda((l) l~>name) ddGetLibList())
printf("found %d libraries\n" length(libs))
libs)
EOF
# Whole .il file -- uploaded automatically in SSH mode
virtuoso-bridge load my_script.il
Use Python only when the SKILL call is one step in a larger scripted workflow or when you need structured high-level APIs such as schematic/layout editors.
Python client
from virtuoso_bridge import VirtuosoClient
client = VirtuosoClient.from_env()
client.execute_skill('...') # run SKILL expression
client.fetch(expr, fields) # batch ~>slot extract (see below)
client.fetch_one(expr, fields) # single-object ~>slot extract
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(target="ciw") # screenshot to the user artifact directory
client.screenshot(output="output", target="ciw") # explicit repo-local output
Batch attribute fetch: fetch() / fetch_one()
execute_skill() is a raw-string in, raw-string out channel. For DFII
objects it returns an opaque handle ("db:0x2800ccbe") that's useless
by itself — to get attributes you'd have to send another SKILL call
per attribute, which is both verbose and slow (~100 ms per
round-trip).
fetch(expr, fields) does the right thing in one round-trip:
sends mapcar(lambda((o) list(o~>f1 o~>f2 ...)) <expr>), parses the
SKILL s-expression response, and returns a list of Python dicts.
# List of selected schematic objects in one call
objs = client.fetch("geGetSelSet()", ["objType", "cellName", "name"])
# [{"objType": "inst", "cellName": "nch_mac", "name": "M1"},
# {"objType": "inst", "cellName": "pch_mac", "name": "M2"}, ...]
print(objs[0]["name"]) # → 'M1'
# All instances in the current schematic — 1 call, not N×fields
insts = client.fetch(
"geGetEditCellView()~>instances",
["name", "cellName", "libName", "viewName"],
)
fetch_one(expr, fields) is the single-object variant — wraps in
list(...) and returns one dict:
cv = client.fetch_one("geGetEditCellView()",
["libName", "cellName", "viewName"])
# {"libName": "PLAYGROUND", "cellName": "AMP", "viewName": "schematic"}
Value decoding (both methods): strings unquoted, nil →
None, t → True, nested SKILL lists → nested Python lists,
bare atoms (numbers / symbols) returned as strings so the caller can
coerce (int(d["fingers"])).
Why not a client["fn"]() lazy-proxy style (à la skillbridge)?
Lazy proxies look nicer syntactically but trigger one round-trip per
attribute access — 100 selected objects × 3 fields = 300 ssh hops
(30 s). 200 ms). If you need the
REPL-style ergonomics, use fetch does it all in one hop (skillbridge alongside this bridge —
they coexist fine on the same Virtuoso session.
CIW output vs return value
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
Printing multi-line text to CIW
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:
- ASCII only — emojis and CJK characters cause a JSON encoding error on the remote SKILL interpreter
- No unescaped SKILL special chars in the text — if the line may contain
"or%, escape them (\\",%%) or useload_il()instead (see03_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
References
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 |
snapshot() (raw SKILL sections) + filter_*_xml + writer functions; read_results (per-point × per-output CSV) + export_waveform (OCEAN) |
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/cellview-on-disk-layout.md |
What's inside each view on disk (sch.oa, data.dm binary format, maestro.sdb/active.state XML skeleton, lock files, SOS markers); which files are text-editable vs must go through DFII API |
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 |
references/skill-finder-python-api.md |
skill-find (search SKILL by name) and skill-info (More Info docs) |
Examples
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 (oneexecute_skillper 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 screenshots
examples/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 parameters04_test_set_instance_params_analoglib.py— update analogLib 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)09_create_pins.py— create schematic pins10_create_wire.py— draw wires between pins11_read_schematic_unified.py— read instances, nets, pins, geometry, and parameters
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 operations
examples/01_virtuoso/maestro/
01_read_focused_maestro.py— in-memory snapshot of the focused maestro (config + env + results + outputs + corners + variables)02_snapshot_with_metrics.py— snapshot the focused maestro to a timestamped directory (disk artifacts)03_bg_open_read_close_maestro.py— background open → read config → close (no GUI window)04_gui_open_snapshot_close.py— GUI open → snapshot artifacts → close (owns lifecycle)05_gui_session_lifecycle.py— GUI session lifecycle integration test (open/close edge cases)06a_rc_create.py— create RC schematic + Maestro setup (cell name auto-timestamped)06b_rc_simulate_and_read.py— run simulation in background, read results, export waveforms07_ensure_maestro_view.py— bootstrap a missing maestro cellview (maeOpenSetup+maeSaveSetup) beforeopen_gui_session08_set_simulator_mode.py— switch between APS / Spectre X (LX/MX/AX/VX/CX) / Spectre FX viaasiSetHighPerformanceOptionVal09_export_sweep_subpoints.py— pull per-sweep-point waveforms via OCEANopenResults(<abs path>)(works aroundmaeOpenResultsrejectingInteractive.N/M)
examples/01_virtuoso/veriloga/
import_veriloga.py— turn a local.vafile into a Cadence Verilog-A cellview via the 5-step IC618 path: placeholder schematic → symbol → veriloga skeleton → upload .va → reparse. This example covers the file/cellview interface only — the.vacontents are out of scope;sample.vais a trivial placeholder.
examples/01_virtuoso/diagnostics/
sniff_cdslck.py— walk a library tree and report.cdslcklock-file owners. Authoritative when SKILL-side session enumeration disagrees with on-disk reality.
examples/01_virtuoso/digital_import/
Hand off Genus/Innovus P&R products into a Virtuoso library. All three scripts wrap standalone Cadence batch tools (strmin / ihdl) via SKILL system() — no GUI forms, no manual bootstrap. See that folder's README.md for prerequisites, PDK-portability notes, and full CLI reference.
import_gds.py— routed layout viastrminimport_verilog.py— schematic + symbol viaihdlbatch (the official CLI entry point for Verilog Import)add_power_labels.py— drop VDD/VSS labels on a routed layout by reflectively reading std-cell pin geometry (no--ref-cellneeded, auto-discovers)
Common workflows
Find which library contains a cell
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.
Create a schematic
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_transistorfor MOS D/G/S/B — it auto-detects stub direction. Never manuallyadd_wirebetween 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_instanceaccumulates 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 useparam~>value =ordbSetq— 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
CCSinvokeCdfCallbacksto update derived parameters (finger_width, display annotations, etc.). Use?orderto run only the changed params — running all callbacks may fail on PDK-specific variables likemdlDir.# 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
nfas read-only. Usefingersinstead:# ✅ "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:
schHiReplacechanges 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")
Read a design (schematic + maestro + netlist)
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 — snapshot the focused window
#
# PREFER THE CLI for one-shot captures. The CLI handles venv + client
# construction, and on-disk output is everything you need for
# analysis (state XMLs, SKILL probe text, per-point psf/* results,
# .rdb, netlist/). Python for this case is pure boilerplate.
#
# $ virtuoso-bridge snapshot -o output/
#
# Use the Python API only when snapshot is one step in a larger
# same-connection pipeline (e.g. open_session → snapshot →
# run_simulation → close_session, or a loop over many cells):
from virtuoso_bridge.virtuoso.maestro import snapshot
d = snapshot(client) # SKILL-only, ~150ms, 1 round-trip
# d["raw_sections"] = [(probe_skill_text, raw_output), ...]
# Each label IS the actual SKILL string we ran (e.g.
# 'maeGetAnalysis("test" "ac" ?session "fnxSession18")');
# value is the verbatim SKILL alist — no Python parsing.
# d also has session / lib / cell / view / mode / unsaved.
# Full disk dump (raw + YAML-filtered XMLs + 16 SKILL probes + per-point
# inputs + spectre results + .rdb):
d = snapshot(client, output_root="output/") # → d["output_dir"]
# IMPORTANT: snapshot() always uses the CURRENTLY FOCUSED maestro window.
# Click the desired ADE Assembler first, or use open_session() to bring it up.
# Rule of thumb: one-shot inspection → CLI; multi-step pipeline → Python.
# 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)
Run a simulation
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
# For per-point x per-output results across sweeps/corners -> use read_results
# (see references/simulation-flow.md). For ad-hoc single-output reads:
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)
Output read/export guardrails (collision-safe)
Apply these rules whenever you read or export any maestro output (scalar or waveform):
History binding is mandatory
- Always use the exact
historyreturned bymaeRunSimulation(). - Pass that
historyexplicitly to result readers/exporters (for example,read_results(..., history=history)andexport_waveform(..., history=history)). - Do not rely on "latest" history inference when reproducibility matters.
- Always use the exact
Remote filename must be unique per export
- Never use a fixed
/tmp/vb_wave_xxx.txtpath. - Use unique naming such as
/tmp/vb_wave_<history>_<timestamp>_<nonce>.txt. - This avoids collisions with stale files from previous runs or other users.
- Never use a fixed
Bind results directory to the same history before ocnPrint
- After
maeOpenResults(?history ...), verify the resolvedresultsDircontains/<history>/. - If mismatch is detected, stop and raise an error instead of exporting the wrong waveform.
- After
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.
Root Cause: Why maeGetOutputValue returns nil for computed expressions
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:
- Maestro saves only pre-computed scalar outputs (like
Noise_rms_out) to the RDB - Raw waveforms (VOUT, VSIN signals) are NOT saved to PSF unless "save=all" is enabled
- Computed expressions (bandwidth, dB20, value) need the waveform data to calculate — returns nil
Check 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()')
Reliable Result Reading: Parse the Log File
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."""
# Resolve the OA library path via SKILL — works on any setup,
# no hardcoded ``/home/USER/...`` assumption.
r = client.execute_skill(f'ddGetObj("{LIB}")~>readPath')
lib_path = (r.output or "").strip().strip('"')
log_path = f"{lib_path}/{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
SKILL channel timeout — diagnosis and recovery
When execute_skill() times out, possible causes:
| Cause | Symptom | Fix |
|---|---|---|
| Modal dialog | GUI popup blocking CIW | virtuoso-bridge dismiss-dialog |
| Auto dialog finder missed a modal | GUI popup visible, SKILL channel blocked | virtuoso-bridge list-windows --json, then virtuoso-bridge dismiss-window WINDOW_ID --action enter |
| 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
# Inspect X11 windows and dismiss one explicitly
virtuoso-bridge list-windows --json
virtuoso-bridge dismiss-window 0x4203583 --action enter
# From Python
client.dismiss_dialog()
Uses xwininfo to find virtuoso-owned dialog windows and XTestFakeKeyEvent to send the requested key action. 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).
Related skills
- spectre — standalone netlist-driven Spectre simulation (no Virtuoso GUI). Use when the user has a
.scsnetlist and wants to run it directly.