Ejemplo n.º 1
0
    def __init__(
            self,
            soldesc: Union[str, Tuple[AtomZsType, AtomPosType]],
            alattice: torch.Tensor,
            basis: Union[str, List[CGTOBasis], List[str],
                         List[List[CGTOBasis]]],
            *,
            grid: Union[int, str] = "sg3",
            spin: Optional[ZType] = None,
            lattsum_opt: Optional[Union[PBCIntOption, Dict]] = None,
            dtype: torch.dtype = torch.float64,
            device: torch.device = torch.device('cpu'),
    ):
        self._dtype = dtype
        self._device = device
        self._grid_inp = grid
        self._grid: Optional[BaseGrid] = None
        charge = 0  # we can't have charged solids for now

        # get the AtomCGTOBasis & the hamiltonian
        # atomzs: (natoms,) dtype: torch.int or dtype for floating point
        # atompos: (natoms, ndim)
        atomzs, atompos = parse_moldesc(soldesc, dtype, device)
        allbases = _parse_basis(atomzs, basis)  # list of list of CGTOBasis
        atombases = [
            AtomCGTOBasis(atomz=atz, bases=bas, pos=atpos)
            for (atz, bas, atpos) in zip(atomzs, allbases, atompos)
        ]
        self._atombases = atombases
        self._atompos = atompos  # (natoms, ndim)
        self._atomzs = atomzs  # (natoms,) int-type
        nelecs_tot: torch.Tensor = torch.sum(atomzs)

        # get the number of electrons and spin and orbital weights
        nelecs, spin, frac_mode = _get_nelecs_spin(nelecs_tot, spin, charge)
        assert not frac_mode, "Fractional Z mode for pbc is not supported"
        _orb_weights, _orb_weights_u, _orb_weights_d = _get_orb_weights(
            nelecs, spin, frac_mode, dtype, device)

        # initialize cache
        self._cache = Cache()

        # save the system's properties
        self._spin = spin
        self._charge = charge
        self._numel = nelecs
        self._orb_weights = _orb_weights
        self._orb_weights_u = _orb_weights_u
        self._orb_weights_d = _orb_weights_d
        self._lattice = Lattice(alattice)
        self._lattsum_opt = PBCIntOption.get_default(lattsum_opt)
Ejemplo n.º 2
0
    def __init__(self,
                 atombases: List[AtomCGTOBasis],
                 spherical: bool = True,
                 df: Optional[DensityFitInfo] = None,
                 efield: Optional[torch.Tensor] = None,
                 cache: Optional[Cache] = None) -> None:
        self.atombases = atombases
        self.spherical = spherical
        self.libcint_wrapper = intor.LibcintWrapper(atombases, spherical)
        self.dtype = self.libcint_wrapper.dtype
        self.device = self.libcint_wrapper.device
        if df is None:
            self._df: Optional[DFMol] = None
        else:
            self._df = DFMol(df, wrapper=self.libcint_wrapper)

        self._efield = efield
        if efield is not None:
            assert efield.ndim == 1 and efield.numel() == 3

        self.is_grid_set = False
        self.is_ao_set = False
        self.is_grad_ao_set = False
        self.is_lapl_ao_set = False
        self.xc: Optional[BaseXC] = None
        self.xcfamily = 1
        self.is_built = False

        # initialize cache
        self._cache = cache if cache is not None else Cache.get_dummy()
        self._cache.add_cacheable_params(
            ["overlap", "kinetic", "nuclattr", "efield"])
        if self._df is None:
            self._cache.add_cacheable_params(["elrep"])
Ejemplo n.º 3
0
    def __init__(
            self,
            atombases: List[AtomCGTOBasis],
            latt: Lattice,
            *,
            kpts: Optional[torch.Tensor] = None,
            wkpts: Optional[
                torch.Tensor] = None,  # weights of k-points to get the density
            spherical: bool = True,
            df: Optional[DensityFitInfo] = None,
            lattsum_opt: Optional[Union[intor.PBCIntOption, Dict]] = None,
            cache: Optional[Cache] = None) -> None:
        self._atombases = atombases
        self._spherical = spherical
        self._lattice = latt
        # alpha for the compensating charge
        # TODO: calculate eta properly or put it in lattsum_opt
        self._eta = 0.2
        self._eta = 0.46213127322256375  # temporary to follow pyscf.df
        # lattice sum integral options
        self._lattsum_opt = intor.PBCIntOption.get_default(lattsum_opt)

        self._basiswrapper = intor.LibcintWrapper(atombases,
                                                  spherical=spherical,
                                                  lattice=latt)
        self.dtype = self._basiswrapper.dtype
        self.cdtype = get_complex_dtype(self.dtype)
        self.device = self._basiswrapper.device

        # set the default k-points and their weights
        self._kpts = kpts if kpts is not None else \
            torch.zeros((1, 3), dtype=self.dtype, device=self.device)
        nkpts = self._kpts.shape[0]
        # default weights are just 1/nkpts (nkpts,)
        self._wkpts = wkpts if wkpts is not None else \
            torch.ones((nkpts,), dtype=self.dtype, device=self.device) / nkpts

        assert self._wkpts.shape[0] == self._kpts.shape[0]
        assert self._wkpts.ndim == 1
        assert self._kpts.ndim == 2

        # initialize cache
        self._cache = cache if cache is not None else Cache.get_dummy()
        self._cache.add_cacheable_params(["overlap", "kinetic", "nuclattr"])

        if df is None:
            self._df: Optional[BaseDF] = None
        else:
            self._df = DFPBC(dfinfo=df,
                             wrapper=self._basiswrapper,
                             kpts=self._kpts,
                             wkpts=self._wkpts,
                             eta=self._eta,
                             lattsum_opt=self._lattsum_opt,
                             cache=self._cache.add_prefix("df"))

        self._is_built = False
