Linq: the basics

linq is a submodule of tangelo that helps you connect to and leverage the features of various backends, may they be simulators or QPUs. This notebook talks about the abstract format used in Tangelo to represent a quantum circuit, and how we can then convert it to other formats or objects used in popular packages such as Braket, Qulacs, Qiskit, Cirq (…) and leverage the different features and performance these platforms offer.

Write code once, run it on various platforms with minimal effort: no need to rewrite everything in order to port your project to a different hardware, or generate what you need for a publication. It also means that whatever new method or code you develop can just run on all the compute backends available.

Table of contents

The scope of this submodule goes beyond this, but this notebook only focuses on the basics: the abstract gate and circuit classes, as well as the translator and simulator modules. This should get you started with simulating a variety of quantum circuits, as well as computing expectation values of operators.


Requirements

In order to run the contents of this notebook, you need to have tangelo installed in your python environment. Some cells may require that a specific backend (such as qiskit, qulacs…) is installed to run. Please have a look at their installation instructions, if needed. Executing the cell below provides a quick way of installing the requirements of this notebook.

# Installation of tangelo if not already installed.
try:
    import tangelo
except ModuleNotFoundError:
    !pip install git+https://github.com/goodchemistryco/Tangelo.git@develop  --quiet
    !pip install qiskit qiskit-aer qulacs amazon-braket-sdk Pennylane --quiet

1. Abstract gate class

The linq submodule aims at providing users a straightforward and transparent way to represent a quantum gate operation, and by extension a quantum circuit. It represents a quantum gate operation acting on qubits as a simple python object. An abstract gate has:

  • A name
  • target qubit(s) (only one qubit supported at the moment)
  • control qubit(s) (only one supported at the moment)
  • parameter(s) (only one supported at the moment)
  • a tag saying whether or not it is “variational”, to keep track of gates with variational parameters

An abstract gate is pretty much just a dictionary, with the fields above. Some gates do not need all these fields to be populated in order to be clearly defined: not all of them require control qubits or parameters for instance.

Let’s have a look at how someone can define a gate and print it. You can see below that at instantiation, you can skip some fields or omit their name if the call is non-ambiguous. We always start with the name and the index of the target qubit, as all gates have them.

Note: qubit indexing starts at 0.

from tangelo.linq import Gate

# Create a Hadamard gate acting on qubit 2
H_gate = Gate("H", 2)
# Create a CNOT gate with control qubit 0 and target qubit 1
CNOT_gate = Gate("CNOT", target=1, control=0)
# Create a parameterized rotation on qubit 1 with angle 2 radians
RX_gate = Gate("RX", 1, parameter=2.)
# Create a parameterized rotation on qubit 1 , with an undefined angle, that will be tagged as variational
RZ_gate = Gate("RZ", 1, parameter="an expression", is_variational=True)
# Create a potato gate, acting on qubit 3.
POTATO_gate = Gate("POTATO", 3)

for gate in [H_gate, CNOT_gate, RX_gate, RZ_gate, POTATO_gate]:
    print(gate)
H         target : [2]   
CNOT      target : [1]   control : [0]   
RX        target : [1]   parameter : 2.0
RZ        target : [1]   parameter : an expression   (variational)
POTATO    target : [3]   

What’s that, you don’t know about the POTATO gate?! Oof. Your imposter syndrom must be in full swing right now. It’s ok. No one has noticed yet. Smile. Take another sip of that coffee. Keep reading.

This abstract gate data structure simply stores information, and does not need to fully specify a valid gate operation that corresponds to, let’s say, a very well-defined matrix representation of the operator. As you can see, the parameter for our Rz gate can be anything, and the actual existence of the POTATO gate is questionable. The gate set supported, the conventions on phases and parameters, are all backend-dependent. Therefore, the fields of your abstract gate only really need to make sense later on, once you have picked a target backend (qiskit, qulacs…).

Note: The controlled gates, such as CNOT, expect that you first pass the target qubits and then the control ones, which may be counter-intuitive and trip you up.

2. Abstract circuit class

An abstract circuit can be simply seen as a list of abstract gates. This class has a few methods to help users know at a glance how many gates or qubits are in a quantum circuit, or if it contains gates tagged as variational for example. Other convenience methods to compute the depth of the circuit, identify unentangled registers or qubits that can be simulated separately, etc, are available.

