Writing logical programs directly¶
Download this notebook - logical_program.ipynb
Guppy FT supports advanced users who want to write programs using the logical operations of a QEC architecture directly, allowing them to manually optimize their programs.
Custom \(T\) gate injection¶
As as example, the following program defines and uses a custom magic injection function that performs QEC cycles before injection.
from guppylang import guppy
import guppyft.code.steane.logical as steane_logical
@guppy
def custom_t(q: steane_logical.Qubit) -> None:
"""Custom T injection gadget with QEC cycles before injection."""
magic = steane_logical.prep_t_state()
steane_logical.qec_cycle(magic)
steane_logical.qec_cycle(q)
steane_logical.inject_t(q, magic)
@guppy
def custom_t_example() -> None:
q = steane_logical.Qubit() # Logical |0>
steane_logical.h(q) # Logical |+>
# Apply 4 T gates, thus acting as a Z gate in total
for i in range(4):
custom_t(q)
# Apply an X measurement
steane_logical.h(q)
meas = steane_logical.measure_z(q)
# The expected result is 1
output("result", meas.decode())
When we compile this program, we obtain a HUGR (the IR of the Quantinuum compiler ecosystem) that is built out of the HUGR extension of this QEC architecture.
Then, we can call the implement_ops() method from a QEC architecture instance to replace the opaque logical definitions with calls to their physical implementations.
from guppyft.code.steane.encode import SteaneBuilder
# Compile the program
logical_pkg = custom_t_example.compile()
# Create an instance of a Steane architecture and use it to produce a runnable program
steane_arch = SteaneBuilder().build(n_blocks=3)
physical_pkg = steane_arch.implement_ops(logical_pkg)
The resulting physical_pkg is ready to be emulated locally or submitted to Nexus cloud (either for emulation or to run on a device). We show local emulation here:
from guppylang.emulator import EmulatorBuilder
output = (
EmulatorBuilder().build(
physical_pkg, n_qubits=17
) # Uses statevector simulation by default
).run()
print(output.collated_shots())
[{'result': [1]}]
Benefitting from automation¶
Programs written at the logical level can still benefit from automated QEC cycle insertion and parallel state factories.
Below is an example of quantum teleportation written directly using Steane logical operations.
@guppy
def steane_teleport() -> None:
# State to be teleported
src = steane_logical.Qubit()
# Create Bell pair
tmp = steane_logical.Qubit()
tgt = steane_logical.Qubit()
steane_logical.h(tmp)
steane_logical.cx(tmp, tgt)
# Teleport
steane_logical.cx(src, tmp)
steane_logical.h(src)
if steane_logical.measure_z(src).decode():
steane_logical.z(tgt)
if steane_logical.measure_z(tmp).decode():
steane_logical.x(tgt)
output("tgt", steane_logical.measure_z(tgt).decode())
We can define two Steane architecture instances, one with default parameters, and another configured to use parallel state factories and QEC cycle insertion.
from guppyft.code.steane.encode import QECPolicy, QECStyle
from guppyft.code.steane.encode import RUSStateFactoryConf
# Define a QEC policy using Knill style syndrome extraction
# We set the threshold to be 2.
# Both the logical `H` and `CX` gates each have a cost of 1.
qec_policy = QECPolicy(style=QECStyle.Knill, threshold=2)
qec_policy.costs.h = 1.0
qec_policy.costs.cx = 1.0
# Define the state factory configuration.
# Our factory will prepare 2 states in parallel,
# and make 5 RUS attempts for each state.
factory_conf = RUSStateFactoryConf(size=2, max_attempts=5)
# We can now provide the `qec_policy` to define our Steane architecture.
configured_steane_arch = (
SteaneBuilder()
.with_qec_policy(qec_policy)
.with_zero_factory_conf(factory_conf)
.build(n_blocks=3)
)
# We also create a default Steane architecture instance for comparison
default_steane_arch = SteaneBuilder().build(n_blocks=3)
The same logical steane_teleport Guppy program is transformed into two different physical programs depending on the architecture instance we use.
The easiest way to observe this difference is by analysing the log of physical operations that are performed when the program is emulated.
We can do this locally using Selene.
We first execute the program with default configuration.
from selene_sim.event_hooks.instruction_log import CircuitExtractor
from selene_sim.backends.bundled_simulators import Stim
# Produce the HUGR of the program using the default architecture
pkg = default_steane_arch.implement_ops(steane_teleport.compile())
# Create a CircuitExtractor to log the instructions that would run on the quantum device
logger = CircuitExtractor()
output = (
EmulatorBuilder()
.build(pkg, n_qubits=22)
.with_simulator(Stim(random_seed=42)) # Use stabilizer state simulation
.with_event_hook(logger)
).run()
default_depth = logger.shots[0].get_user_circuit().depth_2q()
We now do the same for the configured_steane_arch instance.
In this case, the emulator needs to be provided a larger number of qubits due to Knill-style QEC cycles being inserted and state preparation producing states in batches of two.
# Produce the HUGR of the program using the default architecture
pkg = configured_steane_arch.implement_ops(steane_teleport.compile())
# Create a CircuitExtractor to log the instructions that would run on the quantum device
logger = CircuitExtractor()
output = (
EmulatorBuilder()
.build(pkg, n_qubits=43)
.with_simulator(Stim(random_seed=42)) # Use stabilizer state simulation
.with_event_hook(logger)
).run()
configured_depth = logger.shots[0].get_user_circuit().depth_2q()
We should expect that the program that uses the configured architecture instance has larger depth, since QEC cycles are being introduced.
print("Two-qubit depth of programs:")
print(f"\tWithout QEC cycles: {default_depth}")
print(f"\tWith QEC cycles: {configured_depth}")
Two-qubit depth of programs:
Without QEC cycles: 17
With QEC cycles: 44