Qiskit Primitives¶
The existing Qiskit interface to backends (backend.run()) was originally
designed to accept a list of circuits and return counts for every job. Over
time, it became clear that users have diverse purposes for quantum computing,
and therefore the ways in which they define the requirements for their computing
jobs are expanding.
To address this issue, Qiskit introduced the qiskit.primitives module in
version 0.35.0 (Qiskit Terra 0.20.0) to provide a simplified way for end users
to compute outputs of interest from a QuantumCircuit and to use backends.
Qiskit’s primitives provide methods that make it easier to build modular
algorithms and other higher-order programs. Rather than simply returning counts,
they return more immediately meaningful information.
The two currently available primitives are the Sampler and the Estimator.
The first one simulates a circuit and returns the measurement counts, while the
second one calculates and interprets expectation values of quantum operators
that are required for many near-term quantum algorithms.
DDSIM provides its own version of these Qiskit primitives that leverage the default circuit simulator based on decision diagrams, while preserving the methods and functionality of the original Qiskit primitives.
Sampler¶
The Sampler implements the Qiskit
BaseSamplerV2 interface. It takes a list of
QuantumCircuit objects and simulates them using the CircuitSimulator from
MQT DDSIM, returning the measurement counts. The results are encapsulated within
a PrimitiveResult object that also contains the job’s
metadata.
Furthermore, the Sampler also handles
compilation and parameter binding when working with parametrized circuits.
Here, we show an example on how to use this submodule:
1import numpy as np
2from qiskit import QuantumCircuit
3from qiskit.circuit import Parameter
4
5from mqt.ddsim.primitives import Sampler
6
7# Parametrized circuit to create an X-gate
8theta_a = Parameter("theta_a")
9circ = QuantumCircuit(1)
10circ.ry(theta_a, 0)
11circ.measure_all()
12
13# Show circuit
14circ.draw("mpl")
1# Initialize sampler
2sampler = Sampler()
The run() method accepts an iterable of PUB-like objects, where PUB stands for
primitive unified bloc. In the example below, the PUB comprises the circuit and
an array of parameter values, where the latter is optional. For more
information, see Qiskit’s
StatevectorSampler.run(). The
number of shots can be set as an additional argument. If not set, the default
value is 1024.
1pubs = [(circ, [np.pi])]
2shots = int(1e4)
3job = sampler.run(pubs, shots=shots)
4result = job.result()
The result() method of the job returns a PrimitiveResult
object. This object contains a SamplerPubResult, which, in
turn, contains the measurement counts. It furthermore contains job metadata.
1counts = result[0].data["meas"].get_counts()
2
3print(f">>> {result}")
4print(f" > Counts: {counts}")
>>> PrimitiveResult([SamplerPubResult(data=DataBin(meas=BitArray(<shape=(1,), num_shots=10000, num_bits=1>)), metadata={'shots': 10000, 'circuit_metadata': {}})], metadata={'version': 2})
> Counts: {'1': 10000}
It is also possible to simulate multiple circuits in one job:
1# Create a second circuit
2circ_2 = QuantumCircuit(1)
3circ_2.measure_all()
4circ_2.draw("mpl")
In this case, we have two PUB-like objects.
1job = sampler.run([(circ, [np.pi]), circ_2], shots=int(1e4))
2result = job.result()
3
4counts_1 = result[0].data["meas"].get_counts()
5counts_2 = result[1].data["meas"].get_counts()
6
7print(f">>> {result}")
8print(f" > Counts for circuit 1: {counts_1}")
9print(f" > Counts for circuit 2: {counts_2}")
>>> PrimitiveResult([SamplerPubResult(data=DataBin(meas=BitArray(<shape=(1,), num_shots=10000, num_bits=1>)), metadata={'shots': 10000, 'circuit_metadata': {}}), SamplerPubResult(data=DataBin(meas=BitArray(<shape=(1,), num_shots=10000, num_bits=1>)), metadata={'shots': 10000, 'circuit_metadata': {}})], metadata={'version': 2})
> Counts for circuit 1: {'1': 10000}
> Counts for circuit 2: {'0': 10000}
Now, let us use the Sampler on a more
complex circuit. For this example, we will test Grover’s algorithm on a 4-qubit
circuit where the marked states in the binary representation are “0110” and
“1010”.
A similar implementation of Grover’s algorithm using Qiskit’s sampler can be found here: https://learning.quantum.ibm.com/tutorial/grovers-algorithm
1# Built-in imports
2import math
3
4# Qiskit imports
5from qiskit.circuit.library import MCMTGate, GroverOperator, ZGate
6from qiskit.visualization import plot_distribution
7
8# Define an oracle circuit that marks the input states, which are represented as a list of bitstrings
9def grover_oracle(marked_states: list) -> QuantumCircuit:
10 if not isinstance(marked_states, list):
11 marked_states = [marked_states]
12 # Compute the number of qubits in circuit
13 num_qubits = len(marked_states[0])
14
15 qc = QuantumCircuit(num_qubits)
16 # Mark each target state in the input list
17 for target in marked_states:
18 # Flip target bit-string to match Qiskit bit-ordering
19 rev_target = target[::-1]
20 # Find the indices of all the '0' elements in bit-string
21 zero_inds = [ind for ind in range(num_qubits) if rev_target.startswith("0", ind)]
22 # Add a multi-controlled Z-gate with pre- and post-applied X-gates (open-controls)
23 # where the target bit-string has a '0' entry
24 qc.x(zero_inds)
25 qc.compose(MCMTGate(ZGate(), num_qubits - 1, 1), inplace=True)
26 qc.x(zero_inds)
27 return qc
28
29# Enter the states to be marked
30marked_states = ["0110", "1010"]
31oracle = grover_oracle(marked_states)
32oracle.draw("mpl")
1# Build circuit that amplifies the marked states by the oracle
2grover_op = GroverOperator(oracle)
3grover_op.decompose().draw("mpl")
/tmp/ipykernel_2696/2386812682.py:2: DeprecationWarning: The class ``qiskit.circuit.library.grover_operator.GroverOperator`` is deprecated as of Qiskit 2.1. It will be removed in Qiskit 3.0. Use qiskit.circuit.library.grover_operator instead.
grover_op = GroverOperator(oracle)
1# Compute the optimal number of repetitions of the GroverOperator to amplify the probabilities of the marked states
2optimal_num_iterations = math.floor(math.pi / 4 * math.sqrt(2**grover_op.num_qubits / len(marked_states)))
3
4# Build the full Grover circuit
5qc = QuantumCircuit(grover_op.num_qubits)
6# Create even superposition of all basis states
7qc.h(range(grover_op.num_qubits))
8# Apply Grover operator the optimal number of times
9qc.compose(grover_op.power(optimal_num_iterations), inplace=True)
10# Measure all qubits
11qc.measure_all()
12qc.draw("mpl")
After having built the circuit, use the
Sampler to get the measurement counts.
1result = sampler.run([qc], shots=int(1e4)).result()
2counts = result[0].data["meas"].get_counts()
3plot_distribution(counts)
Estimator¶
The Estimator calculates the
expectation value of an observable with respect to a certain quantum state
(described by a quantum circuit). It implements the Qiskit
BaseEstimatorV2 interface. However, in contrast to
Qiskit’s estimator, the DDSIM version exactly computes the expectation value
using its simulator based on decision diagrams instead of sampling.
The Estimator also handles parameter
binding when dealing with parametrized circuits.
Here, we show an example on how to use it:
First, we build the observable and the quantum state. The observable is given as
a SparsePauliOp object, while the quantum state is
described by a QuantumCircuit.
In this example, our observable is the Pauli matrix \(\sigma_{x}\) and the quantum state is \(\frac{1}{\sqrt{2}}(|0\rangle + |1\rangle)\).
1import numpy as np
2from qiskit import QuantumCircuit
3from qiskit.circuit import Parameter
4from qiskit.quantum_info import Pauli
5
6from mqt.ddsim.primitives.estimator import Estimator
7
8# Build quantum state
9circ = QuantumCircuit(1)
10circ.ry(np.pi / 2, 0)
11circ.measure_all()
12
13# Build observable
14pauli_x = Pauli("X")
15
16# Show circuit
17circ.draw("mpl")
1# Initialize estimator
2estimator = Estimator()
The next step involves running the estimation using the run() method. Just
like the run() method of the
Sampler, it accepts an iterable of
PUB-like objects. In the example below, the QuantumCircuit object representing
the quantum state and an array of SparsePauliOp objects
representing the observable. If the circuit is parametrized, we would also pass
an array of parameters. For more information, see Qiskit’s
StatevectorEstimator.run().
1# Enter observable and circuit as a sequence
2job = estimator.run([(circ, [pauli_x])])
3result = job.result()
The result() method of the job returns a PrimitiveResult
object. This object contains a PubResult, which, in turn,
contains the computed expectation values.
1expectation_values = result[0].data["evs"]
2
3print(f">>> {result}")
4print(f" > Expectation values: {expectation_values}")
>>> PrimitiveResult([PubResult(data=DataBin(evs=np.ndarray(<shape=(1,), dtype=float64>), stds=np.ndarray(<shape=(1,), dtype=float64>), shape=(1,)))], metadata={'version': 2})
> Expectation values: [1.]
Now we explore how the Estimator
works with multiple circuits and observables. For this example, we will
calculate the expectation value of \(\sigma_{x}\) and \(\sigma_{y}\) with respect to
the quantum state \(|1\rangle\). Since our observable list has a length of 2, we
need to enter two copies of the QuantumCircuit object representing \(|1\rangle\)
as a sequence:
1# Build quantum state
2circ = QuantumCircuit(1)
3circ.ry(np.pi, 0)
4circ.measure_all()
5
6# Build observables
7pauli_x = Pauli("X")
8pauli_y = Pauli("Y")
9
10# Construct pub
11pub = (circ, [pauli_x, pauli_y])
12
13# Run estimator
14job = estimator.run([pub])
15result = job.result()
1expectation_values = result[0].data["evs"]
2
3print(f">>> {result}")
4print(f" > Expectation values: {expectation_values}")
>>> PrimitiveResult([PubResult(data=DataBin(evs=np.ndarray(<shape=(2,), dtype=float64>), stds=np.ndarray(<shape=(2,), dtype=float64>), shape=(2,)))], metadata={'version': 2})
> Expectation values: [0. 0.]
The first and second entries of the list of values are the expectation value of \(\sigma_{x}\) and \(\sigma_{y}\), respectively.
Let us now calculate the expectation values of \(\sigma_{x}\) with respect to \(\frac{1}{\sqrt{2}}(|0\rangle + |1\rangle)\) and \(\frac{1}{\sqrt{2}}(|0\rangle - |1\rangle)\). For this example we will use parametrized circuits to build the quantum state.
1theta = Parameter("theta")
2circ_2 = QuantumCircuit(1)
3circ_2.ry(theta, 0)
4circ_2.measure_all()
5
6# Show circuit
7circ_2.draw("mpl")
1# Construct pub
2pub = (circ_2, [pauli_x], [[np.pi / 2], [-np.pi / 2]])
3
4# Run estimator
5job = estimator.run([pub])
6result = job.result()
1expectation_values = result[0].data["evs"]
2
3print(f">>> {result}")
4print(f" > Expectation values: {expectation_values}")
>>> PrimitiveResult([PubResult(data=DataBin(evs=np.ndarray(<shape=(2,), dtype=float64>), stds=np.ndarray(<shape=(2,), dtype=float64>), shape=(2,)))], metadata={'version': 2})
> Expectation values: [ 1. -1.]