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

dockonsurf / modules / screening.py @ 1d8c374e

Historique | Voir | Annoter | Télécharger (49,31 ko)

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

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

    
7

    
8
def select_confs(conf_list: list, magns: list, num_sel: int):
9
    """Takes a list ase.Atoms and selects the most different magnitude-wise.
10

11
    Given a list of ase.Atoms objects and a list of magnitudes, it selects a
12
    number of the most different conformers according to every magnitude
13
    specified.
14
     
15
    @param conf_list: list of ase.Atoms objects to select among.
16
    @param magns: list of str with the names of the magnitudes to use for the
17
        conformer selection. Supported magnitudes: 'energy', 'moi'.
18
    @param num_sel: number of conformers to select for every of the magnitudes.
19
    @return: list of the selected ase.Atoms objects.
20
    """
21
    selected_ids = []
22
    if num_sel >= len(conf_list):
23
        logger.warning('Number of conformers per magnitude is equal or larger '
24
                       'than the total number of conformers. Using all '
25
                       f'available conformers: {len(conf_list)}.')
26
        return conf_list
27

    
28
    # Assign mois
29
    if 'moi' in magns:
30
        for conf in conf_list:
31
            conf.info["moi"] = conf.get_moments_of_inertia()[2]
32

    
33
    # pick ids
34
    for magn in magns:
35
        sorted_list = sorted(conf_list, key=lambda conf: abs(conf.info[magn]))
36
        if sorted_list[-1].info['iso'] not in selected_ids:
37
            selected_ids.append(sorted_list[-1].info['iso'])
38
        if num_sel > 1:
39
            for i in range(0, len(sorted_list) - 1,
40
                           len(conf_list) // (num_sel - 1)):
41
                if sorted_list[i].info['iso'] not in selected_ids:
42
                    selected_ids.append(sorted_list[i].info['iso'])
43

    
44
    logger.info(f'Selected {len(selected_ids)} conformers for adsorption.')
45
    return [conf for conf in conf_list if conf.info["iso"] in selected_ids]
46

    
47

    
48
def get_vect_angle(v1: list, v2: list, ref=None, degrees=True):
49
    """Computes the angle between two vectors.
50

51
    @param v1: The first vector.
52
    @param v2: The second vector.
53
    @param ref: Orthogonal vector to both v1 and v2,
54
        along which the sign of the rotation is defined (i.e. positive if
55
        counterclockwise angle when facing ref)
56
    @param degrees: Whether the result should be in radians (True) or in
57
        degrees (False).
58
    @return: The angle in radians if degrees = False, or in degrees if
59
        degrees =True
60
    """
61
    v1_u = v1 / np.linalg.norm(v1)
62
    v2_u = v2 / np.linalg.norm(v2)
63
    angle = np.arccos(np.clip(np.dot(v1_u, v2_u), -1.0, 1.0))
64
    if ref is not None:
65
        # Give sign according to ref direction
66
        angle *= (1 if np.dot(np.cross(v1, v2), ref) >= 0 else -1)
67

    
68
    return angle if not degrees else angle * 180 / np.pi
69

    
70

    
71
def vect_avg(vects):
72
    """Computes the element-wise mean of a set of vectors.
73

74
    @param vects: list of lists-like: containing the vectors (num_vectors,
75
        length_vector).
76
    @return: vector average computed doing the element-wise mean.
77
    """
78
    from modules.utilities import try_command
79
    err = "vect_avg parameter vects must be a list-like, able to be converted" \
80
          " np.array"
81
    array = try_command(np.array, [(ValueError, err)], vects)
82
    if len(array.shape) == 1:
83
        return array
84
    else:
85
        num_vects = array.shape[1]
86
        return np.array([np.average(array[:, i]) for i in range(num_vects)])
87

    
88

    
89
def get_atom_coords(atoms: ase.Atoms, center=None):
90
    """Gets the coordinates of the specified center for an ase.Atoms object.
91

92
    If center is not an index but a list of indices, it computes the
93
    element-wise mean of the coordinates of the atoms specified in the inner
94
    list.
95
    @param atoms: ase.Atoms object for which to obtain the coordinates of.
96
    @param center: index/list of indices of the atoms for which the coordinates
97
                   should be extracted.
98
    @return: np.ndarray of atomic coordinates.
99
    """
100
    err_msg = "Argument 'ctr' must be an integer or a list of integers. "\
101
              "Every integer must be in the range [0, num_atoms)"
102
    if center is None:
103
        center = list(range(len(atoms)))
104
    if isinstance(center, int):
105
        if center not in list(range(len(atoms))):
106
            logger.error(err_msg)
107
            raise ValueError(err_msg)
108
        return atoms[center].position
109
    elif isinstance(center, list):
110
        for elm in center:
111
            if elm not in list(range(len(atoms))):
112
                logger.error(err_msg)
113
                raise ValueError(err_msg)
114
        return vect_avg([atoms[idx].position for idx in center])
115
    else:
116
        logger.error(err_msg)
117
        raise ValueError(err_msg)
118

    
119

    
120
def compute_norm_vect(atoms, idxs, cell):
121
    """Computes the local normal vector of a surface at a given site.
122

123
    Given an ase.Atoms object and a site defined as a linear combination of
124
    atoms it computes the vector perpendicular to the surface, considering the
125
    local environment of the site.
126
    @param atoms: ase.Atoms object of the surface.
127
    @param idxs: list or int: Index or list of indices of the atom/s that define
128
        the site
129
    @param cell: Unit cell. A 3x3 matrix (the three unit cell vectors)
130
    @return: numpy.ndarray of the coordinates of the vector locally
131
    perpendicular to the surface.
132
    """
133
    from modules.ASANN import coordination_numbers as coord_nums
134
    if isinstance(idxs, list):
135
        atm_vect = [-np.round(coord_nums(atoms.get_scaled_positions(),
136
                                         pbc=np.any(cell),
137
                                         cell_vectors=cell)[3][i], 2)
138
                    for i in idxs]
139
        norm_vec = vect_avg([vect / np.linalg.norm(vect) for vect in atm_vect])
140
    elif isinstance(idxs, int):
141
        norm_vec = -coord_nums(atoms.get_scaled_positions(),
142
                               pbc=np.any(cell),
143
                               cell_vectors=cell)[3][idxs]
144
    else:
145
        err = "'idxs' must be either an int or a list"
146
        logger.error(err)
147
        raise ValueError(err)
148
    norm_vec = np.round(norm_vec, 2) / np.linalg.norm(np.round(norm_vec, 2))
149
    logger.info(f"The perpendicular vector to the surface at site '{idxs}' is "
150
                f"{norm_vec}")
151
    return norm_vec
152

    
153

    
154
def align_molec(orig_molec, ctr_coord, ref_vect):
155
    """Align a molecule to a vector by a center.
156

157
    Given a reference vector to be aligned to and some coordinates acting as
158
    alignment center, it first averages the vectors pointing to neighboring
159
    atoms and then tries to align this average vector to the target. If the
160
    average vector is 0 it takes the vector to the nearest neighbor.
161
    @param orig_molec: The molecule to align.
162
    @param ctr_coord: The coordinates to use ase alignment center.
163
    @param ref_vect: The vector to be aligned with.
164
    @return: ase.Atoms of the aligned molecule.
165
    """
166
    from copy import deepcopy
167
    from ase import Atom
168
    from ase.neighborlist import natural_cutoffs, neighbor_list
169

    
170
    molec = deepcopy(orig_molec)
171
    if len(molec) == 1:
172
        err_msg = "Cannot align a single atom"
173
        logger.error(err_msg)
174
        ValueError(err_msg)
175
    cutoffs = natural_cutoffs(molec, mult=1.2)
176
    # Check if ctr_coord are the coordinates of an atom and if not creates a
177
    # dummy one to extract the neighboring atoms.
178
    ctr_idx = None
179
    dummy_atom = False
180
    for atom in molec:
181
        if np.allclose(ctr_coord, atom.position, rtol=1e-2):
182
            ctr_idx = atom.index
183
            break
184
    if ctr_idx is None:
185
        molec.append(Atom('X', position=ctr_coord))
186
        cutoffs.append(0.2)
187
        ctr_idx = len(molec) - 1
188
        dummy_atom = True
189
    # Builds the neighbors and computes the average vector
190
    refs, vects = neighbor_list("iD", molec, cutoffs, self_interaction=False)
191
    neigh_vects = [vects[i] for i, atm in enumerate(refs) if atm == ctr_idx]
192
    # If no neighbors are present, the cutoff of the alignment center is
193
    # set to a value where at least one atom is a neighbor and neighbors are
194
    # recalculated.
195
    if len(neigh_vects) == 0:
196
        min_dist, min_idx = (np.inf, np.inf)
197
        for atom in molec:
198
            if atom.index == ctr_idx:
199
                continue
200
            if molec.get_distance(ctr_idx, atom.index) < min_dist:
201
                min_dist = molec.get_distance(ctr_idx, atom.index)
202
                min_idx = atom.index
203
        cutoffs[ctr_idx] = min_dist - cutoffs[min_idx] + 0.05
204
        refs, vects = neighbor_list("iD", molec, cutoffs,
205
                                    self_interaction=False)
206
        neigh_vects = [vects[i] for i, atm in enumerate(refs) if atm == ctr_idx]
207
    target_vect = vect_avg(neigh_vects)
208
    # If the target vector is 0 (the center is at the baricenter of its
209
    # neighbors). Assuming the adsorption center is coplanar or colinear to its
210
    # neighbors (it would not make a lot of sense to chose a center which is
211
    # the baricenter of neighbors distributed in 3D), the target_vector is
212
    # chosen perpendicular to the nearest neighbor.
213
    if np.allclose(target_vect, 0, rtol=1e-3):
214
        nn_vect = np.array([np.inf] * 3)
215
        for vect in neigh_vects:
216
            if np.linalg.norm(vect) < np.linalg.norm(nn_vect):
217
                nn_vect = vect
218
        cart_axes = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
219
        axis = cart_axes[int(np.argmax([np.linalg.norm(np.cross(ax, nn_vect))
220
                                        for ax in cart_axes]))]
221
        target_vect = np.cross(axis, nn_vect)
222

    
223
    rot_vect = np.cross(target_vect, ref_vect)
224
    if np.allclose(rot_vect, 0):
225
        cart_axes = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
226
        axis = cart_axes[int(np.argmax([np.linalg.norm(np.cross(ax, ref_vect))
227
                                        for ax in cart_axes]))]
228
        rot_vect = np.cross(ref_vect, axis)
229
    rot_angle = -get_vect_angle(ref_vect, target_vect, rot_vect)
230
    molec.rotate(rot_angle, rot_vect, ctr_coord)
231
    if dummy_atom:
232
        del molec[-1]
233
    return molec
234

    
235

    
236
def add_adsorbate(slab, adsorbate, site_coord, ctr_coord, height, offset=None,
237
                  norm_vect=(0, 0, 1)):
238
    """Add an adsorbate to a surface.
239

240
    This function extends the functionality of ase.build.add_adsorbate
241
    (https://wiki.fysik.dtu.dk/ase/ase/build/surface.html#ase.build.add_adsorbate)
242
    by enabling to change the z coordinate and the axis perpendicular to the
243
    surface.
244
    @param slab: ase.Atoms object containing the coordinates of the surface
245
    @param adsorbate: ase.Atoms object containing the coordinates of the
246
        adsorbate.
247
    @param site_coord: The coordinates of the adsorption site on the surface.
248
    @param ctr_coord: The coordinates of the adsorption center in the molecule.
249
    @param height: The height above the surface where to adsorb.
250
    @param offset: Offsets the adsorbate by a number of unit cells. Mostly
251
        useful when adding more than one adsorbate.
252
    @param norm_vect: The vector perpendicular to the surface.
253
    """
254
    from copy import deepcopy
255
    info = slab.info.get('adsorbate_info', {})
256
    pos = np.array([0.0, 0.0, 0.0])  # part of absolute coordinates
257
    spos = np.array([0.0, 0.0, 0.0])  # part relative to unit cell
258
    norm_vect_u = np.array(norm_vect) / np.linalg.norm(norm_vect)
259
    if offset is not None:
260
        spos += np.asarray(offset, float)
261
    if isinstance(site_coord, str):
262
        # A site-name:
263
        if 'sites' not in info:
264
            raise TypeError('If the atoms are not made by an ase.build '
265
                            'function, position cannot be a name.')
266
        if site_coord not in info['sites']:
267
            raise TypeError('Adsorption site %s not supported.' % site_coord)
268
        spos += info['sites'][site_coord]
269
    else:
270
        pos += site_coord
271
    if 'cell' in info:
272
        cell = info['cell']
273
    else:
274
        cell = slab.get_cell()
275
    pos += np.dot(spos, cell)
276
    # Convert the adsorbate to an Atoms object
277
    if isinstance(adsorbate, ase.Atoms):
278
        ads = deepcopy(adsorbate)
279
    elif isinstance(adsorbate, ase.Atom):
280
        ads = ase.Atoms([adsorbate])
281
    else:
282
        # Assume it is a string representing a single Atom
283
        ads = ase.Atoms([ase.Atom(adsorbate)])
284
    pos += height * norm_vect_u
285
    # Move adsorbate into position
286
    ads.translate(pos - ctr_coord)
287
    # Attach the adsorbate
288
    slab.extend(ads)
289

    
290

    
291
def check_collision(slab_molec, slab_num_atoms, min_height, vect, nn_slab=0,
292
                    nn_molec=0, coll_coeff=1.2, exclude_atom=False):
293
    """Checks whether a slab and a molecule collide or not.
294

295
    @param slab_molec: The system of adsorbate-slab for which to detect if there
296
        are collisions.
297
    @param nn_slab: Number of neigbors in the surface.
298
    @param nn_molec: Number of neighbors in the molecule.
299
    @param coll_coeff: The coefficient that multiplies the covalent radius of
300
        atoms resulting in a distance that two atoms being closer to that is
301
        considered as atomic collision.
302
    @param slab_num_atoms: Number of atoms of the bare slab.
303
    @param min_height: The minimum height atoms can have to not be considered as
304
        colliding.
305
    @param vect: The vector perpendicular to the slab.
306
    @param exclude_atom: Whether to exclude the adsorption center in the
307
        molecule in the collision detection.
308
    @return: bool, whether the surface and the molecule collide.
309
    """
310
    from copy import deepcopy
311
    from ase.neighborlist import natural_cutoffs, neighbor_list
312

    
313
    # Check structure overlap by height
314
    if min_height is not False:
315
        cart_axes = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0],
