Statistiques
| Branche: | Tag: | Révision :

dockonsurf / modules / screening.py @ 1e9e784d

Historique | Voir | Annoter | Télécharger (23,51 ko)

1
import logging
2
import numpy as np
3
import ase
4

    
5
logger = logging.getLogger('DockOnSurf')
6

    
7

    
8
def assign_prop(atoms: ase.Atoms, prop_name: str, prop_val):  # TODO Needed?
9
    atoms.info[prop_name] = prop_val
10

    
11

    
12
def select_confs(orig_conf_list: list, calc_dirs: list, magns: list, num_sel: int, code: str):
13
    """Takes a list ase.Atoms and selects the most different magnitude-wise.
14

15
    Given a list of ase.Atoms objects and a list of magnitudes, it selects a
16
    number of the most different conformers according to every magnitude
17
    specified.
18
     
19
    @param orig_conf_list: list of ase.Atoms objects to select among.
20
    @param calc_dirs: List of directories where to read the energies from.
21
    @param magns: list of str with the names of the magnitudes to use for the
22
        conformer selection. Supported magnitudes: 'energy', 'moi'.
23
    @param num_sel: number of conformers to select for every of the magnitudes.
24
    @param code: The code that generated the magnitude information.
25
         Supported codes: See formats.py
26
    @return: list of the selected ase.Atoms objects.
27
    """
28
    from copy import deepcopy
29
    from modules.formats import collect_energies
30

    
31
    conf_list = deepcopy(orig_conf_list)
32
    selected_ids = []
33
    if num_sel >= len(conf_list):
34
        logger.warning('Number of conformers per magnitude is equal or larger '
35
                       'than the total number of conformers. Using all '
36
                       f'available conformers: {len(conf_list)}.')
37
        return conf_list
38

    
39
    # Read properties
40
    if 'energy' in magns:
41
        conf_enrgs = collect_energies(calc_dirs, code, 'isolated')
42
    if 'moi' in magns:
43
        mois = np.array([conf.get_moments_of_inertia() for conf in conf_list])
44

    
45
    # Assign values
46
    for i, conf in enumerate(conf_list):
47
        assign_prop(conf, 'idx', i)
48
        if 'energy' in magns:
49
            assign_prop(conf, 'energy', conf_enrgs[i])
50
        if 'moi' in magns:
51
            assign_prop(conf, 'moi', mois[i, 2])
52

    
53
    # pick ids
54
    for magn in magns:
55
        sorted_list = sorted(conf_list, key=lambda conf: abs(conf.info[magn]))
56
        if sorted_list[-1].info['idx'] not in selected_ids:
57
            selected_ids.append(sorted_list[-1].info['idx'])
58
        if num_sel > 1:
59
            for i in range(0, len(sorted_list) - 1,
60
                           len(conf_list) // (num_sel - 1)):
61
                if sorted_list[i].info['idx'] not in selected_ids:
62
                    selected_ids.append(sorted_list[i].info['idx'])
63

    
64
    logger.info(f'Selected {len(selected_ids)} conformers for adsorption.')
65
    return [conf_list[idx] for idx in selected_ids]
66

    
67

    
68
def get_vect_angle(v1, v2, degrees=False):
69
    """Computes the angle between two vectors.
70

71
    @param v1: The first vector.
72
    @param v2: The second vector.
73
    @param degrees: Whether the result should be in radians (True) or in
74
        degrees (False).
75
    @return: The angle in radians if degrees = False, or in degrees if
76
        degrees =True
77
    """
78
    v1_u = v1 / np.linalg.norm(v1)
79
    v2_u = v2 / np.linalg.norm(v2)
80
    angle = np.arccos(np.clip(np.dot(v1_u, v2_u), -1.0, 1.0))
81
    return angle if not degrees else angle * 180 / np.pi
82

    
83

    
84
def vect_avg(vects):
85
    """Computes the element-wise mean of a set of vectors.
86

87
    @param vects: list of lists-like: containing the vectors (num_vectors,
88
        length_vector).
89
    @return: vector average computed doing the element-wise mean.
90
    """
91
    from utilities import try_command
92
    err = "vect_avg parameter vects must be a list-like, able to be converted" \
93
          " np.array"
94
    array = try_command(np.array, [(ValueError, err)], vects)
95
    if len(array.shape) == 1:
96
        return array
97
    else:
98
        num_vects = array.shape[1]
99
        return np.array([np.average(array[:, i]) for i in range(num_vects)])
100

    
101

    
102
def get_atom_coords(atoms: ase.Atoms, ctrs_list=None):
103
    """Gets the coordinates of the specified indices from a ase.Atoms object.
104

105
    Given an ase.Atoms object and a list of atom indices specified in ctrs_list
106
    it gets the coordinates of the specified atoms. If the element in the
107
    ctrs_list is not an index but yet a list of indices, it computes the
108
    element-wise mean of the coordinates of the atoms specified in the inner
109
    list.
110
    @param atoms: ase.Atoms object for which to obtain the coordinates of.
111
    @param ctrs_list: list of (indices/list of indices) of the atoms for which
112
                      the coordinates should be extracted.
113
    @return: np.ndarray of atomic coordinates.
114
    """
115
    coords = []
116
    err = "'ctrs_list' argument must be an integer, a list of integers or a " \
117
          "list of lists of integers. Every integer must be in the range " \
118
          "[0, num_atoms)"
119
    if ctrs_list is None:
120
        ctrs_list = range(len(atoms))
121
    elif isinstance(ctrs_list, int):
122
        if ctrs_list not in range(len(atoms)):
123
            logger.error(err)
124
            raise ValueError(err)
125
        return atoms[ctrs_list].position
126
    for elem in ctrs_list:
127
        if isinstance(elem, list):
128
            coords.append(vect_avg([atoms[c].position for c in elem]))
129
        elif isinstance(elem, int):
130
            coords.append(atoms[elem].position)
131
        else:
132

    
133
            logger.error(err)
134
            raise ValueError
135
    return np.array(coords)
136

    
137

    
138
def add_adsorbate(slab, adsorbate, site_coord, ctr_coord, height, offset=None,
139
                  norm_vect=(0, 0, 1)):
140
    """Add an adsorbate to a surface.
141

142
    This function extends the functionality of ase.build.add_adsorbate
143
    (https://wiki.fysik.dtu.dk/ase/ase/build/surface.html#ase.build.add_adsorbate)
144
    by enabling to change the z coordinate and the axis perpendicular to the
145
    surface.
146
    @param slab: ase.Atoms object containing the coordinates of the surface
147
    @param adsorbate: ase.Atoms object containing the coordinates of the
148
        adsorbate.
149
    @param site_coord: The coordinates of the adsorption site on the surface.
150
    @param ctr_coord: The coordinates of the adsorption center in the molecule.
151
    @param height: The height above the surface where to adsorb.
152
    @param offset: Offsets the adsorbate by a number of unit cells. Mostly
153
        useful when adding more than one adsorbate.
154
    @param norm_vect: The vector perpendicular to the surface.
155
    """
156
    from copy import deepcopy
157
    info = slab.info.get('adsorbate_info', {})
158
    pos = np.array([0.0, 0.0, 0.0])  # part of absolute coordinates
159
    spos = np.array([0.0, 0.0, 0.0])  # part relative to unit cell
160
    norm_vect_u = np.array(norm_vect) / np.linalg.norm(norm_vect)
161
    if offset is not None:
162
        spos += np.asarray(offset, float)
163
    if isinstance(site_coord, str):
164
        # A site-name:
165
        if 'sites' not in info:
166
            raise TypeError('If the atoms are not made by an ase.build '
167
                            'function, position cannot be a name.')
168
        if site_coord not in info['sites']:
169
            raise TypeError('Adsorption site %s not supported.' % site_coord)
170
        spos += info['sites'][site_coord]
171
    else:
172
        pos += site_coord
173
    if 'cell' in info:
174
        cell = info['cell']
175
    else:
176
        cell = slab.get_cell()
177
    pos += np.dot(spos, cell)
178
    # Convert the adsorbate to an Atoms object
179
    if isinstance(adsorbate, ase.Atoms):
180
        ads = deepcopy(adsorbate)
181
    elif isinstance(adsorbate, ase.Atom):
182
        ads = ase.Atoms([adsorbate])
183
    else:
184
        # Assume it is a string representing a single Atom
185
        ads = ase.Atoms([ase.Atom(adsorbate)])
186
    pos += height * norm_vect_u
187
    # Move adsorbate into position
188
    ads.translate(pos - ctr_coord)
189
    # Attach the adsorbate
190
    slab.extend(ads)
191

    
192

    
193
def check_collision(slab_molec, slab_num_atoms, min_height, vect, nn_slab=0,
194
                    nn_molec=0, coll_coeff=0.9):
195
    """Checks whether a slab and a molecule collide or not.
196

197
    @param slab_molec: The system of adsorbate-slab for which to detect if there
198
        are collisions.
199
    @param nn_slab: Number of neigbors in the surface.
200
    @param nn_molec: Number of neighbors in the molecule.
201
    @param coll_coeff: The coefficient that multiplies the covalent radius of
202
        atoms resulting in a distance that two atoms being closer to that is
203
        considered as atomic collision.
204
    @param slab_num_atoms: Number of atoms of the bare slab.
205
    @param min_height: The minimum height atoms can have to not be considered as
206
        colliding.
207
    @param vect: The vector perpendicular to the slab.
208
    @return: bool, whether the surface and the molecule collide.
209
    """
210
    from ase.neighborlist import natural_cutoffs, neighbor_list
211
    if min_height is not False:
212
        cart_axes = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0],
