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

dockonsurf / modules / screening.py @ e8bebcca

Historique | Voir | Annoter | Télécharger (42,23 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=1.2):
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
    """
217
    from ase.neighborlist import natural_cutoffs, neighbor_list
218

    
219
    # Check structure overlap by height
220
    if min_height is not False:
221
        cart_axes = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0],
222
                     [-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, -1.0]]
223
        if vect.tolist() not in cart_axes:
224
            err_msg = "'min_coll_height' option is only implemented for " \
225
                      "'surf_norm_vect' to be one of the x, y or z axes. "
226
            logger.error(err_msg)
227
            raise ValueError(err_msg)
228
        for atom in slab_molec[slab_num_atoms:]:
229
            for i, coord in enumerate(vect):
230
                if coord == 0:
231
                    continue
232
                if atom.position[i] * coord < min_height * coord:
233
                    return True
234

    
235
    # Check structure overlap by sphere collision
236
    if coll_coeff is not False:
237
        slab_molec_cutoffs = natural_cutoffs(slab_molec, mult=coll_coeff)
238
        slab_molec_nghbs = len(
239
            neighbor_list("i", slab_molec, slab_molec_cutoffs))
240
        if slab_molec_nghbs > nn_slab + nn_molec:
241
            return True
242

    
243
    return False
244

    
245

    
246
def correct_coll(molec, slab, ctr_coord, site_coord, num_pts,
247
                 min_coll_height, norm_vect, slab_nghbs, molec_nghbs,
248
                 coll_coeff, height=2.5):
249
    # TODO Rethink this function
250
    """Tries to adsorb a molecule on a slab trying to avoid collisions by doing
251
    small rotations.
252

253
    @param molec: ase.Atoms object of the molecule to adsorb
254
    @param slab: ase.Atoms object of the surface on which to adsorb the
255
        molecule
256
    @param ctr_coord: The coordinates of the molecule to use as adsorption
257
        center.
258
    @param site_coord: The coordinates of the surface on which to adsorb the
259
        molecule
260
    @param num_pts: Number on which to sample Euler angles.
261
    @param min_coll_height: The lowermost height for which to detect a collision
262
    @param norm_vect: The vector perpendicular to the surface.
263
    @param slab_nghbs: Number of neigbors in the surface.
264
    @param molec_nghbs: Number of neighbors in the molecule.
265
    @param coll_coeff: The coefficient that multiplies the covalent radius of
266
        atoms resulting in a distance that two atoms being closer to that is
267
        considered as atomic collision.
268
    @param height: Height on which to try adsorption
269
    @return collision: bool, whether the structure generated has collisions
270
        between slab and adsorbate.
271
    """
272
    from copy import deepcopy
273
    slab_num_atoms = len(slab)
274
    slab_molec = []
275
    collision = True
276
    max_corr = 6  # Should be an even number
277
    d_angle = 180 / ((max_corr / 2.0) * num_pts)
278
    num_corr = 0
279
    while collision and num_corr <= max_corr:
280
        k = num_corr * (-1) ** num_corr
281
        slab_molec = deepcopy(slab)
282
        molec.euler_rotate(k * d_angle, k * d_angle / 2, k * d_angle,
283
                           center=ctr_coord)
284
        add_adsorbate(slab_molec, molec, site_coord, ctr_coord, height,
285
                      norm_vect=norm_vect)
286
        collision = check_collision(slab_molec, slab_num_atoms, min_coll_height,
287
                                    norm_vect, slab_nghbs, molec_nghbs,
288
                                    coll_coeff)
289
        num_corr += 1
290
    return slab_molec, collision
291

    
292

    
293
def dissociate_h(slab_molec_orig, h_idx, num_atoms_slab, h_acceptor,
294
                 neigh_cutoff=1):
295
    # TODO rethink
296
    """Tries to dissociate a H from the molecule and adsorbs it on the slab.
297

298
    Tries to dissociate a H atom from the molecule and adsorb in on top of the
299
    surface if the distance is shorter than two times the neigh_cutoff value.
300
    @param slab_molec_orig: The ase.Atoms object of the system adsorbate-slab.
301
    @param h_idx: The index of the hydrogen atom to carry out adsorption of.
302
    @param num_atoms_slab: The number of atoms of the slab without adsorbate.
303
    @param h_acceptor: List of atom types or atom numbers that are H-acceptors.
304
    @param neigh_cutoff: half the maximum distance between the surface and the
305
        H for it to carry out dissociation.