316
                     [-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, -1.0]]
317
        if vect.tolist() not in cart_axes:
318
            err_msg = "'min_coll_height' option is only implemented for " \
319
                      "'surf_norm_vect' to be one of the x, y or z axes. "
320
            logger.error(err_msg)
321
            raise ValueError(err_msg)
322
        for atom in slab_molec[slab_num_atoms:]:
323
            if exclude_atom is not False \
324
                    and atom.index == exclude_atom:
325
                continue
326
            for i, coord in enumerate(vect):
327
                if coord == 0:
328
                    continue
329
                if atom.position[i] * coord < min_height * coord:
330
                    return True
331

    
332
    # Check structure overlap by sphere collision
333
    if coll_coeff is not False:
334
        if exclude_atom is not False:
335
            slab_molec_wo_ctr = deepcopy(slab_molec)
336
            del slab_molec_wo_ctr[exclude_atom + slab_num_atoms]
337
            slab_molec_cutoffs = natural_cutoffs(slab_molec_wo_ctr,
338
                                                 mult=coll_coeff)
339
            slab_molec_nghbs = len(neighbor_list("i", slab_molec_wo_ctr,
340
                                                 slab_molec_cutoffs))
341
        else:
342
            slab_molec_cutoffs = natural_cutoffs(slab_molec, mult=coll_coeff)
343
            slab_molec_nghbs = len(neighbor_list("i", slab_molec,
344
                                                 slab_molec_cutoffs))
345
        if slab_molec_nghbs > nn_slab + nn_molec:
346
            return True
347

    
348
    return False
349

    
350

    
351
def correct_coll(molec, slab, ctr_coord, site_coord, num_pts,
352
                 min_coll_height, norm_vect, slab_nghbs, molec_nghbs,
353
                 coll_coeff, height=2.5, excl_atom=False):
354
    # TODO Rename this function
355
    """Tries to adsorb a molecule on a slab trying to avoid collisions by doing
356
    small rotations.
357

358
    @param molec: ase.Atoms object of the molecule to adsorb
359
    @param slab: ase.Atoms object of the surface on which to adsorb the
360
        molecule
361
    @param ctr_coord: The coordinates of the molecule to use as adsorption
362
        center.
363
    @param site_coord: The coordinates of the surface on which to adsorb the
364
        molecule
365
    @param num_pts: Number on which to sample Euler angles.
366
    @param min_coll_height: The lowermost height for which to detect a collision
367
    @param norm_vect: The vector perpendicular to the surface.
368
    @param slab_nghbs: Number of neigbors in the surface.
369
    @param molec_nghbs: Number of neighbors in the molecule.
370
    @param coll_coeff: The coefficient that multiplies the covalent radius of
371
        atoms resulting in a distance that two atoms being closer to that is
372
        considered as atomic collision.
373
    @param height: Height on which to try adsorption.
374
    @param excl_atom: Whether to exclude the adsorption center in the
375
        molecule in the collision detection.
376
    @return collision: bool, whether the structure generated has collisions
377
        between slab and adsorbate.
378
    """
379
    from copy import deepcopy
380
    slab_num_atoms = len(slab)
381
    slab_molec = []
382
    collision = True
383
    max_corr = 6  # Should be an even number
384
    d_angle = 180 / ((max_corr / 2.0) * num_pts)
385
    num_corr = 0
386
    while collision and num_corr <= max_corr:
387
        k = num_corr * (-1) ** num_corr
388
        slab_molec = deepcopy(slab)
389
        molec.euler_rotate(k * d_angle, k * d_angle / 2, k * d_angle,
390
                           center=ctr_coord)
391
        add_adsorbate(slab_molec, molec, site_coord, ctr_coord, height,
392
                      norm_vect=norm_vect)
393
        collision = check_collision(slab_molec, slab_num_atoms, min_coll_height,
394
                                    norm_vect, slab_nghbs, molec_nghbs,
395
                                    coll_coeff, excl_atom)
396
        num_corr += 1
397
    return slab_molec, collision
398

    
399

    
400
def dissociate_h(slab_molec_orig, h_idx, num_atoms_slab, h_acceptor,
401
                 neigh_cutoff=1):
402
    # TODO rethink
403
    """Tries to dissociate a H from the molecule and adsorbs it on the slab.
404

405
    Tries to dissociate a H atom from the molecule and adsorb in on top of the
406
    surface if the distance is shorter than two times the neigh_cutoff value.
407
    @param slab_molec_orig: The ase.Atoms object of the system adsorbate-slab.
408
    @param h_idx: The index of the hydrogen atom to carry out adsorption of.
409
    @param num_atoms_slab: The number of atoms of the slab without adsorbate.
410
    @param h_acceptor: List of atom types or atom numbers that are H-acceptors.
411
    @param neigh_cutoff: half the maximum distance between the surface and the
412
        H for it to carry out dissociation.
413
    @return: An ase.Atoms object of the system adsorbate-surface with H
414
    """
415
    from copy import deepcopy