Users can instantiate abstract circuits by directly passing a list of gates, or using the add_gate method on an existing abstract circuit object. It is also possible to concatenate abstract circuits in a pythonic way using the \(+\) or \(*\) operators: useful to build more complex circuits using simpler ones as building blocks. Circuits are iterable objects, which means that you can traverse them in order to traverse the underlying list of gates they’re made of.

Note: It is more efficient to build circuits by using list comprehension and other python syntaxes to efficiently build a list of Gate objects, and then passing it to the Circuit class; rather than use add_gate or concatenate a lot of small Circuit objects with +.

The number of qubits required by your circuit is automatically computed, based on the highest qubit index appearing in your gates. It is however possible to enforce a fixed-sized for your quantum circuit, that goes beyond this number.

We’re all grown-ups, and technically you could perform surgery on your quantum circuit, to directly modify the fields of some of its gates. This can be useful for instance, to change the parameters used in “variational gates” (i.e gates tagged as variational) in the context of variational algorithms. Use responsibly.

from tangelo.linq import Circuit

# Here's a list of abstract gates
mygates = [Gate("H", 2), Gate("CNOT", 1, control=0), Gate("CNOT", target=2, control=1),
           Gate("Y", 0), Gate("RX", 1, parameter=2.)]

# Users can create empty circuit objects and use add_gate later on
circuit1 = Circuit()
for gate in mygates:
    circuit1.add_gate(gate)
    
# Users can also directly instantiate a circuit with a list of gates (preferred)
circuit2 = Circuit(mygates)

# It is possible to concatenate abstract circuit objects
circuit3 = Circuit(mygates) + Circuit([Gate("RZ", 4, parameter="some angle", is_variational=True)])

# Printing a circuit prints gate list
print(circuit3)

# It is possible to examine properties of an abstract circuit directly
print(f"The number of gates contained in circuit3 is {circuit3.size}")
print(f"The number of qubits in circuit3 is {circuit3.width}")
print(f"Does circuit have gates tagged as variational? {circuit3.is_variational}")

# Even to have an overview of the type of gates it contains, and how many of them
print(f"Gate counts: {circuit3.counts}")

# Dark magic: update the parameters of the first variational gate in the circuit
circuit3._variational_gates[0].parameter = 777.
print(f"\n{circuit3}")
Circuit object. Size 6 

H         target : [2]   
CNOT      target : [1]   control : [0]   
CNOT      target : [2]   control : [1]   
Y         target : [0]   
RX        target : [1]   parameter : 2.0
RZ        target : [4]   parameter : some angle  (variational)

The number of gates contained in circuit3 is 6
The number of qubits in circuit3 is 5
Does circuit have gates tagged as variational? True
Gate counts: {'H': 1, 'CNOT': 2, 'Y': 1, 'RX': 1, 'RZ': 1}

Circuit object. Size 6 

H         target : [2]   
CNOT      target : [1]   control : [0]   
CNOT      target : [2]   control : [1]   
Y         target : [0]   
RX        target : [1]   parameter : 2.0
RZ        target : [4]   parameter : 777.0   (variational)

The abstract circuit can therefore be thought as a list of abstract gates, with some self-awareness: it knows what gate objects it contains, yet does not know what operation they actually implement on a quantum state. The latter is deferred to the translation step, which is different for each backend.

In the future, we may provide other features to optimize or analyze abstract circuits. The advantage of doing so means that whatever benefit come out of it, it will carry to any of the target backends.

Note: It is implied that qubits are measured in the computational basis at the end of the circuit. Users should NOT explicitly perform final measurements using the MEASURE instruction, which is designed to handle mid-circuit measurements, indicating that we intend to simulate a mixed state. As mixed states cannot be represented by statevectors, they are simulated differently and require to draw shots / samples, which may increase simulation time considerably.

Helper functions to build circuits

This package provides helper functions allowing users to put together more general circuits easily, without the need to reinvent the wheel when it comes to common patterns. In the future, users could contribute functions generating useful for benchmarking reasons, building blocks to form larger circuits, or well-known quantum circuits such as the QFT, for instance. These could be contributed as helper functions, or available in a separate folder aiming to gather a collection a circuits for different purposes. If you need to do something that seems fairly common, take a quick look around: you may have all the pieces already (or have the opportunity to design and contribute the pieces you need!).

