Complex types, optional values

So far we have only build graphs with a single input and output and defined everything else as constants. Tierkreis allows for multiple inputs and outputs similar to python, by defining tuples of multiple fields. As an invariance, a graph will still always have exactly one output node which also mimics python.

In this example we will look at defining graph which compiles and runs a single circuit on an IBM simulator. For this you will need valid IBMQ credentials.

Inputs and Outputs

In tierkreis, inputs and outputs are defined as named tuples of TKR types. Were building a graph circuit -> int -> (BackendResult, int)

%pip install tierkreis pytket
/home/runner/work/tierkreis/tierkreis/.venv/bin/python3: No module named pip
Note: you may need to restart the kernel to use updated packages.
from typing import NamedTuple

from tierkreis.controller.data.models import TKR, OpaqueType


class IBMInput(NamedTuple):
    circuit: TKR[OpaqueType["pytket._tket.circuit.Circuit"]]  # noqa: F821
    n_shots: TKR[int]


class IBMOutput(NamedTuple):
    results: TKR[OpaqueType["pytket._tket.backends.BackendResult"]]  # noqa: F821
    n_shots: TKR[int]

Now we construct the graph using the tkr-aer-worker. Note how we address inputs by their name g.inputs.circuit and for the output we construct an output object.

from tierkreis.aer_worker import get_compiled_circuit, submit_single
from tierkreis.builder import GraphBuilder


def compile_run_single() -> GraphBuilder[IBMInput, IBMOutput]:
    g = GraphBuilder(IBMInput, IBMOutput)

    compiled_circuit = g.task(
        get_compiled_circuit(
            circuit=g.inputs.circuit,
            optimisation_level=g.const(2),
        ),
    )
    res = g.task(submit_single(compiled_circuit, g.inputs.n_shots))
    g.outputs(IBMOutput(res, g.inputs.n_shots))
    return g

We now can run this code as before. Inputs currently still have to be defined as inputs

from uuid import UUID

from pytket._tket.circuit import Circuit

from tierkreis.consts import PACKAGE_PATH
from tierkreis.controller import run_graph
from tierkreis.executor import UvExecutor
from tierkreis.storage import FileStorage, read_outputs

storage = FileStorage(UUID(int=109), do_cleanup=True, name="ibmq_example")
executor = UvExecutor(PACKAGE_PATH.parent / "tierkreis_workers", storage.logs_path)


def ghz() -> Circuit:
    circ1 = Circuit(2)
    circ1.H(0)
    circ1.CX(0, 1)
    circ1.measure_all()
    return circ1


inputs = {
    "circuit": ghz(),
    "n_shots": 1024,
    "backend": "ibm_torino",
}
graph = compile_run_single()
run_graph(
    storage,
    executor,
    graph,
    inputs,
)
res = read_outputs(graph, storage)

Optional Values

Tierkreis can also deal with optional values which are indicated python style bny providing the type hint type | None = None. Previously we have defined the optimization level by setting a constant. Inspecting the type declaration of compile_circuit_ibmq reveals the following type: optimisation_level: TKR[str] | None = None which means we can set it optionally. We will also pull this up into the inputs:

class IBMInputOptional(NamedTuple):
    circuit: TKR[OpaqueType["pytket._tket.circuit.Circuit"]]  # noqa: F821
    n_shots: TKR[int]
    optimisation_level: TKR[int] | None = None  # Optional input


def compile_optional() -> GraphBuilder[IBMInputOptional, IBMOutput]:
    g = GraphBuilder(IBMInputOptional, IBMOutput)

    compiled_circuit = g.task(
        get_compiled_circuit(
            circuit=g.inputs.circuit,
            optimisation_level=g.inputs.optimisation_level,
        ),
    )
    res = g.task(submit_single(compiled_circuit, g.inputs.n_shots))
    g.outputs(IBMOutput(res, g.inputs.n_shots))  # type: ignore
    return g

You can now run the the graph with or without th optional input. Note, that not providing all inputs will produce some error messages.

inputs = {
    "circuit": ghz(),
    "n_shots": 1024,
    # "optimisation_level": 1, Uncomment to provide the optional input
}

graph = compile_optional()
storage.clean_graph_files()
run_graph(
    storage,
    executor,
    graph,
    inputs,
)
res = read_outputs(graph, storage)