Circuit Execution and Toolchain Workflow
Overview
This page brings together the core usage of circuit execution, result inspection, circuit analysis and drawing, and OpenQASM import/export, serving as a quick reference for the complete workflow.
Executing Circuits and Inspecting Results
Regular users call Circuit.execute() directly, which returns an ExecutionResult object:
from unitarylab import Circuit
qc = Circuit(2)
qc.h(0)
qc.cx(0, 1)
result = qc.execute()Core Fields and Methods of ExecutionResult
result.state: The final statevector, a read-only one-dimensional NumPy array (converted from the backend state in real time on every access, with no caching).result.backend_state: The raw underlying backend state object (e.g., a torch tensor on GPU), with no device copy performed; can be used directly in performance-sensitive scenarios.result.probabilities: A dictionary of the probability distribution over all computational basis states, with little-endian binary strings as keys and the corresponding probabilities as values.result.probability(bitstring, qubits=None): Queries the probability of a single computational basis state outcome.result.marginal_probabilities(qubits=None, threshold=1e-12): The marginal probability distribution over a specified subset of qubits.result.sample(shots=1, qubits=None, seed=None): Samples a number of computational basis state outcomes according to the probability distribution without collapsing the state, returning a list of strings.result.expectation(observable, qubits=None): Computes the expectation value of an observable , supporting multiple input forms such as"Z","XX", a full-length Pauli string, or a list of(coefficient, operator, qubit)terms.result.measure(target_indices, seed=None): Performs a projective measurement and collapses the internal state for the specified qubits, returning the measurement result string; this seed is independent ofCircuit.execute(seed=...).result.classical_results_map: A dictionary mapping classical bit indices to measured values (0or1), populated only aftermeasureoperations have been added to the circuit.result.shots/result.counts/result.classical_registers: Respectively, the number of shots for this execution, the counts of classical bit strings aggregated across shots, and the measurement results of the last shot grouped by register name.
print(result.state)
# [0.70710678+0.j 0.+0.j 0.+0.j 0.70710678+0.j]
print(result.probabilities)
# {'00': 0.4999..., '11': 0.4999...}
print(result.expectation("ZZ", qubits=(0, 1)))
# Under the Bell state, the ZZ expectation value should be close to 1 (the two qubits are fully correlated)Backend, Device, and Execution Parameters
execute() supports the backend, device, dtype, shots, seed, and backend_options parameters:
result = qc.execute(backend='torch', device='cpu')| Parameter combination | Use case |
|---|---|
backend='torch', device='cpu' (default) | Suitable for most situations |
backend='torch', device='gpu' | Accelerated when a GPU is available (only torch supports gpu) |
backend='numpy', device='cpu' | Pure NumPy environment |
backend='cpp', device='cpu' | Uses compiled C++ gate kernels to accelerate CPU execution |
backend='tensornet', device='cpu' | Large qubit counts with low-entanglement circuits, using an MPS representation |
Typically, calling qc.execute() directly with no parameters is sufficient; when simulating large circuits with a GPU available, you can specify device='gpu'; when the qubit count is very large but entanglement is limited, you can try backend='tensornet' and control max_bond/cutoff/routing via backend_options. seed defaults to a fixed value of 42 (for reproducible results); pass seed=None when true random sampling is needed. For the complete parameter description, see Quick Start and Core Circuit Interface.
Circuit Analysis and Drawing
draw()
qc.draw() # opens a plot window (default output='mpl')
qc.draw(filename='circuit.png') # save as a file
qc.draw(filename='circuit.png', title='Bell State') # add a title
qc.draw(output='text') # plain-text circuit diagram
qc.draw(output='latex') # LaTeX (quantikz) circuit diagram
qc.draw(compact=False) # disable merging of adjacent single-qubit gatesIn Jupyter Notebooks, the diagram is displayed inline; in regular scripts, it opens in a separate window. output supports three modes: 'mpl' (default, Matplotlib), 'text', and 'latex', corresponding internally to MatplotlibCircuit, TextCircuitDrawer, and LatexCircuitDrawer respectively.
CircuitInfo
CircuitInfo provides static circuit analysis, and can be called via the qc.analyze() shortcut:
from unitarylab.circuit_analysis import CircuitInfo
info = CircuitInfo(qc) # equivalent to info = qc.analyze(show=False)
info.show() # prints the overview, instruction list, and layer structureCommonly used methods:
| Method | Description |
|---|---|
size() | Total gate count |
depth() | Circuit depth (the longest serial path, accounting for data dependencies between gates; not the total gate count) |
count_ops() | Number of occurrences of each gate type, returned as a dict |
count_single_qubit_gates() / count_two_qubit_gates() / count_multi_qubit_gates() | Gate counts classified by qubit count |
count_parameterized_gates() | Number of gates with parameters (rotation angles, etc.) |
get_qubit_usage() | Number of operations on each qubit |
get_coupling_map() | Qubit coupling relationships (two-qubit gate connection pairs), e.g. [(0, 1), (1, 2)] |
get_qubit_history(qubit) | Complete operation history for a specified qubit |
get_instructions() / get_layers() / get_parameters() | Respectively return the instruction list, gates grouped by layer, and a summary of parameterized gate parameters |
is_parameterized() | Whether the circuit contains parameterized gates |
get_summary() / to_dict() | Structured overview / exports the analysis results as a dictionary |
show(sections=None, qubit=None) | Prints analysis results by section |
The sections values supported by show(): 'overview' (or its alias 'summary'), 'instructions', 'layers', 'qubit_usage', 'coupling_map', 'parameters', 'qubit_history' (requires also specifying the qubit= parameter). When sections is not passed, it defaults to printing ['overview', 'instructions', 'layers'].
info.show(sections=['overview', 'coupling_map'])
info.show(sections='qubit_history', qubit=0)OpenQASM Import and Export
Both OpenQASM 2.0 and 3.0 are supported, suitable for saving and exchanging circuits, or interoperating with other tools. For a quick reference of the public methods and parameters, see Core Circuit Interface; this section focuses on the underlying interfaces and compatibility limitations.
# Using the qc built earlier; exports OpenQASM 3.0 by default
qasm3_str = qc.to_qasm()
qasm2_str = qc.to_qasm2()
# Import (automatically detects 2.0 / 3.0)
qc2 = Circuit.from_qasm(qasm3_str)Underlying Functions (Advanced Usage)
Circuit.to_qasm() / from_qasm() internally delegate to a set of functions in the unitarylab.backend.qasm module, which operate directly on GateSequence rather than Circuit. Direct calls are only needed when you need to work with GateSequence independently of Circuit, or need to explicitly specify the register structure:
from unitarylab.backend.qasm import (
gate_sequence_to_qasm, # unified entry point, exports QASM 3.0 by default
gate_sequence_to_qasm2,
gate_sequence_to_qasm3,
gate_sequence_from_qasm, # unified entry point, automatically detects the version
qasm2_to_gate_sequence,
qasm3_to_gate_sequence,
circuit_from_gate_sequence, # rebuilds a Circuit from a GateSequence
)
qasm_str = gate_sequence_to_qasm3(qc.gate_sequence, qreg_name='q')
gs = qasm3_to_gate_sequence(qasm_str)
qc3 = circuit_from_gate_sequence(Circuit, gs)Supported Scope and Limitations
Supported gates include: x, y, z, h, s, sdg, t, tdg, rx, ry, rz, p, U (unitary), cx, swap, as well as controlled gate modifiers (ctrl @, negctrl @) and custom gate definitions.
Key limitations:
- Both OpenQASM 2.0 and 3.0 are supported; the default export/auto-detection is based on 3.0. When 2.0 is needed, explicitly call
to_qasm2()/gate_sequence_to_qasm2(). - Gate parameters must be numeric values; symbolic expressions are not supported (e.g.,
pi/2will be numerically evaluated). - Classical control flow (
if,while, etc.) is not supported. - Not all custom gates can be fully round-tripped on export; non-contiguous block gates are inlined and expanded.
unitarygates carrying matrix information (i.e., gates added viaqc.unitary(matrix, target)whoseg.matrixattribute is non-empty) are exported as UnitaryLab extension statements (with matrix elements encoded as alternating real/imaginary parameter values), and can be fully restored viafrom_qasm()— this is a UnitaryLab-specific extension, not a standard OpenQASM 3.0 interoperability format; if the goal is to import into third-party tools, it is still recommended to calltranspile()first. Only when the matrix information for aunitarygate is missing (g.matrix is None, which generally occurs on certain intermediate processing paths) willto_qasm()raise aNotImplementedError, with retry suggestions given in the error message (transpile=Trueordecompose=True, transpile=True).
Comprehensive Example
Below is a minimal complete example that includes execution, result inspection, analysis, drawing, and OpenQASM export:
from unitarylab import Circuit
# 1. Create a Bell circuit
qc = Circuit(2)
qc.h(0)
qc.cx(0, 1)
# 2. Execute the circuit
result = qc.execute()
# 3. Inspect the probabilities
print(result.probabilities)
# {'00': 0.4999..., '11': 0.4999...}
# 4. Circuit analysis
qc.analyze(show=True, sections=["summary"])
# 5. Draw the circuit
qc.draw(title="Bell State")
# 6. Export OpenQASM 3.0 (see above for details)
qasm_str = qc.to_qasm()Notes
- Qubit ordering: The keys of
probabilitiesare little-endian binary strings, with the least significant bit corresponding to qubit 0. - Projective measurement collapses the state:
result.measure()modifies the internal state (projective collapse). If you need to repeatedly query the distributions of different subsystems on the same uncollapsed state, useresult.marginal_probabilities()orresult.sample(), neither of which modifiesresult.state; onlyresult.measure()causes collapse. - GPU and backend: The default settings are suitable for most scenarios; adjust
backend,device,dtype, andbackend_optionsonly when tuning performance. - MDX special characters: In MDX files,
|within table cell content should be written as|; special characters such as{}and\should be placed inside code blocks or inline code.
Recommended Reading
- Full API of the core circuit interface → Core Circuit Interface
- Algorithm library (QFT, QPE, HHL, etc.) → Algorithms and Utility Library