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

dockonsurf / modules / screening.py @ 77f8c47b

Historique | Voir | Annoter | Télécharger (49,57 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
    if not np.isnan(norm_vec).any():
150
        logger.info(f"The perpendicular vector to the surface at site '{idxs}' "
151
                    f"is {norm_vec}")
152
    return norm_vec
153

    
154

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

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

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

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

    
236

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

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

    
291

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

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

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

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

    
349
    return False
350

    
351

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

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

    
400

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

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

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

    
448

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

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

    
498

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

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

    
558
    return slab_ads_list
559

    
560

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
638
    ###########################
639
    #       Translation       #
640
    ###########################
641

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

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

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

    
659
    ###########################
660
    #   Bond angle rotation   #
661
    ###########################
662

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

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

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

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

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

    
704
    ###########################
705
    # Dihedral angle rotation #
706
    ###########################
707

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

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

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

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

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

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

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

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

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

    
826

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

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

    
899
    return slab_ads_list
900

    
901

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

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

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

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

    
1000

    
1001
def run_screening(inp_vars):
1002
    """Carries out the screening of adsorbate structures on a surface.
1003

1004
    @param inp_vars: Calculation parameters from input file.
1005
    """
1006
    import os
1007
    import random
1008
    from modules.formats import collect_confs, adapt_format
1009
    from modules.calculation import run_calc, check_finished_calcs
1010

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

    
1024
        finished_calcs, failed_calcs = check_finished_calcs('isolated',
1025
                                                            inp_vars['code'])
1026
        if not finished_calcs:
1027
            err_msg = "No calculations on 'isolated' finished normally."
1028
            logger.error(err_msg)
1029
            raise FileNotFoundError(err_msg)
1030

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

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

    
1057
    run_calc('screening', inp_vars, surf_ads_list)
1058
    logger.info('Finished the procedures for the screening of adsorbate-surface'
1059
                ' structures section.')