Ejemplo n.º 4
0
    def __init__(self,
                 dfinfo: DensityFitInfo,
                 wrapper: intor.LibcintWrapper,
                 kpts: torch.Tensor,
                 wkpts: torch.Tensor,
                 eta: float,
                 lattsum_opt: intor.PBCIntOption,
                 *,
                 cache: Optional[Cache] = None):
        self._dfinfo = dfinfo
        self._wrapper = wrapper
        self._eta = eta
        self._kpts = kpts
        self._wkpts = wkpts  # weights of each k-points
        self._lattsum_opt = lattsum_opt
        self.dtype = wrapper.dtype
        self.device = wrapper.device
        assert wrapper.lattice is not None
        self._lattice = wrapper.lattice
        self._is_built = False

        # set up cache
        self._cache = cache if cache is not None else Cache.get_dummy()
        self._cache.add_cacheable_params(["j2c", "j3c", "el_mat"])
Ejemplo n.º 5
0
    def __init__(self,
                 moldesc: Union[str, Tuple[AtomZsType, AtomPosType]],
                 basis: BasisInpType,
                 *,
                 orthogonalize_basis: bool = True,
                 ao_parameterizer: str = "qr",

                 grid: Union[int, str] = "sg3",
                 spin: Optional[ZType] = None,
                 charge: ZType = 0,
                 orb_weights: Optional[SpinParam[torch.Tensor]] = None,
                 efield: Union[torch.Tensor, Tuple[torch.Tensor, ...], None] = None,
                 vext: Optional[torch.Tensor] = None,
                 dtype: torch.dtype = torch.float64,
                 device: torch.device = torch.device('cpu'),
                 ):
        self._dtype = dtype
        self._device = device
        self._grid_inp = grid
        self._basis_inp = basis
        self._grid: Optional[BaseGrid] = None
        self._vext = vext

        # make efield a tuple
        self._efield = _normalize_efield(efield)
        self._preproc_efield = _preprocess_efield(self._efield)

        # initialize cache
        self._cache = Cache()

        # get the AtomCGTOBasis & the hamiltonian
        # atomzs: (natoms,) dtype: torch.int or dtype for floating point
        # atompos: (natoms, ndim)
        atomzs, atompos = parse_moldesc(
            moldesc, dtype=dtype, device=device)
        atomzs_int = torch.round(atomzs).to(torch.int) if atomzs.is_floating_point() else atomzs
        allbases = _parse_basis(atomzs_int, basis)  # list of list of CGTOBasis
        atombases = [AtomCGTOBasis(atomz=atz, bases=bas, pos=atpos)
                     for (atz, bas, atpos) in zip(atomzs, allbases, atompos)]
        self._atombases = atombases
        self._hamilton = HamiltonCGTO(atombases, efield=self._preproc_efield,
                                      vext=self._vext,
                                      cache=self._cache.add_prefix("hamilton"),
                                      orthozer=orthogonalize_basis,
                                      aoparamzer=ao_parameterizer)
        self._orthogonalize_basis = orthogonalize_basis
        self._aoparamzer = ao_parameterizer
        self._atompos = atompos  # (natoms, ndim)
        self._atomzs = atomzs  # (natoms,) int-type or dtype if floating point
        self._atomzs_int = atomzs_int  # (natoms,) int-type rounded from atomzs
        nelecs_tot: torch.Tensor = torch.sum(atomzs)

        # orb_weights is not specified, so determine it from spin and charge
        if orb_weights is None:
            # get the number of electrons and spin
            nelecs, spin, frac_mode = _get_nelecs_spin(nelecs_tot, spin, charge)
            _orb_weights, _orb_weights_u, _orb_weights_d = _get_orb_weights(
                nelecs, spin, frac_mode, dtype, device)

            # save the system's properties
            self._spin = spin
            self._charge = charge
            self._numel = nelecs
            self._orb_weights = _orb_weights
            self._orb_weights_u = _orb_weights_u
            self._orb_weights_d = _orb_weights_d

        # orb_weights is specified, so calculate the spin and charge from it
        else:
            if not isinstance(orb_weights, SpinParam):
                raise TypeError("Specifying orb_weights must be in SpinParam type")
            assert orb_weights.u.ndim == 1
            assert orb_weights.d.ndim == 1
            assert len(orb_weights.u) == len(orb_weights.d)

            # check if it is decreasing
            orb_u_dec = torch.all(orb_weights.u[:-1] - orb_weights.u[1:] > -1e-4)
            orb_d_dec = torch.all(orb_weights.d[:-1] - orb_weights.d[1:] > -1e-4)
            if not (orb_u_dec and orb_d_dec):
                # if not decreasing, the variational might give the wrong results
                warnings.warn("The orbitals should be ordered in a non-increasing manner. "
                              "Otherwise, some calculations might be wrong.")

            utot = orb_weights.u.sum()
            dtot = orb_weights.d.sum()
            self._numel = utot + dtot
            self._spin = utot - dtot
            self._charge = nelecs_tot - self._numel

            self._orb_weights_u = orb_weights.u
            self._orb_weights_d = orb_weights.d
            self._orb_weights = orb_weights.u + orb_weights.d
