oitg.circuits API

Functionality for building and running experiments that correspond to a quantum circuit on a number of qubits, and for analysing and visualising the results.

Like much of oitg, most of this code will be used from within ARTIQ experiments, or to analyse results produced by them. However, the code here should not directly depend on ARTIQ; such artefacts should be placed in oxart.circuits instead.

Basics

oitg.circuits.gate module

Defines representations of quantum gates (and sequences of them), and a few basic operations for manipulating them.

A Gate is a named tuple (kind: str, parameters: Tuple[float, ...], operands: Tuple[int, ...]).

kind identifies the type of unitary (e.g. "rx", "cz"). parameters is a list of float values parametrising the chosen gate (e.g. rotation angles). operands is a list of integer indices denoting the target qubits.

This is deliberately just a plain piece of data to make gates directly representable in ARTIQ kernels as well (with the tuples expressed as TLists). Even apart from that, coming up with an extensible design for representing both values and operations isn’t trivial in the first place (cf. expression problem). This sort of weakly typed design (with gate types represented as arbitrary strings) isn’t worse than many alternatives anyway.

Using tuples not only represents the data semantics better than lists (parameters and operands are plain collections with value semantics), but also has the advantage of keeping Gate hashable (so it can be used in sets/dictionaries/…).

If more complex operations (e.g. decomposition into elementary gates, optimisations, etc.) are desired in the future, we might want to directly integrate a QC library (Cirq, Qiskit, pyQuil, …) into our code instead, though.

A GateSequence is a tuple of Gates that represents the coherent portion of a circuit on one or more qubits. Tuples are used instead of lists as most of the time, gate sequences are not changed once built up, and tuples (with their value semantics) are hashable.

A GateGenerator produces a GateSequence gate by gate through an Iterable interface. Note that this will in general not be a list that support indexing, but for instance an iterator. Use e.g. tuple(…) (or similar) to convert the result to a GateSequence.

class oitg.circuits.gate.Gate(kind, parameters, operands)
kind: str

Alias for field number 0

operands: Tuple[int, ...]

Alias for field number 2

parameters: Tuple[float, ...]

Alias for field number 1

oitg.circuits.gate.GateSequence

alias of Tuple[Gate, …]

oitg.circuits.gate.GateGenerator

alias of Iterable[Gate]

oitg.circuits.gate.collect_operands(gates: Iterable[Gate]) Set[int][source]

Return all the qubit operands used in the given gate sequence.

oitg.circuits.gate.remap_operands(gates: Iterable[Gate], operand_map: Dict[int, int]) Iterable[Gate][source]

Change all the operand indices in a gate string according to the given map.

For instance, remap_operands(sequence, {0: 1}) will make all gates in the sequence that target qubit 0 act on qubit 1 instead (including cases where 0 is used as part of a multi-qubit gate).

oitg.circuits.to_matrix module

Tools for converting common gates to unitary matrices.

oitg.circuits.to_matrix.local_matrix(gate: Gate) ndarray[source]

Return the unitary matrix that describes gate on its operands only.

The resulting matrix is the same no matter which target qubit(s) the gate operates on; the first copy of \(\mathbb{C}^2\) corresponds to the first operand, etc.

See single_gate_matrix() for creating the unitary that acts on the given operands in a larger system.

oitg.circuits.to_matrix.single_gate_matrix(gate: Gate, num_qubits: int) ndarray[source]

Return the unitary matrix that describes the action of the given gate.

Parameters:

num_qubits – The number of qubits of the target Hilbert space.

Returns:

A unitary matrix acting on the 2^num_qubits-dimensional Hilbert space.

oitg.circuits.to_matrix.gate_sequence_matrix(gates: Iterable[Gate], num_qubits: int | None = None) ndarray[source]

Return the unitary matrix that describes the action of the given gate sequence.

Parameters:

num_qubits – The number of qubits of the target Hilbert space. If None, the number of qubits is inferred from the gate sequence (i.e., from the largest qubit index used).

Returns:

A unitary matrix acting on the 2^num_qubits-dimensional Hilbert space.

oitg.circuits.to_matrix.apply_gate_sequence(gates: Iterable[Gate], initial_state: ndarray) ndarray[source]

Apply a gate sequence to the given initial state and return the resulting state vector.

oitg.circuits.clifford module

Brute-force Clifford group helpers.

The Gottesmann—Knill theorem states that circuits consisting of gates from the Clifford group \(\mathcal{C}_n = \operatorname{Aut}(\mathrm{Pauli}_n)\) can be efficiently simulated clasically, typically using some implementation of the stabiliser formalism.

This module is emphatically not about that. Instead, it contains utilities for directly manipulating elements of a group of unitaries as gate strings, without making any use of its structure to make the calculations easier. This is only really practical for the single- and two-qubit Clifford groups \(\mathcal{C}_1\) and \(\mathcal{C}_2\).

TODO (DPN): Instead of explicitly listing more than one Clifford group decomposition, this should just directly integrate my decomposition search code and transparently use oitg.cache for caching. In the meantime, just ask if you want to use a different gate set.

class oitg.circuits.clifford.GateGroup(num_qubits: int, gate_sequences: List[Tuple[Gate, ...]], matrices: List[ndarray], inverse_idxs: Dict[bytearray, int])[source]

A group (in the mathematical sense) of gates, where each element is represented by its index in some canonical order.

Parameters:
  • num_qubits – The number of qubits the gates operate on.

  • gate_sequences – The GateSequences corresponding to each group element.

  • matrices – The unitary matrices describing the action of each group element.

  • inverse_idxs – The index of the inverse element for each group element, given as a map of canonical matrix key (see to_canonical_matrix_key()) to element index.

num_elements() int[source]

Return the total number of elements in the group.