213
                     [-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, -1.0]]
214
        if vect.tolist() in cart_axes:
215
            for atom in slab_molec[slab_num_atoms:]:
216
                for i, coord in enumerate(vect):
217
                    if coord == 0:
218
                        continue
219
                    if atom.position[i] * coord < min_height:
220
                        return True
221
            return False
222
    else:
223
        slab_molec_cutoffs = natural_cutoffs(slab_molec, mult=coll_coeff)
224
        slab_molec_nghbs = len(
225
            neighbor_list("i", slab_molec, slab_molec_cutoffs))
226
        if slab_molec_nghbs > nn_slab + nn_molec:
227
            return True
228
        else:
229
            return False
230

    
231

    
232
def dissociate_h(slab_molec_orig, h_idx, num_atoms_slab, neigh_cutoff=1):
233
    # TODO rethink
234
    """Tries to dissociate a H from the molecule and adsorbs it on the slab.
235

236
    Tries to dissociate a H atom from the molecule and adsorb in on top of the
237
    surface if the distance is shorter than two times the neigh_cutoff value.
238
    @param slab_molec_orig: The ase.Atoms object of the system adsorbate-slab.
239
    @param h_idx: The index of the hydrogen atom to carry out adsorption of.
240
    @param num_atoms_slab: The number of atoms of the slab without adsorbate.
241
    @param neigh_cutoff: half the maximum distance between the surface and the
242
        H for it to carry out dissociation.
243
    @return: An ase.Atoms object of the system adsorbate-surface with H
244
    """
245
    from copy import deepcopy
246
    from ase.neighborlist import NeighborList
247
    slab_molec = deepcopy(slab_molec_orig)
248
    cutoffs = len(slab_molec) * [neigh_cutoff]
249
    nl = NeighborList(cutoffs, self_interaction=False, bothways=True)
250
    nl.update(slab_molec)
251
    surf_h_vect = np.array([np.infty] * 3)
252
    for neigh_idx in nl.get_neighbors(h_idx)[0]:
253
        if neigh_idx < num_atoms_slab:
254
            dist = np.linalg.norm(slab_molec[neigh_idx].position -
255
                                  slab_molec[h_idx].position)
256
            if dist < np.linalg.norm(surf_h_vect):
257
                surf_h_vect = slab_molec[neigh_idx].position \
258
                              - slab_molec[h_idx].position
259
    if np.linalg.norm(surf_h_vect) != np.infty:
260
        trans_vect = surf_h_vect - surf_h_vect / np.linalg.norm(surf_h_vect)
261
        slab_molec[h_idx].position = slab_molec[h_idx].position + trans_vect
262
        return slab_molec
263

    
264

    
265
def dissociation(slab_molec, disso_atoms, num_atoms_slab):
266
    # TODO rethink
267
    # TODO multiple dissociation
