def vhf_from_pvhf(trj, partial_dict, water=False): """ Compute the total Van Hove function from partial Van Hove functions Parameters ---------- trj : mdtrj.Trajectory trajectory on which partial vhf were calculated form partial_dict : dict dictionary containing partial vhf as a np.array. Key is a tuple of len 2 with 2 atom types Return ------- total_grt : numpy.ndarray Total Van Hove Function generated from addition of partial Van Hove Functions """ unique_atoms = get_unique_atoms(trj) all_atoms = [atom for atom in trj.topology.atoms] norm_coeff = 0 dict_shape = list(partial_dict.values())[0][0].shape total_grt = np.zeros(dict_shape) for atom_pair in partial_dict.keys(): # checks if key is a tuple if isinstance(atom_pair, tuple) == False: raise ValueError("Dictionary key not valid. Must be a tuple.") for atom in atom_pair: # checks if the atoms in tuple pair are atom types if type(atom) != type(unique_atoms[0]): raise ValueError( "Dictionary key not valid. Must be type `MDTraj.Atom`.") # checks if atoms are in the trajectory if atom not in all_atoms: raise ValueError( f"Dictionary key not valid, `Atom` {atom} not in MDTraj trajectory." ) # checks if key has two atoms if len(atom_pair) != 2: raise ValueError( "Dictionary key not valid. Must only have 2 atoms per pair.") atom1 = atom_pair[0] atom2 = atom_pair[1] coeff = (get_form_factor(element_name=f"{atom1.element.symbol}", water=False) * get_form_factor(element_name=f"{atom2.element.symbol}", water=False) * len(trj.topology.select(f"name {atom1.name}")) / (trj.n_atoms) * len(trj.topology.select(f"name {atom2.name}")) / (trj.n_atoms)) normalized_pvhf = coeff * partial_dict[atom_pair] norm_coeff += coeff total_grt = np.add(total_grt, normalized_pvhf) total_grt /= norm_coeff return total_grt
def compute_van_hove( trj, chunk_length, parallel=False, water=False, r_range=(0, 1.0), bin_width=0.005, n_bins=None, self_correlation=True, periodic=True, opt=True, partial=False, ): """Compute the Van Hove function of a trajectory. Atom pairs referenced in partial Van Hove functions are in alphabetical order. If specific ordering of atom pairs are needed, user should use compute_partial_van_hove then vhf_from_pvhf to compute total Van Hove function. Parameters ---------- trj : mdtraj.Trajectory trajectory on which to compute the Van Hove function chunk_length : int length of time between restarting averaging parallel : bool, default=True Use parallel implementation with `multiprocessing` water : bool use X-ray form factors for water that account for polarization r_range : array-like, shape=(2,), optional, default=(0.0, 1.0) Minimum and maximum radii. bin_width : float, optional, default=0.005 Width of the bins in nanometers. n_bins : int, optional, default=None The number of bins. If specified, this will override the `bin_width` parameter. self_correlation : bool, default=True Whether or not to include the self-self correlations partial : bool, default = False Whether or not to return a dictionary including partial Van Hove function. Returns ------- r : numpy.ndarray r positions generated by histogram binning g_r_t : numpy.ndarray Van Hove function at each time and position """ n_physical_atoms = len([a for a in trj.top.atoms if a.element.mass > 0]) unique_elements = list( set([a.element for a in trj.top.atoms if a.element.mass > 0])) if parallel: data = [] for elem1, elem2 in it.combinations_with_replacement( unique_elements[::-1], 2): # Add a bool to check if self-correlations should be analyzed self_bool = self_correlation if elem1 != elem2: self_bool = False warnings.warn( "Total VHF calculation: No self-correlations for {} and {}, setting `self_correlation` to `False`." .format(elem1, elem2)) data.append([ trj, chunk_length, "element {}".format(elem1.symbol), "element {}".format(elem2.symbol), r_range, bin_width, n_bins, self_bool, periodic, opt, ]) manager = multiprocessing.Manager() partial_dict = manager.dict() jobs = [] version_info = sys.version_info for d in data: with multiprocessing.Pool( processes=multiprocessing.cpu_count()) as pool: if version_info.major == 3 and version_info.minor <= 7: p = pool.Process(target=worker, args=(partial_dict, d)) elif version_info.major == 3 and version_info.minor >= 8: ctx = multiprocessing.get_context() p = pool.Process(ctx, target=worker, args=(partial_dict, d)) jobs.append(p) p.start() for proc in jobs: proc.join() r = partial_dict["r"] del partial_dict["r"] else: partial_dict = dict() for elem1, elem2 in it.combinations_with_replacement( unique_elements[::-1], 2): # Add a bool to check if self-correlations should be analyzed self_bool = self_correlation if elem1 != elem2: self_bool = False warnings.warn( "Total VHF calculation: No self-correlations for {} and {}, setting `self_correlation` to `False`." .format(elem1, elem2)) if elem1.symbol > elem2.symbol: temp = elem1 elem1 = elem2 elem2 = temp print("doing {0} and {1} ...".format(elem1, elem2)) r, g_r_t_partial = compute_partial_van_hove( trj=trj, chunk_length=chunk_length, selection1="element {}".format(elem1.symbol), selection2="element {}".format(elem2.symbol), r_range=r_range, bin_width=bin_width, n_bins=n_bins, self_correlation=self_bool, periodic=periodic, opt=opt, ) partial_dict[("element {}".format(elem1.symbol), "element {}".format(elem2.symbol))] = g_r_t_partial if partial: return partial_dict norm = 0 g_r_t = None for key, val in partial_dict.items(): elem1, elem2 = key concentration1 = (trj.atom_slice(trj.top.select(elem1)).n_atoms / n_physical_atoms) concentration2 = (trj.atom_slice(trj.top.select(elem2)).n_atoms / n_physical_atoms) form_factor1 = get_form_factor(element_name=elem1.split()[1], water=water) form_factor2 = get_form_factor(element_name=elem2.split()[1], water=water) coeff = form_factor1 * concentration1 * form_factor2 * concentration2 if g_r_t is None: g_r_t = np.zeros_like(val) g_r_t += val * coeff norm += coeff # Reshape g_r_t to better represent the discretization in both r and t g_r_t_final = np.empty(shape=(chunk_length, len(r))) for i in range(chunk_length): g_r_t_final[i, :] = np.mean(g_r_t[i::chunk_length], axis=0) g_r_t_final /= norm t = trj.time[:chunk_length] return r, t, g_r_t_final
def compute_van_hove(trj, chunk_length, water=False, r_range=(0, 1.0), bin_width=0.005, n_bins=None, self_correlation=True, periodic=True, opt=True, partial=False): """Compute the partial van Hove function of a trajectory Parameters ---------- trj : mdtraj.Trajectory trajectory on which to compute the Van Hove function chunk_length : int length of time between restarting averaging water : bool use X-ray form factors for water that account for polarization r_range : array-like, shape=(2,), optional, default=(0.0, 1.0) Minimum and maximum radii. bin_width : float, optional, default=0.005 Width of the bins in nanometers. n_bins : int, optional, default=None The number of bins. If specified, this will override the `bin_width` parameter. self_correlation : bool, default=True Whether or not to include the self-self correlations Returns ------- r : numpy.ndarray r positions generated by histogram binning g_r_t : numpy.ndarray Van Hove function at each time and position """ n_physical_atoms = len([a for a in trj.top.atoms if a.element.mass > 0]) unique_elements = list( set([a.element for a in trj.top.atoms if a.element.mass > 0])) partial_dict = dict() for elem1, elem2 in it.combinations_with_replacement( unique_elements[::-1], 2): print('doing {0} and {1} ...'.format(elem1, elem2)) r, g_r_t_partial = compute_partial_van_hove( trj=trj, chunk_length=chunk_length, selection1='element {}'.format(elem1.symbol), selection2='element {}'.format(elem2.symbol), r_range=r_range, bin_width=bin_width, n_bins=n_bins, self_correlation=self_correlation, periodic=periodic, opt=opt) partial_dict[(elem1, elem2)] = g_r_t_partial if partial: return partial_dict norm = 0 g_r_t = None for key, val in partial_dict.items(): elem1, elem2 = key concentration1 = trj.atom_slice( trj.top.select('element {}'.format( elem1.symbol))).n_atoms / n_physical_atoms concentration2 = trj.atom_slice( trj.top.select('element {}'.format( elem2.symbol))).n_atoms / n_physical_atoms form_factor1 = get_form_factor(element_name=elem1.symbol, water=water) form_factor2 = get_form_factor(element_name=elem2.symbol, water=water) coeff = form_factor1 * concentration1 * form_factor2 * concentration2 if g_r_t is None: g_r_t = np.zeros_like(val) g_r_t += val * coeff norm += coeff # Reshape g_r_t to better represent the discretization in both r and t g_r_t_final = np.empty(shape=(chunk_length, len(r))) for i in range(chunk_length): g_r_t_final[i, :] = np.mean(g_r_t[i::chunk_length], axis=0) g_r_t_final /= norm t = trj.time[:chunk_length] return r, t, g_r_t_final
def structure_factor(trj, Q_range=(0.5, 50), n_points=1000, framewise_rdf=False, weighting_factor='fz', isotopes={}, probe="neutron"): """Compute the structure factor through a fourier transform of the radial distribution function. The consdered trajectory must include valid elements. Atomic form factors are estimated by atomic number. The computed structure factor is only valid for certain values of Q. The lowest value of Q that can sufficiently be described by a box of characteristic length `L` is `2 * pi / (L / 2)`. Parameters ---------- trj : mdtraj.Trajectory A trajectory for which the structure factor is to be computed. Q_range : list or np.ndarray, default=(0.5, 50) Minimum and maximum Values of the scattering vector, in `1/nm`, to be consdered. n_points : int, default=1000 framewise_rdf : boolean, default=False If True, computes the rdf frame-by-frame. This can be useful for managing memory in large systems. weighting_factor : string, optional, default='fz' Weighting factor for calculating the structure-factor, default is Faber-Ziman. See https://openscholarship.wustl.edu/etd/1358/ and http://isaacs.sourceforge.net/manual/page26_mn.html for details. isotopes: dict, optional, default=None If the scattering experiment was run with specific isotopic compositions (i.e. an NDIS experiment), specify isotopic composition as follows: { element_1.symbol: { element_1.atomic_number_1: fraction, element_1.atomic_number_2: fraction, ... }, element_2.symbol: { ... }, ... } The sum over the fraction for each isotope for each element must be 1.0. An atomic number of -1 signifies no isotopic enrichment, at which point the average scattering length will be pulled. Returns ------- Q : np.ndarray The values of the scattering vector, in `1/nm`, that was considered. S : np.ndarray The structure factor of the trajectory """ if weighting_factor not in ['fz']: raise ValueError('Invalid weighting_factor `{}` is given.' ' The only weighting_factor currently supported is `fz`.'.format( weighting_factor)) rho = np.mean(trj.n_atoms / trj.unitcell_volumes) L = np.min(trj.unitcell_lengths) top = trj.topology elements = set([a.element for a in top.atoms]) compositions = dict() form_factors = dict() rdfs = dict() Q = np.logspace(np.log10(Q_range[0]), np.log10(Q_range[1]), num=n_points) S = np.zeros(shape=(len(Q))) for elem in elements: compositions[elem.symbol] = len(top.select('element {}'.format(elem.symbol)))/trj.n_atoms form_factors[elem.symbol] = get_form_factor(elem.atomic_number, isotopes.get(elem.atomic_number, {-1: 1.0}), probe=probe) for i, q in enumerate(Q): num = 0 denom = 0 for elem in elements: denom += compositions[elem.symbol] * form_factors[elem.symbol] for (elem1, elem2) in it.product(elements, repeat=2): e1 = elem1.symbol e2 = elem2.symbol f_a = form_factors[e1] f_b = form_factors[e2] x_a = compositions[e1] x_b = compositions[e2] try: g_r = rdfs['{0}{1}'.format(e1, e2)] except KeyError: pairs = top.select_pairs(selection1='element {}'.format(e1), selection2='element {}'.format(e2)) if framewise_rdf: r, g_r = rdf_by_frame(trj, pairs=pairs, r_range=(0, L / 2), bin_width=0.001) else: r, g_r = md.compute_rdf(trj, pairs=pairs, r_range=(0, L / 2), bin_width=0.001) rdfs['{0}{1}'.format(e1, e2)] = g_r integral = simps(r ** 2 * (g_r - 1) * np.sin(q * r) / (q * r), r) if weighting_factor == 'fz': pre_factor = 4 * np.pi * rho # It's an unrestricted double sum, so non-identical pairs need to be counted twice if e1 != e2: pre_factor *= 2.0 partial_sq = (integral*pre_factor) num += (x_a*f_a*x_b*f_b) * (partial_sq) # Faber-Ziman comes out in units of barn/sr/atom. 100 is to convert between fm^2 and barn. if weighting_factor == 'fz': S[i] = num/100.0 else: S[i] = (num/(denom**2)) return Q, S
def structure_factor( trj, Q_range=(0.5, 50), n_points=1000, framewise_rdf=False, weighting_factor="fz", form="atomic", ): """Compute the structure factor through a fourier transform of the radial distribution function. The consdered trajectory must include valid elements. The computed structure factor is only valid for certain values of Q. The lowest value of Q that can sufficiently be described by a box of characteristic length `L` is `2 * pi / (L / 2)`. Parameters ---------- trj : mdtraj.Trajectory A trajectory for which the structure factor is to be computed. Q_range : list or np.ndarray, default=(0.5, 50) Minimum and maximum Values of the scattering vector, in `1/nm`, to be consdered. n_points : int, default=1000 framewise_rdf : boolean, default=False If True, computes the rdf frame-by-frame. This can be useful for managing memory in large systems. weighting_factor : string, optional, default='fz' Weighting factor for calculating the structure-factor, default is Faber-Ziman. See https://openscholarship.wustl.edu/etd/1358/ and http://isaacs.sourceforge.net/manual/page26_mn.html for details. form : string, optional, default='atomic' Method for determining form factors. If default, form factors are estimated from atomic numbers. If 'cromer-mann', form factors are determined from Cromer-Mann tables. Returns ------- Q : np.ndarray The values of the scattering vector, in `1/nm`, that was considered. S : np.ndarray The structure factor of the trajectory """ if weighting_factor not in ["fz", "al"]: raise ValueError( "Invalid weighting_factor `{}` is given." " The only weighting_factor currently supported is `fz`, and `al`." .format(weighting_factor)) rho = np.mean(trj.n_atoms / trj.unitcell_volumes) L = np.min(trj.unitcell_lengths) top = trj.topology elements = set([a.element for a in top.atoms]) compositions = dict() rdfs = dict() Q = np.logspace(np.log10(Q_range[0]), np.log10(Q_range[1]), num=n_points) S = np.zeros(shape=(len(Q))) for elem in elements: compositions[elem.symbol] = ( len(top.select("element {}".format(elem.symbol))) / trj.n_atoms) for i, q in enumerate(Q): num = 0 denom = 0 for elem in elements: denom += _get_normalize(method=weighting_factor, c=compositions[elem.symbol], f=get_form_factor(elem.symbol, q=q / 10, method=form)) for (elem1, elem2) in it.product(elements, repeat=2): e1 = elem1.symbol e2 = elem2.symbol f_a = get_form_factor(e1, q=q / 10, method=form) f_b = get_form_factor(e2, q=q / 10, method=form) x_a = compositions[e1] x_b = compositions[e2] try: g_r = rdfs["{0}{1}".format(e1, e2)] except KeyError: pairs = top.select_pairs( selection1="element {}".format(e1), selection2="element {}".format(e2), ) if framewise_rdf: r, g_r = rdf_by_frame(trj, pairs=pairs, r_range=(0, L / 2), bin_width=0.001) else: r, g_r = md.compute_rdf(trj, pairs=pairs, r_range=(0, L / 2), bin_width=0.001) rdfs["{0}{1}".format(e1, e2)] = g_r integral = simps(r**2 * (g_r - 1) * np.sin(q * r) / (q * r), r) coefficient = x_a * x_b * f_a * f_b pre_factor = 4 * np.pi * rho partial_sq = (integral * pre_factor) num += coefficient * (partial_sq) if weighting_factor == "fz": denom = denom**2 S[i] = num / denom return Q, S
def compute_van_hove(trj, chunk_length, water=False, r_range=(0, 1.0), bin_width=0.005, n_bins=None, periodic=True, opt=True): """Compute the partial van Hove function of a trajectory Parameters ---------- trj : mdtraj.Trajectory trajectory on which to compute the Van Hove function chunk_length : int length of time between restarting averaging water : bool use X-ray form factors for water that account for polarization r_range : array-like, shape=(2,), optional, default=(0.0, 1.0) Minimum and maximum radii. bin_width : float, optional, default=0.005 Width of the bins in nanometers. n_bins : int, optional, default=None The number of bins. If specified, this will override the `bin_width` parameter. Returns ------- r : numpy.ndarray r positions generated by histogram binning g_r_t : numpy.ndarray Van Hove function at each time and position """ unique_elements = list(set([a.element for a in trj.top.atoms])) norm = 0 g_r_t = None for elem1, elem2 in it.combinations_with_replacement(unique_elements[::-1], 2): r, g_r_t_partial = compute_partial_van_hove(trj=trj, chunk_length=chunk_length, selection1='element {}'.format(elem1.symbol), selection2='element {}'.format(elem2.symbol), r_range=r_range, bin_width=bin_width, n_bins=n_bins, periodic=periodic, opt=opt) concentration1 = trj.atom_slice(trj.top.select('element {}'.format(elem1.symbol))).n_atoms / trj.n_atoms concentration2 = trj.atom_slice(trj.top.select('element {}'.format(elem2.symbol))).n_atoms / trj.n_atoms form_factor1 = get_form_factor(element_name=elem1.symbol, water=water) form_factor2 = get_form_factor(element_name=elem2.symbol, water=water) coeff = form_factor1 * concentration1 * form_factor2 * concentration2 if g_r_t is None: g_r_t = np.zeros_like(g_r_t_partial) g_r_t += g_r_t_partial * coeff norm += coeff # Reshape g_r_t to better represent the discretization in both r and t g_r_t_final = np.empty(shape=(chunk_length, len(r))) for i in range(chunk_length): g_r_t_final[i, :] = np.mean(g_r_t[i::chunk_length], axis=0) g_r_t_final /= norm t = trj.time[:chunk_length] return r, t, g_r_t_final