Creating and running circuits
Create and run Simon’s algorithm with Fire Opal
In this tutorial, you will create and run a quantum program with Fire Opal. In particular, you will implement Simon’s algorithm to deduce a hidden bitstring from a black-box function. Simon’s algorithm was the first quantum algorithm to show an exponential speedup compared to its classical counterpart.
For this tutorial, you’ll need to install the Fire Opal Client and Qiskit (IBM’s quantum computing Python SDK):
pip install fire-opal qiskit
By completing this tutorial, you will:
- Define the quantum circuit.
- Run the quantum circuit on real hardware.
- Interpret the results.
1. Define the quantum circuit
from typing import Dict
from qiskit import QuantumCircuit
from qiskit.visualization import plot_histogram
By running the cell below, you will define and visualize a quantum circuit for Simon’s algorithm using the OpenQASM string representation. This representation is required by Fire Opal, and is typically obtained by exporting a quantum circuit written with one of the various quantum-specific Python packages. For example, Qiskit has a tutorial to create this same circuit as a QuantumCircuit
object. This object could then be exported to an OpenQASM string using the QuantumCircuit.qasm()
method.
simon_circuit_qasm = """OPENQASM 2.0;
include "qelib1.inc";
// Define two three-qubit quantum registers and one classical register.
qreg q[6];
creg c[3];
// Put the first register in superposition.
h q[0];
h q[1];
h q[2];
// Build the Simon oracle for hidden bitstring '110'. Begin by copying
// the first register to the second. Then, for each '1' in the hidden
// bitstring, apply XOR targeting the corresponding bit in the second
// register.
barrier q[0],q[1],q[2],q[3],q[4],q[5];
cx q[0],q[3];
cx q[1],q[4];
cx q[2],q[5];
cx q[1],q[4];
cx q[1],q[5];
barrier q[0],q[1],q[2],q[3],q[4],q[5];
// Take the first register out of superposition.
h q[0];
h q[1];
h q[2];
// Measure the first register.
measure q[0] -> c[0];
measure q[1] -> c[1];
measure q[2] -> c[2];
"""
def draw_circuit(qasm_str: str):
"""Draws a QASM circuit."""
circuit = QuantumCircuit.from_qasm_str(qasm_str)
print(circuit)
draw_circuit(simon_circuit_qasm)
┌───┐ ░ ░ ┌───┐┌─┐
q_0: ┤ H ├─░───■───────────────────────░─┤ H ├┤M├──────
├───┤ ░ │ ░ ├───┤└╥┘┌─┐
q_1: ┤ H ├─░───┼────■─────────■────■───░─┤ H ├─╫─┤M├───
├───┤ ░ │ │ │ │ ░ ├───┤ ║ └╥┘┌─┐
q_2: ┤ H ├─░───┼────┼────■────┼────┼───░─┤ H ├─╫──╫─┤M├
└───┘ ░ ┌─┴─┐ │ │ │ │ ░ └───┘ ║ ║ └╥┘
q_3: ──────░─┤ X ├──┼────┼────┼────┼───░───────╫──╫──╫─
░ └───┘┌─┴─┐ │ ┌─┴─┐ │ ░ ║ ║ ║
q_4: ──────░──────┤ X ├──┼──┤ X ├──┼───░───────╫──╫──╫─
░ └───┘┌─┴─┐└───┘┌─┴─┐ ░ ║ ║ ║
q_5: ──────░───────────┤ X ├─────┤ X ├─░───────╫──╫──╫─
░ └───┘ └───┘ ░ ║ ║ ║
c: 3/══════════════════════════════════════════╩══╩══╩═
0 1 2
Note that the circuit above is specific to the hidden bitstring 110
. Also notice that you are measuring the top three qubits to obtain a bitstring at the end of the circuit. This measurement will be important for determining the hidden bitstring after you run the quantum program.
2. Run the quantum circuit on real hardware
To run a circuit with Fire Opal, begin by importing the validate
and execute
functions from the Fire Opal Client package.
from fireopal import validate, execute
You will use IBM’s publicly-available quantum computers to run this circuit. To do so, you will need a personal token for IBM’s quantum computing API. Visit IBM Quantum to obtain your token or sign up for an account.
# Enter your IBM token here.
token = "your_IBM_token"
# These are the properties for the publicly available provider for IBM backends.
hub = "ibm-q"
group = "open"
project = "main"
ibm_credentials = {"token": token, "hub": hub, "group": group, "project": project}
Prior to submitting your circuit for execution, use validate
to ensure that it is free of syntax errors and compatible with Fire Opal’s requirements.
Note that upon running a function from Fire Opal for the first time, your default web browser will open and you will be able to enter your Fire Opal credentials. Once authenticated, the browser will display a success message and you may close the tab.
circuit_errors = validate(circuits=[simon_circuit_qasm], credentials=ibm_credentials)
print(f"Number of circuit errors found: {len(circuit_errors['results'])}")
Number of circuit errors found: 0
Now that you know the circuit is error-free, you can run it using execute
by providing the OpenQASM representation of your circuit and your credentials as shown below. Fire Opal will compile, optimize, and make the circuit robust against noise before execution.
Note that this cell might take awhile to run, depending on the level of activity on IBM’s devices. Visit IBM’s jobs page to track the status of your circuit.
shot_count = 1024
fire_opal_results = execute(
circuits=[simon_circuit_qasm], shot_count=shot_count, credentials=ibm_credentials
)
Congratulations! You’ve successfully run a quantum circuit on real hardware. In the next section, you’ll visualize your results and compare them to an ideal case.
3. Interpret results
First, let’s get the measured bitstrings from your results and plot their relative frequency.
fire_opal_bitstring_counts = fire_opal_results["results"][0]
plot_histogram(fire_opal_bitstring_counts, title="Real device results from Fire Opal")
In the absence of hardware noise, the result from Simon’s Algorithm (let’s call it $z$) with the hidden bitstring $b$ always satisfies $ b \cdot z = 0 \; (\textrm{mod} \;2)$. See the Qiskit textbook for details.
In our case, $b$ is 110
. There are four choices for $z$ that satisfy the constraint above: 000
, 001
, 110
, and 111
. Neglecting the trivial case (000
), the other three choices can be used to construct a linear system of equations to determine $b$.
Notice from your histogram that the correct choices for $z$ are indeed the most likely results from running your circuit on Fire Opal. However, due to hardware noise, some erroneous bitstrings also occur. You can obtain a measure of correctness by checking the probability that a random selection of one of your results would be incorrect:
def dot_mod2(b: str, z: str) -> int:
"""Calculates the dot product between bitstrings `b` and `z`, modulus 2."""
total = 0
for i in range(len(b)):
total += int(b[i]) * int(z[i])
return total % 2
def error_probability(bitstring_counts: Dict[str, int], b: int) -> float:
"""
Returns the chance that if we took a single sample from our results
that we would get a sample that would mislead us into reconstructing a wrong `b`.
"""
failure_probability = 0
for bitstring, count in bitstring_counts.items():
probability = count / shot_count
is_failure = dot_mod2(b, bitstring)
failure_probability += is_failure * probability
print(
f'{probability:>7.1%}{"✅❌"[is_failure]} {b}.{bitstring} = {is_failure} (mod 2)'
)
return failure_probability
b = "110"
print("b = " + b)
print()
print("Fire Opal result:")
print(
f" Probability of sampling an incorrect z: {error_probability(fire_opal_bitstring_counts, b):.1%}"
)
b = 110
Fire Opal result:
22.2%✅ 110.000 = 0 (mod 2)
24.1%✅ 110.001 = 0 (mod 2)
1.4%❌ 110.010 = 1 (mod 2)
1.7%❌ 110.011 = 1 (mod 2)
2.1%❌ 110.100 = 1 (mod 2)
1.6%❌ 110.101 = 1 (mod 2)
21.5%✅ 110.110 = 0 (mod 2)
25.6%✅ 110.111 = 0 (mod 2)
Probability of sampling an incorrect z: 6.6%
If there was no hardware noise at all, the probability of obtaining an incorrect output would be 0%. You can confirm this by executing the circuit on a noise-free quantum circuit simulator backend available through Qiskit. Quantum simulators, which use regular classical computation to simulate the dynamics of a quantum computer, are useful for prototyping and checking algorithm correctness on small quantum circuits.
from qiskit import Aer, assemble
shot_count = 1024
simon_circuit_qiskit = QuantumCircuit.from_qasm_str(simon_circuit_qasm)
aer_sim = Aer.get_backend("aer_simulator")
qobj = assemble(simon_circuit_qiskit, shots=shot_count)
ideal_simulator_results = aer_sim.run(qobj).result()
ideal_simulator_counts = ideal_simulator_results.get_counts()
ideal_counts = {f"{n:03b}": 0 for n in range(8)}
ideal_counts.update(ideal_simulator_counts)
plot_histogram(ideal_counts, title="Ideal simulator results")
You can produce the same table of outputs for the ideal simulator:
b = "110"
print("b = " + b)
print()
print("Ideal simulator result:")
print(
f" Probability of sampling an incorrect z: {error_probability(ideal_counts, b):.1%}"
)
b = 110
Ideal simulator result:
24.6%✅ 110.000 = 0 (mod 2)
24.8%✅ 110.001 = 0 (mod 2)
0.0%❌ 110.010 = 1 (mod 2)
0.0%❌ 110.011 = 1 (mod 2)
0.0%❌ 110.100 = 1 (mod 2)
0.0%❌ 110.101 = 1 (mod 2)
24.7%✅ 110.110 = 0 (mod 2)
25.9%✅ 110.111 = 0 (mod 2)
Probability of sampling an incorrect z: 0.0%
As expected, in the absence of hardware noise there is zero probability of the algorithm producing an erroneous output.
Well done! You’ve now compared the performance of your algorithm on Fire Opal to the ideal case.