gates_for_idx(idx: int) Tuple[Gate, ...][source]

Return the gate sequence for the given element index.

matrix_for_idx(idx: int) ndarray[source]

Return the unitary matrix for the given element index.

find_inverse_idx(matrix: ndarray) int[source]

Look up the index of the element that is the inverse of the given unitary matrix.

pauli_idxs() List[int][source]

Return the indices corresponding to the (generalised) Pauli group (which is always a subgroup of the Clifford group).

oitg.circuits.clifford.to_canonical_matrix(gate_matrix: ndarray) ndarray[source]

Convert the given gate matrix to a canonical form, which is exactly the same no matter the global phase or rounding errors.

Note that this rounds to 4 decimal places for robustness, which is plenty for Clifford group calculations, but might not be sufficient for other applications.

oitg.circuits.clifford.to_canonical_matrix_key(gate_matrix: ndarray) bytearray[source]

Return the canonical matrix corresponding to gate_matrix in a form suitable for use as a dictionary key.

oitg.circuits.clifford.make_clifford_group(num_qubits: int, implementation: Callable[[int], Iterable[Gate]]) GateGroup[source]

Construct a GateGroup instance which enumerates all elements of the num_qubit-qubit Clifford group, with elements decomposed according to the given implementation.

oitg.circuits.clifford.get_clifford_1q_xypm_implementation(idx: int) Iterable[Gate][source]

Return an implementation of the 1-qubit Clifford group element with the given index as ±π/2 rotations about the x and y axes.

Returns:

A minimal-length gate string implementing the given Clifford.

oitg.circuits.clifford.get_clifford_1q_xzpm2_implementation(idx: int) Iterable[Gate][source]

Return an implementation of the 1-qubit Clifford group element with the given index as ±π/2 and π rotations about the x and z axes.

Returns:

A minimal-length gate string implementing the given Clifford.

class oitg.circuits.clifford.EntanglingGate(value)[source]

Specifies the equivalence class (up to local single-qubit Cliffords) of a non-trivial two-qubit Clifford operation.

cz_like = 0
iswap_like = 1
swap_like = 2
oitg.circuits.clifford.get_clifford_2q_implementation(idx: int, clifford_1q_impl: Callable[[int], Iterable[Gate]], entangling_gates_impl: Callable[[EntanglingGate, Callable[[int], Iterable[Gate]]], Iterable[Gate]]) Iterable[Gate][source]

Generate an implementation of the 2-qubit Clifford group element with the given index.

Parameters:
  • idx – The element index.

  • clifford_1q_impl – The single-qubit Clifford gate implementation to use (acting on qubit 0).

  • entangling_gates_impl – The entangling gate implementation to use.

oitg.circuits.clifford.get_cz_entangling_gate_implementation(kind: EntanglingGate, clifford_1q_impl_0: Callable[[int], Iterable[Gate]]) Iterable[Gate][source]

Generate an implementation of the given entangling gate category using CZ gates and the given single-qubit gate implementation.

oitg.circuits.clifford.get_zzw_entangling_gate_implementation(kind: EntanglingGate, clifford_1q_impl_0: Callable[[int], Iterable[Gate]]) Iterable[Gate][source]

Generate an implementation of the given entangling gate category using symmetric ZZ (wobble) gates and the given single-qubit gate implementation.

oitg.circuits.clifford.get_clifford_2q_xypm_cz_implementation(idx: int) Iterable[Gate][source]

Generate an implementation of the given 2-qubit Clifford group element using CZ gates and local single-qubit ±π/2 rotations about the x and y axes.

oitg.circuits.clifford.get_clifford_2q_xzpm2_cz_implementation(idx: int) Iterable[Gate][source]

Generate an implementation of the given 2-qubit Clifford group element using CZ gates and local single-qubit ±π/2 and π rotations about the x and z axes.

oitg.circuits.clifford.get_clifford_2q_xzpm2_zzw_implementation(idx: int) Iterable[Gate][source]

Generate an implementation of the given 2-qubit Clifford group element using a symmetric ZZ entangling gate implemented as a spin-echo wobble gate and local single-qubit ±π/2 and π rotations about the x and z axes.

oitg.circuits.composite_pulses module

Composite pulse implementations for replacing gates with sequences that are logically equivalent, but have different behaviour under imperfections.

References

[BIKN13]

Bando, M., Ichikawa, T., Kondo, Y. & Nakahara, M. Concatenated composite pulses compensating simultaneous systematic errors. J. Phys. Soc. Jpn. 82, 014004 (2013).

[KGK+14]

Kabytayev, C. et al. Robustness of composite pulses to time-dependent control noise. Phys. Rev. A 90, 012316 (2014).

[Wimp94]

Wimperis, Stephen. Broadband, Narrowband, and Passband Composite Pulses for Use in Advanced NMR Experiments. Journal of Magnetic Resonance 109, 221–231 (1994).

exception oitg.circuits.composite_pulses.UnsupportedGate[source]

Raised if a given gate cannot be expanded in the requested form.

oitg.circuits.composite_pulses.to_rxy(gate: Gate) Gate[source]

Canonicalise all single-qubit rotations in the xy-plane to rxy gates.

Returns:

A rxy Gate with positive rotation amount.

oitg.circuits.composite_pulses.bb1(gate: Gate, symmetric: bool = False) Iterable[Gate][source]

Generate implementation of the given single-qubit rotation using a BB1 composite pulse.

BB1, as per [Wimp94], is a broadband amplitude noise suppression sequence, cancelling error terms up to fourth order in amplitude offset.

Parameters:
  • gate – The gate to implement.

  • symmetric – Whether to implement the gate symmetrically using 5 pulses (2 half-rotations around the 3-pulse BB1 identity sequence), or asymmetrically using 4 pulses. The latter has slightly nicer behaviour under detuning errors.

