qml.qchem.taper_operation

taper_operation(operation, generators, paulixops, paulix_sector, wire_order, op_wires=None, op_gen=None)[source]

Transform a gate operation with a Clifford operator and then taper qubits.

The qubit operator for the generator of the gate operation is computed either internally or can be provided manually via the op_gen argument. If this operator commutes with all the \(\mathbb{Z}_2\) symmetries of the molecular Hamiltonian, then this operator is transformed using the Clifford operators \(U\) and tapered; otherwise it is discarded. Finally, the tapered generator is exponentiated using Exp for building the tapered unitary.

Parameters
  • operation (Operation or Callable) – qubit operation to be tapered, or a function that applies that operation

  • generators (list[Hamiltonian]) – generators expressed as PennyLane Hamiltonians

  • paulixops (list[Operation]) – list of single-qubit Pauli-X operators

  • paulix_sector (list[int]) – eigenvalues of the Pauli-X operators

  • wire_order (Sequence[Any]) – order of the wires in the quantum circuit

  • op_wires (Sequence[Any]) – wires for the operation in case any of the provided operation or op_gen are callables

  • op_gen (Hamiltonian or Callable) – generator of the operation, or a function that returns it in case it cannot be computed internally.

Returns

list of operations of type Exp implementing tapered unitary operation

Return type

list[Operation]

Raises
  • ValueError – optional argument op_wires is not provided when the provided operation is a callable

  • TypeError – optional argument op_gen is a callable but does not have wires as its only keyword argument

  • NotImplementedError – generator of the operation cannot be constructed internally

  • ValueError – optional argument op_gen is either not a Hamiltonian or a valid generator of the operation

Example

>>> symbols, geometry = ['He', 'H'], np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 1.4589]])
>>> mol = qchem.Molecule(symbols, geometry, charge=1)
>>> H, n_qubits = qchem.molecular_hamiltonian(symbols, geometry, charge=1)
>>> generators = qchem.symmetry_generators(H)
>>> paulixops = qchem.paulix_ops(generators, n_qubits)
>>> paulix_sector = qchem.optimal_sector(H, generators, mol.n_electrons)
>>> tap_op = qchem.taper_operation(qml.SingleExcitation, generators, paulixops,
...                                paulix_sector, wire_order=H.wires, op_wires=[0, 2])
>>> tap_op(3.14159)
[Exp(1.5707949999999993j PauliY), Exp(0j Identity)]

The obtained tapered operation function can then be used within a QNode:

>>> dev = qml.device('default.qubit', wires=[0, 1])
>>> @qml.qnode(dev)
... def circuit(params):
...     tap_op(params[0])
...     return qml.expval(qml.Z(0)@qml.Z(1))
>>> drawer = qml.draw(circuit, show_all_wires=True)
>>> print(drawer(params=[3.14159]))
0: ──Exp(0.00+1.57j Y)─┤ ╭<Z@Z>
1: ────────────────────┤ ╰<Z@Z>

qml.taper_operation can also be used with the quantum operations, in which case one does not need to specify op_wires args:

>>> qchem.taper_operation(qml.SingleExcitation(3.14159, wires=[0, 2]), generators,
...                       paulixops, paulix_sector, wire_order=H.wires)
[Exp(1.570795j PauliY)]

Moreover, it can also be used within a QNode directly:

>>> dev = qml.device('default.qubit', wires=[0, 1])
>>> @qml.qnode(dev)
... def circuit(params):
...     qchem.taper_operation(qml.DoubleExcitation(params[0], wires=[0, 1, 2, 3]),
...                           generators, paulixops, paulix_sector, H.wires)
...     return qml.expval(qml.Z(0)@qml.Z(1))
>>> drawer = qml.draw(circuit, show_all_wires=True)
>>> print(drawer(params=[3.14159]))
0: ─╭Exp(-0.00-0.79j X@Y)─╭Exp(-0.00-0.79j Y@X)─┤ ╭<Z@Z>
1: ─╰Exp(-0.00-0.79j X@Y)─╰Exp(-0.00-0.79j Y@X)─┤ ╰<Z@Z>

For more involved gates operations such as the ones constructed from matrices, users would need to provide their generators manually via the op_gen argument. The generator can be passed as a Hamiltonian:

>>> op_fun = qml.QubitUnitary(np.array([[0.+0.j, 0.+0.j, 0.+0.j, 0.-1.j],
...                                     [0.+0.j, 0.+0.j, 0.-1.j, 0.+0.j],
...                                     [0.+0.j, 0.-1.j, 0.+0.j, 0.+0.j],
...                                     [0.-1.j, 0.+0.j, 0.+0.j, 0.+0.j]]), wires=[0, 2])
>>> op_gen = qml.Hamiltonian([-0.5 * np.pi],
...                          [qml.X(0) @ qml.X(2)])
>>> qchem.taper_operation(op_fun, generators, paulixops, paulix_sector,
...                       wire_order=H.wires, op_gen=op_gen)
[Exp(1.5707963267948957j PauliX)]

Alternatively, generators can also be specified as a function which returns Hamiltonian and uses wires as its only required keyword argument:

>>> op_gen = lambda wires: qml.Hamiltonian(
...     [0.25, -0.25],
...     [qml.X(wires[0]) @ qml.Y(wires[1]),
...      qml.Y(wires[0]) @ qml.X(wires[1])])
>>> qchem.taper_operation(qml.SingleExcitation, generators, paulixops, paulix_sector,
...                       wire_order=H.wires, op_wires=[0, 2], op_gen=op_gen)(3.14159)
[Exp(1.570795j PauliY)]

Consider \(G\) to be the generator of a unitrary \(V(\theta)\), i.e.,

\[V(\theta) = e^{i G \theta}.\]

Then, for \(V\) to have a non-trivial and compatible tapering with the generators of symmetry \(\tau\), we should have \([V, \tau_i] = 0\) for all \(\theta\) and \(\tau_i\). This would hold only when its generator itself commutes with each \(\tau_i\),

\[[V, \tau_i] = 0 \iff [G, \tau_i]\quad \forall \theta, \tau_i.\]

By ensuring this, we can taper the generator \(G\) using the Clifford operators \(U\), and exponentiate the transformed generator \(G^{\prime}\) to obtain a tapered unitary \(V^{\prime}\),

\[V^{\prime} \equiv e^{i U^{\dagger} G U \theta} = e^{i G^{\prime} \theta}.\]