Note: Most functions do not yield a Circuit object, but a list of gates (see the convention used at the end of the function name). For performance reasons, it is better to concatenate a list of gates and then turn it into a Circuit, than to turn smaller lists of gates into Circuit objects and then use the + operator to concatenate them. This will not be an issue unless you are for example running an algorithm that requires a circuit to be rebuilt frequently and requiring many Circuit objects to be concatenated.

Below, an example of how we can design a function to easily produce Circuit objects implementing a parameter sweep, working for any pauli measurement basis, which is not uncommon in hardware experiments: just a combination of loops and helper functions.

The measurement basis is assumed to be passed as a list of 2-tuples of the form (i, K) where i is an integer denoting the qubit index and K a letter (string) that can take the value ‘I’, ‘X’, ‘Y’, or ‘Z’; which also happens to be the format encountered while traversing a QubitOperator object in Openfermion.

from tangelo.linq.helpers.circuits import measurement_basis_gates, pauli_string_to_of

def theta_sweep(theta, m_basis):
    """ A single-parameter circuit, with change of basis at the end if needed """
    my_gates = [Gate('CNOT', target=0, control=1), 
                Gate('RX', target=1, parameter=theta), 
                Gate('CNOT', target=0, control=1)]
    my_gates += measurement_basis_gates(pauli_string_to_of(m_basis))
    return Circuit(my_gates)

# It is easy with linq to move between string or Openfermion-style representations for Pauli words
for theta, m_basis in [(0.1, 'ZZ'), (0.2, 'ZZ'), (0.3, 'XY')]:
    c = theta_sweep(theta, m_basis)
    print(f"{c}\n")
Circuit object. Size 3 

CNOT      target : [0]   control : [1]   
RX        target : [1]   parameter : 0.1
CNOT      target : [0]   control : [1]   


Circuit object. Size 3 

CNOT      target : [0]   control : [1]   
RX        target : [1]   parameter : 0.2
CNOT      target : [0]   control : [1]   


Circuit object. Size 5 

CNOT      target : [0]   control : [1]   
RX        target : [1]   parameter : 0.3
CNOT      target : [0]   control : [1]   
RY        target : [0]   parameter : -1.5707963267948966
RX        target : [1]   parameter : 1.5707963267948966

3. Translator module

In order to make use of the various compute backends available, the translator module provides users with functions that can translate from, and sometimes to, the abstract circuit format. Users can therefore import or export their quantum circuits to work with their favorite framework, using the umbrella function translate_circuit with the desired target and source parameters.

A translate_operator function is also available, for doing the equivalent with qubit / Pauli operators.

The translator module defines how the content of your abstract gates should be parsed, what gates are supported, the convention used, and is subject to error-checking: the contents must make sense for the target backend, otherwise you will get errors. At that point, your gates must be supported and their parameters correct.

You can find in the translator module the dictionaries telling you what backends are supported, and what gates they currently support. As you can see, the exceptional POTATO gate is not yet supported by any available backend. You’d most certainly get an error if you tried to translate an abstract circuit containing such a gate into a given backend’s format, unless a major breakthrough in toaster-oven technology happens and we integrate this disruptive compute platform.

from tangelo.linq import get_supported_gates

for backend, gates in get_supported_gates().items():
    print(f'{backend} : {gates}')
