Hamiltonian Simulation

In this example we’re going to apply the previously learned concept to run a hamiltonian simulation. As before we are going to define a symbolic circuit as an ansatz.

%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 pytket._tket.circuit import Circuit, fresh_symbol


def build_ansatz() -> Circuit:
    a = fresh_symbol("a")
    b = fresh_symbol("b")
    c = fresh_symbol("c")
    circ = Circuit(4)
    circ.CX(0, 1)
    circ.CX(1, 2)
    circ.CX(2, 3)
    circ.Rz(a, 3)
    circ.CX(2, 3)
    circ.CX(1, 2)
    circ.CX(0, 1)
    circ.Rz(b, 0)
    circ.CX(0, 1)
    circ.CX(1, 2)
    circ.CX(2, 3)
    circ.Rz(c, 3)
    circ.CX(2, 3)
    circ.CX(1, 2)
    circ.CX(0, 1)
    return circ

We are going to simulate a hamiltonian given as a list of Pauli strings and their weights.

from tierkreis.builder import GraphBuilder
from tierkreis.controller.data.models import TKR
from typing import NamedTuple, Literal

from substitution_worker import substitute


class SymbolicExecutionInputs(NamedTuple):
    a: TKR[float]
    b: TKR[float]
    c: TKR[float]
    ham: TKR[list[tuple[Literal["pytket._tket.pauli.QubitPauliString"], float]]]
    ansatz: TKR[Literal["pytket._tket.circuit.Circuit"]]


simulation_graph = GraphBuilder(SymbolicExecutionInputs, TKR[float])
substituted_circuit = simulation_graph.task(
    substitute(
        simulation_graph.inputs.ansatz,
        simulation_graph.inputs.a,
        simulation_graph.inputs.b,
        simulation_graph.inputs.c,
    )
)

We will evaluate this circuit with an observable based on a Pauli string

from tierkreis.pytket_worker import (
    append_pauli_measurement_impl,
    optimise_phase_gadgets,
    expectation,
)
from tierkreis.aer_worker import submit_single


class SubmitInputs(NamedTuple):
    circuit: TKR[Literal["pytket._tket.circuit.Circuit"]]
    pauli_string: TKR[Literal["pytket._tket.pauli.QubitPauliString"]]
    n_shots: TKR[int]


def exp_val():
    g = GraphBuilder(SubmitInputs, TKR[float])

    circuit = g.inputs.circuit
    pauli_string = g.inputs.pauli_string
    n_shots = g.inputs.n_shots

    measurement_circuit = g.task(append_pauli_measurement_impl(circuit, pauli_string))

    compiled_circuit = g.task(optimise_phase_gadgets(measurement_circuit))

    backend_result = g.task(submit_single(compiled_circuit, n_shots))
    av = g.task(expectation(backend_result))
    g.outputs(av)
    return g

Since exp_val runs independently for each Pauli string we can implement this using a map, but we need to prepare the inputs first.

from tierkreis.builtins import unzip
from tierkreis.controller.data.models import TKR

pauli_strings_list, parameters_list = simulation_graph.task(
    unzip(simulation_graph.inputs.ham)
)
input_circuits = simulation_graph.map(
    lambda x: SubmitInputs(substituted_circuit, x, simulation_graph.const(100)),
    pauli_strings_list,
)
exp_values = simulation_graph.map(exp_val(), input_circuits)

To estimate the energy, we can take a weighted sum of the expectation values. For this we want to compute a reduction ((x,y) \z –> x*y+z) which we implement as a fold function.

from tierkreis.builtins import add, times, untuple
from tierkreis.graphs.fold import FoldFunctionInput

ComputeTermsInputs = FoldFunctionInput[
    tuple[float, float], float
]  # (value, accum) -> new_accum


def compute_terms():
    g = GraphBuilder(ComputeTermsInputs, TKR[float])

    res_0, res_1 = g.task(untuple(g.inputs.value))
    prod = g.task(times(res_0, res_1))
    sum = g.task(add(g.inputs.accum, prod))

    g.outputs(sum)
    return g

Preparing the inputs for the fold graph we are going to use tuples (x=exp_val, y=weight) and defining the start value z=0.

from tierkreis.builtins import tkr_zip
from tierkreis.graphs.fold import FoldGraphInputs, fold_graph

tuple_values = simulation_graph.task(tkr_zip(exp_values, parameters_list))
fold_inputs = FoldGraphInputs(simulation_graph.const(0.0), tuple_values)

applying the fold operation yields the final output

computed = simulation_graph.eval(fold_graph(compute_terms()), fold_inputs)
simulation_graph.outputs(computed)

As before we now have to set up tierkreis storage and executors

from pathlib import Path
from uuid import UUID

from tierkreis.consts import PACKAGE_PATH
from tierkreis.controller.executor.multiple import MultipleExecutor
from tierkreis.storage import FileStorage
from tierkreis.controller.executor.uv_executor import UvExecutor

storage = FileStorage(UUID(int=102), name="hamiltonian")
example_executor = UvExecutor(
    registry_path=Path().parent / "example_workers", logs_path=storage.logs_path
)
common_executor = UvExecutor(
    registry_path=PACKAGE_PATH.parent / "tierkreis_workers", logs_path=storage.logs_path
)
multi_executor = MultipleExecutor(
    common_executor,
    executors={"custom": example_executor},
    assignments={"substitution_worker": "custom"},
)

and provide the inputs

from pytket._tket.unit_id import Qubit
from pytket.pauli import Pauli, QubitPauliString

qubits = [Qubit(0), Qubit(1), Qubit(2), Qubit(3)]
hamiltonian = [
    (QubitPauliString(qubits, [Pauli.X, Pauli.Y, Pauli.X, Pauli.I]).to_list(), 0.1),
    (QubitPauliString(qubits, [Pauli.Y, Pauli.Z, Pauli.X, Pauli.Z]).to_list(), 0.5),
    (QubitPauliString(qubits, [Pauli.X, Pauli.Y, Pauli.Z, Pauli.I]).to_list(), 0.3),
    (QubitPauliString(qubits, [Pauli.Z, Pauli.Y, Pauli.X, Pauli.Y]).to_list(), 0.6),
]
inputs = {
    "ansatz": build_ansatz().to_dict(),
    "a": 0.2,
    "b": 0.55,
    "c": 0.75,
    "ham": hamiltonian,
}

before we can run the simulation:

from tierkreis.controller import run_graph
from tierkreis.storage import read_outputs


storage.clean_graph_files()
run_graph(
    storage,
    multi_executor,
    simulation_graph,
    inputs,
    polling_interval_seconds=0.2,
)
output = read_outputs(simulation_graph, storage)
print(output)
0.09999999999999998