├── .gitignore ├── README.md ├── __init__.py ├── bicrystal.py ├── csl.py ├── graingen.py ├── latt.py ├── ldump2cfg.py ├── mdfile.py ├── mdprim.py ├── model.py ├── monocryst.py ├── pse.py ├── rotmat.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.bak 3 | *.py[co] 4 | __pycache__/ 5 | cscope.out 6 | tags 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | gosam is a bunch of Python scripts: 2 | * `bicryst.py` -- to generate atomistic model of grain boundary (bicrystal) 3 | with a coincidence site lattice (CSL) boundary in the PBC box, 4 | * `monocryst.py` -- to generate monocrystal in PBC box (atomistic model), 5 | * `graingen.py` -- create crystalline grains of given shape, with vacancies, 6 | thermal vibrations, etc. 7 | * `mdfile.py` -- convert between several file formats (AtomEye cfg, 8 | VASP POSCAR, LAMMPS, DL_POLY, XMOL XYZ). 9 | * `csl.py` -- to list coincident site lattice (CSL) grain boundaries 10 | in cubic crystals 11 | 12 | **Prerequisites:** Python 2 and [numpy](http://numpy.scipy.org/). 13 | `graingen.py` additionally requires the [qhull](http://www.qhull.org/) program. 14 | 15 | **Installation:** 16 | download, unpack if you've downloaded zip file, and use from command line. 17 | 18 | **Usage**: 19 | See [documentation in the wiki](https://github.com/wojdyr/gosam/wiki) 20 | and feel free to e-mail me with questions. Or open Github issue. 21 | Also, most of the programs print basic usage instructions when invoked 22 | without parameters. 23 | 24 | **License**: GPLv2+ (I'll consider changing it to a more liberal license 25 | if there is a good reason) 26 | 27 | **Citing**: 28 | you may cite 29 | [Marcin Wojdyr et al 2010 Modelling Simul. Mater. Sci. Eng. 18 075009]( 30 | http://dx.doi.org/10.1088/0965-0393/18/7/075009) 31 | ([preprint](http://wojdyr.github.io/Wojdyr-tilt_GB_in_SiC-MSMSE-2010.pdf)), 32 | especially if you use `bicryst.py`. 33 | 34 | **See also:** [debyer](https://github.com/wojdyr/debyer) - a bunch of tools 35 | coded in C and C++ 36 | 37 | contact: wojdyr@gmail.com 38 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # this file is part of gosam (generator of simple atomistic models) 3 | # Licence: GNU General Public License version 2 4 | 5 | # This files marks the containing directory as Python package. 6 | -------------------------------------------------------------------------------- /bicrystal.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # this file is part of gosam (generator of simple atomistic models) 3 | # Licence: GNU General Public License version 2 4 | """\ 5 | tool for generating bicrystals 6 | """ 7 | 8 | usage = """\ 9 | Usage: 10 | bicrystal.py axis plane sigma dim_x dim_y dim_z [options] output_file 11 | 12 | - axis of rotation should be given as three numbers, e.g.: "001", "111" 13 | - boundary is always at plane z = (dim_z / 2) 14 | - plane - it can be given as: 15 | * miller indices of the boundary plane in bottom monocrystal lattice 16 | * "twist" - keyword that means that plane is perpendicular to axis 17 | * miller indices prefixed with letter m (e.g. m011) meaning 18 | median plane; the boundary will be calculated as the median plane 19 | rotated by theta/2 around the axis. 20 | 21 | - instead of sigma (one number) you can give: 22 | * m,n (e.g. 23,4) 23 | * theta=value, i.e. value of angle in degrees (e.g. theta=90) 24 | - dim_x, dim_y and dim_z are in nm 25 | - options: 26 | * nofit - if this option is _not_ specified, PBC dimensions will be tuned 27 | to make the system periodic 28 | * mono1 - generate only upper half of the bicrystal, i.e. monocrystal 29 | * mono2 - generate only bottom half of the bicrystal, i.e. monocrystal 30 | * remove:dist - If there are two atoms in distance < dist [Angstroms], 31 | one of the atoms is removed. 32 | * remove2:dist - for binary systems only; like the option above, but only 33 | pairs of atoms of the same species are checked. 34 | * vacuum:length - vacuum in z direction. Makes 2D slab with z dimension 35 | increased by the length. 36 | * shift:dx,dy,dz - shift nodes in unit cell. 37 | * lattice:name - (default: SiC). For heterophase boundaries give two 38 | names separated by comma (e.g. lattice:Cu,Fe, but in 39 | such case one of the grains may not fit well into PBC). 40 | Since 2018 you may use CIF files instead of the name 41 | (i.e. lattice:my.cif). 42 | * edge:z1,z2 - Removes atoms that have y in lower half of the box and 43 | z1 < z < z2. There is a chance that this will become an edge 44 | dislocation after squeezing or running high temperature MD. 45 | 46 | Examples: 47 | bicrystal.py 001 twist 5 20 20 80 twist_s5.cfg 48 | bicrystal.py 100 m011 5 20 20 80 tilt_s5.cfg 49 | bicrystal.py 100 0,1,0 theta=90 1 1 1 tmp.cfg 50 | 51 | See the wiki for more examples. 52 | 53 | caution: the program was tested only for a few cases (may not work in others) 54 | """ 55 | 56 | import math 57 | from math import sqrt, degrees, radians, acos, ceil 58 | import sys 59 | from copy import deepcopy 60 | import numpy 61 | from numpy import dot, identity, inner, zeros 62 | from numpy import linalg 63 | from monocryst import RotatedMonocrystal, OrthorhombicPbcModel, \ 64 | get_named_lattice 65 | from utils import get_command_line 66 | import csl 67 | from rotmat import rodrigues, print_matrix, round_to_multiplicity 68 | 69 | 70 | class Bicrystal(OrthorhombicPbcModel): 71 | def __init__(self, lattice1, lattice2, dim, rot_u, rot_b, title): 72 | OrthorhombicPbcModel.__init__(self, lattice1, dim, title=title) 73 | self.mono_u = RotatedMonocrystal(lattice1, dim, rot_u) 74 | self.mono_b = RotatedMonocrystal(lattice2, dim, rot_b) 75 | 76 | 77 | def generate_atoms(self, z_margin=0.): 78 | #print "Bicrystal.generate_atoms" 79 | self.atoms = (self.mono_u.generate_atoms(upper=True, z_margin=z_margin) 80 | + self.mono_b.generate_atoms(upper=False, z_margin=z_margin)) 81 | print "Number of atoms in bicrystal: %i" % len(self.atoms) 82 | self.print_boundary_angle() 83 | 84 | def print_boundary_angle(self): 85 | def calc_angle(v1, v2): 86 | return acos( dot(v1, v2) / sqrt(inner(v1,v1) * inner(v2,v2)) ) 87 | u0 = self.mono_u.unit_cell.get_unit_shift(0) 88 | b = [self.mono_b.unit_cell.get_unit_shift(i) for i in range(3)] 89 | b += [-i for i in b] 90 | angles = [degrees(calc_angle(u0, i)) for i in b] 91 | print "angles between upper and bottom:", \ 92 | ", ".join("%.1f" % i for i in angles) 93 | 94 | 95 | class BicrystalOptions: 96 | def __init__(self): 97 | self.axis = None 98 | self.plane = None 99 | self.sigma = None 100 | self.theta = None 101 | self.m = None 102 | self.n = None 103 | self.req_dim = None 104 | self.vacuum = None # margin for dim z 105 | self.dim = None 106 | self.fit = None 107 | self.zfit = None 108 | self.mono1 = None 109 | self.mono2 = None 110 | self.remove_dist = None 111 | self.remove_dist2 = None 112 | self.all = None 113 | self.allall = None 114 | self.lattice_name = "sic" 115 | self.lattice_shift = None 116 | self.edge = None 117 | self.antiphase = False # unused 118 | 119 | 120 | def parse_sigma_and_find_theta(self, sigma_arg): 121 | if sigma_arg.startswith("theta="): 122 | sigma = None 123 | m, n = None, None 124 | theta = radians(float(sigma_arg[6:])) 125 | elif "," not in sigma_arg: 126 | if sigma_arg.startswith("u"): 127 | sigma = int(sigma_arg[1:]) 128 | min_angle = radians(45.) 129 | else: 130 | sigma = int(sigma_arg) 131 | min_angle = None 132 | r = csl.find_theta(self.axis, sigma, min_angle=min_angle) 133 | if r is None: 134 | print "CSL not found! Wrong sigma or axis?" 135 | sys.exit() 136 | theta, m, n = r 137 | else: 138 | m_, n_ = sigma_arg.split(",") 139 | m, n = int(m_), int(n_) 140 | sigma = csl.get_cubic_sigma(self.axis, m, n) 141 | theta = csl.get_cubic_theta(self.axis, m, n) 142 | if sigma is not None: 143 | print "-------> sigma = %i" % sigma 144 | print "-------> theta = %.3f deg" % degrees(theta) 145 | self.sigma = sigma 146 | self.theta = theta 147 | self.m = m 148 | self.n = n 149 | 150 | 151 | def find_dim(self, min_dim): 152 | print "-------> min. dim. [A]: ", min_dim[0], min_dim[1], min_dim[2] 153 | dim = [i * 10 for i in self.req_dim] # nm -> A 154 | if self.mono1 or self.mono2: 155 | dim[2] *= 2 156 | fit_dim = [] 157 | if self.fit: 158 | fit_dim += [0, 1] 159 | if self.zfit: 160 | fit_dim += [2] 161 | for i in fit_dim: 162 | mult = ceil(float(dim[i]) / min_dim[i]) or 1 163 | dim[i] = mult * min_dim[i] 164 | # dim[i] = round_to_multiplicity(min_dim[i], dim[i]) 165 | if self.vacuum: 166 | dim[2] += self.vacuum # margin in dim z 167 | print "-------> dimensions [A]: ", dim[0], dim[1], dim[2] 168 | self.dim = dim 169 | 170 | 171 | def print_boundary_type(axis, plane, theta): 172 | if (plane * sum(abs(axis)) == axis * sum(abs(plane))).all(): 173 | bt = "twist" 174 | elif inner(plane, axis) == 0: 175 | R = rodrigues(axis, theta, verbose=False) 176 | p2 = dot(linalg.inv(R), plane) 177 | plane2 = csl.scale_to_integers(p2) 178 | bt = "tilt (%i %i %i) (%i %i %i)" % (tuple(plane) + tuple(plane2)) 179 | else: 180 | bt = "mixed" 181 | return bt 182 | 183 | 184 | def parse_args(): 185 | if len(sys.argv) < 7: 186 | print usage 187 | sys.exit() 188 | 189 | opts = BicrystalOptions() 190 | opts.axis = csl.parse_miller(sys.argv[1]) 191 | print "-------> rotation axis: [%i %i %i]" % tuple(opts.axis) 192 | 193 | opts.parse_sigma_and_find_theta(sys.argv[3]) 194 | 195 | plane = sys.argv[2] 196 | if plane == "twist": 197 | opts.plane = opts.axis.copy() 198 | elif plane.startswith("m"): 199 | m_plane = csl.parse_miller(plane[1:]) 200 | if inner(m_plane, opts.axis) != 0: 201 | raise ValueError("Axis must be contained in median plane.") 202 | R = rodrigues(opts.axis, opts.theta / 2., verbose=False) 203 | plane_ = dot(R, m_plane) 204 | opts.plane = csl.scale_to_integers(plane_) 205 | else: 206 | opts.plane = csl.parse_miller(plane) 207 | print "-------> boundary plane: (%i %i %i)" % tuple(opts.plane) 208 | bt = print_boundary_type(opts.axis, opts.plane, opts.theta) 209 | print " boundary type:", bt 210 | 211 | opts.req_dim = [float(eval(i, math.__dict__)) for i in sys.argv[4:7]] 212 | 213 | options = sys.argv[7:-1] 214 | for i in options: 215 | if i == "nofit": 216 | assert opts.fit is None 217 | opts.fit = False 218 | if i == "nozfit": 219 | assert opts.zfit is None 220 | opts.zfit = False 221 | elif i == "mono1": 222 | assert opts.mono1 is None 223 | opts.mono1 = True 224 | elif i == "mono2": 225 | assert opts.mono2 is None 226 | opts.mono2 = True 227 | elif i == "all": 228 | assert opts.all is None and opts.allall is None 229 | opts.all = True 230 | elif i == "allall": 231 | assert opts.allall is None and opts.all is None 232 | opts.allall = True 233 | elif i.startswith("remove:"): 234 | assert opts.remove_dist is None 235 | opts.remove_dist = float(i[7:]) 236 | elif i.startswith("remove2:"): 237 | assert opts.remove_dist2 is None 238 | opts.remove_dist2 = float(i[8:]) 239 | elif i.startswith("vacuum:"): 240 | assert opts.vacuum is None 241 | opts.vacuum = float(i[7:]) * 10. #nm -> A 242 | elif i.startswith("lattice:"): 243 | opts.lattice_name = i[8:] 244 | elif i.startswith("shift:"): 245 | s = i[6:].split(",") 246 | if len(s) != 3: 247 | raise ValueError("Wrong format of shift parameter") 248 | opts.lattice_shift = [float(i) for i in s] 249 | elif i.startswith("edge:"): 250 | try: 251 | z1, z2 = i[5:].split(",") 252 | opts.edge = (float(z1), float(z2)) 253 | except (TypeError, ValueError): 254 | raise ValueError("Wrong format of edge parameter") 255 | else: 256 | raise ValueError("Unknown option: %s" % i) 257 | # default values 258 | if opts.fit is None: 259 | opts.fit = True 260 | if opts.zfit is None: 261 | opts.zfit = True 262 | if opts.mono1 is None: 263 | opts.mono1 = False 264 | if opts.mono2 is None: 265 | opts.mono2 = False 266 | #if opts.remove_dist is None: 267 | # opts.remove_dist = 0.8 * opts.atom_min_dist 268 | 269 | opts.output_filename = sys.argv[-1] 270 | return opts 271 | 272 | 273 | def main(): 274 | opts = parse_args() 275 | 276 | # R is a matrix that transforms lattice in the bottom monocrystal 277 | # to lattice in the upper monocrystal 278 | R = rodrigues(opts.axis, opts.theta) 279 | 280 | if opts.sigma: 281 | # C is CSL primitive cell 282 | C = csl.find_csl_matrix(opts.sigma, R) 283 | print_matrix("CSL primitive cell", C) 284 | 285 | ## and now we determine CSL for fcc lattice 286 | #C = csl.pc2fcc(C) 287 | #C = csl.beautify_matrix(C) 288 | #print_matrix("CSL cell for fcc:", C) 289 | else: 290 | C = identity(3) 291 | 292 | # CSL-lattice must be periodic is our system. 293 | # * PBC box must be orthonormal 294 | # * boundaries must be perpendicular to z axis of PBC box 295 | 296 | print "CSL cell with z || [%s %s %s]" % tuple(opts.plane), 297 | Cp = csl.make_parallel_to_axis(C, col=2, axis=opts.plane) 298 | print_matrix("", Cp) 299 | 300 | min_pbc = csl.find_orthorhombic_pbc(Cp) 301 | print_matrix("Minimal(?) orthorhombic PBC", min_pbc) 302 | 303 | min_dim = [] 304 | pbct = min_pbc.transpose().astype(float) 305 | rot = zeros((3, 3)) 306 | for i in range(3): 307 | length = sqrt(inner(pbct[i], pbct[i])) 308 | rot[i] = pbct[i] / length 309 | min_dim.append(length) 310 | invrot = rot.transpose() 311 | assert (numpy.abs(invrot - linalg.inv(rot)) < 1e-9).all(), "%s != %s" % ( 312 | invrot, linalg.inv(rot)) 313 | #print "hack warning: min_dim[1] /= 2." 314 | #min_dim[1] /= 2. 315 | 316 | if "," in opts.lattice_name: 317 | name1, name2 = opts.lattice_name.split(",") 318 | lattice1 = get_named_lattice(name1) 319 | lattice2 = get_named_lattice(name2) 320 | else: 321 | lattice1 = get_named_lattice(opts.lattice_name) 322 | lattice2 = deepcopy(lattice1) 323 | 324 | if opts.lattice_shift: 325 | lattice1.shift_nodes(opts.lattice_shift) 326 | lattice2.shift_nodes(opts.lattice_shift) 327 | 328 | # the anti-phase GB can be made by swapping two species in one grain 329 | if opts.antiphase: 330 | assert lattice2.count_species() == 2 331 | lattice2.swap_node_atoms_names() 332 | 333 | a = lattice1.unit_cell.a 334 | opts.find_dim([i * a for i in min_dim]) 335 | 336 | #rot_mat1 = rodrigues(opts.axis, rot1) 337 | #rot_mat2 = rodrigues(opts.axis, rot2) 338 | rot_mat1 = dot(linalg.inv(R), invrot) 339 | rot_mat2 = invrot 340 | #print "rot1", "det=%g" % linalg.det(rot_mat1), rot_mat1 341 | #print "rot2", "det=%g" % linalg.det(rot_mat2), rot_mat2 342 | 343 | title = get_command_line() 344 | if opts.mono1: 345 | config = RotatedMonocrystal(lattice1, opts.dim, rot_mat1, 346 | title=title) 347 | elif opts.mono2: 348 | config = RotatedMonocrystal(lattice2, opts.dim, rot_mat2, 349 | title=title) 350 | else: 351 | config = Bicrystal(lattice1, lattice2, opts.dim, rot_mat1, rot_mat2, 352 | title=title) 353 | config.generate_atoms(z_margin=opts.vacuum) 354 | 355 | if not opts.mono1 and not opts.mono2 and opts.remove_dist > 0: 356 | print "Removing atoms in distance < %s ..." % opts.remove_dist 357 | config.remove_close_neighbours(opts.remove_dist) 358 | 359 | if opts.remove_dist2: 360 | a_atoms = [] 361 | b_atoms = [] 362 | a_name = config.atoms[0].name 363 | for i in config.atoms: 364 | if i.name == a_name: 365 | a_atoms.append(i) 366 | else: 367 | b_atoms.append(i) 368 | config.atoms = [] 369 | for aa in a_atoms, b_atoms: 370 | print "Removing atoms where %s-%s distance is < %s ..." % ( 371 | aa[0].name, aa[0].name, opts.remove_dist2) 372 | config.remove_close_neighbours(distance=opts.remove_dist2, atoms=aa) 373 | config.atoms += aa 374 | 375 | if opts.edge: 376 | z1, z2 = opts.edge 377 | sel = [n for n, a in enumerate(config.atoms) 378 | if 0 < a.pos[1] <= config.pbc[1][1] / 2. and z1 < a.pos[2] < z2] 379 | print "edge: %d atoms is removed" % len(sel) 380 | for i in reversed(sel): 381 | del config.atoms[i] 382 | 383 | if opts.all: 384 | #config.output_all_removal_possibilities(opts.output_filename) 385 | config.apply_all_possible_cutoffs_to_stgb(opts.output_filename, 386 | single_cutoff=True) 387 | return 388 | 389 | if opts.allall: 390 | config.apply_all_possible_cutoffs_to_stgb(opts.output_filename, 391 | single_cutoff=False) 392 | return 393 | 394 | config.export_atoms(opts.output_filename) 395 | 396 | 397 | if __name__ == '__main__': 398 | main() 399 | 400 | 401 | -------------------------------------------------------------------------------- /csl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # this file is part of gosam (generator of simple atomistic models) 3 | # Licence: GNU General Public License version 2 4 | """\ 5 | Coincidence Site Lattice related utilities. 6 | """ 7 | usage_string = """\ 8 | Usage: 9 | csl.py hkl [limit=M] [max_angle=A] - list all CSL boundaries with rotation 10 | axis [hkl], sigma < M (default: 1000) 11 | and angle < A (default: 90) 12 | 13 | csl.py hkl sigma - details about CSL with given sigma 14 | 15 | csl.py hkl m n - details about CSL generated with integers m and n 16 | 17 | Examples: 18 | csl.py 100 # generates a very long list of GBs 19 | csl.py 100 limit=50 max_angle=45 # generates a shorter list 20 | csl.py 111 31 # show details of the specified GB 21 | """ 22 | 23 | import sys 24 | import functools 25 | from math import degrees, atan, sqrt, pi, ceil 26 | import numpy 27 | from numpy import array, identity, dot, inner 28 | from numpy.linalg import inv, det, solve 29 | from rotmat import rodrigues, print_matrix 30 | 31 | def gcd(a, b): 32 | "Returns the Greatest Common Divisor" 33 | assert isinstance(a, int) 34 | assert isinstance(b, int) 35 | while a: 36 | a, b = b%a, a 37 | return b 38 | 39 | # 0 is coprime only with 1 40 | def coprime(a, b): 41 | return gcd(a,b) in (0, 1) 42 | 43 | def gcd_array(a): 44 | r = abs(a[0]) 45 | for i in a[1:]: 46 | if i != 0: 47 | r = gcd(r, abs(i)) 48 | return r 49 | 50 | 51 | def parse_miller(s): 52 | if len(s) == 3 and s.isdigit(): 53 | return array([int(s[i]) for i in range(3)]) 54 | elif ',' in s: 55 | sp = s.split(",") 56 | assert len(sp) == 3 57 | return array([int(i) for i in sp]) 58 | else: 59 | raise ValueError("Can't parse miller indices: %s" % s) 60 | 61 | 62 | def get_cubic_sigma(hkl, m, n=1): 63 | sqsum = inner(hkl, hkl) 64 | sigma = m*m + n*n * sqsum 65 | while sigma != 0 and sigma % 2 == 0: 66 | sigma /= 2 67 | return (sigma if sigma > 1 else None) 68 | 69 | def get_cubic_theta(hkl, m, n=1): 70 | h,k,l = hkl 71 | sqsum = h*h + k*k + l*l 72 | assert sqsum > 0 73 | if m > 0: 74 | return 2 * atan(sqrt(sqsum) * n / m) 75 | else: 76 | return pi 77 | 78 | def get_theta_m_n_list(hkl, sigma, verbose=False): 79 | if sigma == 1: 80 | return [(0., 0, 0)] 81 | thetas = [] 82 | 83 | # From Grimmer, Acta Cryst. (1984). A40, 108-112 84 | # S = m^2 + (u^2+v^2+w^2) n^2 (eq. 2) 85 | # S = alpha * Sigma (eq. 4) 86 | # where alpha = 1, 2 or 4. 87 | # Since (u^2+v^2+w^2) n^2 > 0, 88 | # thus alpha * Sigma > m^2 => m^2 < 4 * Sigma 89 | max_m = int(ceil(sqrt(4*sigma))) 90 | 91 | for m in range(max_m): 92 | for n in range(1, max_m): 93 | if not coprime(m, n): 94 | continue 95 | s = get_cubic_sigma(hkl, m, n) 96 | if s != sigma: 97 | continue 98 | theta = get_cubic_theta(hkl, m, n) 99 | if verbose: 100 | print "m=%i n=%i" % (m, n), "%.3f" % degrees(theta) 101 | thetas.append((theta, m, n)) 102 | return thetas 103 | 104 | def find_theta(hkl, sigma, verbose=True, min_angle=None): 105 | thetas = get_theta_m_n_list(hkl, sigma, verbose=verbose) 106 | if min_angle: 107 | thetas = [i for i in thetas if i[0] >= min_angle] 108 | if thetas: 109 | return min(thetas, key= lambda x: x[0]) 110 | 111 | 112 | def _get_unimodular_transformation(): 113 | "generator of a few possible unimodular transformations" 114 | # randomly choosen 115 | yield identity(3) 116 | yield array([[1, 0, 1], 117 | [0, 1, 0], 118 | [0, 1, 1]]) 119 | yield array([[1, 0, 1], 120 | [0, 1, 0], 121 | [0, 1, -1]]) 122 | yield array([[1, 0, 1], 123 | [0, 1, 0], 124 | [-1,1, 0]]) 125 | yield array([[1, 0, 1], 126 | [1, 1, 0], 127 | [1, 1, 1]]) 128 | 129 | 130 | def _get_S(): 131 | Sp = identity(3) # for primitive cubic 132 | Sb = array([[0.5, -0.5, 0], 133 | [0.5, 0.5, 0], 134 | [0.5, 0.5, 1]]) # body-centered cubic 135 | Sf = array([[0.5, 0.5, 0], 136 | [0, 0.5, 0.5], 137 | [0.5, 0, 0.5]]) # face-centered cubic 138 | # Sf doesn't work? 139 | return Sp 140 | 141 | 142 | def transpose_3x3(f): 143 | """decorator; transpose the first argument and the return value (both 144 | should be 3x3 arrays). This makes column operations easier""" 145 | @functools.wraps(f) 146 | def wrapper(*args, **kwds): 147 | args_list = list(args) 148 | assert args_list[0].shape == (3,3) 149 | args_list[0] = args_list[0].transpose() 150 | ret_val = f(*args_list, **kwds) 151 | assert ret_val.shape == (3,3) 152 | return ret_val.transpose() 153 | return wrapper 154 | 155 | 156 | @transpose_3x3 157 | def beautify_matrix(T): 158 | # We don't want to change the lattice. 159 | # We use only elementary column operations that don't change det 160 | def looks_better(a, b): 161 | x = numpy.abs(a) 162 | y = numpy.abs(b) 163 | #return x.sum() < y.sum() 164 | #return x.sum() < y.sum() or (x.sum() == y.sum() and x.max() < y.max()) 165 | return x.max() < y.max() or (x.max() == y.max() and x.sum() < y.sum()) 166 | 167 | def try_add(a, b): 168 | changed = False 169 | while looks_better(a+b, a): 170 | a += b 171 | changed = True 172 | return changed 173 | 174 | def try_add_sub(a, b): 175 | return try_add(a, b) or try_add(a, -b) 176 | 177 | while True: 178 | changed = False 179 | for i in range(3): 180 | for j in range(3): 181 | if i != j and not changed: 182 | changed = try_add_sub(T[i], T[j]) 183 | if changed: 184 | break 185 | if not changed: 186 | break 187 | 188 | return T 189 | 190 | 191 | @transpose_3x3 192 | def make_parallel_to_axis(T, col, axis): 193 | """\ 194 | T: matrix 3x3, i.e. 3 vectors, 2*T is integer matrix 195 | axis: vector (3) 196 | return value: 197 | matrix T is transformed using operations: 198 | - interchanging two columns 199 | - adding a multiple of one column to another, 200 | - multiplying column by -1 201 | such that the result matrix has the same det 202 | and has first vector == axis 203 | the transformation is _not_ rotation 204 | """ 205 | double_T = False 206 | if not is_integer(T): 207 | T *= 2 # make it integer, will be /=2 at the end 208 | double_T = True 209 | axis = array(axis) 210 | c = solve(T.transpose(), axis) # c . T == axis 211 | if not is_integer(c): 212 | mult = find_smallest_multiplier(c) 213 | c *= mult 214 | c = c.round().astype(int) 215 | #print "c", c 216 | sel_val = min([i for i in c if i != 0], key=abs) 217 | if abs(sel_val) != 1: # det must be changed 218 | print "\n\tWARNING: Volume increased by %i" % abs(sel_val) 219 | idx = c.tolist().index(sel_val) 220 | #print idx, sel_val 221 | if idx != col: 222 | # change sign to keep the same det 223 | T[idx], T[col] = T[col].copy(), -T[idx] 224 | c[idx], c[col] = c[col], -c[idx] 225 | 226 | T[col] = dot(c,T) 227 | 228 | if c[col] < 0: # sign of det was changed, change it again 229 | T[1] *= -1 230 | 231 | if double_T: 232 | T /= 2. 233 | 234 | return T 235 | 236 | 237 | def is_integer(a, epsilon=1e-7): 238 | "return true if numpy Float array consists off all integers" 239 | return (numpy.abs(a - numpy.round(a)) < epsilon).all() 240 | 241 | def find_smallest_multiplier(a, max_n=1000): 242 | """return the smallest positive integer n such that matrix a multiplied 243 | by n is an integer matrix 244 | """ 245 | for i in range(1, max_n): 246 | if is_integer(i*a): 247 | return i 248 | raise ValueError("Sorry, we can't make this matrix integer:\n%s" % a) 249 | 250 | def find_smallest_real_multiplier(a, max_n=1000): 251 | """return the smallest positive real f such that matrix `a' multiplied 252 | by f is an integer matrix 253 | """ 254 | # |the smallest non-zero element| 255 | m = min(abs(i) for i in a if abs(i) > 1e-9) 256 | for i in range(1, max_n): 257 | t = i / float(m) 258 | if is_integer(t * a): 259 | return t 260 | raise ValueError("Sorry, we can't make this matrix integer:\n%s" % a) 261 | 262 | def scale_to_integers(v): 263 | return array(v * find_smallest_real_multiplier(v)).round().astype(int) 264 | 265 | @transpose_3x3 266 | def make_csl_from_0_lattice(T, n): 267 | if n < 0: 268 | T[0] *= -1 269 | n *= -1 270 | while True: 271 | m = [find_smallest_multiplier(T[i]) for i in (0,1,2)] 272 | prod = m[0] * m[1] * m[2] 273 | #print "prod", prod, n 274 | if prod <= n: 275 | for i in range(3): 276 | T[i] *= m[i] 277 | if prod < n: 278 | assert n % prod == 0 279 | T[0] *= n / prod 280 | break 281 | else: 282 | changed = False 283 | for i in range(3): 284 | for j in range(3): 285 | if changed or i == j or m[i] == 1 or m[j] == 1: 286 | continue 287 | if m[i] <= m[j]: 288 | a, b = i, j 289 | else: 290 | a, b = j, i 291 | for k in plus_minus_gen(m[b]): 292 | if find_smallest_multiplier(T[a] + k * T[b]) < m[a]: 293 | T[a] += k * T[b] 294 | changed = True 295 | break 296 | assert changed, "Problem when making CSL from 0-lattice" 297 | assert is_integer(T) 298 | return T.round().astype(int) 299 | 300 | 301 | def find_csl_matrix(sigma, R): 302 | """\ 303 | Find matrix that determines the coincidence site lattice 304 | for cubic structures. 305 | Parameters: 306 | sigma: CSL sigma 307 | R: rotation matrix 308 | centering: "f" for f.c.c., "b" for b.c.c. and None for p.c. 309 | Return value: 310 | matrix, which column vectors are the unit vectors of the CSL. 311 | Based on H. Grimmer et al., Acta Cryst. (1974) A30, 197 312 | """ 313 | 314 | S = _get_S() 315 | 316 | Rs = dot(dot(inv(S), inv(R)), S) 317 | #print "xxx", inv(R) 318 | #print Rs 319 | found = False 320 | # searching for unimodular transformation that makes det(Tp) != 0 321 | for U in _get_unimodular_transformation(): 322 | assert det(U) in (1, -1) 323 | Tp = identity(3) - dot(U, Rs) 324 | if abs(det(Tp)) > 1e-6: 325 | found = True 326 | print "Unimodular transformation used:\n%s" % U 327 | break 328 | if not found: 329 | print "Error. Try another unimodular transformation U to calculate T'" 330 | sys.exit(1) 331 | 332 | Xp = numpy.round(inv(Tp), 12) 333 | print "0-lattice:\n%s" % Xp 334 | n = round(sigma / det(Xp), 7) 335 | # n is an integral number of 0-lattice units per CSL unit 336 | print "det(X')=",det(Xp), " n=", n 337 | csl = make_csl_from_0_lattice(Xp, n) 338 | assert is_integer(csl) 339 | csl = csl.round().astype(int) 340 | return beautify_matrix(csl) 341 | 342 | 343 | def plus_minus_gen(n): 344 | for i in xrange(1, n): 345 | yield i 346 | yield -i 347 | 348 | def zero_plus_minus_gen(n): 349 | yield 0 350 | for i in plus_minus_gen(n): 351 | yield i 352 | 353 | 354 | @transpose_3x3 355 | def find_orthorhombic_pbc(M): 356 | """\ 357 | We don't change the last axis, 358 | because it was set properly in make_parallel_to_axis(). 359 | 360 | Let M = [x, y, z], pbc = [x2, y2, z2], 361 | where M and pbc are matrices 3x3, and x, y, z, x2, y2, z2 are vectors. 362 | We search for multipliers b,c,d,e,f,g such that pbc is integer matrix, 363 | x2 = b x + d y + e z 364 | y2 = f x + c y + g z 365 | z2 = 0 + 0 + 1 z 366 | z2 = z/GCD(z), b != 0, c != 0, 367 | and max(|x2|, |y2|, |z2|) has the smallest value possible. 368 | (This description is a bit simplified, for details see the code) 369 | 370 | In matrix notation: [b f 0] 371 | M x [d c 0] = pbc 372 | [e g 1] 373 | """ 374 | # M is "half-integer" when using pc2fcc(). 375 | # BTW I'm not sure if pc2fcc() is working properly. 376 | doubleM = not is_integer(M) 377 | if doubleM: 378 | M *= 2 # make it integer, will be /=2 at the end 379 | 380 | assert is_integer(M), M 381 | M = M.round().astype(int) 382 | 383 | # We are searching for solution by iteration over possible b,d,c values, 384 | # -max_multiplier < b,d,c < max_multiplier. 385 | # Increasing max_multiplier obviously slows down the program. 386 | max_multiplier = 27 387 | 388 | pbc = None 389 | max_sq = 0 390 | x, y, z = M 391 | 392 | x_ = x / gcd_array(x) 393 | y_ = y / gcd_array(y) 394 | z_ = z / gcd_array(z) 395 | #print "gcd_array", z, gcd_array(z) 396 | Mx = array([x, y_, z_]) 397 | My = array([x_, y, z_]) 398 | #print "mx",Mx 399 | #print "my",My 400 | 401 | z2 = z_ # previously: z2 = z 402 | mxz = dot(Mx, z2) 403 | myz = dot(My, z2) 404 | for b in plus_minus_gen(max_multiplier): 405 | for d in zero_plus_minus_gen(max_multiplier): 406 | e_ = - (mxz[0] * b + mxz[1] * d) / float(mxz[2]) 407 | e = int(round(e_)) 408 | if abs(e - e_) < 1e-7: 409 | x2 = dot([b,d,e], Mx) 410 | mxy = dot(My, x2) 411 | aa = array([[mxy[0],mxy[2]], 412 | [myz[0],myz[2]]]) 413 | bb = array([-mxy[1], -myz[1]]) 414 | aa_invertible = (abs(det(aa)) > 1e-7) 415 | for c in plus_minus_gen(max_multiplier): 416 | # z2 . y2 == 0 and x2 . y2 == 0 => 417 | # f * mxy[0] + g * mxy[2] == -c * mxy[1] 418 | # f * myz[0] + g * myz[2] == -c * myz[1] 419 | if aa_invertible: 420 | fg = solve(aa, c * bb) 421 | else: # special case, i'm not sure if handled properly 422 | for f in zero_plus_minus_gen(max_multiplier): 423 | g_ = - (myz[0] * f + myz[1] * c) / float(myz[2]) 424 | g = int(round(g_)) 425 | if abs(g - g_) < 1e-7: 426 | y2 = dot([f,c,g], My) 427 | if inner(x2, y2) == 0: 428 | fg = array([f, g]) 429 | break 430 | else: 431 | continue 432 | 433 | if is_integer(fg) and ( 434 | numpy.abs(fg) < max_multiplier - 0.5).all(): 435 | f, g = fg.round().astype(int) 436 | y2 = dot([f,c,g], My) 437 | max_sq_ = max(dot(x2,x2), dot(y2,y2), dot(z2,z2)) 438 | if pbc is None or max_sq_ < max_sq: 439 | pbc = array([x2, y2, z2]) 440 | max_sq = max_sq_ 441 | if pbc is None: 442 | print "No orthorhombic PBC found. (you may increase max_multiplier)" 443 | sys.exit() 444 | 445 | if doubleM: 446 | pbc /= 2. 447 | 448 | # we prefer determinant to be positive (negative one would later cause 449 | # inversion in addition to rotation) 450 | if det(pbc) < 0: 451 | pbc[0] = -pbc[0] 452 | 453 | # optionally swap x2 with y2 454 | id = identity(3) 455 | if (pbc[1] == id[0]).all() or (pbc[0] == -id[1]).all(): 456 | pbc[[0,1]] = pbc[1], -pbc[0] 457 | elif (pbc[1] == -id[0]).all() or (pbc[0] == id[1]).all(): 458 | # note that this didn't work: 459 | # pbc[0], pbc[1] = -pbc[1], pbc[0] 460 | # because pbc[0] is a view, not copy 461 | pbc[[0,1]] = -pbc[1], pbc[0] 462 | 463 | return pbc 464 | 465 | 466 | def find_type(type, Cp): 467 | for i,j,k in (0,0,1), (0,1,0), (1,0,0), (0,1,1), (1,0,1), (1,1,0), (1,1,1): 468 | if ((i * Cp[0] + j * Cp[1] + k * Cp[2]) % 2 == type).all(): 469 | return [i,j,k] 470 | raise ValueError("find_type: %s not found" % type) 471 | 472 | # see the paper by Grimmer, 1.3.1-1.3.3 473 | @transpose_3x3 474 | def pc2fcc(Cp): 475 | t1 = find_type([0,1,1], Cp) 476 | t2 = find_type([1,0,1], Cp) 477 | pos1 = t1.index(1) 478 | pos2 = t2.index(1) 479 | if pos2 == pos1: 480 | try: 481 | pos2 = t2.index(1, pos1+1) 482 | except ValueError: 483 | pos1 = t1.index(1, pos1+1) 484 | Z = identity(3) 485 | Z[pos1] = array(t1) / 2. 486 | Z[pos2] = array(t2) / 2. 487 | #print_matrix("Z (in pc2fcc)", Z.transpose()) 488 | return dot(Z, Cp) 489 | 490 | def print_list(hkl, max_angle, limit): 491 | print "[max. sigma: %s, max angle: %s deg.]" % (limit, max_angle) 492 | data = [] 493 | for i in range(limit): 494 | tt = get_theta_m_n_list(hkl, i, verbose=False) 495 | for t in tt: 496 | theta, m, n = t 497 | if degrees(theta) <= max_angle: 498 | tup = (i, degrees(theta), m, n) 499 | data.append(tup) 500 | print "sigma=%3i theta=%5.2f m=%3i n=%3i" % tup 501 | 502 | data.sort(key= lambda x: x[1]) 503 | print " ============= Sorted by theta ================ " 504 | for i in data: 505 | print "sigma=%3i theta=%5.2f m=%3i n=%3i" % i 506 | 507 | 508 | def print_details(hkl, m, n): 509 | sigma = get_cubic_sigma(hkl, m, n) 510 | theta = get_cubic_theta(hkl, m, n) 511 | print "sigma=%d, theta=%.3f, m=%d, n=%d, axis=[%d,%d,%d]" % ( 512 | sigma, degrees(theta), m, n, hkl[0], hkl[1], hkl[2]) 513 | R = rodrigues(hkl, theta) 514 | print 515 | print "R * sigma =\n%s" % (R * sigma) 516 | C = find_csl_matrix(sigma, R) 517 | print "CSL primitive cell (det=%s):\n%s" % (det(C), C) 518 | ## optional, for FCC 519 | #C = pc2fcc(C) 520 | #C = beautify_matrix(C) 521 | #print_matrix("CSL cell for fcc:", C) 522 | 523 | Cp = make_parallel_to_axis(C, col=2, axis=hkl) 524 | if (Cp != C).any(): 525 | print "after making z || %s:\n%s" % (hkl, Cp) 526 | pbc = find_orthorhombic_pbc(Cp) 527 | print_matrix("Minimal(?) orthorhombic PBC", pbc) 528 | 529 | 530 | def main(): 531 | # parse keyword options 532 | limit=1000 533 | max_angle=90 534 | for a in sys.argv[1:]: 535 | if '=' in a: 536 | key, value = a.split("=", 1) 537 | if key == "limit": 538 | limit = int(value) 539 | elif key == "max_angle": 540 | max_angle = float(value) 541 | else: 542 | raise KeyError("Unknown option: " + key) 543 | 544 | args = [a for a in sys.argv[1:] if '=' not in a] 545 | if len(args) < 1 or len(args) > 3: 546 | print usage_string 547 | return 548 | 549 | hkl = parse_miller(args[0]) 550 | 551 | if len(args) == 1: 552 | print_list(hkl, max_angle=max_angle, limit=limit) 553 | elif len(args) == 2: 554 | sigma = int(args[1]) 555 | thetas = get_theta_m_n_list(hkl, sigma, verbose=False) 556 | thetas.sort(key = lambda x: x[0]) 557 | for theta, m, n in thetas: 558 | print "m=%2d n=%2d %7.3f" % (m, n, degrees(theta)) 559 | if not thetas: 560 | print "Not found." 561 | elif len(args) == 3: 562 | m = int(sys.argv[2]) 563 | n = int(sys.argv[3]) 564 | try: 565 | print_details(hkl, m, n) 566 | except KeyboardInterrupt: 567 | print " Interrupted. Exiting." 568 | 569 | 570 | 571 | if __name__ == '__main__': 572 | main() 573 | 574 | 575 | -------------------------------------------------------------------------------- /graingen.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # this file is part of gosam (generator of simple atomistic models) 3 | # Licence: GNU General Public License version 2 4 | # $Id$ 5 | """ 6 | Module for geometrical modelling of crystal grain. 7 | 8 | One of possible uses is as follows: 9 | 10 | 1 Define unit cell (eg. for triclinic - a, b, c, alpha, beta, gamma) 11 | 12 | 2 Compute parameters of unit cell volume, reciprocal lattice parameters, 13 | transformation matrix M (triclinic -> orthonormal axes), ... 14 | 15 | 3 Define atom positions in unit cell (nodes in cell * atoms in node) 16 | 17 | 4 Define grain surfaces and pressures - set of planes, 18 | each defined by (hkl) and r - distance from origin, 19 | and, optionally, distance D_shell and function emulating pressure 20 | (changing position) if distance from atom to plane is smaller then D_shell 21 | 22 | 5 Compute parameters of normal plane equations of surfaces. 23 | 24 | 6 Get min/max value along a,b and c axes 25 | this step includes computing halfspace intersection about a point 26 | (using qhull program) - it should give all vertices. 27 | 28 | 7 (optional) Show shape of grain using Geomview. 29 | 30 | 8 Iterate over all possible cells (using min/max values) and atoms. 31 | Checking if _node_ is inside of grain; if atoms are in grain shell 32 | (distance from surface < D_shell), moving atoms in shell according 33 | to given function (in direction normal to surface). 34 | 35 | 9 (optional) Make vacancies 36 | 37 | 9 (optional) Modify atom position (eg. to simulate thermal vibation) 38 | 39 | 10 Write output 40 | """ 41 | 42 | from math import cos, sin, acos, asin, sqrt, pi, floor, ceil, radians, degrees 43 | from subprocess import Popen, PIPE 44 | from tempfile import NamedTemporaryFile 45 | import commands 46 | import sys 47 | import os.path 48 | from numpy import array, zeros, dot, sometrue, inner, cross 49 | # file formats 50 | from mdfile import * 51 | from latt import * 52 | import mdprim 53 | from model import Model 54 | from rotmat import rodrigues 55 | 56 | 57 | fcc_nodes = [ 58 | (0.0, 0.0, 0.0), 59 | (0.5, 0.5, 0.0), 60 | (0.0, 0.5, 0.5), 61 | (0.5, 0.0, 0.5), 62 | ] 63 | 64 | bcc_nodes = [ 65 | (0.0, 0.0, 0.0), 66 | (0.5, 0.5, 0.5) 67 | ] 68 | 69 | 70 | class UnexpectedArgsError(Exception): 71 | def __init__(self, value): 72 | Exception.__init__(self) 73 | self.value = value 74 | def __str__(self): 75 | return repr(self.value) 76 | 77 | 78 | class NotInitializedError(Exception): 79 | def __init__(self, value): 80 | Exception.__init__(self) 81 | self.value = value 82 | def __str__(self): 83 | return repr(self.value) 84 | 85 | 86 | 87 | class Plane: 88 | "2D plane in 3D space. Defined using 4 paramaters." 89 | def __init__(self, ABCD=None): 90 | self.initialized = False 91 | if ABCD: 92 | self.set_ABCD(ABCD) 93 | 94 | def __str__(self): 95 | return self.describe_ABCD() + "\nor, in other words,\n" \ 96 | + self.describe_angles() 97 | 98 | 99 | def set_ABCD(self, ABCD): 100 | "set as (A,B,C,D) parameters - A*x+B*y+C*z+D=0" 101 | if len(ABCD) != 4: 102 | raise UnexpectedArgsError("4 (A B C D) arguments for plane " 103 | "required, got %s" % len(ABCD)) 104 | self.A, self.B, self.C, self.D = ABCD 105 | self.initialized = True 106 | self._compute_angles_from_ABCD() 107 | 108 | 109 | def set_angles(self, angles): 110 | """set as (alpha,beta,gamma,p) parameters of normal plane equation: 111 | x*cos(alpha)+y*cos(beta)+z*cos(gamma)-p=0""" 112 | if len(angles) != 4: 113 | raise UnexpectedArgsError("4 (alpha,beta,gamma,p) arguments" 114 | "for plane required, got %s" % len(angles)) 115 | self.alpha, self.beta, self.gamma, self.p = angles 116 | self.initialized = True 117 | self._compute_ABCD_from_angles() 118 | 119 | 120 | def _compute_ABCD_from_angles(self): 121 | "this method is always called when parameters are changed" 122 | self.A,self.B,self.C = cos(self.alpha), cos(self.beta), cos(self.gamma) 123 | self.D = -self.p 124 | #some precomputations -- for optimization 125 | self.cosines = array((self.A, self.B, self.C)) 126 | 127 | 128 | def _compute_angles_from_ABCD(self): 129 | A,B,C,D = self.A, self.B, self.C, self.D 130 | mi = 1./sqrt(A**2 + B**2 + C**2) 131 | if D>0: 132 | mi = -mi 133 | self.alpha = acos(A*mi) 134 | self.beta = acos(B*mi) 135 | self.gamma = acos(C*mi) 136 | self.p = -D*mi 137 | self._compute_ABCD_from_angles() # to ensure A,B,C,D is normalized 138 | 139 | 140 | def _set_distance_from_0(self, dist): 141 | "changes parameters of plane - the new plane is parallel to old" 142 | if not self.initialized: 143 | raise NotInitializedError(None) 144 | assert dist > 0 145 | self.p = dist 146 | self._compute_ABCD_from_angles() 147 | 148 | 149 | def get_distance_from_point(self, P): 150 | "computes distance of plane from point" 151 | return P * self.cosines - self.p 152 | 153 | 154 | def set_as_3points(self, p1, p2, p3): 155 | "define plane by giving three non-colinear points" 156 | if len(p1) != 3 or len(p2) != 3 or len(p3) != 3: 157 | raise UnexpectedArgsError("3 x 3 arguments expected for plane") 158 | par = lambda r, t: p1[r] * (p2[t] - p3[t]) + p2[r] * (p3[t] - p1[t]) \ 159 | + p3[r] * (p1[t] - p2[t]) 160 | self.A = par(1,2) 161 | self.B = par(2,0) 162 | self.C = par(0,1) 163 | if (self.A == self.B == self.C == 0): 164 | raise UnexpectedArgsError("3 colinear points - undefined plane") 165 | _D = p1[0] * (p2[1]*p3[2] - p3[1]*p2[2]) \ 166 | + p2[0] * (p3[1]*p1[2] - p1[1]*p3[2]) \ 167 | + p3[0] * (p1[1]*p2[2] - p2[1]*p1[2]) 168 | self.D = - _D 169 | self.initialized = True 170 | self._compute_angles_from_ABCD() 171 | 172 | 173 | def describe_ABCD(self): 174 | if not self.initialized: return "Plane not initialized" 175 | return "Plane: %s x + %s y + %s z + %s = 0. " % (self.A, self.B, 176 | self.C, self.D) 177 | 178 | def describe_angles(self): 179 | if not self.initialized: return "Plane not initialized" 180 | return "Plane: x cos(alpha) + y cos(beta) + z cos(gamma) - p = 0. \n" \ 181 | "alpha=%s beta=%s gamma=%s p=%s" % (self.alpha, self.beta, 182 | self.gamma, self.p) 183 | 184 | def get_normal_vector(self): 185 | assert self.initialized 186 | return array((self.A, self.B, self.C), float) 187 | 188 | def get_rotation_matrix_to(self, new_plane): 189 | n1 = self.get_normal_vector() 190 | n2 = new_plane.get_normal_vector() 191 | length_prod = sqrt(inner(n1,n1) * inner(n2,n2)) 192 | cos_angle = dot(n1, n2) / length_prod 193 | angle = acos(cos_angle) 194 | p = cross(n1, n2) / length_prod 195 | sin_angle = sqrt(inner(p,p)) 196 | assert abs(sin_angle - sin(angle)) < 1e-6 197 | return rodrigues(p / sin_angle, angle, verbose=True) 198 | 199 | 200 | 201 | class LatticePlane(Plane): 202 | "Plane that can be defined using Miller indices." 203 | def __init__(self, cell=None, hkl=None, r=None): 204 | Plane.__init__(self) 205 | self.set_hkld(hkl, r) 206 | self.set_cell(cell) 207 | 208 | def __str__(self): 209 | return self.describe_hkld() + "\n or \n" + Plane.__str__(self) 210 | 211 | def set_hkld(self, hkl, r): 212 | "define plane using hkl indices and distance from (0,0,0)" 213 | if hkl is None: 214 | self.hkl = None 215 | self.r = r 216 | return 217 | if len(hkl) != 3: 218 | raise UnexpectedArgsError("3 (h k l) arguments for plane" 219 | " required, got %s" % len(hkl)) 220 | self.hkl = array(hkl, float) 221 | self.r = r 222 | if not sometrue(self.hkl): #if all are zeros 223 | raise UnexpectedArgsError("(0 0 0) ???") 224 | self.compute_plane_parameters() 225 | 226 | def set_cell(self, cell): 227 | self.cell = cell 228 | self.compute_plane_parameters() 229 | 230 | def compute_plane_parameters(self): 231 | if not hasattr(self, "cell") or self.cell is None \ 232 | or not hasattr(self, "hkl") or self.hkl is None: 233 | return 234 | #FIXME is there a better way to compute geometrical coordinates 235 | # of plane? Now this method finds 3 points on plane, ... 236 | points = [] 237 | for i, index in enumerate(self.hkl): 238 | if index: 239 | points.append(self.cell.get_unit_shift(i) / index) 240 | for i, index in enumerate(self.hkl): 241 | if not index: 242 | points.append(points[0] + self.cell.get_unit_shift(i)) 243 | self.set_as_3points(*points) 244 | self._set_distance_from_0(self.r) 245 | 246 | 247 | def describe_hkld(self): 248 | if not self.initialized: return "Plane not initialized" 249 | return "Plane indices: (%s %s %s). Distance from origin: %s" % ( 250 | self.hkl[0], self.hkl[1],self.hkl[2], self.r) 251 | 252 | 253 | class SurfaceDeformation: 254 | "Stress-like deformation - perpendicular to surface" 255 | def __init__(self, depth, fun): 256 | # depth 0 doesn't prevent deformation, because some atoms can be 257 | # slightly outside of the surface (i.e. at negative depth) 258 | self.depth = depth 259 | self.fun = fun 260 | def __str__(self): 261 | return "D_shell=%s" % self.depth 262 | 263 | 264 | class LatticeSurface(LatticePlane): 265 | """Surface of CuttedGrain. Stores information about grain shell deformation. 266 | If hkl is None, it is treated as spherical surface. 267 | """ 268 | # default SurfaceDeformation 269 | default_sd = None 270 | 271 | def __init__(self, cell=None, hkl=None, r=None, sd=None): 272 | LatticePlane.__init__(self, cell, hkl, r) 273 | self.sd = sd or LatticeSurface.default_sd 274 | 275 | def __str__(self): 276 | if self.hkl is not None: 277 | return self.describe_hkld() + ". %s" % self.sd 278 | elif self.r: 279 | return "Sphere, r=%s. %s" % (self.r, self.sd) 280 | else: 281 | return "Not initialized LatticeSurface" 282 | 283 | def get_planes(self): 284 | """For sphere - returns bounding planes, eg. cube. 285 | Otherwise, returns itselfs 286 | """ 287 | if self.hkl is not None: 288 | return [self] 289 | else: #sphere 290 | r = self.r 291 | return [Plane((1,0,0,-r)), Plane((0,1,0,-r)), Plane((0,0,1,-r)), 292 | Plane((1,0,0,r)), Plane((0,1,0,r)), Plane((0,0,1,r))] 293 | 294 | 295 | 296 | class FreshModel(Model): 297 | "Base class for configuration generators. Initially Model without atoms." 298 | def __init__(self, lattice, pbc=None, title=""): 299 | Model.__init__(self, atoms=[], pbc=pbc, title=title) 300 | self.lattice = lattice 301 | self.unit_cell = lattice.unit_cell 302 | 303 | def get_vertices(self): 304 | assert None, "Virtual method, should be overwritten" 305 | 306 | def compute_scope(self): 307 | """ 308 | Get minimal and maximal coordinate in a,b,c cell parameters units. 309 | It cuts out a "rectangular parallelepiped"(?) that contains the grain. 310 | """ 311 | self.vertices = self.get_vertices() 312 | #vertices transformed to orthonormal system 313 | unit_vertices = [dot(i, self.unit_cell.M) for i in self.vertices] 314 | 315 | normalized = all(node.is_normalized() for node in self.lattice.nodes) 316 | margin = 0 if normalized else 1 317 | 318 | scopes = [] 319 | for i in zip(*unit_vertices): 320 | m = floor(round(min(i), 9)) - margin 321 | M = ceil(round(max(i), 9)) + margin 322 | scopes.append((int(m), int(M)+1)) 323 | self.a_scope, self.b_scope, self.c_scope = scopes 324 | 325 | 326 | def get_scope_info(self): 327 | "returns info about results of compute_scope() method" 328 | lar = self.a_scope[1] - self.a_scope[0] 329 | lbr = self.b_scope[1] - self.b_scope[0] 330 | lcr = self.c_scope[1] - self.c_scope[0] 331 | ncl = lar*lbr*lcr 332 | nnd = len(self.lattice.nodes) 333 | nat = sum([len(i.atoms_in_node) for i in self.lattice.nodes]) 334 | t = "Considering %ix%ix%i=%i cells, " % (lar, lbr, lcr, ncl) 335 | t += "%i nodes/cell, %i atoms/cell, " % (nnd, nat) 336 | t += "%i nodes, %i atoms." % (ncl*nnd, nat*ncl) 337 | return t 338 | 339 | 340 | def get_all_nodes(self): 341 | "generator of nodes in (and out of) grain" 342 | for i in range(self.a_scope[0], self.a_scope[1]): 343 | for j in range(self.b_scope[0], self.b_scope[1]): 344 | for k in range(self.c_scope[0], self.c_scope[1]): 345 | for node in self.lattice.nodes: 346 | yield node, array((i, j, k), float) + node.pos_in_cell 347 | 348 | 349 | def _do_export_atoms(self, f, format): 350 | if format == "powdercell": 351 | self.lattice.export_powdercell(f) 352 | else: 353 | Model._do_export_atoms(self, f, format) 354 | 355 | 356 | 357 | class CuttedGrain(FreshModel): 358 | """Finite grain made from unfinite crystal lattice (CrystalLattice) 359 | by cleaving using added surfaces (sequence of Plane)""" 360 | 361 | def __init__(self, lattice, surfaces=None, title="generated by gosam"): 362 | FreshModel.__init__(self, lattice, title=title) 363 | if isinstance(surfaces, LatticeSurface): 364 | surfaces = [surfaces] 365 | for i in surfaces: 366 | i.set_cell(self.unit_cell) 367 | self.surfaces = surfaces 368 | self.operations = [] 369 | 370 | 371 | def __str__(self): 372 | return "cutted grain description:\n\n" + self.lattice.__str__() \ 373 | + "\n\n%i surfaces:\n " % len(self.surfaces) \ 374 | + "\n ".join([i.__str__() for i in self.surfaces]) \ 375 | + "\n\nOperations:\n " + "\n ".join(self.operations) 376 | 377 | 378 | def export_for_qhull(self): 379 | planes = [] 380 | for i in self.surfaces: 381 | planes += i.get_planes() 382 | s = """\ 383 | 3 1 384 | 0 0 0 385 | 4 386 | %s\n""" % len(planes) 387 | for i in planes: 388 | s += "%s %s %s %s\n" % (i.A, i.B, i.C, i.D) 389 | return s 390 | 391 | 392 | def show_in_geomview(self): 393 | # it shows cubes instead of spheres 394 | t = self.export_for_qhull() 395 | p = Popen("qhull H Fp | qhull G", shell=True, 396 | stdin=PIPE, stdout=PIPE, close_fds=True) 397 | # H - qhalf 398 | # Fp - print points at halfspace intersections 399 | #...and compute the convex hull of the intersection points for Geomview 400 | # G - display Geomview output 401 | p.stdin.write(t) 402 | p.stdin.close() 403 | off_content = p.stdout.read() 404 | f = NamedTemporaryFile() 405 | #print "*" * 70 406 | #print off_content 407 | f.write(off_content) 408 | f.flush() 409 | f.seek(0) 410 | try: 411 | commands.getoutput("geomview -nopanels %s" % f.name) 412 | except KeyboardInterrupt: 413 | pass 414 | 415 | 416 | def get_vertices(self): 417 | "get vertices of convex hull that contains the grain" 418 | t = self.export_for_qhull() 419 | p = Popen("qhull H Fp", 420 | shell=True, stdin=PIPE, stdout=PIPE) 421 | p.stdin.write(t) 422 | p.stdin.close() 423 | return [[float(j) for j in i.split()] 424 | for i in p.stdout.readlines() if len(i.split()) == 3] 425 | 426 | 427 | def generate_atoms(self): 428 | print "generating atoms (atom positions) ..." 429 | self.atoms = [] 430 | not_included_counter = 0 431 | shifted_counter = 0 432 | self.compute_scope() 433 | print self.get_scope_info() 434 | 435 | #almost inner loop - over all nodes 436 | for node, abs_pos in self.get_all_nodes(): 437 | 438 | node_pos = dot(abs_pos, self.unit_cell.M_1) 439 | 440 | # checking if atoms are inside grain 441 | outside = False 442 | for srf in self.surfaces: 443 | if srf.hkl is not None: 444 | t = -(inner(node_pos, srf.cosines) - srf.p) 445 | else: 446 | t = srf.r - sqrt(inner(node_pos, node_pos)) 447 | if t < -1e-12: 448 | outside = True 449 | not_included_counter += 1 450 | break 451 | srf.tmp_t = t #optimalization ? 452 | if outside: 453 | continue 454 | 455 | # inner loop - over (usually one or a few) atoms in node 456 | for atom in node.atoms_in_node: 457 | shifts = 0 458 | min_dist = None 459 | if atom.non_zero: 460 | xyz = dot(abs_pos+atom.pos, self.unit_cell.M_1) 461 | else: 462 | xyz = node_pos 463 | dxyz = array((0., 0., 0.)) 464 | for srf in self.surfaces: 465 | if atom.non_zero: 466 | if srf.hkl is not None: 467 | t = -(inner(xyz, srf.cosines) - srf.p) 468 | else: 469 | t = srf.r - sqrt(inner(xyz, xyz)) 470 | else: 471 | t = srf.tmp_t 472 | if srf.sd is not None and t < srf.sd.depth: 473 | if type(srf.sd.fun) is dict: 474 | fun = srf.sd.fun.get(atom.name) 475 | else: 476 | fun = srf.sd.fun 477 | shift = fun(t) 478 | if srf.hkl is not None: # plane 479 | dxyz -= shift * srf.cosines 480 | else: # sphere 481 | r0 = srf.r - t 482 | dxyz -= shift / r0 * xyz 483 | shifts += 1 484 | if min_dist is None or t < min_dist: 485 | min_dist = t 486 | self.atoms.append(mdprim.AtomG(atom.name, xyz+dxyz, min_dist)) 487 | if shifts > 0: 488 | shifted_counter += 1 489 | 490 | print "%i nodes outside of grain." % not_included_counter 491 | print "Number of atoms in grain: %i (shifted: %i)" % (len(self.atoms), 492 | shifted_counter) 493 | self.log("atom positions generated") 494 | 495 | 496 | 497 | 498 | def generate_grain(d): 499 | """ 500 | Takes dictionary with names from "configuration file", i.e. globals() 501 | """ 502 | group_nodes = "do_not_group_nodes" not in d or not d["do_not_group_nodes"] 503 | if "nodes" in d and "node_atoms" in d: 504 | if group_nodes: 505 | nodes = [Node(i, d["node_atoms"]) for i in d["nodes"]] 506 | else: 507 | nodes = [Node([atom[1]+node[0], atom[2]+node[1], atom[3]+node[2]], 508 | [AtomInNode(atom[0])]) 509 | for atom in d["node_atoms"] for node in d["nodes"]] 510 | elif "atoms" in d: 511 | nodes = [Node(i[1:], [AtomInNode(i[0])]) for i in d["atoms"]] 512 | else: 513 | print 'Either "nodes" and "node_atoms" or "atoms" should be defined.' 514 | return 515 | lattice = CrystalLattice(d["cell"], nodes) 516 | g = CuttedGrain(lattice, surfaces=d["surfaces"]) 517 | g.generate_atoms() 518 | g.print_stochiometry() 519 | if "vacancy_probability" in d: 520 | g.make_vacancies(d["vacancy_probability"]) 521 | g.print_stochiometry() 522 | if "modifier" in d: 523 | g.modify_atoms(d["modifier"]) 524 | g.round_atom_coordinates() 525 | if "remove_undercoordinated_atoms" in d: 526 | bondlength = d["remove_undercoordinated_atoms"] 527 | g.print_coordination_statistics(bondlength) 528 | g.remove_undercoordinated_atoms(bondlength) 529 | extensions = { "pielaszek": "at", 530 | "xmol": "xyz", 531 | "powdercell": "cel", 532 | "dlpoly": "dlpoly", 533 | "atomeye": "cfg" 534 | } 535 | def format_name(name): 536 | if name in extensions: 537 | return name 538 | elif name in extensions.values(): 539 | for key, value in extensions.items(): 540 | if value == name: 541 | return key 542 | else: 543 | msg = "WARNING: unknown file format in 'output_formats': " + name 544 | logfile.write(msg + "\n") 545 | print msg 546 | 547 | if "output_formats" in d: 548 | formats = [format_name(i) for i in d["output_formats"]] 549 | else: 550 | formats = ["xmol"] 551 | 552 | # this format requires PBC. We put vacuum (>=1nm) between images 553 | if "atomeye" in formats: 554 | g.set_pbc_with_vacuum(width=10) 555 | 556 | if "output_file" in d: 557 | basename = d["output_file"] 558 | else: 559 | filename = os.path.basename(sys.argv[0]) 560 | basename = os.path.splitext(filename)[0] 561 | 562 | for i in formats: 563 | g.export_atoms(file(basename+"."+extensions[i], 'w'), format=i) 564 | 565 | logfile = file(basename+".log", 'w') 566 | logfile.write(str(g) + "\n\n" + "-"*60 + "\n" + file(sys.argv[0]).read()) 567 | return g 568 | 569 | 570 | 571 | if __name__ == '__main__': 572 | print "Use it as module" 573 | 574 | -------------------------------------------------------------------------------- /latt.py: -------------------------------------------------------------------------------- 1 | # this file is part of gosam (generator of simple atomistic models) 2 | # Licence: GNU General Public License version 2 3 | """\ 4 | CrystalLattice, UnitCell, etc. 5 | """ 6 | 7 | from math import cos, sin, acos, sqrt, radians, degrees 8 | from numpy import linalg 9 | from numpy import array, dot, transpose 10 | 11 | 12 | class UnitCell: 13 | """basic unit cell - triclinic 14 | """ 15 | def __init__(self, a, b, c, alpha, beta, gamma, 16 | system="triclinic", rad=False, reciprocal=None): 17 | self.abc = self.a, self.b, self.c = a, b, c 18 | if not rad: #degrees 19 | self.alpha, self.beta, self.gamma = alpha, beta, gamma 20 | self.alpha_rad, self.beta_rad, self.gamma_rad = radians(alpha), \ 21 | radians(beta), radians(gamma) 22 | else: #radians 23 | self.alpha, self.beta, self.gamma = degrees(alpha), \ 24 | degrees(beta), degrees(gamma) 25 | self.alpha_rad, self.beta_rad, self.gamma_rad = alpha, beta, gamma 26 | self._compute_sin_cos_V() 27 | if not reciprocal: #lattice in direct space 28 | self.is_reciprocal = False 29 | self.reciprocal = self.get_reciprocal_unit_cell() 30 | else: #lattice in reciprocal space 31 | self.is_reciprocal = not reciprocal.is_reciprocal 32 | self.reciprocal = reciprocal 33 | self.system = system 34 | self.system_name = self.system.title() + " system " \ 35 | + (self.is_reciprocal and "(reciprocal)" or "(direct)") 36 | self.compute_transformation_matrix() 37 | 38 | 39 | def __str__(self): 40 | return self.system_name \ 41 | + " a=%s, b=%s, c=%s, alpha=%s, beta=%s, gamma=%s" % (self.a, 42 | self.b, self.c, self.alpha, self.beta, self.gamma) 43 | 44 | # overloaded for hexagonal lattice 45 | def get_orthorhombic_supercell(self): 46 | if self.alpha == 90 and self.beta == 90 and self.gamma == 90: 47 | return self.a, self.b, self.c 48 | else: 49 | assert "Not implemented." 50 | 51 | def _compute_sin_cos_V(self): 52 | "precomputations of some values (eg. sin(alpha)) -- for optimization" 53 | a, b, c = self.a, self.b, self.c 54 | alpha, beta, gamma = self.alpha_rad, self.beta_rad, self.gamma_rad 55 | self.sines = array([sin(alpha), sin(beta), sin(gamma)]) 56 | self.cosines = array([cos(alpha), cos(beta), cos(gamma)]) 57 | #Giacovazzo p.62 58 | G = (a*b*c)**2 * (1 - cos(alpha)**2 - cos(beta)**2 - cos(gamma)**2 59 | + 2*cos(alpha)*cos(beta)*cos(gamma)) 60 | self.V = sqrt(G) 61 | 62 | 63 | def get_reciprocal_unit_cell(self): 64 | """ returns instance of UnitCell, that is reciprocal to self 65 | self.reciprocal.(a|b|c|alpha|...) 66 | a* = self.reciprocal.a, etc. 67 | (Giacovazzo, p. 64) 68 | """ 69 | a, b, c, V = self.a, self.b, self.c, self.V 70 | alpha, beta, gamma = self.alpha_rad, self.beta_rad, self.gamma_rad 71 | ar = b*c*sin(alpha)/V 72 | br = a*c*sin(beta)/V 73 | cr = a*b*sin(gamma)/V 74 | cos_alphar = (cos(beta)*cos(gamma)-cos(alpha)) / (sin(beta)*sin(gamma)) 75 | cos_betar = (cos(alpha)*cos(gamma)-cos(beta)) / (sin(alpha)*sin(gamma)) 76 | cos_gammar = (cos(alpha)*cos(beta)-cos(gamma)) / (sin(alpha)*sin(beta)) 77 | return UnitCell(ar, br, cr, acos(cos_alphar), acos(cos_betar), 78 | acos(cos_gammar), rad=True, reciprocal=self) 79 | 80 | 81 | def compute_transformation_matrix(self): 82 | """ sets self.M and self.M_1 (M^-1) 83 | [ 1/a 0 0 ] 84 | M = [ -cosG/(a sinG) 1/(b sinG) 0 ] 85 | [ a*cosB* b*cosA* c* ] 86 | 87 | where A is alpha, B is beta, G is gamma, * means reciprocal space. 88 | E=MA 89 | (Giacovazzo, p.68) 90 | """ 91 | a, b, c = self.a, self.b, self.c 92 | alpha, beta, gamma = self.alpha_rad, self.beta_rad, self.gamma_rad 93 | r = self.reciprocal 94 | self.M = array([ 95 | (1/a, 0, 0), 96 | (-cos(gamma)/(a * sin(gamma)), 1/(b*sin(gamma)), 0), 97 | (r.a * cos(r.beta_rad), r.b * cos(r.alpha_rad), r.c) 98 | ]) 99 | 100 | self.M_1 = array([ 101 | (a, 0, 0), 102 | (b*cos(gamma), b*sin(gamma), 0), 103 | (c*cos(beta), -c*sin(beta)*cos(r.alpha_rad), 1/r.c) 104 | ]) 105 | 106 | def rotate(self, rot_mat): 107 | assert (abs(transpose(rot_mat) - linalg.inv(rot_mat)) < 1e-6).all(), \ 108 | "not orthogonal" 109 | assert abs(linalg.det(rot_mat) - 1) < 1e-6, "not a pure rotation matrix" 110 | self.M_1 = dot(self.M_1, rot_mat) 111 | self.M = dot(transpose(rot_mat), self.M) 112 | #print "1=", dot(self.M_1, self.M) 113 | 114 | def get_unit_shift(self, i): 115 | return self.M_1[i] 116 | 117 | 118 | 119 | class CubicUnitCell(UnitCell): 120 | def __init__(self, a): 121 | UnitCell.__init__(self, a, a, a, 90, 90, 90, system="cubic") 122 | 123 | def __str__(self): 124 | return self.system_name + " a=%s" % self.a 125 | 126 | 127 | class TetragonalUnitCell(UnitCell): 128 | def __init__(self, a, c): 129 | UnitCell.__init__(self, a, a, c, 90, 90, 90, system="tetragonal") 130 | 131 | def __str__(self): 132 | return self.system_name + " a=%s, c=%s" % (self.a, self.c) 133 | 134 | 135 | class OrthorhombicUnitCell(UnitCell): 136 | def __init__(self, a, b, c): 137 | UnitCell.__init__(self, a, b, c, 90, 90, 90, system="orthorhombic") 138 | 139 | def __str__(self): 140 | return self.system_name + " a=%s, b=%s, c=%s"%(self.a, self.b, self.c) 141 | 142 | 143 | class HexagonalUnitCell(UnitCell): 144 | def __init__(self, a, c): 145 | UnitCell.__init__(self, a, a, c, 90, 90, 120, system="hexagonal") 146 | 147 | def __str__(self): 148 | return self.system_name + " a=%s, c=%s" % (self.a, self.c) 149 | 150 | def get_orthorhombic_supercell(self): 151 | return self.a, self.a * 3**0.5, self.c 152 | 153 | 154 | 155 | class AtomInNode: 156 | """atom with its coordinates (as fraction of unit cell parameters) in node 157 | (atoms in one node can't be separated; node is in unit cell) """ 158 | def __init__(self, name, xa=0, yb=0, zc=0): 159 | self.name = name 160 | self.pos = array((xa, yb, zc), float) 161 | self.non_zero = (xa != 0 or yb != 0 or zc != 0) 162 | 163 | def __str__(self): 164 | return "%s at %s in node"% (self.name, tuple(self.pos)) 165 | 166 | 167 | class Node: 168 | """ Node in unit cell consists of a few (eg. 1 or 2) atoms, which should 169 | be kept together when cutting grains 170 | """ 171 | def __init__(self, pos_in_cell, atoms_in_node): 172 | self.pos_in_cell = array(pos_in_cell, float) 173 | self.atoms_in_node = [isinstance(i, AtomInNode) and i or AtomInNode(*i) 174 | for i in atoms_in_node] 175 | 176 | def __str__(self): 177 | return "Node at %s in cell with: %s" % (tuple(self.pos_in_cell), 178 | ", ".join([str(i) for i in self.atoms_in_node])) 179 | 180 | def shift(self, v): 181 | self.pos_in_cell = (self.pos_in_cell + array(v)) % 1.0 182 | 183 | def is_normalized(self): 184 | """Are positions of all the atoms in this node in the <0,1) range. 185 | (i.e. checking self.pos_in_cell+atom.pos) 186 | """ 187 | for atom in self.atoms_in_node: 188 | p = self.pos_in_cell + atom.pos 189 | if (p >= 1).any() or (p < 0).any(): 190 | return False 191 | return True 192 | 193 | 194 | class CrystalLattice: 195 | "Unfinite crystal lattice = unit cell + nodes in cell" 196 | def __init__(self, unit_cell, nodes, name="Mol"): 197 | self.unit_cell = unit_cell 198 | self.nodes = nodes 199 | assert isinstance(name, basestring) 200 | self.name = name 201 | 202 | def __str__(self): 203 | return str(self.unit_cell) + " Nodes:\n" \ 204 | + "\n".join([str(i) for i in self.nodes]) 205 | 206 | def count_species(self): 207 | names = set() 208 | for i in self.nodes: 209 | for j in i.atoms_in_node: 210 | names.add(j.name) 211 | return len(names) 212 | 213 | def swap_node_atoms_names(self): 214 | "works only if there are two atoms in every node" 215 | for i in self.nodes: 216 | ain = i.atoms_in_node 217 | assert len(ain) == 2 218 | ain[0].name, ain[1].name = ain[1].name, ain[0].name 219 | 220 | def shift_nodes(self, v): 221 | for i in self.nodes: 222 | i.shift(v) 223 | 224 | def export_powdercell(self, f): 225 | cell = self.unit_cell 226 | print >>f, "CELL %f %f %f %f %f %f" % (cell.a, cell.b, cell.c, 227 | cell.alpha, cell.beta, cell.gamma) 228 | names = [] 229 | for node in self.nodes: 230 | for atom in node.atoms_in_node: 231 | pos = node.pos_in_cell + atom.pos 232 | names.append(atom.name) 233 | xname = "%s%i" % (atom.name, names.count(atom.name)) 234 | print >>f, "%-4s %-4s %-8s %-8s %-8s" % (atom.name, atom.name, 235 | pos[0], pos[1], pos[2]) 236 | 237 | 238 | def generate_polytype(a, h, polytype): 239 | """utility for dealing with various polytypes. a is a from hexagonal 240 | structure, h is a distance between layers. 241 | Usage: 242 | cell, nodes = generate_polytype(a=3.073, h=2.51, polytype="ABABABC") 243 | """ 244 | pos = { "A" : (0.0, 0.0), 245 | "B" : (1/3., 2/3.), 246 | "C" : (2/3., 1/3.) 247 | } 248 | polytype = polytype.upper() 249 | N = len(polytype) 250 | cell = HexagonalUnitCell(a=a, c=h*N) 251 | nodes = [] 252 | for n, i in enumerate(polytype): 253 | t = pos[i] 254 | nodes.append((t[0], t[1], float(n)/N)) 255 | return cell, nodes 256 | 257 | 258 | 259 | -------------------------------------------------------------------------------- /ldump2cfg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # this file is part of gosam (generator of simple atomistic models) 3 | # Licence: GNU General Public License version 2 4 | """\ 5 | Converts LAMMPS dump files to AtomEye cfg format, 6 | calculates GB energy in bicrystal geometry. 7 | 8 | The `ITEM: ATOMS` line in input file should start with: 9 | ITEM: ATOMS id type x y z 10 | and can be followed by other properties. These extra columns 11 | are saved in AtomEye file as auxiliary properties. 12 | 13 | Note: Old versions of LAMMPS don't specify columns after `ITEM: ATOMS`. 14 | If such a case, either upgrade LAMMPS or add column names manually. 15 | 16 | Input/output files can be gzipped or bzip2'ed. 17 | """ 18 | 19 | usage_string = """\ 20 | Usage: 21 | ldump2cfg.py lammps_dump output.cfg 22 | convert LAMMPS dump file to cfg file 23 | 24 | ldump2cfg.py hist lammps_dump histogram.xy 25 | this may not work now 26 | 27 | ldump2cfg.py ey energy_vs_y.histogram 28 | write GB energy vs y to gbe_vs_y.hist 29 | 30 | ldump2cfg.py lammps_dump1 [lammps_dump2 ...] 31 | calculate GB energies 32 | """ 33 | 34 | import sys 35 | import os.path 36 | import bz2 37 | import gzip 38 | 39 | from mdprim import AtomVF 40 | import model 41 | 42 | #e0 = -6.1637 43 | e0 = -6.1646668 #SiC285.tersoff 44 | id_pos = 0 45 | val_pos = -1 46 | y_pos = 3 47 | z_pos = 4 48 | nbins = 128 49 | #gb_relative_width = 0.7 50 | gb_relative_width = None 51 | 52 | atomeye_species = { 1: "12.01\nC", 53 | 2: "28.09\nSi", 54 | 3: "20.0\nB", 55 | 4: "20.0\nCl" 56 | } 57 | 58 | conversion_eV_A2_to_J_m2 = 16.021765 59 | 60 | 61 | def open_any(name, mode='r'): 62 | if name.endswith(".bz2"): 63 | return bz2.BZ2File(name, mode) 64 | elif name.endswith(".gz"): 65 | return gzip.GzipFile(name, mode) 66 | else: 67 | return file(name, mode) 68 | 69 | 70 | class DumpReader: 71 | def __init__(self, dump_filename): 72 | self.dump = open_any(dump_filename) 73 | self.filename = dump_filename 74 | self._read_header() 75 | 76 | def _read_header(self): 77 | assert self.dump.readline().strip() == "ITEM: TIMESTEP" 78 | self.dump.readline() # skip timestep 79 | assert self.dump.readline().strip() == "ITEM: NUMBER OF ATOMS" 80 | self.natoms = int(self.dump.readline()) 81 | assert self.dump.readline().strip() == "ITEM: BOX BOUNDS" 82 | self.pbc = [] 83 | self.pbc_lo = [] 84 | for i in range(3): 85 | pmin_, pmax_ = self.dump.readline().split() 86 | pmin, pmax = float(pmin_), float(pmax_) 87 | self.pbc.append(pmax - pmin) 88 | self.pbc_lo.append(pmin) 89 | atoms_line = self.dump.readline() 90 | assert atoms_line.startswith("ITEM: ATOMS id type x y z") 91 | self.extra_data = atoms_line.split()[7:] 92 | 93 | def read_atom_line(self): 94 | return self.dump.readline() 95 | 96 | def get_configuration(self): 97 | atoms = [None for i in range(self.natoms)] 98 | for i in range(self.natoms): 99 | id_, type, x_, y_, z_ = self.read_atom_line().split()[:5] 100 | pos = (float(x_) - self.pbc_lo[0], 101 | float(y_) - self.pbc_lo[1], 102 | float(z_) - self.pbc_lo[2]) 103 | n = int(id_) 104 | name = atomeye_species[int(type)].split()[-1] 105 | atoms[n-1] = AtomVF(name, n, pos, vel=None, force=None) 106 | pbc = [[self.pbc[0],0,0], [0,self.pbc[1],0], [0,0,self.pbc[2]]] 107 | title = "from LAMMPS dump :" + self.filename 108 | return model.Model(atoms, pbc=pbc, title=title) 109 | 110 | 111 | def convert(dump_filename, cfg_filename): 112 | "converts LAMMPS dump_filename to AtomEye extended CFG file" 113 | # read header 114 | dr = DumpReader(dump_filename) 115 | #natoms, pbc, pbc_lo = read_dump_header(dump) 116 | # write header 117 | cfg = open_any(cfg_filename, "w") 118 | cfg.write("Number of particles = %d\n" % dr.natoms); 119 | cfg.write("# converted by gosam.ldump2cfg from %s\n" % dump_filename); 120 | cfg.write("# full original path: %s\n" % os.path.abspath(dump_filename)); 121 | cfg.write("A = 1.0 Angstrom (basic length-scale)\n") 122 | for i in range(3): 123 | for j in range(3): 124 | cfg.write("H0(%i,%i) = %f A\n" % (i+1, j+1, 125 | (dr.pbc[i] if i == j else 0.))) 126 | cfg.write(".NO_VELOCITY.\n") 127 | cfg.write("entry_count = %d\n" % (4+len(dr.extra_data))) 128 | for n, name in enumerate(dr.extra_data): 129 | cfg.write("auxiliary[%d] = %s\n" % (n, name)) 130 | cfg.write("auxiliary[%d] = xcolor [0-1]\n" % len(dr.extra_data)) 131 | # read atoms 132 | alist = [None for i in range(dr.natoms)] 133 | for i in range(dr.natoms): 134 | id_, type, x_, y_, z_, ex = dr.read_atom_line().split(None, 5) 135 | x = (float(x_) - dr.pbc_lo[0]) / dr.pbc[0] 136 | y = (float(y_) - dr.pbc_lo[1]) / dr.pbc[1] 137 | z = (float(z_) - dr.pbc_lo[2]) / dr.pbc[2] 138 | alist[int(id_)-1] = (type, x, y, z, ex.strip()) 139 | # write atoms 140 | prev = None 141 | pos0 = _find_pos0(alist) 142 | for (type, x, y, z, ex) in alist: 143 | if type != prev: 144 | cfg.write("%s\n" % atomeye_species[int(type)]) 145 | prev = type 146 | xcol = (2 * (x - pos0)) % 1. 147 | cfg.write("%.7f %.7f %.7f %s %.3f\n" % (x, y, z, ex, xcol)) 148 | 149 | # pos0 is used for xcolor, i.e. for coloring based on the x coordinate 150 | # selection of pos0 matters when we compare different frames 151 | # If there is a rigid surface in the system, we use it as a reference for 152 | # other coordinates. 153 | def _find_pos0(alist): 154 | selected = [a for a in alist if a[0] == "3"] 155 | if selected: 156 | elem = min(selected, key = lambda x: x[3]) 157 | return elem[1] 158 | else: # no rigid surface 159 | return sum(a[1] for a in alist) / len(alist) 160 | 161 | def _print_gb_energy(energies, dr, verbose=False): 162 | count = len(energies) 163 | energy = sum(energies) 164 | excess = energy - count * e0 165 | if verbose: 166 | print "PBC: %g x %g x %g" % tuple(dr.pbc) 167 | print "total energy of %d atoms: %g eV (%g/at.), excess: %g" % ( 168 | count, energy, energy/count, excess) 169 | area = dr.pbc[0] * dr.pbc[1] 170 | gb_energy = excess / area * conversion_eV_A2_to_J_m2 171 | if verbose: 172 | print "GB energy:", gb_energy 173 | print "n*Edisl:", excess * conversion_eV_A2_to_J_m2 * 1e-10 / dr.pbc[0] 174 | return gb_energy 175 | 176 | def calculate_dislocation_energy(dump_filename, y0, z0, r): 177 | dr = DumpReader(dump_filename) 178 | energies = [] 179 | for i in range(dr.natoms): 180 | s = dr.read_atom_line().split() 181 | dy = abs(float(s[3]) - y0) 182 | if dy > dr.pbc[1] / 2: 183 | dy = dr.pbc[1] - dy 184 | dz = abs(float(s[4]) - z0) 185 | if dz > dr.pbc[2] / 2: 186 | dz = dr.pbc[2] - dz 187 | if dy*dy + dz*dz < r*r: 188 | energies.append(float(s[val_pos])) 189 | count = len(energies) 190 | energy = sum(energies) 191 | excess = energy - count * e0 192 | disl_length = dr.pbc[0] 193 | eV_A_to_J_m = conversion_eV_A2_to_J_m2 * 1e-10 194 | e_disl = excess / disl_length * eV_A_to_J_m 195 | print "%d atoms in cyllinder (y-%g)^2 + (z-%g)^2 < %g^2" % (count,y0,z0,r) 196 | print "Edisl [J/m]: %g" % (e_disl) 197 | return e_disl 198 | 199 | def calculate_gbe_of_types12(dump_filename): 200 | # read and parse first snapshot 201 | dr = DumpReader(dump_filename) 202 | 203 | energies = [] 204 | for i in range(dr.natoms): 205 | tokens = dr.read_atom_line().split() 206 | if tokens[1] in ("1", "2"): 207 | val = float(tokens[val_pos]) 208 | energies.append(val) 209 | 210 | return _print_gb_energy(energies, dr) 211 | 212 | 213 | def calculate_total_energy(dump_filename): 214 | dr = DumpReader(dump_filename) 215 | 216 | energies = [] 217 | for i in dr.dump: 218 | s = i.split() 219 | y = float(s[3]) / dr.pbc[1] % 1. 220 | z = float(s[4]) / dr.pbc[2] % 1. 221 | #if not 0.25 <= y < 0.75: 222 | #if not 0.30 <= z < 0.70: 223 | if 1: 224 | energies.append(float(s[val_pos])) 225 | #energies = [float(i.split()[val_pos]) for i in dr.dump] 226 | 227 | _print_gb_energy(energies, dr, verbose=True) 228 | 229 | 230 | def calculate_gb_energy(dump_filename, hist_filename=None): 231 | if gb_relative_width is None: 232 | return calculate_gbe_of_types12(dump_filename) 233 | ########## that's the end #################### 234 | 235 | # read and parse first snapshot 236 | dr = DumpReader(dump_filename) 237 | area = dr.pbc[0] * dr.pbc[1] 238 | 239 | values = [None for i in range(dr.natoms)] 240 | hist = [[] for i in range(nbins)] 241 | 242 | for i in range(dr.natoms): 243 | tokens = dr.read_atom_line().split() 244 | id = int(tokens[id_pos]) 245 | val = float(tokens[val_pos]) 246 | values[id-1] = val 247 | 248 | z = float(tokens[z_pos]) % dr.pbc[2] 249 | bin = int(nbins * z / dr.pbc[2]) 250 | assert bin < nbins 251 | hist[bin].append(values[id-1]) 252 | 253 | #print "total energy:", sum(values) 254 | 255 | hist_y = [] 256 | for n, vv in enumerate(hist): 257 | if vv: 258 | z = (n + 0.5) / nbins * dr.pbc[2] 259 | s = sum(vv) 260 | count = len(vv) 261 | delta = s - e0 * count 262 | gb_energy = delta / area * conversion_eV_A2_to_J_m2 263 | hist_y.append((z, gb_energy, s / count)) 264 | 265 | # the GB is assumed to be at z=0 266 | qb = int(gb_relative_width * len(hist_y) / 2) 267 | gbe = sum(i[1] for i in hist_y[-qb:] + hist_y[:qb]) 268 | 269 | #if aux_filename: 270 | # aux = open_any(aux_filename, "w") 271 | # for i in values: 272 | # print >>aux, i 273 | 274 | if hist_filename: 275 | hist_file = open_any(hist_filename, "w") 276 | for (z, gb_energy, avg) in hist_y: 277 | print >>hist_file, z, avg, gb_energy 278 | 279 | return gbe 280 | 281 | 282 | def calc_gbe_vs_y(dump_filename): 283 | "writes GB energy vs y histogram to file gbe_vs_y.hist" 284 | # read and parse first snapshot 285 | nbins = 50 286 | dr = DumpReader(dump_filename) 287 | width = 10.0 288 | Y = dr.pbc[1] 289 | area = dr.pbc[0] * dr.pbc[1] 290 | 291 | hist = [0.0 for i in range(nbins)] 292 | for i in range(dr.natoms): 293 | tokens = dr.read_atom_line().split() 294 | z = float(tokens[z_pos]) 295 | if abs(z) > width and abs(z) < dr.pbc[2] - width: 296 | continue 297 | y = float(tokens[y_pos]) % Y 298 | bin = int(nbins * y / Y) 299 | assert bin < nbins 300 | delta = float(tokens[val_pos]) - e0 301 | hist[bin] += delta 302 | print "GBE:", sum(hist) / area * conversion_eV_A2_to_J_m2 303 | 304 | hist_file = open_any("gbe_vs_y.hist", "w") 305 | for n, d in enumerate(hist): 306 | print >>hist_file, (n+0.5) / nbins, d / area * conversion_eV_A2_to_J_m2 307 | 308 | 309 | if __name__ == "__main__": 310 | assert len(sys.argv) >1, usage_string 311 | if len(sys.argv) == 3 and sys.argv[1] == "ey": 312 | calc_gbe_vs_y(sys.argv[2]) 313 | if len(sys.argv) >= 3 and sys.argv[1] == "total": 314 | for f in sys.argv[2:]: 315 | if len(sys.argv) > 3: 316 | print f 317 | calculate_total_energy(f) 318 | elif len(sys.argv) == 4 and sys.argv[1] == "hist": 319 | gbe = calculate_gb_energy(sys.argv[2], sys.argv[3]) 320 | print "GB energy: ", round(gbe, 4) 321 | elif len(sys.argv) == 3 and ".cfg" in sys.argv[2]: 322 | convert(sys.argv[1], sys.argv[2]) 323 | elif len(sys.argv) == 6 and sys.argv[1] == "disl": 324 | calculate_dislocation_energy(dump_filename=sys.argv[2], 325 | y0=float(sys.argv[3]), 326 | z0=float(sys.argv[4]), 327 | r=float(sys.argv[5])) 328 | else: 329 | if gb_relative_width: 330 | print "GB energy [J/m2]" 331 | print "GB width assumed as %g%% of slab" % (gb_relative_width * 100) 332 | for i in sys.argv[1:]: 333 | gbe = calculate_gb_energy(i) 334 | name = (i[:-8] if i.endswith(".dump.gz") else i) 335 | print "%s\t%f" % (name, gbe) 336 | 337 | 338 | -------------------------------------------------------------------------------- /mdfile.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # this file is part of gosam (generator of simple atomistic models) 3 | # Licence: GNU General Public License version 2 4 | """ 5 | File import/export. 6 | Supported file types (some of them are supported partially): 7 | XMOL XYZ File Format (search WWW for details) 8 | DL_POLY (see http://www.cse.clrc.ac.uk/msi/software/DL_POLY/) 9 | AtomEye (search WWW for details) 10 | LAMMPS (http://lammps.sandia.gov) 11 | Pielaszek (simple file format used sometimes here, in Unipress) 12 | POSCAR - VASP input file with atom positions - POSCAR (direct format) 13 | GULP - a part of GULP input file with cell vectors and atom coordinates 14 | """ 15 | 16 | import sys 17 | import operator 18 | from optparse import OptionParser 19 | import bz2 20 | import gzip 21 | import numpy 22 | import random 23 | from numpy import linalg 24 | 25 | from mdprim import AtomVF 26 | import model 27 | import pse 28 | from utils import get_command_line 29 | from rotmat import is_diagonal, StdDev 30 | 31 | 32 | def get_orthorhombic_pbc(full_pbc): 33 | """\ 34 | the input is matrix 3x3, the output is diagonal. 35 | Non-diagonal elements must be zero. 36 | """ 37 | if full_pbc is None or len(full_pbc) == 0: 38 | raise ValueError("no PBC") 39 | assert is_diagonal(numpy.array(full_pbc)) 40 | return numpy.diagonal(full_pbc) 41 | 42 | 43 | def export_for_dlpoly(atoms, f, title, sort=True): 44 | levcfg = 0 #only coordinates in file 45 | #imcon = 0 #no periodic boundaries 46 | imcon = 1 #cubic periodic boundaries 47 | a = 200. 48 | with_shells = False 49 | print >>f, title 50 | print >>f, "%10i%10i" % (levcfg, imcon) 51 | print >>f, "%20f%20f%20f" % (a, 0, 0) 52 | print >>f, "%20f%20f%20f" % (0, a, 0) 53 | print >>f, "%20f%20f%20f" % (0, 0, a) 54 | ordered = atoms[:] 55 | if sort: 56 | # atoms are sorted in alphabetical order 57 | ordered.sort(key=operator.attrgetter("name")) 58 | 59 | # output atoms to file 60 | counter = 0 61 | for atm in ordered: 62 | counter += 1 63 | print >>f, "%-8s%20i" % (atm.name, counter) 64 | print >>f, "%20f%20f%20f" % (atm.pos[0], atm.pos[1], atm.pos[2]) 65 | if with_shells: #shell - hack 66 | counter += 1 67 | print >>f, "%-8s%20i" % (atm.name + "-shl", counter) 68 | print >>f, "%20f%20f%20f" % (atm.pos[0], atm.pos[1], atm.pos[2]) 69 | 70 | # gather data useful for FIELD file 71 | atom_counts = {} 72 | for i in ordered: 73 | if i.name not in atom_counts: 74 | atom_counts[i.name] = 1 75 | else: 76 | atom_counts[i.name] += 1 77 | return atom_counts 78 | 79 | 80 | def export_as_xmol(atoms, f, title): 81 | print >>f, len(atoms) 82 | print >>f, title 83 | for point in atoms: 84 | print >>f, point.name, point.pos[0], point.pos[1], point.pos[2] 85 | 86 | 87 | def export_for_pielaszek(atoms, f): 88 | "the simplest format, used sometimes here in Unipress" 89 | for point in atoms: 90 | print >>f, point.pos[0], point.pos[1], point.pos[2], point.name 91 | 92 | 93 | def import_pielaszek(ifile): 94 | "the simplest format, used sometimes here in Unipress" 95 | atoms = [] 96 | for i in ifile: 97 | s = i.split() 98 | pos = (float(s[0]), float(s[1]), float(s[2])) 99 | atoms.append(AtomVF(s[3], len(atoms), pos, None, None)) 100 | return model.Model(atoms, pbc=[]) 101 | 102 | 103 | def import_xmol(ifile): 104 | number_of_atoms = int(ifile.readline()) 105 | title = ifile.readline().strip() 106 | atoms = [] 107 | for i in ifile: 108 | s = i.split() 109 | pos = (float(s[1]), float(s[2]), float(s[3])) 110 | atoms.append(AtomVF(s[0], len(atoms), pos, None, None)) 111 | return model.Model(atoms, pbc=[], title=title) 112 | 113 | 114 | def import_dlpoly_config(ifile): 115 | title = ifile.readline().strip() 116 | second_line = ifile.readline().split() 117 | levcfg = int(second_line[0]) 118 | imcon = int(second_line[1]) 119 | return _get_dlpoly_configuration(ifile, title, levcfg, imcon)[0] 120 | 121 | 122 | def _get_dlpoly_configuration(ifile, title, levcfg, imcon): 123 | def get_numbers(line): 124 | return [float(i) for i in line.split()] 125 | assert imcon <= 3, "Sorry, no support for non-parallelepiped PBC" 126 | pbc = [] 127 | if imcon > 0: 128 | for i in range(3): 129 | pbc.append(get_numbers(ifile.readline())) 130 | atoms = [] 131 | next_line = None 132 | while 1: 133 | record1 = ifile.readline() 134 | if not record1: 135 | break 136 | if record1.startswith("timestep"): 137 | next_line = record1 138 | break 139 | rec1_sp = record1.split() 140 | name = rec1_sp[0] 141 | nr = int(rec1_sp[1]) 142 | pos = get_numbers(ifile.readline()) 143 | velocity = None 144 | force = None 145 | if levcfg > 0: 146 | velocity = get_numbers(ifile.readline()) 147 | if levcfg > 1: 148 | force = get_numbers(ifile.readline()) 149 | atoms.append(AtomVF(name, nr, pos, velocity, force)) 150 | return model.Model(atoms, pbc, title), next_line 151 | 152 | 153 | def import_dlpoly_history(ifile): 154 | title = ifile.readline().strip() 155 | second_line = ifile.readline().split() 156 | levcfg = int(second_line[0]) 157 | imcon = int(second_line[1]) 158 | all_configurations = [] 159 | next_line = ifile.readline() 160 | while next_line: 161 | sys.stdout.write(".") 162 | sys.stdout.flush() 163 | ts = next_line.split() 164 | assert int(ts[3]) == levcfg and int(ts[4]) == imcon 165 | c, next_line = _get_dlpoly_configuration(ifile, title, levcfg, imcon) 166 | #TODO yield c 167 | all_configurations.append(c) 168 | sys.stdout.write("\n") 169 | return all_configurations 170 | 171 | 172 | def dlpoly_history_info(ifile): 173 | title = ifile.readline().strip() 174 | print "title:", title 175 | second_line = ifile.readline().split() 176 | print "number of atoms in frame:", second_line[2] 177 | print "has following frames:", 178 | frame_counter = 0 179 | while 1: 180 | line = ifile.readline() 181 | if not line: 182 | break 183 | if line.startswith("timestep"): 184 | print line.split()[1], 185 | sys.stdout.flush() 186 | frame_counter += 1 187 | print "finished.", frame_counter, "frames were found." 188 | 189 | 190 | def get_stoichiometry_string(configuration): 191 | counts = configuration.count_species() 192 | return "Stoichiometry: " + " ".join("%s:%d" % i for i in counts.iteritems()) 193 | 194 | 195 | def export_for_atomeye(configuration, f): 196 | "AtomEye Extended CFG format" 197 | pbc = configuration.pbc 198 | atoms = configuration.atoms 199 | aux = [ 200 | # uncomment to add temperature (works if there are atom velocities 201 | # in the input file) 202 | 203 | #("temperature [K]", lambda a: a.get_temperature()), 204 | 205 | # add aux value that can be used to color code atomic positions 206 | # in unit cell, in selected direction. The coloring works with hsv 207 | # color scale in AtomEye, or any other scale that is cyclic, i.e. 208 | # the same color corresponds to values 0 and 1. 209 | 210 | #("xcolor [0-1]", in_cell_pos_fun(0, pbc[0][0]/2, 211 | # pos0=_find_pos0(atoms))), 212 | ] 213 | 214 | # atoms from VASP POSCAR with Selective dynamics have allow_change attr 215 | if hasattr(atoms[0], "allow_change"): 216 | aux += [ ("change x", lambda a: float(a.allow_change[0])), 217 | ("change y", lambda a: float(a.allow_change[1])), 218 | ("change z", lambda a: float(a.allow_change[2])) ] 219 | 220 | if pbc is None or len(pbc) == 0: 221 | raise ValueError("no PBC") 222 | if not isinstance(pbc, numpy.ndarray): 223 | pbc = numpy.array(pbc) 224 | print >>f, "Number of particles = %i" % len(atoms) 225 | for i in get_comment_list(configuration): 226 | print >>f, "# " + i 227 | print >>f, "A = 1.0 Angstrom (basic length-scale)" 228 | for i in range(3): 229 | for j in range(3): 230 | print >>f, "H0(%i,%i) = %f A" % (i+1, j+1, configuration.pbc[i][j]) 231 | print >>f, ".NO_VELOCITY." 232 | print >>f, "entry_count = %i" % (3 + len(aux)) 233 | for n, a in enumerate(aux): 234 | print >>f, "auxiliary[%i] = %s" % (n, a[0]) 235 | H_1 = linalg.inv(pbc) 236 | previous_name = None 237 | for i in atoms: 238 | if previous_name != i.name: 239 | print >>f, pse.get_atom_mass(i.name) 240 | print >>f, i.name 241 | previous_name = i.name 242 | s = numpy.dot(i.pos, H_1) % 1.0 243 | entries = [s[0], s[1], s[2]] 244 | for aname, afunc in aux: 245 | entries.append(afunc(i)) 246 | print >>f, " ".join("%f" % i for i in entries) 247 | 248 | 249 | # This function returns reference 0 point for coloring based on 250 | # the x coordinate. The point is to color similar systems in the same way, 251 | # even if the center of mass has been shifted. 252 | # You may need to tune it for your data. 253 | def _find_pos0(atoms): 254 | # in my systems B atoms are in a rigid surface, so we use it as 255 | # a reference 256 | selected = [a for a in atoms if a.name == "B"] 257 | if selected: 258 | elem = min(selected, key = lambda x: x[3]) 259 | return elem[1] 260 | else: # no rigid surface 261 | #return sum(a.pos[0] for a in atoms) / len(atoms) 262 | return atoms[0].pos[0] 263 | 264 | 265 | # returns function that returns value in the [0,1) range that corresponds to 266 | # position of atom in unit cell 267 | def in_cell_pos_fun(dir, cell_size, pos0=0): 268 | print "adding aux for %s position in cell size: %g" % (chr(ord('x')+dir), 269 | cell_size) 270 | return lambda a: ((a.pos[dir] - pos0) / cell_size) % 1. 271 | 272 | 273 | def import_atomeye(ifile): 274 | # read header 275 | pbc = [[0,0,0], [0,0,0], [0,0,0]] 276 | has_velocity = True 277 | comments = "" 278 | for line in ifile: 279 | if not line: 280 | continue 281 | elif line[0] == '#': 282 | comments += line[1:] 283 | elif line.startswith("Number of particles"): 284 | number_of_atoms = int(line.split("=")[1]) 285 | elif line.startswith("H0"): 286 | k,v = line[2:].split("=") 287 | a,b = k.strip().lstrip("(").rstrip(")").split(",") 288 | pbc[int(a)-1][int(b)-1] = float(v.split()[0]) 289 | elif line.strip() == ".NO_VELOCITY.": 290 | has_velocity = False 291 | elif line[0].isdigit(): 292 | break 293 | 294 | atoms = [] 295 | vel = None 296 | H = numpy.array(pbc, float) 297 | for line in ifile: 298 | if not line or line.isspace() or line[0] == '#': 299 | continue 300 | elif line[0].isalpha(): 301 | spec = line.strip() 302 | else: 303 | s = line.split() 304 | if len(s) == 1: # this is probably atomic mass 305 | continue 306 | pos = (float(s[0]), float(s[1]), float(s[2])) 307 | pos = numpy.dot(pos, H) 308 | if has_velocity: 309 | vel = (float(s[3]), float(s[4]), float(s[5])) 310 | # if velocities are also reduced: 311 | #vel = numpy.dot(vel, H) 312 | atoms.append(AtomVF(spec, len(atoms), pos, vel, None)) 313 | return model.Model(atoms, pbc=pbc, comments=comments) 314 | 315 | 316 | # This file format doesn't contain atom names, only numbers of atom types. 317 | # The names corresponding to the numbers are in separate lammps file. 318 | # Here we assume that names of the types follow the line "n atom types" 319 | # as comments, e.g.: "2 atom types # C Si" 320 | def import_lammps_data(ifile): 321 | pbc = [[0,0,0], [0,0,0], [0,0,0]] 322 | comments = "" 323 | while 1: 324 | line = ifile.readline() 325 | if '#' in line: 326 | hash_idx = line.find('#') 327 | comment = line[hash_idx+1:] 328 | line = line[:hash_idx].strip() 329 | else: 330 | comment = "" 331 | line = line.strip() 332 | if not line: 333 | if comment: 334 | comments += comment 335 | continue 336 | if line.endswith("atoms"): 337 | number_of_atoms = int(line.split()[0]) 338 | elif line.endswith("atom types"): 339 | number_of_atom_types = int(line.split()[0]) 340 | atom_names = comment.split() 341 | assert len(atom_names) == number_of_atom_types 342 | elif line.endswith("xlo xhi"): 343 | lo, hi = line.split()[0:2] 344 | length = float(hi) - float(lo) 345 | pbc[0][0] = length 346 | elif line.endswith("ylo yhi"): 347 | lo, hi = line.split()[0:2] 348 | length = float(hi) - float(lo) 349 | pbc[1][1] = length 350 | elif line.endswith("zlo zhi"): 351 | lo, hi = line.split()[0:2] 352 | length = float(hi) - float(lo) 353 | pbc[2][2] = length 354 | elif line == "Atoms": 355 | break 356 | else: 357 | assert 0 358 | 359 | atoms = [] 360 | while len(atoms) < number_of_atoms: 361 | s = ifile.readline().split() 362 | if len(s) < 5: 363 | continue 364 | n = int(s[0]) 365 | assert n == len(atoms) + 1 366 | spec = int(s[1]) 367 | name = atom_names[spec-1] 368 | pos = float(s[2]), float(s[3]), float(s[4]) 369 | atoms.append(AtomVF(name, len(atoms), pos, vel=None, force=None)) 370 | 371 | return model.Model(atoms, pbc=pbc, comments=comments) 372 | 373 | def get_comment_list(configuration): 374 | cmd = get_command_line() 375 | cc = ["file written by gosam (SVN $Revision$)", cmd] 376 | if configuration.title and configuration.title != cmd: 377 | cc.append(configuration.title) 378 | cc.append(get_stoichiometry_string(configuration)) 379 | return cc 380 | 381 | 382 | def export_as_lammps(configuration, f): 383 | "exporting coordinates as LAMMPS input data file" 384 | #configuration.orthogonalize_pbc() 385 | ort_pbc = get_orthorhombic_pbc(configuration.pbc) 386 | for i in get_comment_list(configuration): 387 | print >>f, "# " + i 388 | print >>f 389 | counts = configuration.count_species() 390 | species = sorted(counts.keys()) 391 | print >>f, "\n%d\tatoms" % len(configuration.atoms) 392 | 393 | ## temporary hack: for now i need 4 atom types for 2 species 394 | #species += ["B", "Ge"] 395 | 396 | print >>f, "%d atom types # %s" % (len(species), " ".join(species)) 397 | spmap = dict((i, n+1) for n, i in enumerate(species)) 398 | print >>f, "0 %.6f xlo xhi" % ort_pbc[0] 399 | print >>f, "0 %.6f ylo yhi" % ort_pbc[1] 400 | print >>f, "0 %.6f zlo zhi" % ort_pbc[2] 401 | print >>f, "\nAtoms\n" 402 | for n, i in enumerate(configuration.atoms): 403 | print >>f, "%d\t%d\t%.7f\t%.7f\t%.7f" % (n+1, spmap[i.name], 404 | i.pos[0], i.pos[1], i.pos[2]) 405 | 406 | 407 | def export_as_poscar(configuration, f): 408 | "exporting coordinates as VASP POSCAR file" 409 | # make list of species 410 | counts = configuration.count_species() 411 | # reverse if you want to have Si before C 412 | species = sorted(counts.keys(), reverse=False) 413 | 414 | # line 1: a comment (usually the name of the system; we put here elements) 415 | print >>f, " ".join(species) 416 | # line 2: scaling factor 417 | print >>f, "%.15f" % 1 418 | # line 3, 4, 5: the unit cell of the system 419 | for i in range(3): 420 | print >>f, "%19.15f %19.15f %19.15f" % tuple(configuration.pbc[i]) 421 | 422 | # line 6: the number of atoms per atomic species 423 | for i in species: 424 | print >>f, counts[i], 425 | print >>f 426 | 427 | # optional line 428 | selective_dynamics = hasattr(configuration.atoms[0], "allow_change") 429 | if selective_dynamics: 430 | print >>f, "Selective dynamics" 431 | 432 | # line 7: Cartesian/Direct switch 433 | print >>f, "Direct" 434 | 435 | # line 8, ...: atom coordinates 436 | H_1 = linalg.inv(configuration.pbc) 437 | for sp in species: 438 | for i in configuration.atoms: 439 | if i.name == sp: 440 | s = numpy.dot(i.pos, H_1) % 1.0 441 | line = "%19.15f %19.15f %19.15f" % tuple(s) 442 | if selective_dynamics: 443 | sd_symbols = { True: 'T', False: 'F' } 444 | line += " %s %s %s" % (sd_symbols[i.allow_change[0]], 445 | sd_symbols[i.allow_change[1]], 446 | sd_symbols[i.allow_change[2]]) 447 | print >>f, line 448 | 449 | 450 | def import_poscar(ifile): 451 | first_line = ifile.readline().split("#")[0].strip() 452 | # by our convention, first line contains symbols of elements 453 | species = first_line.split() 454 | scaling_factor = float(ifile.readline()) 455 | assert scaling_factor > 0, \ 456 | "POSCAR import: negative scaling factors are not supported" 457 | 458 | pbc = [] 459 | for i in range(3): 460 | line = ifile.readline() 461 | h = [scaling_factor * float(i) for i in line.split()] 462 | assert len(h) == 3 463 | pbc.append(h) 464 | 465 | line = ifile.readline() 466 | atom_count = [int(i) for i in line.split()] 467 | 468 | # check if it's possible that the species above are set correctly 469 | if len(species) != len(atom_count): 470 | print """\ 471 | Error: the first line should contain symbols of atoms. 472 | 473 | POSCAR/CONTCAR files have no atomic symbols. We use convention that the first 474 | line (which is a comment line) contains atomic symbols in the order used in the 475 | 6th line (the 6th line supplies the number of atoms per atomic species). 476 | The actual comment can be given after '#', e.g. 477 | C Si # cubic SiC""" 478 | sys.exit() 479 | 480 | H = numpy.array(pbc, float) 481 | 482 | selective_dynamics = False 483 | cartesian = False 484 | switch = ifile.readline()[0].lower() 485 | if switch == 's': 486 | selective_dynamics = True 487 | switch = ifile.readline()[0].lower() 488 | if switch in ('c', 'k'): 489 | cartesian = True 490 | 491 | atoms = [] 492 | for n, count in enumerate(atom_count): 493 | name = species[n] 494 | for i in xrange(count): 495 | s = ifile.readline().split() 496 | raw_pos = (float(s[0]), float(s[1]), float(s[2])) 497 | if cartesian: 498 | pos = scaling_factor*numpy.array(raw_pos) 499 | else: 500 | pos = numpy.dot(raw_pos, H) 501 | atom = AtomVF(name, len(atoms), pos, None, None); 502 | if len(s) == 6: # interpret T/F 503 | atom.allow_change = (s[3] != 'F', s[4] != 'F', s[5] != 'F') 504 | atoms.append(atom) 505 | 506 | return model.Model(atoms, pbc=pbc) 507 | 508 | 509 | def export_as_gulp(configuration, f): 510 | "export coordinates in GULP input format" 511 | print >>f, "opti" # replace this keyword with GULP keywords you need 512 | print >>f, "" 513 | print >>f, "title" 514 | print >>f, configuration.title 515 | print >>f, "end" 516 | print >>f, "" 517 | print >>f, "cell" 518 | pbc = get_orthorhombic_pbc(configuration.pbc) 519 | print >>f, "%.6g %.6g %.6g %g %g %g" % (pbc[0], pbc[1], pbc[2], 520 | 90., 90., 90.) 521 | print >>f, "fractional" 522 | H_1 = linalg.inv(configuration.pbc) 523 | for i in configuration.atoms: 524 | s = numpy.dot(i.pos, H_1) % 1.0 525 | print >>f, "%s core %11.6g %11.6g %11.6g 0.0000000 1.0000000" % ( 526 | i.name, s[0], s[1], s[2]) 527 | 528 | def get_type_from_filename(name): 529 | lname = name.lower() 530 | if name.endswith(".cfg"): 531 | return "atomeye" 532 | if name.endswith(".lammps") or name.endswith(".lmps"): 533 | return "lammps" 534 | elif name.endswith(".xyz"): 535 | return "xmol" 536 | elif name.endswith(".at"): 537 | return "pielaszek" 538 | elif name.endswith(".gin"): 539 | return "gulp" 540 | #TODO strip directory(?) 541 | elif "revcon" in lname or "config" in lname: 542 | return "dlpoly" 543 | elif "history" in lname: 544 | return "dlpoly_history" 545 | elif "poscar" in lname or "contcar" in lname: 546 | return "poscar" 547 | elif name.endswith(".bz2"): 548 | return get_type_from_filename(name[:-4]) 549 | elif name.endswith(".gz"): 550 | return get_type_from_filename(name[:-3]) 551 | else: 552 | print "Can't deduce filetype from filename:", name 553 | return None 554 | 555 | 556 | def open_any(name, mode='r'): 557 | if name.endswith(".bz2"): 558 | return bz2.BZ2File(name, mode) 559 | elif name.endswith(".gz"): 560 | return gzip.GzipFile(name, mode) 561 | elif name == '-': 562 | if 'w' in mode: 563 | return sys.stdout 564 | else: 565 | return sys.stdin 566 | else: 567 | return file(name, mode) 568 | 569 | 570 | def import_autodetected(filename): 571 | input_type = get_type_from_filename(filename) 572 | infile = open_any(filename) 573 | if input_type == "xmol": 574 | return import_xmol(infile) 575 | elif input_type == "dlpoly": 576 | return import_dlpoly_config(infile) 577 | elif input_type == "dlpoly_history": 578 | return import_dlpoly_history(infile) 579 | elif input_type == "pielaszek": 580 | return import_pielaszek(infile) 581 | elif input_type == "atomeye": 582 | return import_atomeye(infile) 583 | elif input_type == "lammps": 584 | return import_lammps_data(infile) 585 | elif input_type == "poscar": 586 | return import_poscar(infile) 587 | else: 588 | assert 0 589 | 590 | def parse_translate_option(str): 591 | tr_map = {} 592 | for i in str.split(","): 593 | if "->" not in i: 594 | raise ValueError("wrong format of --translate option") 595 | k, v = i.split("->") 596 | tr_map[k.strip()] = v.strip() 597 | return tr_map 598 | 599 | 600 | def parse_options(argv): 601 | parser = OptionParser("usage: %prog [--pbc=list] input_file output_file") 602 | parser.add_option("--pbc", 603 | help="set PBC, e.g. '[(60,0,0),(0,60,0),(0,0,60)]'") 604 | parser.add_option("--center-zero", action="store_true", 605 | help="move center of mass (with all atoms equal) to 0") 606 | parser.add_option("--prefer-negative", action="store_true", 607 | help="if in PBC, select image in (-h/2, h/2)") 608 | parser.add_option("--filter", 609 | help="criterion for leaving atoms, e.g. '15 < z < 30'") 610 | parser.add_option("--vibrate", type="float", metavar="SIGMA", 611 | help="random displacements (Gaussian distribution, " 612 | "std. dev. equal SIGMA in every direction)") 613 | parser.add_option("--translate", metavar="TR", 614 | help="change atomic symbols, e.g. 'Si->C, C->Si'") 615 | parser.add_option("--reference", help="adds dx, dy and dz properties", 616 | metavar="FILE") 617 | (options, args) = parser.parse_args(argv) 618 | if len(args) == 0: 619 | parser.print_help() 620 | sys.exit() 621 | if not (len(args) == 2 or ("vs" in args and len(args) in (5,6))): 622 | parser.error("Two arguments (input and output filenames) are required") 623 | return (options, args) 624 | 625 | 626 | def put_pbc_image_between_halfs(cfg): 627 | pbc = numpy.diagonal(cfg.pbc) 628 | for atom in cfg.atoms: 629 | for i in range(3): 630 | if atom.pos[i] > pbc[i] / 2.: 631 | atom.pos[i] -= pbc[i] 632 | elif atom.pos[i] < -pbc[i] / 2.: 633 | atom.pos[i] += pbc[i] 634 | 635 | 636 | def process_input(input_filename, options): 637 | cfg = import_autodetected(input_filename) 638 | if options.pbc: 639 | cfg.pbc = eval(options.pbc) 640 | 641 | if options.prefer_negative: 642 | if not cfg.pbc: 643 | print "Error: Option --prefer-negative can be used only in PBC" 644 | sys.exit() 645 | put_pbc_image_between_halfs(cfg) 646 | 647 | if options.reference: 648 | cref = import_autodetected(options.reference) 649 | assert len(cref.atoms) == len(cfg.atoms) 650 | pbc = numpy.diagonal(cfg.pbc) 651 | for n, a1 in enumerate(cfg.atoms): 652 | a0 = cref.atoms[n] 653 | a1.dpos = [a1.pos[0] - a0.pos[0], 654 | a1.pos[1] - a0.pos[1], 655 | a1.pos[2] - a0.pos[2]] 656 | for i in range(3): 657 | if a1.dpos[i] > pbc[i] / 2.: 658 | a1.dpos[i] -= pbc[i] 659 | elif a1.dpos[i] < -pbc[i] / 2.: 660 | a1.dpos[i] += pbc[i] 661 | del cref 662 | 663 | if options.center_zero: 664 | ctr = sum(atom.pos for atom in cfg.atoms) / len(cfg.atoms) 665 | print "center: (%.3f, %.3f, %.3f)" % tuple(ctr) 666 | for atom in cfg.atoms: 667 | atom.pos -= ctr 668 | 669 | if options.filter: 670 | def f(atom): 671 | name = atom.name 672 | x, y, z = atom.pos 673 | return eval(options.filter) 674 | cfg.atoms = [i for i in cfg.atoms if f(i)] 675 | print len(cfg.atoms), "atoms left." 676 | if len(cfg.atoms) == 0: 677 | sys.exit() 678 | 679 | if options.vibrate: 680 | sigma = options.vibrate 681 | for atom in cfg.atoms: 682 | atom.pos += (random.gauss(0, sigma), 683 | random.gauss(0, sigma), 684 | random.gauss(0, sigma)) 685 | 686 | if options.translate: 687 | tr_map = parse_translate_option(options.translate) 688 | for i in cfg.atoms: 689 | if i.name in tr_map: 690 | i.name = tr_map[i.name] 691 | 692 | return cfg 693 | 694 | def export_autodetected(configuration, output_filename): 695 | output_type = get_type_from_filename(output_filename) 696 | #TODO: check if file already exists 697 | ofile = open(output_filename, 'w') 698 | configuration.export_atoms(output_filename, output_type) 699 | 700 | def convert(argv): 701 | "converts atomistic files" 702 | options, args = parse_options(argv) 703 | configuration = process_input(args[0], options) 704 | export_autodetected(configuration, args[1]) 705 | 706 | 707 | def get_atom_func(name): 708 | def x(atom): return atom.pos[0] 709 | def y(atom): return atom.pos[1] 710 | def z(atom): return atom.pos[2] 711 | def vx(atom): return atom.vel[0] # [A/ns] 712 | def vy(atom): return atom.vel[1] 713 | def vz(atom): return atom.vel[2] 714 | def v(atom): return atom.get_velocity() 715 | def T(atom): return atom.get_temperature() 716 | def Ekin(atom): return atom.get_ekin() 717 | def dx(atom): return atom.dpos[0] 718 | def dy(atom): return atom.dpos[1] 719 | def dz(atom): return atom.dpos[2] 720 | return eval(name) 721 | 722 | def avg_plot(argv): 723 | options, args = parse_options(argv) 724 | configuration = process_input(args[0], options) 725 | output_filename = args[1] 726 | yfuncs = [get_atom_func(i) for i in args[2].split(",")] 727 | assert args[3] == "vs" 728 | xfunc = get_atom_func(args[4]) 729 | xy = [] 730 | if len(yfuncs) == 1: 731 | yfunc = yfuncs[0] 732 | for i in configuration.atoms: 733 | xy.append((xfunc(i), yfunc(i))) 734 | else: 735 | for i in configuration.atoms: 736 | xy.append((xfunc(i),) + tuple(yfunc(i) for yfunc in yfuncs)) 737 | minx = min(i[0] for i in xy) 738 | maxx = max(i[0] for i in xy) + 1e-6 739 | print "n=%d, x E <%g,%g)" % (len(xy), minx, maxx) 740 | for n, yf in enumerate(yfuncs): 741 | miny = min(i[n+1] for i in xy) 742 | maxy = max(i[n+1] for i in xy) 743 | avgy = sum(i[n+1] for i in xy) / len(xy) 744 | print "%s E <%g, %g>, avg: %g" % (yf.__name__, miny, maxy, avgy) 745 | 746 | # make histogram 747 | nbins = (int(args[5]) if len(args) >= 6 else 128) 748 | t = nbins / (maxx - minx) 749 | lines = [[minx + (n + 0.5) / t] for n in range(nbins)] 750 | 751 | for yn in range(len(yfuncs)): 752 | data = [StdDev() for i in range(nbins)] 753 | for i in xy: 754 | x, y = i[0], i[1+yn] 755 | bin = int((x - minx) * t) 756 | data[bin].add_x(y) 757 | 758 | for n, d in enumerate(data): 759 | if d.n > 0: 760 | lines[n] += [d.mean, d.n, d.get_stddev()] 761 | 762 | ofile = open_any(output_filename, 'w') 763 | for line in lines: 764 | if len(line) > 1: 765 | print >>ofile, " ".join(str(i) for i in line) 766 | 767 | 768 | if __name__ == '__main__': 769 | if len(sys.argv) > 1 and sys.argv[1] == "--dlpoly-history-info": 770 | dlpoly_history_info(file(sys.argv[2])) 771 | elif "vs" in sys.argv: 772 | avg_plot(sys.argv[1:]) 773 | else: 774 | convert(sys.argv[1:]) 775 | 776 | 777 | -------------------------------------------------------------------------------- /mdprim.py: -------------------------------------------------------------------------------- 1 | # this file is part of gosam (generator of simple atomistic models) 2 | # Licence: GNU General Public License version 2 3 | """\ 4 | Atom and CellMethod classes. 5 | """ 6 | 7 | from math import sqrt, acos 8 | import numpy 9 | from numpy import array, inner 10 | 11 | import pse 12 | import rotmat 13 | 14 | class Atom: 15 | "atom - symbol and its position in 3D space" 16 | def __init__(self, name, pos): 17 | self.name = name 18 | self.pos = array(pos) 19 | 20 | def __str__(self): 21 | return "Atom %s at %s" % (self.name, self.pos) 22 | 23 | def get_dist(self, atom2, pbc_half=None): 24 | d = self.pos - atom2.pos 25 | if pbc_half is not None and (abs(d) > pbc_half).any(): 26 | for i in range(3): 27 | if abs(d[i]) > pbc_half[i]: 28 | d[i] = 2 * pbc_half[i] - abs(d[i]) 29 | return sqrt(inner(d, d)) # sqrt(sum(d**2)) is slower 30 | 31 | def get_shift(self, atom2, pbc=None): 32 | d = atom2.pos - self.pos 33 | if pbc is not None and (abs(d) > pbc / 2).any(): 34 | for i in range(3): 35 | if d[i] > pbc[i] / 2: 36 | d[i] -= pbc[i] 37 | elif d[i] < -pbc[i] / 2: 38 | d[i] += pbc[i] 39 | return d 40 | 41 | def get_angle(self, atom2, atom3): 42 | "angle of atom2-self-atom3 in radians" 43 | v1 = atom2.pos - self.pos 44 | v2 = atom3.pos - self.pos 45 | return acos(inner(v1, v2) / sqrt(inner(v1, v1) * inner(v2, v2))) 46 | 47 | def get_temperature(self): 48 | assert 0, "Unknown temperature" 49 | 50 | def get_velocity(self): 51 | assert 0, "Unknown velocity" 52 | 53 | def get_ekin(self): 54 | assert 0, "Unknown velocity" 55 | 56 | 57 | class AtomG(Atom): 58 | "atom (in grain) - additional information about distance to surface" 59 | def __init__(self, name, pos, min_dist): 60 | Atom.__init__(self, name, pos) 61 | self.min_dist = min_dist 62 | 63 | def __str__(self): 64 | return Atom.__str__(self) + "; min. dist: %f" % (self.min_dist) 65 | 66 | 67 | class AtomVF(Atom): 68 | "atom with velocity info" 69 | def __init__(self, name, nr, pos, vel, force): 70 | Atom.__init__(self, name, pos) 71 | self.nr = nr 72 | self.vel = array(vel) 73 | self.force = array(force) 74 | 75 | def __str__(self): 76 | return "Atom %s at %s; velocity: %s" % (self.name, self.pos, self.vel) 77 | 78 | def get_mass(self): 79 | return pse.get_atom_mass(self.name) 80 | 81 | def get_velocity(self): 82 | return sqrt(inner(self.vel, self.vel)) 83 | 84 | def get_ekin(self): 85 | conv_factor = 1.0364269e-10 # u * (A/ns)^2 -> eV 86 | return conv_factor * 0.5 * self.get_mass() * inner(self.vel, self.vel) 87 | 88 | def get_temperature(self): 89 | if self.vel is None or self.vel[0] is None: 90 | return 0 91 | kB = 8.617343e-5 # eV/K 92 | #kB = 831447.2 # u A^2 / ns^2 K 93 | return 2 * self.get_ekin() / (3*kB) 94 | 95 | 96 | #only supports orthorhombic PBC 97 | class CellMethod: 98 | "Neighbour list maker" 99 | def __init__(self, atoms, r, pbc=None): 100 | self.atoms = atoms 101 | if pbc is not None: 102 | assert rotmat.is_diagonal(pbc) 103 | self.box_d = self.pbc = pbc.diagonal() 104 | else: 105 | box_min, box_max = self._find_containing_box() 106 | self.box_d = box_max - box_min 107 | self.pbc = None 108 | self._make_cells(r) 109 | 110 | def _find_containing_box(self): 111 | m = M = self.atoms[0].pos 112 | for i in self.atoms: 113 | m = numpy.minimum(i.pos, m) 114 | M = numpy.maximum(i.pos, M) 115 | return m, M 116 | 117 | def _get_cell_coord(self, a): 118 | return numpy.floor(a.pos * self.nc / self.box_d).astype(int) % self.nc 119 | 120 | def _make_cells(self, r): 121 | self.r = r 122 | self.nc = (self.box_d / (r + 1e-9)).astype(int) 123 | 124 | # avoid memory problems when cut-off is small 125 | nc_prod = self.nc[0] * self.nc[1] * self.nc[2] 126 | if nc_prod > 1000000 and nc_prod > 2 * len(self.atoms): 127 | f = float(nc_prod) / len(self.atoms) 128 | self.nc /= f**(1./3) 129 | 130 | cell_count = self.nc[0] * self.nc[1] * self.nc[2] 131 | self.cells = [[] for i in range(cell_count)] 132 | for a_idx, a in enumerate(self.atoms): 133 | nx, ny, nz = self._get_cell_coord(a) 134 | cell_idx = (nx * self.nc[1] + ny) * self.nc[2] + nz 135 | self.cells[cell_idx].append(a_idx) 136 | print "... system divided into %d x %d x %d cells ..." % tuple(self.nc) 137 | 138 | 139 | def _get_neigh_cells_in_dim(self, n, c): 140 | assert 0 <= n < c 141 | if c > 2: 142 | return (n-1) % c, n, (n+1) % c 143 | elif c == 2: 144 | return 0, 1 145 | elif c == 1: 146 | return 0, 147 | 148 | def _get_neighbour_cells(self, a): 149 | nx, ny, nz = self._get_cell_coord(a) 150 | for tix in self._get_neigh_cells_in_dim(nx, self.nc[0]): 151 | for tiy in self._get_neigh_cells_in_dim(ny, self.nc[1]): 152 | for tiz in self._get_neigh_cells_in_dim(nz, self.nc[2]): 153 | cell_idx = (tix * self.nc[1] + tiy) * self.nc[2] + tiz 154 | yield cell_idx 155 | 156 | 157 | def get_neighbours(self, n, extra_condition=None): 158 | assert self.pbc is None 159 | a = self.atoms[n] 160 | for cell_idx in self._get_neighbour_cells(a): 161 | for i in self.cells[cell_idx]: 162 | dist = a.get_dist(self.atoms[i]) 163 | if n != i and dist < self.r and (extra_condition is None 164 | or extra_condition(dist)): 165 | yield i 166 | 167 | 168 | def pop_neighbours(self, n, max_dist=None): 169 | if max_dist is None: 170 | max_dist = self.r 171 | if self.pbc is not None: 172 | half_pbc = array(self.pbc) / 2. 173 | else: 174 | half_pbc = None 175 | a = self.atoms[n] 176 | for cell_idx in self._get_neighbour_cells(a): 177 | cell = self.cells[cell_idx] 178 | for i_idx, i in enumerate(cell): 179 | if n != i and (a.get_dist(self.atoms[i], half_pbc) < max_dist): 180 | yield cell.pop(i_idx) 181 | 182 | 183 | def count_neighbours(self, n, extra_condition=None): 184 | counter = 0 185 | for i in self.get_neighbours(n, extra_condition): 186 | counter += 1 187 | return counter 188 | 189 | 190 | def get_atoms_to_remove(self): 191 | """Return map, which keys are indices of atoms for removing, 192 | and values are neighbours that caused atom to be removed. 193 | Atoms are removed to leave not more than one atom in one cell. 194 | """ 195 | to_be_deleted = {} 196 | for n, i in enumerate(self.atoms): 197 | for j in self.pop_neighbours(n): 198 | if j in to_be_deleted: 199 | if n not in to_be_deleted[j]: 200 | to_be_deleted[j].append(n) 201 | elif n in to_be_deleted: 202 | if j not in to_be_deleted[n]: 203 | to_be_deleted[n].append(j) 204 | else: 205 | to_be_deleted[j] = [n] 206 | return to_be_deleted 207 | 208 | 209 | -------------------------------------------------------------------------------- /model.py: -------------------------------------------------------------------------------- 1 | # this file is part of gosam (generator of simple atomistic models) 2 | # Licence: GNU General Public License version 2 3 | """\ 4 | class Model -- atomistic model, optionally in PBC -- vacancies, 5 | colision detection, saving to file, etc. 6 | """ 7 | 8 | import inspect 9 | import random 10 | import sys 11 | import math 12 | import numpy 13 | from numpy import array, dot, linalg 14 | 15 | import mdprim 16 | import mdfile 17 | import rotmat 18 | from rotmat import rodrigues, is_diagonal, pt_in_box 19 | 20 | def _sort_and_uniq(dd): 21 | "sort real number and leave only unique ones (compare using epsilon)" 22 | dd.sort() 23 | n = 0 24 | while n+1 < len(dd): 25 | if abs(dd[n] - dd[n+1]) < 1e-6: 26 | del dd[n+1] 27 | else: 28 | n += 1 29 | 30 | 31 | def _get_orthorhombic_pbc(m): 32 | """\ 33 | the input is matrix 3x3, the output is diagonal. 34 | Distorts the input matrix such that the output 35 | is orthorhombic with the same volume as the 36 | space defined by the input matrix. 37 | """ 38 | if is_diagonal(m): 39 | return m 40 | else: 41 | # x, y, z === unit vector in orthogonal cartesian space 42 | x, y, z = numpy.identity(3) 43 | # xi, yi, zi === initial matrix (m) 44 | xi, yi, zi = m 45 | # xf, yf, zf === final matrix (orthorhombic matrix) 46 | # 47 | # rotate full_pbc to make xi colinear with x 48 | angle = -1.0*numpy.sign(xi[0])*math.acos(xi[0]/linalg.norm(xi)) 49 | ortho = numpy.dot(m, rodrigues(z, angle)) 50 | # yf (zf) is the projection of the rotated yi (zi) onto y (z) 51 | ortho = numpy.diag(numpy.diagonal(ortho)) 52 | return ortho 53 | 54 | def make_drawing_func(probability): 55 | if type(probability) is dict: 56 | return lambda atom: random.random() < probability.get(atom.name, -1) 57 | elif type(probability) in (int, long, float): 58 | return lambda atom: random.random() < probability 59 | else: 60 | return lambda atom: random.random() < probability(atom) 61 | 62 | class Model: 63 | """Configuration -- atoms, PBC (only parallelepiped)""" 64 | def __init__(self, atoms, pbc, title="", comments=None): 65 | self.atoms = atoms 66 | self.pbc = pbc 67 | self.title = title 68 | self.comments = comments 69 | 70 | def log(self, text): 71 | "to enable logging, do 'operations=[]'" 72 | if hasattr(self, "operations"): 73 | self.operations.append(text) 74 | 75 | 76 | def round_atom_coordinates(self, ndigits=9): 77 | "rounds coordinates of all atoms (points)" 78 | for i in self.atoms: 79 | for j in range(3): 80 | i.pos[j] = round(i.pos[j], ndigits) + 0.0 81 | self.log("atom coordinates rounded to %s digits after dot." % ndigits) 82 | 83 | 84 | def make_vacancies(self, vacancy_probability): 85 | """vacancy_probability is either number 86 | or dict: {atom_name: float <0,1>} or function atom -> float <0,1>. 87 | """ 88 | if not vacancy_probability: 89 | return 90 | len_before = len(self.atoms) 91 | func = make_drawing_func(vacancy_probability) 92 | self.atoms = [a for a in self.atoms if not func(a)] 93 | len_after = len(self.atoms) 94 | n_vacancies = len_before-len_after 95 | print "Vacancies: %i atoms were deleted. %i atoms left." % ( 96 | n_vacancies, len_after) 97 | self.log("vacancies where generated (%i of %i) using probabilities: %s" 98 | % (n_vacancies, len_before, vacancy_probability)) 99 | 100 | 101 | def modify_atoms(self, modifier): 102 | "Use given function to modify atom (point) coordinates" 103 | if not modifier: 104 | return 105 | for i in self.atoms: 106 | modifier(i) 107 | self.log("atoms modified using function: \n\t" 108 | + inspect.getsource(modifier).replace("\n", "\n\t")) 109 | 110 | 111 | def roundup_atoms(self): 112 | "collect all atoms into the box defined by the pbc and the\n\ 113 | minimum atom position" 114 | # rigid shift of the atom positions sets min(atom.pos) to (0,0,0) 115 | m = array(map(min, numpy.transpose([a.pos for a in self.atoms]))) 116 | for i in self.atoms: 117 | i.pos -= m 118 | # ensure the pbc are (a) present and (b) a 3x3 matrix 119 | shape = numpy.shape(self.pbc) 120 | if shape == (3,3): 121 | pv = self.pbc 122 | elif shape == (3,): 123 | pv = numpy.diag(self.pbc) 124 | else: 125 | raise ValueError("PBC is not 3 dimensional") 126 | # move all atoms into the box defined by the pbc 127 | pvinv = numpy.linalg.inv(pv) 128 | for i in self.atoms: 129 | d = numpy.floor(numpy.dot(i.pos, pvinv)) 130 | d = numpy.dot(d, pv) 131 | i.pos -= d 132 | 133 | 134 | def orthogonalize_pbc(self, verbose=False): 135 | "moves the atoms periodically to generate orthogonal pbc" 136 | # put all atoms in box with corners (0,0,0), pbc 137 | self.roundup_atoms() 138 | # convenience variables 139 | pvi = self.pbc 140 | pvf = _get_orthorhombic_pbc(pvi) 141 | invpvf = linalg.inv(pvf) 142 | pvi_dot_invpvf = numpy.dot(pvi, invpvf) 143 | # check each atom 144 | for atom in self.atoms: 145 | if verbose: 146 | print "Atom " + str(atom.name) + "...", 147 | shell = 0 148 | # si, scaled position of initial point in final pbc 149 | # sf, scaled position of final point in final pbc 150 | # sx, scaled translation of initial point along initial pbc 151 | # projected onto final pbc 152 | si = dot(atom.pos, invpvf) 153 | sf = si 154 | sx = array([0,0,0]) 155 | while not pt_in_box(sf): 156 | # check the next shell 157 | shell += 1 158 | if verbose: 159 | print "shell " + str(shell) + "...", 160 | # make a list of all the boxes 161 | scaled_pos = sum(sum( 162 | [[[[i,j,k] 163 | for k in range(-shell, shell+1)] 164 | for j in range(-shell, shell+1)] 165 | for i in range(-shell, shell+1)], []),[]) 166 | # only check those boxes on the edge--the shell 167 | # the other will have been checked on a previous 168 | # iteration 169 | scaled_pos = filter(lambda x: shell in numpy.abs(x), scaled_pos) 170 | for sx in scaled_pos: 171 | sf = si + dot(sx, pvi_dot_invpvf) 172 | if pt_in_box(sf): 173 | break 174 | atom.pos += dot(sx, pvi) 175 | if verbose: 176 | print "done" 177 | # set the pbc to the final pbc 178 | self.pbc = pvf 179 | 180 | 181 | def count_neighbours(self, atom, max_bondlength): 182 | "O(N^2); use mdprim.CellMethod instead" 183 | print "WARNING: ineffective neighbour counting in use" 184 | neighbors = 0 185 | for j in self.atoms: 186 | #optimization 187 | if abs(j.pos[0] - atom.pos[0]) < max_bondlength \ 188 | and abs(j.pos[1] - atom.pos[1]) < max_bondlength \ 189 | and 1e-3 < atom.get_dist(j) < max_bondlength: 190 | neighbors += 1 191 | return neighbors 192 | 193 | 194 | def print_coordination_statistics(self, max_bondlength): 195 | print "Coordination statistics: ", 196 | sys.stdout.flush() 197 | stat = {} 198 | #for i in self.atoms: 199 | # n = self.count_neighbours(i, max_bondlength) 200 | # stat[n] = stat.get(n, 0) + 1 201 | cm = mdprim.CellMethod(self.atoms, max_bondlength) 202 | for a_idx, i in enumerate(self.atoms): 203 | n = cm.count_neighbours(a_idx) 204 | stat[n] = stat.get(n, 0) + 1 205 | s = stat.items() 206 | s.sort(lambda x,y: -cmp(x[1], y[1])) 207 | print ", ".join("%i: %i" % i for i in s) 208 | 209 | 210 | def print_stochiometry(self): 211 | print "Stochiometry: ", 212 | stat = {} 213 | for i in self.atoms: 214 | if i.name in stat: 215 | stat[i.name] += 1 216 | else: 217 | stat[i.name] = 1 218 | s = stat.items() 219 | s.sort(lambda x,y: -cmp(x[1], y[1])) 220 | print ", ".join("%s: %i" % i for i in s) 221 | 222 | 223 | def remove_undercoordinated_atoms(self, max_bondlength): 224 | """Remove atoms that have only 1 nearest neighbor 225 | and some with 2 nearest neighbors (stoichiometry is conserved). 226 | For use in tetrahedrally coordinated lattices. 227 | """ 228 | print "Removing under-coordinated atoms..." 229 | before = len(self.atoms) 230 | for iter in range(2): 231 | cm = mdprim.CellMethod(self.atoms, max_bondlength) 232 | to_be_deleted = [] 233 | for n, i in enumerate(self.atoms): 234 | c = cm.count_neighbours(n) 235 | #c = self.count_neighbours(i, max_bondlength) 236 | if c <= 1: 237 | to_be_deleted.append(n) 238 | to_be_deleted.sort(reverse=True) 239 | for i in to_be_deleted: 240 | del self.atoms[i] 241 | rem = before - len(self.atoms) 242 | print "... %i atoms removed." % rem 243 | self.print_coordination_statistics(max_bondlength) 244 | self.print_stochiometry() 245 | self.log("removed %i under-coordinated atoms" % rem) 246 | 247 | 248 | def _print_deleted_dist_stats(self, atoms, to_be_deleted): 249 | dd = [] 250 | pbc_half = array(self.pbc.diagonal()) / 2. 251 | for k,v in to_be_deleted.iteritems(): 252 | for j in v: 253 | dist = atoms[k].get_dist(atoms[j], pbc_half=pbc_half) 254 | dd.append(dist) 255 | if not dd: 256 | print "no atoms were too close" 257 | return 258 | print " deleted atoms distances: from %s to %s" % (min(dd), max(dd)) 259 | 260 | 261 | def _shift_before_removing(self, to_be_deleted): 262 | """if only pairs of atoms of the same species are too close 263 | to each other, move the atom that won't be deleted to the position 264 | between it's old position and the position of the neighbour. 265 | """ 266 | pbc = self.pbc.diagonal() 267 | for k, v in to_be_deleted.iteritems(): 268 | assert len(v) == 1, "%s %s" % (k, v) 269 | assert v[0] not in to_be_deleted, "%s" % v[0] 270 | a = self.atoms[k] 271 | b = self.atoms[v[0]] 272 | assert a.name == b.name, "%s %s" % (a.name, b.name) 273 | # a will be deleted, b not 274 | d = b.get_shift(a, pbc=pbc) 275 | b.pos += d / 2 276 | 277 | 278 | def get_atoms_to_be_removed(self, atoms, distance): 279 | assert rotmat.is_diagonal(self.pbc) 280 | cm = mdprim.CellMethod(atoms, distance, self.pbc) 281 | return cm.get_atoms_to_remove() 282 | 283 | def remove_close_neighbours(self, distance, atoms=None): 284 | """Remove atoms in such a way that no two atoms are in distance 285 | smaller than `distance' 286 | """ 287 | if atoms is None: 288 | atoms = self.atoms 289 | before = len(atoms) 290 | to_be_deleted = self.get_atoms_to_be_removed(atoms, distance) 291 | self._print_deleted_dist_stats(atoms, to_be_deleted) 292 | #self._shift_before_removing(to_be_deleted) 293 | tbd_idx = to_be_deleted.keys() 294 | tbd_idx.sort(reverse=True) 295 | for i in tbd_idx: 296 | del atoms[i] 297 | rem = before - len(atoms) 298 | print "... %i atoms removed." % rem 299 | if atoms is self.atoms: # otherwise self.atoms stats are useless 300 | self.print_stochiometry() 301 | self.log("removed %i too-close atoms" % rem) 302 | 303 | 304 | def add_close_neigh_properties(self): 305 | """ 306 | add .r1 and .r2 members (None or float) to each atom 307 | r1 - the closest distance to other atom 308 | r2 - the closest distance to other atom with the same symbol 309 | """ 310 | r1_max = 1.87 311 | r2_max = 3.00 312 | 313 | for i in self.atoms: 314 | i.r1 = None 315 | i.r2 = None 316 | 317 | pbc_half = array(self.pbc.diagonal()) / 2. 318 | 319 | # r1 320 | to_be_rm1 = self.get_atoms_to_be_removed(self.atoms, r1_max) 321 | for k,v in to_be_rm1.iteritems(): 322 | atom = self.atoms[k] 323 | d = min(atom.get_dist(self.atoms[j], pbc_half=pbc_half) for j in v) 324 | atom.r1 = d 325 | 326 | # r2 327 | a_name = self.atoms[0].name 328 | a_atoms = [i for i in self.atoms if i.name == a_name] 329 | b_atoms = [i for i in self.atoms if i.name != a_name] 330 | for x_atoms in a_atoms, b_atoms: 331 | to_be_rm2 = self.get_atoms_to_be_removed(x_atoms, r2_max) 332 | for k,v in to_be_rm2.iteritems(): 333 | atom = x_atoms[k] 334 | d = min(atom.get_dist(x_atoms[j], pbc_half=pbc_half) for j in v) 335 | #if not atom.r1 or d > atom.r1: 336 | atom.r2 = d 337 | 338 | 339 | def output_all_removal_possibilities(self, filename): 340 | assert "%" in filename 341 | 342 | for n, i in enumerate(self.atoms): 343 | i.nr = n 344 | 345 | self.add_close_neigh_properties() 346 | 347 | distances1 = [0] + [i.r1 + 1e-6 for i in self.atoms if i.r1] 348 | distances2 = [0] + [i.r2 + 1e-6 for i in self.atoms if i.r2] 349 | 350 | _sort_and_uniq(distances1) 351 | _sort_and_uniq(distances2) 352 | print "inter-atomic distances:", distances1 353 | print "same species distances:", distances2 354 | print "atoms count:", len(self.atoms) 355 | print 356 | 357 | counter = 1 358 | orig_atoms = self.atoms 359 | all_rm = [] 360 | for j in distances2: 361 | for i in distances1: 362 | #if j <= i: 363 | # continue 364 | rm = [a.nr for a in orig_atoms if (a.r1 and a.r1 < i) 365 | or (a.r2 and a.r2 < j)] 366 | if rm in all_rm: 367 | print "ignore cutoffs: %g, %g (%d atoms)" % (i, j, len(rm)) 368 | continue 369 | all_rm.append(rm) 370 | self.title = "del %d atoms with cutoffs: %g, %g" % ( 371 | len(rm), i, j) 372 | print self.title 373 | self.atoms = [a for a in orig_atoms if a.nr not in rm] 374 | fn = filename.replace('%', str(counter)) 375 | self.export_atoms(fn) 376 | counter += 1 377 | 378 | 379 | def _find_symmetric_z_distances(self): 380 | distances = [0] 381 | for i in self.atoms: 382 | d = 2 * i.pos[2] 383 | if 1e-7 < d < 3.0: 384 | distances.append(d + 1e-6) 385 | _sort_and_uniq(distances) 386 | print "same species distances:", distances 387 | print "atoms count:", len(self.atoms) 388 | return distances 389 | 390 | # this function is not used, it will be deleted in future 391 | def output_all_removal2_possibilities_TO_BE_REMOVED(self, filename): 392 | assert "%" in filename 393 | distances = self._find_symmetric_z_distances() 394 | orig_atoms = self.atoms 395 | def upper(a): 396 | return 1 if a.pos[1] > 0 else -1 397 | for n, j in enumerate(distances): 398 | # the version using upper() removes atoms from one crystal, 399 | # in upper half (i.e. for y > 1) of the boundary, 400 | # and from the other crystal in the bottom half. 401 | self.atoms = [a for a in orig_atoms 402 | # if not 1e-7 < a.pos[2] < j / 2.] 403 | if not 1e-7 < (upper(a)*a.pos[2]) < j / 2.] 404 | ndel = len(orig_atoms) - len(self.atoms) 405 | self.title = "del %d atoms with cutoff: %g" % (ndel, j) 406 | print self.title 407 | fn = filename.replace('%', str(n)) 408 | self.export_atoms(fn) 409 | 410 | 411 | def apply_all_possible_cutoffs_to_stgb(self, filename, single_cutoff): 412 | assert "%" in filename 413 | distances = self._find_symmetric_z_distances() 414 | orig_atoms = self.atoms 415 | def upper(a): 416 | return 1 if a.pos[1] > 0 else -1 417 | 418 | if single_cutoff: 419 | for n, j in enumerate(distances): 420 | # the version using upper() removes atoms from one crystal, 421 | # in upper half (i.e. for y > 1) of the boundary, 422 | # and from the other crystal in the bottom half. 423 | self.atoms = [a for a in orig_atoms 424 | if not 1e-7 < a.pos[2] < j / 2.] 425 | # if not 1e-7 < (upper(a)*a.pos[2]) < j / 2.] 426 | ndel = len(orig_atoms) - len(self.atoms) 427 | self.title = "del %d atoms with cutoff: %g" % (ndel, j) 428 | print self.title 429 | fn = filename.replace('%', str(n)) 430 | self.export_atoms(fn) 431 | else: 432 | species = self.count_species() 433 | assert len(species) == 2 434 | name1, name2 = sorted(species.keys()) # C, Si 435 | for n1, j1 in enumerate(distances): 436 | for n2, j2 in enumerate(distances): 437 | zmax = { name1: j1 / 2., name2: j2 / 2. } 438 | self.atoms = [a for a in orig_atoms 439 | if not 1e-7 < upper(a)*a.pos[2] < zmax[a.name]] 440 | ndel = len(orig_atoms) - len(self.atoms) 441 | self.title = "del %d atoms with cutoffs: %g, %g" % ( 442 | ndel, j1, j2) 443 | print self.title 444 | fn = filename.replace('%', "%d-%d" % (n1, n2)) 445 | self.export_atoms(fn) 446 | 447 | 448 | def export_atoms(self, f, format=None): 449 | """ 450 | save atoms to file f in one of possible formats 451 | """ 452 | if type(f) in (str, unicode): 453 | f = mdfile.open_any(f, 'w') 454 | if format is None: 455 | format = mdfile.get_type_from_filename(f.name); 456 | if format is None: 457 | return 458 | format = format.lower() 459 | print "Saving atoms to file '%s' in format '%s'" % (f.name, format) 460 | self._do_export_atoms(f, format) 461 | self.log("atoms saved to file '%s' in format '%s'" % (f.name, format)) 462 | 463 | def _do_export_atoms(self, f, format): 464 | if format == "xmol": 465 | mdfile.export_as_xmol(self.atoms, f, self.title) 466 | elif format == "pielaszek": 467 | mdfile.export_for_pielaszek(self.atoms, f) 468 | elif format == "dlpoly": 469 | mdfile.export_for_dlpoly(self.atoms, f, self.title) 470 | elif format == "atomeye": 471 | mdfile.export_for_atomeye(self, f) 472 | elif format == "poscar": 473 | mdfile.export_as_poscar(self, f) 474 | elif format == "gulp": 475 | mdfile.export_as_gulp(self, f) 476 | elif format == "lammps": 477 | mdfile.export_as_lammps(self, f) 478 | else: 479 | print >>f, "Unknown format requested: %s" % format 480 | 481 | 482 | def get_center(self, onAtom=False): 483 | n = len(self.atoms) 484 | ctr_pos = sum([i.pos for i in self.atoms]) / n 485 | ctr = mdprim.Atom("
", ctr_pos) 486 | if onAtom: # the nearest atom 487 | dists = [ctr.get_dist(i) for i in self.atoms] 488 | return self.atoms[dists.index(min(dists))] 489 | else: # the center 490 | return ctr 491 | 492 | 493 | def get_T_vs_centerdist(self, n=100): 494 | ctr = self.get_center() 495 | t = [(ctr.get_dist(i), i.get_temperature()) for i in self.atoms] 496 | print "Average temperature:", sum(i[1] for i in t) / len(t), 497 | print "max.", max(i[1] for i in t) 498 | t.sort(lambda x,y: cmp(x[0], y[0])) 499 | xy = [] 500 | for i in range(len(t)//n): 501 | g = t[i*n: (i+1)*n] 502 | x = sum(j[0] for j in g) / n 503 | y = sum(j[1] for j in g) / n 504 | xy.append((x, y)) 505 | return xy 506 | 507 | 508 | def write_T_vs_centerdist(self, filename, n_group=100): 509 | print "Writing radial distribution of temperature " \ 510 | "(atoms grouped %s) to file %s" % (n_group, filename) 511 | ofile = file(filename, 'w') 512 | for i in self.get_T_vs_centerdist(n_group): 513 | print >>ofile, i[0], i[1] 514 | 515 | def set_pbc_with_vacuum(self, width): 516 | pbc = numpy.zeros((3,3)) 517 | for i in range(3): 518 | k = lambda atom: atom.pos[i] 519 | pbc[i][i] = k(max(self.atoms, key=k)) - k(min(self.atoms, key=k)) \ 520 | + width 521 | self.pbc = pbc 522 | 523 | 524 | def count_species(self): 525 | counts = {} 526 | for i in self.atoms: 527 | if i.name in counts: 528 | counts[i.name] += 1 529 | else: 530 | counts[i.name] = 1 531 | return counts 532 | 533 | 534 | -------------------------------------------------------------------------------- /monocryst.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # this file is part of gosam (generator of simple atomistic models) 3 | # Licence: GNU General Public License version 2 4 | 5 | import sys 6 | from math import sin, cos, sqrt, degrees, radians, asin 7 | from copy import deepcopy 8 | from optparse import OptionParser 9 | from numpy import dot, array, identity, minimum, maximum 10 | 11 | import graingen 12 | import latt 13 | import mdprim 14 | import rotmat 15 | import utils 16 | 17 | def get_diamond_node_pos(): 18 | node_pos = [] 19 | for i in graingen.fcc_nodes: 20 | node_pos.append(i) 21 | node_pos.append((i[0]+0.25, i[1]+0.25, i[2]+0.25)) 22 | return node_pos 23 | 24 | 25 | def make_lattice(cell, node_pos, node_atoms): 26 | nodes = [latt.Node(i, node_atoms) for i in node_pos] 27 | lattice = latt.CrystalLattice(cell, nodes) 28 | return lattice 29 | 30 | 31 | def make_simple_cubic_lattice(symbol, a): 32 | cell = latt.CubicUnitCell(a) 33 | node = latt.Node((0.0, 0.0, 0.0), [(symbol, 0.0, 0.0, 0.0)]) 34 | return latt.CrystalLattice(cell, [node]) 35 | 36 | def make_fcc_lattice(symbol, a): 37 | cell = latt.CubicUnitCell(a) 38 | node_pos = graingen.fcc_nodes[:] 39 | node_atoms = [ (symbol, 0.0, 0.0, 0.0) ] 40 | return make_lattice(cell, node_pos, node_atoms) 41 | 42 | def make_bcc_lattice(symbol, a): 43 | cell = latt.CubicUnitCell(a) 44 | node_pos = graingen.bcc_nodes[:] 45 | node_atoms = [ (symbol, 0.0, 0.0, 0.0) ] 46 | return make_lattice(cell, node_pos, node_atoms) 47 | 48 | 49 | def make_zincblende_lattice(symbols, a): 50 | assert len(symbols) == 2 51 | cell = latt.CubicUnitCell(a) 52 | # nodes in unit cell (as fraction of unit cell parameters) 53 | node_pos = graingen.fcc_nodes[:] 54 | # atoms in node (as fraction of unit cell parameters) 55 | node_atoms = [ 56 | (symbols[0], 0.0, 0.0, 0.0), 57 | (symbols[1], 0.25,0.25,0.25), 58 | ] 59 | return make_lattice(cell, node_pos, node_atoms) 60 | 61 | 62 | def make_sic_polytype_lattice(symbols, a, h, polytype): 63 | """a: hexagonal lattice parameter 64 | h: distance between atomic layers 65 | polytype: a string like "AB" or "ABCACB", 66 | """ 67 | assert len(symbols) == 2 68 | # in the case of 3C (ABC) polytype with cubic lattice a_c: 69 | # a = a_c / sqrt(2); h = a_c / sqrt(3) 70 | cell, nodes = latt.generate_polytype(a=a, h=h, polytype=polytype) 71 | #atoms in node (as fraction of (a,a,h) parameters) 72 | node_atoms = [ 73 | (symbols[0], 0.0, 0.0, 0.0), 74 | (symbols[1], 0.0, 0.0, 0.75 / len(polytype)), 75 | ] 76 | return make_lattice(cell, nodes, node_atoms) 77 | 78 | def make_diamond_lattice(symbol, a): 79 | cell = latt.CubicUnitCell(a) 80 | node_pos = get_diamond_node_pos() 81 | node_atoms = [ (symbol, 0.0, 0.0, 0.0) ] 82 | return make_lattice(cell, node_pos, node_atoms) 83 | 84 | def make_nacl_lattice(symbols, a): 85 | assert len(symbols) == 2 86 | cell = latt.CubicUnitCell(a) 87 | node_pos = graingen.fcc_nodes[:] 88 | node_atoms = [ 89 | (symbols[0], 0.0, 0.0, 0.0), 90 | (symbols[1], 0.5, 0.5, 0.5), 91 | ] 92 | return make_lattice(cell, node_pos, node_atoms) 93 | 94 | # body centered tetragonal 95 | def make_bct_lattice(symbol, a, c): 96 | cell = latt.TetragonalUnitCell(a, c) 97 | node_pos = graingen.bcc_nodes[:] 98 | node_atoms = [ (symbol, 0.0, 0.0, 0.0) ] 99 | return make_lattice(cell, node_pos, node_atoms) 100 | 101 | def make_lattice_from_cif(filename): 102 | try: 103 | import gemmi 104 | except ImportError: 105 | sys.exit('Gemmi is needed to read cif files. Try "pip install gemmi".') 106 | st = gemmi.read_atomic_structure(filename) 107 | cell = latt.UnitCell(st.cell.a, st.cell.b, st.cell.c, 108 | st.cell.alpha, st.cell.beta, st.cell.gamma, system='') 109 | nodes = [] 110 | for site in st.get_all_unit_cell_sites(): 111 | pos = (site.fract.x, site.fract.y, site.fract.z) 112 | atom = latt.AtomInNode(site.type_symbol) 113 | nodes.append(latt.Node(pos, [atom])) 114 | return latt.CrystalLattice(cell, nodes) 115 | 116 | class OrthorhombicPbcModel(graingen.FreshModel): 117 | def __init__(self, lattice, dimensions, title): 118 | pbc = identity(3) * dimensions 119 | graingen.FreshModel.__init__(self, lattice, pbc, title=title) 120 | 121 | def get_vertices(self): 122 | return [(x, y, z) for x in self._min_max[0] 123 | for y in self._min_max[1] 124 | for z in self._min_max[2]] 125 | 126 | def _do_gen_atoms(self, vmin, vmax): 127 | self._min_max = zip(vmin, vmax) 128 | self.compute_scope() 129 | print self.get_scope_info() 130 | for node, abs_pos in self.get_all_nodes(): 131 | for atom in node.atoms_in_node: 132 | xyz = dot(abs_pos+atom.pos, self.unit_cell.M_1) 133 | if (vmin < xyz).all() and (xyz <= vmax).all(): 134 | self.atoms.append(mdprim.Atom(atom.name, xyz)) 135 | 136 | 137 | class RotatedMonocrystal(OrthorhombicPbcModel): 138 | """Monocrystal rotated using rot_mat rotation matrix 139 | """ 140 | def __init__(self, lattice, dim, rot_mat, title=None): 141 | self.lattice = lattice 142 | self.dim = array(dim, dtype=float) 143 | self.rot_mat = rot_mat 144 | if title is None: 145 | title = "generated by gosam.monocryst" 146 | OrthorhombicPbcModel.__init__(self, lattice, self.dim, title=title) 147 | 148 | def generate_atoms(self, upper=None, z_margin=0.): 149 | """upper and z_margin are used for building bicrystal 150 | """ 151 | self.atoms = [] 152 | vmin, vmax = self.get_box_to_fill(self.dim, upper, z_margin) 153 | if self.rot_mat is not None: 154 | self.unit_cell.rotate(self.rot_mat) 155 | self._do_gen_atoms(vmin, vmax) 156 | if upper is None: 157 | print "Number of atoms in monocrystal: %i" % len(self.atoms) 158 | return self.atoms 159 | 160 | def get_box_to_fill(self, dim, upper, z_margin): 161 | # make it a bit asymmetric, to avoid problems with PBC 162 | eps = 0.001 163 | vmin = -self.dim/2. + eps 164 | vmax = self.dim/2. + eps 165 | assert upper in (True, False, None) 166 | if upper is True: 167 | vmin[2] = eps 168 | if z_margin: 169 | vmax[2] -= z_margin / 2 170 | elif upper is False: 171 | vmax[2] = eps 172 | if z_margin: 173 | vmin[2] += z_margin / 2 174 | return vmin, vmax 175 | 176 | 177 | 178 | # primitive adjusting of PBC box for [010] rotation 179 | def test_rotmono_adjust(): 180 | lattice = make_zincblende_lattice(symbols=("Si","C"), a=4.36) 181 | a = lattice.unit_cell.a 182 | dimensions = [10*a, 10*a, 10*a] 183 | theta = radians(float(sys.argv[1])) 184 | 185 | d = dimensions[0] 186 | n_ = d * sin(theta) / a 187 | m_ = d / (a * cos(theta)) 188 | n = round(n_) 189 | m = round(m_) 190 | new_th = 0.5 * asin(2.*n/m) 191 | new_d = m * a * cos(new_th) 192 | print "theta =", degrees(new_th), " d =", new_d 193 | dimensions[0] = new_d 194 | dimensions[1] = round(dimensions[1] / a) * a 195 | theta = new_th 196 | 197 | rot_mat = rotmat.rodrigues((0,1,0), theta, verbose=False) 198 | config = RotatedMonocrystal(lattice, dimensions, rot_mat) 199 | config.generate_atoms() 200 | config.export_atoms("monotest.cfg", format="atomeye") 201 | 202 | 203 | def mono(lattice, nx, ny, nz): 204 | min_dim = lattice.unit_cell.get_orthorhombic_supercell() 205 | dim = [rotmat.round_to_multiplicity(min_dim[0], 10*nx), 206 | rotmat.round_to_multiplicity(min_dim[1], 10*ny), 207 | rotmat.round_to_multiplicity(min_dim[2], 10*nz)] 208 | print "dimensions [A]:", dim[0], dim[1], dim[2] 209 | config = RotatedMonocrystal(deepcopy(lattice), dim, rot_mat=None, 210 | title=utils.get_command_line()) 211 | config.generate_atoms() 212 | return config 213 | 214 | # To change lattice parameters or atomic symbols just modify this function. 215 | def get_named_lattice(name): 216 | if name.endswith('.cif'): 217 | return make_lattice_from_cif(name) 218 | name = name.lower() 219 | if name == "cu": # Cu (fcc, A1) 220 | lattice = make_fcc_lattice(symbol="Cu", a=3.615) 221 | elif name == "fe": # Fe (bcc, A2) 222 | lattice = make_bcc_lattice(symbol="Fe", a=2.87) 223 | elif name == "po": # Polonium (sc, Ah) 224 | lattice = make_simple_cubic_lattice(symbol="Po", a=3.35) 225 | elif name == "nacl": # NaCl (B1) 226 | lattice = make_nacl_lattice(symbols=("Na","Cl"), a=5.64) 227 | elif name == "sic": # SiC (zinc blende structure, B3) 228 | # 4.3210368 A - value for Tersoff (1989) MD potential 229 | # 4.36 A - real value 230 | lattice = make_zincblende_lattice(symbols=("Si","C"), a=4.3210368) 231 | elif name == "si": # Si (diamond structure, A4) 232 | lattice = make_diamond_lattice(symbol="Si", a=5.43) 233 | elif name == "diamond": # C (diamond structure, A4) 234 | lattice = make_diamond_lattice(symbol="C", a=3.567) 235 | elif name.startswith("sic:"): # SiC-like (binary, tetrahedral) polytype 236 | lattice = make_sic_polytype_lattice(symbols=("Si","C"), a=3.073, h=2.52, 237 | polytype=name[4:]) 238 | elif name == "sn": # Sn, body-centered tetragonal 239 | lattice = make_bct_lattice(symbol="Sn", a=5.83, c=3.18) 240 | else: 241 | raise ValueError("Unknown lattice: %s" % name) 242 | return lattice 243 | 244 | 245 | usage = """monocryst.py [options] crystal nx ny nz output_filename 246 | where nx, ny, nz are minimal dimensions in nm, 247 | crystal is a one of predefined lattice types (case insensitive): 248 | Cu, Fe, NaCl, Si, diamond, SiC, SiC:ABABC. 249 | In the last case, any polytype can be given after colon.""" 250 | 251 | def main(): 252 | parser = OptionParser(usage) 253 | parser.add_option("--margin", type="float", 254 | help="increase PBC by given margin (of vacuum)") 255 | parser.add_option("--center-zero", action="store_true", 256 | help="shift center to (0, 0, 0)") 257 | (options, args) = parser.parse_args(sys.argv) 258 | if len(args) == 1: 259 | parser.print_help() 260 | sys.exit() 261 | if len(args) != 6: 262 | parser.error("5 arguments are required, not %d" % (len(args) - 1)) 263 | 264 | lattice = get_named_lattice(args[1]) 265 | nx, ny, nz = float(args[2]), float(args[3]), float(args[4]) 266 | config = mono(lattice, nx, ny, nz) 267 | if options.center_zero: 268 | print "centering..." 269 | m = config.atoms[0].pos.copy() 270 | M = config.atoms[0].pos.copy() 271 | for atom in config.atoms: 272 | m = minimum(m, atom.pos) 273 | M = maximum(M, atom.pos) 274 | ctr = (m + M) / 2. 275 | for atom in config.atoms: 276 | atom.pos -= ctr 277 | if options.margin is not None: 278 | margin = options.margin * 10 279 | print "adding margins %g A" % margin 280 | for i in range(3): 281 | config.pbc[i][i] += margin 282 | config.export_atoms(args[5]) 283 | 284 | 285 | 286 | if __name__ == '__main__': 287 | main() 288 | 289 | 290 | -------------------------------------------------------------------------------- /pse.py: -------------------------------------------------------------------------------- 1 | # this file is part of gosam (generator of simple atomistic models) 2 | # Licence (of this file): public domain 3 | 4 | """\ 5 | Periodic System of Elements (Z - symbol - name - mass) 6 | 7 | Based on: 8 | CRC Handbook of Chemistry & Physics, 63rd edition, 1982-1983 9 | CRC Handbook of Chemistry & Physics, 70th edition, 1989-1990 10 | 11 | You may find the same data in cctbx (C++) and atominfo (C). 12 | """ 13 | 14 | 15 | 16 | class Pse: 17 | def __init__(self, Z, Symbol, Name, Mass): 18 | self.Z = Z 19 | self.Symbol = Symbol 20 | self.Name = Name 21 | self.Mass = Mass 22 | def __str__(self): 23 | return "Z=%s %s (%s) mass=%s" % (self.Z, self.Name, 24 | self.Symbol, self.Mass) 25 | 26 | 27 | pse = [ 28 | Pse( 1, "H", "hydrogen", 1.008 ), 29 | Pse( 1, "D", "deuterium", 2.000 ), 30 | Pse( 2, "He", "helium", 4.003 ), 31 | Pse( 3, "Li", "lithium", 6.941 ), 32 | Pse( 4, "Be", "beryllium", 9.012 ), 33 | Pse( 5, "B", "boron", 10.811 ), 34 | Pse( 6, "C", "carbon", 12.011 ), 35 | Pse( 7, "N", "nitrogen", 14.007 ), 36 | Pse( 8, "O", "oxygen", 15.999 ), 37 | Pse( 9, "F", "fluorine", 18.998 ), 38 | Pse( 10, "Ne", "neon", 20.180 ), 39 | Pse( 11, "Na", "sodium", 22.990 ), 40 | Pse( 12, "Mg", "magnesium", 24.305 ), 41 | Pse( 13, "Al", "aluminium", 26.982 ), 42 | Pse( 14, "Si", "silicon", 28.086 ), 43 | Pse( 15, "P", "phosphorus", 30.974 ), 44 | Pse( 16, "S", "sulphur", 32.066 ), 45 | Pse( 17, "Cl", "chlorine", 35.452 ), 46 | Pse( 18, "Ar", "argon", 39.948 ), 47 | Pse( 19, "K", "potassium", 39.098 ), 48 | Pse( 20, "Ca", "calcium", 40.078 ), 49 | Pse( 21, "Sc", "scandium", 44.956 ), 50 | Pse( 22, "Ti", "titanium", 47.883 ), 51 | Pse( 23, "V", "vanadium", 50.941 ), 52 | Pse( 24, "Cr", "chromium", 51.996 ), 53 | Pse( 25, "Mn", "manganese", 54.938 ), 54 | Pse( 26, "Fe", "iron", 55.847 ), 55 | Pse( 27, "Co", "cobalt", 58.933 ), 56 | Pse( 28, "Ni", "nickel", 58.691 ), 57 | Pse( 29, "Cu", "copper", 63.546 ), 58 | Pse( 30, "Zn", "zinc", 65.392 ), 59 | Pse( 31, "Ga", "gallium", 69.723 ), 60 | Pse( 32, "Ge", "germanium", 72.612 ), 61 | Pse( 33, "As", "arsenic", 74.922 ), 62 | Pse( 34, "Se", "selenium", 78.963 ), 63 | Pse( 35, "Br", "bromine", 79.904 ), 64 | Pse( 36, "Kr", "krypton", 83.801 ), 65 | Pse( 37, "Rb", "rubidium", 85.468 ), 66 | Pse( 38, "Sr", "strontium", 87.621 ), 67 | Pse( 39, "Y", "yttrium", 88.906 ), 68 | Pse( 40, "Zr", "zirconium", 91.224 ), 69 | Pse( 41, "Nb", "niobium", 92.906 ), 70 | Pse( 42, "Mo", "molybdenum", 95.941 ), 71 | Pse( 43, "Tc", "technetium", 98.000 ), 72 | Pse( 44, "Ru", "ruthenium", 101.072 ), 73 | Pse( 45, "Rh", "rhodium", 102.905 ), 74 | Pse( 46, "Pd", "palladium", 106.421 ), 75 | Pse( 47, "Ag", "silver", 107.868 ), 76 | Pse( 48, "Cd", "cadmium", 112.411 ), 77 | Pse( 49, "In", "indium", 114.821 ), 78 | Pse( 50, "Sn", "tin", 118.710 ), 79 | Pse( 51, "Sb", "antimony", 121.753 ), 80 | Pse( 52, "Te", "tellurium", 127.603 ), 81 | Pse( 53, "I", "iodine", 126.904 ), 82 | Pse( 54, "Xe", "xenon", 131.292 ), 83 | Pse( 55, "Cs", "caesium", 132.905 ), 84 | Pse( 56, "Ba", "barium", 137.327 ), 85 | Pse( 57, "La", "lanthanum", 138.906 ), 86 | Pse( 58, "Ce", "cerium", 140.115 ), 87 | Pse( 59, "Pr", "praseodymium", 140.908 ), 88 | Pse( 60, "Nd", "neodymium", 144.243 ), 89 | Pse( 61, "Pm", "promethium", 145.000 ), 90 | Pse( 62, "Sm", "samarium", 150.363 ), 91 | Pse( 63, "Eu", "europium", 151.965 ), 92 | Pse( 64, "Gd", "gadolinium", 157.253 ), 93 | Pse( 65, "Tb", "terbium", 158.925 ), 94 | Pse( 66, "Dy", "dysprosium", 162.503 ), 95 | Pse( 67, "Ho", "holmium", 164.930 ), 96 | Pse( 68, "Er", "erbium", 167.263 ), 97 | Pse( 69, "Tm", "thulium", 168.934 ), 98 | Pse( 70, "Yb", "ytterbium", 173.043 ), 99 | Pse( 71, "Lu", "lutetium", 174.967 ), 100 | Pse( 72, "Hf", "hafnium", 178.492 ), 101 | Pse( 73, "Ta", "tantalum", 180.948 ), 102 | Pse( 74, "W", "tungsten", 183.853 ), 103 | Pse( 75, "Re", "rhenium", 186.207 ), 104 | Pse( 76, "Os", "osmium", 190.210 ), 105 | Pse( 77, "Ir", "iridium", 192.223 ), 106 | Pse( 78, "Pt", "platinum", 195.083 ), 107 | Pse( 79, "Au", "gold", 196.967 ), 108 | Pse( 80, "Hg", "mercury", 200.593 ), 109 | Pse( 81, "Tl", "thallium", 204.383 ), 110 | Pse( 82, "Pb", "lead", 207.210 ), 111 | Pse( 83, "Bi", "bismuth", 208.980 ), 112 | Pse( 84, "Po", "polonium", 209.000 ), 113 | Pse( 85, "At", "astatine", 210.000 ), 114 | Pse( 86, "Rn", "radon", 222.000 ), 115 | Pse( 87, "Fr", "francium", 223.000 ), 116 | Pse( 88, "Ra", "radium", 226.025 ), 117 | Pse( 89, "Ac", "actinium", 227.028 ), 118 | Pse( 90, "Th", "thorium", 232.038 ), 119 | Pse( 91, "Pa", "protactinium", 231.035 ), 120 | Pse( 92, "U", "uranium", 238.028 ), 121 | Pse( 93, "Np", "neptunium", 237.048 ), 122 | Pse( 94, "Pu", "plutonium", 244.000 ), 123 | Pse( 95, "Am", "americium", 243.000 ), 124 | Pse( 96, "Cm", "curium", 247.000 ), 125 | Pse( 97, "Bk", "berkelium", 247.000 ), 126 | Pse( 98, "Cf", "californium", 251.000 ), 127 | Pse( 99, "Es", "einsteinium", 254.000 ), 128 | Pse( 100, "Fm", "fermium", 257.000 ), 129 | Pse( 101, "Md", "mendelevium", 258.000 ), 130 | Pse( 102, "No", "nobelium", 259.000 ), 131 | Pse( 103, "Lr", "lawrencium", 260.000 ), 132 | ] 133 | 134 | 135 | # dictionary for easier access using symbols 136 | pse_dict = {} 137 | for i in pse: 138 | pse_dict[i.Symbol] = i 139 | 140 | def get_atom_mass(symbol): 141 | if symbol in pse_dict: 142 | return pse_dict[symbol].Mass 143 | else: 144 | return 0 145 | 146 | 147 | -------------------------------------------------------------------------------- /rotmat.py: -------------------------------------------------------------------------------- 1 | # this file is part of gosam (generator of simple atomistic models) 2 | # Licence: GNU General Public License version 2 3 | """\ 4 | Mathematical and matrix-related utilities. 5 | Most notably Rodrigues rotation formula. 6 | """ 7 | 8 | from math import sin, cos, acos, sqrt, degrees 9 | from numpy import array, dot, inner, identity, linalg, sign 10 | 11 | def rodrigues(a, angle, verbose=False): 12 | "use Rodrigues' rotation formula to get rotation matrix" 13 | a = array(a, dtype=float) 14 | a /= sqrt(inner(a, a)) # make unit vector 15 | #assert abs(sin_angle - sin(acos(cos_angle))) < 1e-6 16 | if verbose: 17 | print "rotation angle:", degrees(angle) 18 | print "rotation axis:", a 19 | omega = array([[ 0., -a[2], a[1]], 20 | [ a[2], 0., -a[0]], 21 | [-a[1], a[0], 0.]]) 22 | rm = (identity(3) + omega * sin(angle) 23 | + dot(omega, omega) * (1 - cos(angle))) 24 | if verbose: 25 | print "rotation matrix:", rm 26 | return rm 27 | 28 | 29 | def print_matrix(text, M): 30 | print "%s (det=%s):\n%s" % (text, linalg.det(M), M) 31 | 32 | 33 | def round_to_multiplicity(m, val): 34 | "round val to the nearest multiplicity of m, but avoid zero" 35 | return (round(float(val) / m) or 1) * m 36 | 37 | def is_diagonal(m): 38 | m = array(m) 39 | assert m.shape == (3,3) 40 | return (sign(m) == identity(3)).all() 41 | 42 | 43 | class StdDev: 44 | def __init__(self): 45 | self.n = 0 46 | self.mean = 0. 47 | self.S = 0. 48 | 49 | def __str__(self): 50 | return "%s +- %s" % (self.mean, self.get_stddev()) 51 | 52 | def get_variance(self): 53 | return self.S / (self.n - 1) 54 | 55 | def get_stddev(self): 56 | return sqrt(self.get_variance()) 57 | 58 | def add_x(self, x): 59 | self.n += 1 60 | delta = x - self.mean 61 | self.mean += delta / self.n 62 | self.S += delta * (x - self.mean) 63 | 64 | 65 | def pt_in_box(pt, max=array([1.,1.,1.]), min=array([0., 0., 0.])): 66 | """True if min <= pt < max, False otherwise""" 67 | pt = array(pt) 68 | return all(pt < max) and all(min <= pt) 69 | 70 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | # this file is part of gosam (generator of simple atomistic models) 2 | # Licence: GNU General Public License version 2 3 | 4 | "misc utilities" 5 | 6 | import sys 7 | 8 | def get_command_line(): 9 | "return command used to call the program (from sys.argv)" 10 | def quote(s): 11 | need_quote = "|&;<>()$`\\' \t\n*?[#~=%" 12 | t = s.replace('"', '\\"') 13 | if set(need_quote) & set(s): 14 | return '"%s"' % t 15 | else: 16 | return t 17 | return " ".join(quote(i) for i in sys.argv) 18 | 19 | 20 | --------------------------------------------------------------------------------