qat.synthopline.generate_trotter_ansatz

qat.synthopline.generate_trotter_ansatz(*operators, nsteps=1, group=False, final_observable=None, init_state=0, strategy='naive', **kwargs)

Builds a ready-to-use QLM job via a Trotter expansion of set of operators.

The function takes a collection of Observable. Each such operator \(H\) will be expanded as a product of Pauli rotations, collectivelly parametrized by the same variational parameter. These expansions are then concatenated in order to build one layer of the final circuit. The number of layers can be specified by the steps parameter.

For instance, if the input consists in two operators \(H_1 = \sum \alpha_i h^{(1)}_i\) and \(H_2 = \sum \alpha_j h^{(2)}_j\), the resulting parametrized circuit will have shape:

\[\prod_j exp\left( -a_2 \alpha_j h^{(2)}_j \right) \prod_i exp\left( -a_1 \alpha_i h^{(1)}_i \right)\]

where \(a_1,a_2\) are variational parameters (i.e abstract parameters).

Notice that :

  • the order between the two products is the same as the order of the inputs (in term of circuits \(H_1\) comes before \(H_2\))

  • the terms of the two operators are collectively parametrized

  • the term ordering in each product might vary depending on the backend method

Example

This example illustrates the role of the group parameter:

from qat.synthopline.pauli_synth import generate_trotter_ansatz
from qat.synthopline.util import extract_pauli_rotations
from qat.core import Observable

H0 = Observable.sigma_z(0, 3) * Observable.sigma_z(1, 3)\
    +  Observable.sigma_z(1, 3) * Observable.sigma_z(2, 3)
print("H0:\n", H0)
H1 = Observable.sigma_x(0, 3) * Observable.sigma_x(1, 3)\
    +  Observable.sigma_x(1, 3) * Observable.sigma_x(2, 3)
print("H1:\n", H1)

print("Without grouping:")
ansatz = generate_trotter_ansatz(H0, H1, nsteps=1, strategy="pauli_synth")
print(sum(1 for op in ansatz.circuit.iterate_simple() if op[0] == "CNOT"), "CNOTs")
# We expect to find all terms of H0, followed by all terms of H1
rotations, _ = extract_pauli_rotations(ansatz.circuit)
for angle, rotation in rotations:
    print(rotation, angle)

print("With grouping:")
ansatz = generate_trotter_ansatz(H0, H1, nsteps=1, group=True, strategy="pauli_synth")
print(sum(1 for op in ansatz.circuit.iterate_simple() if op[0] == "CNOT"), "CNOTs")
# We expect to find terms of H0 and H1 mixed in arbitrary order
rotations, _ = extract_pauli_rotations(ansatz.circuit)
for angle, rotation in rotations:
    print(rotation, angle)
H0:
 1.0 * (ZZ|[0, 1]) +
1.0 * (ZZ|[1, 2])
H1:
 1.0 * (XX|[0, 1]) +
1.0 * (XX|[1, 2])
Without grouping:
4 CNOTs
ZZI t_{0}
IZZ t_{0}
IXX t_{1}
XXI t_{1}
With grouping:
3 CNOTs
IXX t_{1}
IZZ t_{0}
XXI t_{1}
ZZI t_{0}

Notice how the order in which the rotation were synthesized changed. In the second case, all rotations were synthesized in one go, which led to some optimization (we gained one CNOT). Depending of the application, both situations can be useful.

Parameters
  • *operators (list of Observable) – a collection of operators (at least one)

  • nsteps (optional, int) – the number of Trotter steps to perform. Defaults to 1.

  • group (optional, bool) – if set to True, the operators are (1) parametrized and then (2) summed and considered as a single large operator to expand. This is the expected behavior for UCCSD Ansätze, for instance. Defaults to False.

  • final_observable (optional, Observable) – the final observable to sample. If unset, the first operator is used as final operator.

  • init_state (int/str) – a description of the initial state to prepare. Input can be given as a string with characters in ‘0’, ‘1’, ‘+’, ‘-’, or as an integer, in which case it will be interpreted as a bitstring over \(n\) qubits, qubit 0 being the most significant bit. Defaults to 0.

  • strategy (optional, str) – the backend strategy to use to generate the circuit. See below for details. Defaults to naive.

  • kwargs – any strategy dependent argument can be passed as a kwargs.

Strategies:

  • naive generates a naive quantum circuit implementation. Each term of each operator is diagonalized using local Clifford gates, then reduced using a cascade of CNOT gates, thus using \(2(k-1)\) CNOT gates per rotation of weight \(k\).

  • pauli_synth uses a recursive strategy to optimize the rotation order as an attempt to reduce the overall CNOT cost of the circuit.

  • commute was introduce in [CSD20]. It consists in co-diagonalizing commuting terms of each operator, and synthesizing them using the GraySynth algorithm.

  • commute_improved is a variant of commute where co-diagonalization fixes are pulled to the end of the circuit. This can sometimes reduce the CNOT count.

  • greedy greedily picks the next Pauli rotation to implement in order to reduce the CNOT count.

  • greedy_depth picks the next Pauli rotation to implement in order to reduce the overall CNOT depth.

Additional arguments:

  • pauli_synth:
    • graph (optional, networkx.Graph): an architecture graph. If no graph is provided, the algorithm assumes perfect connectivity.

  • commute:
    • co_diag_backend (optional, int): the co-diagonalization backend to use in order to generate the Clifford subcircuits. 0 is for Duncan et al heuristic (see reference), 1 is for naive graph state synthesis, and 2 is for syndrome decoding based graph state synthesis. Default is 0.

  • commute_improved:
    • co_diag_backend (optional, int): same as for “commute”.

Warning

Different backend methods might produce non-equivalent circuits (up to commutation of some Pauli rotations).

Note

Produced jobs are parametrized by variables with name t_{i}. Indexes are skipped from one Trotter layer to the next.

For instance, two Trotter steps for three operators will produce a job with variable names t_{0}, t_{1}, t_{2} for the first layer, and t_{4}, t_{5}, t_{6} for the second layer.

Note

This method is available as an application in Qaptiva Access.