416
    from ase.neighborlist import NeighborList
417
    slab_molec = deepcopy(slab_molec_orig)
418
    cutoffs = len(slab_molec) * [neigh_cutoff]
419
    nl = NeighborList(cutoffs, self_interaction=False, bothways=True, skin=0.0)
420
    nl.update(slab_molec)
421
    surf_h_vect = np.array([np.infty] * 3)
422
    if h_acceptor == 'all':
423
        h_acceptor = list(range(num_atoms_slab))
424
    for neigh_idx in nl.get_neighbors(h_idx)[0]:
425
        for elm in h_acceptor:
426
            if isinstance(elm, int):
427
                if neigh_idx == elm and neigh_idx < num_atoms_slab:
428
                    dist = np.linalg.norm(slab_molec[neigh_idx].position -
429
                                          slab_molec[h_idx].position)
430
                    if dist < np.linalg.norm(surf_h_vect):
431
                        surf_h_vect = slab_molec[neigh_idx].position \
432
                                      - slab_molec[h_idx].position
433
            else:
434
                if slab_molec[neigh_idx].symbol == elm \
435
                        and neigh_idx < num_atoms_slab:
436
                    dist = np.linalg.norm(slab_molec[neigh_idx].position -
437
                                          slab_molec[h_idx].position)
438
                    if dist < np.linalg.norm(surf_h_vect):
439
                        surf_h_vect = slab_molec[neigh_idx].position \
440
                                      - slab_molec[h_idx].position
441

    
442
    if np.linalg.norm(surf_h_vect) != np.infty:
443
        trans_vect = surf_h_vect - surf_h_vect / np.linalg.norm(surf_h_vect)
444
        slab_molec[h_idx].position = slab_molec[h_idx].position + trans_vect
445
        return slab_molec
446

    
447

    
448
def dissociation(slab_molec, h_donor, h_acceptor, num_atoms_slab):
449
    # TODO multiple dissociation
450
    """Decides which H atoms to dissociate according to a list of atoms.
451

452
    Given a list of chemical symbols or atom indices it checks for every atom
453
    or any of its neighbor if it's a H and calls dissociate_h to try to carry
454
    out dissociation of that H. For atom indices, it checks both whether
455
    the atom index or its neighbors are H, for chemical symbols, it only checks
456
    if there is a neighbor H.
457
    @param slab_molec: The ase.Atoms object of the system adsorbate-slab.
458
    @param h_donor: List of atom types or atom numbers that are H-donors.
459
    @param h_acceptor: List of atom types or atom numbers that are H-acceptors.
460
    @param num_atoms_slab: Number of atoms of the bare slab.
461
    @return:
462
    """
463
    from ase.neighborlist import natural_cutoffs, NeighborList
464
    molec = slab_molec[num_atoms_slab:]
465
    cutoffs = natural_cutoffs(molec)
466
    nl = NeighborList(cutoffs, self_interaction=False, bothways=True)
467
    nl.update(molec)
468
    disso_structs = []
469
    for el in h_donor:
470
        if isinstance(el, int):
471
            if molec[el].symbol == 'H':
472
                disso_struct = dissociate_h(slab_molec, el + num_atoms_slab,
473
                                            num_atoms_slab, h_acceptor)
474
                if disso_struct is not None:
475
                    disso_structs.append(disso_struct)
476
            else:
477
                for neigh_idx in nl.get_neighbors(el)[0]:
478
                    if molec[neigh_idx].symbol == 'H':
479
                        disso_struct = dissociate_h(slab_molec, neigh_idx +
480
                                                    num_atoms_slab,
481
                                                    num_atoms_slab, h_acceptor)
482
                        if disso_struct is not None:
483
                            disso_structs.append(disso_struct)
484
        else:
485
            for atom in molec:
486
                if atom.symbol.lower() == el.lower():
487
                    for neigh_idx in nl.get_neighbors(atom.index)[0]:
488
                        if molec[neigh_idx].symbol == 'H':
489
                            disso_struct = dissociate_h(slab_molec, neigh_idx
490
                                                        + num_atoms_slab,
491
                                                        num_atoms_slab,
492
                                                        h_acceptor)
493
                            if disso_struct is not None:
494
                                disso_structs.append(disso_struct)
495
    return disso_structs
496

    
497

    
498
def ads_euler(orig_molec, slab, ctr_coord, site_coord, num_pts,
499
              min_coll_height, coll_coeff, norm_vect, slab_nghbs, molec_nghbs,
500
              h_donor, h_acceptor, height, excl_atom):
501
    """Generates adsorbate-surface structures by sampling over Euler angles.
502

503
    This function generates a number of adsorbate-surface structures at
504
    different orientations of the adsorbate sampled at multiple Euler (zxz)
505
    angles.
506
    @param orig_molec: ase.Atoms object of the molecule to adsorb.
507
    @param slab: ase.Atoms object of the surface on which to adsorb the
508
        molecule.
509
    @param ctr_coord: The coordinates of the molecule to use as adsorption
510
        center.
511
    @param site_coord: The coordinates of the surface on which to adsorb the
512
        molecule.
513
    @param num_pts: Number on which to sample Euler angles.
514
    @param min_coll_height: The lowest height for which to detect a collision.
515
    @param coll_coeff: The coefficient that multiplies the covalent radius of
516
        atoms resulting in a distance that two atoms being closer to that is
517
        considered as atomic collision.
518
    @param norm_vect: The vector perpendicular to the surface.
519
    @param slab_nghbs: Number of neigbors in the surface.
520
    @param molec_nghbs: Number of neighbors in the molecule.
521
    @param h_donor: List of atom types or atom numbers that are H-donors.
522
    @param h_acceptor: List of atom types or atom numbers that are H-acceptors.
523
    @param height: Height on which to try adsorption.
524
    @param excl_atom: Whether to exclude the adsorption center in the
525
        molecule in the collision detection.
526
    @return: list of ase.Atoms object conatining all the orientations of a given
527
        conformer.
528
    """
529
    from copy import deepcopy
530
    slab_ads_list = []
531
    prealigned_molec = align_molec(orig_molec, ctr_coord, norm_vect)
532
    # rotation around z
533
    for alpha in np.arange(0, 360, 360 / num_pts):
534
        # rotation around x'
535
        for beta in np.arange(0, 180, 180 / num_pts):
536
            # rotation around z'
537
            for gamma in np.arange(0, 360, 360 / num_pts):
538
                if beta == 0 and gamma > 0:
539
                    continue
540
                molec = deepcopy(prealigned_molec)
541
                molec.euler_rotate(alpha, beta, gamma, center=ctr_coord)