268
    """Decides which H atoms to dissociate according to a list of atoms.
269

270
    Given a list of chemical symbols or atom indices it checks for every atom
271
    or any of its neighbor if it's a H and calls dissociate_h to try to carry
272
    out dissociation of that H. For atom indices, it checks both whether
273
    the atom index or its neighbors are H, for chemical symbols, it only checks
274
    if there is a neighbor H.
275
    @param slab_molec: The ase.Atoms object of the system adsorbate-slab.
276
    @param disso_atoms: The indices or chemical symbols of the atoms
277
    @param num_atoms_slab:
278
    @return:
279
    """
280
    from ase.neighborlist import natural_cutoffs, NeighborList
281
    molec = slab_molec[num_atoms_slab:]
282
    cutoffs = natural_cutoffs(molec)
283
    nl = NeighborList(cutoffs, self_interaction=False, bothways=True)
284
    nl.update(molec)
285
    disso_structs = []
286
    for el in disso_atoms:
287
        if isinstance(el, int):
288
            if molec[el].symbol == 'H':
289
                disso_struct = dissociate_h(slab_molec, el + num_atoms_slab,
290
                                            num_atoms_slab)
291
                if disso_struct is not None:
292
                    disso_structs.append(disso_struct)
293
            else:
294
                for neigh_idx in nl.get_neighbors(el)[0]:
295
                    if molec[neigh_idx].symbol == 'H':
296
                        disso_struct = dissociate_h(slab_molec, neigh_idx +
297
                                                    num_atoms_slab,
298
                                                    num_atoms_slab)
299
                        if disso_struct is not None:
300
                            disso_structs.append(disso_struct)
301
        else:
302
            for atom in molec:
303
                if atom.symbol.lower() == el.lower():
304
                    for neigh_idx in nl.get_neighbors(atom.index)[0]:
305
                        if molec[neigh_idx].symbol == 'H':
306
                            disso_struct = dissociate_h(slab_molec, neigh_idx \
307
                                                        + num_atoms_slab,
308
                                                        num_atoms_slab)
309
                            if disso_struct is not None:
310
                                disso_structs.append(disso_struct)
311
    return disso_structs
312

    
313

    
314
def correct_coll(molec, slab, ctr_coord, site_coord, num_pts,
315
                 min_coll_height, norm_vect, slab_nghbs, molec_nghbs,
316
                 coll_coeff, height=2.5):
317
    # TODO Rethink this function
318
    """Tries to adsorb a molecule on a slab trying to avoid collisions by doing
319
    small rotations.
320

321
    @param molec: ase.Atoms object of the molecule to adsorb
322
    @param slab: ase.Atoms object of the surface on which to adsorb the
323
        molecule
324
    @param ctr_coord: The coordinates of the molecule to use as adsorption
325
        center.
326
    @param site_coord: The coordinates of the surface on which to adsorb the
327
        molecule
328
    @param num_pts: Number on which to sample Euler angles.
329
    @param min_coll_height: The lowermost height for which to detect a collision
330
    @param norm_vect: The vector perpendicular to the surface.
331
    @param slab_nghbs: Number of neigbors in the surface.
332
    @param molec_nghbs: Number of neighbors in the molecule.
333
    @param coll_coeff: The coefficient that multiplies the covalent radius of
334
        atoms resulting in a distance that two atoms being closer to that is
335
        considered as atomic collision.
336
    @param height: Height on which to try adsorption
337
    @return collision: bool, whether the structure generated has collisions
338
        between slab and adsorbate.
339
    """
340
    from copy import deepcopy
341
    slab_num_atoms = len(slab)
342
    collision = True
343
    max_corr = 6  # Should be an even number
344
    d_angle = 180 / ((max_corr / 2.0) * num_pts)
345
    num_corr = 0
346
    while collision and num_corr <= max_corr:
347
        k = num_corr * (-1) ** num_corr
348
        slab_molec = deepcopy(slab)
349
        molec.euler_rotate(k * d_angle, k * d_angle / 2, k * d_angle,
350
                           center=ctr_coord)
351
        add_adsorbate(slab_molec, molec, site_coord, ctr_coord, height,
352
                      norm_vect=norm_vect)
353
        collision = check_collision(slab_molec, slab_num_atoms, min_coll_height,
354
                                    norm_vect, slab_nghbs, molec_nghbs,
355
                                    coll_coeff)
356
        num_corr += 1
357
    return slab_molec, collision
