Q-CTRL logo

Jupyter Get the notebook

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:

  1. Define the quantum circuit.
  2. Run the quantum circuit on real hardware.
  3. 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)

     ┌───┐ ░                           ░ ┌───┐┌─┐      
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

            f'{probability:>7.1%}{"✅❌"[is_failure]}  {b}.{bitstring} = {is_failure} (mod 2)'

    return failure_probability
b = "110"
print("b = " + b)
print("Fire Opal result:")
    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)}

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("Ideal simulator result:")
    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.