Configuring Storage and Executors, Submitting to Quantinuum, Using Predefined Workers

In this example we’re covering a finer grained control over the workflow run. These are the internals that are set inside the run_workflow function. For this we’re going to construct a graph thad runs a pytket circuit on a simulator using the quantinuum_worker The worker can be installed using pip install tkr-quantinuum-worker

Opaque Types

Tierkreis can use any type that is serializable as in and outputs. To use such types for type hinting you can use the OpaqueType.

%pip install tierkreis pytket qnexus
from tierkreis.controller.data.models import OpaqueType

Circuit = OpaqueType["pytket._tket.circuit.Circuit"]
BackendResult = OpaqueType["pytket.backends.backendresult.BackendResult"]

Now we can construct a graph using the quantinuum_worker. The api definitions for workers build by the tierkreis teams are already included in tierkreis. Still it is necessary to install the worker manually. We define a graph Circuit -> BackendResult using hardcoded information for which emulator backend to use.

from tierkreis.builder import GraphBuilder
from tierkreis.controller.data.models import TKR
from tierkreis.quantinuum_worker import (
    compile_using_info,
    get_backend_info,
    run_circuit,
)

g = GraphBuilder(TKR[Circuit], TKR[BackendResult])
info = g.task(get_backend_info(device_name=g.const("H2-1")))
compiled_circuit = g.task(compile_using_info(g.inputs, info))
results = g.task(
    run_circuit(
        circuit=compiled_circuit,
        n_shots=g.const(10),
        device_name=g.const("H2-1SC"),
    ),
)
g.outputs(results)

Now we will define our own storage and executor. Storage is responsible for setting up the checkpointing; it stores the state of the computation. We have to provide a uuid, and optionally a name, as before.

from uuid import UUID

from tierkreis.storage import FileStorage

storage = FileStorage(UUID(int=209), do_cleanup=True, name="quantinuum_submission")

Since the quantinuum_worker is a python worker we will use uv to run it. An executor lives in context, where it can access workers to run their main entrypoints. We define this by providing a path to the directory our workers live in, in this case in the tierkreis_workers directory.

from tierkreis.consts import PACKAGE_PATH
from tierkreis.executor import UvExecutor

executor = UvExecutor(PACKAGE_PATH.parent / "tierkreis_workers", storage.logs_path)

Since this graph is using the qnexus api internally you also need to run the following once:

from qnexus.client.auth import login

login()

Once we provide the graph inputs we can now run it by providing a storage and an executor.

from pathlib import Path

from pytket.qasm.qasm import circuit_from_qasm

from tierkreis import run_graph

circuit = circuit_from_qasm(Path().parent / "data" / "ghz_state_n23.qasm")
run_graph(storage, executor, g, circuit)

And finally we can print the outputs

from tierkreis.storage import read_outputs

outputs = read_outputs(g, storage)