358

    
359

    
360
def ads_euler(orig_molec, slab, ctr_coord, site_coord, num_pts,
361
              min_coll_height, coll_coeff, norm_vect, slab_nghbs, molec_nghbs,
362
              disso_atoms):
363
    """Generates adsorbate-surface structures by sampling over Euler angles.
364

365
    This function generates a number of adsorbate-surface structures at
366
    different orientations of the adsorbate sampled at multiple Euler (zxz)
367
    angles.
368
    @param orig_molec: ase.Atoms object of the molecule to adsorb
369
    @param slab: ase.Atoms object of the surface on which to adsorb the molecule
370
    @param ctr_coord: The coordinates of the molecule to use as adsorption
371
        center.
372
    @param site_coord: The coordinates of the surface on which to adsorb the
373
        molecule
374
    @param num_pts: Number on which to sample Euler angles.
375
    @param min_coll_height: The lowermost height for which to detect a collision
376
    @param coll_coeff: The coefficient that multiplies the covalent radius of
377
        atoms resulting in a distance that two atoms being closer to that is
378
        considered as atomic collision.
379
    @param norm_vect: The vector perpendicular to the surface.
380
    @param slab_nghbs: Number of neigbors in the surface.
381
    @param molec_nghbs: Number of neighbors in the molecule.
382
    @param disso_atoms: List of atom types or atom numbers to try to dissociate.
383
    @return: list of ase.Atoms object conatining all the orientations of a given
384
        conformer
385
    """
386
    from copy import deepcopy
387
    slab_ads_list = []
388
    # rotation around z
389
    for alpha in np.arange(0, 360, 360 / num_pts):
390
        # rotation around x'
391
        for beta in np.arange(0, 180, 180 / num_pts):
392
            # rotation around z'
393
            for gamma in np.arange(0, 360, 360 / num_pts):
394
                molec = deepcopy(orig_molec)
395
                molec.euler_rotate(alpha, beta, gamma, center=ctr_coord)
396
                slab_molec, collision = correct_coll(molec, slab,
397
                                                     ctr_coord, site_coord,
398
                                                     num_pts, min_coll_height,
399
                                                     norm_vect,
400
                                                     slab_nghbs, molec_nghbs,
401
                                                     coll_coeff)
402
                if not collision and \
403
                        not any([(slab_molec.positions == conf.positions).all()
404
                                 for conf in slab_ads_list]):
405
                    slab_ads_list.append(slab_molec)
406
                    slab_ads_list.extend(dissociation(slab_molec, disso_atoms,
407
                                                      len(slab)))
408

    
409
    return slab_ads_list
410

    
411

    
412
def ads_chemcat(site, ctr, pts_angle):
413
    return "TO IMPLEMENT"
414

    
415

    
416
def adsorb_confs(conf_list, surf, molec_ctrs, sites, algo, num_pts, neigh_ctrs,
417
                 norm_vect, min_coll_height, coll_coeff, disso_atoms):
418
    """Generates a number of adsorbate-surface structure coordinates.
419

420
    Given a list of conformers, a surface, a list of atom indices (or list of
421
    list of indices) of both the surface and the adsorbate, it generates a
422
    number of adsorbate-surface structures for every possible combination of
423
    them at different orientations.
424
    @param conf_list: list of ase.Atoms of the different conformers
425
    @param surf: the ase.Atoms object of the surface
426
    @param molec_ctrs: the list atom indices of the adsorbate.
427
    @param sites: the list of atom indices of the surface.
428
    @param algo: the algorithm to use for the generation of adsorbates.
429
    @param num_pts: the number of points per angle orientation to sample
430
    @param neigh_ctrs: the indices of the neighboring atoms to the adsorption
431
        atoms.
432
    @param norm_vect: The vector perpendicular to the surface.
433
    @param min_coll_height: The lowermost height for which to detect a collision
434
    @param coll_coeff: The coefficient that multiplies the covalent radius of
435
        atoms resulting in a distance that two atoms being closer to that is
436
        considered as atomic collision.
437
    @param disso_atoms: List of atom types or atom numbers to try to dissociate.
438
    @return: list of ase.Atoms for the adsorbate-surface structures
439
    """
440
    from ase.neighborlist import natural_cutoffs, neighbor_list
