class TrajAnalyser: def __init__(self, traj, start=0, stop=-1, transform=no_transform): self.traj = Trajectory(traj) self.transform = transform self.numbers = self[0].get_atomic_numbers() self.species = np.unique(self.numbers).tolist() self.masses = self[0].get_masses() self.set_range(start, stop) self.indices = np.arange(0, self.numbers.shape[0]) def set_range(self, start, stop): self._start = start self._stop = stop def __getitem__(self, k): return self.transform(self.traj[k]) @property def start(self): return self._start @property def stop(self): return self.last if self._stop == -1 else self._stop @property def last(self): return self.traj.__len__() def select(self, *args): if len(args) == 0: return np.full(self.numbers.shape[0], False) elif 'all' in args: return np.full(self.numbers.shape[0], True) else: return np.stack([ self.numbers == a for b in iterable(args) for a in iterable(b) ]).any(axis=0) def select_indices(self, *args): return self.indices[self.select(*args)] def get_pair(self, i, j): return self[i], self[j] def get_rand_pair(self, s, delta): succ = False while not succ: try: i, j = s.sample_pair(delta) a, b = self[i], self[j] succ = True except: pass return a, b def get_slice(self, start=None, stop=None, step=1): if start is None: start = self.start if stop is None: stop = self.stop return start, stop, step def slice(self, **kwargs): for i in range(*self.get_slice(**kwargs)): yield self[i] def get_scalars(self, prop=('volume', ), **kwargs): data = [] for atoms in self.slice(**kwargs): data += [[getattr(atoms, f'get_{q}')() for q in prop]] return zip(*data) def center_of_mass(self, select='all', prop=('positions', ), **kwargs): I = self.select(select) data = [] for atoms in self.slice(**kwargs): data += [[ getattr(atoms, f'get_{q}')()[I].sum(axis=0) for q in prop ]] return zip(*data) def ave_vol(self, srange=None, sample_size=100, stats=None): s = Sampler(*srange) if srange else Sampler(self.start, self.stop) if stats is None: stats = mean_var v = stats([self[s.sample()].get_volume() for _ in range(sample_size)]) return v def msd(self, select='all', I=None, wrt=None, **kwargs): """ select: atoms types e.g. select=[3] for all Li atoms I: indices, if I is given, select is ignored wrt: with respect to which MD step kwargs: start, stop, step """ if I is None: I = self.select(select) start, stop, step = self.get_slice(**kwargs) if wrt is None: wrt = start if type(wrt) == int: x = self[wrt].get_positions()[I] else: x = wrt[I] d = np.array([((atoms.get_positions()[I] - x)**2).sum(axis=-1).mean() for atoms in self.slice(**kwargs)]) return np.arange(start, stop, step), d def displacements(self, select='all', deltas=None, srange=None, sample_size=100, corr=None, stats=None): I = self.select(select) s = Sampler(*srange) if srange else Sampler(self.start, self.stop) if deltas is None: deltas = get_exponential_deltas(s.start, s.stop) if corr is None: corr = correlator if stats is None: stats = mean_var data = [[ stats(data) for data in zip(*[ iterable(corr(*self.get_rand_pair(s, delta), I)) for _ in range(sample_size) ]) ] for delta in deltas] results = [ list(zip(*[dat[j] for dat in data])) for j in range(len(data[0])) ] return deltas, results def diffusion_constants(self, dt=1., select='all', sample_size=100): deltas, results = self.displacements(select=select, sample_size=sample_size, stats=stats) time = np.array(deltas) * dt msd = np.array([d.statistic for d in results[0][0]]) msd_err = np.array([d.statistic for d in results[0][2]]) smd = np.array([d.statistic for d in results[1][0]]) smd_err = np.array([d.statistic for d in results[1][2]]) Dt = tuple(a / 6 for a in get_slopes(time, msd, msd_err)) Dj = tuple(a / 6 for a in get_slopes(time, smd, smd_err)) return Dt, Dj def hist_rtp_displacements(self, delta, rmax=10., bins=[100, 30, 60], select='all', srange=None, sample_size=100): """ delta: steps returns: r, theta, phi, histogram, density (of selected atoms) """ I = self.select(select) s = Sampler(*srange) if srange else Sampler(self.start, self.stop) _bins = [ np.linspace(0, rmax, bins[0]), np.linspace(0, np.pi, bins[1]), np.linspace(-np.pi, np.pi, bins[2]) ] h = np.zeros(shape=np.array(bins) - 1) vol = [] for _ in range(sample_size): a, b = self.get_rand_pair(s, delta) vol += [a.get_volume(), b.get_volume()] d = (b.positions[I] - a.positions[I]).reshape(-1, 3) rtp = np.array(cart_coord_to_sph(*d.T)).T h += np.histogramdd(rtp, bins=_bins)[0] r, t, p = (a[:-1] + (a[1] - a[0]) / 2 for a in _bins) N = I.sum() h /= (N * sample_size) rho = N / np.array(vol).mean() return r, t, p, h, rho def get_positions(self, select='all', **kwargs): I = self.select(select) return np.stack( [atoms.get_positions()[I] for atoms in self.slice(**kwargs)]) def write_traj(self, out, **kwargs): traj = Trajectory(out, 'w') for atoms in self.slice(**kwargs): traj.write(atoms) def rdf(self, rmax, nbins, select='all', srange=None, sample_size=100, file=None): I = self.select(select) s = Sampler(*srange) if srange else Sampler(self.start, self.stop) data = AtomsData( [TorchAtoms(self[s.sample()][I]) for _ in range(sample_size)]) r, gdict = rdf(data, rmax, nbins) if file is not None: #header = 'r ' + ' '.join(f'{key}' for key in gdict.keys()) header = ' '.join(f'{k[0]}-{k[1]}' for k in gdict.keys()) out = np.stack([ r, ] + [gdict[key] for key in gdict.keys()]).T np.savetxt(file, out, header=header) return r, gdict @staticmethod def read_rdf(file): g = np.loadtxt(file) gdict = {} with open(file) as f: header = f.readline() for a, b in zip(*[header.split(), g.T]): if a == '#': r = b else: key = tuple(int(v) for v in a.split('-')) gdict[key] = b return r, gdict def attach_nl(self, cutoff=None, si=False, bw=True): if cutoff is None: coff = natural_cutoffs(self[0]) elif type(cutoff) is float: coff = np.full_like(self[0].numbers, cutoff) elif type(cutoff) is dict: coff = list(map(cutoff.get, self[0].numbers)) elif type(cutoff) is list: coff = cutoff self.nl = NeighborList(coff, skin=0.0, self_interaction=si, bothways=bw) def get_neighbors(self, j, atoms=None, only=None): if atoms: if type(atoms) == int: atoms = self[atoms] self.nl.update(atoms) k, off = self.nl.get_neighbors(j) if only: I = self.numbers[k] == only k = k[I] off = off[I] return k, off def get_mic(self, atoms, j, k, off): cells = (off[..., None].astype(np.float) * atoms.cell).sum(axis=1) r = atoms.positions[k] - atoms.positions[j] + cells return r