Source code for pyscf.fci.direct_spin1_symm

#!/usr/bin/env python
# Copyright 2014-2021 The PySCF Developers. All Rights Reserved.
#
# 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.
#
# Author: Qiming Sun <osirpt.sun@gmail.com>
#

'''
Different FCI solvers are implemented to support different type of symmetry.
                    Symmetry
File                Point group   Spin singlet   Real hermitian*    Alpha/beta degeneracy
direct_spin0_symm   Yes           Yes            Yes                Yes
direct_spin1_symm   Yes           No             Yes                Yes
direct_spin0        No            Yes            Yes                Yes
direct_spin1        No            No             Yes                Yes
direct_uhf          No            No             Yes                No
direct_nosym        No            No             No**               Yes

*  Real hermitian Hamiltonian implies (ij|kl) = (ji|kl) = (ij|lk) = (ji|lk)
** Hamiltonian is real but not hermitian, (ij|kl) != (ji|kl) ...
'''

import sys
import ctypes
import numpy
import numpy as np
from pyscf import ao2mo
from pyscf import lib
from pyscf.lib import logger
from pyscf import symm
from pyscf.scf.hf_symm import map_degeneracy
from pyscf.fci import cistring
from pyscf.fci import direct_spin1
from pyscf.fci import addons
from pyscf.fci.spin_op import contract_ss
from pyscf.fci.addons import _unpack_nelec
from pyscf import __config__

libfci = direct_spin1.libfci

TOTIRREPS = 8

