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

dockonsurf / modules / screening.py @ 1e36f905

Historique | Voir | Annoter | Télécharger (22,84 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, 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
    @param orig_conf_list: list of ase.Atoms objects to select among.
19
    @param magns: list of str with the names of the magnitudes to use for the
20
        conformer selection.
21
        Supported magnitudes: 'energy', 'moi'.
22
    @param num_sel: number of conformers to select for every of the magnitudes.
23
    @param code: The code that generated the magnitude information.
24
         Supported codes: See formats.py
25
    @return: list of the selected ase.Atoms objects.
26
    """
27
    from copy import deepcopy
28
    from modules.formats import read_energies
29

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

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

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

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

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

    
66

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

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

    
82

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

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

    
100

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

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

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

    
136

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

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

    
191

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

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

    
230

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

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

    
263

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

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

    
312

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

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

    
358

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

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

    
408
    return slab_ads_list
409

    
410

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

    
414

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

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

    
467

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

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

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

    
484
    logger.info('Carrying out procedures for the screening of adsorbate-surface'
485
                ' structures.')
486
    conf_list = read_coords(inp_vars['code'], 'isolated', 'ase',
487
                            inp_vars['special_atoms'])
488
    logger.info(f"Found {len(conf_list)} structures of isolated conformers.")
489
    selected_confs = select_confs(conf_list, inp_vars['select_magns'],
490
                                  inp_vars['confs_per_magn'],
491
                                  inp_vars['code'])
492
    surf = adapt_format('ase', inp_vars['surf_file'], inp_vars['special_atoms'])
493
    surf_ads_list = adsorb_confs(selected_confs, surf,
494
                                 inp_vars['molec_ads_ctrs'], inp_vars['sites'],
495
                                 inp_vars['ads_algo'],
496
                                 inp_vars['sample_points_per_angle'],
497
                                 inp_vars['molec_neigh_ctrs'],
498
                                 inp_vars['surf_norm_vect'],
499
                                 inp_vars['min_coll_height'],
500
                                 inp_vars['collision_threshold'],
501
                                 inp_vars['disso_atoms'])
502
    if len(surf_ads_list) > inp_vars['max_structures']:
503
        surf_ads_list = random.sample(surf_ads_list, inp_vars['max_structures'])
504
    logger.info(f'Generated {len(surf_ads_list)} adsorbate-surface atomic '
505
                f'configurations to carry out a calculation of.')
506
    for i in range(len(surf_ads_list)):
507
        for j in range(i):
508
            if (surf_ads_list[i].positions == surf_ads_list[j].positions).all():
509
                logger.warning(i, j, 'Same')
510

    
511
    run_calc('screening', inp_vars, surf_ads_list)
512
    logger.info('Finished the procedures for the screening of adsorbate-surface'
513
                ' structures section.')