Variational Ansätze synthesis

Problem

In plenty of applications ranging from material science to quantum chemistry and linear system resolution, one needs to perform a task called Hamiltonian simulation.

In this setup, we are given some Hamiltonian \(H = \sum_i \alpha_i h_i\) and we want to build a circuit implementing \(e^{-i\gamma H}\). The standard approach is to perform a (first order) Trotter-Suzuki expansion. This expansion is based on the fact that the operator \(\left(\prod_i e^{-i\gamma \frac{\alpha_i}{n}h_i}\right)^n\) converges to our target unitary operator as \(n\) diverges.

Hence, we can simply take a large enough \(n\) and implement a circuit composed of \(n\) identical layers, each corresponding to a sequence of simpler unitary operators \(e^{-i\beta h_i}\). Under the assumption that each \(h_i\) is simple (e.g a Pauli operator), we can efficiently produce this circuit and thus approximate the propagator \(e^{-i\gamma H}\).

It is rather easy to formulate a simple circuit that implements each of the terms of a so called Trotter expansion (at least in the case where the \(h_i\) are Pauli operators, which fits most applications). However, it is far from optimal.

Synthesis

Synthopline comes with a collection of algorithms that, given a high level description of \(H\), can generate circuits implementing its Trotter expanded propagator. The main function packing these algorithms is the qat.synthopline.generate_trotter_ansatz() method.

For instance, we can use this method to generate a QAOA like Ansatz:

from qat.synthopline.pauli_synth import generate_trotter_ansatz

from qat.core import Observable

import networkx as nx

# Generating a random graph
nbqbits = 3
graph = nx.generators.path_graph(nbqbits)

# Generating a target Hamiltonian
H1 = sum(Observable.sigma_z(a, nbqbits) * Observable.sigma_z(b, nbqbits) for a, b in graph.edges())
# Generating an initial Hamiltonian
H0 = sum(Observable.sigma_x(qb, nbqbits) for qb in range(nbqbits))
# Our initial state will be a uniform superposition |+>
init_state = "+" * nbqbits

# nsteps plays the role of the `depth` parameter
ansatz = generate_trotter_ansatz(H1, H0, nsteps=2, final_observable=H1, init_state=init_state)

for name, params, qbits in ansatz.circuit.iterate_simple():
    print(name, params[0] if params else "", qbits)
H  [0]
H  [1]
H  [2]
CNOT  [1, 0]
PH t_{0} [0]
CNOT  [1, 0]
CNOT  [2, 1]
PH t_{0} [1]
CNOT  [2, 1]
H  [0]
PH t_{1} [0]
H  [0]
H  [1]
PH t_{1} [1]
H  [1]
H  [2]
PH t_{1} [2]
H  [2]
CNOT  [1, 0]
PH t_{3} [0]
CNOT  [1, 0]
CNOT  [2, 1]
PH t_{3} [1]
CNOT  [2, 1]
H  [0]
PH t_{4} [0]
H  [0]
H  [1]
PH t_{4} [1]
H  [1]
H  [2]
PH t_{4} [2]
H  [2]

The resulting job is more or less equivalent to a job produced by the qaoa_job() method of the CombinatorialProblem class.

As for linear operator and phase polynomial synthesis, we provide a helper function to generate random instances of Pauli products called qat.synthopline.pauli_synth.generate_random_observable().

Additionally, we also provide a utility function that extracts a sequence of Pauli rotation out of a quantum circuit called qat.synthopline.util.extract_pauli_rotations().

Benchmarks

We ran all the possible backend strategies of qat.synthopline.generate_trotter_ansatz() on random observablen measuring both the CNOT count and the CNOT depth of the output circuit. Results are reported in the two figures below.

../_images/benchmark_trotter.png ../_images/benchmark_trotter_depth.png
According to these benchmarks:
  • The choice of the greedy strategy seems the logical one to optimize CNOT count

  • The choice of the greedy_depth strategy seems the best choice to minimize CNOT depth