oitg.circuits.composite_pulses.reduced_c_in_sk(gate: Gate) Iterable[Gate][source]

Generate implementation of the given single-qubit rotation using a reduced CORPSE/SK1 concatenated composite pulse.

Reduced CinSK, as described in [BIKN13] (and among those analysed in [KGK+14]) is robust against both pulse amplitude and detuning errors. Note that the CORPSE part (that gives detuning robustness) requires non-multiple-of-π/2 gate rotation amounts.

oitg.circuits.composite_pulses.expand_using(method: Callable, gates: Iterable[Gate], ignore_kinds: Iterable[str] = [], ignore_unsupported_gates: bool = True, insert_barriers: bool = True) Iterable[Gate][source]

Expand all gates in the given sequence using composite pulses.

Parameters:
  • method – A callable implementing the chosen composite pulse type (e.g. bb1()).

  • ignore_kinds – A set of gate kinds not to attempt to expand.

  • ignore_unsupported_gates – If True, silently pass over unsupported gates without expanding them.

  • insert_barriers – Insert a barrier after each composite pulse.

oitg.circuits.qasm module

Functionality for interfacing with OpenQASM.

QASM is a straightforward text-based format for representing quantum computations in the circuit model. At this point, we do not support any classical feedback, etc.; just straight-line gate sequences starting with state preparation and ending with measurement.

For more complex QASM support, we should integrate an external library.

oitg.circuits.qasm.stringify_param(p) str[source]

Returns the string form of a parameter p; with fractions of π pretty-printed for readability.

oitg.circuits.qasm.gate_to_qasm(gate: Gate) Iterable[str][source]

Returns a QASM statement corresponding to the given Gate.

oitg.circuits.qasm.stringify_qasm(stmts: Iterable[str]) str[source]

Concatenate/format a list of QASM statements into a single string.

oitg.circuits.qasm.parse_gate_sequence_string(string: str) Iterable[Gate][source]

Parse a simple gate sequence in ;-delimited string form into its constituent gates.

oitg.circuits.results module

Common helpers for analysing experiment results.

Also see oitg.results for finding and loading HDF5 files.

oitg.circuits.results.collect_outcomes(sequences: List[Tuple[Gate, ...]], run_order: List[int], outcomes: List[ndarray])[source]

Total up the number of observations per outcome for gate sequence runner experiments.

This function works on the arrays passed to/returned by oitg.circuits.runner run_sequence() implementations; see collect_outcomes_from_datasets() for processing the resulting datasets.

Parameters:
  • sequences – A list of all sequences run.

  • run_order – A list giving the index in sequences for each result.

  • outcomes – A list of the same length as run_order, giving the number of occurrences of each outcome for each respective gate sequence.

Returns:

A dictionary mapping gate sequences to arrays giving the number of times each measurement outcome was observed after running them. Array elements represent the different qubit measurement outcomes in canonical order (i.e. 000, 001, 010, …).

oitg.circuits.results.collect_outcomes_from_datasets(datasets: Dict[str, Any], prefix: str = 'data.circuits.') Dict[Tuple[Gate, ...], ndarray][source]

Total up the number of observations per outcome for gate sequence runner experiments.

This function works on the datasets written by oitg.circuits.runner implementations. See collect_outcomes() for an implementation that directly operates on lists, and oitg.results for extracting datasets from an ARTIQ results file.

Parameters:
  • datasets – A dictionary containing all the datasets written by the experiment.

  • prefix – The dataset key prefix the circuit results were saved under.

Returns:

A dictionary mapping gate sequences to arrays giving the number of times each measurement outcome was observed after running them; see collect_outcomes().

oitg.circuits.visualisation module

oitg.circuits.visualisation.save_circuit_pdf(filename: str, gates: Tuple[Gate, ...])[source]

Render a gate sequence as a circuit diagram and save it to PDF.

This requires Matplotlib and Qiskit Terra to be installed.

Parameters:
  • filename – Path to the output PDF file.

  • gates – The gate sequence to display.

Protocols

Gate Set Tomography (oitg.circuits.protocols.gst)

Implements circuit generation for Gate Set Tomography.

Gate Set Tomography is a method for self-consistent process tomography of a given set of gates. It does not require any prior knowledge about the gates used to prepare and measure in various bases, and is robust against state preparation and measurement errors.

The protocol was developed by and around Robin Blume-Kohout at Sandia; see e.g. [BGN+13] and [Blum15] for more details, and [KWS+14] and [BGN+17] for experimental demonstrations.

This module only contains the (small amount of) code necessary to generate a list of gate sequences for a GST experiment. While analysis is relatively straightforward, there is a comprehensive and well-tested open-source implementation already available in the form of the [pyGSTi] package.

References

[BGN+13]

Blume-Kohout, R. et al. Robust, self-consistent, closed-form tomography of quantum logic gates on a trapped ion qubit. arxiv:1310.4492 (2013).

[Blum15]

Blume-Kohout, R. et al. Report: Turbocharging Quantum Tomography. (Sandia National Laboratories, 2015).

[KWS+14]

Kim, D. et al. Microwave-driven coherent operation of a semiconductor quantum dot charge qubit. Nature Nanotechnology 10, 243–247 (2015).

[BGN+17]

Blume-Kohout, R. et al. Demonstration of qubit operations below a rigorous fault tolerance threshold with gate set tomography. Nature Communications 8, (2017).

[pyGSTi]

A python implementation of Gate Set Tomography. http://www.pygsti.info/

class oitg.circuits.protocols.gst.GSTSpec(prep_fiducials: List[Tuple[Gate, ...]], meas_fiducials: List[Tuple[Gate, ...]], germs: List[Tuple[Gate, ...]], pygsti_name: str = '')[source]

