Guppy to H2¶
Guppy is a quantum-first programming language built for Quantinuum Helios. The experimental HUGR-QIR tool converts Guppy programs into QIR, enabling their execution on System Model H2. QIR is an industry-standard representation used to access different hardware platforms.
HUGR (Hierarchical Unified Graph Representation) is a compact, graph-based representation of Guppy programs. HUGR and QIR are different representations of quantum programs. Whereas open-source, off-the-shelf tools such as NVIDIA CUDA-Q generate QIR, HUGR is generated from Guppy source. The HUGR-QIR package requires users to generate HUGR before translating it to QIR. HUGR-generated QIR is checked for backward compatibility with System Model H2 using the Quantinuum QIR checker [1].
For Guppy features supported by QIR, see the platform specifications. Not all Guppy features are available in the full QIR adaptive profile specified in the linked table. Additionally, HUGR-QIR does not support conversion of the following Guppy features:
Guppy functions that accept or return arrays (see the caveats)
Unbounded loops
Dynamic qubit allocation
Installation¶
HUGR-QIR is available on PyPI and requires Python 3.12 or newer. See the HUGR-QIR GitHub repository for more information.
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).read()
b1 = measure(q1).read()
b2 = b0 ^ b1
result("0", b2)
By default, hugr_to_qir validates the generated QIR. Passing 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 QIR as textual LLVM IR.
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 for submission to Quantinuum Systems. For more information, see QIR submission.
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 the available QIS functions.
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).read())
result("1", measure(q1).read())
hugr = main.compile()
guppy_llvm_ir = hugr_to_qir(main.compile(), output_format=OutputFormat.LLVM_IR)
print(guppy_llvm_ir)
For compliance with H2, iterations are compiled into a non-compact representation that uses forward branching.
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).read())
result("1", measure(q1).read())
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)¶
HUGR-QIR supports converting programs that use Guppy’s integrated RNG to QIR, including the seeded RNG constructor and the random_int and random_int_bounded(int) methods. The following example demonstrates the use of the seeded constructor and random_int_bounded:
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).read()
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 cannot currently be converted when used in a non-comptime context. To use arrays in Guppy programs and convert them to QIR, allocate them within a @guppy.comptime context. In this context, arrays are equivalent to Python lists. The following guidelines should help ensure a smooth conversion to QIR:
Allocate arrays only within
@guppy.comptimefunctions. Alternatively, use Python lists.Even within
@guppy.comptimefunctions, do not pass in or return arrays.
Use regular Python functions (called from a
@guppy.comptimecontext) for reusable operations on qubit arrays/lists at compile time.These functions can accept and return qubit lists.
To implement runtime logic, use
@guppy-annotated functions, but do not pass or allocate arrays. Only pass or allocate tuples or basic types.Do not use
measure_array/discard_array; instead, use compile-time loops over arrays to perform measurements and report results.
The following example demonstrates this approach:
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
# You can define pure Python functions that use Guppy functions and objects, then call them from `@guppy.comptime` functions.
# Examples include cx_registers, measure_qubits_conditionally, and record_result_array.
# These 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 (such as 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 Boolean values.
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 Boolean values into one integer 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 a Guppy comptime function, 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).read()
if c:
h(q0)
a = measure(q0).read()
if a:
h(q1)
b = measure(q1).read()
return a, b
def main_generator(size: int, theta: float) -> GuppyFunctionDefinition[[None], None]:
# This function returns the Guppy main function for a given size and angle.
# Expect an even integer for the size.
assert size % 2 == 0
@guppy.comptime # For QIR generation, this function must be comptime because 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¶
An example like the one above, but with a loop whose number of iterations depends on measurement results within the loop, generates 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).read()
if b:
h(q0)
i += 1
result("0", measure(q0).read())
result("1", measure(q1).read())
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¶
Using arrays of qubits in non-comptime functions generates invalid QIR. See the compile-time array caveats 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)