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

dockonsurf / modules / screening.py @ 695dcff8

Historique | Voir | Annoter | Télécharger (17,55 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(neighbor_list("i", slab_molec, slab_molec_cutoffs))
224
        if slab_molec_nghbs > nn_slab + nn_molec:
225
            return True
226
        else:
227
            return False
228

    
229

    
230
def correct_coll(molec, slab, ctr_coord, site_coord, num_pts,
231
                 min_coll_height, norm_vect, slab_nghbs, molec_nghbs,
232
                 coll_coeff):
233
    # TODO Rethink this function
234
    """Tries to adsorb a molecule on a slab trying to avoid collisions by doing
235
    small rotations.
236

237
    @param molec: ase.Atoms object of the molecule to adsorb
238
    @param slab: ase.Atoms object of the surface on which to adsorb the
239
        molecule
240
    @param ctr_coord: The coordinates of the molecule to use as adsorption
241
        center.
242
    @param site_coord: The coordinates of the surface on which to adsorb the
243
        molecule
244
    @param num_pts: Number on which to sample Euler angles.
245
    @param min_coll_height: The lowermost height for which to detect a collision
246
    @param norm_vect: The vector perpendicular to the surface.
247
    @param slab_nghbs: Number of neigbors in the surface.
248
    @param molec_nghbs: Number of neighbors in the molecule.
249
    @param coll_coeff: The coefficient that multiplies the covalent radius of
250
        atoms resulting in a distance that two atoms being closer to that is
251
        considered as atomic collision.
252
    @return collision: bool, whether the structure generated has collisions
253
        between slab and adsorbate.
254
    """
255
    from copy import deepcopy
256
    slab_num_atoms = len(slab)
257
    collision = True
258
    max_corr = 6  # Should be an even number
259
    d_angle = 180 / ((max_corr / 2.0) * num_pts)
260
    num_corr = 0
261
    while collision and num_corr <= max_corr:
262
        k = num_corr * (-1) ** num_corr
263
        slab_molec = deepcopy(slab)
264
        molec.euler_rotate(k * d_angle, k * d_angle / 2, k * d_angle,
265
                           center=ctr_coord)
266
        add_adsorbate(slab_molec, molec, site_coord, ctr_coord, 2.5,
267
                      norm_vect=norm_vect)
268
        collision = check_collision(slab_molec, slab_num_atoms, min_coll_height,
269
                                    norm_vect, slab_nghbs, molec_nghbs,
270
                                    coll_coeff)
271
        num_corr += 1
272
    return slab_molec, collision
273

    
274

    
275
def ads_euler(orig_molec, slab, ctr_coord, site_coord, num_pts,
276
              min_coll_height, coll_coeff, norm_vect, slab_nghbs, molec_nghbs):
277
    """Generates adsorbate-surface structures by sampling over Euler angles.
278

279
    This function generates a number of adsorbate-surface structures at
280
    different orientations of the adsorbate sampled at multiple Euler (zxz)
281
    angles.
282
    @param orig_molec: ase.Atoms object of the molecule to adsorb
283
    @param slab: ase.Atoms object of the surface on which to adsorb the molecule
284
    @param ctr_coord: The coordinates of the molecule to use as adsorption
285
        center.
286
    @param site_coord: The coordinates of the surface on which to adsorb the
287
        molecule
288
    @param num_pts: Number on which to sample Euler angles.
289
    @param min_coll_height: The lowermost height for which to detect a collision
290
    @param coll_coeff: The coefficient that multiplies the covalent radius of
291
        atoms resulting in a distance that two atoms being closer to that is
292
        considered as atomic collision.
293
    @param norm_vect: The vector perpendicular to the surface.
294
    @param slab_nghbs: Number of neigbors in the surface.
295
    @param molec_nghbs: Number of neighbors in the molecule.
296
    @return: list of ase.Atoms object conatining all the orientations of a given
297
        conformer
298
    """
299
    from copy import deepcopy
300
    slab_ads_list = []
301
    # rotation around z
302
    for alpha in np.arange(0, 360, 360 / num_pts):
303
        # rotation around x'
304
        for beta in np.arange(0, 180, 180 / num_pts):
305
            # rotation around z'
306
            for gamma in np.arange(0, 360, 360 / num_pts):
307
                molec = deepcopy(orig_molec)
308
                molec.euler_rotate(alpha, beta, gamma, center=ctr_coord)
309
                slab_molec, collision = correct_coll(molec, slab,
310
                                                     ctr_coord, site_coord,
311
                                                     num_pts, min_coll_height,
312
                                                     norm_vect,
313
                                                     slab_nghbs, molec_nghbs,
314
                                                     coll_coeff)
315
                if not collision:
316
                    slab_ads_list.append(slab_molec)
317

    
318
    return slab_ads_list
319

    
320

    
321
def ads_chemcat(site, ctr, pts_angle):
322
    return "TO IMPLEMENT"
323

    
324

    
325
def adsorb_confs(conf_list, surf, molec_ctrs, sites, algo, num_pts, neigh_ctrs,
326
                 norm_vect, min_coll_height, coll_coeff):
327
    """Generates a number of adsorbate-surface structure coordinates.
328

329
    Given a list of conformers, a surface, a list of atom indices (or list of
330
    list of indices) of both the surface and the adsorbate, it generates a
331
    number of adsorbate-surface structures for every possible combination of
332
    them at different orientations.
333
    @param conf_list: list of ase.Atoms of the different conformers
334
    @param surf: the ase.Atoms object of the surface
335
    @param molec_ctrs: the list atom indices of the adsorbate.
336
    @param sites: the list of atom indices of the surface.
337
    @param algo: the algorithm to use for the generation of adsorbates.
338
    @param num_pts: the number of points per angle orientation to sample
339
    @param neigh_ctrs: the indices of the neighboring atoms to the adsorption
340
        atoms.
341
    @param norm_vect: The vector perpendicular to the surface.
342
    @param min_coll_height: The lowermost height for which to detect a collision
343
    @param coll_coeff: The coefficient that multiplies the covalent radius of
344
        atoms resulting in a distance that two atoms being closer to that is
345
        considered as atomic collision.
346
    @return: list of ase.Atoms for the adsorbate-surface structures
347
    """
348
    from ase.neighborlist import natural_cutoffs, neighbor_list
349
    surf_ads_list = []
350
    sites_coords = get_atom_coords(surf, sites)
351
    if min_coll_height is not False:
352
        surf_cutoffs = natural_cutoffs(surf, mult=coll_coeff)
353
        surf_nghbs = len(neighbor_list("i", surf, surf_cutoffs))
354
    else:
355
        surf_nghbs = 0
356
    for conf in conf_list:
357
        molec_ctr_coords = get_atom_coords(conf, molec_ctrs)
358
        molec_neigh_coords = get_atom_coords(conf, neigh_ctrs)
359
        if min_coll_height is not False:
360
            conf_cutoffs = natural_cutoffs(conf, mult=coll_coeff)
361
            molec_nghbs = len(neighbor_list("i", conf, conf_cutoffs))
362
        else:
363
            molec_nghbs = 0
364
        for site in sites_coords:
365
            for ctr in molec_ctr_coords:
366
                if algo == 'euler':
367
                    surf_ads_list.extend(ads_euler(conf, surf, ctr, site,
368
                                                   num_pts, min_coll_height,
369
                                                   coll_coeff, norm_vect,
370
                                                   surf_nghbs, molec_nghbs))
371
                elif algo == 'chemcat':
372
                    surf_ads_list.extend(ads_chemcat(site, ctr, num_pts))
373
    return surf_ads_list
374

    
375

    
376
def run_screening(inp_vars):
377
    """Carry out the screening of adsorbate coordinates on a surface
378

379
    @param inp_vars: Calculation parameters from input file.
380
    """
381
    import os
382
    from modules.formats import read_coords, adapt_format
383
    from modules.calculation import run_calc
384

    
385
    if not os.path.isdir("isolated"):
386
        err = "'isolated' directory not found. It is needed in order to carry "
387
        "out the screening of structures to be adsorbed"
388
        logger.error(err)
389
        raise ValueError(err)
390

    
391
    conf_list = read_coords(inp_vars['code'], 'isolated', 'ase')
392
    logger.info(f"Found {len(conf_list)} structures of isolated conformers.")
393
    selected_confs = select_confs(conf_list, inp_vars['select_magns'],
394
                                  inp_vars['confs_per_magn'],
395
                                  inp_vars['code'])
396
    surf = adapt_format('ase', inp_vars['surf_file'])
397
    surf_ads_list = adsorb_confs(selected_confs, surf,
398
                                 inp_vars['molec_ads_ctrs'], inp_vars['sites'],
399
                                 inp_vars['ads_algo'],
400
                                 inp_vars['sample_points_per_angle'],
401
                                 inp_vars['molec_neigh_ctrs'],
402
                                 inp_vars['surf_norm_vect'],
403
                                 inp_vars['min_coll_height'],
404
                                 inp_vars['collision_threshold'])
405
    logger.info(f'Generated {len(surf_ads_list)} adsorbate-surface atomic '
406
                f'configurations, to carry out a calculation of.')
407
    run_calc('screening', inp_vars, surf_ads_list)