Quantum-as-a-Service for VS Code
Run OpenQASM 3 and Qiskit circuits on the VLQ quantum computer from VS Code, through
LEXIS and HEAppE, using the
qaas Python package (QHEAppE client).
Features and roadmap
Status: ✅ available and tested (live items verified on VLQ, 2026-11-11) · 🔶 available, not yet verified against live VLQ · ⚠️ available, blocked by a server-side issue · 🗓️ planned
Python environment
| Feature |
Status |
Notes |
| Find interpreters: workspace venvs, conda / miniconda / miniforge envs, pyenv versions, Python extension environments, system Python |
✅ |
QaaS: Select Python Interpreter shows each one's Python version and installed qaas |
| Create a Python 3.12 venv (from any 3.12 interpreter, e.g. pyenv) or a conda env, then install qaas |
✅ |
QaaS: Create Python Environment for qaas |
Install / upgrade qaas[<extra>]>=1.0.0,<2 from PyPI |
✅ |
Runs python -m pip as a VS Code task. Checks for Python 3.12 and warns outside a venv/conda env |
| Environment check (qaas, qiskit, iqm-client, iqm-pulla, py4lexis versions) |
✅ |
Flags qaas older than 1.0.0. Runs before LEXIS commands: an unsuitable interpreter (e.g. macOS's Python 3.9) gets Select Interpreter / Create Environment / Install qaas instead of a crash |
Login and backends
| Feature |
Status |
Notes |
| Synchronize with LEXIS Platform: sign in, check the backend, import jobs from HEAppE, refresh active jobs and the calibration set |
🔶 |
Jobs view title bar and welcome screen. Each step is verified on VLQ; the combined flow is new |
| Sign-in prompt before any LEXIS command when not signed in: browser login, paste a refresh/access token, or a dev connection file |
🔶 |
QaaS: Sign in to LEXIS… switches account or method |
| LEXIS browser login, kept for the whole VS Code session |
🔶 |
The login URL is also offered as a notification (works on remote hosts) |
| LEXIS token in VS Code secret storage |
✅ |
QaaS: Set LEXIS Token / Log Out of LEXIS (also in the Jobs view's ⋯ menu). A refresh/offline token is renewed automatically; an access token is used as is. Tokens are stored per LEXIS instance and only sent to that instance. Log out revokes the refresh tokens on the server (stored ones and this session's browser logins) and clears them; a token in the dev file or QAAS_LEXIS_TOKEN is revoked only if you confirm, since other tools may share it. Verified with both on production and the test instance |
| Other LEXIS instances (test instance) via settings or a git-ignored dev connection file |
✅ |
See Testing against the LEXIS test instance. Status bar shows LEXIS test; jobs remember their instance |
QaaS: Test LEXIS Connection |
✅ |
Logs in, shows the user and token expiry, lists backends |
| List backends and choose the default |
✅ |
Status bar shows the current backend |
| Topology view: qubit connectivity, calibration fidelities, chosen layout |
🗓️ |
|
| Remaining allocation in the status bar |
🗓️ |
|
OpenQASM 3
| Feature |
Status |
Notes |
.qasm language: syntax highlighting, comments, brackets |
✅ |
|
| Errors shown in the editor (Qiskit OpenQASM 3 importer, on open and save) |
✅ |
No login needed |
| Transpile for VLQ: diff of original vs. transpiled QASM, depth / 2-qubit gates / layout |
✅ |
Transpile above the file or in the editor title. Shows VLQ's native r, cz and move (via the resonator) |
| Run on VLQ, with shots prompt and billed-time estimate before submitting |
✅ |
Several .qasm files selected in the explorer are sent as one job |
input parameters with value prompts and sweeps |
🗓️ |
|
Python scripts and notebooks
| Feature |
Status |
Notes |
Billed-time estimate above every backend.run(...) and pulla.submit_playlist(...) |
✅ |
In .py files and .ipynb cells. Reads shots and circuit counts from literals and simple assignments, including earlier notebook cells |
| Run Python file on VLQ, with LEXIS settings passed as environment variables |
✅ |
Asks for confirmation with the total estimate |
| Track jobs submitted from scripts automatically |
🗓️ |
For now: Import Jobs from HEAppE |
| Results renderer for notebook outputs |
🗓️ |
|
Job manager
| Feature |
Status |
Notes |
| Jobs view with state, shots, estimate, calibration set and source file |
✅ |
|
| Jobs survive restarts; active jobs are refreshed in the background |
✅ |
Background refresh starts only when no login prompt can appear |
Import past jobs from HEAppE (ListJobsForCurrentUser) |
✅ |
|
Jobs grouped into Own and accessible and Other users' jobs (no access), labelled by LEXIS resource (e.g. VLQ-EU) |
🔶 |
HEAppE lists every user's jobs to Managers/Administrators but only lets the submitter read them. The no-access group (by resource) has no actions and is never polled; a 403 moves an imported job there. Ownership and resources verified on VLQ |
| Cancel, copy ID, remove from list |
⚠️ |
Copy ID and remove work. Cancel reaches HEAppE, but HEAppE sends QScheduler session:X:task:Y minus the prefix as a session ID, which QScheduler rejects (HTTP 502); reported to HEAppE. The extension reports the refusal |
| Notification when a job finishes; optional automatic download |
✅ |
|
Results and analysis
| Feature |
Status |
Notes |
Download results to a configurable path (qaas.resultsDirectory) or a folder you pick |
✅ |
Writes counts.json, counts.csv, raw_artifacts.json, job.json; shots.csv with per-shot measurements (qaas.downloadMeasurements) |
| Download several selected jobs at once |
✅ |
|
| Results view: histogram, shots, distinct outcomes, entropy, most frequent outcome, parity ⟨Z…Z⟩ |
✅ |
One section per circuit |
| Per-bit P(1) and ⟨Z⟩ |
✅ |
|
| Pairwise correlations (connected ⟨ZᵢZⱼ⟩ − ⟨Zᵢ⟩⟨Zⱼ⟩) as a heatmap |
✅ |
Up to 16 bits; larger registers in Data Wrangler or the notebook |
| Compare with the ideal simulation: Hellinger fidelity, total variation distance, overlaid histogram |
✅ |
Exact statevector probabilities of the submitted OpenQASM 3 circuits (up to 24 qubits, final measurements only) |
| Compare with another downloaded job |
✅ |
E.g. the same circuit on another calibration set |
| Tidy CSV export for other tools |
✅ |
Outcomes as \|0011⟩ so leading zeros survive type inference, plus one 0/1 column per bit (c0 = rightmost) |
| Open results in Data Wrangler |
✅ |
Optional; offers to install it. Column statistics of c0…cN give per-bit probabilities |
Analysis notebook (analysis.ipynb) next to the results |
✅ |
pandas + matplotlib: summary, per-bit statistics, correlations, histogram, per-shot data, ideal comparison. Never overwritten once created |
Billing estimate
| Feature |
Status |
Notes |
| Estimator in the sidebar: shots, circuits, submissions, circuit time |
✅ |
Also lists the submissions found in the active editor |
| Formula from the VLQ consumption guide |
✅ |
Checked against the guide's examples in unit tests |
| Exact circuit time from the transpiled circuit and calibration data (instead of the 0–50 µs range) |
🗓️ |
|
Calibrations
| Feature |
Status |
Notes |
| Calibrations view: the current calibration set and every set seen on jobs, in operator files or in earlier refreshes |
✅ |
HEAppE/QScheduler cannot list calibration sets, so the view lists known IDs; Open Calibration Set by ID… adds others. A new current set is announced with Compare |
| Fetch quality metrics of the current set or one by ID, cached locally |
✅ |
calibrationMetrics via QScheduler MachineCalibration (metrics). Sets IQM no longer serves are reported clearly. Verified on VLQ |
| Calibration panel: median tiles with the worst qubit, per-qubit dot plot with uncertainties, chip diagram (qubits around the resonator) coloured by any metric, table of all metrics per qubit with ⚠ on the two worst |
🔶 |
T1, T2*, T2 echo, PRX / Clifford / CZ / MOVE fidelity, readout fidelity and errors, QND; resonator T1/T2 and GHZ population in a separate table. Rendered with real VLQ data, not yet clicked through in VS Code |
| Compare two calibration sets: paired points, change per tile, table of changes (blue better / red worse) |
🔶 |
|
| Export a calibration set as tidy CSV (raw SI value, display value, uncertainty), optionally in Data Wrangler |
✅ |
|
| Calibration history over time (trend per qubit) |
🗓️ |
Needs IQM/QScheduler to list past calibration sets |
Pulse-level operator catalog
| Feature |
Status |
Notes |
Operators as files (.qaas/operators/*.yaml, also .yml/.json) for custom prx pulses |
✅ |
Versioned with your code. JSON schema gives completion and checks (with the YAML extension) |
Shapes: built-in IQM waveforms, expressions of t, sample points; Q channel zero, derivative of I (DRAG) or its own shape |
✅ |
One Python code generator for preview, submission and inserted code. Matches IQM's own waveforms sample for sample |
| Safe expressions |
✅ |
Parsed and allowlisted (arithmetic, pi, e, numpy math functions); a shared catalog cannot run arbitrary code |
| Pulse Operators view: operators, calibration entries per calibration set, values per qubit |
✅ |
Status: calibrated, draft, needs migration |
| Errors in operator files: YAML, schema, duplicate names, unknown waveforms, expressions, peak amplitude > 1 |
✅ |
Deep checks run in Python on save, no login needed |
| Waveform editor: live I/Q plot, values per qubit, checks against the hardware limits |
✅ |
Peak amplitude, sample count, duration granularity, comparison with the native gate |
| Calibration entries tied to the calibration set they were tuned on |
✅ |
Older entries are marked; a calibration set change is reported |
Current calibration set and native prx values from the backend |
✅ |
Refresh Current Calibration Set; cached, re-read before pulse runs after qaas.calibrationMaxAgeMinutes |
| Migrate an operator to the current calibration set |
✅ |
Rescale amplitudes by the native gate's change (default) or copy. Result is a draft until verified |
| Start a draft from the native gate (duration, amplitude) |
✅ |
For new operators or new calibration sets |
| Run OpenQASM 3 at pulse level with operators |
✅ |
// @qaas operator prx=<name> in the file, or Run with operator…. Asks before using draft or older values |
| Pulse jobs in the Jobs view, with operators and calibration set; result download |
✅ |
Same results view, CSV exports and notebook as circuit jobs. Also after a restart: the job is recompiled from its stored sources and operators to map results to bits |
Pulse-level jobs marked in the Jobs view (pulse icon, pulse-level), including imported ones (HEAppE task qpulla_pulse_task) |
✅ |
Imported pulse jobs (notebooks, scripts) are downloaded by rebuilding counts from their raw per-qubit readouts (QB1__c_2_0_1 = qubit, register, bit); identical to the exact conversion on VLQ. For parameter sweeps the shots of all sweep points are combined |
| Jobs whose results could not be fetched: warning icon, orange label, reason in the tooltip |
✅ |
Cleared by the next successful download |
Insert Python code for an operator (classes, calibration, register_qaas_operators / apply_qaas_operators) |
✅ |
For scripts and notebooks using QPulla |
| Rabi amplitude calibration notebook that writes the result back |
✅ |
Generated per operator and qubit; 7 jobs, estimate shown before creating. Fits the Rabi curve to all rounds (weighted by shots). Uses LEXIS_TOKEN when set, otherwise the browser login. The result file is offered for saving into the operator |
defcal in OpenQASM 3 |
🗓️ |
Qiskit's OpenQASM 3 importer does not support defcal yet; the comment directive is used instead |
| Pulse schedule timeline after compilation |
🗓️ |
|
| Ramsey and T1 calibration recipes |
🗓️ |
|
Operators for other gates (cz, measure, move) |
🗓️ |
Only prx (PRX_CustomWaveforms) for now |
Developed and tested on macOS (Apple silicon, VS Code 1.145). Compatibility with Windows and Linux
still has to be checked. Areas that differ per platform and need testing there:
| Area |
What to check |
| Interpreter discovery |
venv layout (Scripts\python.exe on Windows), conda / miniconda / miniforge locations, pyenv vs. pyenv-win, the Microsoft Store Python stub |
| Environment creation and qaas install |
python -m venv, conda create, pip as a VS Code task in PowerShell / cmd |
| Run Python File on VLQ |
Terminal quoting and environment variables in PowerShell and cmd |
| Python bridge |
Process start and the JSON-lines protocol on Windows (stdin/stdout duplication, line endings), browser-login notification |
| Paths |
qaas.resultsDirectory variables, ~ expansion, .qaas/ files, downloads with backslashes |
| Webviews and Data Wrangler |
Results and calibration panels, waveform editor, openWith for Data Wrangler |
| Platform |
Status |
| macOS |
✅ |
| Linux |
🗓️ to be checked |
| Windows |
🗓️ to be checked |
Operator files
# .qaas/operators/hann_drag.yaml
schema: qaas-operator/v1
name: hann_drag # implementation name for the IQM compiler
gate: prx
description: Hann pulse with DRAG
parameters: # shared by I and Q; unit s = seconds, Hz, or dimensionless
width: { default: 0.2 }
waveform:
i: { type: expression, expression: "cos(pi * t) ** 2" } # t in [-1/2, 1/2] of the pulse
q: { type: derivative } # or zero / builtin / expression / samples
calibrations:
- calibrationSetId: 7f3c… # calibration set the values were tuned on
backend: VLQ
status: calibrated # or draft (seeded / migrated, not verified)
verifiedLoci: [QB1]
reference: { implementation: drag_crf, loci: { QB1: { duration: 4.0e-8, amplitude_i: 0.31 } } }
loci:
QB1: { duration: 4.0e-8, amplitude_i: 0.42, amplitude_q: 0.02 }
- Built-in shapes:
{ type: builtin, name: Gaussian } and other waveforms from iqm.models.playlist.waveforms.
Their fields become parameters (time-like ones in seconds).
- Expressions: arithmetic,
**, %, pi, e, comparisons inside where, and sin cos tan arcsin arccos arctan sinh cosh tanh exp log sqrt abs sign minimum maximum clip where heaviside.
- Samples: values in [-1, 1], linearly interpolated over the pulse.
- Values per qubit:
duration, amplitude_i, amplitude_q, and any parameter to override for that qubit.
- OpenQASM 3: add
// @qaas operator prx=hann_drag to run the file at pulse level with the operator.
- Calibration results from the Rabi notebook are written to
.qaas/calibrations/*.json.
Billing estimate
| Feature |
Status |
Notes |
| Estimator in the sidebar: shots, circuits, submissions, circuit time |
✅ |
Also lists the submissions found in the active editor |
| Formula from the VLQ consumption guide |
✅ |
Checked against the guide's examples in unit tests |
| Exact circuit time from the transpiled circuit and calibration data (instead of the 0–50 µs range) |
🗓️ |
|
Pulse-level operator catalog
| Feature |
Status |
Notes |
Operators as files (.qaas/operators/*.yaml): waveform, parameters, target gate, qubits |
🗓️ |
Versioned in git |
| Each operator tied to the calibration set it was tuned on |
🗓️ |
Jobs already record their calibration set |
| Migrate operators from an old calibration set to the current one |
🗓️ |
|
| Waveform editor with live I/Q plot and hardware limit checks |
🗓️ |
|
Use custom operators from OpenQASM 3 (defcal) |
🗓️ |
|
| Pulse schedule timeline after compilation |
🗓️ |
|
| Calibration recipes (Rabi, Ramsey, T1) that write fitted values back to the operator |
🗓️ |
|
Billing estimate
billed [s] = VLQ jobs × max(shots × (350 µs + circuit time), 0.9 s) circuit time ≤ 50 µs
qaas sends every circuit of a batch as its own HEAppE task, so the extension counts each
circuit as one VLQ job, each with the 0.9 s minimum. Unless the circuit time is known,
estimates are shown as a range: 350 µs per shot (low) to 400 µs per shot (worst case, use
for budgeting). Submission latency of 3–15 s is not billed.
Settings
| Setting |
Default |
Description |
qaas.pythonPath |
(empty) |
Python 3.12 with qaas. Empty uses the Python extension's selection, then python3. A value from workspace settings is used only after you allow it once (interpreters picked with Select Python Interpreter are allowed automatically); the same applies to qaas.condaPath |
qaas.condaPath |
(empty) |
conda executable when it is not found automatically |
qaas.packageExtra |
iqm_qiskit |
IQM extra installed with qaas |
qaas.lexisProject |
(empty) |
LEXIS project identifier |
qaas.lexisResource |
(empty) |
LEXIS resource; empty lets the client choose |
qaas.backendName |
VLQ |
Backend name |
qaas.lexisAaiUrl |
(empty) |
LEXIS login (Keycloak) URL; empty is production https://aai.lexis.tech |
qaas.lexisApiUrl |
(empty) |
LEXIS Platform API URL; empty is production https://api.lexis.tech |
qaas.lexisRealm |
(empty) |
Keycloak realm; empty is the py4lexis default LEXIS_AAI_v2 |
qaas.devConnectionFile |
${workspaceFolder}/.qaas/dev.env |
Dev connection file; when it exists it overrides the LEXIS settings and token |
qaas.defaultShots |
1024 |
Shots for OpenQASM runs |
qaas.confirmSubmit |
true |
Confirm with the estimate before submitting |
qaas.showEstimates |
true |
Estimates above submissions in code |
qaas.validateQasm |
true |
Check .qasm files on open and save |
qaas.jobPollSeconds |
20 |
Refresh interval for active jobs |
qaas.resultsDirectory |
${workspaceFolder}/qaas-results/${jobId}-${jobName} |
Download folder. Variables: ${workspaceFolder}, ${userHome}, ${jobId}, ${jobName}, ${project}, ${date}, ~ |
qaas.downloadMeasurements |
false |
Also download per-shot measurements |
qaas.autoDownloadResults |
false |
Download when a job finishes |
qaas.importJobLimit |
50 |
Jobs fetched by Import Jobs from HEAppE |
qaas.pulseSampleRate |
2e9 |
Drive sample rate for waveform previews before the calibration set is fetched |
qaas.calibrationMaxAgeMinutes |
60 |
Pulse runs re-read the current calibration set when the cached one is older |
Scripts started with Run Python File on VLQ get LEXIS_PROJECT, LEXIS_RESOURCE,
QAAS_BACKEND, LEXIS_TOKEN and, for a non-production instance, PY4LEXIS_AAI_URL,
PY4LEXIS_API_URL and PY4LEXIS_REALM as environment variables. LEXIS_TOKEN is always a
short-lived access token issued by the bridge (also after a browser login), never the stored
refresh token, because everything started in that terminal can read it. Scripts that still need
LEXIS after it expires (usually minutes; the expiry is shown when the script starts) must be run
again.
Testing against the LEXIS test instance
Run QaaS: Open Dev Connection File. It creates .qaas/dev.env (and adds it to
.qaas/.gitignore) with these keys:
PY4LEXIS_AAI_URL=<login (Keycloak) URL of the instance>
PY4LEXIS_API_URL=<LEXIS Platform API URL of the instance>
PY4LEXIS_REALM=<Keycloak realm of the instance>
QAAS_LEXIS_PROJECT=<test project>
QAAS_LEXIS_RESOURCE=<test resource>
# QAAS_BACKEND_NAME=VLQ
QAAS_LEXIS_TOKEN=<refresh or offline token>
- Values in the file override the
qaas.* settings and the stored token; changes apply to the
next request, no reload needed. Delete or rename the file to go back to production.
- A refresh/offline token is exchanged for access tokens by py4lexis and renewed as they
expire. An access token works too but expires within minutes. Without a token, the browser
login opens on the test instance.
- The status bar shows e.g.
VLQ · LEXIS test on a warning background while the file points at a
non-production instance. Jobs record their instance, so test and production jobs can share the
Jobs view and are always refreshed against the right one.
- QaaS: Test LEXIS Connection logs in, prints the user, token kind and expiry, and lists the
backends in the QaaS output channel.
- Instead of the file, the URLs can be set with
qaas.lexisAaiUrl, qaas.lexisApiUrl and
qaas.lexisRealm (e.g. in workspace settings), and the token with Set LEXIS Token.
- A token is only ever sent to the instance it belongs to. Order: the dev file's token (when the
file selects that instance), the token stored for that instance with Set LEXIS Token or
Sign in, then the
QAAS_LEXIS_TOKEN environment variable (production only). A workspace or
dev file that points at another server therefore gets no stored token; sign in again there.
- The extension is disabled in untrusted (Restricted Mode) workspaces. In trusted ones, values a
cloned repository could plant are used only after you allow them once per folder: a
non-production LEXIS login set by workspace settings or the dev file (the browser login would
show that server's page),
qaas.pythonPath / qaas.condaPath from workspace settings, an
interpreter inside the workspace picked by the Python extension, and a workspace
qaas.resultsDirectory outside the workspace. Instances and paths from your user settings
need no approval.
How it works
The extension does not reimplement the client. It starts
python/qaas_bridge.py once with your interpreter and talks to it
over stdin/stdout (one JSON request and response per line). The process keeps the LEXIS login
and HEAppE clients alive, and calls qheappe_client (QProvider, QBackend, QJob).
Library output goes to the QaaS output channel. py4lexis writes its log (no tokens) to
lexis.log in the extension's log folder (Developer: Open Extension Logs Folder), readable
by your user only.
Development
npm install
pip install -r requirements-dev.txt # ruff
npm run compile # or: npm run watch, then F5 for an Extension Development Host
npm run test:unit # estimator, scanner, statistics, catalog model (node --test)
npm run test:py # Python bridge and operator code generator (needs qaas[iqm_qiskit])
npm test # integration tests in a downloaded VS Code
npm run lint # Biome (TypeScript/JSON) + Ruff (Python)
npm run format # format everything
npm run package # build a .vsix
Formatting and linting: Biome for TypeScript, JavaScript and JSON;
Ruff for Python. The workspace recommends both VS Code
extensions and formats on save.
Authors
Developed at IT4Innovations National Supercomputing Center,
VSB - Technical University of Ostrava.
- Ing. Jan Swiatkowski
- Ing. Jan Martinovič, Ph.D.
License
Apache-2.0
| |