Ejemplo n.º 6
0
class Mol(BaseSystem):
    """
    Describe the system of an isolated molecule.

    Arguments
    ---------
    * moldesc: str or 2-elements tuple
        Description of the molecule system.
        If string, it can be described like ``"H 1 0 0; H -1 0 0"``.
        If tuple, the first element of the tuple is the Z number of the atoms while
        the second element is the position of the atoms: ``(atomzs, atomposs)``.
    * basis: str, CGTOBasis, list of str, or CGTOBasis
        The string describing the gto basis. If it is a list, then it must have
        the same length as the number of atoms.
    * grid: int
        Describe the grid.
        If it is an integer, then it uses the default grid with specified level
        of accuracy.
    * spin: int, float, torch.Tensor, or None
        The difference between spin-up and spin-down electrons.
        It must be an integer or ``None``.
        If ``None``, then it is ``num_electrons % 2``.
        For floating point atomzs and/or charge, the ``spin`` must be specified.
    * charge: int, float, or torch.Tensor
        The charge of the molecule.
    * orb_weights: SpinParam[torch.Tensor] or None
        Specifiying the orbital occupancy (or weights) directly. If specified,
        ``spin`` and ``charge`` arguments are ignored.
    * vext: tensor or None
        The tensor describing the external potential given in the grid.
        The grid position can be obtained by ``Mol().get_grid().get_rgrid()``.
    * efield: tensor, tuple of tensor, or None
        Uniform electric field of the system. If a tensor, then it is assumed
        to be a constant electric field with the energy is
        calculated based on potential at ``(0, 0, 0)`` is ``0``.
        If a tuple of tensor, then the first element will have a shape of ``(ndim,)``
        representing the constant electric field, second element is the gradient
        of electric field with the last dimension is the direction of the electric
        field, third element is the gradgrad of electric field, etc.
        If ``None``, then the electric field is assumed to be ``0``.
    * dtype: torch.dtype
        The data type of tensors in this class.
    * device: torch.device
        The device on which the tensors in this class are stored.

    * orthogonalize_basis: bool
        (computational option)
        If True, orthogonalize the basis in the hamiltonian calculation.
        If False, then use the raw basis, this might not work with over-complete
        basis.
    * ao_parameterizer: str
        (computational option)
        Specifying the atomic orbital parameterizer.
    """

    def __init__(self,
                 moldesc: Union[str, Tuple[AtomZsType, AtomPosType]],
                 basis: BasisInpType,
                 *,
                 orthogonalize_basis: bool = True,
                 ao_parameterizer: str = "qr",

                 grid: Union[int, str] = "sg3",
                 spin: Optional[ZType] = None,
                 charge: ZType = 0,
                 orb_weights: Optional[SpinParam[torch.Tensor]] = None,
                 efield: Union[torch.Tensor, Tuple[torch.Tensor, ...], None] = None,
                 vext: Optional[torch.Tensor] = None,
                 dtype: torch.dtype = torch.float64,
                 device: torch.device = torch.device('cpu'),
                 ):
        self._dtype = dtype
        self._device = device
        self._grid_inp = grid
        self._basis_inp = basis
        self._grid: Optional[BaseGrid] = None
        self._vext = vext

        # make efield a tuple
        self._efield = _normalize_efield(efield)
        self._preproc_efield = _preprocess_efield(self._efield)

        # initialize cache
        self._cache = Cache()

        # get the AtomCGTOBasis & the hamiltonian
        # atomzs: (natoms,) dtype: torch.int or dtype for floating point
        # atompos: (natoms, ndim)
        atomzs, atompos = parse_moldesc(
            moldesc, dtype=dtype, device=device)
        atomzs_int = torch.round(atomzs).to(torch.int) if atomzs.is_floating_point() else atomzs
        allbases = _parse_basis(atomzs_int, basis)  # list of list of CGTOBasis
        atombases = [AtomCGTOBasis(atomz=atz, bases=bas, pos=atpos)
                     for (atz, bas, atpos) in zip(atomzs, allbases, atompos)]
        self._atombases = atombases
        self._hamilton = HamiltonCGTO(atombases, efield=self._preproc_efield,
                                      vext=self._vext,
                                      cache=self._cache.add_prefix("hamilton"),
                                      orthozer=orthogonalize_basis,
                                      aoparamzer=ao_parameterizer)
        self._orthogonalize_basis = orthogonalize_basis
        self._aoparamzer = ao_parameterizer
        self._atompos = atompos  # (natoms, ndim)
        self._atomzs = atomzs  # (natoms,) int-type or dtype if floating point
        self._atomzs_int = atomzs_int  # (natoms,) int-type rounded from atomzs
        nelecs_tot: torch.Tensor = torch.sum(atomzs)

        # orb_weights is not specified, so determine it from spin and charge
        if orb_weights is None:
            # get the number of electrons and spin
            nelecs, spin, frac_mode = _get_nelecs_spin(nelecs_tot, spin, charge)
            _orb_weights, _orb_weights_u, _orb_weights_d = _get_orb_weights(
                nelecs, spin, frac_mode, dtype, device)

            # save the system's properties
            self._spin = spin
            self._charge = charge
            self._numel = nelecs
            self._orb_weights = _orb_weights
            self._orb_weights_u = _orb_weights_u
            self._orb_weights_d = _orb_weights_d

        # orb_weights is specified, so calculate the spin and charge from it
        else:
            if not isinstance(orb_weights, SpinParam):
                raise TypeError("Specifying orb_weights must be in SpinParam type")
            assert orb_weights.u.ndim == 1
            assert orb_weights.d.ndim == 1
            assert len(orb_weights.u) == len(orb_weights.d)

            # check if it is decreasing
            orb_u_dec = torch.all(orb_weights.u[:-1] - orb_weights.u[1:] > -1e-4)
            orb_d_dec = torch.all(orb_weights.d[:-1] - orb_weights.d[1:] > -1e-4)
            if not (orb_u_dec and orb_d_dec):
                # if not decreasing, the variational might give the wrong results
                warnings.warn("The orbitals should be ordered in a non-increasing manner. "
                              "Otherwise, some calculations might be wrong.")

            utot = orb_weights.u.sum()
            dtot = orb_weights.d.sum()
            self._numel = utot + dtot
            self._spin = utot - dtot
            self._charge = nelecs_tot - self._numel

            self._orb_weights_u = orb_weights.u
            self._orb_weights_d = orb_weights.d
            self._orb_weights = orb_weights.u + orb_weights.d

    def densityfit(self, method: Optional[str] = None,
                   auxbasis: Optional[BasisInpType] = None) -> BaseSystem:
        """
        Indicate that the system's Hamiltonian uses density fit for its integral.

        Arguments
        ---------
        method: Optional[str]
            Density fitting method. Available methods in this class are:

            * ``"coulomb"``: Minimizing the Coulomb inner product, i.e. ``min <p-p_fit|r_12|p-p_fit>``
              Ref: Eichkorn, et al. Chem. Phys. Lett. 240 (1995) 283-290.
              (default)
            * ``"overlap"``: Minimizing the overlap inner product, i.e. min <p-p_fit|p-p_fit>

        auxbasis: Optional[BasisInpType]
            Auxiliary basis for the density fit. If not specified, then it uses
            ``"cc-pvtz-jkfit"``.
        """
        if method is None:
            method = "coulomb"
        if auxbasis is None:
            # TODO: choose the auxbasis properly
            auxbasis = "cc-pvtz-jkfit"

        # get the auxiliary basis
        assert auxbasis is not None
        auxbasis_lst = _parse_basis(self._atomzs_int, auxbasis)
        atomauxbases = [AtomCGTOBasis(atomz=atz, bases=bas, pos=atpos)
                        for (atz, bas, atpos) in zip(self._atomzs, auxbasis_lst, self._atompos)]

        # change the hamiltonian to have density fit
        df = DensityFitInfo(method=method, auxbases=atomauxbases)
        self._hamilton = HamiltonCGTO(self._atombases, df=df, efield=self._preproc_efield,
                                      vext=self._vext,
                                      cache=self._cache.add_prefix("hamilton"),
                                      orthozer=self._orthogonalize_basis,
                                      aoparamzer=self._aoparamzer)
        return self

    def get_hamiltonian(self) -> BaseHamilton:
        """
        Returns the Hamiltonian that corresponds to the system, i.e.
        :class:`~dqc.hamilton.HamiltonCGTO`
        """
        return self._hamilton

    def set_cache(self, fname: str, paramnames: Optional[List[str]] = None) -> BaseSystem:
        """
        Setup the cache of some parameters specified by `paramnames` to be read/written
        on a file.
        If the file exists, then the parameters will not be recomputed, but just
        loaded from the cache instead.
        Cache is usually used for repeated calculations where the cached parameters
        are not changed (e.g. running multiple systems with slightly different environment.)

        Arguments
        ---------
        fname: str
            The file to store the cache.
        paramnames: list of str or None
            List of parameter names to be read/write from the cache.
        """
        all_paramnames = self._cache.get_cacheable_params()
        if paramnames is not None:
            # check the paramnames
            for pname in paramnames:
                if pname not in all_paramnames:
                    msg = "Parameter %s is not cache-able. Cache-able parameters are %s" % \
                        (pname, all_paramnames)
                    raise ValueError(msg)

        self._cache.set(fname, paramnames)

        return self

    def get_orbweight(self, polarized: bool = False) -> Union[torch.Tensor, SpinParam[torch.Tensor]]:
        if not polarized:
            return self._orb_weights
        else:
            return SpinParam(u=self._orb_weights_u, d=self._orb_weights_d)

    def get_nuclei_energy(self) -> torch.Tensor:
        # atomzs: (natoms,)
        # atompos: (natoms, ndim)

        # r12: (natoms, natom)
        r12 = safe_cdist(self._atompos, self._atompos, add_diag_eps=True, diag_inf=True)
        z12 = self._atomzs.unsqueeze(-2) * self._atomzs.unsqueeze(-1)  # (natoms, natoms)
        q_by_r = z12 / r12
        return q_by_r.sum() * 0.5

    def setup_grid(self) -> None:
        grid_inp = self._grid_inp
        logger.log("Constructing the integration grid")
        self._grid = get_predefined_grid(self._grid_inp, self._atomzs_int, self._atompos,
                                         dtype=self._dtype, device=self._device)
        logger.log("Constructing the integration grid: done")

        # #        0,  1,  2,  3,  4,  5
        # nr   = [20, 40, 60, 75, 100, 125][grid_inp]
        # prec = [13, 17, 21, 29, 41, 59][grid_inp]
        # radgrid = RadialGrid(nr, "chebyshev", "logm3",
        #                      dtype=self._dtype, device=self._device)
        # sphgrid = LebedevGrid(radgrid, prec=prec)
        #
        # natoms = self._atompos.shape[-2]
        # sphgrids = [sphgrid for _ in range(natoms)]
        # self._grid = BeckeGrid(sphgrids, self._atompos)

    def get_grid(self) -> BaseGrid:
        if self._grid is None:
            raise RuntimeError("Please run mol.setup_grid() first before calling get_grid()")
        return self._grid

    def requires_grid(self) -> bool:
        req_grid = self._vext is not None
        return req_grid

    def getparamnames(self, methodname: str, prefix: str = "") -> List[str]:
        if methodname == "get_nuclei_energy":
            params = [prefix + "_atompos"]
            if torch.is_floating_point(self._atomzs):
                params += [prefix + "_atomzs"]
            return params
        else:
            raise KeyError("Unknown methodname: %s" % methodname)

    ################### properties ###################
    @property
    def atompos(self) -> torch.Tensor:
        return self._atompos

    @property
    def atomzs(self) -> torch.Tensor:
        return self._atomzs

    @property
    def atommasses(self) -> torch.Tensor:
        # returns the atomic mass (only for non-isotope for now)
        if torch.is_floating_point(self._atomzs):
            raise RuntimeError("Atom masses are not available for floating point Z")
        return torch.tensor([get_atom_mass(int(atomz)) for atomz in self._atomzs],
                            dtype=self._dtype, device=self._device)

    @property
    def spin(self) -> ZType:
        return self._spin

    @property
    def charge(self) -> ZType:
        return self._charge

    @property
    def numel(self) -> ZType:
        return self._numel

    @property
    def efield(self) -> Optional[Tuple[torch.Tensor, ...]]:
        return self._efield