# Note eri is NOT the 2e hamiltonian matrix, the 2e hamiltonian is
# h2e = eri_{pq,rs} p^+ q r^+ s
#     = (pq|rs) p^+ r^+ s q - (pq|rs) \delta_{qr} p^+ s
# so eri is defined as
#       eri_{pq,rs} = (pq|rs) - (1/Nelec) \sum_q (pq|qs)
# to restore the symmetry between pq and rs,
#       eri_{pq,rs} = (pq|rs) - (.5/Nelec) [\sum_q (pq|qs) + \sum_p (pq|rp)]
# Please refer to the treatment in direct_spin1.absorb_h1e
[docs] def contract_2e(eri, fcivec, norb, nelec, link_index=None, orbsym=None, wfnsym=0): if orbsym is None: return direct_spin1.contract_2e(eri, fcivec, norb, nelec, link_index) eri = ao2mo.restore(4, eri, norb) neleca, nelecb = _unpack_nelec(nelec) link_indexa, link_indexb = direct_spin1._unpack(norb, nelec, link_index) na, nlinka = link_indexa.shape[:2] nb, nlinkb = link_indexb.shape[:2] eri_irs, rank_eri, irrep_eri = reorder_eri(eri, norb, orbsym) strsa = cistring.gen_strings4orblist(range(norb), neleca) aidx, link_indexa = gen_str_irrep(strsa, orbsym, link_indexa, rank_eri, irrep_eri) nas = np.array([x.size for x in aidx], dtype=np.int32) if neleca == nelecb: bidx, link_indexb = aidx, link_indexa nbs = nas else: strsb = cistring.gen_strings4orblist(range(norb), nelecb) bidx, link_indexb = gen_str_irrep(strsb, orbsym, link_indexb, rank_eri, irrep_eri) nbs = np.array([x.size for x in bidx], dtype=np.int32) eri_ir_dims = np.array([x.shape[0] for x in eri_irs], dtype=np.int32) eri_irs = np.hstack([x.ravel() for x in eri_irs]) wfnsym_in_d2h = wfnsym % 10 orbsym_in_d2h = np.asarray(orbsym) % 10 max_ir = orbsym_in_d2h.max() if max_ir >= 4: nirreps = 8 elif max_ir >= 2: nirreps = 4 elif max_ir >= 1: nirreps = 2 else: nirreps = 1 if fcivec.size == na * nb: fcivec_shape = fcivec.shape fcivec = fcivec.reshape((na,nb), order='C') ci0 = [] for ir in range(nirreps): ma, mb = aidx[ir].size, bidx[wfnsym_in_d2h ^ ir].size ci0.append(np.zeros((ma, mb))) if ma * mb > 0: lib.take_2d(fcivec, aidx[ir], bidx[wfnsym_in_d2h ^ ir], out=ci0[ir]) ci_size = np.array([x.size for x in ci0], dtype=np.int32) ci0 = np.hstack([x.ravel() for x in ci0]) else: ci_size = [] for ir in range(nirreps): ma, mb = aidx[ir].size, bidx[wfnsym_in_d2h ^ ir].size ci_size.append(ma * mb) ci_size = np.array(ci_size, dtype=np.int32) ci0 = fcivec ci1 = np.zeros_like(ci0) libfci.FCIcontract_2e_symm1( eri_irs.ctypes.data_as(ctypes.c_void_p), ci0.ctypes.data_as(ctypes.c_void_p), ci1.ctypes.data_as(ctypes.c_void_p), eri_ir_dims.ctypes.data_as(ctypes.c_void_p), ci_size.ctypes.data_as(ctypes.c_void_p), nas.ctypes.data_as(ctypes.c_void_p), nbs.ctypes.data_as(ctypes.c_void_p), link_indexa.ctypes.data_as(ctypes.c_void_p), link_indexb.ctypes.data_as(ctypes.c_void_p), ctypes.c_int(norb), ctypes.c_int(nlinka), ctypes.c_int(nlinkb), ctypes.c_int(nirreps), ctypes.c_int(wfnsym_in_d2h)) if fcivec.size == na * nb: ci_loc = np.append(0, np.cumsum(ci_size)) ci1new = np.zeros_like(fcivec) for ir in range(nirreps): if ci_size[ir] > 0: ma, mb = aidx[ir].size, bidx[wfnsym_in_d2h ^ ir].size buf = ci1[ci_loc[ir]:ci_loc[ir+1]].reshape(ma, mb) lib.takebak_2d(ci1new, buf, aidx[ir], bidx[wfnsym_in_d2h ^ ir]) ci1 = ci1new.reshape(fcivec_shape) return ci1.view(direct_spin1.FCIvector)
[docs] def kernel(h1e, eri, norb, nelec, ci0=None, level_shift=1e-3, tol=1e-10, lindep=1e-14, max_cycle=50, max_space=12, nroots=1, davidson_only=False, pspace_size=400, orbsym=None, wfnsym=None, ecore=0, **kwargs): assert (len(orbsym) == norb) cis = FCISolver(None) cis.level_shift = level_shift cis.conv_tol = tol cis.lindep = lindep cis.max_cycle = max_cycle cis.max_space = max_space cis.nroots = nroots cis.davidson_only = davidson_only cis.pspace_size = pspace_size cis.orbsym = orbsym cis.wfnsym = wfnsym unknown = {} for k, v in kwargs.items(): if not hasattr(cis, k): unknown[k] = v setattr(cis, k, v) if unknown: sys.stderr.write('Unknown keys %s for FCI kernel %s\n' % (str(unknown.keys()), __name__)) e, c = cis.kernel(h1e, eri, norb, nelec, ci0, ecore=ecore, **unknown) return e, c
make_rdm1 = direct_spin1.make_rdm1 make_rdm1s = direct_spin1.make_rdm1s make_rdm12 = direct_spin1.make_rdm12 trans_rdm1s = direct_spin1.trans_rdm1s trans_rdm1 = direct_spin1.trans_rdm1 trans_rdm12 = direct_spin1.trans_rdm12
[docs] def energy(h1e, eri, fcivec, norb, nelec, link_index=None, orbsym=None, wfnsym=0): h2e = direct_spin1.absorb_h1e(h1e, eri, norb, nelec) * .5 ci1 = contract_2e(h2e, fcivec, norb, nelec, link_index, orbsym, wfnsym) return np.dot(fcivec.ravel(), ci1.ravel())
def _id_wfnsym(cisolver, norb, nelec, orbsym, wfnsym): '''Guess wfnsym or convert wfnsym to symmetry ID if it's a symmetry label''' gpname = getattr(cisolver.mol, 'groupname', None) if wfnsym is None: neleca, nelecb = _unpack_nelec(nelec) wfnsym = 0 # Ag, A1 or A for i in orbsym[nelecb:neleca]: wfnsym ^= i % 10 if gpname in ('Dooh', 'Coov'): l = 0 for i in orbsym[nelecb:neleca]: l += symm.basis.linearmole_irrep2momentum(i) wfnsym += (l//2) * 10 elif isinstance(wfnsym, str): wfnsym = symm.irrep_name2id(gpname, wfnsym) return wfnsym def _gen_strs_irrep(strs, orbsym): # % 10 to convert irrep_ids to irrep of D2h orbsym_in_d2h = np.asarray(orbsym) % 10 irreps = np.zeros(len(strs), dtype=np.int32) if isinstance(strs, cistring.OIndexList): nocc = strs.shape[1] for i in range(nocc): irreps ^= orbsym_in_d2h[strs[:,i]] else: for i, ir in enumerate(orbsym_in_d2h): irreps[np.bitwise_and(strs, 1 << i) > 0] ^= ir return irreps def _get_init_guess(airreps, birreps, nroots, hdiag, nelec, orbsym, wfnsym=0): neleca, nelecb = _unpack_nelec(nelec) na = len(airreps) nb = len(birreps) sym_allowed = airreps[:,None] == wfnsym ^ birreps if neleca == nelecb and na == nb: idx = np.arange(na) sym_allowed[idx[:,None] < idx] = False idx_a, idx_b = np.where(sym_allowed) hdiag = hdiag.reshape(na,nb)[idx_a,idx_b] if hdiag.size <= nroots: hdiag_indices = np.arange(hdiag.size) else: hdiag_indices = np.argpartition(hdiag, nroots-1)[:nroots] ci0 = [] for k in hdiag_indices: addra, addrb = idx_a[k], idx_b[k] x = np.zeros((na, nb)) x[addra,addrb] = 1 ci0.append(x.ravel().view(direct_spin1.FCIvector)) if len(ci0) == 0: raise lib.exceptions.WfnSymmetryError( f'Initial guess for symmetry {wfnsym} not found') return ci0
[docs] def get_init_guess(norb, nelec, nroots, hdiag, orbsym, wfnsym=0): neleca, nelecb = _unpack_nelec(nelec) na = cistring.num_strings(norb, neleca) nb = cistring.num_strings(norb, nelecb) if hdiag.size == na * nb: strsa = cistring.gen_strings4orblist(range(norb), neleca) airreps = birreps = _gen_strs_irrep(strsa, orbsym) if neleca != nelecb: strsb = cistring.gen_strings4orblist(range(norb), nelecb) birreps = _gen_strs_irrep(strsb, orbsym) return _get_init_guess(airreps, birreps, nroots, hdiag, nelec, orbsym, wfnsym) ci0 = [] if hdiag.size <= nroots: hdiag_indices = np.arange(hdiag.size) else: hdiag_indices = np.argpartition(hdiag, nroots-1)[:nroots] for k in hdiag_indices: x = np.zeros_like(hdiag) x[k] = 1. ci0.append(x.ravel().view(direct_spin1.FCIvector)) if len(ci0) == 0: raise lib.exceptions.WfnSymmetryError( f'Initial guess for symmetry {wfnsym} not found') return ci0
def _validate_degen_mapping(mapping, norb): '''Check if 2D irreps are properly paired''' mapping = np.asarray(mapping) return (mapping.max() < norb and # Must be self-conjugated numpy.array_equal(mapping[mapping], numpy.arange(norb)))
[docs] def get_init_guess_cyl_sym(norb, nelec, nroots, hdiag, orbsym, wfnsym=0): neleca, nelecb = _unpack_nelec(nelec) strsa = strsb = cistring.gen_strings4orblist(range(norb), neleca) airreps_d2h = birreps_d2h = _gen_strs_irrep(strsa, orbsym) a_ls = b_ls = _strs_angular_momentum(strsa, orbsym) if neleca != nelecb: strsb = cistring.gen_strings4orblist(range(norb), nelecb) birreps_d2h = _gen_strs_irrep(strsb, orbsym) b_ls = _strs_angular_momentum(strsb, orbsym) wfnsym_in_d2h = wfnsym % 10 wfn_momentum = symm.basis.linearmole_irrep2momentum(wfnsym) na = len(strsa) nb = len(strsb) hdiag = hdiag.reshape(na,nb) degen_mapping = orbsym.degen_mapping ci0 = [] iroot = 0 wfn_ungerade = wfnsym_in_d2h >= 4 a_ungerade = airreps_d2h >= 4 b_ungerade = birreps_d2h >= 4 sym_allowed = a_ungerade[:,None] == b_ungerade ^ wfn_ungerade # total angular momentum == wfn_momentum sym_allowed &= a_ls[:,None] == wfn_momentum - b_ls if neleca == nelecb and na == nb: idx = np.arange(na) sym_allowed[idx[:,None] < idx] = False idx_a, idx_b = np.where(sym_allowed) for k in hdiag[idx_a,idx_b].argsort(): addra, addrb = idx_a[k], idx_b[k] ca = _cyl_sym_csf2civec(strsa, addra, orbsym, degen_mapping) cb = _cyl_sym_csf2civec(strsb, addrb, orbsym, degen_mapping) if wfnsym in (0, 1, 4, 5): addra1, sign_a = _sv_associated_det(strsa[addra], degen_mapping) addrb1, sign_b = _sv_associated_det(strsb[addrb], degen_mapping) if wfnsym in (1, 4) and addra == addra1 and addrb == addrb1: # Remove the A1 repr from initial guess. # The product of two E reprs can produce A1, A2 and another E repr. # addra == addra1 and addrb == addrb1 can be found in the A1 repr. # However, this may also incorrectly remove the A2 repr, see the # explanation in issue #2291. # In this case, the direct_spin1_cyl_sym solver can be used to # solve A2. See example mcscf/18-o2_spatial_spin_symmetry.py continue x = ca[:,None] * cb # If (E+) and (E-) are associated determinants # (E+)(E-') + (E-)(E+') => A1 # (E+)(E-') - (E-)(E+') => A2 if addra != addra1: ca = _cyl_sym_csf2civec(strsa, addra1, orbsym, degen_mapping) if addrb != addrb1: cb = _cyl_sym_csf2civec(strsb, addrb1, orbsym, degen_mapping) if wfnsym in (0, 5): # A1g, A1u x += sign_a * sign_b * ca[:,None] * cb #assert (sign_a*sign_b==1 and x.imag==0) or (sign_a*sign_b==-1 and x.real==0) elif wfnsym in (1, 4): # A2g, A2u x -= sign_a * sign_b * ca[:,None] * cb #assert (sign_a*sign_b==1 and x.real==0) or (sign_a*sign_b==-1 and x.imag==0) if np.linalg.norm(x.real) > 1e-6: x = x.real.copy() else: x = x.imag.copy() elif wfn_momentum > 0: x = ca.real[:,None] * cb.real x-= ca.imag[:,None] * cb.imag else: x = ca.imag[:,None] * cb.real x+= ca.real[:,None] * cb.imag norm = np.linalg.norm(x) if norm < 1e-3: continue x *= 1./norm ci0.append(x.ravel().view(direct_spin1.FCIvector)) iroot += 1 if iroot >= nroots: break if len(ci0) == 0: raise lib.exceptions.WfnSymmetryError( f'Initial guess for symmetry {wfnsym} not found') return ci0
def _cyl_sym_csf2civec(strs, addr, orbsym, degen_mapping): '''For orbital basis rotation from E(+/-) basis to Ex/Ey basis, mimic the CI transformation addons.transform_ci(civec, (0, nelec), u) ''' norb = orbsym.size one_particle_strs = np.asarray([1 << i for i in range(norb)]) occ_masks = (strs[:,None] & one_particle_strs) != 0 na = strs.size occ_idx_all_strs = np.where(occ_masks)[1].reshape(na,-1) u = _cyl_sym_orbital_rotation(orbsym, degen_mapping) ui = u[occ_masks[addr]].T.copy() minors = ui[occ_idx_all_strs] civec = np.linalg.det(minors) return civec def _cyl_sym_orbital_rotation(orbsym, degen_mapping): '''Rotation to transform (E+)/(E-) basis to Ex/Ey basis |Ex/Ey> = |E(+/-)> * u ''' norb = orbsym.size u = np.zeros((norb, norb), dtype=np.complex128) sqrth = .5**.5 sqrthi = sqrth * 1j for i, j in enumerate(degen_mapping): if i == j: # 1d irrep if orbsym[i] in (1, 4): # A2g, A2u u[i,i] = 1j else: u[i,i] = 1 elif orbsym[i] % 10 in (0, 2, 5, 7): # Ex, E(+) u[j,j] = sqrthi u[i,j] = sqrthi u[j,i] = sqrth u[i,i] = -sqrth return u def _sv_associated_det(ci_str, degen_mapping): '''Associated determinant for the sigma_v operation''' ci_str1 = 0 nelec = 0 sign = 1 for i, j in enumerate(degen_mapping): if ci_str & (1 << i) > 0: if i > j and ci_str & (1 << j) > 0: # Ex, Ey orbitals swapped sign = -sign ci_str1 |= 1 << j nelec += 1 return cistring.str2addr(degen_mapping.size, nelec, ci_str1), sign def _strs_angular_momentum(strs, orbsym): # angular momentum for each orbital orb_l = (orbsym // 10) * 2 e1_mask = np.isin(orbsym % 10, (2, 3, 6, 7)) orb_l[e1_mask] += 1 ey_mask = np.isin(orbsym % 10, (1, 3, 4, 6)) orb_l[ey_mask] *= -1 # total angular for each determinant (CSF) ls = np.zeros(len(strs), dtype=int) if isinstance(strs, cistring.OIndexList): nocc = strs.shape[1] for i in range(nocc): ls += orb_l[strs[:,i]] else: for i, l in enumerate(orb_l): ls[np.bitwise_and(strs, 1 << i) > 0] += l return ls
[docs] def reorder_eri(eri, norb, orbsym): if orbsym is None: return [eri], np.arange(norb), np.zeros(norb,dtype=np.int32) # % 10 to map irrep IDs of Dooh or Coov, etc. to irreps of D2h, C2v orbsym = np.asarray(orbsym) % 10 # irrep of (ij| pair trilirrep = (orbsym[:,None] ^ orbsym)[np.tril_indices(norb)] # and the number of occurrences for each irrep dimirrep = np.asarray(np.bincount(trilirrep), dtype=np.int32) # we sort the irreps of (ij| pair, to group the pairs which have same irreps # "order" is irrep-id-sorted index. The (ij| paired is ordered that the # pair-id given by order[0] comes first in the sorted pair # "rank" is a sorted "order". Given nth (ij| pair, it returns the place(rank) # of the sorted pair old_eri_irrep = np.asarray(trilirrep, dtype=np.int32) rank_in_irrep = np.empty_like(old_eri_irrep) eri_irs = [np.zeros((0,0))] * TOTIRREPS for ir, nnorb in enumerate(dimirrep): idx = np.asarray(np.where(trilirrep == ir)[0], dtype=np.int32) rank_in_irrep[idx] = np.arange(nnorb, dtype=np.int32) eri_ir = lib.take_2d(eri, idx, idx) # Drop small integrals which may break symmetry? #eri_ir[abs(eri_ir) < 1e-13] = 0 eri_irs[ir] = eri_ir return eri_irs, rank_in_irrep, old_eri_irrep
[docs] def argsort_strs_by_irrep(strs, orbsym): airreps = _gen_strs_irrep(strs, orbsym) aidx = [np.zeros(0,dtype=np.int32)] * TOTIRREPS for ir in range(TOTIRREPS): aidx[ir] = np.where(airreps == ir)[0] return aidx
[docs] def gen_str_irrep(strs, orbsym, link_index, rank_eri, irrep_eri): aidx = argsort_strs_by_irrep(strs, orbsym) na = len(strs) rank = np.zeros(na, dtype=np.int32) for idx in aidx: if idx.size > 0: rank[idx] = np.arange(idx.size, dtype=np.int32) link_index = link_index.copy() link_index[:,:,2] = rank[link_index[:,:,2]] link_index[:,:,1] = irrep_eri[link_index[:,:,0]] link_index[:,:,0] = rank_eri[link_index[:,:,0]] link_index = link_index.take(np.hstack(aidx), axis=0) return aidx, link_index
def _guess_wfnsym_cyl_sym(civec, strsa, strsb, orbsym): degen_mapping = orbsym.degen_mapping idx = abs(civec).argmax() na = strsa.size nb = strsb.size addra = idx // nb addrb = idx % nb addra1, sign_a = _sv_associated_det(strsa[addra], degen_mapping) addrb1, sign_b = _sv_associated_det(strsb[addrb], degen_mapping) addra, addra1 = min(addra,addra1), max(addra,addra1) addrb, addrb1 = min(addrb,addrb1), max(addrb,addrb1) ca = ca1 = _cyl_sym_csf2civec(strsa, addra, orbsym, degen_mapping) cb = cb1 = _cyl_sym_csf2civec(strsb, addrb, orbsym, degen_mapping) if addra != addra1: ca1 = _cyl_sym_csf2civec(strsa, addra1, orbsym, degen_mapping) if addrb != addrb1: cb1 = _cyl_sym_csf2civec(strsb, addrb1, orbsym, degen_mapping) ua = np.stack([ca, ca1]) ub = np.stack([cb, cb1]) # civec is in the Ex/Ey basis. Transform the largest coefficient to # (E+)/(E-) basis. c_max = ua.conj().dot(civec.reshape(na,nb)).dot(ub.conj().T) airreps_d2h = _gen_strs_irrep(strsa[[addra,addra1]], orbsym) birreps_d2h = _gen_strs_irrep(strsb[[addrb,addrb1]], orbsym) a_ls = _strs_angular_momentum(strsa[[addra,addra1]], orbsym) b_ls = _strs_angular_momentum(strsb[[addrb,addrb1]], orbsym) a_ungerade = airreps_d2h >= 4 b_ungerade = birreps_d2h >= 4 idx = abs(c_max).argmax() idx_a, idx_b = idx // 2, idx % 2 wfn_ungerade = a_ungerade[idx_a] ^ b_ungerade[idx_b] wfn_momentum = a_ls[idx_a] + b_ls[idx_b] if wfn_momentum == 0: # For A1g and A1u, CI coefficient and its sigma_v associated one have # the same sign if (sign_a*sign_b * c_max[0,0].real * c_max[1,1].real > 1e-4 or sign_a*sign_b * c_max[0,0].imag * c_max[1,1].imag > 1e-4): # A1 if wfn_ungerade: wfnsym = 5 else: wfnsym = 0 elif (sign_a*sign_b * c_max[0,0].real * c_max[1,1].real < -1e-4 or sign_a*sign_b * c_max[0,0].imag * c_max[1,1].imag < -1e-4): # A2 # For A2g and A2u, CI coefficient and its sigma_v associated one # have opposite signs if wfn_ungerade: wfnsym = 4 else: wfnsym = 1 elif abs(c_max[0,1] - c_max[1,0]) < 1e-4: # Off-diagonal terms only # (E+)(E-') + (E-)(E+') => A1 if wfn_ungerade: wfnsym = 5 else: wfnsym = 0 elif abs(c_max[0,1] + c_max[1,0]) < 1e-4: # (E+)(E-') - (E-)(E+') => A2 if wfn_ungerade: wfnsym = 4 else: wfnsym = 1 else: raise RuntimeError('Symmetry broken wavefunction') elif wfn_momentum % 2 == 1: if abs(c_max[idx_a,idx_b].real) > 1e-6: # Ex if wfn_ungerade: wfnsym = 7 else: wfnsym = 2 else: # Ey if wfn_ungerade: wfnsym = 6 else: wfnsym = 3 else: if abs(c_max[idx_a,idx_b].real) > 1e-6: # Ex if wfn_ungerade: wfnsym = 5 else: wfnsym = 0 else: # Ey if wfn_ungerade: wfnsym = 4 else: wfnsym = 1 wfnsym += (abs(wfn_momentum) // 2) * 10 return wfnsym
[docs] def guess_wfnsym(solver, norb, nelec, fcivec=None, orbsym=None, wfnsym=None, **kwargs): ''' Guess point group symmetry of the FCI wavefunction. If fcivec is given, the symmetry of fcivec is used. Otherwise the symmetry is same to the HF determinant. ''' if orbsym is None: orbsym = solver.orbsym verbose = kwargs.get('verbose', None) log = logger.new_logger(solver, verbose) neleca, nelecb = nelec = _unpack_nelec(nelec, solver.spin) groupname = getattr(solver.mol, 'groupname', None) if fcivec is None: # guess wfnsym if initial guess is not given wfnsym = _id_wfnsym(solver, norb, nelec, orbsym, wfnsym) log.debug('Guessing CI wfn symmetry = %s', wfnsym) elif wfnsym is None: if groupname in ('Dooh', 'Coov'): strsa = strsb = cistring.gen_strings4orblist(range(norb), neleca) if neleca != nelecb: strsb = cistring.gen_strings4orblist(range(norb), nelecb) if not isinstance(fcivec, np.ndarray) or fcivec.ndim > 2: fcivec = fcivec[0] wfnsym = _guess_wfnsym_cyl_sym(fcivec, strsa, strsb, orbsym) else: wfnsym = addons.guess_wfnsym(fcivec, norb, nelec, orbsym) log.debug('Guessing CI wfn symmetry = %s', wfnsym) else: # verify if the input wfnsym is consistent with the symmetry of fcivec strsa = strsb = cistring.gen_strings4orblist(range(norb), neleca) if neleca != nelecb: strsb = cistring.gen_strings4orblist(range(norb), nelecb) if groupname in ('Dooh', 'Coov'): if not isinstance(fcivec, np.ndarray) or fcivec.ndim > 2: fcivec = fcivec[0] wfnsym1 = _guess_wfnsym_cyl_sym(fcivec, strsa, strsb, orbsym) if wfnsym1 != _id_wfnsym(solver, norb, nelec, orbsym, wfnsym): raise lib.exceptions.WfnSymmetryError( f'Input wfnsym {wfnsym} is not consistent with ' f'fcivec symmetry {wfnsym1}') wfnsym = wfnsym1 else: na, nb = strsa.size, strsb.size orbsym_in_d2h = np.asarray(orbsym) % 10 airreps = np.zeros(na, dtype=np.int32) birreps = np.zeros(nb, dtype=np.int32) for i, ir in enumerate(orbsym_in_d2h): airreps[np.bitwise_and(strsa, 1 << i) > 0] ^= ir birreps[np.bitwise_and(strsb, 1 << i) > 0] ^= ir wfnsym = _id_wfnsym(solver, norb, nelec, orbsym, wfnsym) groupname = getattr(solver.mol, 'groupname', None) mask = airreps[:,None] == (wfnsym % 10) ^ birreps if isinstance(fcivec, np.ndarray) and fcivec.ndim <= 2: fcivec = [fcivec] if all(abs(c.reshape(na, nb)[mask]).max() < 1e-5 for c in fcivec): raise lib.exceptions.WfnSymmetryError( 'Input wfnsym {wfnsym} is not consistent with fcivec coefficients') return wfnsym
[docs] def sym_allowed_indices(nelec, orbsym, wfnsym): '''Indices of symmetry allowed determinants for each irrep''' norb = orbsym.size neleca, nelecb = nelec strsa = strsb = cistring.gen_strings4orblist(range(norb), neleca) aidx = bidx = argsort_strs_by_irrep(strsa, orbsym) if neleca != nelecb: strsb = cistring.gen_strings4orblist(range(norb), nelecb) bidx = argsort_strs_by_irrep(strsb, orbsym) nb = len(strsb) wfnsym_in_d2h = wfnsym % 10 ab_idx = [(aidx[ir][:,None] * nb + bidx[wfnsym_in_d2h ^ ir]).ravel() for ir in range(TOTIRREPS)] return ab_idx
[docs] class FCISolver(direct_spin1.FCISolver): _keys = {'wfnsym', 'sym_allowed_idx'} pspace_size = getattr(__config__, 'fci_direct_spin1_symm_FCI_pspace_size', 400) def __init__(self, mol=None, **kwargs): # wfnsym will be guessed based on initial guess if it is None self.wfnsym = None self.sym_allowed_idx = None direct_spin1.FCISolver.__init__(self, mol, **kwargs)
[docs] def dump_flags(self, verbose=None): direct_spin1.FCISolver.dump_flags(self, verbose) log = logger.new_logger(self, verbose) if isinstance(self.wfnsym, str): log.info('Input CI wfn symmetry = %s', self.wfnsym) elif isinstance(self.wfnsym, (int, np.number)): groupname = getattr(self.mol, 'groupname', None) if groupname is not None: try: log.info('Input CI wfn symmetry = %s', symm.irrep_id2name(groupname, self.wfnsym)) except KeyError: raise RuntimeError('FCISolver cannot find mwfnsym Id %s in group %s. ' 'This might be caused by the projection from ' 'high-symmetry group to D2h symmetry.' % (self.wfnsym, groupname)) else: log.info('CI wfn symmetry = %s', self.wfnsym) return self
absorb_h1e = direct_spin1.FCISolver.absorb_h1e
[docs] def make_hdiag(self, h1e, eri, norb, nelec, compress=False): nelec = _unpack_nelec(nelec, self.spin) hdiag = direct_spin1.make_hdiag(h1e, eri, norb, nelec) # TODO: hdiag should return symmetry allowed elements only. However, # get_init_guess_cyl_sym does not strictly follow the D2h (and subgroup) # symmetry treatments. The diagonal of entire Hamiltonian is required. if compress and self.sym_allowed_idx is not None: hdiag = hdiag.ravel()[np.hstack(self.sym_allowed_idx)] return hdiag
[docs] def pspace(self, h1e, eri, norb, nelec, hdiag, np=400): nelec = _unpack_nelec(nelec, self.spin) na = cistring.num_strings(norb, nelec[0]) nb = cistring.num_strings(norb, nelec[1]) s_idx = numpy.hstack(self.sym_allowed_idx) if hdiag.size == s_idx.size: hdiag, hdiag0 = numpy.empty(na*nb), hdiag.ravel() hdiag[:] = 1e9 hdiag[s_idx] = hdiag0 elif not getattr(self.mol, 'groupname', None) in ('Dooh', 'Coov'): # Screen symmetry forbidden elements hdiag, hdiag0 = numpy.empty(na*nb), hdiag.ravel() hdiag[:] = 1e9 hdiag[s_idx] = hdiag0[s_idx] np = min(np, hdiag.size) addr0, h = direct_spin1.pspace(h1e, eri, norb, nelec, hdiag, np) # mapping the address in (na,nb) civec to address in sym-allowed civec addr0_sym_allow = numpy.where(numpy.isin(addr0, s_idx))[0] addr0 = addr0[addr0_sym_allow] s_idx_allowed = numpy.where(numpy.isin(s_idx, addr0))[0] addr1 = s_idx[s_idx_allowed] new_idx = numpy.empty_like(s_idx_allowed) new_idx[addr0.argsort()] = addr1.argsort() addr = s_idx_allowed[new_idx] return addr, h[addr0_sym_allow[:,None],addr0_sym_allow]
[docs] def contract_1e(self, f1e, fcivec, norb, nelec, link_index=None, **kwargs): nelec = direct_spin1._unpack_nelec(nelec) na = cistring.num_strings(norb, nelec[0]) nb = cistring.num_strings(norb, nelec[1]) if fcivec.size != na * nb: fcivec, ci0 = np.zeros(na*nb), fcivec fcivec[np.hstack(self.sym_allowed_idx)] = ci0 return direct_spin1.contract_1e(f1e, fcivec, norb, nelec, link_index, **kwargs)
[docs] def contract_2e(self, eri, fcivec, norb, nelec, link_index=None, orbsym=None, wfnsym=None, **kwargs): if orbsym is None: orbsym = self.orbsym if wfnsym is None: wfnsym = self.wfnsym wfnsym = _id_wfnsym(self, norb, nelec, orbsym, wfnsym) nelec = _unpack_nelec(nelec, self.spin) return contract_2e(eri, fcivec, norb, nelec, link_index, orbsym, wfnsym, **kwargs)
[docs] def contract_ss(self, fcivec, norb, nelec): nelec = direct_spin1._unpack_nelec(nelec) na = cistring.num_strings(norb, nelec[0]) nb = cistring.num_strings(norb, nelec[1]) if fcivec.size == na*nb: return contract_ss(fcivec, norb, nelec) fcivec, ci0 = np.zeros(na*nb), fcivec s_idx = np.hstack(self.sym_allowed_idx) fcivec[s_idx] = ci0 ci1 = contract_ss(fcivec, norb, nelec) return ci1.ravel()[s_idx]
[docs] def get_init_guess(self, norb, nelec, nroots, hdiag, orbsym=None, wfnsym=None): if orbsym is None: orbsym = self.orbsym if wfnsym is None: wfnsym = _id_wfnsym(self, norb, nelec, orbsym, self.wfnsym) s_idx = np.hstack(self.sym_allowed_idx) if getattr(self.mol, 'groupname', None) in ('Dooh', 'Coov'): ci0 = get_init_guess_cyl_sym( norb, nelec, nroots, hdiag, orbsym, wfnsym) return [x[s_idx] for x in ci0] else: return get_init_guess(norb, nelec, nroots, hdiag.ravel()[s_idx], orbsym, wfnsym)
guess_wfnsym = guess_wfnsym
[docs] def kernel(self, h1e, eri, norb, nelec, ci0=None, tol=None, lindep=None, max_cycle=None, max_space=None, nroots=None, davidson_only=None, pspace_size=None, orbsym=None, wfnsym=None, ecore=0, **kwargs): if nroots is None: nroots = self.nroots if orbsym is None: orbsym = self.orbsym if wfnsym is None: wfnsym = self.wfnsym if self.verbose >= logger.WARN: if 'verbose' not in kwargs: kwargs['verbose'] = self.verbose self.check_sanity() self.norb = norb self.nelec = nelec = _unpack_nelec(nelec, self.spin) link_index = direct_spin1._unpack(norb, nelec, None) if getattr(self.mol, 'groupname', None) in ('Dooh', 'Coov'): if not hasattr(orbsym, 'degen_mapping'): degen_mapping = map_degeneracy(h1e.diagonal(), orbsym) orbsym = lib.tag_array(orbsym, degen_mapping=degen_mapping) if davidson_only is None: davidson_only = True if not _validate_degen_mapping(orbsym.degen_mapping, norb): raise lib.exceptions.PointGroupSymmetryError( 'Incomplete 2D-irrep orbitals for cylindrical symmetry.\n' f'orbsym = {orbsym}. ' f'Retry {self.__class__} with D2h subgroup symmetry.') wfnsym_ir = self.guess_wfnsym(norb, nelec, ci0, orbsym, wfnsym, **kwargs) self.sym_allowed_idx = sym_allowed_indices(nelec, orbsym, wfnsym_ir) s_idx = np.hstack(self.sym_allowed_idx) self.orbsym = orbsym logger.debug(self, 'Num symmetry allowed elements %d', sum([x.size for x in self.sym_allowed_idx])) if s_idx.size == 0: raise lib.exceptions.WfnSymmetryError( f'Symmetry allowed determinants not found for wfnsym {wfnsym}') if wfnsym_ir > 7: # Symmetry broken for Dooh and Coov groups is often observed. # A larger max_space is helpful to reduce the error. Also it is # hard to converge to high precision. if max_space is None and self.max_space == FCISolver.max_space: max_space = 20 + 7 * nroots if tol is None and self.conv_tol == FCISolver.conv_tol: tol = 1e-7 if ci0 is None and getattr(self.mol, 'groupname', None) in ('Dooh', 'Coov'): # self.hdiag returns stripped H_diag (for D2h symmetry). # Different convention of symmetry representations were used in # get_init_guess_cyl_sym (which follows direct_spin1_cyl_sym.py). # Some symmetry forbidden elements for D2h are needed in # get_init_guess_cyl_sym function. Thus the entire hdiag is computed. hdiag = self.make_hdiag(h1e, eri, norb, nelec, compress=False) ci0 = self.get_init_guess(norb, nelec, nroots, hdiag, orbsym, wfnsym_ir) with lib.temporary_env(self, wfnsym=wfnsym_ir): e, c = direct_spin1.kernel_ms1( self, h1e, eri, norb, nelec, ci0, link_index, tol, lindep, max_cycle, max_space, nroots, davidson_only, pspace_size, ecore=ecore, **kwargs) na = link_index[0].shape[0] nb = link_index[1].shape[0] if nroots > 1: c, c_raw = [], c for vec in c_raw: c1 = np.zeros(na*nb) c1[s_idx] = vec.T c.append(c1.reshape(na, nb).view(direct_spin1.FCIvector)) else: c1 = np.zeros(na*nb) c1[s_idx] = c c = c1.reshape(na, nb).view(direct_spin1.FCIvector) self.eci, self.ci = e, c return e, c
FCI = FCISolver