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

dockonsurf / modules / screening.py @ 5261a07f

Historique | Voir | Annoter | Télécharger (40,77 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,
13
                 num_sel: int, code: str):
14
    """Takes a list ase.Atoms and selects the most different magnitude-wise.
15

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

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

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

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

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

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

    
68

    
69
def get_vect_angle(v1: list, v2: list, ref=None, degrees=True):
70
    """Computes the angle between two vectors.
71

72
    @param v1: The first vector.
73
    @param v2: The second vector.
74
    @param ref: Orthogonal vector to both v1 and v2,
75
        along which the sign of the rotation is defined (i.e. positive if
76
        counterclockwise angle when facing ref)
77
    @param degrees: Whether the result should be in radians (True) or in
78
        degrees (False).
79
    @return: The angle in radians if degrees = False, or in degrees if
80
        degrees =True
81
    """
82
    v1_u = v1 / np.linalg.norm(v1)
83
    v2_u = v2 / np.linalg.norm(v2)
84
    angle = np.arccos(np.clip(np.dot(v1_u, v2_u), -1.0, 1.0))
85
    if ref is not None:
86
        # Give sign according to ref direction
87
        angle *= (1 if np.dot(np.cross(v1, v2), ref) >= 0 else -1)
88

    
89
    return angle if not degrees else angle * 180 / np.pi
90

    
91

    
92
def vect_avg(vects):
93
    """Computes the element-wise mean of a set of vectors.
94

95
    @param vects: list of lists-like: containing the vectors (num_vectors,
96
        length_vector).
97
    @return: vector average computed doing the element-wise mean.
98
    """
99
    from utilities import try_command
100
    err = "vect_avg parameter vects must be a list-like, able to be converted" \
101
          " np.array"
102
    array = try_command(np.array, [(ValueError, err)], vects)
103
    if len(array.shape) == 1:
104
        return array
105
    else:
106
        num_vects = array.shape[1]
107
        return np.array([np.average(array[:, i]) for i in range(num_vects)])
108

    
109

    
110
def get_atom_coords(atoms: ase.Atoms, ctrs_list=None):
111
    """Gets the coordinates of the specified indices from a ase.Atoms object.
112

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

    
144

    
145
def add_adsorbate(slab, adsorbate, site_coord, ctr_coord, height, offset=None,
146
                  norm_vect=(0, 0, 1)):
147
    """Add an adsorbate to a surface.
148

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

    
199

    
200
def check_collision(slab_molec, slab_num_atoms, min_height, vect, nn_slab=0,
201
                    nn_molec=0, coll_coeff=0.9):
202
    """Checks whether a slab and a molecule collide or not.
203

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

    
238

    
239
def dissociate_h(slab_molec_orig, h_idx, num_atoms_slab, neigh_cutoff=1):
240
    # TODO rethink
241
    """Tries to dissociate a H from the molecule and adsorbs it on the slab.
242

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

    
271

    
272
def dissociation(slab_molec, disso_atoms, num_atoms_slab):
273
    # TODO rethink
274
    # TODO multiple dissociation
275
    """Decides which H atoms to dissociate according to a list of atoms.
276

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

    
320

    
321
def correct_coll(molec, slab, ctr_coord, site_coord, num_pts,
322
                 min_coll_height, norm_vect, slab_nghbs, molec_nghbs,
323
                 coll_coeff, height=2.5):
324
    # TODO Rethink this function
325
    """Tries to adsorb a molecule on a slab trying to avoid collisions by doing
326
    small rotations.
327

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

    
367

    
368
def ads_euler(orig_molec, slab, ctr_coord, site_coord, num_pts,
369
              min_coll_height, coll_coeff, norm_vect, slab_nghbs, molec_nghbs,
370
              disso_atoms):
371
    """Generates adsorbate-surface structures by sampling over Euler angles.
372

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

    
418
    return slab_ads_list
