virtuoso

star 444

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.

Arcadia-1 By Arcadia-1 schedule Updated 6/11/2026

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:

  1. Search references/ for the function name or keyword
  2. Check examples/ for a working example of the same operation
  3. Read the actual function signature (help() for Python, references/*.md for 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-bridge is a Python CLI. Use uv + 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)

  1. Check .env — the bridge looks up .env in this order: --env FILE (CLI flag) → first parent .env that looks like a Virtuoso Bridge config (VB_REMOTE_HOST or VB_LOCAL_PORT) → ~/.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 (user-level, shared across projects). If the user already told you their SSH target, prefer virtuoso-bridge init user@host [-J user@jump] to fill host/user/jump + port in one step; otherwise plain virtuoso-bridge init writes an empty template for them to edit.
  2. virtuoso-bridge start — starts the local bridge service and SSH tunnel.
  3. If status is degraded — the user must load the setup script in Virtuoso CIW (the start output tells them exactly what to run).
  4. virtuoso-bridge status — verify everything is healthy before proceeding.
  5. virtuoso-bridge windows — list all open Virtuoso windows (num + name).
  6. virtuoso-bridge eval 'EXPR' — run a one-line SKILL expression from the shell and print the full VirtuosoResult JSON.
  7. virtuoso-bridge eval --stdin — run multi-line SKILL from stdin; the CLI auto-wraps multiple forms in progn(...) and returns the last form.
  8. virtuoso-bridge load FILE.il — run a .il file in the live Virtuoso session; uploads the file automatically in SSH mode.
  9. virtuoso-bridge screenshot [ciw|current|N] [-o DIR|FILE] — screenshot a window. Default target is CIW; default output is the user artifact screenshots directory.
  10. 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, nilNone, tTrue, 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). fetch does it all in one hop (200 ms). If you need the REPL-style ergonomics, use 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 use load_il() instead (see 03_load_il.py)

IMPORTANT: Always write .py files, never use python -c. python -c "..." has three layers of quoting (shell + Python + SKILL). \\n easily becomes \\\\n, causing printf to silently produce no output. Always write code to a .py file and run python 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 expressions
  • 02_ciw_print.py — print messages to CIW (one execute_skill per line)
  • 03_load_il.py — upload and load .il files
  • 04_list_library_cells.py — list libraries and cells
  • 05_multiline_skill.py — multi-line SKILL with comments, loops, procedures
  • 06_screenshot.py — capture layout/schematic screenshots

examples/01_virtuoso/schematic/

  • 01a_create_rc_stepwise.py — create RC schematic via operations
  • 01b_create_rc_load_skill.py — create RC schematic via .il script
  • 02_read_connectivity.py — read instance connections and nets
  • 03_read_instance_params.py — read CDF instance parameters
  • 04_test_set_instance_params_analoglib.py — update analogLib instance parameters
  • 05_rename_instance.py — rename schematic instances
  • 06_delete_instance.py — delete instances
  • 07_delete_cell.py — delete cells from library
  • 08_import_cdl_cap_array.py — import CDL netlist via spiceIn (SSH)
  • 09_create_pins.py — create schematic pins
  • 10_create_wire.py — draw wires between pins
  • 11_read_schematic_unified.py — read instances, nets, pins, geometry, and parameters

examples/01_virtuoso/layout/

  • 01_create_layout.py — create layout with rects, paths, instances
  • 02_add_polygon.py — add polygons
  • 03_add_via.py — add vias
  • 04_multilayer_routing.py — multi-layer routing
  • 05_bus_routing.py — bus routing
  • 06_read_layout.py — read layout shapes
  • 07–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 waveforms
  • 07_ensure_maestro_view.py — bootstrap a missing maestro cellview (maeOpenSetup + maeSaveSetup) before open_gui_session
  • 08_set_simulator_mode.py — switch between APS / Spectre X (LX/MX/AX/VX/CX) / Spectre FX via asiSetHighPerformanceOptionVal
  • 09_export_sweep_subpoints.py — pull per-sweep-point waveforms via OCEAN openResults(<abs path>) (works around maeOpenResults rejecting Interactive.N/M)

examples/01_virtuoso/veriloga/

  • import_veriloga.py — turn a local .va file 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 .va contents are out of scope; sample.va is a trivial placeholder.

examples/01_virtuoso/diagnostics/

  • sniff_cdslck.py — walk a library tree and report .cdslck lock-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 via strmin
  • import_verilog.py — schematic + symbol via ihdl batch (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-cell needed, 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_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")
    

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):

  1. History binding is mandatory

    • Always use the exact history returned by maeRunSimulation().
    • Pass that history explicitly to result readers/exporters (for example, read_results(..., history=history) and export_waveform(..., history=history)).
    • Do not rely on "latest" history inference when reproducibility matters.
  2. Remote filename must be unique per export

    • Never use a fixed /tmp/vb_wave_xxx.txt path.
    • Use unique naming such as /tmp/vb_wave_<history>_<timestamp>_<nonce>.txt.
    • This avoids collisions with stale files from previous runs or other users.
  3. Bind results directory to the same history before ocnPrint

    • After maeOpenResults(?history ...), verify the resolved resultsDir contains /<history>/.
    • If mismatch is detected, stop and raise an error instead of exporting the wrong waveform.

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 .scs netlist and wants to run it directly.
Install via CLI
npx skills add https://github.com/Arcadia-1/virtuoso-bridge-lite --skill virtuoso
Repository Details
star Stars 444
call_split Forks 107
navigation Branch main
article Path SKILL.md
More from Creator