542
                slab_molec, collision = correct_coll(molec, slab, ctr_coord,
543
                                                     site_coord, num_pts,
544
                                                     min_coll_height, norm_vect,
545
                                                     slab_nghbs, molec_nghbs,
546
                                                     coll_coeff, height,
547
                                                     excl_atom)
548
                if not collision and not any([np.allclose(slab_molec.positions,
549
                                                          conf.positions)
550
                                              for conf in slab_ads_list]):
551
                    slab_ads_list.append(slab_molec)
552
                    if h_donor is not False:
553
                        slab_ads_list.extend(dissociation(slab_molec, h_donor,
554
                                                          h_acceptor,
555
                                                          len(slab)))
556

    
557
    return slab_ads_list
558

    
559

    
560
def internal_rotate(molecule, surf, ctr1_mol, ctr2_mol, ctr3_mol, ctr1_surf,
561
                    ctr2_surf, bond_vector, bond_angle_target,
562
                    dihedral_angle_target=None, mol_dihedral_angle_target=None):
563
    """Performs translation and rotation of an adsorbate defined by an
564
    adsorption bond length, direction, angle and dihedral angle
565

566
    Carles modification of chemcat's transform_adsorbate to work with
567
    coordinates instead of ase.Atom
568
    Parameters:
569
        molecule (ase.Atoms): The molecule to adsorb.
570

571
        surf (ase.Atoms): The surface ontop of which to adsorb.
572

573
        ctr1_mol (int/list): The position of the adsorption center in the
574
        molecule that will be bound to the surface.
575

576
        ctr2_mol (int/list): The position of a second center of the
577
        adsorbate used to define the adsorption bond angle, and the dihedral
578
        adsorption angle.
579

580
        ctr3_mol (int/list): The position of a third center in the
581
        adsorbate used to define the adsorbate dihedral angle.
582

583
        ctr1_surf (int/list): The position of the site on the surface that
584
        will be bound to the molecule.
585

586
        ctr2_surf (int/list): The position of a second center of the
587
        surface used to define the dihedral adsorption angle.
588

589
        bond_vector (numpy.ndarray): The adsorption bond desired.
590
            Details: offset = vect(atom1_surf, atom1_mol)
591

592
        bond_angle_target (float or int): The adsorption bond angle desired (in
593
            degrees).
594
            Details: bond_angle_target = angle(atom1_surf, atom1_mol, atom2_mol)
595

596
        dihedral_angle_target (float or int): The dihedral adsorption angle
597
            desired (in degrees).
598
            Details: dihedral_angle_target = dihedral(atom2_surf, atom1_surf,
599
            atom1_mol, atom2_mol)
600
                The rotation vector is facing the adsorbate from the surface
601
                (i.e. counterclockwise rotation when facing the surface (i.e.
602
                view from top))
603

604
        mol_dihedral_angle_target (float or int): The adsorbate dihedral angle
605
            desired (in degrees).
606
            Details: mol_dihedral_angle_target = dihedral(atom1_surf, atom1_mol,
607
            atom2_mol, atom3_mol)
608
                The rotation vector is facing atom2_mol from atom1_mol
609

610
    Returns:
611
        None (the `molecule` object is modified in-place)
612
    """
613
    vect_surf = get_atom_coords(surf, ctr2_surf) - get_atom_coords(surf,
614
                                                                   ctr1_surf)
615
    vect_inter = get_atom_coords(molecule, ctr1_mol) \
616
        - get_atom_coords(surf, ctr1_surf)
617
    vect_mol = get_atom_coords(molecule, ctr2_mol) - get_atom_coords(molecule,
618
                                                                     ctr1_mol)
619
    vect2_mol = get_atom_coords(molecule, ctr3_mol) - get_atom_coords(molecule,
620
                                                                      ctr2_mol)
621

    
622
    # Check if dihedral angles can be defined
623
    do_dihedral = dihedral_angle_target is not None
624
    do_mol_dihedral = mol_dihedral_angle_target is not None
625
    dihedral_use_mol2 = False
626
    # Check if vect_surf and bond_vector are aligned
627
    if np.allclose(np.cross(vect_surf, bond_vector), 0):
628
        do_dihedral = False
629
    # Check if requested bond angle is not flat
630
    if np.isclose((bond_angle_target + 90) % 180 - 90, 0):
631
        do_mol_dihedral = False
632
        dihedral_use_mol2 = True
633
    # Check if vect_mol and vect2_mol are not aligned
634
    if np.allclose(np.cross(vect_mol, vect2_mol), 0):
635
        do_mol_dihedral = False
636

    
637
    ###########################
638
    #       Translation       #
639
    ###########################
640

    
641
    # Compute and apply translation of adsorbate
642
    translation = bond_vector - vect_inter
643
    molecule.translate(translation)
644

    
645
    # Update adsorption bond
646
    vect_inter = get_atom_coords(molecule, ctr1_mol) - \
647
        get_atom_coords(surf, ctr1_surf)
648

    
649
    # Check if translation was successful
650
    if np.allclose(vect_inter, bond_vector):
651
        pass  # print("Translation successfully applied (error: ~ {:.5g} unit "
652
        # "length)".format(np.linalg.norm(vect_inter - bond_vector)))
653
    else:
654
        err = 'An unknown error occured during the translation'
655
        logger.error(err)
656
        raise AssertionError(err)
657

    
658
    ###########################
659
    #   Bond angle rotation   #
660
    ###########################
661

    
662
    # Compute rotation vector
663
    rotation_vector = np.cross(-vect_inter, vect_mol)
664
    if np.allclose(rotation_vector, 0, atol=1e-5):
665
        # If molecular bonds are aligned, any vector orthogonal to vect_inter
666
        # can be used Such vector can be found as the orthogonal rejection of
667
        # either X-axis, Y-axis or Z-axis onto vect_inter (since they cannot
668
        # be all aligned)
669
        non_aligned_vector = np.zeros(3)
670
        # Select the most orthogonal axis (lowest dot product):
671
        non_aligned_vector[np.argmin(np.abs(vect_inter))] = 1
672
        rotation_vector = non_aligned_vector - np.dot(non_aligned_vector,
673
                                                      vect_inter) / np.dot(
674
            vect_inter, vect_inter) * vect_inter
675

    
676
    # Retrieve current bond angle
677
    bond_angle_ini = get_vect_angle(-vect_inter, vect_mol, rotation_vector)
678

    
679
    # Apply rotation to reach desired bond_angle
680
    molecule.rotate(bond_angle_target - bond_angle_ini, v=rotation_vector,
681
                    center=get_atom_coords(molecule, ctr1_mol))
