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 Python package and other dependencies:

pip install fire-opal qiskit matplotlib qctrl-visualizer qiskit-ibm-provider

By completing this tutorial, you will:

  1. Define the quantum circuit.
  2. Run the quantum circuit on real hardware and a simulator.
  3. Compare the results.

1. Import packages and dependencies

import fireopal
from qiskit_ibm_runtime import QiskitRuntimeService
from typing import Dict
import qiskit
from qctrlvisualizer import plot_bitstring_probabilities_histogram

2. Define helper functions and the circuit

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 should then be exported to an OpenQASM string using the QuantumCircuit.qasm() method.

def counts_to_probabilities(counts, shot_count):
    probabilites = {keystr: count / shot_count for keystr, count in counts.items()}
    return probabilites


def draw_circuit(qasm_str: str):
    """Draws a QASM circuit."""
    circuit = qiskit.QuantumCircuit.from_qasm_str(qasm_str)
    print(circuit)
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];
"""

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.

3. Set up your credentials and backend

You can either use IBM's publicly-available quantum computers to run this circuit, you can use a private provider. 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.

# These are the properties for the publicly available provider for IBM backends.
# If you have access to a private provider and wish to use it, replace these values.
hub = "ibm-q"
group = "open"
project = "main"
token = "YOUR_IBM_TOKEN"
credentials = fireopal.credentials.make_credentials_for_ibmq(
    token=token, hub=hub, group=group, project=project
)

QiskitRuntimeService.save_account(
    token, instance=hub + "/" + group + "/" + project, overwrite=True
)
service = QiskitRuntimeService()

Choose a backend from those to which your provider has access. You can check your accessible devices through your IBM Quantum account.

# Enter your desired IBM backend here.
backend_name = "desired_backend"

4. Validate the circuit

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 = fireopal.validate(
    circuits=[simon_circuit_qasm], credentials=credentials, backend_name=backend_name
)
print(f"Number of circuit errors found: {len(circuit_errors['results'])}")
Number of circuit errors found: 0

5. Run the circuit with Fire Opal

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 = fireopal.execute(
    circuits=[simon_circuit_qasm],
    shot_count=shot_count,
    credentials=credentials,
    backend_name=backend_name,
)

5.1. Run the circuit without Fire Opal

For comparison, you can run the circuit on IBM hardware without using Fire Opal.

backend = provider.get_backend(backend_name)

circuit_qiskit = qiskit.QuantumCircuit.from_qasm_str(simon_circuit_qasm)
ibm_result = qiskit.execute(circuit_qiskit, backend=backend, shots=shot_count).result()
ibm_counts = ibm_result.get_counts()

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.

6. Plot and intepret results

Here you'll get the measured bitstrings from your results and plot their relative frequency. First, the results from the execution with Fire Opal are shown, followed by the results without Fire Opal, and then the ideal distribution.

# Plot Fire Opal results

fire_opal_bitstring_results = fire_opal_results["results"][0]
plot_bitstring_probabilities_histogram(
    {"with Fire Opal": fire_opal_bitstring_results, "without Fire Opal": ibm_counts}
)

png-1

6.1. Compare to the ideal distribution

To get a sense of what the distribution should look like in the absence of hardware noise, you can run the same circuit using a simulator. 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.

# Run on simulator and plot the ideal results

from qiskit import Aer, transpile

shot_count = 1024

simon_circuit_qiskit = QuantumCircuit.from_qasm_str(simon_circuit_qasm)

aer_sim = Aer.get_backend("aer_simulator")
transpiled_circuit_qiskit = transpile(simon_circuit_qiskit, aer_sim)
ideal_simulator_result = aer_sim.run(transpiled_circuit_qiskit).result()
ideal_simulator_counts = ideal_simulator_result.get_counts(transpiled_circuit_qiskit)

ideal_counts = {f"{n:03b}": 0 for n in range(8)}
ideal_counts.update(ideal_simulator_counts)
ideal_probabilities = counts_to_probabilities(
    counts=ideal_counts, shot_count=shot_count
)
plot_bitstring_probabilities_histogram({"ideal": ideal_probabilities})

png-2

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, and the results obtained using Fire Opal are closer to those measured without. However, due to hardware noise, some erroneous bitstrings are bound occur. In the next section, you can quantify the accuracy of each run by checking the probability that a random selection of one of your results would be incorrect.

7. Calculate and compare error probability

The following helper functions check the probability of getting the wrong result from each of the previously generated distributions.

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_results: Dict[str, float], 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, probability in bitstring_results.items():
        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

First, check Fire Opal's distribution for the probability of inaccuracy.

b = "110"
print("b = " + b)
print()
print("Fire Opal result:")
print(
    f"  Probability of sampling an incorrect z: {error_probability(fire_opal_bitstring_results, b):.1%}"
)
b = 110

Fire Opal result:
  24.2%✅  110.000 = 0 (mod 2)
  21.6%✅  110.001 = 0 (mod 2)
   2.9%❌  110.010 = 1 (mod 2)
   2.2%❌  110.011 = 1 (mod 2)
   2.6%❌  110.100 = 1 (mod 2)
   2.2%❌  110.101 = 1 (mod 2)
  24.4%✅  110.110 = 0 (mod 2)
  19.7%✅  110.111 = 0 (mod 2)
  Probability of sampling an incorrect z: 10.1%

Next, perform the same check with the results generated without Fire Opal.

ibm_probabilities = counts_to_probabilities(counts=ibm_counts, shot_count=shot_count)

b = "110"
print("b = " + b)
print()
print("Fire Opal result:")
print(
    f"  Probability of sampling an incorrect z: {error_probability(ibm_probabilities, b):.1%}"
)
b = 110

Fire Opal result:
  23.1%✅  110.000 = 0 (mod 2)
  18.7%✅  110.001 = 0 (mod 2)
   8.1%❌  110.010 = 1 (mod 2)
   4.8%❌  110.011 = 1 (mod 2)
   4.9%❌  110.100 = 1 (mod 2)
   5.3%❌  110.101 = 1 (mod 2)
  18.8%✅  110.110 = 0 (mod 2)
  16.4%✅  110.111 = 0 (mod 2)
  Probability of sampling an incorrect z: 23.0%

The probability of getting an incorrect result without Fire Opal is more than double the chances with Fire Opal!

As a quick check, you can also compare the results of the ideal simulator. If there was no hardware noise at all, the probability of obtaining an incorrect output should be 0%.

b = "110"
print("b = " + b)
print()
print("Ideal simulator result:")
print(
    f"  Probability of sampling an incorrect z: {error_probability(ideal_probabilities, b):.1%}"
)
b = 110

Ideal simulator result:
  25.4%✅  110.000 = 0 (mod 2)
  27.1%✅  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)
  25.8%✅  110.110 = 0 (mod 2)
  21.8%✅  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.

8. Conclusions

Congratulations! You've now run Simon's algorithm and seen the benefits of Fire Opal. By suppressing hardware noise, Fire Opal gets much closer to the ideal distribution and lowers the likelihood of obtaining incorrect measurements. This is even more crucial when running deeper and more complex circuits.

The quantum control technology can be applied across various algorithms. Next, try using Fire Opal's QAOA solver by running the tutorial Solving MaxCut using Fire Opal.

The package versions below were used to produce this notebook.

from fireopal import print_package_versions

print_package_versions()
| Package               | Version |
| --------------------- | ------- |
| Python                | 3.11.0  |
| networkx              | 2.8.8   |
| numpy                 | 1.26.2  |
| sympy                 | 1.12    |
| fire-opal             | 6.7.0   |
| qctrl-workflow-client | 2.2.0   |

Was this useful?

cta background

New to Fire Opal?

Get access to everything you need to automate and optimize quantum hardware performance at scale.