projectq : ['CNOT', 'H', 'MEASURE', 'PHASE', 'RX', 'RY', 'RZ', 'S', 'T', 'X', 'Y', 'Z']
ionq : ['CNOT', 'CPHASE', 'CRX', 'CRY', 'CRZ', 'CX', 'CY', 'CZ', 'H', 'PHASE', 'RX', 'RY', 'RZ', 'S', 'SWAP', 'T', 'X', 'XX', 'Y', 'Z']
qdk : ['CH', 'CNOT', 'CPHASE', 'CRX', 'CRY', 'CRZ', 'CS', 'CSWAP', 'CT', 'CX', 'CY', 'CZ', 'H', 'MEASURE', 'PHASE', 'RX', 'RY', 'RZ', 'S', 'SWAP', 'T', 'X', 'Y', 'Z']
openqasm : ['CNOT', 'CPHASE', 'CRZ', 'CSWAP', 'CY', 'CZ', 'H', 'MEASURE', 'PHASE', 'RX', 'RY', 'RZ', 'S', 'SWAP', 'T', 'X', 'Y', 'Z']
qulacs : ['CH', 'CNOT', 'CPHASE', 'CRX', 'CRY', 'CRZ', 'CSWAP', 'CX', 'CY', 'CZ', 'H', 'MEASURE', 'PHASE', 'RX', 'RY', 'RZ', 'S', 'SWAP', 'T', 'X', 'XX', 'Y', 'Z']
qiskit : ['CH', 'CNOT', 'CPHASE', 'CRX', 'CRY', 'CRZ', 'CSWAP', 'CX', 'CY', 'CZ', 'H', 'MEASURE', 'PHASE', 'RX', 'RY', 'RZ', 'S', 'SWAP', 'T', 'X', 'XX', 'Y', 'Z']
cirq : ['CH', 'CNOT', 'CPHASE', 'CRX', 'CRY', 'CRZ', 'CSWAP', 'CX', 'CY', 'CZ', 'H', 'MEASURE', 'PHASE', 'RX', 'RY', 'RZ', 'S', 'SWAP', 'T', 'X', 'XX', 'Y', 'Z']
braket : ['CNOT', 'CPHASE', 'CRZ', 'CSWAP', 'CX', 'CY', 'CZ', 'H', 'PHASE', 'RX', 'RY', 'RZ', 'S', 'SWAP', 'T', 'X', 'XX', 'Y', 'Z']

The result of the translation step is specific to the given backend: some will return a quantum circuit object, some will return a string of instructions in their specific syntax & language, some a serialized JSON object…

Note: Do not use the MEASURE Gate in your noiseless/shotless simulations, unless your circuit requires a mid-circuit measurement. This gate is intended for the simulation of mixed states.

Note: Backends often have different conventions, operations may differ up to a phase or sign convention for parametrized gates for instance. Since the outcome of your simulation should not depend on the target backend, the translation step enforces a given convention, detailed in the documentation. In particular, this implies that the native circuit written for a given backend may not be equivalent to the same one written in abstract format, and then translated to this backend. Be mindful of that as you try to take some code written for a given backend, to port it to tangelo.

