Quokka (or “Quokka Puck”) is a brand new quantum educational device from Chris Ferrie and Simon Devitt, originally funded by a Kickstarter campaign. It is a beautifully designed, standalone, plug and play, 30-qubit quantum computer emulator. Sure you can simulate quantum programs on your laptop, but Quokka is a dedicated device that is designed to make quantum computing education accessible and fun. It is a great way to learn quantum programming and quantum algorithms, experience the interaction flow typical for real quantum hardware, and it is a perfect gift for anyone interested in quantum computing.
In today’s post, we will have a look at how you can run Q# on a Quokka Puck device.
Background π
The documentation for getting started with Quokka is very detailed and easy to follow - and therefore very much out of scope for this post. The post assumes that you have already set up your Quokka device and are ready to run some code on it.
Quokka exposes an HTTP interface and supports OpenQASM 2.0. This means any integration path for a quantum ecosystem relies on two steps - compiling relevant code to an OpenQASM circuit and then running it on the Quokka device.
This poses certain challenges for Q#, which does not natively support exporting a program to OpenQASM. Thankfully, there is an easy solution to this, and I blogged about that recently - a library called Q# Bridge is a super flexible and easy way to export Q# code to OpenQASM.
Integrating Q# into Quokka π
Let’s explore a Jupyter notebook that brings all these things together. First, create the Python environment of your choice (e.g. using venv or conda) and install the following requirements.txt dependencies:
ipykernel
python-dotenv
matplotlib
requests
qsharp_bridge @ https://github.com/qsharp-community/qsharp-bridge/releases/download/0.1.2/qsharp_bridge-0.1.2-py3-none-linux_x86_64.whl; sys_platform == "linux"
qsharp_bridge @ https://github.com/qsharp-community/qsharp-bridge/releases/download/0.1.2/qsharp_bridge-0.1.2-py3-none-macosx_11_0_universal2.whl; sys_platform == "darwin"
qsharp_bridge @ https://github.com/qsharp-community/qsharp-bridge/releases/download/0.1.2/qsharp_bridge-0.1.2-py3-none-win_amd64.whl; sys_platform == "win32"
We will use ipykernel to run the notebook, python-dotenv to load environment variables from a file, matplotlib to visualize the results, and requests to communicate with the Quokka device. Q# Bridge will allow us to convert Q# code to OpenQASM, which is required to run the code on Quokka. The library is not available on PyPI yet, so we are using the pre-built wheels from the releases page. Make sure to pick the right wheel for your platform.
To begin with, we should import the dependencies into the notebook and load the environment variables:
from qsharp_bridge import *
import matplotlib.pyplot as plt
import os
import json
import requests
from dotenv import load_dotenv
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
load_dotenv()
Notice that we do not need to reference the official qsharp package, as we are not running any Q# code directly in this notebook - instead the execution will be done on the Quokka device.
Quokka is reachable over a local HTTP (not HTTPS) server, so we need to tell requests to ignore SSL warnings. We also load the environment variables from a .env file, which we will create in the next step. This is where we will store the URL of the Quokka device.
If you do not own a Quokka, there are publicly available devices that you can use. You can create an account on the Quokka website to find out more and use that URL instead.
QUOKKA_URL=your quokka url
We will then add a visualization function to plot the results of our code execution - effectively a histogram of measurement outcomes for 2 qubits (as we are interested in 2-qubit operations, more on that in a moment):
def visualize(counts):
plt.figure(figsize=(8, 4))
plt.bar(counts.keys(), counts.values())
plt.tight_layout()
plt.show()
Next we will add the code to run a Q# program on Quokka. We will keep the code relatively generic, so that it can work with any executable Q# block. In order to generate the QASM2 code out of Q#, we need will call qasm2 function from Q# Bridge library - I already blogged about this feature a while ago.
Then the request to Quokka is created and sent for execution on the device. The response is parsed and the results are visualized using the visualize function we created earlier.
def run_on_quokka(shots, qsharp_code) -> dict[str, int]:
generation_options = QasmGenerationOptions(include_qelib=False, reset_behavior=QasmResetBehavior.IGNORED)
qasm_code = qasm2(qsharp_code, generation_options)
req = {
'script': qasm_code,
'count': shots
}
quokka_url = f"{os.environ['QUOKKA_URL']}/qsim/qasm"
result = requests.post(quokka_url, json=req, verify=False)
data = json.loads(result.content)
entries = [str(entry) for entry in data['result']['c']]
counts = {entry: entries.count(entry) for entry in set(entries)}
visualize(counts)
return counts
We need one more helper function, one that will allow us to take any Q# expression and wrap it into an executable program:
def wrap_in_entrypoint(qsharp_code, qsharp_expression) -> str:
return qsharp_code + "\n\n" + f"""
operation Main() : (Result, Result) {{
{qsharp_expression}
}}"""
The constraint here is, similarly to our histogram earlier, that the expression must return two qubit measurement results. And this is really everything - we are now well positioned to test this with some real Q# code.
Trying it out π
Let’s now try to run some Q# on Quokka. Here is the code:
import Std.Math.*;
operation RunBell(op: ((Qubit, Qubit) => Unit)) : (Result, Result) {
use (q1, q2) = (Qubit(), Qubit());
PrepareBellState(q1, q2);
op(q1, q2);
H(q1);
H(q2);
(M(q1), M(q2))
}
operation PrepareBellState(q1 : Qubit, q2: Qubit) : Unit {
X(q1);
X(q2);
H(q1);
CNOT(q1, q2);
}
operation Uab(q1 : Qubit, q2: Qubit) : Unit {
R1(PI() / 3.0, q2);
}
operation Uac(q1 : Qubit, q2: Qubit) : Unit {
R1(2.0 * PI() / 3.0, q2);
}
operation Ubc(q1 : Qubit, q2: Qubit) : Unit {
R1(PI() / 3.0, q1);
R1(2.0 * PI() / 3.0, q2);
}
This sets up a simplified simulation of a Bell test experiment. We will start by preparing an entangled Bell State. We will then define Z-axis rotation operations (Uab, Uac, Ubc), with specific angles ($\frac{\pi}{3}$ and $\frac{2\pi}{3}$). They simulate different measurement settings that one might choose in an actual Bell test. The different rotations correspond to choosing different angles for the measurement apparatus on each qubit.
By running RunBell multiple times with different measurement settings we can gather statistical data on the outcomes (bits resulting from measurements). From these outcomes, we can compute correlation coefficients between the results of the two qubits. Bell inequalities set an upper limit on the strength of correlations predicted by any local hidden variable theory. Quantum mechanics, however, predicts correlations that can exceed this classical bound.
The test code is then as follows (assuming we’d load the Q# code into the code variable):
shots = 1024
code_to_test = wrap_in_entrypoint(code, "RunBell(Uab)")
results_ab = run_on_quokka(shots, code_to_test)
p_ab = results_ab["[0, 0]"] / shots
print(f"P(a+,b+) = {p_ab}")
code_to_test = wrap_in_entrypoint(code, "RunBell(Ubc)")
results_bc = run_on_quokka(shots, code_to_test)
p_bc = results_bc["[0, 0]"] / shots
print(f"P(b+,c+) = {p_bc}")
code_to_test = wrap_in_entrypoint(code, "RunBell(Uac)")
results_ac = run_on_quokka(shots, code_to_test)
p_ac = results_ac["[0, 0]"] / shots
print(f"P(a+,c+) = {p_ac}")
bell_result = p_ab + p_bc >= p_ac;
print(f"Bell's inequality satisfied? {bell_result}");
Each unitary operation (e.g., Uab, Uac, Ubc) corresponds to a different choice of measurement basis on the qubits. The notation p_ab, p_ac, and p_bc represents the probability of obtaining $\ket{00}$ under these specific measurement settings. The outcome $\ket{00}$ is used as an indicator for the correlations that are predicted to behave differently under classical local realism versus quantum mechanics.
The inequality:
$p_{ab} + p_{bc} \ge p_{ac}$
is a form of Bell’s inequality. Under any local hidden variable (classical) theory, this relationship between the probabilities must hold. Quantum mechanics, due to entanglement, can predict a situation where the correlations are stronger - this can lead to a violation of the inequality, meaning that the observed data might show
$p_{ab} + p_{bc} < p_{ac}$
thereby contradicting local realism.
If we now run the code, we should see the results of each set of measurements returned by Quokka visualized in a histogram. The final result should be:
Bell's inequality satisfied? False
meaning that the Bell’s inequality was violated.
Conclusion π
This is a simplified example, but it allows to demonstrate the integration of Q# with Quokka. You can use the same approach to run any Q# code on Quokka, as long as you can express it in terms of OpenQASM 2.0.
It is also incredibly satisfying that with a few lines of code, we managed to “experience” the same phenomena that brought Einstein, Podolsky and Rosen to the conclusion that quantum mechanics is incomplete, and, more recently, brought the Nobel Prize to Alain Aspect, John Clauser and Anton Zeilinger.
The full Jupyter Notebook code for this post is available on Github.