419

    
420

    
421
def chemcat_rotate(molecule, surf, ctr1_mol, ctr2_mol, ctr3_mol, ctr1_surf,
422
                   ctr2_surf, bond_vector, bond_angle_target,
423
                   dihedral_angle_target=None, mol_dihedral_angle_target=None):
424
    """Performs translation and rotation of an adsorbate defined by an
425
    adsorption bond length, direction, angle and dihedral angle
426

427
    Carles modification of chemcat's transform_adsorbate to work with
428
    coordinates instead of ase.Atom
429
    Parameters:
430
        molecule (ase.Atoms): The molecule to adsorb.
431

432
        surf (ase.Atoms): The surface ontop of which to adsorb.
433

434
        ctr1_mol (int/list): The position of the adsorption center in the
435
        molecule that will be bound to the surface.
436

437
        ctr2_mol (int/list): The position of a second center of the
438
        adsorbate used to define the adsorption bond angle, and the dihedral
439
        adsorption angle.
440

441
        ctr3_mol (int/list): The position of a third center in the
442
        adsorbate used to define the adsorbate dihedral angle.
443

444
        ctr1_surf (int/list): The position of the site on the surface that
445
        will be bound to the molecule.
446

447
        ctr2_surf (int/list): The position of a second center of the
448
        surface used to define the dihedral adsorption angle.
449

450
        bond_vector (numpy.ndarray): The adsorption bond desired.
451
            Details: offset = vect(atom1_surf, atom1_mol)
452

453
        bond_angle_target (float or int): The adsorption bond angle desired (in
454
            degrees).
455
            Details: bond_angle_target = angle(atom1_surf, atom1_mol, atom2_mol)
456

457
        dihedral_angle_target (float or int): The dihedral adsorption angle
458
            desired (in degrees).
459
            Details: dihedral_angle_target = dihedral(atom2_surf, atom1_surf,
460
            atom1_mol, atom2_mol)
461
                The rotation vector is facing the adsorbate from the surface
462
                (i.e. counterclockwise rotation when facing the surface (i.e.
463
                view from top))
464

465
        mol_dihedral_angle_target (float or int): The adsorbate dihedral angle
466
            desired (in degrees).
467
            Details: mol_dihedral_angle_target = dihedral(atom1_surf, atom1_mol,
468
            atom2_mol, atom3_mol)
469
                The rotation vector is facing atom2_mol from atom1_mol
470

471
    Returns:
472
        None (the `molecule` object is modified in-place)
473
    """
474
    vect_surf = get_atom_coords(surf, ctr2_surf) - get_atom_coords(surf,
475
                                                                   ctr1_surf)
476
    vect_inter = get_atom_coords(molecule, ctr1_mol) - get_atom_coords(surf,
477
                                                                       ctr1_surf)
478
    vect_mol = get_atom_coords(molecule, ctr2_mol) - get_atom_coords(molecule,
479
                                                                     ctr1_mol)
480
    vect2_mol = get_atom_coords(molecule, ctr3_mol) - get_atom_coords(molecule,
481
                                                                      ctr2_mol)
482

    
483
    # Check if dihedral angles can be defined
484
    do_dihedral = dihedral_angle_target is not None
485
    do_mol_dihedral = mol_dihedral_angle_target is not None
486
    dihedral_use_mol2 = False
487
    # Check if vect_surf and vect_inter are not aligned
488
    if np.allclose(np.cross(vect_surf, vect_inter), 0):
489
        logger.warning(
490
            "Surface atoms are incompatible with adsorption "
491
            "direction/bond. An adsorption dihedral angle cannot be defined.")
492
        do_dihedral = False
493
    # Check if requested bond angle is not flat
494
    if np.isclose((bond_angle_target + 90) % 180 - 90, 0):
495
        logger.warning("Requested bond angle is flat. Only a single dihedral "
496
                       "angle can be defined (ctr2_surf, ctr1_surf, ctr2_mol, "
497
                       "ctr3_mol).")
498
        do_mol_dihedral = False
499
        dihedral_use_mol2 = True