Specifies a model for Gate Set Tomography.

Parameters:
  • prep_fiducials – The list of preparation fiducials to use.

  • meas_fiducials – The list of measurement fiducials to use.

  • germs – The list of gate sequence germs to use.

  • pygsti_name – The name of the equivalent pyGSTi standard model construction, if any.

oitg.circuits.protocols.gst.generate module

oitg.circuits.protocols.gst.generate.generate_std_gst_sequences(spec: GSTSpec, max_len_exponent: int) List[Tuple[Gate, ...]][source]

Return a list of sequences to run to perform gate set tomography according to the given spec, with standard power-of-two sequence lengths up to (approximately) \(2^{\mathrm{max\_len\_exponent}}\).

See generate_gst_sequences().

Parameters:
  • spec – The fiducials and germs to use.

  • max_len_exponent – The number of long-sequence refinement steps.

oitg.circuits.protocols.gst.generate.generate_gst_sequences(spec: GSTSpec, target_lens: Sequence[int]) List[Tuple[Gate, ...]][source]

Return a list of sequences to run to perform gate set tomography according to the given spec, with number of gates per sequence limited to the given lengths.

Matches pyGSTi’s default method of truncating sequences, where germs are only repeated in whole (rounding down target lengths divided by germ lengths), and fiducial lengths do not count.

Parameters:
  • spec – The fiducials and germs to use.

  • target_lens – The target sequence lengths.

Returns:

A concatenated list of gate sequences to run (de-duplicated).

oitg.circuits.protocols.gst.specs module

Predefined GSTSpecs corresponding to commonly used gate sets.

oitg.circuits.protocols.gst.specs.make_1q_xz_pi_2_spec() GSTSpec[source]

Return a single-qubit gate set using π/2 x- and z-rotations, corresponding to pyGSTi’s std1Q_XZ model.

oitg.circuits.protocols.gst.specs.make_2q_xy_pi_2_cphase_spec() GSTSpec[source]

Return a two-qubit gate set using a CPHASE (CZ) gate and local π/2 x- and y-rotations, corresponding to pyGSTi’s std2Q_XYCPHASE model.

oitg.circuits.protocols.gst.specs.make_2q_xy_pi_2_wobble_spec() GSTSpec[source]

Return a two-qubit gate set using a wobble gate and local π/2 x- and y-rotations.

Fiducial/germ selection is based on pyGSTi’s std2Q_XYCNOT model, with CNOT replaced by the wobble gate. As suggested in the pyGSTi documentation, this still leads to a complete set of germs, but might not be optimal. (The CNOT construction was chosen over the CPHASE-based germ set as its germ score was slightly better after switching to the wobble gate; fiducials are only single-qubit and hence the same for both anyway.)

oitg.circuits.protocols.gst.specs.make_2q_xz_pi_2_wobble_spec() GSTSpec[source]

Return a two-qubit gate set using a wobble gate and local π/2 x- and z-rotations.

Fiducial/germ selection is based on pyGSTi’s optimisation functions, run from scratch for the wobble gate.

Process Tomography (oitg.circuits.protocols.process_tomo)

This package contains a generic process tomography implementation.

For many cases, you might want to consider Gate Set Tomography in place of simple process tomography for its robustness and favourable error scaling properties.

oitg.circuits.protocols.process_tomo.generate module

oitg.circuits.protocols.process_tomo.generate.generate_process_tomography_sequences(target: Tuple[Gate, ...], num_qubits: int) List[Tuple[Gate, ...]][source]

Return a list of gate sequences to perform process tomography on the given target sequence.

The system is prepared in a tensor product of single-qubit Pauli-operator eigenstates before applying the target sequence and measuring the expectation value of a tensor product of Pauli operators. See generate_process_tomography_fiducial_pairs().

Parameters:
  • target – The gate sequence to perform tomography on.

  • num_qubits – The number of qubits making up the Hilbert space of interest.

oitg.circuits.protocols.process_tomo.generate.wrap_target_in_process_tomography_fiducials(target: Tuple[Gate, ...], fiducial_pairs: List[Tuple[Tuple[Gate, ...], Tuple[Gate, ...]]]) List[Tuple[Gate, ...]][source]

Return a list of gate sequences to perform process tomography on the given target sequence.

The system is prepared in a tensor product of single-qubit Pauli-operator eigenstates before applying the target sequence and measuring the expectation value of a tensor product of Pauli operators.

Parameters:
  • target – The gate sequence to perform tomography on.

  • fiducial_pairs – See generate_process_tomography_fiducial_pairs()

oitg.circuits.protocols.process_tomo.generate.generate_process_tomography_fiducial_pairs(num_qubits: int) List[Tuple[Tuple[Gate, ...], Tuple[Gate, ...]]][source]

Return a list of tuples of gate sequences implementing the preparation and measurement for process tomography.

For state preparation, all six Pauli eigenstates are created (i.e. ±x, ±y, ±z). Even though this yields an over-complete set of input states (\(6^n\) instead of the \(4^n\) required ones), this is a small price to pay for the resulting symmetry – even for two-qubit gates this only just more than doubles (\(9 / 4\)) the number of sequences, so we can always just take fewer shots per sequence to compensate.

Parameters:

num_qubits – The number of qubits making up the Hilbert space of interest.

oitg.circuits.protocols.process_tomo.analyse module

Implements different methods for reconstructing an estimate for a quantum channel (process) from tomography measurements.

Currently, linear inversion tomography and two maximum-likelihood techniques are supported.

All implementations assume that the processes in question have the same input and output dimensions, i.e. are endomorphisms on the space of bounded operators (density matrices) on some qubit Hilbert space.

References

[FH01] (1,2)

