Skip to Content

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 of Circuit.execute(seed=...).
  • result.classical_results_map: A dictionary mapping classical bit indices to measured values (0 or 1), populated only after measure operations 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 combinationUse 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 gates

In 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 structure

Commonly used methods:

MethodDescription
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/2 will 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.
  • unitary gates carrying matrix information (i.e., gates added via qc.unitary(matrix, target) whose g.matrix attribute is non-empty) are exported as UnitaryLab extension statements (with matrix elements encoded as alternating real/imaginary parameter values), and can be fully restored via from_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 call transpile() first. Only when the matrix information for a unitary gate is missing (g.matrix is None, which generally occurs on certain intermediate processing paths) will to_qasm() raise a NotImplementedError, with retry suggestions given in the error message (transpile=True or decompose=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 probabilities are 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, use result.marginal_probabilities() or result.sample(), neither of which modifies result.state; only result.measure() causes collapse.
  • GPU and backend: The default settings are suitable for most scenarios; adjust backend, device, dtype, and backend_options only 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.

Last updated on