# Copyright 2021 Huawei Technologies Co., Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
# pylint: disable=duplicate-code
"""Qubit unitary coupled-cluster ansatz."""
import itertools
import warnings
import numpy
from mindquantum.core.circuit import Circuit
from mindquantum.core.gates import CNOT, RY, X
from mindquantum.core.operators import QubitExcitationOperator, hermitian_conjugated
from mindquantum.core.parameterresolver import ParameterResolver
from .._ansatz import Ansatz
def _check_int_list(input_list, name):
"""Check a list with int."""
if not isinstance(input_list, list):
raise ValueError(f"The input {str(name)} should be a list, but get {type(input_list)}.")
for i in input_list:
if not isinstance(i, int):
raise ValueError(f"The indices of {str(name)} should be integer, but get {type(i)}.")
# pylint: disable=too-few-public-methods
[docs]class QubitUCCAnsatz(Ansatz):
r"""
Qubit Unitary Coupled-Cluster (qUCC) ansatz class.
Qubit Unitary Coupled-Cluster (qUCC) ansatz is a variant of unitary
coupled-cluster ansatz which uses qubit excitation operators instead of
Fermion excitation operators. The Fock space spanned by qubit excitation
operators is equivalent as Fermion operators, therefore the exact
wave function can be approximated using qubit excitation operators at
the expense of a higher order of Trotterization.
The greatest advantage of qUCC is that the number of CNOT gates is much
smaller than the original version of UCC, even using a 3rd or 4th order
Trotterization. Also, the accuracy is greatly improved despite that the
number of variational parameters is increased.
Note:
The Hartree-Fock circuit is not included.
Currently, generalized=True is not allowed since the theory needs verification.
Reference: `Efficient quantum circuits for quantum computational
chemistry <https://doi.org/10.1103/PhysRevA.102.062612>`_.
Args:
n_qubits (int): The number of qubits (spin-orbitals) in the simulation. Default: None.
n_electrons (int): The number of electrons of the given molecule. Default: None.
occ_orb(list): Indices of manually assigned occupied spatial orbitals. Default: None.
vir_orb(list): Indices of manually assigned virtual spatial orbitals. Default: None.
generalized(bool): Whether to use generalized excitations which
do not distinguish occupied or virtual orbitals (qUCCGSD). Currently,
generalized=True is not allowed since the theory needs verification. Default: False.
trotter_step (int): The number of Trotter steps. Default is one. It is
recommended to set a value larger than or equal to 2 to achieve a
good accuracy. Default: 1.
Examples:
>>> from mindquantum.algorithm.nisq import QubitUCCAnsatz
>>> QubitUCCAnsatz().n_qubits
0
>>> qucc = QubitUCCAnsatz(4, 2, trotter_step=2)
>>> qucc.circuit[:10]
q0: ──X──────────●──────────X───────────────────────────────X──────────●──────────X───────
│ │ │ │ │ │
q1: ──┼──────────┼──────────┼────X──────────●──────────X────┼──────────┼──────────┼────X──
│ │ │ │ │ │ │ │ │ │
q2: ──●────RY(t_0_q_s_0)────●────●────RY(t_0_q_s_1)────●────┼──────────┼──────────┼────┼──
│ │ │ │
q3: ────────────────────────────────────────────────────────●────RY(t_0_q_s_2)────●────●──
>>> qucc.n_qubits
4
>>> qucc_2 = QubitUCCAnsatz(occ_orb=[0, 1], vir_orb=[2])
>>> qucc_2.operator_pool
[-1.0*t_0_q_s_0 [Q0^ Q4] +
1.0*t_0_q_s_0 [Q4^ Q0] , -1.0*t_0_q_s_1 [Q1^ Q4] +
1.0*t_0_q_s_1 [Q4^ Q1] , -1.0*t_0_q_s_2 [Q2^ Q4] +
1.0*t_0_q_s_2 [Q4^ Q2] , -1.0*t_0_q_s_3 [Q3^ Q4] +
1.0*t_0_q_s_3 [Q4^ Q3] , -1.0*t_0_q_s_4 [Q0^ Q5] +
1.0*t_0_q_s_4 [Q5^ Q0] , -1.0*t_0_q_s_5 [Q1^ Q5] +
1.0*t_0_q_s_5 [Q5^ Q1] , -1.0*t_0_q_s_6 [Q2^ Q5] +
1.0*t_0_q_s_6 [Q5^ Q2] , -1.0*t_0_q_s_7 [Q3^ Q5] +
1.0*t_0_q_s_7 [Q5^ Q3] , -1.0*t_0_q_d_0 [Q1^ Q0^ Q5 Q4] +
1.0*t_0_q_d_0 [Q5^ Q4^ Q1 Q0] , -1.0*t_0_q_d_1 [Q2^ Q0^ Q5 Q4] +
1.0*t_0_q_d_1 [Q5^ Q4^ Q2 Q0] , -1.0*t_0_q_d_2 [Q2^ Q1^ Q5 Q4] +
1.0*t_0_q_d_2 [Q5^ Q4^ Q2 Q1] , -1.0*t_0_q_d_3 [Q3^ Q0^ Q5 Q4] +
1.0*t_0_q_d_3 [Q5^ Q4^ Q3 Q0] , -1.0*t_0_q_d_4 [Q3^ Q1^ Q5 Q4] +
1.0*t_0_q_d_4 [Q5^ Q4^ Q3 Q1] , -1.0*t_0_q_d_5 [Q3^ Q2^ Q5 Q4] +
1.0*t_0_q_d_5 [Q5^ Q4^ Q3 Q2] ]
"""
# pylint: disable=too-many-arguments
def __init__(self, n_qubits=None, n_electrons=None, occ_orb=None, vir_orb=None, generalized=False, trotter_step=1):
"""Initialize a QubitUCCAnsatz object."""
if n_qubits is not None and not isinstance(n_qubits, int):
raise ValueError(f"The number of qubits should be integer, but get {type(n_qubits)}.")
if n_electrons is not None and not isinstance(n_electrons, int):
raise ValueError(f"The number of electrons should be integer, but get {type(n_electrons)}.")
if isinstance(n_electrons, int) and n_electrons > n_qubits:
raise ValueError(
"The number of electrons must be smaller than the number of qubits (spin-orbitals) in the ansatz!"
)
if occ_orb is not None:
_check_int_list(occ_orb, "occupied orbitals")
if vir_orb is not None:
_check_int_list(vir_orb, "virtual orbitals")
if not isinstance(generalized, bool):
raise ValueError(f"The parameter generalized should be bool, but get {type(generalized)}.")
# Although the code for generalized excitations has been written,
# the physical underneath of such type of operators is still not clear,
# therefore, currently it seems reasonable to make generalized qUCC unavailable.
if isinstance(generalized, bool) and generalized is not False:
raise NotImplementedError("Generalized version of qubit-UCC not implemented!")
if not isinstance(trotter_step, int) or trotter_step < 1:
raise ValueError("Trotter step must be a positive integer!")
# n_qubits is also need for _implement()
super().__init__("Qubit UCC", n_qubits, n_qubits, n_electrons, occ_orb, vir_orb, generalized, trotter_step)
def _single_qubit_excitation_circuit(self, i, k, theta):
"""
Implement circuit for single qubit excitation.
k: creation
"""
circuit_singles = Circuit()
circuit_singles += CNOT(i, k)
circuit_singles += RY(theta).on(k, i)
circuit_singles += CNOT(i, k)
return circuit_singles
def _double_qubit_excitation_circuit(self, i, j, k, l, theta): # noqa: E741 # pylint: disable=too-many-arguments
"""
Implement circuit for double qubit excitation.
k, l: creation
"""
circuit_doubles = Circuit()
circuit_doubles += CNOT.on(k, l)
circuit_doubles += CNOT.on(i, j)
circuit_doubles += CNOT.on(j, l)
circuit_doubles += X.on(k)
circuit_doubles += X.on(i)
circuit_doubles += RY(theta).on(l, ctrl_qubits=[i, j, k])
circuit_doubles += X.on(k)
circuit_doubles += X.on(i)
circuit_doubles += CNOT.on(j, l)
circuit_doubles += CNOT.on(i, j)
circuit_doubles += CNOT.on(k, l)
return circuit_doubles
# pylint: disable=arguments-differ,too-many-arguments,too-many-locals,too-many-branches,too-many-statements
def _implement(
self, n_qubits=None, n_electrons=None, occ_orb=None, vir_orb=None, generalized=False, trotter_step=1
):
"""Implement qubit UCC circuit according to the reference paper."""
occ_indices = []
vir_indices = []
n_orb = 0
n_orb_occ = 0
n_orb_vir = 0
if n_qubits is not None:
if n_qubits % 2 != 0:
raise ValueError('The total number of qubits (spin-orbitals) should be even.')
n_orb = n_qubits // 2
if n_electrons is not None:
n_orb_occ = int(numpy.ceil(n_electrons / 2))
n_orb_vir = n_orb - n_orb_occ
occ_indices = list(range(n_orb_occ))
vir_indices = [i + n_orb_occ for i in range(n_orb_vir)]
warn_flag = False
if occ_orb is not None:
if len(set(occ_orb)) != len(occ_orb):
raise ValueError("Indices for occupied orbitals should be unique!")
warn_flag = True
n_orb_occ = len(occ_orb)
occ_indices = occ_orb
if vir_orb is not None:
if len(set(vir_orb)) != len(vir_orb):
raise ValueError("Indices for virtual orbitals should be unique!")
warn_flag = True
n_orb_vir = len(vir_orb)
vir_indices = vir_orb
if set(occ_indices).intersection(vir_indices):
raise ValueError("Occupied and virtual orbitals should be different!")
indices_tot = occ_indices + vir_indices
max_idx = 0
if set(indices_tot):
max_idx = max(set(indices_tot))
n_orb = max(n_orb, max_idx)
if warn_flag:
warnings.warn(
"[Note] Override n_qubits and n_electrons with manually set occ_orb and vir_orb. Handle with caution!"
)
if generalized:
occ_indices = indices_tot
vir_indices = indices_tot
n_occ = len(occ_indices)
if n_occ == 0:
warnings.warn("The number of occupied orbitals is zero. Ansatz may contain no parameters.")
n_vir = len(vir_indices)
if n_vir == 0:
warnings.warn("The number of virtual orbitals is zero. Ansatz may contain no parameters.")
# Convert spatial-orbital indices to spin-orbital indices
occ_indices_spin = []
vir_indices_spin = []
for i in occ_indices:
occ_indices_spin.append(i * 2)
occ_indices_spin.append(i * 2 + 1)
for i in vir_indices:
vir_indices_spin.append(i * 2)
vir_indices_spin.append(i * 2 + 1)
indices_spin_tot = list(set(occ_indices_spin + vir_indices_spin))
if generalized:
occ_indices_spin = indices_spin_tot
vir_indices_spin = indices_spin_tot
n_occ_spin = len(occ_indices_spin)
n_vir_spin = len(vir_indices_spin)
ansatz_circuit = Circuit()
# Record the operator pool, which can be used for qubit-ADAPT-VQE or other iterative ansatz.
generator_quccsd_singles = []
generator_quccsd_doubles = []
for trotter_idx in range(trotter_step):
singles_counter = 0
# pylint: disable=invalid-name
for (p, q) in itertools.product(vir_indices_spin, occ_indices_spin):
coeff_s = ParameterResolver({f't_{trotter_idx}_q_s_{singles_counter}': 1})
q_pq = QubitExcitationOperator(((p, 1), (q, 0)), 1.0)
q_pq = q_pq - hermitian_conjugated(q_pq)
q_pq = q_pq.normal_ordered()
if list(q_pq.terms):
# The operator pool do not have to contain operators
# from different Trottered steps.
if trotter_idx == 0:
generator_quccsd_singles.append(q_pq * coeff_s)
ansatz_circuit += self._single_qubit_excitation_circuit(q, p, coeff_s)
singles_counter += 1
doubles_counter = 0
for pq_counter, (p_idx, q_idx) in enumerate(itertools.product(range(n_vir_spin), range(n_vir_spin))):
# Only take half of the loop to avoid repeated excitations
if q_idx > p_idx:
continue
p = vir_indices_spin[p_idx]
q = vir_indices_spin[q_idx]
for rs_counter, (r_idx, s_idx) in enumerate(itertools.product(range(n_occ_spin), range(n_occ_spin))):
# Only take half of the loop to avoid repeated excitations
if s_idx > r_idx:
continue
r = occ_indices_spin[r_idx]
s = occ_indices_spin[s_idx]
if generalized and pq_counter > rs_counter:
continue
coeff_d = ParameterResolver({f't_{trotter_idx}_q_d_{doubles_counter}': 1})
q_pqrs = QubitExcitationOperator(((p, 1), (q, 1), (r, 0), (s, 0)), 1.0)
q_pqrs = q_pqrs - hermitian_conjugated(q_pqrs)
q_pqrs = q_pqrs.normal_ordered()
if list(q_pqrs.terms):
# The operator pool do not have to contain operators
# from different Trottered steps.
if trotter_idx == 0:
generator_quccsd_doubles.append(q_pqrs * coeff_d)
ansatz_circuit += self._double_qubit_excitation_circuit(r, s, p, q, coeff_d)
doubles_counter += 1
n_qubits_circuit = 0
if list(ansatz_circuit):
n_qubits_circuit = ansatz_circuit.n_qubits
# If the ansatz's n_qubits is not set by user, use n_qubits_circuit.
if self.n_qubits is None:
self.n_qubits = n_qubits_circuit
if self.n_qubits < n_qubits_circuit:
raise ValueError(
(
f'The number of qubits in the ansatz circuit {n_qubits_circuit} is larger ',
'than the input n_qubits {n_qubits}! Please check input parameters such as occ_orb, etc.',
)
)
self._circuit = ansatz_circuit
self.operator_pool = generator_quccsd_singles + generator_quccsd_doubles