306
    @return: An ase.Atoms object of the system adsorbate-surface with H
307
    """
308
    from copy import deepcopy
309
    from ase.neighborlist import NeighborList
310
    slab_molec = deepcopy(slab_molec_orig)
311
    cutoffs = len(slab_molec) * [neigh_cutoff]
312
    nl = NeighborList(cutoffs, self_interaction=False, bothways=True, skin=0.0)
313
    nl.update(slab_molec)
314
    surf_h_vect = np.array([np.infty] * 3)
315
    if h_acceptor == 'all':
316
        h_acceptor = list(range(num_atoms_slab))
317
    for neigh_idx in nl.get_neighbors(h_idx)[0]:
318
        for elm in h_acceptor:
319
            if isinstance(elm, int):
320
                if neigh_idx == elm and neigh_idx < num_atoms_slab:
321
                    dist = np.linalg.norm(slab_molec[neigh_idx].position -
322
                                          slab_molec[h_idx].position)
323
                    if dist < np.linalg.norm(surf_h_vect):
324
                        surf_h_vect = slab_molec[neigh_idx].position \
325
                                      - slab_molec[h_idx].position
326
            else:
327
                if slab_molec[neigh_idx].symbol == elm \
328
                        and neigh_idx < num_atoms_slab:
329
                    dist = np.linalg.norm(slab_molec[neigh_idx].position -
330
                                          slab_molec[h_idx].position)
331
                    if dist < np.linalg.norm(surf_h_vect):
332
                        surf_h_vect = slab_molec[neigh_idx].position \
333
                                      - slab_molec[h_idx].position
334

    
335
    if np.linalg.norm(surf_h_vect) != np.infty:
336
        trans_vect = surf_h_vect - surf_h_vect / np.linalg.norm(surf_h_vect)
337
        slab_molec[h_idx].position = slab_molec[h_idx].position + trans_vect
338
        return slab_molec
339

    
340

    
341
def dissociation(slab_molec, h_donor, h_acceptor, num_atoms_slab):
342
    # TODO multiple dissociation
343
    """Decides which H atoms to dissociate according to a list of atoms.
344

345
    Given a list of chemical symbols or atom indices it checks for every atom
346
    or any of its neighbor if it's a H and calls dissociate_h to try to carry
347
    out dissociation of that H. For atom indices, it checks both whether
348
    the atom index or its neighbors are H, for chemical symbols, it only checks
349
    if there is a neighbor H.
350
    @param slab_molec: The ase.Atoms object of the system adsorbate-slab.
351
    @param h_donor: List of atom types or atom numbers that are H-donors.
352
    @param h_acceptor: List of atom types or atom numbers that are H-acceptors.
353
    @param num_atoms_slab: Number of atoms of the bare slab.
354
    @return:
355
    """
356
    from ase.neighborlist import natural_cutoffs, NeighborList
357
    molec = slab_molec[num_atoms_slab:]
358
    cutoffs = natural_cutoffs(molec)
359
    nl = NeighborList(cutoffs, self_interaction=False, bothways=True)
360
    nl.update(molec)
361
    disso_structs = []
362
    for el in h_donor:
363
        if isinstance(el, int):
364
            if molec[el].symbol == 'H':
365
                disso_struct = dissociate_h(slab_molec, el + num_atoms_slab,
366
                                            num_atoms_slab, h_acceptor)
367
                if disso_struct is not None:
368
                    disso_structs.append(disso_struct)
369
            else:
370
                for neigh_idx in nl.get_neighbors(el)[0]:
371
                    if molec[neigh_idx].symbol == 'H':
372
                        disso_struct = dissociate_h(slab_molec, neigh_idx +
373
                                                    num_atoms_slab,
374
                                                    num_atoms_slab, h_acceptor)
375
                        if disso_struct is not None:
376
                            disso_structs.append(disso_struct)
377
        else:
378
            for atom in molec:
379
                if atom.symbol.lower() == el.lower():
380
                    for neigh_idx in nl.get_neighbors(atom.index)[0]:
381
                        if molec[neigh_idx].symbol == 'H':
382
                            disso_struct = dissociate_h(slab_molec, neigh_idx
383
                                                        + num_atoms_slab,
384
                                                        num_atoms_slab,
385
                                                        h_acceptor)
386
                            if disso_struct is not None:
387
                                disso_structs.append(disso_struct)
388
    return disso_structs
389

    
390

    
391
def ads_euler(orig_molec, slab, ctr_coord, site_coord, num_pts,
392
              min_coll_height, coll_coeff, norm_vect, slab_nghbs, molec_nghbs,
393
              h_donor, h_acceptor):
394
    """Generates adsorbate-surface structures by sampling over Euler angles.
