Guppy to H2¶
Guppy is a quantum-first programming language built for Quantinuum Helios. An experimental tool, HUGR-QIR, enables conversion of a Guppy program into QIR. As a result, experimental QIR conversion enables execution on System Model H2 with Guppy programs. QIR is an industry standard tool used to access different hardware platforms.
HUGR (Hierarchical Unified Graph Representation) is a compact graph-based representation of Guppy programs. HUGR and QIR are equivalents. Whereas opensource off-the-shelf tools (NVIDIA CUDA-Q) generate QIR, HUGR is generated from Guppy source. The HUGR-QIR package requires the user first generate HUGR, before transpiling to QIR. HUGR-generated QIR is verified for backwards compatibility on System Model H2, using the quantinuum qir checker [1].
For Guppy features supported by QIR, please see here. Not all Guppy features are available in the full QIR adaptive profile (specified in the linked table). In addition to this, HUGR-QIR does not support conversion of the following Guppy features:
Guppy functions accepting or returning arrays (See caveats)
Unbounded Loops
Dynamic Qubit Allocation
Installation¶
HUGR-QIR is available on PyPi and requires Python 3.10. The corresponding GitHub repository is available at https://github.com/quantinuum/hugr-qir.git.
pip install hugr-qir
User Workflow¶
from typing import no_type_check
from guppylang import guppy, qubit
from guppylang.std.builtins import result
from guppylang.std.quantum import h, measure
@guppy
@no_type_check
def main() -> None:
q0 = qubit()
q1 = qubit()
h(q0)
h(q1)
b0 = measure(q0)
b1 = measure(q1)
b2 = b0 ^ b1
result("0", b2)
By default, the function will automatically validate the generated QIR. Using the keyword argument, output_format=OutputFormat.BITCODE, returns QIR bitcode.
from hugr_qir.hugr_to_qir import hugr_to_qir
from hugr_qir.output import OutputFormat
guppy_qir_bitcode = hugr_to_qir(main.compile(), output_format=OutputFormat.BITCODE)
Specifying OutputFormat.LLVM_IR returns textual QIR.
guppy_qir_string = hugr_to_qir(main.compile(), output_format=OutputFormat.LLVM_IR)
print(guppy_qir_string)
Only QIR bitcode can be uploaded to Nexus and submitted to Quantinuum Systems. More information on submission is available here.
Supported on H2¶
Supported Actions
Native gates (see below);
Measurements
Result Tagging
Conditional Branching
For Loops
Random Number Generation (RNG)
Shot number retrieval
All hardware native gate operations are supported on System Model H2. Users cannot call the General \(SU(4)\) Entangler gate using Guppy. For a list of supported gate operations see here.
from typing import no_type_check
from guppylang import guppy
from guppylang.std.builtins import result
from guppylang.std.quantum import h, cx, qubit, measure
from hugr_qir.hugr_to_qir import hugr_to_qir
from hugr_qir.output import OutputFormat
@guppy
@no_type_check
def main() -> None:
q0 = qubit()
q1 = qubit()
h(q0)
cx(q0, q1)
result("0", measure(q0))
result("1", measure(q1))
hugr = main.compile()
guppy_llvm_ir = hugr_to_qir(main.compile(), output_format=OutputFormat.LLVM_IR)
print(guppy_llvm_ir)
Iterations are compiled to a non-compact representation using forward branching for compliance with H2.
from typing import no_type_check
from guppylang import guppy, qubit
from guppylang.std.builtins import result
from guppylang.std.quantum import h, measure
@guppy
@no_type_check
def main() -> None:
q0 = qubit()
q1 = qubit()
for _ in range(10):
q3 = qubit()
h(q3)
b = measure(q3)
if b:
h(q0)
result("0", measure(q0))
result("1", measure(q1))
from hugr_qir.hugr_to_qir import hugr_to_qir
from hugr_qir.output import OutputFormat
guppy_llvm_ir = hugr_to_qir(main.compile(), output_format=OutputFormat.LLVM_IR)
print(guppy_llvm_ir)
Random Number Generator (RNG)¶
Conversion of the Guppy integrated RNG to QIR support the constructor with seed, guppylang.std.qsystem.random.RNG.random_int and guppylang.std.qsystem.random.RNG.random_int_bounded(int). The following example shows how this can be used:
from guppylang import guppy
from guppylang.std.builtins import result
from guppylang.std.angles import angle
from guppylang.std.quantum import measure, qubit
from guppylang.std.qsystem import phased_x, rz
from guppylang.std.qsystem.random import RNG
@guppy
def main() -> None:
q = qubit()
rng = RNG(8) # seed=8
index = rng.random_int_bounded(5)
if index == 1:
phased_x(q, angle(1), angle(0))
elif index == 2:
phased_x(q, angle(0), angle(1))
elif index == 3:
rz(q, angle(1))
elif index == 4:
phased_x(q, angle(1), angle(0))
phased_x(q, angle(0), angle(1))
res = measure(q)
result("result", res)
rng.discard()
hugr = main.compile()
guppy_qir = hugr_to_qir(main.compile(), output_format=OutputFormat.LLVM_IR, validate_qir=True)
print(guppy_qir)
Use of compile time arrays¶
Guppy arrays within a non-comptime context cannot be converted at the moment. The best way to still make use of arrays in guppy and convert them to QIR are by making sure you allocate them within a @guppy.comptime context. In this context, arrays are equivalent to python lists. The following rules for writing guppy programs should lead to a smooth conversion to QIR:
Only allocate arrays within
@guppy.comptimefunctions (or alternatively use python lists)Even with
@guppy.comptimefunctions you should not pass in or return arrays
Use regular python functions (called from
@guppy.comptimecontext) for repeatable operations on qubit arrays/lists at compile time.These functions can accept and return qubit lists.
To implement runtime logic: Use
@guppyannotated functions, but do not pass or allocate arrays. Only pass/allocate tuples or basic typesDo not use
measure_array/discard_array, use compile time loops on arrays to do measurements and report results
One example showing this:
from pathlib import Path
from typing import no_type_check
from guppylang import guppy
from guppylang.defs import GuppyFunctionDefinition
from guppylang.std.angles import angle
from guppylang.std.builtins import array, owned
from guppylang.std.platform import result
from guppylang.std.qsystem.random import RNG
from guppylang.std.quantum import crz, cx, h, measure, qubit
# One can define pure python functions that use guppy functions and objects and call them within guppy.comptime functions.
# Examples for this are: cx_registers, measure_qubits_conditionally and record_result_array
# Theses functions can accept and return arrays without breaking QIR generation.
def cx_registers(qreg1: array[qubit], qreg2: array[qubit]) -> None:
# This function can be called from a guppy comptime function
# it can accept and return qubit lists or qubit arrays.
size = len(qreg1)
for i in range(size):
cx(qreg1[i], qreg2[i])
def measure_qubits_conditionally(q: list[qubit]) -> list[bool]:
size = len(q)
creg_results: list[bool] = []
for i in range(0, size - 1, 2):
# Guppy functions (like conditional_measure) can be called from
# python functions that are later included in a guppy module.
qres_i, qres_ip1 = conditional_measure(q[i], q[i + 1])
creg_results.append(qres_i)
creg_results.append(qres_ip1)
return creg_results
def record_result_array(prefix: str, creg_results: list[bool]) -> None:
# Record all values as individual bools
size = len(creg_results)
for i in range(size):
result(f"{prefix}_{i}", creg_results[i])
def record_result_array_record_int(prefix: str, creg_results: list[bool]) -> None:
# Combine all bool values into one int for recording. (Consider endianness)
size = len(creg_results)
integer_value = 0
for b in creg_results:
integer_value = (integer_value << 1) | int(b) # for big-endian
# integer_value = (integer_value << 1) | int(b) # for little-endian
result(prefix, integer_value)
@guppy
@no_type_check
def conditional_measure(q0: qubit @ owned, q1: qubit @ owned) -> tuple[bool, bool]:
# This function must be a guppy function, not guppy comptime, because it conditions
# on runtime values (here: measurement results).
# For QIR generation: this type of function cannot use arrays.
q2 = qubit()
h(q2)
c = measure(q2)
if c:
h(q0)
a = measure(q0)
if a:
h(q1)
b = measure(q1)
return a, b
def main_generator(size: int, theta: float) -> GuppyFunctionDefinition[[None], None]:
# This function returns the guppy main function for a given size
# parameter and a given angle theta.
# expects an even int for the size
assert size % 2 == 0
@guppy.comptime # For QIR generation: this function must be comptime since it uses arrays.
@no_type_check
def main() -> None:
q = array(qubit() for _ in range(size))
q2 = array(qubit() for _ in range(size))
h(q[0])
cx_registers(q, q2)
r1 = measure_qubits_conditionally(q)
r2 = measure_qubits_conditionally(q2)
record_result_array("q1", r1)
record_result_array_record_int("q2", r2)
return main
from hugr_qir.hugr_to_qir import hugr_to_qir
from hugr_qir.output import OutputFormat
guppy_qir = hugr_to_qir(main_generator(size=6, theta=0.3).compile(), output_format=OutputFormat.LLVM_IR, validate_qir=True)
print(guppy_qir)
Unsupported Conversions¶
Use Case 1: Unbounded Loops¶
Constructing a similar example like the one above with a loop which numbers of execution depends on the measurement results inside the loop leads to the generation of invalid QIR.
from typing import no_type_check
from guppylang import guppy, qubit
from guppylang.std.builtins import result
from guppylang.std.quantum import h, measure
@guppy
@no_type_check
def main() -> None:
q0 = qubit()
q1 = qubit()
i = 0
while i < 10:
q3 = qubit()
h(q3)
b = measure(q3)
if b:
h(q0)
i += 1
result("0", measure(q0))
result("1", measure(q1))
hugr = main.compile()
try:
guppy_qir = hugr_to_qir(main.compile(), output_format=OutputFormat.BITCODE, validate_qir=True)
print(guppy_qir)
except Exception as e:
print('Validation failed as expected:')
print(e)
Use Case 2: Non Comptime Qubit Arrays¶
A program using Arrays over qubits leads to invalid QIR if they are used in non comptime functions. See here for alternatives.
from guppylang import guppy
from guppylang.std.builtins import array, result
from guppylang.std.quantum import qubit, measure_array, h, cx
@guppy
def ghz_state() -> array[qubit, 4]:
qubit_array = array(qubit() for _ in range(4))
h(qubit_array[0])
for i in range(1, 4):
cx(qubit_array[0], qubit_array[i])
return qubit_array
@guppy
def main() -> None:
qubit_array = ghz_state()
measure_results = measure_array(qubit_array)
res = 0
for i in range(4):
if measure_results[i]:
res += 2**int(i)
result("result", res)
hugr = main.compile()
try:
guppy_qir = hugr_to_qir(main.compile(), output_format=OutputFormat.BITCODE, validate_qir=True)
print(guppy_qir)
except Exception as e:
print('Validation failed as expected:')
print(e)