Fiurášek, J. & Hradil, Z. Maximum-likelihood estimation of quantum processes. Physical Review A 63, (2001).

[RHKL07]

Reháček, J., Hradil, Z., Knill, E. & Lvovsky, A. I. Diluted maximum-likelihood algorithm for quantum tomography. Physical Review A 75, 1–5 (2007).

[AL12]

Anis, A. & Lvovsky, A. I. Maximum-likelihood coherent-state quantum process tomography. New J. Phys. 14, 105021 (2012).

[KBLG18] (1,2,3,4)

Knee, G. C., Bolduc, E., Leach, J. & Gauger, E. M. Quantum process tomography via completely positive and trace-preserving projection. Physical Review A 98, (2018).

oitg.circuits.protocols.process_tomo.analyse.guess_prepare_target_measure_split(all_sequences: List[Tuple[Gate, ...]]) Tuple[Tuple[Gate, ...], List[Tuple[Tuple[Gate, ...], Tuple[Gate, ...]]]][source]

For a given list of gate sequences making up a tomography experiment, guesses which is the target sequence to be analysed, and the preparation/measuemrent fiducial sequences used.

Parameters:

all_sequences – A list of all the gate sequence for which data was acquired.

Returns:

A tuple (target_seq, [(prep_seq, meas_seq)]) of the guess for the target sequence and, for each input sequence, the respective state preparation sequence before/measurement sequence after the tomography target.

oitg.circuits.protocols.process_tomo.analyse.auto_prepare_data(outcomes: Dict[Tuple[Gate, ...], ndarray]) Tuple[List[ndarray], List[ndarray], ndarray][source]

Given a results dictionary, guess the target and fiducial sequences and extract the tomography input data.

See prepare_data() for details.

Parameters:

outcomes – A dictionary mapping sequences run to the number each outcome was observed.

Returns:

A tuple of prepared states, measured states, and the respective number of observations each (see prepare_data()).

oitg.circuits.protocols.process_tomo.analyse.prepare_data(outcomes: Dict[Tuple[Tuple[Gate, ...], Tuple[Gate, ...]], ndarray], initial_state: List[ndarray] | None = None, readout_projectors: List[ndarray] | None = None) Tuple[List[ndarray], List[ndarray], ndarray][source]

Given a dictionary of observed measurement outcomes indexed by pairs of the state preparation and measurement gate sequences used, compute the prepared/measured states and respective number of operations.

Assumes the initial state (i.e. before the preparation sequences are run) is \(\left|00\ldots0\right>\)), and outcomes are given in canonical order (i.e. corresponding to projection onto \(\left|0\ldots00\right>, \left|0\ldots01\right>, \ldots, \left|1\ldots11\right>\).)

Parameters:

outcomes – A dictionary mapping tuples (prepare, measure) of state preparation/measurement gate sequences to an array giving the number each outcome was observed.

Returns:

A tuple of prepared states, measured states, and the respective number of observations each. Rows in the retuned observation count array correspond to the different prepared states, and columns to the measured state. Measured states corresponding to the different outcomes are enumerated explicitly, so for a \(n\)-qubit system and \(k\) measurement fiducials, there will be \(k\ 2^n\) measured states/columns in the observation count matrix. All states are given as density matrices/projectors.

oitg.circuits.protocols.process_tomo.analyse.build_choi_predictor(prep_operators: Iterable[ndarray], meas_operators: Iterable[ndarray]) ndarray[source]

Given a list of prepared and measured states as state vectors, return a matrix that predicts observed probabilities when applied to the Choi matrix of a process.

In other words, for given prepared states \(\rho_i\) and measurement operators \(P_j\), the returned matrix \(M\) computes the outcomes \(p_{ij} = \operatorname{tr}\left(\mathcal{E}(\rho_i) P_j \right)\) by applying it to the Choi matrix \(C_\mathcal{E}\), i.e. \(p = M\ C_\mathcal{E}\).

This is the same convention for the order of entries in the outcome matrix \(p\) as used by prepare_data().

Parameters:
  • prep_operators – The prepared states, given as density matrices. Typically, the states are pure states \(\left(\left|\psi_i\right>\right)_i\), and \(\rho_i = \left|\psi_i\right>\left<\psi_i\right|\).

  • meas_operators – The measurement operators. Typically, the measurements are ideal projections onto states \(\left(\left|\phi_j\right>\right)_j\), and \(P_j = \left|\phi_j\right>\left<\phi_j\right|\).

oitg.circuits.protocols.process_tomo.analyse.invert_choi_predictor(choi_predictor: ndarray, observations: ndarray) ndarray[source]

Obtain an estimate for the process \(\mathcal{E}\) by applying the inverse of the given Choi predictor to a matrix of experimentally measured outcomes.

If the set of measurements is over-complete, the least-squares estimate will be computed (which is sometimes stated by using the Moore-Penrose pseudo-inverse).

Parameters:
  • choi_predictor – The Choi predictor matrix, as computed by build_choi_predictor().

  • observations – The number of observations per prepared/measured state; see prepare_data(). Will be normalised row-by-row as necessary.

Returns:

The linear inversion tomography estimate for the process, as a Choi matrix \(C_{\mathcal{E}}\). Note that in the presence of sampling noise or experimental imperfections, the returned superoperator will not necessarily be physical, i.e. neither completely positive nor trace-preserving.

oitg.circuits.protocols.process_tomo.analyse.linear_inversion_tomography(prep_projectors: List[ndarray], meas_operators: List[ndarray], observations: ndarray) ndarray[source]

Calculate the linear inversion estimate of the quantum process that has produced the given observations.

Parameters:
  • prep_projectors – A list of the states prepared in the tomography experiment.

  • meas_operators – A list of the measurement operators (projectors) in the tomography experiment.

  • observations – A matrix giving the number of times each outcome was observed in the experiment; see prepare_data().