Ejemplo n.º 7
0
    def __init__(
            self,
            moldesc: Union[str, Tuple[AtomZsType, AtomPosType]],
            basis: BasisInpType,
            *,
            grid: Union[int, str] = "sg3",
            spin: Optional[ZType] = None,
            charge: ZType = 0,
            orb_weights: Optional[SpinParam[torch.Tensor]] = None,
            efield: Optional[torch.Tensor] = None,
            dtype: torch.dtype = torch.float64,
            device: torch.device = torch.device('cpu'),
    ):
        self._dtype = dtype
        self._device = device
        self._grid_inp = grid
        self._basis_inp = basis
        self._grid: Optional[BaseGrid] = None
        self._efield = efield

        # initialize cache
        self._cache = Cache()

        # get the AtomCGTOBasis & the hamiltonian
        # atomzs: (natoms,) dtype: torch.int or dtype for floating point
        # atompos: (natoms, ndim)
        atomzs, atompos = _parse_moldesc(moldesc, dtype, device)
        atomzs_int = torch.round(atomzs).to(
            torch.int) if atomzs.is_floating_point() else atomzs
        allbases = _parse_basis(atomzs_int, basis)  # list of list of CGTOBasis
        atombases = [
            AtomCGTOBasis(atomz=atz, bases=bas, pos=atpos)
            for (atz, bas, atpos) in zip(atomzs, allbases, atompos)
        ]
        self._atombases = atombases
        self._hamilton = HamiltonCGTO(atombases,
                                      efield=efield,
                                      cache=self._cache.add_prefix("hamilton"))
        self._atompos = atompos  # (natoms, ndim)
        self._atomzs = atomzs  # (natoms,) int-type or dtype if floating point
        self._atomzs_int = atomzs_int  # (natoms,) int-type rounded from atomzs
        nelecs_tot: torch.Tensor = torch.sum(atomzs)

        # orb_weights is not specified, so determine it from spin and charge
        if orb_weights is None:
            # get the number of electrons and spin
            nelecs, spin, frac_mode = _get_nelecs_spin(nelecs_tot, spin,
                                                       charge)
            _orb_weights, _orb_weights_u, _orb_weights_d = _get_orb_weights(
                nelecs, spin, frac_mode, dtype, device)

            # save the system's properties
            self._spin = spin
            self._charge = charge
            self._numel = nelecs
            self._orb_weights = _orb_weights
            self._orb_weights_u = _orb_weights_u
            self._orb_weights_d = _orb_weights_d

        # orb_weights is specified, so calculate the spin and charge from it
        else:
            assert isinstance(orb_weights, SpinParam)
            assert orb_weights.u.ndim == 1
            assert orb_weights.d.ndim == 1
            assert len(orb_weights.u) == len(orb_weights.d)

            utot = orb_weights.u.sum()
            dtot = orb_weights.d.sum()
            self._numel = utot + dtot
            self._spin = utot - dtot
            self._charge = nelecs_tot - self._numel

            self._orb_weights_u = orb_weights.u
            self._orb_weights_d = orb_weights.d
            self._orb_weights = orb_weights.u + orb_weights.d
