diff --git a/tangelo/algorithms/variational/tests/test_vqe_solver.py b/tangelo/algorithms/variational/tests/test_vqe_solver.py index cd3f0625f..ec43b0bb6 100644 --- a/tangelo/algorithms/variational/tests/test_vqe_solver.py +++ b/tangelo/algorithms/variational/tests/test_vqe_solver.py @@ -19,8 +19,6 @@ from tangelo.algorithms import BuiltInAnsatze, VQESolver from tangelo.molecule_library import mol_H2_sto3g, mol_H4_sto3g, mol_H4_cation_sto3g, mol_NaH_sto3g, mol_H4_sto3g_symm from tangelo.toolboxes.ansatz_generator.uccsd import UCCSD -from tangelo.toolboxes.ansatz_generator.qmf import QMF -from tangelo.toolboxes.ansatz_generator.qcc import QCC from tangelo.toolboxes.qubit_mappings.mapping_transform import fermion_to_qubit_mapping from tangelo.toolboxes.molecular_computation.rdms import matricize_2rdm from tangelo.toolboxes.optimizers.rotosolve import rotosolve @@ -119,7 +117,7 @@ def test_simulate_qmf_h2(self): """ vqe_options = {"molecule": mol_H2_sto3g, "ansatz": BuiltInAnsatze.QMF, "qubit_mapping": "jw", - "verbose": True} + "verbose": False} vqe_solver = VQESolver(vqe_options) vqe_solver.build() @@ -131,7 +129,20 @@ def test_simulate_qcc_h2(self): parameters, exact simulator. """ vqe_options = {"molecule": mol_H2_sto3g, "ansatz": BuiltInAnsatze.QCC, "qubit_mapping": "jw", - "verbose": True} + "verbose": False} + vqe_solver = VQESolver(vqe_options) + vqe_solver.build() + + energy = vqe_solver.simulate() + self.assertAlmostEqual(energy, -1.137270, delta=1e-4) + + def test_simulate_ilc_h2(self): + """Run VQE on H2 molecule, with ILC ansatz, JW qubit mapping, initial + parameters, exact simulator. + """ + + vqe_options = {"molecule": mol_H2_sto3g, "ansatz": BuiltInAnsatze.ILC, "qubit_mapping": "jw", + "verbose": False} vqe_solver = VQESolver(vqe_options) vqe_solver.build() @@ -209,7 +220,7 @@ def test_simulate_qmf_h4(self): """ vqe_options = {"molecule": mol_H4_sto3g, "ansatz": BuiltInAnsatze.QMF, "qubit_mapping": "jw", - "verbose": True} + "verbose": False} vqe_solver = VQESolver(vqe_options) vqe_solver.build() @@ -220,14 +231,28 @@ def test_simulate_qcc_h4(self): """Run VQE on H4 molecule, with QCC ansatz, JW qubit mapping, initial parameters, exact simulator. """ + vqe_options = {"molecule": mol_H4_sto3g, "ansatz": BuiltInAnsatze.QCC, "qubit_mapping": "jw", - "verbose": True} + "verbose": False} vqe_solver = VQESolver(vqe_options) vqe_solver.build() energy = vqe_solver.simulate() self.assertAlmostEqual(energy, -1.963270, delta=1e-4) + def test_simulate_ilc_h4(self): + """Run VQE on H4 molecule, with ILC ansatz, JW qubit mapping, initial + parameters, exact simulator. + """ + + vqe_options = {"molecule": mol_H4_sto3g, "ansatz": BuiltInAnsatze.ILC, "qubit_mapping": "jw", + "verbose": False} + vqe_solver = VQESolver(vqe_options) + vqe_solver.build() + + energy = vqe_solver.simulate() + self.assertAlmostEqual(energy, -1.960877, delta=1e-4) + def test_simulate_h4_open(self): """Run VQE on H4 molecule, with UCCSD ansatz, JW qubit mapping, initial parameters, exact simulator """ vqe_options = {"molecule": mol_H4_cation_sto3g, "ansatz": BuiltInAnsatze.UCCSD, "qubit_mapping": "jw", @@ -244,7 +269,7 @@ def test_simulate_qmf_h4_open(self): """ vqe_options = {"molecule": mol_H4_cation_sto3g, "ansatz": BuiltInAnsatze.QMF, "qubit_mapping": "jw", - "verbose": True} + "verbose": False} vqe_solver = VQESolver(vqe_options) vqe_solver.build() @@ -257,7 +282,20 @@ def test_simulate_qcc_h4_open(self): """ vqe_options = {"molecule": mol_H4_cation_sto3g, "ansatz": BuiltInAnsatze.QCC, "qubit_mapping": "jw", - "verbose": True} + "verbose": False} + vqe_solver = VQESolver(vqe_options) + vqe_solver.build() + + energy = vqe_solver.simulate() + self.assertAlmostEqual(energy, -1.638020, delta=1e-4) + + def test_simulate_ilc_h4_open(self): + """Run VQE on H4 + molecule, with ILC ansatz, JW qubit mapping, initial + parameters, exact simulator. + """ + + vqe_options = {"molecule": mol_H4_cation_sto3g, "ansatz": BuiltInAnsatze.ILC, "qubit_mapping": "jw", + "verbose": False} vqe_solver = VQESolver(vqe_options) vqe_solver.build() diff --git a/tangelo/algorithms/variational/vqe_solver.py b/tangelo/algorithms/variational/vqe_solver.py index 159e86c1e..7c3519c93 100644 --- a/tangelo/algorithms/variational/vqe_solver.py +++ b/tangelo/algorithms/variational/vqe_solver.py @@ -29,7 +29,8 @@ from tangelo.toolboxes.operators import count_qubits, FermionOperator, qubitop_to_qubitham from tangelo.toolboxes.qubit_mappings.mapping_transform import fermion_to_qubit_mapping from tangelo.toolboxes.ansatz_generator.ansatz import Ansatz -from tangelo.toolboxes.ansatz_generator import UCCSD, RUCC, HEA, UpCCGSD, QMF, QCC, VSQS, UCCGD, VariationalCircuitAnsatz +from tangelo.toolboxes.ansatz_generator import UCCSD, RUCC, HEA, UpCCGSD, QMF, QCC, VSQS, UCCGD, ILC,\ + VariationalCircuitAnsatz from tangelo.toolboxes.ansatz_generator.penalty_terms import combined_penalty from tangelo.toolboxes.post_processing.bootstrapping import get_resampled_frequencies from tangelo.toolboxes.ansatz_generator.fermionic_operators import number_operator, spinz_operator, spin2_operator @@ -47,6 +48,7 @@ class BuiltInAnsatze(Enum): QCC = 6 VSQS = 7 UCCGD = 8 + ILC = 9 class VQESolver: @@ -112,11 +114,11 @@ def __init__(self, opt_dict): if not (bool(self.molecule) ^ bool(self.qubit_hamiltonian)): raise ValueError(f"A molecule OR qubit Hamiltonian object must be provided when instantiating {self.__class__.__name__}.") - # The QCC ansatz requires up_then_down=True when mapping="jw" + # The QCC & ILC ansatze require up_then_down=True when mapping="jw" if isinstance(self.ansatz, BuiltInAnsatze): - if self.ansatz == BuiltInAnsatze.QCC and self.qubit_mapping.lower() == "jw" and not self.up_then_down: - warnings.warn("The QCC ansatz requires spin-orbital ordering to be all spin-up " - "first followed by all spin-down for the JW mapping.", RuntimeWarning) + if self.ansatz in (BuiltInAnsatze.QCC, BuiltInAnsatze.ILC) and self.qubit_mapping.lower() == "jw" and not self.up_then_down: + warnings.warn("Efficient generator screening for QCC-based ansatze requires spin-orbital ordering to be " + "all spin-up first followed by all spin-down for the JW mapping.", RuntimeWarning) self.up_then_down = True self.default_backend_options = default_backend_options @@ -193,6 +195,8 @@ def build(self): self.ansatz = VSQS(self.molecule, self.qubit_mapping, self.up_then_down, **self.ansatz_options) elif self.ansatz == BuiltInAnsatze.UCCGD: self.ansatz = UCCGD(self.molecule, self.qubit_mapping, self.up_then_down, **self.ansatz_options) + elif self.ansatz == BuiltInAnsatze.ILC: + self.ansatz = ILC(self.molecule, self.qubit_mapping, self.up_then_down, **self.ansatz_options) else: raise ValueError(f"Unsupported ansatz. Built-in ansatze:\n\t{self.builtin_ansatze}") elif not isinstance(self.ansatz, Ansatz): diff --git a/tangelo/toolboxes/ansatz_generator/__init__.py b/tangelo/toolboxes/ansatz_generator/__init__.py index 8559fe0bc..3ce7100aa 100644 --- a/tangelo/toolboxes/ansatz_generator/__init__.py +++ b/tangelo/toolboxes/ansatz_generator/__init__.py @@ -13,6 +13,7 @@ # limitations under the License. from .vsqs import VSQS +from .ilc import ILC from .qcc import QCC from .qmf import QMF from .uccsd import UCCSD diff --git a/tangelo/toolboxes/ansatz_generator/_qubit_cc.py b/tangelo/toolboxes/ansatz_generator/_qubit_cc.py index e270cee36..d7ecb26cc 100644 --- a/tangelo/toolboxes/ansatz_generator/_qubit_cc.py +++ b/tangelo/toolboxes/ansatz_generator/_qubit_cc.py @@ -33,10 +33,10 @@ from itertools import combinations from tangelo.toolboxes.operators.operators import QubitOperator -from ._qubit_mf import get_op_expval +from tangelo.toolboxes.ansatz_generator._qubit_mf import get_op_expval -def construct_dis(pure_var_params, qubit_ham, qcc_deriv_thresh, verbose=False): +def construct_dis(qubit_ham, pure_var_params, deqcc_dtau_thresh): """Construct the DIS of QCC generators, which proceeds as follows: 1. Identify the flip indices of all Hamiltonian terms and group terms by flip indices. 2. Construct a representative generator using flip indices from each candidate DIS group @@ -47,43 +47,36 @@ def construct_dis(pure_var_params, qubit_ham, qcc_deriv_thresh, verbose=False): odd number of Y operators. Args: - pure_var_params (numpy array of float): A purified QMF variational parameter set. qubit_ham (QubitOperator): A qubit Hamiltonian. - qcc_deriv_thresh (float): Threshold value of |dEQCC/dtau| so that if |dEQCC/dtau| >= - qcc_deriv_thresh for a generator, add its candidate group to the DIS. - verbose (bool): Flag for QCC verbosity. + pure_var_params (numpy array of float): A purified QMF variational parameter set. + deqcc_dtau_thresh (float): Threshold for |dEQCC/dtau| so that a candidate group is added + to the DIS if |dEQCC/dtau| >= deqcc_dtau_thresh for a generator. Returns: list of list: the DIS of QCC generators. """ # Use a qubit Hamiltonian and purified QMF parameter set to construct the DIS - dis, dis_groups = [], get_dis_groups(pure_var_params, qubit_ham, qcc_deriv_thresh) + dis, dis_groups = [], get_dis_groups(qubit_ham, pure_var_params, deqcc_dtau_thresh) if dis_groups: - if verbose: - print(f"The DIS contains {len(dis_groups)} unique generator group(s).\n") - for i, dis_group in enumerate(dis_groups): + for dis_group in dis_groups: dis_group_idxs = [int(idxs) for idxs in dis_group[0].split(" ")] dis_group_gens = get_gens_from_idxs(dis_group_idxs) dis.append(dis_group_gens) - if verbose: - print(f"DIS group {i} | group size = {len(dis_group_gens)} | " - f"flip indices = {dis_group_idxs} | |dEQCC/dtau| = " - f"{abs(dis_group[1])} a.u.\n") else: raise ValueError(f"The DIS is empty: there are no candidate DIS groups where " - f"|dEQCC/dtau| >= {qcc_deriv_thresh} a.u. Terminate the QCC simulation.\n") + f"|dEQCC/dtau| >= {deqcc_dtau_thresh} a.u. Terminate simulation.\n") return dis -def get_dis_groups(pure_var_params, qubit_ham, qcc_deriv_thresh): +def get_dis_groups(qubit_ham, pure_var_params, deqcc_dtau_thresh): """Construct unique DIS groups characterized by the flip indices and |dEQCC/dtau|. Args: - pure_var_params (numpy array of float): A purified QMF variational parameter set. qubit_ham (QubitOperator): A qubit Hamiltonian. - qcc_deriv_thresh (float): Threshold value of |dEQCC/dtau| so that if |dEQCC/dtau| >= - qcc_deriv_thresh for a generator, add its candidate group to the DIS. + pure_var_params (numpy array of float): A purified QMF variational parameter set. + deqcc_dtau_thresh (float): Threshold for |dEQCC/dtau| so that a candidate group is added + to the DIS if |dEQCC/dtau| >= deqcc_dtau_thresh for a generator. Returns: list of tuple: the DIS group flip indices (str) and signed value of dEQCC/dtau (float). @@ -94,7 +87,7 @@ def get_dis_groups(pure_var_params, qubit_ham, qcc_deriv_thresh): for qham_items in qubit_ham.terms.items()) flip_idxs = list(filter(None, (get_idxs_deriv(q_gen[0], *q_gen[1]) for q_gen in qham_gen))) - # Group Hamiltonian terms with the same flip indices and sum signed dEQCC/tau values + # Group Hamiltonian terms with the same flip indices and sum of the signed dEQCC/tau values candidates = dict() for idxs in flip_idxs: deriv_old = candidates.get(idxs[0], 0.) @@ -102,7 +95,7 @@ def get_dis_groups(pure_var_params, qubit_ham, qcc_deriv_thresh): # Return a sorted list of flip indices and signed dEQCC/dtau values for each DIS group dis_groups = [idxs_deriv for idxs_deriv in candidates.items() - if abs(idxs_deriv[1]) >= qcc_deriv_thresh] + if abs(idxs_deriv[1]) >= deqcc_dtau_thresh] return sorted(dis_groups, key=lambda deriv: abs(deriv[1]), reverse=True) @@ -124,18 +117,18 @@ def get_idxs_deriv(qham_term, *qham_qmf_data): """ coef, pure_params = qham_qmf_data - idxs, gen_list, idxs_deriv = "", [], None + idxs, gen_tup, idxs_deriv = "", tuple(), None for pauli_factor in qham_term: # The indices of X and Y operators are flip indices idx, pauli_op = pauli_factor if "X" in pauli_op or "Y" in pauli_op: gen = (idx, "Y") if idxs == "" else (idx, "X") idxs = idxs + f" {idx}" if idxs != "" else f"{idx}" - gen_list.append(gen) + gen_tup += (gen, ) # Generators must have at least two flip indices - if len(gen_list) > 1: + if len(gen_tup) > 1: qham_gen_comm = QubitOperator(qham_term, -1j * coef) - qham_gen_comm *= QubitOperator(tuple(gen_list), 1.) + qham_gen_comm *= QubitOperator(gen_tup, 1.) deriv = get_op_expval(qham_gen_comm, pure_params).real idxs_deriv = (idxs, deriv) return idxs_deriv diff --git a/tangelo/toolboxes/ansatz_generator/_qubit_ilc.py b/tangelo/toolboxes/ansatz_generator/_qubit_ilc.py new file mode 100644 index 000000000..d9543f561 --- /dev/null +++ b/tangelo/toolboxes/ansatz_generator/_qubit_ilc.py @@ -0,0 +1,244 @@ +# Copyright 2021 Good Chemistry Company. +# +# 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. + +"""This module implements a collection of functions related to the ILC ansatz: +1. Function to create the anti-commuting set (ACS) of generators from the QCC DIS; +2. An efficient solver that performs Gaussian elimination over GF(2); +3. Function that computes the ILC parameters via matrix diagonalization. + +Refs: + 1. R. A. Lang, I. G. Ryabinkin, and A. F. Izmaylov. + arXiv:2002.05701v1, 2020, 1–10. + 2. R. A. Lang, I. G. Ryabinkin, and A. F. Izmaylov. + J. Chem. Theory Comput. 2021, 17, 1, 66–78. + 3. Ç. K. Koç and S. N. Arachchige. + J. Parallel Distrib. Comput., 1991, 13, 118–122. +""" + +import warnings + +import scipy +import numpy as np + +from tangelo.toolboxes.operators.operators import QubitOperator +from tangelo.toolboxes.ansatz_generator._qubit_mf import get_op_expval + + +def construct_acs(dis, max_ilc_gens, n_qubits): + """Driver function for constructing the anti-commuting set of generators from + the direct interaction set (DIS) of QCC generators. + + Args: + dis (list of list): DIS of QCC generators. + max_ilc_gens (int): maximum number of ILC generators allowed in the ansatz. + n_qubits (int): number of qubits + + Returns: + list of QubitOperator: the anti-commuting set (ACS) of ILC generators + """ + + bad_sln_idxs, good_sln = [], False + while not good_sln: + gen_idxs, ilc_gens = [idx for idx in range(max_ilc_gens) if idx not in bad_sln_idxs], [] + n_gens = len(gen_idxs) + ng2, ngnq = n_gens * (n_gens + 1) // 2, n_gens * n_qubits + + # a_mat --> A and z_vec --> z in Appendix A, Refs. 1 & 2. + a_mat, z_vec, one_vec = np.zeros((ng2, ngnq)), np.zeros(ngnq), np.ones((ng2, 1)) + for idx, gen_idx in enumerate(gen_idxs): + gen = dis[gen_idx][0] + for term in gen.terms: + for paulis in term: + p_idx, pauli = paulis + if 'X' in pauli or 'Y' in pauli: + z_vec[idx * n_qubits + p_idx] = 1. + + # Form the rectangular matrix-vector product A * z (Appendix A, Refs. 1 & 2). + rowdx = 0 + for i in range(n_gens): + a_mat[rowdx, i * n_qubits:(i+1) * n_qubits] = z_vec[i * n_qubits:(i+1) * n_qubits] + rowdx += 1 + for j in range(i + 1, n_gens): + a_mat[rowdx, i * n_qubits:(i+1) * n_qubits] = z_vec[j * n_qubits:(j+1) * n_qubits] + a_mat[rowdx, j * n_qubits:(j+1) * n_qubits] = z_vec[i * n_qubits:(i+1) * n_qubits] + rowdx += 1 + + # Solve A * z = b --> here b = 1 + z_sln = gauss_elim_over_gf2(a_mat, b_vec=one_vec) + + # Check solution: odd # of Y ops, at least two flip indices, and mutually anti-commutes + for i in range(n_gens): + n_flip, n_y, gen_idx, gen_tup = 0, 0, gen_idxs[i], tuple() + for j in range(n_qubits): + gen, idx = None, i * n_qubits + j + if z_vec[idx] == 1.: + n_flip += 1 + if z_sln[idx] == 0.: + gen = (j, 'X') + else: + gen = (j, 'Y') + n_y += 1 + else: + if z_sln[idx] == 1.: + gen = (j, 'Z') + if gen: + gen_tup += (gen, ) + # check number of flip indices and number of Y Pauli ops + if n_flip > 1 and n_y % 2 == 1: + gen_i = QubitOperator(gen_tup, 1.) + good_sln = True + # check mutual anti-commutativity of each new ILC generator with all the rest + for gen_j in ilc_gens: + if gen_i * gen_j != -1. * gen_j * gen_i: + if gen_idx not in bad_sln_idxs: + bad_sln_idxs.append(gen_idx) + good_sln = False + else: + if gen_idx not in bad_sln_idxs: + bad_sln_idxs.append(gen_idx) + good_sln = False + if good_sln: + ilc_gens.append(gen_i) + return ilc_gens + + +def gauss_elim_over_gf2(a_mat, b_vec=None): + """Driver function that performs Gaussian elimination to solve A * z = b + over the binary field where b is the known solution vector. This routine + was adapted based on Ref. 3. All elements of a_mat and b_vec are assumed + to be the integers 0 or 1. + + Args: + a_mat (numpy array of int): rectangular matrix of dimension n x m that + holds the action of A * z, where z is a column vector of dimension m x 1. + No default. + b_vec (numpy array of int): column vector of dimension n x 1 holding the + initial solution of A * z. Default, np.zeros((n, 1)). + + Returns: + numpy array of float: solution for the z vector of dimension (n, ) + """ + + n_rows, n_cols = np.shape(a_mat) + z_vals, z_sln, piv_idx = [], [-1] * n_cols, 0 + # check that b_vec was properly supplied; ortherwise initialize as a vector of zeros + if not isinstance(b_vec, np.ndarray): + b_vec = np.zeros((n_rows, 1)) + a_mat = np.append(a_mat, b_vec, axis=1) + n_cols += 1 + for i in range(n_cols): + a_mat_max, max_idx = 0., piv_idx + # locate the pivot index by searching each row for a non-zero value. + for j in range(piv_idx, n_rows): + # if a pivot index is found, set the value to the col index for the row in which it was found + if a_mat[j, i] > a_mat_max: + max_idx = j + a_mat_max = a_mat[j, i] + # if a pivot index is not found in a given row, reset a_mat_max to -1 and move to the next row + elif j == n_rows-1 and a_mat_max == 0.: + piv_idx = max_idx + a_mat_max = -1. + # update the matrix by flipping the row and columns to achieve row echelon form + if a_mat_max > 0.: + if max_idx > piv_idx: + a_mat[[piv_idx, max_idx]] = a_mat[[max_idx, piv_idx]] + for j in range(piv_idx + 1, n_rows): + if a_mat[j, i] == 1.: + a_mat[j, i:n_cols] = np.fmod(a_mat[j, i:n_cols] + a_mat[piv_idx, i:n_cols], 2) + piv_idx += 1 + # extract the solution from the bottom to the top since it is now in row echelon form + b_vec = a_mat[0:n_rows, n_cols-1].tolist() + for i in range(n_rows - 1, -1, -1): + col_idx, z_free = -1., [] + for j in range(n_cols-1): + if a_mat[i, j] == 1.: + if col_idx == -1: + col_idx = j + else: + z_free.append(j) + if col_idx >= 0.: + z_vals.append([col_idx, z_free, b_vec[i]]) + # check for free solutions -- select 0 for the free solution + # for the ILC generator screening procedure, 0 leads to an I op and 1 leads to a Z Pauli op + for z_val in (z_vals): + b_val = z_val[2] + for z_free in (z_val[1]): + if z_sln[z_free] == -1: + z_sln[z_free] = 0. + b_val = np.fmod(b_val + z_sln[z_free], 2) + z_sln[z_val[0]] = b_val + # check that z_sln does not have any -1 values left -- if so, a solution was not found. + for z_val in z_sln: + if z_val == -1: + warnings.warn("Gaussian elimination over GF(2) failed to find a solution.", RuntimeWarning) + return np.array(z_sln) + + +def get_ilc_params_by_diag(qubit_ham, ilc_gens, qmf_var_params): + """Driver function that solves the generalized eigenvalue problem Hc = ESc required + to obtain the ground state coefficients (ILC parameters). These are subsequently recast + according to Appendix C of Ref. 1 in a form that is suitable for constructing ILC circuits. + + Args: + qubit_ham (QubitOperator): the qubit Hamiltonian of the system. + ilc_gens (list of QubitOperator): the anti-commuting set of ILC Pauli words. + + Returns: + list of float: the ILC parameters corresponding to the ACS of ILC generators + """ + + # Add the identity operator to the local copy of the ACS + ilc_gens.insert(0, QubitOperator.identity()) + n_var_params = len(ilc_gens) + qubit_ham_mat = np.zeros((n_var_params, n_var_params), dtype=complex) + qubit_overlap_mat = np.zeros((n_var_params, n_var_params), dtype=complex) + + # Construct the lower triangular matrices for the qubit Hamiltonian and overlap integrals + for i in range(n_var_params): + # H T_i|QMF> = H |psi_i> + h_psi_i = qubit_ham * ilc_gens[i] + + # = = H_ii + qubit_ham_mat[i, i] = get_op_expval(ilc_gens[i] * h_psi_i, qmf_var_params) + + # = = 1 + qubit_overlap_mat[i, i] = 1. + 0j + + for j in range(i + 1, n_var_params): + # = = H_ji + qubit_ham_mat[j, i] = get_op_expval(ilc_gens[j] * h_psi_i, qmf_var_params) + + # = --> exactly zero only for pure QMF states + qubit_overlap_mat[j, i] = get_op_expval(ilc_gens[j] * ilc_gens[i], qmf_var_params) + if i == 0: + qubit_ham_mat[j, i] *= 1j + qubit_overlap_mat[j, i] *= 1j + + # Solve the generalized eigenvalue problem + _, subspace_coefs = scipy.linalg.eigh(a=qubit_ham_mat, b=qubit_overlap_mat, lower=True, driver="gvd") + + # Compute the ILC parameters using the ground state coefficients + gs_coefs = subspace_coefs[:, 0] + if gs_coefs[0].real > 0.: + gs_coefs *= -1. + denom_sum, ilc_var_params = 0., [] + for i in range(2): + denom_sum += pow(gs_coefs[i].real, 2.) + pow(gs_coefs[i].imag, 2.) + beta_1 = np.arcsin(gs_coefs[1] / np.sqrt(denom_sum)) + ilc_var_params.append(beta_1.real) + for i in range(2, n_var_params): + denom_sum += pow(gs_coefs[i].real, 2.) + pow(gs_coefs[i].imag, 2.) + beta = np.arcsin(gs_coefs[i] / np.sqrt(denom_sum)) + ilc_var_params.append(beta.real) + return ilc_var_params diff --git a/tangelo/toolboxes/ansatz_generator/_qubit_mf.py b/tangelo/toolboxes/ansatz_generator/_qubit_mf.py index c9065df67..9219f3005 100644 --- a/tangelo/toolboxes/ansatz_generator/_qubit_mf.py +++ b/tangelo/toolboxes/ansatz_generator/_qubit_mf.py @@ -13,12 +13,17 @@ # limitations under the License. """This module implements a collection of functions related to the QMF -ansatz: (1) analytically evaluate an expectation value of a QubitOperator -using a QMF wave function; (2) initialize the QMF variational parameter set -{Omega} from a Hartree-Fock reference state; (3) purify {Omega} when building -and screening the DIS of QCC generators; (4) build a QMF state preparation -circuit using {Omega}; (5) create penalty terms for N, S^2, and Sz to penalize -a mean-field Hamiltonian. For more information, see references below. +ansatz: + 1. Analytical evaluation of an expectation value of a QubitOperator + using a QMF wave function; + 2. Initialization of the QMF variational parameter set {Omega} from a + Hartree-Fock reference state; + 3. Purification {Omega} when building and screening the DIS of QCC generators; + 4. Construction of a QMF state circuit using {Omega}; + 5. Addition of terms for N, S^2, and Sz that penalize a mean-field Hamiltonian + in order to obtain solutions corresponding to specific electron number + and spin symmetries. +For more information, see references below. Refs: 1. I. G. Ryabinkin and S. N. Genin. @@ -34,8 +39,8 @@ from tangelo.linq import Circuit, Gate from tangelo.toolboxes.operators.operators import FermionOperator from tangelo.toolboxes.qubit_mappings.statevector_mapping import get_vector -from .penalty_terms import combined_penalty, number_operator_penalty, spin2_operator_penalty,\ - spin_operator_penalty +from tangelo.toolboxes.ansatz_generator.penalty_terms import combined_penalty, number_operator_penalty,\ + spin2_operator_penalty, spin_operator_penalty def get_op_expval(qubit_op, qmf_var_params): @@ -44,7 +49,7 @@ def get_op_expval(qubit_op, qmf_var_params): Args: qubit_op (QubitOperator): A qubit operator to compute the expectation value of. - qmf_var_params (numpy array of float): The QMF variational parameter set. + qmf_var_params (numpy array of float): QMF variational parameter set. Returns: complex: expectation value of all qubit operator terms. @@ -105,8 +110,7 @@ def init_qmf_from_hf(n_spinorbitals, n_electrons, mapping, up_then_down=False, s return np.concatenate((np.pi * thetas, np.zeros((len(thetas),), dtype=float))) -def purify_qmf_state(qmf_var_params, n_spinorbitals, n_electrons, mapping, up_then_down=False, - spin=None, verbose=False): +def purify_qmf_state(qmf_var_params, n_spinorbitals, n_electrons, mapping, up_then_down=False, spin=None): """The efficient construction and screening of the DIS requires a z-collinear QMF state. If the QMF state specified by qmf_var_params is not z-collinear, this function adjusts the parameters to the nearest z-collinear computational basis state. @@ -119,7 +123,6 @@ def purify_qmf_state(qmf_var_params, n_spinorbitals, n_electrons, mapping, up_th up_then_down (bool): Change basis ordering putting all spin-up orbitals first, followed by all spin-down. spin (int): 2*S = n_alpha - n_beta. - verbose (bool): Flag for QMF verbosity. Returns: numpy array of float: purified QMF parameter set that corresponds to the @@ -137,8 +140,6 @@ def purify_qmf_state(qmf_var_params, n_spinorbitals, n_electrons, mapping, up_th else: vector = get_vector(n_spinorbitals, n_electrons, mapping, up_then_down, spin) pure_var_params[i] = np.pi * vector[i] - if verbose: - print(f"Purified QMF_{i} Bloch angles: (theta, phi) = ({pure_var_params[i]}, {pure_var_params[i + n_qubits]})\n") return pure_var_params @@ -148,8 +149,8 @@ def get_qmf_circuit(qmf_var_params, variational=True): and the second n_qubit elements in {Omega} are parameters for RZ gates. Args: - qmf_var_params (numpy array of float): The QMF variational parameter set. - variational (bool): Flag to treat {Omega} variationally or not. + qmf_var_params (numpy array of float): QMF variational parameter set. + variational (bool): Flag to treat {Omega} variationally or keep them fixed. Returns: Circuit: instance of tangelo.linq Circuit class. diff --git a/tangelo/toolboxes/ansatz_generator/ilc.py b/tangelo/toolboxes/ansatz_generator/ilc.py new file mode 100755 index 000000000..117e2b742 --- /dev/null +++ b/tangelo/toolboxes/ansatz_generator/ilc.py @@ -0,0 +1,264 @@ +# Copyright 2021 Good Chemistry Company. +# +# 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. + +"""This module defines the qubit coupled cluster ansatz class with involutory +linear combinations (ILC) of anti-commuting sets (ACS) of Pauli words +(generators). Relative to the direct interation set (DIS) of QCC generators, +which incur an exponential growth of Hamiltonian terms upon dressing, the ACS +of ILC generators enables Hamiltonian dressing such that the number of terms +grows quadratically and exact quadratic truncation of the Baker-Campbell-Hausdorff +expansion. For more information about this ansatz, see references below. + +Refs: + 1. R. A. Lang, I. G. Ryabinkin, and A. F. Izmaylov. + arXiv:2002.05701v1, 2020, 1–10. + 2. R. A. Lang, I. G. Ryabinkin, and A. F. Izmaylov. + J. Chem. Theory Comput. 2021, 17, 1, 66–78. +""" + +import warnings + +import numpy as np + +from tangelo.toolboxes.qubit_mappings.mapping_transform import get_qubit_number,\ + fermion_to_qubit_mapping +from tangelo.linq import Circuit +from tangelo.toolboxes.ansatz_generator.ansatz import Ansatz +from tangelo.toolboxes.ansatz_generator.ansatz_utils import exp_pauliword_to_gates +from tangelo.toolboxes.ansatz_generator._qubit_mf import init_qmf_from_hf, get_qmf_circuit, purify_qmf_state +from tangelo.toolboxes.ansatz_generator._qubit_cc import construct_dis +from tangelo.toolboxes.ansatz_generator._qubit_ilc import construct_acs, get_ilc_params_by_diag + + +class ILC(Ansatz): + """This class implements the ILC ansatz. Closed-shell and restricted open-shell ILC are + supported. While the form of the ILC ansatz is the same for either variation, the underlying + fermionic mean-field state is treated differently depending on the spin. Closed-shell + or restricted open-shell ILC implies that spin = 0 or spin != 0 and the fermionic mean-field + state is obtained using a RHF or ROHF Hamiltonian, respectively. + + Args: + molecule (SecondQuantizedMolecule): The molecular system. + mapping (str): One of the supported mapping identifiers. Default, "jw". + up_then_down (bool): Change basis ordering putting all spin-up orbitals first, + followed by all spin-down. Default, False. + ilc_op_list (list of QubitOperator): Generator list for the ILC ansatz. Default, None. + qmf_circuit (Circuit): An instance of tangelo.linq Circuit class implementing a QMF state + circuit. If passed from the QMF ansatz class, parameters are variational. + If None, one is created with QMF parameters that are not variational. Default, None. + qmf_var_params (list or numpy array of float): QMF variational parameter set. + If None, the values are determined using a Hartree-Fock reference state. Default, None. + qubit_ham (QubitOperator): Pass a qubit Hamiltonian to the ansatz class and ignore + the fermionic Hamiltonian in molecule. Default, None. + deilc_dtau_thresh (float): Threshold for |dEILC/dtau| so that a candidate group is added + to the DIS if |dEILC/dtau| >= deilc_dtau_thresh for a generator. Default, 1.e-3 a.u. + ilc_tau_guess (float): The initial guess for all ILC variational parameters. + Default, 1.e-2 a.u. + max_ilc_gens (int or None): Maximum number of generators allowed in the ansatz. If None, + one generator from each DIS group is selected. If int, then min(|DIS|, max_ilc_gens) + generators are selected in order of decreasing |dEILC/dtau|. Default, None. + n_trotter (int): Number of Trotterization steps used to create the ILC ansatz circuit. + Default, 1. + """ + + def __init__(self, molecule, mapping="jw", up_then_down=False, ilc_op_list=None, + qmf_circuit=None, qmf_var_params=None, qubit_ham=None, ilc_tau_guess=1.e-2, + deilc_dtau_thresh=1.e-3, max_ilc_gens=None, n_trotter=1): + + self.molecule = molecule + self.n_spinorbitals = self.molecule.n_active_sos + if self.n_spinorbitals % 2 != 0: + raise ValueError("The total number of spin-orbitals should be even.") + + self.spin = molecule.spin + self.fermi_ham = self.molecule.fermionic_hamiltonian + self.n_electrons = self.molecule.n_electrons + self.mapping = mapping + self.n_qubits = get_qubit_number(self.mapping, self.n_spinorbitals) + self.up_then_down = up_then_down + if self.mapping.lower() == "jw" and not self.up_then_down: + warnings.warn("Efficient generator screening for the ILC ansatz requires spin-orbital " + "ordering to be all spin-up first followed by all spin-down for the JW " + "mapping.", RuntimeWarning) + self.up_then_down = True + + self.ilc_op_list = ilc_op_list + self.ilc_tau_guess = ilc_tau_guess + self.deilc_dtau_thresh = deilc_dtau_thresh + self.max_ilc_gens = max_ilc_gens + self.qmf_var_params = qmf_var_params + self.qmf_circuit = qmf_circuit + self.n_trotter = n_trotter + + if qubit_ham is None: + self.qubit_ham = fermion_to_qubit_mapping(self.fermi_ham, self.mapping, + self.n_spinorbitals, self.n_electrons, + self.up_then_down, self.spin) + else: + self.qubit_ham = qubit_ham + + if self.qmf_var_params is None: + self.qmf_var_params = init_qmf_from_hf(self.n_spinorbitals, self.n_electrons, + self.mapping, self.up_then_down, self.spin) + elif isinstance(self.qmf_var_params, list): + self.qmf_var_params = np.array(self.qmf_var_params) + if self.qmf_var_params.size != 2 * self.n_qubits: + raise ValueError("The number of QMF variational parameters must be 2 * n_qubits.") + + # Get purified QMF parameters and build the DIS & ACS or use a list of generators. + if self.ilc_op_list is None: + pure_var_params = purify_qmf_state(self.qmf_var_params, self.n_spinorbitals, + self.n_electrons, self.mapping, self.up_then_down, self.spin) + self.dis = construct_dis(self.qubit_ham, pure_var_params, self.deilc_dtau_thresh) + self.max_ilc_gens = len(self.dis) if self.max_ilc_gens is None\ + else min(len(self.dis), self.max_ilc_gens) + self.acs = construct_acs(self.dis, self.max_ilc_gens, self.n_qubits) + self.n_var_params = len(self.acs) + else: + self.dis = None + self.acs = self.ilc_op_list + self.n_var_params = len(self.ilc_op_list) + + # Supported reference state initialization + self.supported_reference_state = {"HF"} + # Supported var param initialization + self.supported_initial_var_params = {"qmf_state", "ilc_tau_guess", "random", "diag"} + + # Default starting parameters for initialization + self.default_reference_state = "HF" + self.var_params_default = "ilc_tau_guess" + self.var_params = None + self.rebuild_dis = False + self.rebuild_acs = False + self.ilc_circuit = None + self.circuit = None + + def set_var_params(self, var_params=None): + """Set values for variational parameters, such as zeros or floats, + providing some keywords for users, and also supporting direct user input + (list or numpy array). Return the parameters so that workflows such as VQE can + retrieve these values. """ + + if var_params is None: + var_params = self.var_params_default + + if isinstance(var_params, str): + var_params = var_params.lower() + if var_params not in self.supported_initial_var_params: + raise ValueError(f"Supported keywords for initializing variational parameters: " + f"{self.supported_initial_var_params}") + # Initialize the ILC wave function as |ILC> = |QMF> + if var_params == "qmf_state": + initial_var_params = np.zeros((self.n_var_params,), dtype=float) + # Initialize all ILC parameters to the same value specified by self.ilc_tau_guess + elif var_params == "ilc_tau_guess": + initial_var_params = self.ilc_tau_guess * np.ones((self.n_var_params,)) + # Initialize tau parameters randomly over the domain [-ilc_tau_guess, ilc_tau_guess] + elif var_params == "random": + initial_var_params = 2. * self.ilc_tau_guess * np.random.random((self.n_var_params,)) - self.ilc_tau_guess + # Initialize ILC parameters by matrix diagonalization (see Appendix B, Refs. 1 & 2). + elif var_params == "diag": + initial_var_params = get_ilc_params_by_diag(self.qubit_ham, self.acs, self.qmf_var_params) + else: + initial_var_params = np.array(var_params) + if initial_var_params.size != self.n_var_params: + raise ValueError(f"Expected {self.n_var_params} variational parameters but " + f"received {initial_var_params.size}.") + self.var_params = initial_var_params + return initial_var_params + + def prepare_reference_state(self): + """Returns circuit preparing the reference state of the ansatz (e.g prepare reference + wavefunction with HF, multi-reference state, etc). These preparations must be consistent + with the transform used to obtain the operator. """ + + if self.default_reference_state not in self.supported_reference_state: + raise ValueError(f"Only supported reference state methods are: " + f"{self.supported_reference_state}.") + if self.default_reference_state == "HF": + reference_state_circuit = get_qmf_circuit(self.qmf_var_params, False) + return reference_state_circuit + + def build_circuit(self, var_params=None): + """Build and return the quantum circuit implementing the state preparation ansatz + (with currently specified initial_state and var_params). """ + + if var_params is not None: + self.set_var_params(var_params) + elif self.var_params is None: + self.set_var_params() + + # Build a QMF state preparation circuit + if self.qmf_circuit is None: + self.qmf_circuit = self.prepare_reference_state() + + # Build create the list of ILC qubit operators + self.ilc_op_list = self._get_ilc_op() + + # Obtain quantum circuit through trotterization of the list of ILC operators + pauli_word_gates = [] + for _ in range(self.n_trotter): + for ilc_op in self.ilc_op_list: + pauli_word, coef = list(ilc_op.terms.items())[0] + pauli_word_gates += exp_pauliword_to_gates(pauli_word, float(coef/self.n_trotter), variational=True) + self.ilc_circuit = Circuit(pauli_word_gates) + self.circuit = self.qmf_circuit + self.ilc_circuit if self.qmf_circuit.size != 0\ + else self.ilc_circuit + + def update_var_params(self, var_params): + """Shortcut: set value of variational parameters in the existing ansatz circuit member. + Preferable to rebuilding your circuit from scratch, which can be an involved process. + """ + + # Update the ILC variational parameters + self.set_var_params(var_params) + + # Build the ILC ansatz operator + self.ilc_op_list = self._get_ilc_op() + + pauli_word_gates = [] + for _ in range(self.n_trotter): + for ilc_op in self.ilc_op_list: + pauli_word, coef = list(ilc_op.terms.items())[0] + pauli_word_gates += exp_pauliword_to_gates(pauli_word, float(coef/self.n_trotter), variational=True) + self.ilc_circuit = Circuit(pauli_word_gates) + self.circuit = self.qmf_circuit + self.ilc_circuit if self.qmf_circuit.size != 0\ + else self.ilc_circuit + + def _get_ilc_op(self): + """Returns the ILC operators ordered according to the argument of + Eq. C1, Appendix C, Ref. 1. + + Returns: + list of QubitOperator: the list of ILC qubit operators + """ + + # Rebuild DIS & ACS in case qubit_ham changed or they and qubit_op_list don't exist + if self.rebuild_dis or self.rebuild_acs or not self.acs: + pure_var_params = purify_qmf_state(self.qmf_var_params, self.n_spinorbitals, + self.n_electrons, self.mapping, self.up_then_down, self.spin) + self.dis = construct_dis(self.qubit_ham, pure_var_params, self.deilc_dtau_thresh) + self.max_ilc_gens = len(self.dis) if self.max_ilc_gens is None\ + else min(len(self.dis), self.max_ilc_gens) + self.acs = construct_acs(self.dis, self.max_ilc_gens, self.n_qubits) + self.ilc_op_list = None + + # Build the ILC qubit operator list + ilc_op_list = [] + for i in range(self.n_var_params - 1, 0, -1): + ilc_op_list.append(-0.5 * self.var_params[i] * self.acs[i]) + ilc_op_list.append(-self.var_params[0] * self.acs[0]) + for i in range(1, self.n_var_params): + ilc_op_list.append(-0.5 * self.var_params[i] * self.acs[i]) + return ilc_op_list diff --git a/tangelo/toolboxes/ansatz_generator/qcc.py b/tangelo/toolboxes/ansatz_generator/qcc.py index 737da5975..12c15fc72 100755 --- a/tangelo/toolboxes/ansatz_generator/qcc.py +++ b/tangelo/toolboxes/ansatz_generator/qcc.py @@ -34,16 +34,17 @@ """ import warnings + import numpy as np from tangelo.toolboxes.operators.operators import QubitOperator from tangelo.toolboxes.qubit_mappings.mapping_transform import get_qubit_number,\ fermion_to_qubit_mapping from tangelo.linq import Circuit -from .ansatz import Ansatz -from .ansatz_utils import exp_pauliword_to_gates -from ._qubit_mf import init_qmf_from_hf, get_qmf_circuit, purify_qmf_state -from ._qubit_cc import construct_dis +from tangelo.toolboxes.ansatz_generator.ansatz import Ansatz +from tangelo.toolboxes.ansatz_generator.ansatz_utils import exp_pauliword_to_gates +from tangelo.toolboxes.ansatz_generator._qubit_mf import init_qmf_from_hf, get_qmf_circuit, purify_qmf_state +from tangelo.toolboxes.ansatz_generator._qubit_cc import construct_dis class QCC(Ansatz): @@ -55,62 +56,66 @@ class QCC(Ansatz): Args: molecule (SecondQuantizedMolecule): The molecular system. - mapping (str): One of the supported qubit mapping identifiers. Default, "JW". - up_then_down (bool): Change basis ordering putting all spin up orbitals first, - followed by all spin down. Default, False. - qubit_op_list (list of QubitOperator): A list of QCC generators to use for the ansatz. - Default, None. + mapping (str): One of the supported qubit mapping identifiers. Default, "jw". + up_then_down (bool): Change basis ordering putting all spin-up orbitals first, + followed by all spin-down. Default, False. + qcc_op_list (list of QubitOperator): Generator list for the QCC ansatz. Default, None. qmf_circuit (Circuit): An instance of tangelo.linq Circuit class implementing a QMF state - preparation circuit. A variational circuit can be passed from the QMF ansatz class. - Otherwise a non-variational circuit is created by default. Default, None. - qmf_var_params (list or numpy array of float): The QMF variational parameter set. + circuit. If passed from the QMF ansatz class, parameters are variational. + If None, one is created with QMF parameters that are not variational. Default, None. + qmf_var_params (list or numpy array of float): QMF variational parameter set. If None, the values are determined using a Hartree-Fock reference state. Default, None. - qubit_mf_ham (QubitOperator): Allows a qubit Hamiltonian to be passed to the QCC ansatz - class. If not None, then the fermionic Hamiltonian is ignored. Default, None. - qcc_guess (float): Sets the initial guess for all amplitudes in the QCC variational - parameter set. Default, 1.e-1 a.u. - qcc_deriv_thresh (float): Threshold value of |dEQCC/dtau| so that if |dEQCC/dtau| >= - qcc_deriv_thresh for a generator, add its candidate group to the DIS. - max_qcc_gens (int or None): Maximum number of generators allowed for the ansatz. - If None, one generator from each DIS group is used. If set to an int, then - min(size(DIS), max_qcc_gens) generators are used for the ansatz. Default, None. - verbose (bool): Flag for QCC verbosity. Default, False. + qubit_ham (QubitOperator): Pass a qubit Hamiltonian to the QCC ansatz class and ignore + the fermionic Hamiltonian in molecule. Default, None. + deqcc_dtau_thresh (float): Threshold for |dEQCC/dtau| so that a candidate group is added + to the DIS if |dEQCC/dtau| >= deqcc_dtau_thresh for a generator. Default, 1.e-3 a.u. + qcc_tau_guess (float): The initial guess for all QCC variational parameters. + Default, 1.e-2 a.u. + max_qcc_gens (int or None): Maximum number of generators allowed in the ansatz. If None, + one generator from each DIS group is selected. If int, then min(|DIS|, max_qcc_gens) + generators are selected in order of decreasing |dEQCC/dtau|. Default, None. """ - def __init__(self, molecule, mapping="JW", up_then_down=False, qubit_op_list=None, - qmf_circuit=None, qmf_var_params=None, qubit_mf_ham=None, qcc_guess=1.e-1, - qcc_deriv_thresh=1.e-3, max_qcc_gens=None, verbose=False): + def __init__(self, molecule, mapping="jw", up_then_down=False, qcc_op_list=None, + qmf_circuit=None, qmf_var_params=None, qubit_ham=None, qcc_tau_guess=1.e-2, + deqcc_dtau_thresh=1.e-3, max_qcc_gens=None): self.molecule = molecule self.n_spinorbitals = self.molecule.n_active_sos if self.n_spinorbitals % 2 != 0: raise ValueError("The total number of spin-orbitals should be even.") - self.n_electrons = self.molecule.n_active_electrons self.spin = molecule.spin + self.fermi_ham = self.molecule.fermionic_hamiltonian + self.n_electrons = self.molecule.n_electrons self.mapping = mapping self.n_qubits = get_qubit_number(self.mapping, self.n_spinorbitals) self.up_then_down = up_then_down - if self.mapping.upper() == "JW" and not self.up_then_down: - warnings.warn("The QCC ansatz requires spin-orbital ordering to be all spin-up " - "first followed by all spin-down for the JW mapping.", RuntimeWarning) + if self.mapping.lower() == "jw" and not self.up_then_down: + warnings.warn("Efficient generator screening for the QCC ansatz requires spin-orbital " + "ordering to be all spin-up first followed by all spin-down for the JW " + "mapping.", RuntimeWarning) self.up_then_down = True - self.qcc_guess = qcc_guess - self.qcc_deriv_thresh = qcc_deriv_thresh + self.molecule = molecule + self.n_spinorbitals = self.molecule.n_active_sos + if self.n_spinorbitals % 2 != 0: + raise ValueError("The total number of spin-orbitals should be even.") + + self.qcc_tau_guess = qcc_tau_guess + self.deqcc_dtau_thresh = deqcc_dtau_thresh self.max_qcc_gens = max_qcc_gens - self.qubit_op_list = qubit_op_list + self.qcc_op_list = qcc_op_list self.qmf_var_params = qmf_var_params self.qmf_circuit = qmf_circuit - self.verbose = verbose - if qubit_mf_ham is None: + if qubit_ham is None: self.fermi_ham = self.molecule.fermionic_hamiltonian self.qubit_ham = fermion_to_qubit_mapping(self.fermi_ham, self.mapping, self.n_spinorbitals, self.n_electrons, self.up_then_down, self.spin) else: - self.qubit_ham = qubit_mf_ham + self.qubit_ham = qubit_ham if self.qmf_var_params is None: self.qmf_var_params = init_qmf_from_hf(self.n_spinorbitals, self.n_electrons, @@ -121,27 +126,25 @@ def __init__(self, molecule, mapping="JW", up_then_down=False, qubit_op_list=Non raise ValueError("The number of QMF variational parameters must be 2 * n_qubits.") # Get purified QMF parameters and use them to build the DIS or use a list of generators. - if self.qubit_op_list is None: - pure_var_params = purify_qmf_state(self.qmf_var_params, self.n_spinorbitals, - self.n_electrons, self.mapping, self.up_then_down, - self.spin, self.verbose) - self.dis = construct_dis(pure_var_params, self.qubit_ham, self.qcc_deriv_thresh, - self.verbose) + if self.qcc_op_list is None: + pure_var_params = purify_qmf_state(self.qmf_var_params, self.n_spinorbitals, self.n_electrons, + self.mapping, self.up_then_down, self.spin) + self.dis = construct_dis(self.qubit_ham, pure_var_params, self.deqcc_dtau_thresh) self.n_var_params = len(self.dis) if self.max_qcc_gens is None\ else min(len(self.dis), self.max_qcc_gens) else: self.dis = None - self.n_var_params = len(self.qubit_op_list) + self.n_var_params = len(self.qcc_op_list) # Supported reference state initialization self.supported_reference_state = {"HF"} # Supported var param initialization - self.supported_initial_var_params = {"zeros", "qcc_guess"} + self.supported_initial_var_params = {"qmf_state", "qcc_tau_guess", "random"} # Default starting parameters for initialization self.pauli_to_angles_mapping = {} self.default_reference_state = "HF" - self.var_params_default = "qcc_guess" + self.var_params_default = "qcc_tau_guess" self.var_params = None self.rebuild_dis = False self.qcc_circuit = None @@ -162,11 +165,14 @@ def set_var_params(self, var_params=None): raise ValueError(f"Supported keywords for initializing variational parameters: " f"{self.supported_initial_var_params}") # Initialize the QCC wave function as |QCC> = |QMF> - if var_params == "zeros": + if var_params == "qmf_state": initial_var_params = np.zeros((self.n_var_params,), dtype=float) - # Initialize all tau parameters to the same value specified by self.qcc_guess - elif var_params == "qcc_guess": - initial_var_params = self.qcc_guess * np.ones((self.n_var_params,)) + # Initialize all tau parameters to the same value specified by self.qcc_tau_guess + elif var_params == "qcc_tau_guess": + initial_var_params = self.qcc_tau_guess * np.ones((self.n_var_params,)) + # Initialize tau parameters randomly over the domain [-qcc_tau_guess, qcc_tau_guess] + elif var_params == "random": + initial_var_params = 2. * self.qcc_tau_guess * np.random.random((self.n_var_params,)) - self.qcc_tau_guess else: initial_var_params = np.array(var_params) if initial_var_params.size != self.n_var_params: @@ -204,9 +210,9 @@ def build_circuit(self, var_params=None): self.qmf_circuit = self.prepare_reference_state() # Obtain quantum circuit through trivial trotterization of the qubit operator - # Keep track of the order in which pauli words have been visited for fast parameter updates - pauli_words = sorted(qubit_op.terms.items(), key=lambda x: len(x[0])) + # Track the order in which pauli words have been visited for fast parameter updates pauli_words_gates = [] + pauli_words = sorted(qubit_op.terms.items(), key=lambda x: len(x[0])) for i, (pauli_word, coef) in enumerate(pauli_words): pauli_words_gates += exp_pauliword_to_gates(pauli_word, coef) self.pauli_to_angles_mapping[pauli_word] = i @@ -219,14 +225,16 @@ def update_var_params(self, var_params): Preferable to rebuilding your circuit from scratch, which can be an involved process. """ + # Update the QCC variational parameters self.set_var_params(var_params) - # Build the qubit operator required for QCC + # Build the QCC ansatz qubit operator qubit_op = self._get_qcc_qubit_op() - # If qubit_op terms have changed, rebuild circuit. else update variational gates directly + # If qubit_op terms have changed, rebuild circuit if set(self.pauli_to_angles_mapping.keys()) != set(qubit_op.terms.keys()): self.build_circuit(var_params) + # Otherwise update variational gates directly else: for pauli_word, coef in qubit_op.terms.items(): gate_index = self.pauli_to_angles_mapping[pauli_word] @@ -242,50 +250,33 @@ def _get_qcc_qubit_op(self): The exponentiated terms of the QCC operator, U = PROD_k exp(-0.5j * tau_k * P_k), are used to build a QCC circuit. - Args: - var_params (numpy array of float): The QCC variational parameter set. - n_var_params (int): Size of the QCC variational parameter set. - qmf_var_params (numpy array of float): The QMF variational parameter set. - qubit_ham (QubitOperator): A qubit Hamiltonian. - qcc_deriv_thresh (float): Threshold value of |dEQCC/dtau| so that if |dEQCC/dtau| >= - qcc_deriv_thresh for a generator, add its candidate group to the DIS. - dis (list of list): The DIS of QCC generators. - qubit_op_list (list of QubitOperator): A list of generators to use when building the QCC - operator instead of selecting from DIS groups. - rebuild_dis (bool): Rebuild the DIS. This is useful if qubit_ham of qmf_var_params have - changed (e.g. in iterative methods like iQCC or QCC-ILC). If True, qubit_op_list is - reset to None. - verbose (bool): Flag for QCC verbosity. Default, False. - Returns: QubitOperator: QCC ansatz qubit operator. """ - # Rebuild the DIS in case qubit_ham changed or both the DIS and qubit_op_list don't exist - if self.rebuild_dis or (self.dis is None and self.qubit_op_list is None): - pure_var_params = purify_qmf_state(self.qmf_var_params, self.n_spinorbitals, - self.n_electrons, self.mapping, self.up_then_down, - self.spin, self.verbose) - self.dis = construct_dis(pure_var_params, self.qubit_ham, self.qcc_deriv_thresh, - self.verbose) + # Rebuild DIS if qubit_ham or qmf_var_params changed or if DIS and qcc_op_list are None. + if self.rebuild_dis or (self.dis is None and self.qcc_op_list is None): + pure_var_params = purify_qmf_state(self.qmf_var_params, self.n_spinorbitals, self.n_electrons, + self.mapping, self.up_then_down, self.spin) + self.dis = construct_dis(self.qubit_ham, pure_var_params, self.deqcc_dtau_thresh) self.n_var_params = len(self.dis) if self.max_qcc_gens is None\ else min(len(self.dis), self.max_qcc_gens) - self.qubit_op_list = None + self.qcc_op_list = None # Build the QCC operator using the DIS or a list of generators qcc_qubit_op = QubitOperator.zero() - if self.qubit_op_list is None: - self.qubit_op_list = [] + if self.qcc_op_list is None: + self.qcc_op_list = [] for i in range(self.n_var_params): - # Instead of randomly choosing a generator, get the last one. - qcc_gen = self.dis[i][-1] + # Instead of randomly choosing a generator, grab the first one. + qcc_gen = self.dis[i][0] qcc_qubit_op -= 0.5 * self.var_params[i] * qcc_gen - self.qubit_op_list.append(qcc_gen) + self.qcc_op_list.append(qcc_gen) else: - if len(self.qubit_op_list) == self.n_var_params: - for i, qcc_gen in enumerate(self.qubit_op_list): + if len(self.qcc_op_list) == self.n_var_params: + for i, qcc_gen in enumerate(self.qcc_op_list): qcc_qubit_op -= 0.5 * self.var_params[i] * qcc_gen else: raise ValueError(f"Expected {self.n_var_params} generators in " - f"{self.qubit_op_list} but received {len(self.qubit_op_list)}.\n") + f"{self.qcc_op_list} but received {len(self.qcc_op_list)}.\n") return qcc_qubit_op diff --git a/tangelo/toolboxes/ansatz_generator/qmf.py b/tangelo/toolboxes/ansatz_generator/qmf.py index fc1d2583c..3729434eb 100755 --- a/tangelo/toolboxes/ansatz_generator/qmf.py +++ b/tangelo/toolboxes/ansatz_generator/qmf.py @@ -30,12 +30,14 @@ """ import warnings + import numpy as np from tangelo.toolboxes.qubit_mappings.mapping_transform import get_qubit_number,\ fermion_to_qubit_mapping -from .ansatz import Ansatz -from ._qubit_mf import get_qmf_circuit, init_qmf_from_hf, penalize_mf_ham +from tangelo.toolboxes.ansatz_generator.ansatz import Ansatz +from tangelo.toolboxes.ansatz_generator._qubit_mf import get_qmf_circuit, init_qmf_from_hf,\ + penalize_mf_ham class QMF(Ansatz): @@ -49,27 +51,27 @@ class QMF(Ansatz): especially when a random initial guess is used. It is recommended that penalty terms are added to the mean-field Hamiltonian to enforce appropriate electron number and spin angular momentum symmetries on the QMF wave function during optimization (see Ref. 4). If using - penalty terms is to be avoided, an inital guess based on a Hartree-Fock reference state will + penalty terms is to be avoided, an initial guess based on a Hartree-Fock reference state will likely converge quickly to the desired state, but this is not guaranteed. Args: molecule (SecondQuantizedMolecule): The molecular system. - mapping (str): One of the supported qubit mapping identifiers. Default, "JW". + mapping (str): One of the supported qubit mapping identifiers. Default, "jw". up_then_down (bool): Change basis ordering putting all spin up orbitals first, followed by all spin down. Default, False. - init_qmf (dict): Parameters for initializing the QMF variational parameter set and - mean-field Hamiltonian penalization. The keys are "init_params", "N", "S^2", or "Sz" - (str). The value of "init_params" must be in self.supported_initial_var_params (str). - The value of "N", "S^2", or "Sz" is (tuple or None). If a tuple, its elements are - the penalty term coefficient, mu (float), and target value of a penalty operator - (int). Example - "key": (mu, target). If "N", "S^2", or "Sz" is None, a penalty term - is added with default mu and target values: mu = 1.5 and target is derived - from molecule as = n_electrons, = spin_z * (spin_z + 1), and = spin_z, - where spin_z = spin // 2. Key, value pairs are case sensitive and mu > 0. + init_qmf (dict): Controls for QMF variational parameter initialization and mean-field + Hamiltonian penalization. Supported keys are "init_params", "N", "S^2", or "Sz" (str). + Values of "init_params" must be in self.supported_initial_var_params (str). Values of + "N", "S^2", or "Sz" are (tuple or None). If tuple, the elements are a penalty + term coefficient, mu (float), and a target value of the penalty operator (int). + Example - "key": (mu, target). If "N", "S^2", or "Sz" is None, a penalty term is added + with default mu and target values: mu = 1.5 and target is derived from molecule as + = n_electrons, = spin_z * (spin_z + 1), and = spin_z, where + spin_z = spin // 2. Key, value pairs are case sensitive and mu > 0. Default, {"init_params": "hf_state"}. """ - def __init__(self, molecule, mapping="JW", up_then_down=False, init_qmf=None): + def __init__(self, molecule=None, mapping="jw", up_then_down=False, init_qmf=None): self.molecule = molecule self.n_spinorbitals = self.molecule.n_active_sos @@ -77,8 +79,9 @@ def __init__(self, molecule, mapping="JW", up_then_down=False, init_qmf=None): raise ValueError("The total number of spin-orbitals should be even.") self.n_orbitals = self.n_spinorbitals // 2 - self.n_electrons = self.molecule.n_active_electrons self.spin = molecule.spin + self.fermi_ham = self.molecule.fermionic_hamiltonian + self.n_electrons = self.molecule.n_active_electrons self.mapping = mapping self.n_qubits = get_qubit_number(self.mapping, self.n_spinorbitals) @@ -86,13 +89,13 @@ def __init__(self, molecule, mapping="JW", up_then_down=False, init_qmf=None): self.init_qmf = {"init_params": "hf_state"} if init_qmf is None else init_qmf # Supported var param initialization - self.supported_initial_var_params = {"zeros", "half_pi", "pis", "random", "hf_state"} + self.supported_initial_var_params = {"vacuum", "half_pi", "minus_half_pi", "full_pi", + "random", "hf_state"} # Supported reference state initialization self.supported_reference_state = {"HF"} - # Get the mean-field fermionic Hamiltonian and check for penalty terms - self.fermi_ham = self.molecule.fermionic_hamiltonian + # Add any penalty terms to the fermionic Hamiltonian if isinstance(self.init_qmf, dict): if "init_params" not in self.init_qmf.keys(): raise KeyError(f"Missing key 'init_params' in {self.init_qmf}. " @@ -114,7 +117,7 @@ def __init__(self, molecule, mapping="JW", up_then_down=False, init_qmf=None): self.fermi_ham += penalize_mf_ham(self.init_qmf, self.n_orbitals) else: if self.var_params_default != "hf_state": - warnings.warn("It is recommended that the QMF parameters are intialized " + warnings.warn("It is recommended that the QMF parameters are initialized " "using a Hartree-Fock reference state if penalty terms are " "not added to the mean-field Hamiltonian.", RuntimeWarning) else: @@ -123,6 +126,7 @@ def __init__(self, molecule, mapping="JW", up_then_down=False, init_qmf=None): else: raise TypeError(f"{self.init_qmf} must be dictionary type.") + # Get the qubit Hamiltonian self.qubit_ham = fermion_to_qubit_mapping(self.fermi_ham, self.mapping, self.n_spinorbitals, self.n_electrons, self.up_then_down, self.spin) @@ -147,20 +151,23 @@ def set_var_params(self, var_params=None): raise ValueError(f"Supported keywords for initializing variational parameters: " f"{self.supported_initial_var_params}") # Initialize |QMF> as |00...0> - if var_params == "zeros": + if var_params == "vacuum": initial_var_params = np.zeros((self.n_var_params,), dtype=float) - # Initialize |QMF> as (i/sqrt(2))^n_qubits * tensor_prod(|0> + |1>) + # Initialize |QMF> as (1/sqrt(2))^n_qubits * tensor_prod(|0> + 1j|1>) elif var_params == "half_pi": initial_var_params = 0.5 * np.pi * np.ones((self.n_var_params,)) - # Initialize |QMF> as (-1)^n_qubits |11...1> state - elif var_params == "pis": + # Initialize |QMF> as (1/sqrt(2))^n_qubits * tensor_prod(|0> - 1j|1>) + elif var_params == "minus_half_pi": + initial_var_params = -0.5 * np.pi * np.ones((self.n_var_params,)) + # Initialize |QMF> as (i)^n_qubits * |11...1> + elif var_params == "full_pi": initial_var_params = np.pi * np.ones((self.n_var_params,)) - # Initialize theta and phi angles randomly over [0, pi] and [0, 2*pi], respectively + # Random initialization of thetas over [0, pi] and phis over [0, 2 * pi] elif var_params == "random": initial_thetas = np.pi * np.random.random((self.n_qubits,)) initial_phis = 2. * np.pi * np.random.random((self.n_qubits,)) initial_var_params = np.concatenate((initial_thetas, initial_phis)) - # Initialize theta angles so that |QMF> = |HF> state and set all phi angles to 0. + # Initialize thetas as 0 or pi such that |QMF> = |HF> and set all phis to 0 elif var_params == "hf_state": initial_var_params = init_qmf_from_hf(self.n_spinorbitals, self.n_electrons, self.mapping, self.up_then_down, self.spin) diff --git a/tangelo/toolboxes/ansatz_generator/tests/test_ilc.py b/tangelo/toolboxes/ansatz_generator/tests/test_ilc.py new file mode 100644 index 000000000..619ed973a --- /dev/null +++ b/tangelo/toolboxes/ansatz_generator/tests/test_ilc.py @@ -0,0 +1,161 @@ +# Copyright 2021 Good Chemistry Company. +# +# 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. + +"""Unit tests for closed-shell and restricted open-shell qubit coupled cluster +with involutory linear combinations (ILC) of anti-commuting sets (ACS) of Pauli words.""" + +import unittest + +import numpy as np + +from tangelo.linq import Simulator +from tangelo.toolboxes.ansatz_generator.ilc import ILC +from tangelo.toolboxes.ansatz_generator._qubit_ilc import gauss_elim_over_gf2 +from tangelo.toolboxes.operators.operators import QubitOperator +from tangelo.molecule_library import mol_H2_sto3g, mol_H4_cation_sto3g + +sim = Simulator() + + +class ILCTest(unittest.TestCase): + """Unit tests for various functionalities of the ILC ansatz class. Examples for both closed- + and restricted open-shell ILC are provided using H2, H4, and H4+. + """ + + @staticmethod + def test_ilc_set_var_params(): + """ Verify behavior of set_var_params for different inputs (keyword, list, numpy array). """ + + ilc_ansatz = ILC(mol_H2_sto3g, up_then_down=True) + + one_zero = np.zeros((1,), dtype=float) + + ilc_ansatz.set_var_params("qmf_state") + np.testing.assert_array_almost_equal(ilc_ansatz.var_params, one_zero, decimal=6) + + ilc_ansatz.set_var_params([0.]) + np.testing.assert_array_almost_equal(ilc_ansatz.var_params, one_zero, decimal=6) + + one_tenth = 0.1 * np.ones((1,)) + + ilc_ansatz.set_var_params([0.1]) + np.testing.assert_array_almost_equal(ilc_ansatz.var_params, one_tenth, decimal=6) + + ilc_ansatz.set_var_params(np.array([0.1])) + np.testing.assert_array_almost_equal(ilc_ansatz.var_params, one_tenth, decimal=6) + + def test_ilc_incorrect_number_var_params(self): + """ Return an error if user provide incorrect number of variational parameters """ + + ilc_ansatz = ILC(mol_H2_sto3g, up_then_down=True) + + self.assertRaises(ValueError, ilc_ansatz.set_var_params, np.array([1.] * 2)) + + @staticmethod + def test_gauss_elim_over_gf2_sqrmat(): + """ Verify behavior of the Gaussian elimination over the binary field function. """ + + # a_matrix stores the action of A * z over GF(2); dimension is n x m + a_matrix = np.array([[1, 0, 1, 1], [1, 1, 0, 1], [1, 1, 1, 0], [0, 0, 1, 0]]) + + # b_vec stores the solution vector for the equation A * z = b_vec; dimension is n x 1 + b_vec = np.array([1, 0, 1, 0]).reshape((4, 1)) + + # z_ref stores the serves as the reference for the output of gauss_elim_over_gf2 + z_ref = np.array([0, 1, 0, 1]) + + # solve A * z = b and compare to reference solution + z_sln = gauss_elim_over_gf2(a_matrix, b_vec) + + np.testing.assert_array_almost_equal(z_sln, z_ref, decimal=6) + + @staticmethod + def test_gauss_elim_over_gf2_rectmat(): + """ Verify behavior of the Gaussian elimination over the binary field function. """ + + # a_matrix stores the action of A * z over GF(2); dimension is n x m + a_matrix = np.array([[0, 0, 1, 0, 1], [1, 1, 0, 0, 0], [0, 0, 0, 1, 1]]) + + # b_vec stores the solution vector for the equation A * z = b_vec; dimension is n x 1 + b_vec = np.array([1, 1, 0]).reshape((3, 1)) + + # z_ref stores the serves as the reference for the output of gauss_elim_over_gf2 + z_ref = np.array([1, 0, 1, 0, 0]) + + # solve A * z = b and compare to reference solution + z_sln = gauss_elim_over_gf2(a_matrix, b_vec) + + np.testing.assert_array_almost_equal(z_sln, z_ref, decimal=6) + + @staticmethod + def test_gauss_elim_over_gf2_lindep(): + """ Verify behavior of the Gaussian elimination over the binary field function. """ + + # a_matrix stores the action of A * z over GF(2); dimension is n x m + a_matrix = np.array([[0, 0, 1, 0, 1], [0, 0, 1, 0, 1], [0, 0, 0, 1, 1]]) + + # b_vec stores the solution vector for the equation A * z = b_vec; dimension is n x 1 + b_vec = np.array([1, 0, 1]).reshape((3, 1)) + + # z_ref stores the serves as the reference for the output of gauss_elim_over_gf2 + z_ref = np.array([-1, -1, 1, 1, 0]) + + # solve A * z = b and compare to reference solution + z_sln = gauss_elim_over_gf2(a_matrix, b_vec) + + np.testing.assert_array_almost_equal(z_sln, z_ref, decimal=6) + + def test_ilc_h2(self): + """ Verify closed-shell functionality when using the ILC class separately for H2 """ + + # Build the ILC ansatz, which sets the QMF parameters automatically if none are passed + ilc_var_params = [0.11360304] + ilc_op_list = [QubitOperator("X0 Y1 Y2 Y3")] + ilc_ansatz = ILC(mol_H2_sto3g, up_then_down=True, ilc_op_list=ilc_op_list) + + # Build a QMF + ILC circuit + ilc_ansatz.build_circuit() + + # Get qubit hamiltonian for energy evaluation + qubit_hamiltonian = ilc_ansatz.qubit_ham + + # Assert energy returned is as expected for given parameters + ilc_ansatz.update_var_params(ilc_var_params) + energy = sim.get_expectation_value(qubit_hamiltonian, ilc_ansatz.circuit) + self.assertAlmostEqual(energy, -1.1372697, delta=1e-6) + + def test_ilc_h4_cation(self): + """ Verify restricted open-shell functionality when using the ILC class for H4+ """ + + # Build the ILC ansatz, which sets the QMF parameters automatically if none are passed + ilc_op_list = [QubitOperator("Y0 Z2 X4 Z6"), QubitOperator("Y1 Y2 Z4 X5 Y6"), QubitOperator("X0 Z2 Z4 Y6"), + QubitOperator("X1 Y2 X4 Z6"), QubitOperator("Y1 Y2 X4 Y5 Z6"), QubitOperator("Y1 Y2 Z4 Z5 Y6"), + QubitOperator("Y0 Z1 Z2 Y5 Y6"), QubitOperator("Y0 Z1 Z2 Y4 Y5 Z6")] + ilc_var_params = [ 0.14017492, -0.10792805, -0.05835484, 0.12468933, 0.07173118, 0.04683807, 0.02852163, -0.03133538] + ilc_ansatz = ILC(mol_H4_cation_sto3g, "BK", False, ilc_op_list) + + # Build a QMF + ILC circuit + ilc_ansatz.build_circuit() + + # Get qubit hamiltonian for energy evaluation + qubit_hamiltonian = ilc_ansatz.qubit_ham + + # Assert energy returned is as expected for given parameters + ilc_ansatz.update_var_params(ilc_var_params) + energy = sim.get_expectation_value(qubit_hamiltonian, ilc_ansatz.circuit) + self.assertAlmostEqual(energy, -1.6379638, delta=1e-6) + + +if __name__ == "__main__": + unittest.main() diff --git a/tangelo/toolboxes/ansatz_generator/tests/test_qcc.py b/tangelo/toolboxes/ansatz_generator/tests/test_qcc.py index 8d392aea2..602c8f8eb 100644 --- a/tangelo/toolboxes/ansatz_generator/tests/test_qcc.py +++ b/tangelo/toolboxes/ansatz_generator/tests/test_qcc.py @@ -14,9 +14,10 @@ """Unit tests for closed-shell and restricted open-shell qubit coupled cluster (QCC) ansatze. """ +import os import unittest + import numpy as np -import os from openfermion import load_operator from tangelo.linq import Simulator @@ -33,7 +34,7 @@ class QCCTest(unittest.TestCase): """Unit tests for various functionalities of the QCC ansatz class. Examples for both closed- - and restricted open-shell QCC are provided using H2, H4 +, and H4 2+ as well as for using the + and restricted open-shell QCC are provided using H2, H4+, and H4+2 as well as for using the QMF and QCC classes together. """ @@ -45,7 +46,7 @@ def test_qcc_set_var_params(): one_zero = np.zeros((1,), dtype=float) - qcc_ansatz.set_var_params("zeros") + qcc_ansatz.set_var_params("qmf_state") np.testing.assert_array_almost_equal(qcc_ansatz.var_params, one_zero, decimal=6) qcc_ansatz.set_var_params([0.]) @@ -72,7 +73,7 @@ def test_qcc_h2(self): # Build the QCC ansatz, which sets the QMF parameters automatically if none are passed qcc_var_params = [0.22613627] qcc_op_list = [QubitOperator("X0 Y1 Y2 Y3")] - qcc_ansatz = QCC(mol_H2_sto3g, up_then_down=True, qubit_op_list=qcc_op_list) + qcc_ansatz = QCC(mol_H2_sto3g, up_then_down=True, qcc_op_list=qcc_op_list) # Build a QMF + QCC circuit qcc_ansatz.build_circuit() @@ -111,7 +112,7 @@ def test_qmf_qcc_h2(self): self.assertAlmostEqual(energy, -1.137270174660901, delta=1e-6) def test_qcc_h4_cation(self): - """ Verify restricted open-shell functionality when using the QCC class for H4 + """ + """ Verify restricted open-shell functionality when using the QCC class for H4+ """ # Build the QCC ansatz, which sets the QMF parameters automatically if none are passed qcc_op_list = [QubitOperator("X0 Y1 Y2 X3 X4 Y5"), QubitOperator("Y1 X3 X4 X5"), @@ -135,7 +136,7 @@ def test_qcc_h4_cation(self): self.assertAlmostEqual(energy, -1.6380901, delta=1e-6) def test_qmf_qcc_h4_cation(self): - """ Verify restricted open-shell functionality when using QMF + QCC ansatze for H4 + """ + """ Verify restricted open-shell functionality when using QMF + QCC ansatze for H4+ """ # Build the QMF ansatz with optimized parameters qmf_var_params = [3.14159302e+00, 6.20193478e-07, 1.51226426e-06, 3.14159350e+00, @@ -166,7 +167,7 @@ def test_qmf_qcc_h4_cation(self): self.assertAlmostEqual(energy, -1.6382913, delta=1e-6) def test_qcc_h4_double_cation(self): - """ Verify restricted open-shell functionality when using the QCC class for H4 2+ """ + """ Verify restricted open-shell functionality when using the QCC class for H4+2 """ # Build the QCC ansatz, which sets the QMF parameters automatically if none are passed qcc_op_list = [QubitOperator("X0 Y2"), QubitOperator("Y0 X4"), QubitOperator("X0 Y6"), @@ -186,7 +187,7 @@ def test_qcc_h4_double_cation(self): self.assertAlmostEqual(energy, -0.8547019, delta=1e-6) def test_qmf_qcc_h4_double_cation(self): - """ Verify restricted open-shell functionality when using QMF + QCC ansatze for H4 2+ """ + """ Verify restricted open-shell functionality when using QMF + QCC ansatze for H4+2 """ # Build the QMF ansatz with optimized parameters qmf_var_params = [3.14159247e+00, 3.14158884e+00, 1.37660700e-06, 3.14159264e+00, diff --git a/tangelo/toolboxes/ansatz_generator/tests/test_qmf.py b/tangelo/toolboxes/ansatz_generator/tests/test_qmf.py index 0c0eea88a..4d9c08726 100644 --- a/tangelo/toolboxes/ansatz_generator/tests/test_qmf.py +++ b/tangelo/toolboxes/ansatz_generator/tests/test_qmf.py @@ -15,6 +15,7 @@ """Unit tests for closed-shell and restricted open-shell qubit mean field (QMF) ansatz. """ import unittest + import numpy as np from tangelo.linq import Simulator @@ -37,7 +38,7 @@ def test_qmf_set_var_params(): eight_zeros = np.zeros((8,), dtype=float) - qmf_ansatz.set_var_params("zeros") + qmf_ansatz.set_var_params("vacuum") np.testing.assert_array_almost_equal(qmf_ansatz.var_params, eight_zeros, decimal=6) qmf_ansatz.set_var_params([0.] * 8) @@ -45,7 +46,7 @@ def test_qmf_set_var_params(): eight_pis = np.pi * np.ones((8,)) - qmf_ansatz.set_var_params("pis") + qmf_ansatz.set_var_params("full_pi") np.testing.assert_array_almost_equal(qmf_ansatz.var_params, eight_pis, decimal=6) qmf_ansatz.set_var_params(np.array([np.pi] * 8))