500
        logger.warning("Dihedral adsorption angle rotation will be perfomed "
501
                       "with (ctr2_surf, ctr1_surf, ctr2_mol, ctr3_mol).")
502
    # Check if vect_mol and vect2_mol are not aligned
503
    if np.allclose(np.cross(vect_mol, vect2_mol), 0):
504
        logger.warning("Adsorbates atoms are aligned. An adsorbate dihedral "
505
                       "angle cannot be defined.")
506
        do_mol_dihedral = False
507
    if not do_dihedral:
508
        logger.warning("Dihedral adsorption angle rotation will not be "
509
                       "performed.")
510
    if not do_mol_dihedral:
511
        logger.warning("Adsorbate dihedral angle rotation will not be "
512
                       "performed.")
513

    
514
    ###########################
515
    #       Translation       #
516
    ###########################
517

    
518
    # Compute and apply translation of adsorbate
519
    translation = bond_vector - vect_inter
520
    molecule.translate(translation)
521

    
522
    # Update adsorption bond
523
    vect_inter = get_atom_coords(molecule, ctr1_mol) - get_atom_coords(surf,
524
                                                                       ctr1_surf)
525

    
526
    # Check if translation was successful
527
    if np.allclose(vect_inter, bond_vector):
528
        pass  # print("Translation successfully applied (error: ~ {:.5g} unit "
529
        # "length)".format(np.linalg.norm(vect_inter - bond_vector)))
530
    else:
531
        err = 'An unknown error occured during the translation'
532
        logger.error(err)
533
        raise AssertionError(err)
534

    
535
    ###########################
536
    #   Bond angle rotation   #
537
    ###########################
538

    
539
    # Compute rotation vector
540
    rotation_vector = np.cross(-vect_inter, vect_mol)
541
    if np.allclose(rotation_vector, 0, atol=1e-5):
542
        # If molecular bonds are aligned, any vector orthogonal to vect_inter
543
        # can be used Such vector can be found as the orthogonal rejection of
544
        # either X-axis, Y-axis or Z-axis onto vect_inter (since they cannot
545
        # be all aligned)
546
        non_aligned_vector = np.zeros(3)
547
        # Select the most orthogonal axis (lowest dot product):
548
        non_aligned_vector[np.argmin(np.abs(vect_inter))] = 1
549
        rotation_vector = non_aligned_vector - np.dot(non_aligned_vector,
550
                                                      vect_inter) / np.dot(
551
            vect_inter, vect_inter) * vect_inter
552

    
553
    # Retrieve current bond angle
554
    bond_angle_ini = get_vect_angle(-vect_inter, vect_mol, rotation_vector)
555

    
556
    # Apply rotation to reach desired bond_angle
557
    molecule.rotate(bond_angle_target - bond_angle_ini, v=rotation_vector,
558
                    center=get_atom_coords(molecule, ctr1_mol))
559

    
560
    # Update molecular bonds
561
    vect_mol = get_atom_coords(molecule, ctr2_mol) - get_atom_coords(molecule,
562
                                                                     ctr1_mol)
563
    vect2_mol = get_atom_coords(molecule, ctr3_mol) - get_atom_coords(molecule,
564
                                                                      ctr2_mol)
565

    
566
    # Check if rotation was successful
567
    bond_angle = get_vect_angle(-vect_inter, vect_mol)
568
    if np.isclose((bond_angle - bond_angle_target + 90) % 180 - 90, 0,
569
                  atol=1e-3) and np.allclose(get_atom_coords(molecule, ctr1_mol)
570
                                             - get_atom_coords(surf,
571
                                                               ctr1_surf),
572
                                             vect_inter):
573
        pass  # print("Rotation successfully applied (error: {:.5f}°)".format(
574
        # (bond_angle - bond_angle_target + 90) % 180 - 90))
575
    else:
576
        err = 'An unknown error occured during the rotation'
577
        logger.error(err)
578
        raise AssertionError(err)
579

    
580
    ###########################
581
    # Dihedral angle rotation #
582
    ###########################
583

    
584
    # Perform dihedral rotation if possible