Returns:

The linear inversion tomography estimate for the process as a Choi matrix; see invert_choi_predictor().

oitg.circuits.protocols.process_tomo.analyse.negative_log_likelihood(choi_predictor: ndarray, observation_vec: ndarray, choi: ndarray) float[source]

Return the negative log-likelihood for the given outcomes to be observed (with experiments as described by the Choi predictor) as a function of the given superoperator in Choi representation.

See negative_log_likelihood_gradient() for calculating the gradient.

oitg.circuits.protocols.process_tomo.analyse.negative_log_likelihood_gradient(choi_predictor: ndarray, observation_vec: ndarray, choi: ndarray) ndarray[source]

Calculate the derivative of the log-likelihood \(\mathcal{L}(C_\mathcal{E})\) around the given Choi matrix \(C_\mathcal{E}\).

See negative_log_likelihood() for computing the value at a given point.

Returns:

A matrix giving the gradient \(\nabla \mathcal{L}(C_\mathcal{E}) = \frac{\partial\mathcal{L}(C_\mathcal{E})}{\partial C_\mathcal{E}}\), in the usual element-wise matrix calculus sense.

oitg.circuits.protocols.process_tomo.analyse.diluted_mle_tomography(choi_predictor: ndarray, observations: ndarray, rel_tol: float = 1e-10, iteration_limit: int = 10000) ndarray[source]

Calculate the tomography estimate of the quantum process that has produced the given observations using a diluted fixed-point iteration method.

By definition, the maximum-likelihood estimate for the superoperator maximises the likelihood function. The extremal condition can be stated in a form amenable to fixed-point iteration as shown in [FH01]. The trace-preserving constraint is expressed via Lagrange multipliers ([FH01] eq. 16), and complete positivity is enforced at each step by explicitly forcing the iterates to be Hermitian.

The particular formulation used here is described in [AL12] section 2.1. The dilution parameter is chosen to ensure progress at each step as suggested in [RHKL07] (but without any of the possible further optimisations for maximising the likelihood increase discussed there).

Parameters:
  • choi_predictor – The Choi predictor matrix, see build_choi_predictor().

  • observations – The number of observations per prepared/measured state; see prepare_data(). Will be normalised row-by-row as necessary.

  • rel_tol – Stopping criterion, given as relative change of the negative log-likelihood.

  • iteration_limit – Maximum number of iterations; an error is thrown if it is reached before the stopping criterion is fulfilled.

Returns:

The CPTP tomography estimate for the process, given in the Choi representation.

oitg.circuits.protocols.process_tomo.analyse.project_into_cp(choi: ndarray) ndarray[source]

For the given superoperator (in Choi representation), return the nearest completely-positive one.

See [KBLG18] eq. 8.

class oitg.circuits.protocols.process_tomo.analyse.TPProjector(pure_state_dimension)[source]

Projects superoperators into the trace-preserving subspace.

(This is a class to allow some ancillary matrices to be re-used across different project() invocations.)

Parameters:

pure_state_dimension – The dimension \(d\) of the Hilbert space the density operators of which the superoperators to project act on; i.e. \(d = 2^n\) for \(n\) qubits, and the Choi matrices are \(d^2 \times d^2\) in size.

project(choi: ndarray) ndarray[source]

Return the result of orthogonally projecting the given Choi operator into the trace-preserving subspace.

See [KBLG18] eq. 12.

oitg.circuits.protocols.process_tomo.analyse.project_into_cptp(choi: ndarray, tp_projector: TPProjector, tol: float = 0.0001, iteration_limit: int = 10000) ndarray[source]

Project the given Choi matrix onto the closest superoperator that is both completely positive and trace-preserving.

This implements Algorithm 1 from [KBLG18].

If the iteration did not converge after the given number of steps, an exception is raised.

oitg.circuits.protocols.process_tomo.analyse.pgdb_mle_tomography(choi_predictor: ndarray, observations: ndarray) ndarray[source]

Calculate the tomography estimate of the quantum process that has produced the given observations using a projected gradient descent algorithm with backtracking, as proposed by [KBLG18].

Parameters:
  • choi_predictor – The Choi predictor matrix, see build_choi_predictor().

  • observations – The number of observations per prepared/measured state; see prepare_data(). Will be normalised row-by-row as necessary.

Returns:

The CPTP tomography estimate for the process, given in the Choi representation.

oitg.circuits.protocols.process_tomo.tools module

A few helpers for manipulating quantum states/channels as NumPy arrays.

oitg.circuits.protocols.process_tomo.tools.mat2vec(matrix)[source]

Return the given matrix in vector form.

As per NumPy shape conventions, this is done by stacking rows one after another. The convention chosen is irrelevant, though, as long as it is used consistently.

See vec2mat().

oitg.circuits.protocols.process_tomo.tools.vec2mat(vector)[source]

Transforms a matrix-as-vector back into a matrix.

See mat2vec().

oitg.circuits.protocols.process_tomo.tools.projector(ket)[source]

Return the projector \(\left|\psi\right>\left<\psi\right|\) for the given ket \(\left|\psi\right>\) as a dense matrix.

oitg.circuits.protocols.process_tomo.tools.choi2liou(choi)[source]

Convert a superoperator in Choi representation to Liouville representation.

With our normalisation convention, this is almost an involution up to the different normalisation factors.

See liou2choi().

oitg.circuits.protocols.process_tomo.tools.liou2choi(liou)[source]

Convert a superoperator in Liouville representation to Choi representation.

See choi2liou().

oitg.circuits.protocols.process_tomo.tools.avg_gate_fidelity(liou, target_unitary)[source]

Compute the average gate fidelity of the given superoperator to the target unitary.

