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