395

396
    This function generates a number of adsorbate-surface structures at
397
    different orientations of the adsorbate sampled at multiple Euler (zxz)
398
    angles.
399
    @param orig_molec: ase.Atoms object of the molecule to adsorb.
400
    @param slab: ase.Atoms object of the surface on which to adsorb the
401
        molecule.
402
    @param ctr_coord: The coordinates of the molecule to use as adsorption
403
        center.
404
    @param site_coord: The coordinates of the surface on which to adsorb the
405
        molecule.
406
    @param num_pts: Number on which to sample Euler angles.
407
    @param min_coll_height: The lowest height for which to detect a collision.
408
    @param coll_coeff: The coefficient that multiplies the covalent radius of
409
        atoms resulting in a distance that two atoms being closer to that is
410
        considered as atomic collision.
411
    @param norm_vect: The vector perpendicular to the surface.
412
    @param slab_nghbs: Number of neigbors in the surface.
413
    @param molec_nghbs: Number of neighbors in the molecule.
414
    @param h_donor: List of atom types or atom numbers that are H-donors.
415
    @param h_acceptor: List of atom types or atom numbers that are H-acceptors.
416
    @return: list of ase.Atoms object conatining all the orientations of a given
417
        conformer.
418
    """
419
    from copy import deepcopy
420
    slab_ads_list = []
421
    # rotation around z
422
    for alpha in np.arange(0, 360, 360 / num_pts):
423
        # rotation around x'
424
        for beta in np.arange(0, 180, 180 / num_pts):
425
            # rotation around z'
426
            for gamma in np.arange(0, 360, 360 / num_pts):
427
                molec = deepcopy(orig_molec)
428
                molec.euler_rotate(alpha, beta, gamma, center=ctr_coord)
429
                slab_molec, collision = correct_coll(molec, slab,
430
                                                     ctr_coord, site_coord,
431
                                                     num_pts, min_coll_height,
432
                                                     norm_vect,
433
                                                     slab_nghbs, molec_nghbs,
434
                                                     coll_coeff)
435
                if not collision and not any([np.allclose(slab_molec.positions,
436
                                                          conf.positions)
437
                                              for conf in slab_ads_list]):
438
                    slab_ads_list.append(slab_molec)
439
                    if h_donor is not False:
440
                        slab_ads_list.extend(dissociation(slab_molec, h_donor,
441
                                                          h_acceptor,
442
                                                          len(slab)))
443

    
444
    return slab_ads_list
445

    
446

    
447
def chemcat_rotate(molecule, surf, ctr1_mol, ctr2_mol, ctr3_mol, ctr1_surf,
448
                   ctr2_surf, bond_vector, bond_angle_target,
449
                   dihedral_angle_target=None, mol_dihedral_angle_target=None):
450
    """Performs translation and rotation of an adsorbate defined by an
451
    adsorption bond length, direction, angle and dihedral angle
452

453
    Carles modification of chemcat's transform_adsorbate to work with
454
    coordinates instead of ase.Atom
455
    Parameters:
456
        molecule (ase.Atoms): The molecule to adsorb.
457

458
        surf (ase.Atoms): The surface ontop of which to adsorb.
459

460
        ctr1_mol (int/list): The position of the adsorption center in the
461
        molecule that will be bound to the surface.
462

463
        ctr2_mol (int/list): The position of a second center of the
464
        adsorbate used to define the adsorption bond angle, and the dihedral
465
        adsorption angle.
466

467
        ctr3_mol (int/list): The position of a third center in the
468
        adsorbate used to define the adsorbate dihedral angle.
469

470
        ctr1_surf (int/list): The position of the site on the surface that
471
        will be bound to the molecule.
472

473
        ctr2_surf (int/list): The position of a second center of the
474
        surface used to define the dihedral adsorption angle.
475

476
        bond_vector (numpy.ndarray): The adsorption bond desired.
477
            Details: offset = vect(atom1_surf, atom1_mol)
478

479
        bond_angle_target (float or int): The adsorption bond angle desired (in
480
            degrees).
481
            Details: bond_angle_target = angle(atom1_surf, atom1_mol, atom2_mol)
482

483
        dihedral_angle_target (float or int): The dihedral adsorption angle
484
            desired (in degrees).