441
    surf_ads_list = []
442
    sites_coords = get_atom_coords(surf, sites)
443
    if min_coll_height is False:
444
        surf_cutoffs = natural_cutoffs(surf, mult=coll_coeff)
445
        surf_nghbs = len(neighbor_list("i", surf, surf_cutoffs))
446
    else:
447
        surf_nghbs = 0
448
    for conf in conf_list:
449
        molec_ctr_coords = get_atom_coords(conf, molec_ctrs)
450
        molec_neigh_coords = get_atom_coords(conf, neigh_ctrs)
451
        if min_coll_height is False:
452
            conf_cutoffs = natural_cutoffs(conf, mult=coll_coeff)
453
            molec_nghbs = len(neighbor_list("i", conf, conf_cutoffs))
454
        else:
455
            molec_nghbs = 0
456
        for site in sites_coords:
457
            for ctr in molec_ctr_coords:
458
                if algo == 'euler':
459
                    surf_ads_list.extend(ads_euler(conf, surf, ctr, site,
460
                                                   num_pts, min_coll_height,
461
                                                   coll_coeff, norm_vect,
462
                                                   surf_nghbs, molec_nghbs,
463
                                                   disso_atoms))
464
                elif algo == 'chemcat':
465
                    surf_ads_list.extend(ads_chemcat(site, ctr, num_pts))
466
    return surf_ads_list
467

    
468

    
469
def run_screening(inp_vars):
470
    """Carries out the screening of adsorbate structures on a surface.
471

472
    @param inp_vars: Calculation parameters from input file.
473
    """
474
    import os
475
    import random
476
    from modules.formats import collect_coords, adapt_format
477
    from modules.calculation import run_calc, check_finished_calcs
478

    
479
    if not os.path.isdir("isolated"):
480
        err = "'isolated' directory not found. It is needed in order to carry "
481
        "out the screening of structures to be adsorbed"
482
        logger.error(err)
483
        raise FileNotFoundError(err)
484

    
485
    logger.info('Carrying out procedures for the screening of adsorbate-surface'
486
                ' structures.')
487

    
488
    finished_calcs, unfinished_calcs = check_finished_calcs('isolated',
489
                                                            inp_vars['code'])
490
    if len(unfinished_calcs) == 0:
491
        logger.info(f"Found {len(finished_calcs)} structures of isolated "
492
                    f"conformers.")
493
    else:
494
        logger.info(f"Found {len(finished_calcs)} structures of isolated "
495
                    f"conformers whose calculation finished normally.")
496
        logger.warning(f"Found {len(unfinished_calcs)} calculations more that "
497
                       f"did not finish normally: {unfinished_calcs}. Using "
498
                       f"only the {len(finished_calcs)} ones who finished "
499
                       f"normally")
500

    
501
    conformer_atoms_list = collect_coords(finished_calcs, inp_vars['code'],
502
                                          'isolated', inp_vars['special_atoms'])
503
    selected_confs = select_confs(conformer_atoms_list, finished_calcs,
504
                                  inp_vars['select_magns'],
505
                                  inp_vars['confs_per_magn'],
506
                                  inp_vars['code'])
507
    surf = adapt_format('ase', inp_vars['surf_file'], inp_vars['special_atoms'])
508
    surf_ads_list = adsorb_confs(selected_confs, surf,
509
                                 inp_vars['molec_ads_ctrs'], inp_vars['sites'],
510
                                 inp_vars['ads_algo'],
511
                                 inp_vars['sample_points_per_angle'],
512
                                 inp_vars['molec_neigh_ctrs'],
513
                                 inp_vars['surf_norm_vect'],
514
                                 inp_vars['min_coll_height'],
515
                                 inp_vars['collision_threshold'],
516
                                 inp_vars['disso_atoms'])
517
    if len(surf_ads_list) > inp_vars['max_structures']:
518
        surf_ads_list = random.sample(surf_ads_list, inp_vars['max_structures'])
519
    logger.info(f'Generated {len(surf_ads_list)} adsorbate-surface atomic '
520
                f'configurations to carry out a calculation of.')
521

    
522
    run_calc('screening', inp_vars, surf_ads_list)
523
    logger.info('Finished the procedures for the screening of adsorbate-surface'
524
                ' structures section.')