585
    if do_dihedral:
586
        # Retrieve current dihedral angle (by computing the angle between the
587
        # vector rejection of vect_surf and vect_mol onto vect_inter)
588
        vect_inter_inner = np.dot(vect_inter, vect_inter)
589
        vect_surf_reject = vect_surf - np.dot(vect_surf, vect_inter) / \
590
                           vect_inter_inner * vect_inter
591
        if dihedral_use_mol2:
592
            vect_mol_reject = vect2_mol - np.dot(vect2_mol, vect_inter) / \
593
                              vect_inter_inner * vect_inter
594
        else:
595
            vect_mol_reject = vect_mol - np.dot(vect_mol, vect_inter) / \
596
                              vect_inter_inner * vect_inter
597
        dihedral_angle_ini = get_vect_angle(vect_surf_reject, vect_mol_reject,
598
                                            vect_inter)
599

    
600
        # Apply dihedral rotation along vect_inter
601
        molecule.rotate(dihedral_angle_target - dihedral_angle_ini,
602
                        v=vect_inter, center=get_atom_coords(molecule,
603
                                                             ctr1_mol))
604

    
605
        # Update molecular bonds
606
        vect_mol = get_atom_coords(molecule, ctr2_mol) - \
607
                   get_atom_coords(molecule, ctr1_mol)
608
        vect2_mol = get_atom_coords(molecule, ctr3_mol) - \
609
                    get_atom_coords(molecule, ctr2_mol)
610

    
611
        # Check if rotation was successful
612
        # Check dihedral rotation
613
        if dihedral_use_mol2:
614
            vect_mol_reject = vect2_mol - np.dot(vect2_mol, vect_inter) / \
615
                              vect_inter_inner * vect_inter
616
        else:
617
            vect_mol_reject = vect_mol - np.dot(vect_mol, vect_inter) / \
618
                              vect_inter_inner * vect_inter
619
        dihedral_angle = get_vect_angle(vect_surf_reject, vect_mol_reject,
620
                                        vect_inter)
621
        # Check bond rotation is unmodified
622
        bond_angle = get_vect_angle(-vect_inter, vect_mol)
623
        if np.isclose((dihedral_angle - dihedral_angle_target + 90) % 180 - 90,
624
                      0, atol=1e-3) \
625
                and np.isclose((bond_angle - bond_angle_target + 90) %
626
                               180 - 90, 0, atol=1e-5) \
627
                and np.allclose(get_atom_coords(molecule,
628
                                                                    ctr1_mol)
629
                                                    - get_atom_coords(surf,
630
                                                                      ctr1_surf),
631
                                                    vect_inter):
632
            pass  # print( "Dihedral rotation successfully applied (error: {
633
            # :.5f}°)".format((dihedral_angle - dihedral_angle_target + 90) %
634
            # 180 - 90))
635
        else:
636
            err = 'An unknown error occured during the dihedral rotation'
637
            logger.error(err)
638
            raise AssertionError(err)
639

    
640
    #####################################
641
    # Adsorbate dihedral angle rotation #
642
    #####################################
643

    
644
    # Perform adsorbate dihedral rotation if possible
645
    if do_mol_dihedral:
646
        # Retrieve current adsorbate dihedral angle (by computing the angle
647
        # between the orthogonal rejection of vect_inter and vect2_mol onto
648
        # vect_mol)
649
        vect_mol_inner = np.dot(vect_mol, vect_mol)
650
        bond_inter_reject = -vect_inter - np.dot(-vect_inter, vect_mol) / \
651
            vect_mol_inner * vect_mol
652
        bond2_mol_reject = vect2_mol - np.dot(vect2_mol, vect_mol) / \
653
            vect_mol_inner * vect_mol
654
        dihedral_angle_ini = get_vect_angle(bond_inter_reject,
655
                                            bond2_mol_reject, vect_mol)
656

    
657
        # Apply dihedral rotation along vect_mol
658
        molecule.rotate(mol_dihedral_angle_target - dihedral_angle_ini,
659
                        v=vect_mol, center=get_atom_coords(molecule, ctr1_mol))