485
            Details: dihedral_angle_target = dihedral(atom2_surf, atom1_surf,
486
            atom1_mol, atom2_mol)
487
                The rotation vector is facing the adsorbate from the surface
488
                (i.e. counterclockwise rotation when facing the surface (i.e.
489
                view from top))
490

491
        mol_dihedral_angle_target (float or int): The adsorbate dihedral angle
492
            desired (in degrees).
493
            Details: mol_dihedral_angle_target = dihedral(atom1_surf, atom1_mol,
494
            atom2_mol, atom3_mol)
495
                The rotation vector is facing atom2_mol from atom1_mol
496

497
    Returns:
498
        None (the `molecule` object is modified in-place)
499
    """
500
    vect_surf = get_atom_coords(surf, ctr2_surf) - get_atom_coords(surf,
501
                                                                   ctr1_surf)
502
    vect_inter = get_atom_coords(molecule, ctr1_mol) - get_atom_coords(surf,
503
                                                                       ctr1_surf)
504
    vect_mol = get_atom_coords(molecule, ctr2_mol) - get_atom_coords(molecule,
505
                                                                     ctr1_mol)
506
    vect2_mol = get_atom_coords(molecule, ctr3_mol) - get_atom_coords(molecule,
507
                                                                      ctr2_mol)
508

    
509
    # Check if dihedral angles can be defined
510
    do_dihedral = dihedral_angle_target is not None
511
    do_mol_dihedral = mol_dihedral_angle_target is not None
512
    dihedral_use_mol2 = False
513
    # Check if vect_surf and vect_inter are not aligned
514
    if np.allclose(np.cross(vect_surf, vect_inter), 0):
515
        logger.warning(
516
            "Surface atoms are incompatible with adsorption "
517
            "direction/bond. An adsorption dihedral angle cannot be defined.")
518
        do_dihedral = False
519
    # Check if requested bond angle is not flat
520
    if np.isclose((bond_angle_target + 90) % 180 - 90, 0):
521
        logger.warning("Requested bond angle is flat. Only a single dihedral "
522
                       "angle can be defined (ctr2_surf, ctr1_surf, ctr2_mol, "
523
                       "ctr3_mol).")
524
        do_mol_dihedral = False
525
        dihedral_use_mol2 = True
526
        logger.warning("Dihedral adsorption angle rotation will be perfomed "
527
                       "with (ctr2_surf, ctr1_surf, ctr2_mol, ctr3_mol).")
528
    # Check if vect_mol and vect2_mol are not aligned
529
    if np.allclose(np.cross(vect_mol, vect2_mol), 0):
530
        logger.warning("Adsorbates atoms are aligned. An adsorbate dihedral "
531
                       "angle cannot be defined.")
532
        do_mol_dihedral = False
533
    if not do_dihedral:
534
        logger.warning("Dihedral adsorption angle rotation will not be "
535
                       "performed.")
536
    if not do_mol_dihedral:
537
        logger.warning("Adsorbate dihedral angle rotation will not be "
538
                       "performed.")
539

    
540
    ###########################
541
    #       Translation       #
542
    ###########################
543

    
544
    # Compute and apply translation of adsorbate
545
    translation = bond_vector - vect_inter
546
    molecule.translate(translation)
547

    
548
    # Update adsorption bond
549
    vect_inter = get_atom_coords(molecule, ctr1_mol) - get_atom_coords(surf,
550
                                                                       ctr1_surf)
551

    
552
    # Check if translation was successful
553
    if np.allclose(vect_inter, bond_vector):
554
        pass  # print("Translation successfully applied (error: ~ {:.5g} unit "
555
        # "length)".format(np.linalg.norm(vect_inter - bond_vector)))
556
    else:
557
        err = 'An unknown error occured during the translation'
558
        logger.error(err)
559
        raise AssertionError(err)
560

    
561
    ###########################
562
    #   Bond angle rotation   #
563
    ###########################
564

    
565
    # Compute rotation vector
566
    rotation_vector = np.cross(-vect_inter, vect_mol)
567
    if np.allclose(rotation_vector, 0, atol=1e-5):
568
        # If molecular bonds are aligned, any vector orthogonal to vect_inter
569
        # can be used Such vector can be found as the orthogonal rejection of
570
        # either X-axis, Y-axis or Z-axis onto vect_inter (since they cannot
571
        # be all aligned)
572
        non_aligned_vector = np.zeros(3)
573
        # Select the most orthogonal axis (lowest dot product):
574
        non_aligned_vector[np.argmin(np.abs(vect_inter))] = 1
575
        rotation_vector = non_aligned_vector - np.dot(non_aligned_vector,
576
                                                      vect_inter) / np.dot(
577
            vect_inter, vect_inter) * vect_inter
578

    
579
    # Retrieve current bond angle
580
    bond_angle_ini = get_vect_angle(-vect_inter, vect_mol, rotation_vector)
581

    
582
    # Apply rotation to reach desired bond_angle
583
    molecule.rotate(bond_angle_target - bond_angle_ini, v=rotation_vector,
584
                    center=get_atom_coords(molecule, ctr1_mol))
585

    
586
    # Update molecular bonds
587
    vect_mol = get_atom_coords(molecule, ctr2_mol) - get_atom_coords(molecule,
588
                                                                     ctr1_mol)
589
    vect2_mol = get_atom_coords(molecule, ctr3_mol) - get_atom_coords(molecule,
590
                                                                      ctr2_mol)
591

    
592
    # Check if rotation was successful
593
    bond_angle = get_vect_angle(-vect_inter, vect_mol)
594
    if np.isclose((bond_angle - bond_angle_target + 90) % 180 - 90, 0,
595
                  atol=1e-3) and np.allclose(get_atom_coords(molecule, ctr1_mol)
596
                                             - get_atom_coords(surf,
597
                                                               ctr1_surf),
598
                                             vect_inter):
599
        pass  # print("Rotation successfully applied (error: {:.5f}°)".format(
600
        # (bond_angle - bond_angle_target + 90) % 180 - 90))
601
    else:
602
        err = 'An unknown error occured during the rotation'
603
        logger.error(err)
604
        raise AssertionError(err)
605

    
606
    ###########################
607
    # Dihedral angle rotation #
608
    ###########################
609

    
610
    # Perform dihedral rotation if possible
611
    if do_dihedral:
612
        # Retrieve current dihedral angle (by computing the angle between the
613
        # vector rejection of vect_surf and vect_mol onto vect_inter)
614
        vect_inter_inner = np.dot(vect_inter, vect_inter)
615
        vect_surf_reject = vect_surf - np.dot(vect_surf, vect_inter) / \
616
                           vect_inter_inner * vect_inter
617
        if dihedral_use_mol2:
618
            vect_mol_reject = vect2_mol - np.dot(vect2_mol, vect_inter) / \
619
                              vect_inter_inner * vect_inter
620
        else:
621
            vect_mol_reject = vect_mol - np.dot(vect_mol, vect_inter) / \
622
                              vect_inter_inner * vect_inter
623
        dihedral_angle_ini = get_vect_angle(vect_surf_reject, vect_mol_reject,
624
                                            vect_inter)
625

    
626
        # Apply dihedral rotation along vect_inter
627
        molecule.rotate(dihedral_angle_target - dihedral_angle_ini,
628
                        v=vect_inter, center=get_atom_coords(molecule,
629
                                                             ctr1_mol))
630

    
631
        # Update molecular bonds
632
        vect_mol = get_atom_coords(molecule, ctr2_mol) - \
633
                   get_atom_coords(molecule, ctr1_mol)
634
        vect2_mol = get_atom_coords(molecule, ctr3_mol) - \
635
                    get_atom_coords(molecule, ctr2_mol)
636

    
637
        # Check if rotation was successful
638
        # Check dihedral rotation
639
        if dihedral_use_mol2:
640
            vect_mol_reject = vect2_mol - np.dot(vect2_mol, vect_inter) / \
641
                              vect_inter_inner * vect_inter
642
        else:
643
            vect_mol_reject = vect_mol - np.dot(vect_mol, vect_inter) / \
644
                              vect_inter_inner * vect_inter
645
        dihedral_angle = get_vect_angle(vect_surf_reject, vect_mol_reject,
646
                                        vect_inter)
647
        # Check bond rotation is unmodified
648
        bond_angle = get_vect_angle(-vect_inter, vect_mol)
649
        if np.isclose((dihedral_angle - dihedral_angle_target + 90) % 180 - 90,
650
                      0, atol=1e-3) \
651
                and np.isclose((bond_angle - bond_angle_target + 90) %
652
                               180 - 90, 0, atol=1e-5) \
653
                and np.allclose(get_atom_coords(molecule, ctr1_mol)
654
                                - get_atom_coords(surf, ctr1_surf),
655
                                vect_inter):
656
            pass  # print( "Dihedral rotation successfully applied (error: {
657
            # :.5f}°)".format((dihedral_angle - dihedral_angle_target + 90) %
658
            # 180 - 90))
659
        else:
660
            err = 'An unknown error occured during the dihedral rotation'
661
            logger.error(err)
662
            raise AssertionError(err)
663

    
664
    #####################################
665
    # Adsorbate dihedral angle rotation #
666
    #####################################
667

    
668
    # Perform adsorbate dihedral rotation if possible
669
    if do_mol_dihedral:
670
        # Retrieve current adsorbate dihedral angle (by computing the angle
671
        # between the orthogonal rejection of vect_inter and vect2_mol onto
672
        # vect_mol)
673
        vect_mol_inner = np.dot(vect_mol, vect_mol)
674
        bond_inter_reject = -vect_inter - np.dot(-vect_inter, vect_mol) / \
675
            vect_mol_inner * vect_mol
676
        bond2_mol_reject = vect2_mol - np.dot(vect2_mol, vect_mol) / \
677
            vect_mol_inner * vect_mol
678
        dihedral_angle_ini = get_vect_angle(bond_inter_reject,
679
                                            bond2_mol_reject, vect_mol)
680

    
681
        # Apply dihedral rotation along vect_mol
682
        molecule.rotate(mol_dihedral_angle_target - dihedral_angle_ini,
683
                        v=vect_mol, center=get_atom_coords(molecule, ctr1_mol))
684

    
685
        # Update molecular bonds
686
        vect_mol = get_atom_coords(molecule, ctr2_mol) \
687
            - get_atom_coords(molecule, ctr1_mol)
688
        vect2_mol = get_atom_coords(molecule, ctr3_mol) \
689
            - get_atom_coords(molecule, ctr2_mol)
690

    
691
        # Check if rotation was successful
692
        # Check adsorbate dihedral rotation
693
        vect_mol_inner = np.dot(vect_mol, vect_mol)
694
        bond2_mol_reject = vect2_mol - np.dot(vect2_mol, vect_mol) / \
695
            vect_mol_inner * vect_mol
696
        mol_dihedral_angle = get_vect_angle(bond_inter_reject,
697
                                            bond2_mol_reject, vect_mol)
698
        # Check dihedral rotation
699
        vect_inter_inner = np.dot(vect_inter, vect_inter)
700
        vect_surf_reject = vect_surf - np.dot(vect_surf, vect_inter) / \
701
            vect_inter_inner * vect_inter
702
        vect_mol_reject = vect_mol - np.dot(vect_mol, vect_inter) / \
703
            vect_inter_inner * vect_inter
704
        dihedral_angle = get_vect_angle(vect_surf_reject, vect_mol_reject,
705
                                        vect_inter)
706
        # Check bond rotation is unmodified
707
        bond_angle = get_vect_angle(-vect_inter, vect_mol)
708
        if np.isclose((mol_dihedral_angle - mol_dihedral_angle_target + 90) %
709
                      180 - 90, 0, atol=1e-3) \
710
                and np.isclose((dihedral_angle -
711
                                dihedral_angle_target + 90) % 180 - 90, 0,
712
                               atol=1e-5) \
713
                and np.isclose((bond_angle - bond_angle_target + 90) % 180 - 90,
714
                               0, atol=1e-5) \
715
                and np.allclose(get_atom_coords(molecule, ctr1_mol) -
716
                                get_atom_coords(surf, ctr1_surf),
717
                                vect_inter):
718
            pass  # print(
719
            # "Adsorbate dihedral rotation successfully applied (error:
720
            # {:.5f}°)".format((mol_dihedral_angle - mol_dihedral_angle_target
721
            # + 90) % 180 - 90))
722
        else:
723
            err = 'An unknown error occured during the adsorbate dihedral ' \
724
                  'rotation'
725
            logger.error(err)
726
            raise AssertionError(err)
727

    
728

    
729
def ads_chemcat(orig_molec, slab, ctr1_mol, ctr2_mol, ctr3_mol, ctr1_surf,
730
                ctr2_surf, num_pts, min_coll_height, coll_coeff, norm_vect,
731
                slab_nghbs, molec_nghbs, h_donor, h_acceptor, max_hel):
732
    """Generates adsorbate-surface structures by sampling over chemcat angles.