Ejemplo n.º 8
0
class Mol(BaseSystem):
    """
    Describe the system of an isolated molecule.

    Arguments
    ---------
    * moldesc: str or 2-elements tuple (atomzs, atompos)
        Description of the molecule system.
        If string, it can be described like "H 0 0 0; H 0.5 0.5 0.5".
        If tuple, the first element of the tuple is the Z number of the atoms while
        the second element is the position of the atoms.
    * basis: str, CGTOBasis or list of str or CGTOBasis
        The string describing the gto basis. If it is a list, then it must have
        the same length as the number of atoms.
    * grid: int
        Describe the grid.
        If it is an integer, then it uses the default grid with specified level
        of accuracy.
        Default: 3
    * spin: int, float, torch.Tensor, or None
        The difference between spin-up and spin-down electrons.
        It must be an integer or None.
        If None, then it is ``num_electrons % 2``.
        For floating point atomzs and/or charge, the ``spin`` must be specified.
        Default: None
    * charge: int, float, or torch.Tensor
        The charge of the molecule.
        Default: 0
    * orb_weights: SpinParam[torch.Tensor] or None
        Specifiying the orbital occupancy (or weights) directly. If specified,
        ``spin`` and ``charge`` arguments are ignored.
    * efield: Optional[torch.Tensor]
        Uniform electric field of the system. If present, then the energy is
        calculated based on potential at (0, 0, 0) = 0.
        If None, then the electric field is assumed to be 0.
    * dtype: torch.dtype
        The data type of tensors in this class.
        Default: torch.float64
    * device: torch.device
        The device on which the tensors in this class are stored.
        Default: torch.device('cpu')
    """
    def __init__(
            self,
            moldesc: Union[str, Tuple[AtomZsType, AtomPosType]],
            basis: BasisInpType,
            *,
            grid: Union[int, str] = "sg3",
            spin: Optional[ZType] = None,
            charge: ZType = 0,
            orb_weights: Optional[SpinParam[torch.Tensor]] = None,
            efield: Optional[torch.Tensor] = None,
            dtype: torch.dtype = torch.float64,
            device: torch.device = torch.device('cpu'),
    ):
        self._dtype = dtype
        self._device = device
        self._grid_inp = grid
        self._basis_inp = basis
        self._grid: Optional[BaseGrid] = None
        self._efield = efield

        # initialize cache
        self._cache = Cache()

        # get the AtomCGTOBasis & the hamiltonian
        # atomzs: (natoms,) dtype: torch.int or dtype for floating point
        # atompos: (natoms, ndim)
        atomzs, atompos = _parse_moldesc(moldesc, dtype, device)
        atomzs_int = torch.round(atomzs).to(
            torch.int) if atomzs.is_floating_point() else atomzs
        allbases = _parse_basis(atomzs_int, basis)  # list of list of CGTOBasis
        atombases = [
            AtomCGTOBasis(atomz=atz, bases=bas, pos=atpos)
            for (atz, bas, atpos) in zip(atomzs, allbases, atompos)
        ]
        self._atombases = atombases
        self._hamilton = HamiltonCGTO(atombases,
                                      efield=efield,
                                      cache=self._cache.add_prefix("hamilton"))
        self._atompos = atompos  # (natoms, ndim)
        self._atomzs = atomzs  # (natoms,) int-type or dtype if floating point
        self._atomzs_int = atomzs_int  # (natoms,) int-type rounded from atomzs
        nelecs_tot: torch.Tensor = torch.sum(atomzs)

        # orb_weights is not specified, so determine it from spin and charge
        if orb_weights is None:
            # get the number of electrons and spin
            nelecs, spin, frac_mode = _get_nelecs_spin(nelecs_tot, spin,
                                                       charge)
            _orb_weights, _orb_weights_u, _orb_weights_d = _get_orb_weights(
                nelecs, spin, frac_mode, dtype, device)

            # save the system's properties
            self._spin = spin
            self._charge = charge
            self._numel = nelecs
            self._orb_weights = _orb_weights
            self._orb_weights_u = _orb_weights_u
            self._orb_weights_d = _orb_weights_d

        # orb_weights is specified, so calculate the spin and charge from it
        else:
            assert isinstance(orb_weights, SpinParam)
            assert orb_weights.u.ndim == 1
            assert orb_weights.d.ndim == 1
            assert len(orb_weights.u) == len(orb_weights.d)

            utot = orb_weights.u.sum()
            dtot = orb_weights.d.sum()
            self._numel = utot + dtot
            self._spin = utot - dtot
            self._charge = nelecs_tot - self._numel

            self._orb_weights_u = orb_weights.u
            self._orb_weights_d = orb_weights.d
            self._orb_weights = orb_weights.u + orb_weights.d

    def densityfit(self,
                   method: Optional[str] = None,
                   auxbasis: Optional[BasisInpType] = None) -> BaseSystem:
        """
        Indicate that the system's Hamiltonian uses density fit for its integral.

        Arguments
        ---------
        method: Optional[str]
            Density fitting method. Available methods in this class are:

            * "coulomb": Minimizing the Coulomb inner product, i.e. min <p-p_fit|r_12|p-p_fit>
              Ref: Eichkorn, et al. Chem. Phys. Lett. 240 (1995) 283-290.
              (default)
            * "overlap": Minimizing the overlap inner product, i.e. min <p-p_fit|p-p_fit>

        auxbasis: Optional[BasisInpType]
            Auxiliary basis for the density fit. If not specified, then it uses
            "cc-pvtz-jkfit".
        """
        if method is None:
            method = "coulomb"
        if auxbasis is None:
            # TODO: choose the auxbasis properly
            auxbasis = "cc-pvtz-jkfit"

        # get the auxiliary basis
        assert auxbasis is not None
        auxbasis_lst = _parse_basis(self._atomzs_int, auxbasis)
        atomauxbases = [
            AtomCGTOBasis(atomz=atz, bases=bas, pos=atpos)
            for (atz, bas,
                 atpos) in zip(self._atomzs, auxbasis_lst, self._atompos)
        ]

        # change the hamiltonian to have density fit
        df = DensityFitInfo(method=method, auxbases=atomauxbases)
        self._hamilton = HamiltonCGTO(self._atombases,
                                      df=df,
                                      efield=self._efield,
                                      cache=self._cache.add_prefix("hamilton"))
        return self

    def get_hamiltonian(self) -> BaseHamilton:
        return self._hamilton

    def set_cache(self,
                  fname: str,
                  paramnames: Optional[List[str]] = None) -> BaseSystem:
        """
        Setup the cache of some parameters specified by `paramnames` to be read/written
        on a file.
        If the file exists, then the parameters will not be recomputed, but just
        loaded from the cache instead.

        Arguments
        ---------
        fname: str
            The file to store the cache.
        paramnames: Optional[List[str]]
            List of parameter names to be read/write from the cache.
        """
        all_paramnames = self._cache.get_cacheable_params()
        if paramnames is not None:
            # check the paramnames
            for pname in paramnames:
                if pname not in all_paramnames:
                    msg = "Parameter %s is not cache-able. Cache-able parameters are %s" % \
                        (pname, all_paramnames)
                    raise ValueError(msg)

        self._cache.set(fname, paramnames)

        return self

    def get_orbweight(
        self,
        polarized: bool = False
    ) -> Union[torch.Tensor, SpinParam[torch.Tensor]]:
        if not polarized:
            return self._orb_weights
        else:
            return SpinParam(u=self._orb_weights_u, d=self._orb_weights_d)

    def get_nuclei_energy(self) -> torch.Tensor:
        # atomzs: (natoms,)
        # atompos: (natoms, ndim)

        # r12: (natoms, natom)
        r12 = safe_cdist(self._atompos,
                         self._atompos,
                         add_diag_eps=True,
                         diag_inf=True)
        z12 = self._atomzs.unsqueeze(-2) * self._atomzs.unsqueeze(
            -1)  # (natoms, natoms)
        q_by_r = z12 / r12
        return q_by_r.sum() * 0.5

    def setup_grid(self) -> None:
        grid_inp = self._grid_inp
        self._grid = get_grid(self._grid_inp,
                              self._atomzs_int,
                              self._atompos,
                              dtype=self._dtype,
                              device=self._device)

        # #        0,  1,  2,  3,  4,  5
        # nr   = [20, 40, 60, 75, 100, 125][grid_inp]
        # prec = [13, 17, 21, 29, 41, 59][grid_inp]
        # radgrid = RadialGrid(nr, "chebyshev", "logm3",
        #                      dtype=self._dtype, device=self._device)
        # sphgrid = LebedevGrid(radgrid, prec=prec)
        #
        # natoms = self._atompos.shape[-2]
        # sphgrids = [sphgrid for _ in range(natoms)]
        # self._grid = BeckeGrid(sphgrids, self._atompos)

    def get_grid(self) -> BaseGrid:
        if self._grid is None:
            raise RuntimeError(
                "Please run mol.setup_grid() first before calling get_grid()")
        return self._grid

    def getparamnames(self, methodname: str, prefix: str = "") -> List[str]:
        pass

    @property
    def spin(self) -> ZType:
        return self._spin

    @property
    def charge(self) -> ZType:
        return self._charge

    @property
    def numel(self) -> ZType:
        return self._numel