682

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

    
689
    # Check if rotation was successful
690
    bond_angle = get_vect_angle(-vect_inter, vect_mol)
691
    if np.isclose((bond_angle - bond_angle_target + 90) % 180 - 90, 0,
692
                  atol=1e-3) and np.allclose(get_atom_coords(molecule, ctr1_mol)
693
                                             - get_atom_coords(surf,
694
                                                               ctr1_surf),
695
                                             vect_inter):
696
        pass  # print("Rotation successfully applied (error: {:.5f}°)".format(
697
        # (bond_angle - bond_angle_target + 90) % 180 - 90))
698
    else:
699
        err = 'An unknown error occured during the rotation'
700
        logger.error(err)
701
        raise AssertionError(err)
702

    
703
    ###########################
704
    # Dihedral angle rotation #
705
    ###########################
706

    
707
    # Perform dihedral rotation if possible
708
    if do_dihedral:
709
        # Retrieve current dihedral angle (by computing the angle between the
710
        # vector rejection of vect_surf and vect_mol onto vect_inter)
711
        vect_inter_inner = np.dot(vect_inter, vect_inter)
712
        vect_surf_reject = vect_surf - np.dot(vect_surf, vect_inter) / \
713
            vect_inter_inner * vect_inter
714
        if dihedral_use_mol2:
715
            vect_mol_reject = vect2_mol - np.dot(vect2_mol, vect_inter) / \
716
                              vect_inter_inner * vect_inter
717
        else:
718
            vect_mol_reject = vect_mol - np.dot(vect_mol, vect_inter) / \
719
                              vect_inter_inner * vect_inter
720
        dihedral_angle_ini = get_vect_angle(vect_surf_reject, vect_mol_reject,
721
                                            vect_inter)
722

    
723
        # Apply dihedral rotation along vect_inter
724
        molecule.rotate(dihedral_angle_target - dihedral_angle_ini,
725
                        v=vect_inter, center=get_atom_coords(molecule,
726
                                                             ctr1_mol))
727

    
728
        # Update molecular bonds
729
        vect_mol = get_atom_coords(molecule, ctr2_mol) - \
730
            get_atom_coords(molecule, ctr1_mol)
731
        vect2_mol = get_atom_coords(molecule, ctr3_mol) - \
732
            get_atom_coords(molecule, ctr2_mol)
733

    
734
        # Check if rotation was successful
735
        # Check dihedral rotation
736
        if dihedral_use_mol2:
737
            vect_mol_reject = vect2_mol - np.dot(vect2_mol, vect_inter) / \
738
                              vect_inter_inner * vect_inter
739
        else:
740
            vect_mol_reject = vect_mol - np.dot(vect_mol, vect_inter) / \
741
                              vect_inter_inner * vect_inter
742
        dihedral_angle = get_vect_angle(vect_surf_reject, vect_mol_reject,
743
                                        vect_inter)
744
        # Check bond rotation is unmodified
745
        bond_angle = get_vect_angle(-vect_inter, vect_mol)
746
        if np.isclose((dihedral_angle - dihedral_angle_target + 90) % 180 - 90,
747
                      0, atol=1e-3) \
748
                and np.isclose((bond_angle - bond_angle_target + 90) %
749
                               180 - 90, 0, atol=1e-5) \
750
                and np.allclose(get_atom_coords(molecule, ctr1_mol)
751
                                - get_atom_coords(surf, ctr1_surf),
752
                                vect_inter):
753
            pass  # print( "Dihedral rotation successfully applied (error: {
754
            # :.5f}°)".format((dihedral_angle - dihedral_angle_target + 90) %
755
            # 180 - 90))
756
        else:
757
            err = 'An unknown error occured during the dihedral rotation'
758
            logger.error(err)
759
            raise AssertionError(err)
760

    
761
    #####################################
762
    # Adsorbate dihedral angle rotation #
763
    #####################################
764

    
765
    # Perform adsorbate dihedral rotation if possible
766
    if do_mol_dihedral:
767
        # Retrieve current adsorbate dihedral angle (by computing the angle
768
        # between the orthogonal rejection of vect_inter and vect2_mol onto
769
        # vect_mol)
770
        vect_mol_inner = np.dot(vect_mol, vect_mol)
771
        bond_inter_reject = -vect_inter - np.dot(-vect_inter, vect_mol) / \
772
            vect_mol_inner * vect_mol
773
        bond2_mol_reject = vect2_mol - np.dot(vect2_mol, vect_mol) / \
774
            vect_mol_inner * vect_mol
775
        dihedral_angle_ini = get_vect_angle(bond_inter_reject,
776
                                            bond2_mol_reject, vect_mol)
777

    
778
        # Apply dihedral rotation along vect_mol
779
        molecule.rotate(mol_dihedral_angle_target - dihedral_angle_ini,
780
                        v=vect_mol, center=get_atom_coords(molecule, ctr1_mol))
781

    
782
        # Update molecular bonds
783
        vect_mol = get_atom_coords(molecule, ctr2_mol) \
784
            - get_atom_coords(molecule, ctr1_mol)
785
        vect2_mol = get_atom_coords(molecule, ctr3_mol) \
786
            - get_atom_coords(molecule, ctr2_mol)
787

    
788
        # Check if rotation was successful
789
        # Check adsorbate dihedral rotation
790
        vect_mol_inner = np.dot(vect_mol, vect_mol)
791
        bond2_mol_reject = vect2_mol - np.dot(vect2_mol, vect_mol) / \
792
            vect_mol_inner * vect_mol
793
        mol_dihedral_angle = get_vect_angle(bond_inter_reject,
794
                                            bond2_mol_reject, vect_mol)
795
        # Check dihedral rotation
796
        vect_inter_inner = np.dot(vect_inter, vect_inter)
797
        vect_surf_reject = vect_surf - np.dot(vect_surf, vect_inter) / \
798
            vect_inter_inner * vect_inter
799
        vect_mol_reject = vect_mol - np.dot(vect_mol, vect_inter) / \
800
            vect_inter_inner * vect_inter
801
        dihedral_angle = get_vect_angle(vect_surf_reject, vect_mol_reject,
802
                                        vect_inter)
803
        # Check bond rotation is unmodified
804
        bond_angle = get_vect_angle(-vect_inter, vect_mol)
805
        if np.isclose((mol_dihedral_angle - mol_dihedral_angle_target + 90) %
806
                      180 - 90, 0, atol=1e-3) \