733

734
    @param orig_molec: ase.Atoms object of the molecule to adsorb (adsorbate).
735
    @param slab: ase.Atoms object of the surface on which to adsorb the molecule
736
    @param ctr1_mol: The index/es of the center in the adsorbate to use as
737
        adsorption center.
738
    @param ctr2_mol: The index/es of the center in the adsorbate to use for the
739
        definition of the surf-adsorbate angle, surf-adsorbate dihedral angle
740
        and adsorbate dihedral angle.
741
    @param ctr3_mol: The index/es of the center in the adsorbate to use for the
742
        definition of the adsorbate dihedral angle.
743
    @param ctr1_surf: The index/es of the center in the surface to use as
744
        adsorption center.
745
    @param ctr2_surf: The index/es of the center in the surface to use for the
746
        definition of the surf-adsorbate dihedral angle.
747
    @param num_pts: Number on which to sample Euler angles.
748
    @param min_coll_height: The lowest height for which to detect a collision
749
    @param coll_coeff: The coefficient that multiplies the covalent radius of
750
        atoms resulting in a distance that two atoms being closer to that is
751
        considered as atomic collision.
752
    @param norm_vect: The vector perpendicular to the surface.
753
    @param slab_nghbs: Number of neigbors in the surface.
754
    @param molec_nghbs: Number of neighbors in the molecule.