Parameters:
  • liou – The superoperator, in Liouville form (i.e \(\overline{U} \otimes U\) in the ideal case, where \(U\) is the given target unitary).

  • target_unitary – The unitary matrix of the target gate liou is supposed to implement.

Randomised Benchmarking (oitg.circuits.protocols.rbm)

Implements randomised benchmarking for (in theory) arbitrarily many qubits.

In practice, the current implementation is only useful for one- or two-qubit systems, as direct enumeration of all Clifford group elements is very inefficient for larger systems.

oitg.circuits.protocols.rbm.generate module

Randomised benchmarking sequence generation.

In the same spirit as oitg.circuits.clifford, this implements pretty much the most pedestrian scheme possible and just tracks the total unitary generated instead of implementing stabilizer calculations.

oitg.circuits.protocols.rbm.generate.generate_rbm_experiment(group: GateGroup, sequence_lengths: Iterable[int], randomisations_per_length: int, pauli_randomize_last=True, interleave_gates=None, derive_shorter_by_truncation=False, seed=None) List[Tuple[List[int], Tuple[Gate, ...], int]][source]
Parameters:
  • sequence_lengths – List of sequence lengths to generate. For each length k, a number of sequences will be generated with k Clifford group elements (or 2 * k - 1 when interleaving a gate).

  • randomisations_per_length – Number of random gate sequences for each given sequence length (twice that for interleaved benchmarking).

  • pauli_randomize_last – If True, the last Clifford element is chosen to invert the previous only gates up to a randomly selected Pauli group element, thus randomising the outcome between 0 and 1. This should always be used, as the analysis is susceptible to SPAM errors otherwise.

  • interleave_gates – If not None, the given gate sequence is interleaved between each two Clifford gates in a second copy of every sequence (to implement interleaved benchmarking).

  • derive_shorter_by_truncation – Derive shorter sequences by truncating longer sequences (and then appending the appropriate inverse gate) instead of generating fresh random sequences. This can be handy for debugging when initially working out phase relationships in the experiment.

  • seed – Base seed for random number generator. If not provided, an unpredictable seed is used.

Returns:

A list of tuples (clifford_idxs, gates, expected_result) giving for each experiment to run the corresponding list of Clifford elements (-1 for the interleaved gate), the gate sequences, and the respective expected results (as the canonical integer representation of the binary string of results, where for each qubit 0 indicates the initial state, and 1 the one orthogonal to that). For interleaved benchmarking, the sequences with interleaved gates comprise the second half of the list.

Robust Phase Estimation (oitg.circuits.protocols.rpe)

Contains a generic implementation of robust phase estimation for single-qubit gate rotation angles.

Robust phase estimation formalises the intuitive strategy of iteratively using more and more repetitions of a given pulse to increase the calibration precision without “skipping a fringe”.

Sequences of power-of-two length are used to iteratively subdivide the interval of possible phases; theoretically achieving \(\mathrm{O}(1 / N)\) scaling (where \(N\) is the sequence length). By also including sequences with an extra π/2 pulse at the end, the protocol is robust against SPAM errors.

A general description of the protocol is given in [KLY15]. We follow the same procedure described by the Sandia group in [RKLM17]. (A variant of the protocol can be used to extract the angle between the rotation axis of two gates; this is not implemented here.)

References

[KLY15]

Kimmel, S., Low, G. H. & Yoder, T. J. Robust calibration of a universal single-qubit gate set via robust phase estimation. Physical Review A 062315, 1–13 (2015).

[RKLM17] (1,2)

Rudinger, K., Kimmel, S., Lobser, D. & Maunz, P. Experimental Demonstration of a Cheap and Accurate Phase Estimation. Physical Review Letters 118, 1–6 (2017).

oitg.circuits.protocols.rpe.generate module

oitg.circuits.protocols.rpe.generate.generate_rpe_sequences(target: Tuple[Gate, ...], pi_2: Tuple[Gate, ...], max_len_exponent: int) Iterable[Tuple[Gate, ...]][source]

Generate sequences for robust phase estimation, up to a total sequence length of \(2^{\mathrm{max\_len\_exponent}} + 1\).

Parameters:
  • target – A gate sequence implementing the target rotation.

  • pi_2 – A gate sequence implementing a π/2 pulse along the same axis as the target gate. (Can be same as target for measuring a π/2 pulse).

  • max_len_exponent – The number of power-of-two refinement steps.

Returns:

A generator yielding all the gate sequences to run.

oitg.circuits.protocols.rpe.analyse module

oitg.circuits.protocols.rpe.analyse.analyse(outcomes: Dict[Tuple[Gate, ...], ndarray]) float[source]

Analyse a Robust Phase Estimation data set and return the gate rotation angle estimate.

Follows the algorithm described in [RKLM17].

Parameters:

outcomes – A dictionary mapping gate sequences to number of observed outcomes, as returned by oitg.results.collect_outcomes().

Returns:

The final estimate for the target gate rotation angle, in \([0, 2\pi)\).

oitg.circuits.protocols.rpe.analyse.estimate_phase(pauli_xy_estimates: Iterable[Tuple[float, float]]) float[source]

Obtain Robust Phase Estimation gate rotation angle estimate from Pauli expectation values.

Parameters:

pauli_xy_estimates – For each of the exponentially lengthening sequence of gates (2^k, k=0, …, n), the projection onto the initial state on the Bloch sphere (cosine in total angle), and that on the state rotated by -π / 2 (sine in total angle). Assuming we start in the |+> eigenstate and the gate applied is a Z rotation, the first element would be the Pauli X expectation value, the second the Pauli Y expectation value.

Returns:

The gate angle estimate, in \([0, 2\pi)\).

Circuit execution

oitg.circuits.runner module

Common interface for gate sequence execution.