660

    
661
        # Update molecular bonds
662
        vect_mol = get_atom_coords(molecule, ctr2_mol) \
663
            - get_atom_coords(molecule, ctr1_mol)
664
        vect2_mol = get_atom_coords(molecule, ctr3_mol) \
665
            - get_atom_coords(molecule, ctr2_mol)
666

    
667
        # Check if rotation was successful
668
        # Check adsorbate dihedral rotation
669
        vect_mol_inner = np.dot(vect_mol, vect_mol)
670
        bond2_mol_reject = vect2_mol - np.dot(vect2_mol, vect_mol) / \
671
            vect_mol_inner * vect_mol
672
        mol_dihedral_angle = get_vect_angle(bond_inter_reject,
673
                                            bond2_mol_reject, vect_mol)
674
        # Check dihedral rotation
675
        vect_inter_inner = np.dot(vect_inter, vect_inter)
676
        vect_surf_reject = vect_surf - np.dot(vect_surf, vect_inter) / \
677
            vect_inter_inner * vect_inter
678
        vect_mol_reject = vect_mol - np.dot(vect_mol, vect_inter) / \
679
            vect_inter_inner * vect_inter
680
        dihedral_angle = get_vect_angle(vect_surf_reject, vect_mol_reject,
681
                                        vect_inter)
682
        # Check bond rotation is unmodified
683
        bond_angle = get_vect_angle(-vect_inter, vect_mol)
684
        if np.isclose((mol_dihedral_angle - mol_dihedral_angle_target + 90) %
685
                      180 - 90, 0, atol=1e-3) \
686
                and np.isclose((dihedral_angle -
687
                                dihedral_angle_target + 90) % 180 - 90, 0,
688
                               atol=1e-5) \
689
                and np.isclose((bond_angle - bond_angle_target + 90) % 180 - 90,
690
                               0, atol=1e-5) \
691
                and np.allclose(get_atom_coords(molecule, ctr1_mol) -
692
                                get_atom_coords(surf, ctr1_surf),
693
                                vect_inter):
694
            pass  # print(
695
            # "Adsorbate dihedral rotation successfully applied (error:
696
            # {:.5f}°)".format((mol_dihedral_angle - mol_dihedral_angle_target
697
            # + 90) % 180 - 90))
698
        else:
699
            err = 'An unknown error occured during the adsorbate dihedral ' \
700
                  'rotation'
701
            logger.error(err)
702
            raise AssertionError(err)
703

    
704

    
705
def ads_chemcat(orig_molec, slab, ctr1_mol, ctr2_mol, ctr3_mol, ctr1_surf,
706
                ctr2_surf, num_pts, min_coll_height, coll_coeff, norm_vect,
707
                slab_nghbs, molec_nghbs, disso_atoms, max_h):
708
    """Generates adsorbate-surface structures by sampling over chemcat angles.
709

710
    @param orig_molec: ase.Atoms object of the molecule to adsorb (adsorbate).
711
    @param slab: ase.Atoms object of the surface on which to adsorb the molecule
712
    @param ctr1_mol: The index/es of the center in the adsorbate to use as
713
        adsorption center.
714
    @param ctr2_mol: The index/es of the center in the adsorbate to use for the
715
        definition of the surf-adsorbate angle, surf-adsorbate dihedral angle
716
        and adsorbate dihedral angle.
717
    @param ctr3_mol: The index/es of the center in the adsorbate to use for the
718
        definition of the adsorbate dihedral angle.
719
    @param ctr1_surf: The index/es of the center in the surface to use as
720
        adsorption center.
721
    @param ctr2_surf: The index/es of the center in the surface to use for the
722
        definition of the surf-adsorbate dihedral angle.
723
    @param num_pts: Number on which to sample Euler angles.
724
    @param min_coll_height: The lowest height for which to detect a collision
725
    @param coll_coeff: The coefficient that multiplies the covalent radius of
726
        atoms resulting in a distance that two atoms being closer to that is
727
        considered as atomic collision.
728
    @param norm_vect: The vector perpendicular to the surface.
729
    @param slab_nghbs: Number of neigbors in the surface.
730
    @param molec_nghbs: Number of neighbors in the molecule.
731
    @param disso_atoms: List of atom types or atom numbers to try to dissociate.
732
    @param max_h: Maximum value for sampling the helicopter
733
        (surf-adsorbate dihedral) angle.
734
    @return: list of ase.Atoms object conatining all the orientations of a given
735
        conformer.
736
    """