807
                and np.isclose((dihedral_angle -
808
                                dihedral_angle_target + 90) % 180 - 90, 0,
809
                               atol=1e-5) \
810
                and np.isclose((bond_angle - bond_angle_target + 90) % 180 - 90,
811
                               0, atol=1e-5) \
812
                and np.allclose(get_atom_coords(molecule, ctr1_mol) -
813
                                get_atom_coords(surf, ctr1_surf),
814
                                vect_inter):
815
            pass  # print(
816
            # "Adsorbate dihedral rotation successfully applied (error:
817
            # {:.5f}°)".format((mol_dihedral_angle - mol_dihedral_angle_target
818
            # + 90) % 180 - 90))
819
        else:
820
            err = 'An unknown error occured during the adsorbate dihedral ' \
821
                  'rotation'
822
            logger.error(err)
823
            raise AssertionError(err)
824

    
825

    
826
def ads_internal(orig_molec, slab, ctr1_mol, ctr2_mol, ctr3_mol, ctr1_surf,
827
                 ctr2_surf, num_pts, min_coll_height, coll_coeff, norm_vect,
828
                 slab_nghbs, molec_nghbs, h_donor, h_acceptor, max_hel, height,
829
                 excl_atom):
830
    """Generates adsorbate-surface structures by sampling over internal angles.
831

832
    @param orig_molec: ase.Atoms object of the molecule to adsorb (adsorbate).
833
    @param slab: ase.Atoms object of the surface on which to adsorb the molecule
834
    @param ctr1_mol: The index/es of the center in the adsorbate to use as
835
        adsorption center.
836
    @param ctr2_mol: The index/es of the center in the adsorbate to use for the
837
        definition of the surf-adsorbate angle, surf-adsorbate dihedral angle
838
        and adsorbate dihedral angle.
839
    @param ctr3_mol: The index/es of the center in the adsorbate to use for the
840
        definition of the adsorbate dihedral angle.
841
    @param ctr1_surf: The index/es of the center in the surface to use as
842
        adsorption center.
843
    @param ctr2_surf: The index/es of the center in the surface to use for the
844
        definition of the surf-adsorbate dihedral angle.
845
    @param num_pts: Number on which to sample Euler angles.
846
    @param min_coll_height: The lowest height for which to detect a collision
847
    @param coll_coeff: The coefficient that multiplies the covalent radius of
848
        atoms resulting in a distance that two atoms being closer to that is
849
        considered as atomic collision.
850
    @param norm_vect: The vector perpendicular to the surface.
851
    @param slab_nghbs: Number of neigbors in the surface.
852
    @param molec_nghbs: Number of neighbors in the molecule.
853
    @param h_donor: List of atom types or atom numbers that are H-donors.
854
    @param h_acceptor: List of atom types or atom numbers that are H-acceptors.
855
    @param max_hel: Maximum value for sampling the helicopter
856
        (surf-adsorbate dihedral) angle.
857
    @param height: Height on which to try adsorption.
858
    @param excl_atom: Whether to exclude the adsorption center in the
859
        molecule in the collision detection.
860
    @return: list of ase.Atoms object conatining all the orientations of a given
861
        conformer.
862
    """
863
    from copy import deepcopy
864
    slab_ads_list = []
865
    # Rotation over bond angle
866
    for alpha in np.arange(90, 180+1, 90 / max(1, num_pts-1))[:num_pts]:
867
        # Rotation over surf-adsorbate dihedral angle
868
        for beta in np.arange(0, max_hel, max_hel / num_pts):
869
            # Rotation over adsorbate bond dihedral angle
870
            for gamma in np.arange(90, 270+1, 180/max(1, num_pts-1))[:num_pts]:
871
                # Avoid duplicates as gamma rotation has no effect on plane
872
                # angles.
873
                if alpha == 180 and gamma > 90:
874
                    continue
875
                new_molec = deepcopy(orig_molec)
876
                internal_rotate(new_molec, slab, ctr1_mol, ctr2_mol, ctr3_mol,
877
                                ctr1_surf, ctr2_surf, norm_vect, alpha,
878
                                beta, gamma)
879
                site_coords = get_atom_coords(slab, ctr1_surf)
880
                ctr_coords = get_atom_coords(new_molec, ctr1_mol)
881
                slab_molec, collision = correct_coll(new_molec, slab,
882
                                                     ctr_coords, site_coords,
883
                                                     num_pts, min_coll_height,
884
                                                     norm_vect, slab_nghbs,
885
                                                     molec_nghbs, coll_coeff,
886
                                                     height, excl_atom)
887
                slab_molec.info = {**slab_molec.info, **new_molec.info}
888
                if not collision and \
889
                        not any([np.allclose(slab_molec.positions,
890
                                             conf.positions)
891
                                 for conf in slab_ads_list]):
892
                    slab_ads_list.append(slab_molec)
893
                    if h_donor is not False:
894
                        slab_ads_list.extend(dissociation(slab_molec, h_donor,
895
                                                          h_acceptor,
896
                                                          len(slab)))
897

    
898
    return slab_ads_list
899

    
900

    
901
def adsorb_confs(conf_list, surf, inp_vars):
902
    """Generates a number of adsorbate-surface structure coordinates.
903

904
    Given a list of conformers, a surface, a list of atom indices (or list of
905
    list of indices) of both the surface and the adsorbate, it generates a
906
    number of adsorbate-surface structures for every possible combination of
907
    them at different orientations.
908
    @param conf_list: list of ase.Atoms of the different conformers
909
    @param surf: the ase.Atoms object of the surface
910
    @param inp_vars: Calculation parameters from input file.
911
    @return: list of ase.Atoms for the adsorbate-surface structures
912
    """
913
    from copy import deepcopy
914
    from ase.neighborlist import natural_cutoffs, neighbor_list
915
    molec_ctrs = inp_vars['molec_ctrs']
916
    sites = inp_vars['sites']
917
    angles = inp_vars['set_angles']
918
    num_pts = inp_vars['sample_points_per_angle']
919
    inp_norm_vect = inp_vars['surf_norm_vect']
920
    min_coll_height = inp_vars['min_coll_height']
921
    coll_coeff = inp_vars['collision_threshold']
922
    exclude_ads_ctr = inp_vars['exclude_ads_ctr']
923
    h_donor = inp_vars['h_donor']
924
    h_acceptor = inp_vars['h_acceptor']
925
    height = inp_vars['adsorption_height']
926

    
927
    if inp_vars['pbc_cell'] is not False:
928
        surf.set_pbc(True)
929
        surf.set_cell(inp_vars['pbc_cell'])
930

    
931
    surf_ads_list = []