755
    @param h_donor: List of atom types or atom numbers that are H-donors.
756
    @param h_acceptor: List of atom types or atom numbers that are H-acceptors.
757
    @param max_hel: Maximum value for sampling the helicopter
758
        (surf-adsorbate dihedral) angle.
759
    @return: list of ase.Atoms object conatining all the orientations of a given
760
        conformer.
761
    """
762
    from copy import deepcopy
763
    slab_ads_list = []
764
    # Rotation over bond angle
765
    for alpha in np.arange(90, 180, 90 / min(num_pts, 2)):
766
        # Rotation over surf-adsorbate dihedral angle
767
        for beta in np.arange(0, max_hel, max_hel / num_pts):
768
            # Rotation over adsorbate bond dihedral angle
769
            for gamma in np.arange(0, 360, 360 / num_pts):
770
                new_molec = deepcopy(orig_molec)
771
                chemcat_rotate(new_molec, slab, ctr1_mol, ctr2_mol, ctr3_mol,
772
                               ctr1_surf, ctr2_surf, norm_vect, alpha,
773
                               beta, gamma)
774
                site_coords = get_atom_coords(slab, ctr1_surf)
775
                ctr_coords = get_atom_coords(new_molec, ctr1_mol)
776
                slab_molec, collision = correct_coll(new_molec, slab,
777
                                                     ctr_coords, site_coords,
778
                                                     num_pts, min_coll_height,
779
                                                     norm_vect, slab_nghbs,
780
                                                     molec_nghbs, coll_coeff)
781
                if not collision and \
782
                        not any([np.allclose(slab_molec.positions,
783
                                             conf.positions)
784
                                 for conf in slab_ads_list]):
785
                    slab_ads_list.append(slab_molec)
786
                    if h_donor is not False:
787
                        slab_ads_list.extend(dissociation(slab_molec, h_donor,
788
                                                          h_acceptor,
789
                                                          len(slab)))
790

    
791
    return slab_ads_list
792

    
793

    
794
def adsorb_confs(conf_list, surf, inp_vars):
795
    """Generates a number of adsorbate-surface structure coordinates.