Many experimental protocols consist of acquiring outcome statistics for a number of gate sequences (for instance those implemented in oitg.protocols). Traditionally, a considerable fraction of the time spent on implementing these protocols in one’s experiment comes from having to write code to convert between different data formats for specifying gate sequences and results, fetching result data from result files, etc. This module addresses this by specifying a common interface to sequence runners, that is, code that acquires outcome data for a list of given gate sequences.

This interface is given by the SequenceRunner abstract base class, and the SequenceRunnerOptions structure, which implementations of the former should use to accept settings giving the details of how (often) to execute the sequences (in place of, say, an unwieldly long list of constructor arguments).

There are two main aspects of acquiring experimental data that this interface addresses beyond just running sequentially through a long list of gate strings. The first is the fact that experiments will tend to run as kernels on an ARTIQ core device. Kernels are slow to start, and so is, to a lesser degree, any subsequent network communication. Hence, it is advisable to compile/run more than one gate sequence at once. On the other hand, core device resources are limited, so trying to e.g. keep DMA recordings for thousands of long gate sequences in memory at the same time would be a bad idea. The natural solution to this is to process the total list of gate sequences in chunks of appropriate lengths. The chunk size is configurable, as it depends on the particulars of the target system (keeping them about 1 s in wall clock duration might be a good starting point). The exact interpretation will depend on the specifics of the sequence runner implementation, but a common design would be to have a long-running kernel on the core device, which fetches sequences from the host in slices of the configured chunk length, acquires the data for them, and reports the results back to the host afterwards (while fetching the next chunk).

The second aspect are repeats and randomisation. Typical experiments would acquire a few hundred or thousand single-shot measurements per sequence, and might comprise thousands of total sequences. For instance, a multi-qubit tomography experiment might run for an hour or more. On these time scales, slow changes in the laboratory environment, such as thermal drifts (with their typical time scale of ~10 min), are very noticeable. To avoid any systematic shifts in the resulting data, a useful strategy is to acquire the data points in random order to wash out any correlations. Randomly permuting the sequences run shot-by-shot would be ideal, but possibly very expensive, as mentioned above. A possible compromise is to a) acquire several shots per sequence at a time, and/or b) choose at random only within each chunk, where precomputed sequences can be quickly accessed.

The appropriate tradeoffs for both these aspects can be chosen by appropriately configuring the SequenceRunnerOptions: The configurable chunk size allows to limit the amount of resources needed on the core device (e.g. DMA sequences, size of result arrays). The three (conceptual) nested loops (global repeats -> [chunking] -> repeats per chunk -> shots per repeat) allow trading off randomisation quality vs. performance: If switching between different sequences in the same chunk is cheap, choose few shots per repeat, but many repeats per chunk. If you are concerned about slow drifts, use fewer total shots per chunk, but more global repeats.

See e.g. oxart.circuits.runner for actual sequence runner implementations using ARTIQ.

class oitg.circuits.runner.SequenceRunnerOptions(num_global_repeats: int = 1, randomise_globally: bool = True, chunk_size: int = 1, num_repeats_per_chunk: int = 1, num_shots_per_repeat: int = 100, randomise_per_repeat: bool = True)[source]

Specifies common SequenceRunner options.

See the module-level docstring for background on the concepts.

If you do not want to support the whole interface immediately while bringing up a new runner implementation (e.g. no per-chunk repeats or randomisation), consider emitting a warning and, in the case of repeats, folding the factor into other multipliers.

Parameters:
  • num_global_repeats – The number of times to run through the list of sequences given.

  • randomise_globally – Whether to randomise the order of sequences between global repeats (so that they are divided into different chunks, etc.).

  • chunk_size – Number of sequences to execute at once (typically in one RPC cycle to the core device).

  • num_repeats_per_chunk – The number of times to cycle through all sequences within each chunk.

  • num_shots_per_repeat – The number of shots (single measurements) to acquire for each sequence per repeat.

  • randomise_per_repeat – Whether to randomise the order of sequences within each repeat.

class oitg.circuits.runner.SequenceRunner[source]

Executes a number of gate sequences to gather outcome statistics.

See the module-level docstring for details.

run_sequences(sequences: Iterable[Tuple[Gate, ...]], num_qubits: int | None = None, progress_callback: Callable[[ndarray, ndarray], None] | None = None, progress_callback_interval: float = 5.0, dataset_prefix: None | str = 'data.circuits.')[source]

Runs the given sequences and returns result statistics.

Parameters:
  • sequences – The gate sequences to execute.

  • num_qubits – The number of qubits in the circuit; used for readout. If not given, this will be inferred from the largest operand index used in the circuit.

  • progress_callback – An optional callback to be invoked periodically throughout data acquisition, e.g. for updating some on-line plots/analysis. Two NumPy arrays are supplied as arguments, giving the sequence indices and outcomes acquired since the last time the callback was invoked (see return value).

  • progress_callback_interval – The interval between invocations of the progress callback, in seconds.

  • dataset_prefix – Prefix for dataset keys to write executed sequences and their results to. If None, results are not written to datasets. Sequence runner implementations not embedded into an ARTIQ experiment can ignore this.

Returns:

A tuple (run_order, outcomes) of NumPy arrays. run_order lists the order in which the sequences were executed (possibly with repeats) by their respective index in the passed collection. outcomes lists the number of occurrences of the respective measurement outcomes as a two-dimensional array, where the first index matches run_order, and the second specifies the outcome in canonical binary order (i.e. corresponding to projection onto \(\left|0\ldots00\right>, \left|0\ldots01\right>, \ldots, \left|1\ldots11\right>\)).

oitg.circuits.runner.stringify_gate_sequence(seq: Iterable[Gate])[source]

Return the string used to represent seq in the result datasets.