932
    sites_coords = [get_atom_coords(surf, site) for site in sites]
933
    if coll_coeff is not False:
934
        surf_cutoffs = natural_cutoffs(surf, mult=coll_coeff)
935
        surf_nghbs = len(neighbor_list("i", surf, surf_cutoffs))
936
    else:
937
        surf_nghbs = 0
938
    for i, conf in enumerate(conf_list):
939
        molec_ctr_coords = [get_atom_coords(conf, ctr) for ctr in molec_ctrs]
940
        if inp_vars['pbc_cell'] is not False:
941
            conf.set_pbc(True)
942
            conf.set_cell(inp_vars['pbc_cell'])
943
        for s, site in enumerate(sites_coords):
944
            if isinstance(inp_norm_vect, str) and inp_norm_vect == 'auto':
945
                norm_vect = compute_norm_vect(surf, sites[s],
946
                                              inp_vars['pbc_cell'])
947
            else:
948
                norm_vect = inp_norm_vect
949
            for c, ctr in enumerate(molec_ctr_coords):
950
                if exclude_ads_ctr and isinstance(molec_ctrs[c], int):
951
                    exclude_atom = molec_ctrs[c]
952
                else:
953
                    exclude_atom = False
954
                    if exclude_ads_ctr and not isinstance(molec_ctrs[c], int):
955
                        logger.warning("'exclude_ads_ctr' only works for atomic"
956
                                       "centers and not for many-atoms "
957
                                       f"barycenters. {molec_ctrs[c]} are not "
958
                                       f"going to be excluded from collison.")
959
                if coll_coeff and exclude_atom is not False:
960
                    conf_wo_ctr = deepcopy(conf)
961
                    del conf_wo_ctr[exclude_atom]
962
                    conf_cutoffs = natural_cutoffs(conf_wo_ctr, mult=coll_coeff)
963
                    molec_nghbs = len(neighbor_list("i", conf_wo_ctr,
964
                                                    conf_cutoffs))
965
                elif coll_coeff and exclude_atom is False:
966
                    conf_cutoffs = natural_cutoffs(conf, mult=coll_coeff)
967
                    molec_nghbs = len(neighbor_list("i", conf, conf_cutoffs))
968
                else:
969
                    molec_nghbs = 0
970
                if angles == 'euler':
971
                    surf_ads_list.extend(ads_euler(conf, surf, ctr, site,
972
                                                   num_pts, min_coll_height,
973
                                                   coll_coeff, norm_vect,
974
                                                   surf_nghbs, molec_nghbs,
975
                                                   h_donor, h_acceptor, height,
976
                                                   exclude_atom))
977
                elif angles == 'internal':
978
                    mol_ctr1 = molec_ctrs[c]
979
                    mol_ctr2 = inp_vars["molec_ctrs2"][c]
980
                    mol_ctr3 = inp_vars["molec_ctrs3"][c]
981
                    surf_ctr1 = sites[s]
982
                    surf_ctr2 = inp_vars["surf_ctrs2"][s]
983
                    max_h = inp_vars["max_helic_angle"]
984
                    surf_ads_list.extend(ads_internal(conf, surf, mol_ctr1,
985
                                                      mol_ctr2, mol_ctr3,
986
                                                      surf_ctr1, surf_ctr2,
987
                                                      num_pts, min_coll_height,
988
                                                      coll_coeff, norm_vect,
989
                                                      surf_nghbs, molec_nghbs,
990
                                                      h_donor, h_acceptor,
991
                                                      max_h, height,
992
                                                      exclude_atom))
993
    return surf_ads_list
994

    
995

    
996
def run_screening(inp_vars):
997
    """Carries out the screening of adsorbate structures on a surface.
998

999
    @param inp_vars: Calculation parameters from input file.
1000
    """
1001
    import os
1002
    import random
1003
    from modules.formats import collect_confs, adapt_format
1004
    from modules.calculation import run_calc, check_finished_calcs
1005

    
1006
    logger.info('Carrying out procedures for the screening of adsorbate-surface'
1007
                ' structures.')
1008
    if inp_vars['use_molec_file']:
1009
        selected_confs = [adapt_format('ase', inp_vars['use_molec_file'],
1010
                                       inp_vars['special_atoms'])]
1011
        logger.info(f"Using '{inp_vars['use_molec_file']}' as only conformer.")
1012
    else:
1013
        if not os.path.isdir("isolated"):
1014
            err = "'isolated' directory not found. It is needed in order to " \
1015
                  "carry out the screening of structures to be adsorbed"
1016
            logger.error(err)
1017
            raise FileNotFoundError(err)
1018

    
1019
        finished_calcs, failed_calcs = check_finished_calcs('isolated',
1020
                                                            inp_vars['code'])
1021
        if not finished_calcs:
1022
            err_msg = "No calculations on 'isolated' finished normally."
1023
            logger.error(err_msg)
1024
            raise FileNotFoundError(err_msg)
1025

    
1026
        logger.info(f"Found {len(finished_calcs)} structures of isolated "
1027
                    f"conformers whose calculation finished normally.")
1028
        if len(failed_calcs) != 0:
1029
            logger.warning(
1030
                f"Found {len(failed_calcs)} calculations more that "
1031
                f"did not finish normally: {failed_calcs}. \n"
1032
                f"Using only the ones that finished normally: "
1033
                f"{finished_calcs}.")
1034

    
1035
        conf_list = collect_confs(finished_calcs, inp_vars['code'], 'isolated',
1036
                                  inp_vars['special_atoms'])
1037
        selected_confs = select_confs(conf_list, inp_vars['select_magns'],
1038
                                      inp_vars['confs_per_magn'])
1039
    surf = adapt_format('ase', inp_vars['surf_file'], inp_vars['special_atoms'])
1040
    surf.info = {}
1041
    surf_ads_list = adsorb_confs(selected_confs, surf, inp_vars)
1042
    if len(surf_ads_list) > inp_vars['max_structures']:
1043
        surf_ads_list = random.sample(surf_ads_list, inp_vars['max_structures'])
1044
    elif len(surf_ads_list) == 0:
1045
        err_msg = "No configurations were generated: Check the parameters in" \
1046
                  "dockonsurf.inp"
1047
        logger.error(err_msg)
1048
        raise ValueError(err_msg)
1049
    logger.info(f'Generated {len(surf_ads_list)} adsorbate-surface atomic '
1050
                f'configurations to carry out a calculation of.')
1051

    
1052
    run_calc('screening', inp_vars, surf_ads_list)
1053
    logger.info('Finished the procedures for the screening of adsorbate-surface'
1054
                ' structures section.')