Below, we show how translate_circuit returns backend-specific objects, all with their usual built-in functionalities. See how the print function behaves differently for each of them for instance, and remember that you can use their built-in methods to accomplish many things (ex: after translating your circuit into a Qiskit.QuantumCircuit object, you can use the draw method to export it in a nice format: https://qiskit.org/documentation/stubs/qiskit.circuit.QuantumCircuit.draw.html).

Here are a few examples. Some cells may fail depending on what packages you have installed.

from tangelo.linq import translate_circuit

# Using the Tangelo format as an intermediate, many combinations of source and target
# formats are available. 
circ3_projectq = translate_circuit(circuit3, target="projectq", source="tangelo")
print(f"{circ3_projectq}\n")

# We now use the shorthand for translating a circuit 
# implicitly from tangelo format, to something else.
circ3_projectq = translate_circuit(circuit3, "ionq")
print(f"{circ3_projectq}\n")

circ3_projectq = translate_circuit(circuit3, "qdk")
print(f"{circ3_projectq}\n")
Allocate | Qureg[0]
Allocate | Qureg[1]
Allocate | Qureg[2]
Allocate | Qureg[3]
Allocate | Qureg[4]
H | Qureg[2]
CX | ( Qureg[0], Qureg[1] )
CX | ( Qureg[1], Qureg[2] )
Y | Qureg[0]
Rx(2.0) | Qureg[1]
Rz(777.0) | Qureg[4]


{'qubits': 5, 'circuit': [{'gate': 'h', 'targets': [2]}, {'gate': 'x', 'targets': [1], 'controls': [0]}, {'gate': 'x', 'targets': [2], 'controls': [1]}, {'gate': 'y', 'targets': [0]}, {'gate': 'rx', 'targets': [1], 'rotation': 2.0}, {'gate': 'rz', 'targets': [4], 'rotation': 777.0}]}

namespace MyNamespace
{
    open Microsoft.Quantum.Intrinsic;
    open Microsoft.Quantum.Canon;
    open Microsoft.Quantum.Arrays;
    open Microsoft.Quantum.Extensions.Convert;
    open Microsoft.Quantum.Characterization;
    open Microsoft.Quantum.Measurement;


    /// # Summary:
    ///         Returns the estimated probabilities associated to the different measurements
    ///         after applying the state-preparation provided by the user at runtime to the quantum state
    operation EstimateFrequencies(nQubits : Int, nShots : Int) : Double[]
    {
        mutable frequencies = new Double[2^nQubits];

        for (iShot in 1..nShots)
        {
            // Apply Q# operation (state preparation to measurements) to qubit register of size nQubits
            mutable results = MyQsharpOperation();

            // Update frequencies based on sample value
            mutable index = 0;
            for (iQubit in nQubits-1..-1..0)
            {
                if (results[iQubit] == One) {set index = index + 2^iQubit;}
            }

            set frequencies w/= index <- frequencies[index] + 1.0;
        }

        // Rescale to obtain frequencies observed
        for (iFreq in 0..2^nQubits-1)
        {
            set frequencies w/= iFreq <- frequencies[iFreq] / ToDouble(nShots);
        }

        return frequencies;
    }

    /// INSERT TRANSLATED CIRCUIT HERE
@EntryPoint()
operation MyQsharpOperation() : Result[] {
    mutable c = new Result[5];
    using (qreg = Qubit[5]) {
        H(qreg[2]);
        CNOT(qreg[0], qreg[1]);
        CNOT(qreg[1], qreg[2]);
        Y(qreg[0]);
        Rx(2.0, qreg[1]);
        Rz(777.0, qreg[4]);

        return ForEach(MResetZ, qreg);
    }
}

}

circ3_qiskit = translate_circuit(circuit3, "qiskit")
print(f"{circ3_qiskit}\n")

circ3_openqasm = translate_circuit(circuit3, "openqasm")
print(f"{circ3_openqasm}\n")
                ┌───┐         
q_0: ─────■─────┤ Y ├─────────
        ┌─┴─┐   └───┘┌───────┐
q_1: ───┤ X ├─────■──┤ Rx(2) ├
        ├───┤   ┌─┴─┐└───────┘
q_2: ───┤ H ├───┤ X ├─────────
        └───┘   └───┘         
q_3: ─────────────────────────
     ┌─────────┐              
q_4: ┤ Rz(777) ├──────────────
     └─────────┘              
c_0: ═════════════════════════
                              
c_1: ═════════════════════════
                              
c_2: ═════════════════════════
                              
c_3: ═════════════════════════
                              
c_4: ═════════════════════════
                              

OPENQASM 2.0;
include "qelib1.inc";
qreg q[5];
creg c[5];
h q[2];
cx q[0],q[1];
cx q[1],q[2];
y q[0];
rx(2) q[1];
rz(777) q[4];

circ3_qulacs = translate_circuit(circuit3, "qulacs")
print(f"{circ3_qulacs}\n")
*** Quantum Circuit Info ***
# of qubit: 5
# of step : 3
# of gate : 6
# of 1 qubit gate: 4
# of 2 qubit gate: 2
Clifford  : no
Gaussian  : no


circ3_braket = translate_circuit(circuit3, "braket")
print(f"{circ3_braket}")
T  : |    0     |1|   2    |
                            
q0 : -C----------Y----------
      |                     
q1 : -X----------C-Rx(2.00)-
                 |          
q2 : -H----------X----------
                            
q4 : -Rz(777.00)------------

T  : |    0     |1|   2    |
circ3_cirq = translate_circuit(circuit3, "cirq")
print(f"{circ3_cirq}")
0: ───I───@─────────────Y────────────────
          │
1: ───I───X─────────────@───Rx(0.637π)───
                        │
2: ───I───H─────────────X────────────────

3: ───I──────────────────────────────────

4: ───I───Rz(-0.673π)────────────────────

Note: We provide limited support for translation from a backend-specific format to the abstract format, for convenience. Some of these formats are OpenQASM or projectq.

Saving, loading and sharing quantum circuits

You may need to save, load or share quantum circuits or operators with collaborators, which may be used to working on other platforms.

A first suggestion is to use the OpenQASM format, as it is a rather common human-readable format, supported by most framework. Tangelo proposes convertion to OPENQASM 2.0, and limited reverse convertion from a subset of OpenQASM to Tangelo format.

Otherwise, python packages such as pickle or json may come in handy to export compatible non-string objects, but can be a bit tricky if version numbers of dependencies do not match.

4. Backends

The get_backend function provides a common interface to all supported compute backends, allowing users to focus on the high-level details of their simulation (number of shots, noise model…) without writing low-level code for the target backend. It returns an object inheriting from the abstract Backend class, which can refer to a simulator or an actual QPU. There are built-in backends for various simulators (qiskit, qulacs, cirq…) and it’s possible to define your own backend.

The backend object handles all translation and quantum circuit simulation steps, directly returning the results to the user. Some post-processing methods, such as the computation of the expectation value of a qubit operator, are available as well.

Not all backends provide the same features: some of them do not give access to a state-vector representation of the quantum state, or do not support noisy simulation for instance. Just like the translator module, the simulator module provides a data-structure containing the currently supported features and characteristics of some backends.

from tangelo.linq import get_backend, backend_info

for backend, info in backend_info.items():
    print(f'{backend} : {info}')
qdk : {'statevector_available': False, 'statevector_order': None, 'noisy_simulation': False}
qiskit : {'statevector_available': True, 'statevector_order': 'msq_first', 'noisy_simulation': True}
qulacs : {'statevector_available': True, 'statevector_order': 'msq_first', 'noisy_simulation': True}
cirq : {'statevector_available': True, 'statevector_order': 'lsq_first', 'noisy_simulation': True}

Noiseless simulation

In the example below, we show how get_backend allows for switching backends easily, to perform quantum circuit simulation. We first show how to perform noiseless simulation with access to the exact amplitudes through the state-vector representation of the quantum state, and then show how a noiseless shot-based simulation can be performed.

The simulate method returns a 2-tuple:

  • the first entry is a sparse histogram of frequencies associated to the different observed states, in least-significant qubit first order (e.g ‘01’ means qubit 0 (resp 1) measured in \(|0>\) (resp \(|1>\)) state). That is, it is to be read “left-to-right” in order to map each qubit to the basis state it was observed in.
  • the second entry contains the state-vector representation of the quantum state, if available on the target backend and if the user requires it using the return_statevector optional parameter. Watch out for the order of the amplitudes in the state vector: the backend_info data-structure described above helps with understanding how they map to the different basis states.

The simulate method can also take the optional parameter initial_statevector, which allows to start the system in a given state and “resume” a simulation. It can in particular be useful to avoid resimulating something again and again, when the result is already known and could just be loaded instead. This is one of the perks of statevector simulators !

# Create a circuit object in abstract format
c = Circuit([Gate("RX", 0, parameter=2.), Gate("RY", 1, parameter=-1.)])

# Qiskit noiseless simulator (no shot count or noise model specified)
sim_qiskit = get_backend(target="qiskit")
print(sim_qiskit.simulate(c))

# cirq noiseless simulator (no shot count or noise model specified)
sim_cirq = get_backend(target="cirq")
print(sim_cirq.simulate(c))

# Qulacs noiseless simulator (no shot count or noise model specified)
# Ask to return the state vector as well (exposes the complex amplitudes)
sim_qulacs = get_backend(target="qulacs")
print(f"\n{sim_qulacs.simulate(c, return_statevector=True)}")
({'00': 0.2248275934887112, '10': 0.5453235594453588, '01': 0.06709898823771769, '11': 0.16274985882821247}, None)
({'00': 0.2248275934887112, '01': 0.06709898823771769, '10': 0.5453235594453588, '11': 0.16274985882821247}, None)

({'00': 0.2248275934887112, '10': 0.5453235594453588, '01': 0.06709898823771769, '11': 0.16274985882821247}, array([ 0.47415988+0.j        ,  0.        -0.73846026j,
       -0.25903472+0.j        ,  0.        +0.40342268j]))

Note: If you do not specify any target when calling get_backend, the implementation defaults to either a qulacs simulator if the package found in your environment, or a cirq simulator otherwise.

Note: The native equivalents of this abstract circuit in Qulacs and Qiskit would not return the same results. This is due to the fact that \(Rx_{qiskit}(\theta) = Rx_{qulacs}(-\theta)\) (e.g sign convention), which is also true for the \(Ry\) and \(Rz\) gates. This package however ensures consistent behavior across backends, by enforcing a specifying convention in the translator. For more information about conventions used, please refer to the documentation or check out the source code of the translator module.

In the above cell, we called get_backend without specifying a number of shots or a noise model, but it is possible to do so. You can tweak the behavior of the Backend object by modifying -some- of its attributes after it has been instantied. In particular:

  • freq_threshold is the threshold used to discard negligible frequencies from the histogram returned by the simulate method, in order to avoid returning \(2^{n\_qubits}\) numbers and focus on the main observables.
  • n_shots and noise_model can be changed after the class has been instantiated

In the cell below, we can see how instantiating a shot-based backend and increasing shot count yields results that are getting closer to the exact theoretical distribution.

# Exact probabilities
sim_qulacs = get_backend(target="cirq")
exact_freqs, _ = sim_qulacs.simulate(c)
print(exact_freqs)

# Approximation with different number of shots (higher=more accurate)
sim_qulacs_shots = get_backend(target="cirq", n_shots=100)
freqs, _ = sim_qulacs_shots.simulate(c)
print(freqs)

sim_qulacs_shots.n_shots=10**4
freqs, _ = sim_qulacs_shots.simulate(c)
print(freqs)

sim_qulacs_shots.n_shots=10**6
freqs, _ = sim_qulacs_shots.simulate(c)
print(freqs)
{'00': 0.2248275934887112, '01': 0.06709898823771769, '10': 0.5453235594453588, '11': 0.16274985882821247}
{'10': 0.58, '00': 0.2, '01': 0.05, '11': 0.17}
{'00': 0.2275, '11': 0.1665, '10': 0.5419, '01': 0.0641}
{'10': 0.545223, '00': 0.224795, '11': 0.163024, '01': 0.066958}

Mixed states

The abstract circuit format provides a MEASURE instruction, supported by most compute backends, with the intent of simulating mixed states / mid-circuit measurements in the computational basis (e.g along the Z axis). As mixed states cannot be represented by a statevector, the statevector simulator backends default to simulating the quantum circuit with shots, in order to return a histogram of frequencies. Users thus must ensure the n_shots attribute of their Backend object has been set.

Simulating a mixed state can be considerably slower than simulating a pure state (some backends are particularly not good at this). Including final measurements in your state-preparation circuit is not recommended, if you intend to use linq to simulate it.

# This circuit prepares a Bell pair (superposition of states |00> and |11>), then measures and flips qubit 0.
# Depending on the result of the measurement, the statevector describing the quantum state would be different.
circuit_mixed = Circuit([Gate("H", 0), Gate("CNOT", target=1, control=0), Gate("MEASURE", 0), Gate("X", 0)])
sim = get_backend(target="cirq", n_shots=10**5)

freqs, _ = sim.simulate(circuit_mixed)
print(freqs)
{'01': 0.50089, '10': 0.49911}

We provide compute backends that are more appropriate to simulate mixed states, relying on density matrices for example.

Expectation values

The get_expectation_valuemethod can be used to compute the expectation value of a qubit / Pauli operator with regards to a state-preparation circuit, and get_expectation_value_from_frequencies_oneterm can directly take a sparse histogram of frequencies obtained by executing or simulating a quantum circuit on a backend (which could be resulting from a QPU experiment).

Note: For now, users are to manually loop over terms and corresponding histograms if they intend to compute the expectation value of a linear combination of operators.

# Openfermion operators can be used
from openfermion.ops import QubitOperator
op = 1.0 * QubitOperator('Z0')

# Option1: Directly through a simulator backend, providing the state-preparation circuit
sim = get_backend(target="cirq")
expval1 = sim.get_expectation_value(op, c)
print(expval1)

# Option2: Assume quantum circuit simulation was performed separately (by this package or different services)
freqs, _ = sim.simulate(c)  # circuit c must be consistent with operator, regarding measurement along non-z axes
term, coef = tuple(op.terms.items())[0]  # This yields ((0, 'Z'),), 1.0
expval2 = coef * sim.get_expectation_value_from_frequencies_oneterm(term, freqs)
print(expval2)
-0.4161468365471424
-0.4161468365471424

Beyond that

This notebook provided a general introduction to the linq submodule, which hopefully helped you getting started with quantum circuits in Tangelo. There are many other notebooks available that illustrate quantum algorithms using these data structures, or more advanced features of linq.

What will you do with Tangelo ?