def cost_func(params): assert len(params) == len(pool) # compute wf for function call wf = copy.deepcopy(initial_wf) for op, coeff in zip(pool, params): if np.isclose(coeff, 0): continue if isinstance(op, ABCHamiltonian): fqe_op = op else: print("Found a OF Hamiltonian") fqe_op = build_hamiltonian(1j * op, self.sdim, conserve_number=True) if isinstance(fqe_op, ABCHamiltonian): wf = wf.time_evolve(coeff, fqe_op) else: raise ValueError("Can't evolve operator type {}".format( type(fqe_op))) # compute gradients grad_vec = np.zeros(len(params), dtype=np.complex128) # avoid extra gradient computation if we can if opt_method not in ['Nelder-Mead', 'COBYLA']: for pidx, _ in enumerate(params): # evolve e^{iG_{n-1}g_{n-1}}e^{iG_{n-2}g_{n-2}}x # G_{n-3}e^{-G_{n-3}g_{n-3}...|0> grad_wf = copy.deepcopy(initial_wf) for gidx, (op, coeff) in enumerate(zip(pool, params)): if isinstance(op, ABCHamiltonian): fqe_op = op else: fqe_op = build_hamiltonian(1j * op, self.sdim, conserve_number=True) if not np.isclose(coeff, 0): grad_wf = grad_wf.time_evolve(coeff, fqe_op) # if looking at the pth parameter then apply the # operator to the state if gidx == pidx: grad_wf = grad_wf.apply(fqe_op) # grad_val = grad_wf.expectationValue(self.elec_hamil, # brawfn=wf) grad_val = grad_wf.expectationValue(self.k2_fop, brawfn=wf) grad_vec[pidx] = -1j * grad_val + 1j * grad_val.conj() assert np.isclose(grad_vec[pidx].imag, 0) return (wf.expectationValue(self.k2_fop).real, np.array(grad_vec.real, order='F'))
def get_hamiltonian_from_openfermion(ops: 'FermionOperator', norb: int = 0, conserve_number: bool = True, e_0: complex = 0. + 0.j ) -> 'hamiltonian.Hamiltonian': """Given an OpenFermion Hamiltonian return the fqe hamiltonian. Args: ops (openfermion.FermionOperator) - a string of FermionOperators \ representing the Hamiltonian. norb (int) - the number of spatial orbitals in the Hamiltonian conserve_number (bool) - a flag to indicate if the Hamiltonian will be \ applied to a number_conserving wavefunction. e_0 (complex) - the scalar potential of the hamiltonian Returns: hamiltonian (fqe.hamiltonians.hamiltonian) """ assert isinstance(ops, FermionOperator) return build_hamiltonian(ops, norb=norb, conserve_number=conserve_number, e_0=e_0)
def __init__(self, oei: np.ndarray, tei: np.ndarray, operator_pool, n_alpha: int, n_beta: int, iter_max=30, verbose=True, stopping_epsilon=1.0E-3, delta_e_eps=1.0E-6): """ ADAPT-VQE object. Args: oei: one electron integrals in the spatial basis tei: two-electron integrals in the spatial basis operator_pool: Object with .op_pool that is a list of antihermitian FermionOperators n_alpha: Number of alpha-electrons n_beta: Number of beta-electrons iter_max: Maximum ADAPT-VQE steps to take verbose: Print the iteration information stopping_epsilon: define the <[G, H]> value that triggers stopping """ elec_hamil = RestrictedHamiltonian((oei, np.einsum("ijlk", -0.5 * tei))) soei, stei = spinorb_from_spatial(oei, tei) astei = np.einsum('ijkl', stei) - np.einsum('ijlk', stei) molecular_hamiltonian = InteractionOperator(0, soei, 0.25 * astei) reduced_ham = make_reduced_hamiltonian(molecular_hamiltonian, n_alpha + n_beta) self.reduced_ham = reduced_ham self.k2_ham = of.get_fermion_operator(reduced_ham) self.k2_fop = build_hamiltonian(self.k2_ham, elec_hamil.dim(), conserve_number=True) self.elec_hamil = elec_hamil self.iter_max = iter_max self.sdim = elec_hamil.dim() # change to use multiplicity to derive this for open shell self.nalpha = n_alpha self.nbeta = n_beta self.sz = self.nalpha - self.nbeta self.nele = self.nalpha + self.nbeta self.verbose = verbose self.operator_pool = operator_pool self.stopping_eps = stopping_epsilon self.delta_e_eps = delta_e_eps
def test_vbc_time_evolve(): molecule = build_h4square_moleculardata() oei, tei = molecule.get_integrals() nele = molecule.n_electrons nalpha = nele // 2 nbeta = nele // 2 sz = 0 norbs = oei.shape[0] nso = 2 * norbs fqe_wf = fqe.Wavefunction([[nele, sz, norbs]]) fqe_wf.set_wfn(strategy='random') fqe_wf.normalize() nfqe_wf = fqe.get_number_conserving_wavefunction(nele, norbs) nfqe_wf.sector((nele, sz)).coeff = fqe_wf.sector((nele, sz)).coeff _, tpdm = nfqe_wf.sector((nele, sz)).get_openfermion_rdms() d3 = nfqe_wf.sector((nele, sz)).get_three_pdm() adapt = VBC(oei, tei, nalpha, nbeta, iter_max=50) acse_residual = two_rdo_commutator_symm(adapt.reduced_ham.two_body_tensor, tpdm, d3) sos_op = adapt.get_takagi_tensor_decomp(acse_residual, None) test_wf = copy.deepcopy(nfqe_wf) test_wf = sos_op.time_evolve(test_wf) true_wf = copy.deepcopy(nfqe_wf) for v, cc in zip(sos_op.basis_rotation, sos_op.charge_charge): vc = v.conj() new_tensor = np.einsum('pi,si,ij,qj,rj->pqrs', v, vc, -1j * cc, v, vc) if np.isclose(np.linalg.norm(new_tensor), 0): continue fop = of.FermionOperator() for p, q, r, s in product(range(nso), repeat=4): op = ((p, 1), (s, 0), (q, 1), (r, 0)) fop += of.FermionOperator(op, coefficient=new_tensor[p, q, r, s]) fqe_op = build_hamiltonian(1j * fop, conserve_number=True) true_wf = true_wf.time_evolve(1, fqe_op) true_wf = evolve_fqe_givens_unrestricted(true_wf, sos_op.one_body_rotation) assert np.isclose(abs(fqe.vdot(true_wf, test_wf))**2, 1)
def adapt_vqe(self, initial_wf: Wavefunction, opt_method: str = 'L-BFGS-B', opt_options=None, v_reconstruct: bool = True, num_ops_add: int = 1): """ Run ADAPT-VQE using Args: initial_wf: Initial wavefunction at the start of the calculation opt_method: scipy optimizer to use opt_options: options for scipy optimizer v_reconstruct: use valdemoro reconstruction num_ops_add: add this many operators from the pool to the wavefunction """ if opt_options is None: opt_options = {} operator_pool = [] operator_pool_fqe: List[ABCHamiltonian] = [] existing_parameters: List[float] = [] self.gradients = [] self.energies = [initial_wf.expectationValue(self.k2_fop)] iteration = 0 while iteration < self.iter_max: # get current wavefunction wf = copy.deepcopy(initial_wf) for fqe_op, coeff in zip(operator_pool_fqe, existing_parameters): wf = wf.time_evolve(coeff, fqe_op) # calculate rdms for grad _, tpdm = wf.sector((self.nele, self.sz)).get_openfermion_rdms() if v_reconstruct: d3 = 6 * valdemaro_reconstruction(tpdm / 2, self.nele) else: d3 = wf.sector((self.nele, self.sz)).get_three_pdm() # get ACSE Residual and 2-RDM gradient acse_residual = two_rdo_commutator_symm( self.reduced_ham.two_body_tensor, tpdm, d3) one_body_residual = one_rdo_commutator_symm( self.reduced_ham.two_body_tensor, tpdm) # calculate grad of each operator in the pool pool_grad = [] for operator in self.operator_pool.op_pool: grad_val = 0 for op_term, coeff in operator.terms.items(): idx = [xx[0] for xx in op_term] if len(idx) == 4: grad_val += acse_residual[tuple(idx)] * coeff elif len(idx) == 2: grad_val += one_body_residual[tuple(idx)] * coeff pool_grad.append(grad_val) max_grad_terms_idx = \ np.argsort(np.abs(pool_grad))[::-1][:num_ops_add] pool_terms = [ self.operator_pool.op_pool[i] for i in max_grad_terms_idx ] operator_pool.extend(pool_terms) fqe_ops: List[ABCHamiltonian] = [] for f_op in pool_terms: fqe_ops.append( build_hamiltonian(1j * f_op, self.sdim, conserve_number=True)) operator_pool_fqe.extend(fqe_ops) existing_parameters.extend([0] * len(fqe_ops)) new_parameters, current_e = self.optimize_param( operator_pool_fqe, existing_parameters, initial_wf, opt_method, opt_options=opt_options) existing_parameters = new_parameters.tolist() if self.verbose: print(iteration, current_e, max(np.abs(pool_grad))) self.energies.append(current_e) self.gradients.append(pool_grad) if max(np.abs(pool_grad)) < self.stopping_eps or np.abs( self.energies[-2] - self.energies[-1]) < self.delta_e_eps: break iteration += 1
def test_generalized_doubles_takagi(): molecule = build_lih_moleculardata() oei, tei = molecule.get_integrals() nele = 4 nalpha = 2 nbeta = 2 sz = 0 norbs = oei.shape[0] nso = 2 * norbs fqe_wf = fqe.Wavefunction([[nele, sz, norbs]]) fqe_wf.set_wfn(strategy='hartree-fock') fqe_wf.normalize() _, tpdm = fqe_wf.sector((nele, sz)).get_openfermion_rdms() d3 = fqe_wf.sector((nele, sz)).get_three_pdm() soei, stei = spinorb_from_spatial(oei, tei) astei = np.einsum('ijkl', stei) - np.einsum('ijlk', stei) molecular_hamiltonian = of.InteractionOperator(0, soei, 0.25 * astei) reduced_ham = make_reduced_hamiltonian(molecular_hamiltonian, nalpha + nbeta) acse_residual = two_rdo_commutator_symm(reduced_ham.two_body_tensor, tpdm, d3) for p, q, r, s in product(range(nso), repeat=4): if p == q or r == s: continue assert np.isclose(acse_residual[p, q, r, s], -acse_residual[s, r, q, p].conj()) Zlp, Zlm, _, one_body_residual = doubles_factorization_takagi(acse_residual) test_fop = get_fermion_op(one_body_residual) # test the first four factors for ll in range(4): test_fop += 0.25 * get_fermion_op(Zlp[ll])**2 test_fop += 0.25 * get_fermion_op(Zlm[ll])**2 op1mat = Zlp[ll] op2mat = Zlm[ll] w1, v1 = sp.linalg.schur(op1mat) w1 = np.diagonal(w1) assert np.allclose(v1 @ np.diag(w1) @ v1.conj().T, op1mat) v1c = v1.conj() w2, v2 = sp.linalg.schur(op2mat) w2 = np.diagonal(w2) assert np.allclose(v2 @ np.diag(w2) @ v2.conj().T, op2mat) oww1 = np.outer(w1, w1) fqe_wf = fqe.Wavefunction([[nele, sz, norbs]]) fqe_wf.set_wfn(strategy='hartree-fock') fqe_wf.normalize() nfqe_wf = fqe.get_number_conserving_wavefunction(nele, norbs) nfqe_wf.sector((nele, sz)).coeff = fqe_wf.sector((nele, sz)).coeff this_generatory = np.einsum('pi,si,ij,qj,rj->pqrs', v1, v1c, oww1, v1, v1c) fop = of.FermionOperator() for p, q, r, s in product(range(nso), repeat=4): op = ((p, 1), (s, 0), (q, 1), (r, 0)) fop += of.FermionOperator(op, coefficient=this_generatory[p, q, r, s]) fqe_fop = build_hamiltonian(1j * fop, norb=norbs, conserve_number=True) exact_wf = fqe.apply_generated_unitary(nfqe_wf, 1, 'taylor', fqe_fop) test_wf = fqe.algorithm.low_rank.evolve_fqe_givens_unrestricted( nfqe_wf, v1.conj().T) test_wf = fqe.algorithm.low_rank.evolve_fqe_charge_charge_unrestricted( test_wf, -oww1.imag) test_wf = fqe.algorithm.low_rank.evolve_fqe_givens_unrestricted( test_wf, v1) assert np.isclose(abs(fqe.vdot(test_wf, exact_wf))**2, 1)
def vbc(self, initial_wf: Wavefunction, opt_method: str = 'L-BFGS-B', opt_options=None, num_opt_var=None, v_reconstruct=False, generator_decomp=None, generator_rank=None): """The variational Brillouin condition method Solve for the 2-body residual and then variationally determine the step size. This exact simulation cannot be implemented without Trotterization. A proxy for the approximate evolution is the update_ rank pameter which limites the rank of the residual. Args: initial_wf: initial wavefunction opt_method: scipy optimizer name num_opt_var: Number of optimization variables to consider v_reconstruct: use valdemoro reconstruction of 3-RDM to calculate the residual generator_decomp: None, takagi, or svd generator_rank: number of generator terms to take """ if opt_options is None: opt_options = {} self.num_opt_var = num_opt_var nso = 2 * self.sdim operator_pool: List[Union[ABCHamiltonian, SumOfSquaresOperator]] = [] operator_pool_fqe: List[ Union[ABCHamiltonian, SumOfSquaresOperator]] = [] existing_parameters: List[float] = [] self.energies = [] self.energies = [initial_wf.expectationValue(self.k2_fop)] self.residuals = [] iteration = 0 while iteration < self.iter_max: # get current wavefunction wf = copy.deepcopy(initial_wf) for op, coeff in zip(operator_pool_fqe, existing_parameters): if np.isclose(coeff, 0): continue if isinstance(op, ABCHamiltonian): wf = wf.time_evolve(coeff, op) elif isinstance(op, SumOfSquaresOperator): for v, cc in zip(op.basis_rotation, op.charge_charge): wf = evolve_fqe_givens_unrestricted(wf, v.conj().T) wf = evolve_fqe_charge_charge_unrestricted( wf, coeff * cc) wf = evolve_fqe_givens_unrestricted(wf, v) wf = evolve_fqe_givens_unrestricted(wf, op.one_body_rotation) else: raise ValueError("Can't evolve operator type {}".format( type(op))) # calculate rdms for grad _, tpdm = wf.sector((self.nele, self.sz)).get_openfermion_rdms() if v_reconstruct: d3 = 6 * valdemaro_reconstruction_functional( tpdm / 2, self.nele) else: d3 = wf.sector((self.nele, self.sz)).get_three_pdm() # get ACSE Residual and 2-RDM gradient acse_residual = two_rdo_commutator_symm( self.reduced_ham.two_body_tensor, tpdm, d3) if generator_decomp is None: fop = get_fermion_op(acse_residual) elif generator_decomp is 'svd': new_residual = np.zeros_like(acse_residual) for p, q, r, s in product(range(nso), repeat=4): new_residual[p, q, r, s] = (acse_residual[p, q, r, s] - acse_residual[s, r, q, p]) / 2 fop = self.get_svd_tensor_decomp(new_residual, generator_rank) elif generator_decomp is 'takagi': fop = self.get_takagi_tensor_decomp(acse_residual, generator_rank) else: raise ValueError( "Generator decomp must be None, svd, or takagi") operator_pool.extend([fop]) fqe_ops: List[Union[ABCHamiltonian, SumOfSquaresOperator]] = [] if isinstance(fop, ABCHamiltonian): fqe_ops.append(fop) elif isinstance(fop, SumOfSquaresOperator): fqe_ops.append(fop) else: fqe_ops.append( build_hamiltonian(1j * fop, self.sdim, conserve_number=True)) operator_pool_fqe.extend(fqe_ops) existing_parameters.extend([0]) if self.num_opt_var is not None: if len(operator_pool_fqe) < self.num_opt_var: pool_to_op = operator_pool_fqe params_to_op = existing_parameters current_wf = copy.deepcopy(initial_wf) else: pool_to_op = operator_pool_fqe[-self.num_opt_var:] params_to_op = existing_parameters[-self.num_opt_var:] current_wf = copy.deepcopy(initial_wf) for fqe_op, coeff in zip( operator_pool_fqe[:-self.num_opt_var], existing_parameters[:-self.num_opt_var]): current_wf = current_wf.time_evolve(coeff, fqe_op) temp_cwf = copy.deepcopy(current_wf) for fqe_op, coeff in zip(pool_to_op, params_to_op): if np.isclose(coeff, 0): continue temp_cwf = temp_cwf.time_evolve(coeff, fqe_op) new_parameters, current_e = self.optimize_param( pool_to_op, params_to_op, current_wf, opt_method, opt_options=opt_options) if len(operator_pool_fqe) < self.num_opt_var: existing_parameters = new_parameters.tolist() else: existing_parameters[-self.num_opt_var:] = \ new_parameters.tolist() else: new_parameters, current_e = self.optimize_param( operator_pool_fqe, existing_parameters, initial_wf, opt_method, opt_options=opt_options) existing_parameters = new_parameters.tolist() if self.verbose: print(iteration, current_e, np.max(np.abs(acse_residual)), len(existing_parameters)) self.energies.append(current_e) self.residuals.append(acse_residual) if np.max(np.abs(acse_residual)) < self.stopping_eps or np.abs( self.energies[-2] - self.energies[-1]) < self.delta_e_eps: break iteration += 1
def test_build_hamiltonian_paths(self): """Check that all cases of hamiltonian objects are built """ self.assertRaises(TypeError, build_hamiltonian, 0) with self.subTest(name='general'): ops = FermionOperator('1^ 4^ 0 3', 1.0) \ + FermionOperator('0^ 5^ 3 2^ 4^ 1 7 6', 1.2) \ + FermionOperator('1^ 6', -0.3) ops += hermitian_conjugated(ops) self.assertIsInstance(build_hamiltonian(ops), general_hamiltonian.General) with self.subTest(name='sparse'): ops = FermionOperator('5^ 1^ 3^ 2 0 1', 1.0 - 1.j) \ + FermionOperator('1^ 0^ 2^ 3 1 5', 1.0 + 1.j) self.assertIsInstance(build_hamiltonian(ops), sparse_hamiltonian.SparseHamiltonian) with self.subTest(name='diagonal'): ops = FermionOperator('1^ 1', 1.0) \ + FermionOperator('2^ 2', 2.0) \ + FermionOperator('3^ 3', 3.0) \ + FermionOperator('4^ 4', 4.0) self.assertIsInstance(build_hamiltonian(ops), diagonal_hamiltonian.Diagonal) with self.subTest(name='gso'): ops = FermionOperator() for i in range(4): for j in range(4): opstr = str(i) + '^ ' + str(j) coeff = complex((i + 1) * (j + 1) * 0.1) ops += FermionOperator(opstr, coeff) self.assertIsInstance(build_hamiltonian(ops), gso_hamiltonian.GSOHamiltonian) with self.subTest(name='restricted'): ops = FermionOperator() for i in range(0, 3, 2): for j in range(0, 3, 2): coeff = complex((i + 1) * (j + 1) * 0.1) opstr = str(i) + '^ ' + str(j) ops += FermionOperator(opstr, coeff) opstr = str(i + 1) + '^ ' + str(j + 1) ops += FermionOperator(opstr, coeff) self.assertIsInstance(build_hamiltonian(ops), restricted_hamiltonian.RestrictedHamiltonian) with self.subTest(name='sso'): ops = FermionOperator() for i in range(0, 3, 2): for j in range(0, 3, 2): coeff = complex((i + 1) * (j + 1) * 0.1) opstr = str(i) + '^ ' + str(j) ops += FermionOperator(opstr, coeff) opstr = str(i + 1) + '^ ' + str(j + 1) coeff *= 1.5 ops += FermionOperator(opstr, coeff) self.assertIsInstance(build_hamiltonian(ops), sso_hamiltonian.SSOHamiltonian) with self.subTest(name='diagonal_coulomb'): ops = FermionOperator() for i in range(4): for j in range(4): opstring = str(i) + '^ ' + str(j) + '^ ' + str( i) + ' ' + str(j) ops += FermionOperator(opstring, 0.001 * (i + 1) * (j + 1)) self.assertIsInstance(build_hamiltonian(ops), diagonal_coulomb.DiagonalCoulomb)