Ejemplo n.º 9
0
class Sol(BaseSystem):
    """
    Describe the system of a solid (i.e. periodic boundary condition system).

    Arguments
    ---------
    * soldesc: str or 2-elements tuple
        Description of the molecule system.
        If string, it can be described like ``"H 1 0 0; H -1 0 0"``.
        If tuple, the first element of the tuple is the Z number of the atoms while
        the second element is the position of the atoms: ``(atomzs, atomposs)``.
    * basis: str, CGTOBasis, list of str, or CGTOBasis
        The string describing the gto basis. If it is a list, then it must have
        the same length as the number of atoms.
    * grid: int
        Describe the grid.
        If it is an integer, then it uses the default grid with specified level
        of accuracy.
    * spin: int, float, torch.Tensor, or None
        The difference between spin-up and spin-down electrons.
        It must be an integer or ``None``.
        If ``None``, then it is ``num_electrons % 2``.
        For floating point atomzs and/or charge, the ``spin`` must be specified.
    * charge: int, float, or torch.Tensor
        The charge of the molecule.
    * orb_weights: SpinParam[torch.Tensor] or None
        Specifiying the orbital occupancy (or weights) directly. If specified,
        ``spin`` and ``charge`` arguments are ignored.
    * dtype: torch.dtype
        The data type of tensors in this class.
    * device: torch.device
        The device on which the tensors in this class are stored.
    """
    def __init__(
            self,
            soldesc: Union[str, Tuple[AtomZsType, AtomPosType]],
            alattice: torch.Tensor,
            basis: Union[str, List[CGTOBasis], List[str],
                         List[List[CGTOBasis]]],
            *,
            grid: Union[int, str] = "sg3",
            spin: Optional[ZType] = None,
            lattsum_opt: Optional[Union[PBCIntOption, Dict]] = None,
            dtype: torch.dtype = torch.float64,
            device: torch.device = torch.device('cpu'),
    ):
        self._dtype = dtype
        self._device = device
        self._grid_inp = grid
        self._grid: Optional[BaseGrid] = None
        charge = 0  # we can't have charged solids for now

        # get the AtomCGTOBasis & the hamiltonian
        # atomzs: (natoms,) dtype: torch.int or dtype for floating point
        # atompos: (natoms, ndim)
        atomzs, atompos = parse_moldesc(soldesc, dtype, device)
        allbases = _parse_basis(atomzs, basis)  # list of list of CGTOBasis
        atombases = [
            AtomCGTOBasis(atomz=atz, bases=bas, pos=atpos)
            for (atz, bas, atpos) in zip(atomzs, allbases, atompos)
        ]
        self._atombases = atombases
        self._atompos = atompos  # (natoms, ndim)
        self._atomzs = atomzs  # (natoms,) int-type
        nelecs_tot: torch.Tensor = torch.sum(atomzs)

        # get the number of electrons and spin and orbital weights
        nelecs, spin, frac_mode = _get_nelecs_spin(nelecs_tot, spin, charge)
        assert not frac_mode, "Fractional Z mode for pbc is not supported"
        _orb_weights, _orb_weights_u, _orb_weights_d = _get_orb_weights(
            nelecs, spin, frac_mode, dtype, device)

        # initialize cache
        self._cache = Cache()

        # save the system's properties
        self._spin = spin
        self._charge = charge
        self._numel = nelecs
        self._orb_weights = _orb_weights
        self._orb_weights_u = _orb_weights_u
        self._orb_weights_d = _orb_weights_d
        self._lattice = Lattice(alattice)
        self._lattsum_opt = PBCIntOption.get_default(lattsum_opt)

    def densityfit(self,
                   method: Optional[str] = None,
                   auxbasis: Optional[BasisInpType] = None) -> BaseSystem:
        """
        Indicate that the system's Hamiltonian uses density fit for its integral.

        Arguments
        ---------
        method: Optional[str]
            Density fitting method. Available methods in this class are:

            * ``"gdf"``: Density fit with gdf compensating charge to perform
                the lattice sum. Ref https://doi.org/10.1063/1.4998644 (default)

        auxbasis: Optional[BasisInpType]
            Auxiliary basis for the density fit. If not specified, then it uses
            ``"cc-pvtz-jkfit"``.
        """
        if method is None:
            method = "gdf"
        if auxbasis is None:
            # TODO: choose the auxbasis properly
            auxbasis = "cc-pvtz-jkfit"

        # get the auxiliary basis
        assert auxbasis is not None
        auxbasis_lst = _parse_basis(self._atomzs, auxbasis)
        atomauxbases = [
            AtomCGTOBasis(atomz=atz, bases=bas, pos=atpos)
            for (atz, bas,
                 atpos) in zip(self._atomzs, auxbasis_lst, self._atompos)
        ]

        # change the hamiltonian to have density fit
        df = DensityFitInfo(method=method, auxbases=atomauxbases)
        self._hamilton = HamiltonCGTO_PBC(
            self._atombases,
            df=df,
            latt=self._lattice,
            lattsum_opt=self._lattsum_opt,
            cache=self._cache.add_prefix("hamilton"))
        return self

    def get_hamiltonian(self) -> BaseHamilton:
        """
        Returns the Hamiltonian that corresponds to the system, i.e.
        :class:`~dqc.hamilton.HamiltonCGTO_PBC`
        """
        return self._hamilton

    def set_cache(self,
                  fname: str,
                  paramnames: Optional[List[str]] = None) -> BaseSystem:
        """
        Setup the cache of some parameters specified by `paramnames` to be read/written
        on a file.
        If the file exists, then the parameters will not be recomputed, but just
        loaded from the cache instead.
        Cache is usually used for repeated calculations where the cached parameters
        are not changed (e.g. running multiple systems with slightly different environment.)

        Arguments
        ---------
        fname: str
            The file to store the cache.
        paramnames: list of str or None
            List of parameter names to be read/write from the cache.
        """
        self._cache.set(fname, paramnames)
        return self

    def get_orbweight(
        self,
        polarized: bool = False
    ) -> Union[torch.Tensor, SpinParam[torch.Tensor]]:
        if not polarized:
            return self._orb_weights
        else:
            return SpinParam(u=self._orb_weights_u, d=self._orb_weights_d)

    def get_nuclei_energy(self) -> torch.Tensor:
        # self._atomzs: (natoms,)
        # self._atompos: (natoms, ndim)

        # r12: (natoms, natoms)
        r12_inf = safe_cdist(self._atompos,
                             self._atompos,
                             add_diag_eps=True,
                             diag_inf=True)
        r12 = safe_cdist(self._atompos, self._atompos, add_diag_eps=True)
        z12 = self._atomzs.unsqueeze(-2) * self._atomzs.unsqueeze(
            -1)  # (natoms, natoms)

        precision = self._lattsum_opt.precision
        eta = self._lattice.estimate_ewald_eta(precision) * 2
        vol = self._lattice.volume()
        rcut = scipy.special.erfcinv(
            float(vol.detach()) * eta * eta / (2 * np.pi) * precision) / eta
        gcut = scipy.special.erfcinv(
            precision * np.sqrt(np.pi) / 2 / eta) * 2 * eta

        # get the shift vector in real space and in reciprocal space
        ls = self._lattice.get_lattice_ls(rcut=rcut,
                                          exclude_zeros=True)  # (nls, ndim)
        # gv: (ngv, ndim), gvweights: (ngv,)
        gv, gvweights = self._lattice.get_gvgrids(gcut=gcut,
                                                  exclude_zeros=True)
        gv_norm2 = torch.einsum("gd,gd->g", gv, gv)  # (ngv)

        # get the shift in position
        atpos_shift = self._atompos - ls.unsqueeze(-2)  # (nls, natoms, ndim)
        r12_ls = safe_cdist(atpos_shift,
                            self._atompos)  # (nls, natoms, natoms)

        # calculate the short range
        short_range_comp1 = torch.erfc(
            eta * r12_ls) / r12_ls  # (nls, natoms, natoms)
        short_range_comp2 = torch.erfc(eta * r12) / r12_inf  # (natoms, natoms)
        short_range1 = torch.sum(z12 * short_range_comp1)  # scalar
        short_range2 = torch.sum(z12 * short_range_comp2)  # scalar
        short_range = short_range1 + short_range2

        # calculate the long range sum
        coul_g = 4 * np.pi / gv_norm2 * gvweights  # (ngv,)
        # this part below is quicker, but raises warning from pytorch
        si = torch.exp(
            1j *
            torch.matmul(self._atompos, gv.transpose(-2, -1)))  # (natoms, ngv)
        zsi = torch.einsum("a,ag->g", self._atomzs.to(si.dtype), si)  # (ngv,)
        zexpg2 = zsi * torch.exp(-gv_norm2 / (4 * eta * eta))
        long_range = torch.einsum("a,a,a->", zsi.conj(), zexpg2,
                                  coul_g.to(zsi.dtype)).real  # (scalar)

        # # alternative way to compute the long-range part
        # r12_pair = self._atompos.unsqueeze(-2) - self._atompos  # (natoms, natoms, ndim)
        # long_range_exp = torch.exp(-gv_norm2 / (4 * eta * eta)) * coul_g  # (ngv,)
        # long_range_cos = torch.cos(torch.einsum("gd,abd->gab", gv, -r12_pair))  # (ngv, natoms, natoms)
        # long_range = torch.sum(long_range_exp[:, None, None] * long_range_cos * z12)  # scalar

        # background interaction
        vbar1 = -torch.sum(self._atomzs**2) * (2 * eta / np.sqrt(np.pi))
        vbar2 = -torch.sum(self._atomzs)**2 * np.pi / (eta * eta * vol)
        vbar = vbar1 + vbar2  # (scalar)

        eii = short_range + long_range + vbar
        return eii * 0.5

    def setup_grid(self) -> None:
        self._grid = get_predefined_grid(self._grid_inp,
                                         self._atomzs,
                                         self._atompos,
                                         lattice=self._lattice,
                                         dtype=self._dtype,
                                         device=self._device)

    def get_grid(self) -> BaseGrid:
        if self._grid is None:
            raise RuntimeError(
                "Please run mol.setup_grid() first before calling get_grid()")
        return self._grid

    def requires_grid(self) -> bool:
        return False

    def getparamnames(self, methodname: str, prefix: str = "") -> List[str]:
        pass

    ################### properties ###################
    @property
    def atompos(self) -> torch.Tensor:
        return self._atompos

    @property
    def atomzs(self) -> torch.Tensor:
        return self._atomzs

    @property
    def atommasses(self) -> torch.Tensor:
        # returns the atomic mass (only for non-isotope for now)
        return torch.tensor(
            [get_atom_mass(int(atomz)) for atomz in self._atomzs],
            dtype=self._dtype,
            device=self._device)

    @property
    def spin(self) -> ZType:
        return self._spin

    @property
    def charge(self) -> ZType:
        return self._charge

    @property
    def numel(self) -> ZType:
        return self._numel

    @property
    def efield(self) -> Optional[Tuple[torch.Tensor, ...]]:
        # solid with external efield has not been implemented
        return None