from math import acos, pi, sin, cos, sqrt
import numpy as np
from numpy.random import uniform
from pwtools import atomic_data, _flib
from pwtools.crys import cc2cell, volume_cc, Structure
from pwtools.num import fempty
[docs]
class RandomStructureFail(Exception):
def __init__(self, msg):
self.msg = msg
def __str__(self):
return self.msg
[docs]
class RandomStructure:
"""Create a random structure, based on atom number and composition alone
(`symbols`).
The mean target cell volume and cell side lengths are determined from covalent
radii of all atoms (see pwtools.atomic_data.covalent_radii).
Examples
--------
>>> rs = crys.RandomStructure(['Si']*50)
>>> st = rs.get_random_struct()
For many atoms (~ 200), the creation takes a while b/c as more and more
atoms are already present, we need many more tries to get another random
atom into the struct. Then, _atoms_too_close() is called a lot, which is the
bottleneck. Hint: ``plot(rs.counters['coords'])``.
"""
# Notes
# -----
# * Maybe add outer loop, create new cryst_const and try again if creating
# struct failed once.
# * Maybe don't raise exceptions if struct creation fails, only print
# warning / let user decide -- add arg error={True,False}.
# * The bottleneck is get_random_struct() -- the loop over
# _atoms_too_close().
[docs]
def __init__(self,
symbols,
vol_scale=4,
angle_range = [60.0, 120.0],
vol_range_scale=[0.7,1.3],
length_range_scale=[0.7,1.3],
close_scale=1,
cell_maxtry=100,
atom_maxtry=1000,
verbose=False):
"""
Parameters
----------
symbols : list of strings
Atom symbols. Defines number of atoms and composition.
vol_scale : float
Scale volume estimated from sum of covalent spheres (lower bound
for volume). Only values > 1 make sense. This is used
to get the mean target volume (increase "the holes" between spheres).
Large values will create large cells with much space between atoms.
angle_range : sequence of 2 floats [min, max]
Range of allowed random cell angles.
vol_range_scale : sequence of 2 floats [min, max]
Scale estimated mean volume by `min` and `max` to get allowed
volume range.
length_range_scale : sequence of 2 floats [min, max]
Scale estimated mean cell side length (3rd root of mean volume)
by `min` and `max` to get allowed cell side range.
close_scale : float
Scale allowed distance between atoms (from sum of covalent radii).
Use < 1.0 to make tightly packed structures, i.e. small values will
allow close atoms, large values make big spaces but will also
make the structure generation more likely to fail.
cell_maxtry / atom_maxtry : integer, optional
Maximal attempts to create a random cell / insert random atom into
cell before RandomStructureFail is raised.
verbose : bool, optional
Print stuff while trying to generate structs.
"""
self.symbols = symbols
self.close_scale = close_scale
self.angle_range = angle_range
self.natoms = len(self.symbols)
self.cell_maxtry = cell_maxtry
self.atom_maxtry = atom_maxtry
self.verbose = verbose
self.cov_radii = np.array([atomic_data.pt[sym]['cov_rad'] for \
sym in symbols])
self.dij_min = (self.cov_radii + self.cov_radii[:,None]) * self.close_scale
self.vol_mean = 4.0/3.0 * pi * (self.cov_radii**3.0).sum() * vol_scale
self.length_mean = self.vol_mean ** (1.0/3.0)
self.vol_range = [self.vol_mean*vol_range_scale[0],
self.vol_mean*vol_range_scale[1]]
self.length_range = [self.length_mean * length_range_scale[0],
self.length_mean * length_range_scale[1]]
self.counters = {'cryst_const': None, 'coords': None}
[docs]
def get_random_cryst_const(self):
"""Create random cryst_const.
Returns
-------
cryst_const : 1d array (6,)
Raises
------
RandomStructureFail
"""
def _get():
return np.concatenate((uniform(self.length_range[0],
self.length_range[1], 3),
uniform(self.angle_range[0],
self.angle_range[1], 3)))
cnt = 1
while cnt <= self.cell_maxtry:
cc = _get()
vol = volume_cc(cc)
if not self.vol_range[0] <= vol <= self.vol_range[1]:
cc = _get()
cnt += 1
else:
self.counters['cryst_const'] = cnt
return cc
raise RandomStructureFail("failed creating random cryst_const")
def _atoms_too_close(self, iatom):
"""Check if any two atoms are too close, i.e. closer then the sum of
their covalent radii, scaled by self.close_scale.
Minimum image distances are used.
Parameters
----------
iatom : int
Index into self.coords_frac, defining the number of atoms currently
in there (iatom +1).
"""
# dist: min image dist correct only up to rmax_smith(), but we check if
# atoms are too close; too big dists are no problem; choose only upper
# triangle from array `dist`
natoms_filled = iatom + 1
coords_frac = self.coords_frac[:natoms_filled,:]
# This part is 10x slower than the fortran version --------
# distvecs_frac: (natoms_filled, natoms_filled, 3)
# distvecs: (natoms_filled, natoms_filled, 3)
# dist: (natoms_filled, natoms_filled)
##distvecs_frac = coords_frac[:,None,:] - coords_frac[None,:,:]
##distvecs_frac = min_image_convention(sij)
##distvecs = np.dot(sij, self.cell)
##dist = np.sqrt((rij**2.0).sum(axis=2))
#-----------------------------------------------------------
nn = natoms_filled
# XXX For maximum speed: dummy1 and dummy2 may be big and are allocated
# each time this method is called, which may be *many* times. Either
# re-write the fortran code to not require them as input or allocate
# them in __init__().
distsq,dummy1,dummy2 = fempty((nn,nn)), fempty((nn,nn,3)), \
fempty((nn,nn,3))
distsq, dummy1, dummy2 = _flib.distsq_frac(coords_frac,
self.cell,
1,
distsq,
dummy1,
dummy2)
dist = np.sqrt(distsq)
# This part is fast
dij_min_filled = self.dij_min[:natoms_filled,:natoms_filled]
return np.triu(dist < dij_min_filled, k=1).any()
def _add_random_atom(self, iatom):
self.coords_frac[iatom,:] = np.random.rand(3)
def _try_random_struct(self):
try:
st = self._get_random_struct()
return st
except RandomStructureFail as err:
if self.verbose:
print(err.msg)
return None
def _get_random_struct(self):
"""Generate random cryst_const and atom coords.
Returns
-------
Structure
Raises
------
RandomStructureFail
"""
self.cell = cc2cell(self.get_random_cryst_const())
self.coords_frac = np.empty((self.natoms, 3))
self.counters['coords'] = []
for iatom in range(self.natoms):
if iatom == 0:
cnt = 1
self._add_random_atom(iatom)
else:
cnt = 1
while cnt <= self.atom_maxtry:
self._add_random_atom(iatom)
if self._atoms_too_close(iatom):
self._add_random_atom(iatom)
cnt += 1
else:
break
self.counters['coords'].append(cnt)
if cnt > self.atom_maxtry:
raise RandomStructureFail("failed to create random coords for "
"iatom=%i of %i" %(iatom,
self.natoms-1))
st = Structure(symbols=self.symbols,
coords_frac=self.coords_frac,
cell=self.cell)
return st
def _get_random_struct_nofail(self):
"""Same as _get_random_struct(), but if RandomStructureFail is raised,
start over.
Returns
-------
Structure
"""
st = self._try_random_struct()
cnt = 0
while st is None:
if self.verbose:
print(" try: %i" %cnt)
st = self._try_random_struct()
cnt += 1
return st
[docs]
def get_random_struct(self, fail=True):
"""Generate random cryst_const and atom coords.
If `fail=True`, then RandomStructureFail may be raised if structure
generation fails (`cell_maxtry` or `atom_maxtry` exceeded).
If `fail=False`, then the process is repeated over and over (may run
forever). Use this if you know that your settings (all `*_scale`
inputs) are sensible + struct generation fails "sometimes" and you
absolutely want to create a struct.
Parameters
----------
fail : bool, optional
Returns
-------
Structure
Raises
------
RandomStructureFail (if `fail=True`).
"""
if fail:
return self._get_random_struct()
else:
return self._get_random_struct_nofail()
[docs]
def random_struct(*args, **kwds):
"""Shortcut for ``RandomStructure(*args, **kwds).get_random_struct()``.
"""
rs = RandomStructure(*args, **kwds)
return rs.get_random_struct(fail=True)