796

797
    Given a list of conformers, a surface, a list of atom indices (or list of
798
    list of indices) of both the surface and the adsorbate, it generates a
799
    number of adsorbate-surface structures for every possible combination of
800
    them at different orientations.
801
    @param conf_list: list of ase.Atoms of the different conformers
802
    @param surf: the ase.Atoms object of the surface
803
    @param inp_vars: Calculation parameters from input file.
804
    @return: list of ase.Atoms for the adsorbate-surface structures
805
    """
806
    from ase.neighborlist import natural_cutoffs, neighbor_list
807
    molec_ctrs = inp_vars['molec_ctrs']
808
    sites = inp_vars['sites']
809
    angles = inp_vars['set_angles']
810
    num_pts = inp_vars['sample_points_per_angle']
811
    norm_vect = inp_vars['surf_norm_vect']
812
    min_coll_height = inp_vars['min_coll_height']
813
    coll_coeff = inp_vars['collision_threshold']
814
    h_donor = inp_vars['h_donor']
815
    h_acceptor = inp_vars['h_acceptor']
816

    
817
    if inp_vars['pbc_cell'] is not False:
818
        surf.set_pbc(True)
819
        surf.set_cell(inp_vars['pbc_cell'])
820

    
821
    surf_ads_list = []
822
    sites_coords = get_atom_coords(surf, sites)
823
    if coll_coeff is not False:
824
        surf_cutoffs = natural_cutoffs(surf, mult=coll_coeff)
825
        surf_nghbs = len(neighbor_list("i", surf, surf_cutoffs))
826
    else:
827
        surf_nghbs = 0
828
    for i, conf in enumerate(conf_list):
829
        molec_ctr_coords = get_atom_coords(conf, molec_ctrs)
830
        if inp_vars['pbc_cell'] is not False:
831
            conf.set_pbc(True)
832
            conf.set_cell(inp_vars['pbc_cell'])
833
        if coll_coeff is not False:
834
            conf_cutoffs = natural_cutoffs(conf, mult=coll_coeff)
835
            molec_nghbs = len(neighbor_list("i", conf, conf_cutoffs))
836
        else:
837
            molec_nghbs = 0
838
        for s, site in enumerate(sites_coords):
839
            for c, ctr in enumerate(molec_ctr_coords):
840
                if angles == 'euler':
841
                    surf_ads_list.extend(ads_euler(conf, surf, ctr, site,
842
                                                   num_pts, min_coll_height,
843
                                                   coll_coeff, norm_vect,
844
                                                   surf_nghbs, molec_nghbs,
845
                                                   h_donor, h_acceptor))
846
                elif angles == 'chemcat':
847
                    mol_ctr1 = molec_ctrs[c]
848
                    mol_ctr2 = inp_vars["molec_ctrs2"][c]
849
                    mol_ctr3 = inp_vars["molec_ctrs3"][c]
850
                    surf_ctr1 = sites[s]
851
                    surf_ctr2 = inp_vars["surf_ctrs2"][s]
852
                    max_h = inp_vars["max_helic_angle"]
853
                    surf_ads_list.extend(ads_chemcat(conf, surf, mol_ctr1,
854
                                                     mol_ctr2, mol_ctr3,
855
                                                     surf_ctr1, surf_ctr2,
856
                                                     num_pts, min_coll_height,
857
                                                     coll_coeff, norm_vect,
858
                                                     surf_nghbs, molec_nghbs,
859
                                                     h_donor, h_acceptor,
860
                                                     max_h))
861
    return surf_ads_list
862

    
863

    
864
def run_screening(inp_vars):
865
    """Carries out the screening of adsorbate structures on a surface.