737
    from copy import deepcopy
738
    slab_ads_list = []
739
    # Rotation over bond angle
740
    for alpha in np.arange(90, 180, 90 / min(num_pts, 2)):
741
        # Rotation over surf-adsorbate dihedral angle
742
        for beta in np.arange(0, max_h, max_h / num_pts):
743
            # Rotation over adsorbate bond dihedral angle
744
            for gamma in np.arange(0, 360, 360 / num_pts):
745
                new_molec = deepcopy(orig_molec)
746
                chemcat_rotate(new_molec, slab, ctr1_mol, ctr2_mol, ctr3_mol,
747
                               ctr1_surf, ctr2_surf, norm_vect, alpha,
748
                               beta, gamma)
749
                site_coords = get_atom_coords(slab, ctr1_surf)
750
                ctr_coords = get_atom_coords(new_molec, ctr1_mol)
751
                slab_molec, collision = correct_coll(new_molec, slab,
752
                                                     ctr_coords, site_coords,
753
                                                     num_pts, min_coll_height,
754
                                                     norm_vect, slab_nghbs,
755
                                                     molec_nghbs, coll_coeff,
756
                                                     2.5)
757
                if not collision and \
758
                        not any([np.allclose(slab_molec.positions,
759
                                             conf.positions)
760
                                 for conf in slab_ads_list]):
761
                    slab_ads_list.append(slab_molec)
762
                    slab_ads_list.extend(dissociation(slab_molec, disso_atoms,
763
                                                      len(slab)))
764

    
765
    return slab_ads_list
766

    
767

    
768
def adsorb_confs(conf_list, surf, inp_vars):
769
    """Generates a number of adsorbate-surface structure coordinates.
770

771
    Given a list of conformers, a surface, a list of atom indices (or list of
772
    list of indices) of both the surface and the adsorbate, it generates a
773
    number of adsorbate-surface structures for every possible combination of
774
    them at different orientations.
775
    @param conf_list: list of ase.Atoms of the different conformers
776
    @param surf: the ase.Atoms object of the surface
777
    @param inp_vars: Calculation parameters from input file.
778
    @return: list of ase.Atoms for the adsorbate-surface structures
779
    """
780
    from ase.neighborlist import natural_cutoffs, neighbor_list
781
    molec_ctrs = inp_vars['molec_ctrs']
782
    sites = inp_vars['sites']
783
    angles = inp_vars['set_angles']
784
    num_pts = inp_vars['sample_points_per_angle']
785
    norm_vect = inp_vars['surf_norm_vect']
786
    min_coll_height = inp_vars['min_coll_height']
787
    coll_coeff = inp_vars['collision_threshold']
788
    disso_atoms = inp_vars['disso_atoms']
789

    
790
    if inp_vars['pbc_cell'] is not False:
791
        surf.set_pbc(True)
792
        surf.set_cell(inp_vars['pbc_cell'])
793

    
794
    surf_ads_list = []
795
    sites_coords = get_atom_coords(surf, sites)
796
    if min_coll_height is False:
797
        surf_cutoffs = natural_cutoffs(surf, mult=coll_coeff)
798
        surf_nghbs = len(neighbor_list("i", surf, surf_cutoffs))
799
    else:
800
        surf_nghbs = 0
801
    for i, conf in enumerate(conf_list):
802
        molec_ctr_coords = get_atom_coords(conf, molec_ctrs)
803
        if inp_vars['pbc_cell'] is not False:
