def _get_spins(sample, scell, momtype='e'): cell = sample.cell.get_cell() fxyz, xyz = supcell_gridgen(cell, scell) fpos = sample.cell.get_scaled_positions() pos = sample.cell.get_positions() sfpos = fpos[None, :, :] + fxyz[:, None, :] spos = pos[None, :, :] + xyz[:, None, :] spins = (spos * 0.0j) k = sample.mm.k fc = sample.mm.fc ker = np.exp(-2.0j * np.pi * np.dot(k, sfpos.T)) spins = np.real(fc[None, :, :] * ker[:, None, None]) # Now turn those into actual dipole moments... if momtype == 'e': g_e = (cnst.physical_constants['Bohr magneton'][0] * cnst.physical_constants['electron g factor'][0]) / cnst.hbar gammas = np.repeat([g_e], len(pos)) elif momtype == 'n': gammas = _get_isotope_data(sample.cell.get_chemical_symbols(), 'gamma') else: raise ValueError('Invalid momtype argument passed to _get_spins') gammas = np.repeat(gammas[None, :], len(fxyz), axis=0) return spos, spins, gammas
def __init__(self, seedname, gw_fac=3, path=''): super(PEPChargeDistribution, self).__init__(seedname, gw_fac, path) # Partition the charges using the Becke method # Becke, A. D. ‘A multicenter numerical integration scheme for polyatomic molecules’ # J. Chem. Phys. 1988, 88, p 2547-2553. http://dx.doi.org/10.1063/1.454033 xyz = self._elec_den.xyz maxR = max_distance_in_cell(self.cell) scell = minimum_supcell(maxR, self.cell) scgfrac, scgxyz = supcell_gridgen(self.cell, scell) # For each point, find their minimum periodic distance from each ion pos = self.positions.T scgion = pos[:, :, None] + (scgxyz.T)[:, None, :] rA = np.zeros( (xyz.shape[1], xyz.shape[2], xyz.shape[3], len(self.positions))) for i in range(xyz.shape[1]): for j in range(xyz.shape[2]): rA[i, j, :, :] = np.amin(np.linalg.norm( xyz[:, i, j, :, None, None] - scgion[:, None, :, :], axis=0), axis=-1) rAB = np.linalg.norm(pos[:, :, None] - pos[:, None, :], axis=0) rAB += np.diag([np.inf] * len(rAB)) muAB = ((rA[:, :, :, :, None] - rA[:, :, :, None, :]) / rAB[None, None, None, :, :]) sk = 0.5 * (1.0 - _fk(muAB, 3)) # Set the diagonal to 1 so that it doesn't affect the products ii = range(len(self.positions)) sk[:, :, :, ii, ii] = 1 # Now weight functions wA = np.prod(sk, axis=-1) self._wA = wA / np.sum(wA, axis=-1)[:, :, :, None] # Finally, partition the density self._rhopart = self._rho[:, :, :, None] * self._wA self._rhopart_G = np.fft.fftn(self._rhopart, axes=(0, 1, 2)) Gnorm = np.linalg.norm(self._g_grid, axis=0) Gnorm_fixed = np.where(Gnorm > 0, Gnorm, np.inf) vol = self.volume self._Vpart_G = 4 * np.pi / Gnorm_fixed[:, :, :, None]**2 * ( self._rhopart_G / vol) # Now compute interaction energy of ions self._rhoion_G = self._rhopart_G + self._rhoi_G self._Vion_G = self._Vpart_G + self._Vi_G self._ionE = np.real( np.sum(self._rhoion_G * np.conj(np.sum( self._Vion_G, axis=-1))[:, :, :, None])) * _cK * cnst.e * 1e10
def __init__(self, a, sigma=2.0, rcut=10): shape = minimum_supcell(rcut, a.get_cell()) igrid, grid = supcell_gridgen(a.get_cell(), shape) self.p = a.get_positions()[None, :, :] + grid[:, None, :] self.p = self.p.reshape((-1, 3)) sph_i = np.where(np.linalg.norm(self.p, axis=1) <= rcut) self.p = self.p[sph_i] self.s = sigma
def __init__(self, atoms, mu_pos, isotopes={}, isotope_list=None, cutoff=10, overlap_eps=1e-3): # Get positions, cell, and species, only things we care about self.cell = np.array(atoms.get_cell()) pos = atoms.get_positions() el = np.array(atoms.get_chemical_symbols()) self.mu_pos = np.array(mu_pos) # Is it periodic? if np.any(atoms.get_pbc()): scell = minimum_supcell(cutoff, self.cell) else: scell = [1, 1, 1] grid_f, grid = supcell_gridgen(self.cell, scell) self.grid_f = grid_f r = (pos[:, None, :] + grid[None, :, :] - self.mu_pos[None, None, :]).reshape((-1, 3)) rnorm = np.linalg.norm(r, axis=1) sphere = np.where(rnorm <= cutoff)[0] sphere = sphere[np.argsort(rnorm[sphere])[::-1]] # Sort by length r = r[sphere] rnorm = rnorm[sphere] self._r = r self._rn = rnorm self._rn = np.where(self._rn > overlap_eps, self._rn, np.inf) self._ri = sphere self._dT = (3 * r[:, :, None] * r[:, None, :] / self._rn[:, None, None]**2 - np.eye(3)[None, :, :]) / 2 self._an = len(pos) self._gn = self.grid_f.shape[0] self._a_i = self._ri // self._gn self._ijk = self.grid_f[self._ri % self._gn] # Get gammas self.gammas = _get_isotope_data(el, 'gamma', isotopes, isotope_list) self.gammas = self.gammas[self._a_i] Dn = _dip_constant(self._rn * 1e-10, m_gamma, self.gammas) De = _dip_constant( self._rn * 1e-10, m_gamma, cnst.physical_constants['electron gyromag. ratio'][0]) self._D = {'n': Dn, 'e': De} # Start with all zeros self.spins = rnorm * 0
def from_box(atoms, abc0, abc1, periodic=False, scaled=False): """Generate a selection for the given Atoms object of all atoms within a given box volume. | Args: | atoms (ase.Atoms): Atoms object on which to perform selection | abc0 ([float, float, float]): bottom corner of box | abc1 ([float, float, float]): top corner of box | periodic (Optional[bool]): if True, include periodic copies of the | atoms | scaled (Optional[bool]): if True, consider scaled (fractional) | coordinates instead of absolute ones | Returns: | selection (AtomSelection) """ if scaled: pos = atoms.get_scaled_positions() else: pos = atoms.get_positions() # Do we need periodic copies? if periodic and any(atoms.get_pbc()): # Get the range max_r = np.linalg.norm(np.array(abc1) - abc0) scell_shape = minimum_supcell(max_r, latt_cart=atoms.get_cell(), pbc=atoms.get_pbc()) grid_frac, grid = supcell_gridgen(atoms.get_cell(), scell_shape) if scaled: pos = (pos[:, None, :] + grid_frac[None, :, :]) else: pos = (pos[:, None, :] + grid[None, :, :]) where_i = np.where( np.all(pos > abc0, axis=-1) & np.all(pos < abc1, axis=-1))[:2] sel_i = where_i[0] sel = AtomSelection(atoms, sel_i) if periodic: sel.set_array('cell_indices', grid_frac[where_i[1]]) return sel
def from_sphere(atoms, center, r, periodic=False, scaled=False): """Generate a selection for the given Atoms object of all atoms within a given spherical volume. | Args: | atoms (ase.Atoms): Atoms object on which to perform selection | center ([float, float, float]): center of the sphere | r (float): radius of the sphere | periodic (Optional[bool]): if True, include periodic copies of the | atoms | scaled (Optional[bool]): if True, consider scaled (fractional) | coordinates instead of absolute ones | Returns: | selection (AtomSelection) """ if scaled: pos = atoms.get_scaled_positions() else: pos = atoms.get_positions() # Do we need periodic copies? if periodic and any(atoms.get_pbc()): # Get the range r_bounds = minimum_supcell(r, latt_cart=atoms.get_cell(), pbc=atoms.get_pbc()) grid_frac, grid = supcell_gridgen(atoms.get_cell(), r_bounds) if scaled: pos = (pos[:, None, :] + grid_frac[None, :, :]) else: pos = (pos[:, None, :] + grid[None, :, :]) where_i = np.where(np.linalg.norm(pos - center, axis=-1) <= r) sel_i = where_i[0] sel = AtomSelection(atoms, sel_i) if periodic: sel.set_array('cell_indices', grid_frac[where_i[1]]) return sel
def extract(s, cutoff, isonuclear, isotopes, isotope_list): # Supercell size scell_shape = minimum_supcell(cutoff, s.get_cell()) _, scell = supcell_gridgen(s.get_cell(), scell_shape) pos = s.get_positions() elems = np.array(s.get_chemical_symbols()) gammas = _get_isotope_data(elems, 'gamma', isotopes, isotope_list) dip_rss = [] for i, el in enumerate(elems): # Distances? if not isonuclear: rij = pos.copy() gj = np.tile(gammas, len(scell)) else: rij = pos[np.where(elems == el)] gj = gammas[i] rij = rij[None, :, :] + scell[:, None, :] - pos[i, None, None] Rij = np.linalg.norm(rij.reshape((-1, 3)), axis=-1) # Valid indices? ij = np.where((Rij > 0) & (Rij <= cutoff)) Rij = Rij[ij] * 1e-10 try: gj = gj[ij] except IndexError: pass dip = _dip_constant(Rij, gammas[i], gj) dip_rss.append(np.sqrt(np.sum(dip**2))) return np.array(dip_rss)
def __init__(self, seedname, gw_fac=3, path=''): super().__init__(seedname, gw_fac, path) """ Partition the charges using the Becke method Becke, A. D. ‘A multicenter numerical integration scheme for polyatomic molecules’ J. Chem. Phys. 1988, 88, p 2547-2553. http://dx.doi.org/10.1063/1.454033 """ xyz = self._elec_den.xyz maxR = max_distance_in_cell(self.cell) scell = minimum_supcell(maxR, self.cell) scgfrac, scgxyz = supcell_gridgen(self.cell, scell) # For each point, find their minimum periodic distance from each ion pos = self.positions.T scgion = pos[:, :, None] + (scgxyz.T)[:, None, :] # Grid xn, yn, zn = xyz.shape[1:] N = len(self.positions) rA = np.zeros((xn, yn, zn, N)) for i in range(xn): for j in range(yn): rA[i, j, :, :] = np.amin(np.linalg.norm( xyz[:, i, j, :, None, None] - scgion[:, None, :, :], axis=0), axis=-1) rAB = np.linalg.norm(pos[:, :, None] - pos[:, None, :], axis=0) rAB += np.diag([np.inf] * len(rAB)) muAB = ((rA[:, :, :, :, None] - rA[:, :, :, None, :]) / rAB[None, None, None, :, :]) sk = 0.5 * (1.0 - _fk(muAB, 3)) # Set the diagonal to 1 so that it doesn't affect the products ii = range(len(self.positions)) sk[:, :, :, ii, ii] = 1 # Now weight functions wA = np.prod(sk, axis=-1) self._wA = wA / np.sum(wA, axis=-1)[:, :, :, None] # Finally, partition the density self._rhopart = self._rho[:, :, :, None] * self._wA self._rhopart_G = np.fft.fftn(self._rhopart, axes=(0, 1, 2)) Gnorm = np.linalg.norm(self._g_grid, axis=0) Gnorm_fixed = np.where(Gnorm > 0, Gnorm, np.inf) vol = self.volume self._Vpart_G = 4 * np.pi / Gnorm_fixed[:, :, :, None]**2 * ( self._rhopart_G / vol) # The individual ion density must be redefined too self._rhoipart_G = np.zeros((xn, yn, zn, N)) * 0.j pos = self.atoms.get_positions() for i, p in enumerate(pos): self._rhoipart_G[:, :, :, i] = ( self._q[i] * np.exp(-1.0j * np.sum(self._g_grid[:, :, :, :] * p[:, None, None, None], axis=0) - 0.5 * (self._gw[i] * Gnorm)**2)) # # Now compute interaction energy of ions self._rhotot_G = self._rhoe_G + self._rhoi_G self._Vtot_G = self._Ve_G + self._Vi_G for r in (self._rhoe_G, self._rhoi_G): for v in (self._Ve_G, self._Vi_G): print( np.real(np.sum(r * np.conj(np.sum(v, axis=-1)))) * _cK * cnst.e * 1e10) self._ionE = np.real( np.sum(self._rhotot_G * np.conj(np.sum( self._Vtot_G, axis=-1)))) * _cK * cnst.e * 1e10
def compute_hfine_tensor( points, spins, cell=None, self_i=0, species="e", cut_r=10, lorentz=True, fermi_mm=0, ): """Compute the hyperfine tensor experienced at point of index i generated by a number of localised spins at points, for a given periodic unit cell and species. | Args: | points (np.ndarray): coordinates of points at which the spins are | localised | spins (np.ndarray): magnetic moments (as spin quantum number, e.g. | 0.5 for an electron or 1H nucleus with spin up) | cell (np.ndarray): unit cell (if None, considered non-periodic) | self_i (int): index of point at which to compute the tensor. | Local spin density will give rise to a Fermi | contact term | species (str or [str]): symbol or list of symbols identifying the | species generating the magnetic field. | Determines the magnetic moments | cut_r (float): cutoff radius for dipolar component calculation | lorentz (bool): if True, include a Lorentz term (average bulk | magnetization). Default is True | fermi_mm (float): Magnetic moment density at site i to use for | computation of the Fermi contact term. Units | are Bohr magnetons/Ang^3 | Returns: | HT (np.ndarray): hyperfine tensor at point i """ N = len(points) magmoms = np.array(spins).astype(float) species = np.array(species).astype("S2") if species.shape == (): species = np.repeat(species[None], N) for i, s in enumerate(species): if s == b"e": mm = 2 * _bohrmag elif s == b"mu": mm = mu_cnst.m_gamma * cnst.hbar else: mm = _get_isotope_data(s, "gamma")[0] * cnst.hbar magmoms[i] *= mm * abs(cnst.physical_constants["electron g factor"][0]) # Do we need a supercell? r = np.array(points) - points[self_i] if cell is not None: scell = minimum_supcell(cut_r, latt_cart=cell) fxyz, xyz = supcell_gridgen(cell, scell) r = r[:, None, :] + xyz[None, :, :] else: r = r[:, None, :] rnorm = np.linalg.norm(r, axis=-1) # Expunge the ones that are outside of the sphere sphere = np.where(rnorm <= cut_r) r = r[sphere] rnorm = rnorm[sphere] magmoms = magmoms[sphere[0]] # Find the contact point self_i = np.argmin(rnorm) magmoms[self_i] = 0 rnorm_inv = 1.0 / np.where(rnorm > 0, rnorm, np.inf) rdyad = r[:, None, :] * r[:, :, None] rdip = 3 * rdyad * rnorm_inv[:, None, None] ** 2 - np.eye(3)[None, :, :] HT = np.sum(magmoms[:, None, None] * rdip * rnorm_inv[:, None, None] ** 3, axis=0) HT *= cnst.mu_0 / (4 * np.pi) * 1e30 # Add Lorentz term if cell is not None and lorentz: avgM = np.sum(magmoms) * 3.0 / (4.0 * np.pi * cut_r ** 3) HT += np.eye(3) * avgM * cnst.mu_0 / 3.0 * 1e30 # Add contact term if fermi_mm: fermi_mm *= _bohrmag * abs(cnst.physical_constants["electron g factor"][0]) HT += np.eye(3) * fermi_mm * 2.0 / 3.0 * cnst.mu_0 * 1e30 return HT
def dq_buildup(self, sel_i, sel_j=None, t_max=1e-3, t_steps=1000, R_cut=3, kdq=0.155, A=1, tau=np.inf): """ Return a dictionary of double quantum buildup curves for given pairs of atoms, built according to the theory given in: G. Pileio et al., "Analytical theory of gamma-encoded double-quantum recoupling sequences in solid-state nuclear magnetic resonance" Journal of Magnetic Resonance 186 (2007) 65-74 | Args: | sel_i (AtomSelection or [int]): Selection or list of indices of | atoms for which to compute the | curves. By default is None | (= all of them). | sel_i (AtomSelection or [int]): Selection or list of indices of | atoms for which to compute the | curves with sel_i. By default is | None (= same as sel_i). | t_max (float): maximum DQ buildup time, in seconds. Default | is 1e-3. | t_steps (int): number of DQ buildup time steps. Default is 1000. | R_cut (float): cutoff radius for which periodic copies to consider | in each pair, in Angstrom. Default is 3. | kdq (float): same as the k constant in eq. 35 of the reference. A | parameter depending on the specific sequence used. | Default is 0.155. | A (float): overall scaling factor for the curve. Default is 1. | tau (float): exponential decay factor for the curve. Default | is np.inf. | Returns: | curves (dict): a dictionary of all buildup curves indexed by pair, | plus the time axis in seconds as member 't'. """ tdq = np.linspace(0, t_max, t_steps) # Selections if sel_i is None: sel_i = AtomSelection.all(s) elif not isinstance(sel_i, AtomSelection): sel_i = AtomSelection(self._sample, sel_i) if sel_j is None: sel_j = sel_i elif not isinstance(sel_j, AtomSelection): sel_j = AtomSelection(self._sample, sel_j) # Find gammas elems = self._sample.get_chemical_symbols() gammas = _get_isotope_data(elems, 'gamma', {}, self._isos) # Need to sort them and remove any duplicates, also take i < j as # convention pairs = [ tuple(sorted((i, j))) for i in sorted(sel_i.indices) for j in sorted(sel_j.indices) ] scell_shape = minimum_supcell(R_cut, latt_cart=self._sample.get_cell()) nfg, ng = supcell_gridgen(self._sample.get_cell(), scell_shape) pos = self._sample.get_positions() curves = {'t': tdq} for ij in pairs: r = pos[ij[1]] - pos[ij[0]] all_r = r[None, :] + ng all_R = np.linalg.norm(all_r, axis=1) # Apply cutoff all_R = all_R[np.where((all_R <= R_cut) * (all_R > 0))] n = all_R.shape[0] bij = _dip_constant(all_R * 1e-10, gammas[ij[0]], gammas[ij[1]]) th = 1.5 * kdq * abs(bij[:, None]) * tdq[None, :] * 2 * np.pi x = (2 * th / np.pi)**0.5 Fs, Fc = fresnel(x * 2**0.5) x[:, 0] = np.inf bdup = 0.5-(1.0/(x*8**0.5)) * \ (Fc*np.cos(2*th) + Fs*np.sin(2*th)) bdup[:, 0] = 0 curves[ij] = A * np.sum(bdup, axis=0) * np.exp(-tdq / tau) return curves
def molecularNeighbourhoodGen(struct, mols, central_mol=0, max_R=10, method='com', use_supercell=False): """Generator function to create a spherical molecular neighbourhood. Given a structure and its molecules as returned by the Molecules property, produce supercell structures that contain one molecule each, progressively further away from the one indicated as central. | Args: | struct (ase.Atoms): original structure | mols ([ase.AtomsSelection]): list of molecules, as returned by the | soprano.properties.linkage.Molecules | class. | central_mol (int): index of the molecule whose centre of mass is | considered central. Default is 0. | max_R (float): maximum radius of the neighbourhood sphere. Default is | 10 Ang. | method (str): method to compute distance between molecules. 'com' | means using the center of mass. 'nearest' means using | the closest atom. Default is 'com'. | use_supercell (bool): if True, all returned structures will have a | cell large enough to contain the entire | neighbourhood. Default is False. | Returns: | molecularNeighbourhoodGen (generator): an iterator object that yields | structures within the given | spherical neighbourhood. """ # Supercell size? scell = minimum_supcell(max_R, struct.get_cell()) fgrid, grid = supcell_gridgen(struct.get_cell(), scell) # Center? mol_structs = [m.subset(struct) for m in mols] mol_coms = np.array([a.get_center_of_mass() for a in mol_structs]) # Origin p0 = mol_coms[central_mol] # Positions? if method == 'com': positions = mol_coms[None, :, :]+grid[:, None, :]-p0 elif method == 'nearest': positions = np.zeros((len(grid), len(mols), 3)) for i, a in enumerate(mol_structs): dp = a.get_positions() - p0 p = dp[None, :, :] + grid[:, None, :] # Closest one? positions[:, i, :] = p[range(len(grid)), np.argmin(np.linalg.norm(p, axis=-1), axis=1)] else: raise RuntimeError('Invalid method passed to ' 'molecularNeighbourhoodGen') # Order of appearance? distances = np.linalg.norm(positions, axis=-1) sphere = np.where(distances <= max_R) distances = distances[sphere[0], sphere[1]] fxyz = fgrid[sphere[0]] xyz = grid[sphere[0]] mol_i = np.arange(len(mols))[sphere[1]] positions = positions[sphere[0], sphere[1]] # Now the order order = np.argsort(distances) for i in order: a = mol_structs[mol_i[i]].copy() # Create the structure if use_supercell: a.set_cell(np.dot(np.diag(scell), a.get_cell())) a.set_positions(a.get_positions() + xyz[i]) # Add some info a.info['neighbourhood_info'] = { 'molecule_index': mol_i[i], 'molecule_cell': fxyz[i], 'molecule_distance': distances[i] } yield a