Source code for vayesta.core.fragmentation.fragmentation

import contextlib
import numpy as np
import scipy
import scipy.linalg
import re

from vayesta.core.util import dot, fix_orbital_sign, time_string, timer
from vayesta.core.fragmentation import helper


[docs]def check_orthonormal(log, mo_coeff, ovlp, mo_name="orbital", tol=1e-7): """Check orthonormality of mo_coeff. Supports both RHF and UHF. """ # RHF if np.ndim(mo_coeff[0]) == 1: err = dot(mo_coeff.T, ovlp, mo_coeff) - np.eye(mo_coeff.shape[-1]) l2 = np.linalg.norm(err) linf = abs(err).max() if max(l2, linf) > tol: log.error("Orthogonality error of %ss: L(2)= %.2e L(inf)= %.2e !", mo_name, l2, linf) else: log.debugv("Orthogonality error of %ss: L(2)= %.2e L(inf)= %.2e", mo_name, l2, linf) return l2, linf # UHF l2a, linfa = check_orthonormal(log, mo_coeff[0], ovlp, mo_name="alpha-%s" % mo_name, tol=tol) l2b, linfb = check_orthonormal(log, mo_coeff[1], ovlp, mo_name="beta-%s" % mo_name, tol=tol) return (l2a, l2b), (linfa, linfb)
[docs]class Fragmentation: """Fragmentation for a quantum embedding method class.""" name = "<not set>" def __init__(self, emb, add_symmetric=True, log=None): self.emb = emb self.add_symmetric = add_symmetric self.log = log or emb.log self.log.info("%s Fragmentation" % self.name) self.log.info("%s--------------" % (len(self.name) * "-")) self.ovlp = self.mf.get_ovlp() # Secondary fragment state: self.secfrag_register = [] # Rotational symmetry state: self.sym_register = [] # -- Output: self.coeff = None self.labels = None # Generated fragments: self.fragments = []
[docs] def kernel(self): self.coeff = self.get_coeff() self.labels = self.get_labels()
# --- As contextmanager: def __enter__(self): self.log.changeIndentLevel(1) self._time0 = timer() self.kernel() return self def __exit__(self, exc_type, exc_value, exc_traceback): if exc_type is not None: return if self.add_symmetric: # Rotational + inversion symmetries are now done within own context # Translation symmetry are done in the fragmentation context (here): translation = self.emb.symmetry.translation if translation is not None: fragments_sym = self.emb.create_transsym_fragments(translation, fragments=self.fragments) self.log.info( "Adding %d translationally-symmetry related fragments from %d base fragments", len(fragments_sym), len(self.fragments), ) self.fragments.extend(fragments_sym) # Add fragments to embedding class self.log.debug("Adding %d fragments to embedding class", len(self.fragments)) self.emb.fragments.extend(self.fragments) # Check if fragmentation is (occupied) complete and orthonormal: orth = self.emb.has_orthonormal_fragmentation() comp = self.emb.has_complete_fragmentation() occcomp = self.emb.has_complete_occupied_fragmentation() self.log.info( "Fragmentation: orthogonal= %r, occupied-complete= %r, virtual-complete= %r", self.emb.has_orthonormal_fragmentation(), self.emb.has_complete_occupied_fragmentation(), self.emb.has_complete_virtual_fragmentation(), ) self.log.timing("Time for %s fragmentation: %s", self.name, time_string(timer() - self._time0)) del self._time0 self.log.changeIndentLevel(-1) # --- Adding fragments:
[docs] def add_atomic_fragment(self, atoms, orbital_filter=None, name=None, **kwargs): """Create a fragment of one or multiple atoms, which will be solved by the embedding method. Parameters ---------- atoms: int, str, list[int], or list[str] Atom indices or symbols which should be included in the fragment. name: str, optional Name for the fragment. If None, a name is automatically generated from the chosen atoms. Default: None. **kwargs: Additional keyword arguments are passed through to the fragment constructor. Returns ------- Fragment: Fragment object. """ atom_indices, atom_symbols = self.get_atom_indices_symbols(atoms) name, indices = self.get_atomic_fragment_indices(atoms, orbital_filter=orbital_filter, name=name) return self._create_fragment(indices, name, atoms=atom_indices, **kwargs)
[docs] def add_atomshell_fragment(self, atoms, shells, **kwargs): if isinstance(shells, (int, np.integer)): shells = [shells] orbitals = [] atom_indices, atom_symbols = self.get_atom_indices_symbols(atoms) for idx, sym in zip(atom_indices, atom_symbols): for shell in shells: orbitals.append("%d%3s %s" % (idx, sym, shell)) return self.add_orbital_fragment(orbitals, atoms=atom_indices, **kwargs)
[docs] def add_orbital_fragment(self, orbitals, atom_filter=None, name=None, **kwargs): """Create a fragment of one or multiple orbitals, which will be solved by the embedding method. Parameters ---------- orbitals: int, str, list[int], or list[str] Orbital indices or labels which should be included in the fragment. name: str, optional Name for the fragment. If None, a name is automatically generated from the chosen orbitals. Default: None. **kwargs: Additional keyword arguments are passed through to the fragment constructor. Returns ------- Fragment: Fragment object. """ name, indices = self.get_orbital_fragment_indices(orbitals, atom_filter=atom_filter, name=name) return self._create_fragment(indices, name, **kwargs)
[docs] def add_all_atomic_fragments(self, **kwargs): """Create a single fragment for each atom in the system. Parameters ---------- **kwargs: Additional keyword arguments are passed through to each fragment constructor. """ fragments = [] natom = self.emb.kcell.natm if self.emb.kcell is not None else self.emb.mol.natm for atom in range(natom): frag = self.add_atomic_fragment(atom, **kwargs) fragments.append(frag) return fragments
[docs] def add_full_system(self, name="full-system", **kwargs): atoms = list(range(self.mol.natm)) return self.add_atomic_fragment(atoms, name=name, **kwargs)
def _create_fragment(self, indices, name, **kwargs): if len(indices) == 0: raise ValueError("Fragment %s is empty." % name) c_frag = self.get_frag_coeff(indices) c_env = self.get_env_coeff(indices) fid, mpirank = self.emb.register.get_next() frag = self.emb.Fragment(self.emb, fid, name, c_frag, c_env, mpi_rank=mpirank, **kwargs) self.fragments.append(frag) # Log fragment orbitals: self.log.debugv("Fragment %ss:\n%r", self.name, indices) self.log.debug("Fragment %ss of fragment %s:", self.name, name) labels = np.asarray(self.labels)[indices] helper.log_orbitals(self.log.debug, labels) # Secondary fragments: if self.secfrag_register: self.secfrag_register[0].append(frag) # Symmetric fragments: for sym in self.sym_register: sym.append(frag) return frag # --- Rotational symmetry fragments:
[docs] @contextlib.contextmanager def rotational_symmetry(self, order, axis, center=(0, 0, 0), unit="Ang"): if self.secfrag_register: raise NotImplementedError("Rotational symmetries have to be added before adding secondary fragments") self.sym_register.append([]) yield fragments = self.sym_register.pop() fragments_sym = self.emb.create_rotsym_fragments( order, axis, center, fragments=fragments, unit=unit, symbol="R%d" % len(self.sym_register) ) self.log.info("Adding %d rotationally-symmetric fragments", len(fragments_sym)) self.fragments.extend(fragments_sym) # For additional (nested) symmetries: for sym in self.sym_register: sym.extend(fragments_sym)
[docs] @contextlib.contextmanager def inversion_symmetry(self, center=(0, 0, 0), unit="Ang"): if self.secfrag_register: raise NotImplementedError("Symmetries have to be added before adding secondary fragments") self.sym_register.append([]) yield fragments = self.sym_register.pop() fragments_sym = self.emb.create_invsym_fragments(center, fragments=fragments, unit=unit, symbol="I") self.log.info("Adding %d inversion-symmetric fragments", len(fragments_sym)) self.fragments.extend(fragments_sym) # For additional (nested) symmetries: for sym in self.sym_register: sym.extend(fragments_sym)
[docs] @contextlib.contextmanager def mirror_symmetry(self, axis, center=(0, 0, 0), unit="Ang"): if self.secfrag_register: raise NotImplementedError("Symmetries have to be added before adding secondary fragments") self.sym_register.append([]) yield fragments = self.sym_register.pop() fragments_sym = self.emb.create_mirrorsym_fragments( axis, center=center, fragments=fragments, unit=unit, symbol="M" ) self.log.info("Adding %d mirror-symmetric fragments", len(fragments_sym)) self.fragments.extend(fragments_sym) # For additional (nested) symmetries: for sym in self.sym_register: sym.extend(fragments_sym)
# --- Secondary fragments:
[docs] @contextlib.contextmanager def secondary_fragments(self, bno_threshold=None, bno_threshold_factor=0.1, solver="MP2"): if self.secfrag_register: raise NotImplementedError("Nested secondary fragments") self.secfrag_register.append([]) yield fragments = self.secfrag_register.pop() fragments_sec = self._create_secondary_fragments( fragments, bno_threshold=bno_threshold, bno_threshold_factor=bno_threshold_factor, solver=solver ) self.log.info("Adding %d secondary fragments", len(fragments_sec)) self.fragments.extend(fragments_sec) # If we are already in a symmetry context, the symmetry-related secondary fragments should also be added: for sym in self.sym_register: sym.extend(fragments_sec)
def _create_secondary_fragments(self, fragments, bno_threshold=None, bno_threshold_factor=0.1, solver="MP2"): def _create_fragment(fx, flags=None, **kwargs): if fx.sym_parent is not None: raise NotImplementedError("Secondary fragments need to be added before symmetry-derived fragments") flags = (flags or {}).copy() flags["is_secfrag"] = True fx_copy = fx.copy(solver=solver, flags=flags, **kwargs) fx_copy.flags.bath_parent_fragment_id = fx.id self.log.debugv("Adding secondary fragment: %s", fx_copy) return fx_copy fragments_sec = [] for fx in fragments: bath_opts = fx.opts.bath_options.copy() if bno_threshold is not None: bath_opts["threshold"] = bno_threshold bath_opts.pop("threshold_occ", None) bath_opts.pop("threshold_vir", None) else: if bath_opts.get("threshold", None) is not None: bath_opts["threshold"] *= bno_threshold_factor if bath_opts.get("threshold_occ", None) is not None: bath_opts["threshold_occ"] *= bno_threshold_factor if bath_opts.get("threshold_vir", None) is not None: bath_opts["threshold_vir"] *= bno_threshold_factor frag = _create_fragment(fx, name="%s[x%d|+%s]" % (fx.name, fx.id, solver), bath_options=bath_opts) fragments_sec.append(frag) # Double counting wf_factor = -(fx.opts.wf_factor or 1) frag = _create_fragment( fx, name="%s[x%d|-%s]" % (fx.name, fx.id, solver), wf_factor=wf_factor, flags=dict(is_envelop=False) ) fragments_sec.append(frag) fx.flags.is_envelop = False return fragments_sec # --- For convenience: @property def mf(self): return self.emb.mf @property def mol(self): return self.mf.mol @property def nao(self): return self.mol.nao_nr() @property def nmo(self): return self.mo_coeff.shape[-1]
[docs] def get_ovlp(self): return self.ovlp
@property def mo_coeff(self): return self.mf.mo_coeff @property def mo_occ(self): return self.mf.mo_occ # --- These need to be implemented
[docs] def get_coeff(self): """Abstract method.""" raise NotImplementedError()
[docs] def get_labels(self): """Abstract method.""" raise NotImplementedError()
[docs] def search_labels(self, labels): """Abstract method.""" raise NotImplementedError()
# ---
[docs] def get_atoms(self): """Get the base atom for each fragment orbital.""" return [l[0] for l in self.labels]
[docs] def symmetric_orth(self, mo_coeff, ovlp=None, tol=1e-15): """Use as mo_coeff = np.dot(mo_coeff, x) to get orthonormal orbitals.""" if ovlp is None: ovlp = self.get_ovlp() m = dot(mo_coeff.T, ovlp, mo_coeff) e, v = scipy.linalg.eigh(m) e_min = e.min() keep = e >= tol e, v = e[keep], v[:, keep] x = dot(v / np.sqrt(e), v.T) x = fix_orbital_sign(x)[0] return x, e_min
# def check_orth(self, mo_coeff, mo_name=None, tol=1e-7): # """Check orthonormality of mo_coeff.""" # err = dot(mo_coeff.T, self.get_ovlp(), mo_coeff) - np.eye(mo_coeff.shape[-1]) # l2 = np.linalg.norm(err) # linf = abs(err).max() # if mo_name is None: mo_name = self.name # if max(l2, linf) > tol: # self.log.error("Orthogonality error of %ss: L(2)= %.2e L(inf)= %.2e !", mo_name, l2, linf) # else: # self.log.debugv("Orthogonality error of %ss: L(2)= %.2e L(inf)= %.2e", mo_name, l2, linf) # return l2, linf
[docs] def check_orthonormal(self, mo_coeff, mo_name=None, tol=1e-7): if mo_name is None: mo_name = self.name return check_orthonormal(self.log, mo_coeff, self.get_ovlp(), mo_name=mo_name, tol=tol)
[docs] def get_atom_indices_symbols(self, atoms): """Convert a list of integer or strings to atom indices and symbols.""" if np.ndim(atoms) == 0: atoms = [atoms] if isinstance(atoms[0], (int, np.integer)): atom_indices = atoms atom_symbols = [self.mol.atom_symbol(atm) for atm in atoms] return atom_indices, atom_symbols if isinstance(atoms[0], str): atom_symbols = atoms all_atom_symbols = [self.mol.atom_symbol(atm) for atm in range(self.mol.natm)] for sym in atom_symbols: if sym not in all_atom_symbols: raise ValueError("Cannot find atom with symbol %s in system." % sym) atom_indices = np.nonzero(np.isin(all_atom_symbols, atom_symbols))[0] return atom_indices, atom_symbols raise ValueError("A list of integers or string is required! atoms= %r" % atoms)
[docs] def get_atomic_fragment_indices(self, atoms, orbital_filter=None, name=None): """Get fragment indices for one atom or a set of atoms. Parameters ---------- atoms: list or int or str List of atom IDs or labels. For a single atom, a single integer or string can be passed as well. orbital_filter: list, optional Additionally restrict fragment orbitals to a specific orbital type (e.g. '2p'). Default: None. name: str, optional Name for fragment. Returns ------- name: str Name of fragment. indices: list List of fragment orbitals indices, with coefficients corresponding to `self.coeff[:,indices]`. """ atom_indices, atom_symbols = self.get_atom_indices_symbols(atoms) if name is None: name = "/".join(atom_symbols) self.log.debugv("Atom indices of fragment %s: %r", name, atom_indices) self.log.debugv("Atom symbols of fragment %s: %r", name, atom_symbols) # Indices of IAOs based at atoms indices = np.nonzero(np.isin(self.get_atoms(), atom_indices))[0] # Filter orbital types if orbital_filter is not None: if isinstance(orbital_filter, list): orbital_filter = [re.escape(x) for x in orbital_filter] elif isinstance(orbital_filter, str): orbital_filter = re.escape(orbital_filter) keep = self.search_labels(orbital_filter) indices = [i for i in indices if i in keep] return name, indices
[docs] def get_orbital_indices_labels(self, orbitals): """Convert a list of integer or strings to orbital indices and labels.""" if np.ndim(orbitals) == 0: orbitals = [orbitals] if isinstance(orbitals[0], (int, np.integer)): orbital_indices = orbitals orbital_labels = (np.asarray(self.labels, dtype=object)[orbitals]).tolist() orbital_labels = [("%d%3s %s%-s" % tuple(l)).strip() for l in orbital_labels] return orbital_indices, orbital_labels if isinstance(orbitals[0], str): orbital_labels = orbitals # Check labels for l in orbital_labels: if len(self.search_labels(l)) == 0: raise ValueError("Cannot find orbital with label %s in system." % l) orbital_indices = self.search_labels(orbital_labels) return orbital_indices, orbital_labels raise ValueError("A list of integers or string is required! orbitals= %r" % orbitals)
[docs] def get_orbital_fragment_indices(self, orbitals, atom_filter=None, name=None): if atom_filter is not None: raise NotImplementedError() indices, orbital_labels = self.get_orbital_indices_labels(orbitals) if name is None: name = "/".join(orbital_labels) self.log.debugv("Orbital indices of fragment %s: %r", name, indices) self.log.debugv("Orbital labels of fragment %s: %r", name, orbital_labels) return name, indices
[docs] def get_frag_coeff(self, indices): """Get fragment coefficients for a given set of orbital indices.""" c_frag = self.coeff[:, indices].copy() return c_frag
[docs] def get_env_coeff(self, indices): """Get environment coefficients for a given set of orbital indices.""" env = np.ones((self.coeff.shape[-1]), dtype=bool) env[indices] = False c_env = self.coeff[:, env].copy() return c_env