def bc_solve(self, initial_wf): """Propagate BC differential equation until convergence. Args: initial_wf: Initial wavefunction to evolve. """ fqe_wf = copy.deepcopy(initial_wf) sdim = self.sdim iter_max = self.iter_max iteration = 0 h = 1.0e-4 self.acse_energy = [fqe_wf.expectationValue(self.elec_hamil).real] while iteration < iter_max: if self.parallel: acse_residual = get_acse_residual_fqe_parallel( fqe_wf, self.elec_hamil, sdim) acse_res_op = get_fermion_op(acse_residual) tpdm_grad = get_tpdm_grad_fqe_parallel(fqe_wf, acse_residual, sdim) else: acse_residual = get_acse_residual_fqe(fqe_wf, self.elec_hamil, sdim) acse_res_op = get_fermion_op(acse_residual) tpdm_grad = get_tpdm_grad_fqe(fqe_wf, acse_residual, sdim) # epsilon_opt = - Tr[K, D'(lambda)] / Tr[K, D''(lambda)] # K is reduced Hamiltonian # get approximate D'' by short propagation # TODO: do this with cumulant reconstruction instead of wf prop. fqe_wfh = fqe_wf.time_evolve(h, 1j * acse_res_op) acse_residualh = get_acse_residual_fqe(fqe_wfh, self.elec_hamil, sdim) tpdm_gradh = get_tpdm_grad_fqe(fqe_wfh, acse_residualh, sdim) tpdm_gradgrad = (1 / h) * (tpdm_gradh - tpdm_grad) epsilon = -np.einsum("ijkl,ijkl", self.reduced_ham.two_body_tensor, tpdm_grad) epsilon /= np.einsum("ijkl,ijkl", self.reduced_ham.two_body_tensor, tpdm_gradgrad) epsilon = epsilon.real fqe_wf = fqe_wf.time_evolve(epsilon, 1j * acse_res_op) current_energy = fqe_wf.expectationValue(self.elec_hamil).real self.acse_energy.append(current_energy.real) print_string = "Iter {: 5f}\tcurrent energy {: 5.10f}\t".format( iteration, current_energy) print_string += "|dE| {: 5.10f}\tStep size {: 5.10f}".format( np.abs(self.acse_energy[-2] - self.acse_energy[-1]), epsilon) if self.verbose: print(print_string) if (iteration >= 1 and np.abs(self.acse_energy[-2] - self.acse_energy[-1]) < 0.5e-4): break iteration += 1
def test_generalized_doubles(): generator = generate_antisymm_generator(2) nso = generator.shape[0] for p, q, r, s in product(range(nso), repeat=4): if p < q and s < r: assert np.isclose(generator[p, q, r, s], -generator[q, p, r, s]) ul, vl, one_body_residual, ul_ops, vl_ops, one_body_op = \ doubles_factorization_svd(generator) generator_mat = np.reshape(np.transpose(generator, [0, 3, 1, 2]), (nso**2, nso**2)).astype(np.float) one_body_residual_test = -np.einsum('pqrq->pr', generator) assert np.allclose(generator_mat, generator_mat.T) assert np.allclose(one_body_residual, one_body_residual_test) tgenerator_mat = np.zeros_like(generator_mat) for row_gem, col_gem in product(range(nso**2), repeat=2): p, s = row_gem // nso, row_gem % nso q, r = col_gem // nso, col_gem % nso tgenerator_mat[row_gem, col_gem] = generator[p, q, r, s] assert np.allclose(tgenerator_mat, generator_mat) u, sigma, vh = np.linalg.svd(generator_mat) fop = copy.deepcopy(one_body_op) fop2 = copy.deepcopy(one_body_op) fop3 = copy.deepcopy(one_body_op) fop4 = copy.deepcopy(one_body_op) for ll in range(len(sigma)): ul.append(np.sqrt(sigma[ll]) * u[:, ll].reshape((nso, nso))) ul_ops.append( get_fermion_op(np.sqrt(sigma[ll]) * u[:, ll].reshape((nso, nso)))) vl.append(np.sqrt(sigma[ll]) * vh[ll, :].reshape((nso, nso))) vl_ops.append( get_fermion_op(np.sqrt(sigma[ll]) * vh[ll, :].reshape((nso, nso)))) Smat = ul[ll] + vl[ll] Dmat = ul[ll] - vl[ll] S = ul_ops[ll] + vl_ops[ll] Sd = of.hermitian_conjugated(S) D = ul_ops[ll] - vl_ops[ll] Dd = of.hermitian_conjugated(D) op1 = S + 1j * of.hermitian_conjugated(S) op2 = S - 1j * of.hermitian_conjugated(S) op3 = D + 1j * of.hermitian_conjugated(D) op4 = D - 1j * of.hermitian_conjugated(D) assert np.isclose( of.normal_ordered(of.commutator( op1, of.hermitian_conjugated(op1))).induced_norm(), 0) assert np.isclose( of.normal_ordered(of.commutator( op2, of.hermitian_conjugated(op2))).induced_norm(), 0) assert np.isclose( of.normal_ordered(of.commutator( op3, of.hermitian_conjugated(op3))).induced_norm(), 0) assert np.isclose( of.normal_ordered(of.commutator( op4, of.hermitian_conjugated(op4))).induced_norm(), 0) fop3 += (1 / 8) * ((S**2 - Sd**2) - (D**2 - Dd**2)) fop4 += (1 / 16) * ((op1**2 + op2**2) - (op3**2 + op4**2)) op1mat = Smat + 1j * Smat.T op2mat = Smat - 1j * Smat.T op3mat = Dmat + 1j * Dmat.T op4mat = Dmat - 1j * Dmat.T assert np.allclose(of.commutator(op1mat, op1mat.conj().T), 0) assert np.allclose(of.commutator(op2mat, op2mat.conj().T), 0) assert np.allclose(of.commutator(op3mat, op3mat.conj().T), 0) assert np.allclose(of.commutator(op4mat, op4mat.conj().T), 0) # check that we have normal operators and that the outer product # of their eigenvalues is imaginary. Also check vv is unitary if not np.isclose(sigma[ll], 0): assert np.isclose( of.normal_ordered(get_fermion_op(op1mat) - op1).induced_norm(), 0) assert np.isclose( of.normal_ordered(get_fermion_op(op2mat) - op2).induced_norm(), 0) assert np.isclose( of.normal_ordered(get_fermion_op(op3mat) - op3).induced_norm(), 0) assert np.isclose( of.normal_ordered(get_fermion_op(op4mat) - op4).induced_norm(), 0) ww, vv = np.linalg.eig(op1mat) eye = np.eye(nso) assert np.allclose(np.outer(ww, ww).real, 0) assert np.allclose(vv.conj().T @ vv, eye) ww, vv = np.linalg.eig(op2mat) assert np.allclose(np.outer(ww, ww).real, 0) assert np.allclose(vv.conj().T @ vv, eye) ww, vv = np.linalg.eig(op3mat) assert np.allclose(np.outer(ww, ww).real, 0) assert np.allclose(vv.conj().T @ vv, eye) ww, vv = np.linalg.eig(op4mat) assert np.allclose(np.outer(ww, ww).real, 0) assert np.allclose(vv.conj().T @ vv, eye) fop2 += 0.25 * ul_ops[ll] * vl_ops[ll] fop2 += 0.25 * vl_ops[ll] * ul_ops[ll] fop2 += -0.25 * of.hermitian_conjugated( vl_ops[ll]) * of.hermitian_conjugated(ul_ops[ll]) fop2 += -0.25 * of.hermitian_conjugated( ul_ops[ll]) * of.hermitian_conjugated(vl_ops[ll]) fop += vl_ops[ll] * ul_ops[ll] true_fop = get_fermion_op(generator) assert np.isclose(of.normal_ordered(fop - true_fop).induced_norm(), 0) assert np.isclose(of.normal_ordered(fop2 - true_fop).induced_norm(), 0) assert np.isclose(of.normal_ordered(fop3 - true_fop).induced_norm(), 0) assert np.isclose(of.normal_ordered(fop4 - true_fop).induced_norm(), 0)
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 bc_solve_rdms(self, initial_wf): """Propagate BC differential equation until convergence. State is evolved and then 3-RDM is measured. This information is used to construct a new state Args: initial_wf: Initial wavefunction to evolve. """ fqe_wf = copy.deepcopy(initial_wf) iter_max = self.iter_max iteration = 0 sector = (self.nalpha + self.nbeta, self.sz) h = 1.0e-4 self.acse_energy = [fqe_wf.expectationValue(self.elec_hamil).real] while iteration < iter_max: # extract FqeData object each iteration in case fqe_wf is copied fqe_data = fqe_wf.sector(sector) # get RDMs from FqeData d3 = fqe_data.get_three_pdm() _, tpdm = fqe_data.get_openfermion_rdms() # get ACSE Residual and 2-RDM gradient acse_residual = two_rdo_commutator_symm( self.reduced_ham.two_body_tensor, tpdm, d3) tpdm_grad = two_rdo_commutator_antisymm(acse_residual, tpdm, d3) acse_res_op = get_fermion_op(acse_residual) # epsilon_opt = - Tr[K, D'(lambda)] / Tr[K, D''(lambda)] # K is reduced Hamiltonian # get approximate D'' by short propagation # TODO: do this with cumulant reconstruction instead of wf prop. fqe_wfh = fqe_wf.time_evolve(h, 1j * acse_res_op) fqe_datah = fqe_wfh.sector(sector) d3h = fqe_datah.get_three_pdm() _, tpdmh = fqe_datah.get_openfermion_rdms() acse_residualh = two_rdo_commutator_symm( self.reduced_ham.two_body_tensor, tpdmh, d3h) tpdm_gradh = two_rdo_commutator_antisymm(acse_residualh, tpdmh, d3h) tpdm_gradgrad = (1 / h) * (tpdm_gradh - tpdm_grad) epsilon = -np.einsum("ijkl,ijkl", self.reduced_ham.two_body_tensor, tpdm_grad) epsilon /= np.einsum("ijkl,ijkl", self.reduced_ham.two_body_tensor, tpdm_gradgrad) epsilon = epsilon.real fqe_wf = fqe_wf.time_evolve(epsilon, 1j * acse_res_op) current_energy = fqe_wf.expectationValue(self.elec_hamil).real self.acse_energy.append(current_energy.real) print_string = "Iter {: 5f}\tcurrent energy {: 5.10f}\t".format( iteration, current_energy) print_string += "|dE| {: 5.10f}\tStep size {: 5.10f}".format( np.abs(self.acse_energy[-2] - self.acse_energy[-1]), epsilon) if self.verbose: print(print_string) if (iteration >= 1 and np.abs(self.acse_energy[-2] - self.acse_energy[-1]) < 0.5e-4): break iteration += 1
def doubles_factorization_svd(generator_tensor: np.ndarray, eig_cutoff=None): """ Given an antisymmetric antihermitian tensor perform a double factorized low-rank decomposition. Given: A = sum_{pqrs}A^{pq}_{sr}p^ q^ r s with A^{pq}_{sr} = -A^{qp}_{sr} = -A^{pq}_{rs} = -A^{sr}_{pq} Rewrite A as a sum-of squares s.t A = sum_{l}Y_{l}^2 where Y_{l} are normal operator one-body operators such that the spectral theorem holds and we can use the double factorization to implement an approximate evolution. """ if not np.allclose(generator_tensor.imag, 0): raise TypeError("generator_tensor must be a real matrix") if eig_cutoff is not None: if eig_cutoff % 2 != 0: raise ValueError("eig_cutoff must be an even number") nso = generator_tensor.shape[0] generator_tensor = generator_tensor.real generator_mat = np.zeros((nso**2, nso**2)) for row_gem, col_gem in product(range(nso**2), repeat=2): p, s = row_gem // nso, row_gem % nso q, r = col_gem // nso, col_gem % nso generator_mat[row_gem, col_gem] = generator_tensor[p, q, r, s] test_generator_mat = np.reshape( np.transpose(generator_tensor, [0, 3, 1, 2]), (nso**2, nso**2)).astype(np.float) assert np.allclose(test_generator_mat, generator_mat) if not np.allclose(generator_mat, generator_mat.T): raise ValueError("generator tensor does not correspond to four-fold" " antisymmetry") one_body_residual = -np.einsum('pqrq->pr', generator_tensor) u, sigma, vh = np.linalg.svd(generator_mat) ul = [] ul_ops = [] vl = [] vl_ops = [] if eig_cutoff is None: max_sigma = len(sigma) else: max_sigma = eig_cutoff for ll in range(max_sigma): ul.append(np.sqrt(sigma[ll]) * u[:, ll].reshape((nso, nso))) ul_ops.append( get_fermion_op(np.sqrt(sigma[ll]) * u[:, ll].reshape((nso, nso)))) vl.append(np.sqrt(sigma[ll]) * vh[ll, :].reshape((nso, nso))) vl_ops.append( get_fermion_op(np.sqrt(sigma[ll]) * vh[ll, :].reshape((nso, nso)))) S = ul_ops[ll] + vl_ops[ll] D = ul_ops[ll] - vl_ops[ll] op1 = S + 1j * of.hermitian_conjugated(S) op2 = S - 1j * of.hermitian_conjugated(S) op3 = D + 1j * of.hermitian_conjugated(D) op4 = D - 1j * of.hermitian_conjugated(D) assert np.isclose( of.normal_ordered(of.commutator( op1, of.hermitian_conjugated(op1))).induced_norm(), 0) assert np.isclose( of.normal_ordered(of.commutator( op2, of.hermitian_conjugated(op2))).induced_norm(), 0) assert np.isclose( of.normal_ordered(of.commutator( op3, of.hermitian_conjugated(op3))).induced_norm(), 0) assert np.isclose( of.normal_ordered(of.commutator( op4, of.hermitian_conjugated(op4))).induced_norm(), 0) one_body_op = of.FermionOperator() for p, q in product(range(nso), repeat=2): tfop = ((p, 1), (q, 0)) one_body_op += of.FermionOperator(tfop, coefficient=one_body_residual[p, q]) return ul, vl, one_body_residual, ul_ops, vl_ops, one_body_op