866

867
    @param inp_vars: Calculation parameters from input file.
868
    """
869
    import os
870
    import random
871
    from modules.formats import collect_coords, adapt_format
872
    from modules.calculation import run_calc, check_finished_calcs
873

    
874
    logger.info('Carrying out procedures for the screening of adsorbate-surface'
875
                ' structures.')
876
    if not os.path.isdir("isolated"):
877
        err = "'isolated' directory not found. It is needed in order to carry "
878
        "out the screening of structures to be adsorbed"
879
        logger.error(err)
880
        raise FileNotFoundError(err)
881

    
882
    finished_calcs, unfinished_calcs = check_finished_calcs('isolated',
883
                                                            inp_vars['code'])
884
    logger.info(f"Found {len(finished_calcs)} structures of isolated "
885
                f"conformers whose calculation finished normally.")
886
    if len(unfinished_calcs) != 0:
887
        logger.warning(f"Found {len(unfinished_calcs)} calculations more that "
888
                       f"did not finish normally: {unfinished_calcs}. \n"
889
                       f"Using only the ones that finished normally: "
890
                       f"{finished_calcs}.")
891

    
892
    conformer_atoms_list = collect_coords(finished_calcs, inp_vars['code'],
893
                                          'isolated', inp_vars['special_atoms'])
894
    selected_confs = select_confs(conformer_atoms_list, finished_calcs,
895
                                  inp_vars['select_magns'],
896
                                  inp_vars['confs_per_magn'],
897
                                  inp_vars['code'])
898
    surf = adapt_format('ase', inp_vars['surf_file'], inp_vars['special_atoms'])
899
    surf_ads_list = adsorb_confs(selected_confs, surf, inp_vars)
900
    if len(surf_ads_list) > inp_vars['max_structures']:
901
        surf_ads_list = random.sample(surf_ads_list, inp_vars['max_structures'])
902
    logger.info(f'Generated {len(surf_ads_list)} adsorbate-surface atomic '
903
                f'configurations to carry out a calculation of.')
904

    
905
    run_calc('screening', inp_vars, surf_ads_list)
906
    logger.info('Finished the procedures for the screening of adsorbate-surface'
907
                ' structures section.')