804
            conf.set_pbc(True)
805
            conf.set_cell(inp_vars['pbc_cell'])
806
        if min_coll_height is False:
807
            conf_cutoffs = natural_cutoffs(conf, mult=coll_coeff)
808
            molec_nghbs = len(neighbor_list("i", conf, conf_cutoffs))
809
        else:
810
            molec_nghbs = 0
811
        for s, site in enumerate(sites_coords):
812
            for c, ctr in enumerate(molec_ctr_coords):
813
                if angles == 'euler':
814
                    surf_ads_list.extend(ads_euler(conf, surf, ctr, site,
815
                                                   num_pts, min_coll_height,
816
                                                   coll_coeff, norm_vect,
817
                                                   surf_nghbs, molec_nghbs,
818
                                                   disso_atoms))
819
                elif angles == 'chemcat':
820
                    mol_ctr1 = molec_ctrs[c]
821
                    mol_ctr2 = inp_vars["molec_ctrs2"][c]
822
                    mol_ctr3 = inp_vars["molec_ctrs3"][c]
823
                    surf_ctr1 = sites[s]
824
                    surf_ctr2 = inp_vars["surf_ctrs2"][s]
825
                    max_h = inp_vars["max_helic_angle"]
826
                    surf_ads_list.extend(ads_chemcat(conf, surf, mol_ctr1,
827
                                                     mol_ctr2, mol_ctr3,
828
                                                     surf_ctr1, surf_ctr2,
829
                                                     num_pts, min_coll_height,
830
                                                     coll_coeff, norm_vect,
831
                                                     surf_nghbs, molec_nghbs,
832
                                                     disso_atoms, max_h))
833
    return surf_ads_list
834

    
835

    
836
def run_screening(inp_vars):
837
    """Carries out the screening of adsorbate structures on a surface.
838

839
    @param inp_vars: Calculation parameters from input file.
840
    """
841
    import os
842
    import random
843
    from modules.formats import collect_coords, adapt_format
844
    from modules.calculation import run_calc, check_finished_calcs
845

    
846
    logger.info('Carrying out procedures for the screening of adsorbate-surface'
847
                ' structures.')
848
    if not os.path.isdir("isolated"):
849
        err = "'isolated' directory not found. It is needed in order to carry "
850
        "out the screening of structures to be adsorbed"
851
        logger.error(err)
852
        raise FileNotFoundError(err)
853

    
854
    finished_calcs, unfinished_calcs = check_finished_calcs('isolated',
855
                                                            inp_vars['code'])
856
    logger.info(f"Found {len(finished_calcs)} structures of isolated "
857
                f"conformers whose calculation finished normally.")
858
    if len(unfinished_calcs) != 0:
859
        logger.warning(f"Found {len(unfinished_calcs)} calculations more that "
860
                       f"did not finish normally: {unfinished_calcs}. \n"
861
                       f"Using only the ones that finished normally: "
862
                       f"{finished_calcs}.")
863

    
864
    conformer_atoms_list = collect_coords(finished_calcs, inp_vars['code'],
865
                                          'isolated', inp_vars['special_atoms'])
866
    selected_confs = select_confs(conformer_atoms_list, finished_calcs,
867
                                  inp_vars['select_magns'],
868
                                  inp_vars['confs_per_magn'],
869
                                  inp_vars['code'])
870
    surf = adapt_format('ase', inp_vars['surf_file'], inp_vars['special_atoms'])
871
    surf_ads_list = adsorb_confs(selected_confs, surf, inp_vars)
872
    if len(surf_ads_list) > inp_vars['max_structures']:
873
        surf_ads_list = random.sample(surf_ads_list, inp_vars['max_structures'])
874
    logger.info(f'Generated {len(surf_ads_list)} adsorbate-surface atomic '
875
                f'configurations to carry out a calculation of.')
876

    
877
    run_calc('screening', inp_vars, surf_ads_list)
878
    logger.info('Finished the procedures for the screening of adsorbate-surface'
879
                ' structures section.')