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

dockonsurf / modules / screening.py @ 39df9c43

Historique | Voir | Annoter | Télécharger (47,68 ko)

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

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

    
7

    
8
def assign_prop(atoms: ase.Atoms, prop_name: str, prop_val):  # TODO Needed?
9
    atoms.info[prop_name] = prop_val
10

    
11

    
12
def select_confs(orig_conf_list: list, calc_dirs: list, magns: list,
13
                 num_sel: int, code: str):
14
    """Takes a list ase.Atoms and selects the most different magnitude-wise.
15

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

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

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

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

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

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

    
68

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

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

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

    
91

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

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

    
109

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

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

    
144

    
145
def compute_norm_vect(atoms, idxs, cell):
146
    """Computes the local normal vector of a surface at a given site.
147

148
    Given an ase.Atoms object and a site defined as a linear combination of
149
    atoms it computes the vector perpendicular to the surface, considering the
150
    local environment of the site.
151
    @param atoms: ase.Atoms object of the surface.
152
    @param idxs: list or int: Index or list of indices of the atom/s that define
153
        the site
154
    @param cell: Unit cell. A 3x3 matrix (the three unit cell vectors)
155
    @return: numpy.ndarray of the coordinates of the vector locally
156
    perpendicular to the surface.
157
    """
158
    from modules.ASANN import coordination_numbers as coord_nums
159
    if isinstance(idxs, list):
160
        atm_vect = [-np.round(coord_nums(atoms.get_scaled_positions(),
161
                                         pbc=np.any(cell),
162
                                         cell_vectors=cell)[3][i], 2)
163
                    for i in idxs]
164
        norm_vec = vect_avg([vect / np.linalg.norm(vect) for vect in atm_vect])
165
    elif isinstance(idxs, int):
166
        norm_vec = -coord_nums(atoms.get_scaled_positions(),
167
                               pbc=np.any(cell),
168
                               cell_vectors=cell)[3][idxs]
169
    else:
170
        err = "'idxs' must be either an int or a list"
171
        logger.error(err)
172
        raise ValueError(err)
173
    norm_vec = np.round(norm_vec, 2) / np.linalg.norm(np.round(norm_vec, 2))
174
    logger.info(f"The perpendicular vector to the surface at site '{idxs}' is "
175
                f"{norm_vec}")
176
    return norm_vec
177

    
178

    
179
def align_molec(molec, ctr_coord, ref_vect):
180
    """Align a molecule to a vector by a center.
181

182
    Given a reference vector to be aligned to and some coordinates acting as
183
    alignment center, it first averages the vectors pointing to neighboring
184
    atoms and then tries to align this average vector to the target. If the
185
    average vector is 0 it takes the vector to the nearest neighbor.
186
    @param molec: The molecule to alugn
187
    @param ctr_coord: The coordinates to use ase alignment center.
188
    @param ref_vect: The vector to be aligned with.
189
    @return: ase.Atoms of the aligned molecule
190
    """
191
    from ase import Atom
192
    from ase.neighborlist import natural_cutoffs, neighbor_list
193

    
194
    if len(molec) == 1:
195
        err_msg = "Cannot align a single atom"
196
        logger.error(err_msg)
197
        ValueError(err_msg)
198
    cutoffs = natural_cutoffs(molec, mult=1.2)
199
    # Check if ctr_coord are the coordinates of an atom and if not creates a
200
    # dummy one to extract the neighboring atoms.
201
    ctr_idx = None
202
    dummy_atom = False
203
    for atom in molec:
204
        if np.allclose(ctr_coord, atom.position, rtol=1e-2):
205
            ctr_idx = atom.index
206
            break
207
    if ctr_idx is None:
208
        molec.append(Atom('X', position=ctr_coord))
209
        cutoffs.append(0.2)
210
        ctr_idx = len(molec) - 1
211
        dummy_atom = True
212
    # Builds the neighbors and computes the average vector
213
    refs, vects = neighbor_list("iD", molec, cutoffs, self_interaction=False)
214
    neigh_vects = [vects[i] for i, atm in enumerate(refs) if atm == ctr_idx]
215
    # If no neighbors are present, the cutoff of the alignment center is
216
    # set to a value where at least one atom is a neighbor and neighbors are
217
    # recalculated.
218
    if len(neigh_vects) == 0:
219
        min_dist, min_idx = (np.inf, np.inf)
220
        for atom in molec:
221
            if atom.index == ctr_idx:
222
                continue
223
            if molec.get_distance(ctr_idx, atom.index) < min_dist:
224
                min_dist = molec.get_distance(ctr_idx, atom.index)
225
                min_idx = atom.index
226
        cutoffs[ctr_idx] = min_dist - cutoffs[min_idx] + 0.05
227
        refs, vects = neighbor_list("iD", molec, cutoffs,
228
                                    self_interaction=False)
229
        neigh_vects = [vects[i] for i, atm in enumerate(refs) if atm == ctr_idx]
230
    target_vect = vect_avg(neigh_vects)
231
    # If the target vector is 0 (the center is at the baricenter of its
232
    # neighbors). Assuming the adsorption center is coplanar or colinear to its
233
    # neighbors (it would not make a lot of sense to chose a center which is
234
    # the baricenter of neighbors distributed in 3D), the target_vector is
235
    # chosen perpendicular to the nearest neighbor.
236
    if np.allclose(target_vect, 0, rtol=1e-3):
237
        nn_vect = np.array([np.inf] * 3)
238
        for vect in neigh_vects:
239
            if np.linalg.norm(vect) < np.linalg.norm(nn_vect):
240
                nn_vect = vect
241
        cart_axes = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
242
        axis = cart_axes[int(np.argmax([np.linalg.norm(np.cross(ax, nn_vect))
243
                                        for ax in cart_axes]))]
244
        target_vect = np.cross(axis, nn_vect)
245

    
246
    rot_vect = np.cross(target_vect, ref_vect)
247
    if np.allclose(rot_vect, 0):
248
        cart_axes = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
249
        axis = cart_axes[int(np.argmax([np.linalg.norm(np.cross(ax, ref_vect))
250
                                        for ax in cart_axes]))]
251
        rot_vect = np.cross(ref_vect, axis)
252
    rot_angle = -get_vect_angle(ref_vect, target_vect, rot_vect)
253
    molec.rotate(rot_angle, rot_vect, ctr_coord)
254
    if dummy_atom:
255
        del molec[-1]
256
    return molec
257

    
258

    
259
def add_adsorbate(slab, adsorbate, site_coord, ctr_coord, height, offset=None,
260
                  norm_vect=(0, 0, 1)):
261
    """Add an adsorbate to a surface.
262

263
    This function extends the functionality of ase.build.add_adsorbate
264
    (https://wiki.fysik.dtu.dk/ase/ase/build/surface.html#ase.build.add_adsorbate)
265
    by enabling to change the z coordinate and the axis perpendicular to the
266
    surface.
267
    @param slab: ase.Atoms object containing the coordinates of the surface
268
    @param adsorbate: ase.Atoms object containing the coordinates of the
269
        adsorbate.
270
    @param site_coord: The coordinates of the adsorption site on the surface.
271
    @param ctr_coord: The coordinates of the adsorption center in the molecule.
272
    @param height: The height above the surface where to adsorb.
273
    @param offset: Offsets the adsorbate by a number of unit cells. Mostly
274
        useful when adding more than one adsorbate.
275
    @param norm_vect: The vector perpendicular to the surface.
276
    """
277
    from copy import deepcopy
278
    info = slab.info.get('adsorbate_info', {})
279
    pos = np.array([0.0, 0.0, 0.0])  # part of absolute coordinates
280
    spos = np.array([0.0, 0.0, 0.0])  # part relative to unit cell
281
    norm_vect_u = np.array(norm_vect) / np.linalg.norm(norm_vect)
282
    if offset is not None:
283
        spos += np.asarray(offset, float)
284
    if isinstance(site_coord, str):
285
        # A site-name:
286
        if 'sites' not in info:
287
            raise TypeError('If the atoms are not made by an ase.build '
288
                            'function, position cannot be a name.')
289
        if site_coord not in info['sites']:
290
            raise TypeError('Adsorption site %s not supported.' % site_coord)
291
        spos += info['sites'][site_coord]
292
    else:
293
        pos += site_coord
294
    if 'cell' in info:
295
        cell = info['cell']
296
    else:
297
        cell = slab.get_cell()
298
    pos += np.dot(spos, cell)
299
    # Convert the adsorbate to an Atoms object
300
    if isinstance(adsorbate, ase.Atoms):
301
        ads = deepcopy(adsorbate)
302
    elif isinstance(adsorbate, ase.Atom):
303
        ads = ase.Atoms([adsorbate])
304
    else:
305
        # Assume it is a string representing a single Atom
306
        ads = ase.Atoms([ase.Atom(adsorbate)])
307
    pos += height * norm_vect_u
308
    # Move adsorbate into position
309
    ads.translate(pos - ctr_coord)
310
    # Attach the adsorbate
311
    slab.extend(ads)
312

    
313

    
314
def check_collision(slab_molec, slab_num_atoms, min_height, vect, nn_slab=0,
315
                    nn_molec=0, coll_coeff=1.2):
316
    """Checks whether a slab and a molecule collide or not.
317

318
    @param slab_molec: The system of adsorbate-slab for which to detect if there
319
        are collisions.
320
    @param nn_slab: Number of neigbors in the surface.
321
    @param nn_molec: Number of neighbors in the molecule.
322
    @param coll_coeff: The coefficient that multiplies the covalent radius of
323
        atoms resulting in a distance that two atoms being closer to that is
324
        considered as atomic collision.
325
    @param slab_num_atoms: Number of atoms of the bare slab.
326
    @param min_height: The minimum height atoms can have to not be considered as
327
        colliding.
328
    @param vect: The vector perpendicular to the slab.
329
    @return: bool, whether the surface and the molecule collide.
330
    """
331
    from ase.neighborlist import natural_cutoffs, neighbor_list
332

    
333
    # Check structure overlap by height
334
    if min_height is not False:
335
        cart_axes = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0],
336
                     [-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, -1.0]]
337
        if vect.tolist() not in cart_axes:
338
            err_msg = "'min_coll_height' option is only implemented for " \
339
                      "'surf_norm_vect' to be one of the x, y or z axes. "
340
            logger.error(err_msg)
341
            raise ValueError(err_msg)
342
        for atom in slab_molec[slab_num_atoms:]:
343
            for i, coord in enumerate(vect):
344
                if coord == 0:
345
                    continue
346
                if atom.position[i] * coord < min_height * coord:
347
                    return True
348

    
349
    # Check structure overlap by sphere collision
350
    if coll_coeff is not False:
351
        slab_molec_cutoffs = natural_cutoffs(slab_molec, mult=coll_coeff)
352
        slab_molec_nghbs = len(
353
            neighbor_list("i", slab_molec, slab_molec_cutoffs))
354
        if slab_molec_nghbs > nn_slab + nn_molec:
355
            return True
356

    
357
    return False
358

    
359

    
360
def correct_coll(molec, slab, ctr_coord, site_coord, num_pts,
361
                 min_coll_height, norm_vect, slab_nghbs, molec_nghbs,
362
                 coll_coeff, height=2.5):
363
    # TODO Rethink this function
364
    """Tries to adsorb a molecule on a slab trying to avoid collisions by doing
365
    small rotations.
366

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

    
406

    
407
def dissociate_h(slab_molec_orig, h_idx, num_atoms_slab, h_acceptor,
408
                 neigh_cutoff=1):
409
    # TODO rethink
410
    """Tries to dissociate a H from the molecule and adsorbs it on the slab.
411

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

    
449
    if np.linalg.norm(surf_h_vect) != np.infty:
450
        trans_vect = surf_h_vect - surf_h_vect / np.linalg.norm(surf_h_vect)
451
        slab_molec[h_idx].position = slab_molec[h_idx].position + trans_vect
452
        return slab_molec
453

    
454

    
455
def dissociation(slab_molec, h_donor, h_acceptor, num_atoms_slab):
456
    # TODO multiple dissociation
457
    """Decides which H atoms to dissociate according to a list of atoms.
458

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

    
504

    
505
def ads_euler(orig_molec, slab, ctr_coord, site_coord, num_pts,
506
              min_coll_height, coll_coeff, norm_vect, slab_nghbs, molec_nghbs,
507
              h_donor, h_acceptor):
508
    """Generates adsorbate-surface structures by sampling over Euler angles.
509

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

    
559
    return slab_ads_list
560

    
561

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

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

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

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

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

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

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

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

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

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

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

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

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

    
624
    # Check if dihedral angles can be defined
625
    do_dihedral = dihedral_angle_target is not None
626
    do_mol_dihedral = mol_dihedral_angle_target is not None
627
    dihedral_use_mol2 = False
628
    # Check if vect_surf and vect_inter are not aligned
629
    if np.allclose(np.cross(vect_surf, vect_inter), 0):
630
        logger.warning(
631
            "Surface atoms are incompatible with adsorption "
632
            "direction/bond. An adsorption dihedral angle cannot be defined.")
633
        do_dihedral = False
634
    # Check if requested bond angle is not flat
635
    if np.isclose((bond_angle_target + 90) % 180 - 90, 0):
636
        logger.warning("Requested bond angle is flat. Only a single dihedral "
637
                       "angle can be defined (ctr2_surf, ctr1_surf, ctr2_mol, "
638
                       "ctr3_mol).")
639
        do_mol_dihedral = False
640
        dihedral_use_mol2 = True
641
        logger.warning("Dihedral adsorption angle rotation will be perfomed "
642
                       "with (ctr2_surf, ctr1_surf, ctr2_mol, ctr3_mol).")
643
    # Check if vect_mol and vect2_mol are not aligned
644
    if np.allclose(np.cross(vect_mol, vect2_mol), 0):
645
        logger.warning("Adsorbates atoms are aligned. An adsorbate dihedral "
646
                       "angle cannot be defined.")
647
        do_mol_dihedral = False
648
    if not do_dihedral:
649
        logger.warning("Dihedral adsorption angle rotation will not be "
650
                       "performed.")
651
    if not do_mol_dihedral:
652
        logger.warning("Adsorbate dihedral angle rotation will not be "
653
                       "performed.")
654

    
655
    ###########################
656
    #       Translation       #
657
    ###########################
658

    
659
    # Compute and apply translation of adsorbate
660
    translation = bond_vector - vect_inter
661
    molecule.translate(translation)
662

    
663
    # Update adsorption bond
664
    vect_inter = get_atom_coords(molecule, ctr1_mol) - \
665
        get_atom_coords(surf, ctr1_surf)
666

    
667
    # Check if translation was successful
668
    if np.allclose(vect_inter, bond_vector):
669
        pass  # print("Translation successfully applied (error: ~ {:.5g} unit "
670
        # "length)".format(np.linalg.norm(vect_inter - bond_vector)))
671
    else:
672
        err = 'An unknown error occured during the translation'
673
        logger.error(err)
674
        raise AssertionError(err)
675

    
676
    ###########################
677
    #   Bond angle rotation   #
678
    ###########################
679

    
680
    # Compute rotation vector
681
    rotation_vector = np.cross(-vect_inter, vect_mol)
682
    if np.allclose(rotation_vector, 0, atol=1e-5):
683
        # If molecular bonds are aligned, any vector orthogonal to vect_inter
684
        # can be used Such vector can be found as the orthogonal rejection of
685
        # either X-axis, Y-axis or Z-axis onto vect_inter (since they cannot
686
        # be all aligned)
687
        non_aligned_vector = np.zeros(3)
688
        # Select the most orthogonal axis (lowest dot product):
689
        non_aligned_vector[np.argmin(np.abs(vect_inter))] = 1
690
        rotation_vector = non_aligned_vector - np.dot(non_aligned_vector,
691
                                                      vect_inter) / np.dot(
692
            vect_inter, vect_inter) * vect_inter
693

    
694
    # Retrieve current bond angle
695
    bond_angle_ini = get_vect_angle(-vect_inter, vect_mol, rotation_vector)
696

    
697
    # Apply rotation to reach desired bond_angle
698
    molecule.rotate(bond_angle_target - bond_angle_ini, v=rotation_vector,
699
                    center=get_atom_coords(molecule, ctr1_mol))
700

    
701
    # Update molecular bonds
702
    vect_mol = get_atom_coords(molecule, ctr2_mol) - get_atom_coords(molecule,
703
                                                                     ctr1_mol)
704
    vect2_mol = get_atom_coords(molecule, ctr3_mol) - get_atom_coords(molecule,
705
                                                                      ctr2_mol)
706

    
707
    # Check if rotation was successful
708
    bond_angle = get_vect_angle(-vect_inter, vect_mol)
709
    if np.isclose((bond_angle - bond_angle_target + 90) % 180 - 90, 0,
710
                  atol=1e-3) and np.allclose(get_atom_coords(molecule, ctr1_mol)
711
                                             - get_atom_coords(surf,
712
                                                               ctr1_surf),
713
                                             vect_inter):
714
        pass  # print("Rotation successfully applied (error: {:.5f}°)".format(
715
        # (bond_angle - bond_angle_target + 90) % 180 - 90))
716
    else:
717
        err = 'An unknown error occured during the rotation'
718
        logger.error(err)
719
        raise AssertionError(err)
720

    
721
    ###########################
722
    # Dihedral angle rotation #
723
    ###########################
724

    
725
    # Perform dihedral rotation if possible
726
    if do_dihedral:
727
        # Retrieve current dihedral angle (by computing the angle between the
728
        # vector rejection of vect_surf and vect_mol onto vect_inter)
729
        vect_inter_inner = np.dot(vect_inter, vect_inter)
730
        vect_surf_reject = vect_surf - np.dot(vect_surf, vect_inter) / \
731
            vect_inter_inner * vect_inter
732
        if dihedral_use_mol2:
733
            vect_mol_reject = vect2_mol - np.dot(vect2_mol, vect_inter) / \
734
                              vect_inter_inner * vect_inter
735
        else:
736
            vect_mol_reject = vect_mol - np.dot(vect_mol, vect_inter) / \
737
                              vect_inter_inner * vect_inter
738
        dihedral_angle_ini = get_vect_angle(vect_surf_reject, vect_mol_reject,
739
                                            vect_inter)
740

    
741
        # Apply dihedral rotation along vect_inter
742
        molecule.rotate(dihedral_angle_target - dihedral_angle_ini,
743
                        v=vect_inter, center=get_atom_coords(molecule,
744
                                                             ctr1_mol))
745

    
746
        # Update molecular bonds
747
        vect_mol = get_atom_coords(molecule, ctr2_mol) - \
748
            get_atom_coords(molecule, ctr1_mol)
749
        vect2_mol = get_atom_coords(molecule, ctr3_mol) - \
750
            get_atom_coords(molecule, ctr2_mol)
751

    
752
        # Check if rotation was successful
753
        # Check dihedral rotation
754
        if dihedral_use_mol2:
755
            vect_mol_reject = vect2_mol - np.dot(vect2_mol, vect_inter) / \
756
                              vect_inter_inner * vect_inter
757
        else:
758
            vect_mol_reject = vect_mol - np.dot(vect_mol, vect_inter) / \
759
                              vect_inter_inner * vect_inter
760
        dihedral_angle = get_vect_angle(vect_surf_reject, vect_mol_reject,
761
                                        vect_inter)
762
        # Check bond rotation is unmodified
763
        bond_angle = get_vect_angle(-vect_inter, vect_mol)
764
        if np.isclose((dihedral_angle - dihedral_angle_target + 90) % 180 - 90,
765
                      0, atol=1e-3) \
766
                and np.isclose((bond_angle - bond_angle_target + 90) %
767
                               180 - 90, 0, atol=1e-5) \
768
                and np.allclose(get_atom_coords(molecule, ctr1_mol)
769
                                - get_atom_coords(surf, ctr1_surf),
770
                                vect_inter):
771
            pass  # print( "Dihedral rotation successfully applied (error: {
772
            # :.5f}°)".format((dihedral_angle - dihedral_angle_target + 90) %
773
            # 180 - 90))
774
        else:
775
            err = 'An unknown error occured during the dihedral rotation'
776
            logger.error(err)
777
            raise AssertionError(err)
778

    
779
    #####################################
780
    # Adsorbate dihedral angle rotation #
781
    #####################################
782

    
783
    # Perform adsorbate dihedral rotation if possible
784
    if do_mol_dihedral:
785
        # Retrieve current adsorbate dihedral angle (by computing the angle
786
        # between the orthogonal rejection of vect_inter and vect2_mol onto
787
        # vect_mol)
788
        vect_mol_inner = np.dot(vect_mol, vect_mol)
789
        bond_inter_reject = -vect_inter - np.dot(-vect_inter, vect_mol) / \
790
            vect_mol_inner * vect_mol
791
        bond2_mol_reject = vect2_mol - np.dot(vect2_mol, vect_mol) / \
792
            vect_mol_inner * vect_mol
793
        dihedral_angle_ini = get_vect_angle(bond_inter_reject,
794
                                            bond2_mol_reject, vect_mol)
795

    
796
        # Apply dihedral rotation along vect_mol
797
        molecule.rotate(mol_dihedral_angle_target - dihedral_angle_ini,
798
                        v=vect_mol, center=get_atom_coords(molecule, ctr1_mol))
799

    
800
        # Update molecular bonds
801
        vect_mol = get_atom_coords(molecule, ctr2_mol) \
802
            - get_atom_coords(molecule, ctr1_mol)
803
        vect2_mol = get_atom_coords(molecule, ctr3_mol) \
804
            - get_atom_coords(molecule, ctr2_mol)
805

    
806
        # Check if rotation was successful
807
        # Check adsorbate dihedral rotation
808
        vect_mol_inner = np.dot(vect_mol, vect_mol)
809
        bond2_mol_reject = vect2_mol - np.dot(vect2_mol, vect_mol) / \
810
            vect_mol_inner * vect_mol
811
        mol_dihedral_angle = get_vect_angle(bond_inter_reject,
812
                                            bond2_mol_reject, vect_mol)
813
        # Check dihedral rotation
814
        vect_inter_inner = np.dot(vect_inter, vect_inter)
815
        vect_surf_reject = vect_surf - np.dot(vect_surf, vect_inter) / \
816
            vect_inter_inner * vect_inter
817
        vect_mol_reject = vect_mol - np.dot(vect_mol, vect_inter) / \
818
            vect_inter_inner * vect_inter
819
        dihedral_angle = get_vect_angle(vect_surf_reject, vect_mol_reject,
820
                                        vect_inter)
821
        # Check bond rotation is unmodified
822
        bond_angle = get_vect_angle(-vect_inter, vect_mol)
823
        if np.isclose((mol_dihedral_angle - mol_dihedral_angle_target + 90) %
824
                      180 - 90, 0, atol=1e-3) \
825
                and np.isclose((dihedral_angle -
826
                                dihedral_angle_target + 90) % 180 - 90, 0,
827
                               atol=1e-5) \
828
                and np.isclose((bond_angle - bond_angle_target + 90) % 180 - 90,
829
                               0, atol=1e-5) \
830
                and np.allclose(get_atom_coords(molecule, ctr1_mol) -
831
                                get_atom_coords(surf, ctr1_surf),
832
                                vect_inter):
833
            pass  # print(
834
            # "Adsorbate dihedral rotation successfully applied (error:
835
            # {:.5f}°)".format((mol_dihedral_angle - mol_dihedral_angle_target
836
            # + 90) % 180 - 90))
837
        else:
838
            err = 'An unknown error occured during the adsorbate dihedral ' \
839
                  'rotation'
840
            logger.error(err)
841
            raise AssertionError(err)
842

    
843

    
844
def ads_chemcat(orig_molec, slab, ctr1_mol, ctr2_mol, ctr3_mol, ctr1_surf,
845
                ctr2_surf, num_pts, min_coll_height, coll_coeff, norm_vect,
846
                slab_nghbs, molec_nghbs, h_donor, h_acceptor, max_hel):
847
    """Generates adsorbate-surface structures by sampling over chemcat angles.
848

849
    @param orig_molec: ase.Atoms object of the molecule to adsorb (adsorbate).
850
    @param slab: ase.Atoms object of the surface on which to adsorb the molecule
851
    @param ctr1_mol: The index/es of the center in the adsorbate to use as
852
        adsorption center.
853
    @param ctr2_mol: The index/es of the center in the adsorbate to use for the
854
        definition of the surf-adsorbate angle, surf-adsorbate dihedral angle
855
        and adsorbate dihedral angle.
856
    @param ctr3_mol: The index/es of the center in the adsorbate to use for the
857
        definition of the adsorbate dihedral angle.
858
    @param ctr1_surf: The index/es of the center in the surface to use as
859
        adsorption center.
860
    @param ctr2_surf: The index/es of the center in the surface to use for the
861
        definition of the surf-adsorbate dihedral angle.
862
    @param num_pts: Number on which to sample Euler angles.
863
    @param min_coll_height: The lowest height for which to detect a collision
864
    @param coll_coeff: The coefficient that multiplies the covalent radius of
865
        atoms resulting in a distance that two atoms being closer to that is
866
        considered as atomic collision.
867
    @param norm_vect: The vector perpendicular to the surface.
868
    @param slab_nghbs: Number of neigbors in the surface.
869
    @param molec_nghbs: Number of neighbors in the molecule.
870
    @param h_donor: List of atom types or atom numbers that are H-donors.
871
    @param h_acceptor: List of atom types or atom numbers that are H-acceptors.
872
    @param max_hel: Maximum value for sampling the helicopter
873
        (surf-adsorbate dihedral) angle.
874
    @return: list of ase.Atoms object conatining all the orientations of a given
875
        conformer.
876
    """
877
    from copy import deepcopy
878
    slab_ads_list = []
879
    # Rotation over bond angle # TODO Check sampling
880
    for alpha in np.arange(90, 180, 90 / min(num_pts, 2)):
881
        # Rotation over surf-adsorbate dihedral angle
882
        for beta in np.arange(0, max_hel, max_hel / num_pts):
883
            # Rotation over adsorbate bond dihedral angle
884
            for gamma in np.arange(0, 360, 360 / num_pts):
885
                new_molec = deepcopy(orig_molec)
886
                chemcat_rotate(new_molec, slab, ctr1_mol, ctr2_mol, ctr3_mol,
887
                               ctr1_surf, ctr2_surf, norm_vect, alpha,
888
                               beta, gamma)
889
                site_coords = get_atom_coords(slab, ctr1_surf)
890
                ctr_coords = get_atom_coords(new_molec, ctr1_mol)
891
                slab_molec, collision = correct_coll(new_molec, slab,
892
                                                     ctr_coords, site_coords,
893
                                                     num_pts, min_coll_height,
894
                                                     norm_vect, slab_nghbs,
895
                                                     molec_nghbs, coll_coeff)
896
                if not collision and \
897
                        not any([np.allclose(slab_molec.positions,
898
                                             conf.positions)
899
                                 for conf in slab_ads_list]):
900
                    slab_ads_list.append(slab_molec)
901
                    if h_donor is not False:
902
                        slab_ads_list.extend(dissociation(slab_molec, h_donor,
903
                                                          h_acceptor,
904
                                                          len(slab)))
905

    
906
    return slab_ads_list
907

    
908

    
909
def adsorb_confs(conf_list, surf, inp_vars):
910
    """Generates a number of adsorbate-surface structure coordinates.
911

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

    
932
    if inp_vars['pbc_cell'] is not False:
933
        surf.set_pbc(True)
934
        surf.set_cell(inp_vars['pbc_cell'])
935

    
936
    surf_ads_list = []
937
    sites_coords = get_atom_coords(surf, sites)
938
    if coll_coeff is not False:
939
        surf_cutoffs = natural_cutoffs(surf, mult=coll_coeff)
940
        surf_nghbs = len(neighbor_list("i", surf, surf_cutoffs))
941
    else:
942
        surf_nghbs = 0
943
    for i, conf in enumerate(conf_list):
944
        molec_ctr_coords = get_atom_coords(conf, molec_ctrs)
945
        if inp_vars['pbc_cell'] is not False:
946
            conf.set_pbc(True)
947
            conf.set_cell(inp_vars['pbc_cell'])
948
        if coll_coeff is not False:
949
            conf_cutoffs = natural_cutoffs(conf, mult=coll_coeff)
950
            molec_nghbs = len(neighbor_list("i", conf, conf_cutoffs))
951
        else:
952
            molec_nghbs = 0
953
        for s, site in enumerate(sites_coords):
954
            if inp_norm_vect == 'auto':
955
                norm_vect = compute_norm_vect(surf, sites[s],
956
                                              inp_vars['pbc_cell'])
957
            else:
958
                norm_vect = inp_norm_vect
959
            for c, ctr in enumerate(molec_ctr_coords):
960
                if angles == 'euler':
961
                    surf_ads_list.extend(ads_euler(conf, surf, ctr, site,
962
                                                   num_pts, min_coll_height,
963
                                                   coll_coeff, norm_vect,
964
                                                   surf_nghbs, molec_nghbs,
965
                                                   h_donor, h_acceptor))
966
                elif angles == 'chemcat':
967
                    mol_ctr1 = molec_ctrs[c]
968
                    mol_ctr2 = inp_vars["molec_ctrs2"][c]
969
                    mol_ctr3 = inp_vars["molec_ctrs3"][c]
970
                    surf_ctr1 = sites[s]
971
                    surf_ctr2 = inp_vars["surf_ctrs2"][s]
972
                    max_h = inp_vars["max_helic_angle"]
973
                    surf_ads_list.extend(ads_chemcat(conf, surf, mol_ctr1,
974
                                                     mol_ctr2, mol_ctr3,
975
                                                     surf_ctr1, surf_ctr2,
976
                                                     num_pts, min_coll_height,
977
                                                     coll_coeff, norm_vect,
978
                                                     surf_nghbs, molec_nghbs,
979
                                                     h_donor, h_acceptor,
980
                                                     max_h))
981
    return surf_ads_list
982

    
983

    
984
def run_screening(inp_vars):
985
    """Carries out the screening of adsorbate structures on a surface.
986

987
    @param inp_vars: Calculation parameters from input file.
988
    """
989
    import os
990
    import random
991
    from modules.formats import collect_coords, adapt_format
992
    from modules.calculation import run_calc, check_finished_calcs
993

    
994
    logger.info('Carrying out procedures for the screening of adsorbate-surface'
995
                ' structures.')
996
    if not os.path.isdir("isolated"):
997
        err = "'isolated' directory not found. It is needed in order to carry "
998
        "out the screening of structures to be adsorbed"
999
        logger.error(err)
1000
        raise FileNotFoundError(err)
1001

    
1002
    finished_calcs, unfinished_calcs = check_finished_calcs('isolated',
1003
                                                            inp_vars['code'])
1004
    if not finished_calcs:
1005
        err_msg = "No calculations on 'isolated' finished normally."
1006
        logger.error(err_msg)
1007
        raise FileNotFoundError(err_msg)
1008

    
1009
    logger.info(f"Found {len(finished_calcs)} structures of isolated "
1010
                f"conformers whose calculation finished normally.")
1011
    if len(unfinished_calcs) != 0:
1012
        logger.warning(f"Found {len(unfinished_calcs)} calculations more that "
1013
                       f"did not finish normally: {unfinished_calcs}. \n"
1014
                       f"Using only the ones that finished normally: "
1015
                       f"{finished_calcs}.")
1016

    
1017
    conformer_atoms_list = collect_coords(finished_calcs, inp_vars['code'],
1018
                                          'isolated', inp_vars['special_atoms'])
1019
    selected_confs = select_confs(conformer_atoms_list, finished_calcs,
1020
                                  inp_vars['select_magns'],
1021
                                  inp_vars['confs_per_magn'],
1022
                                  inp_vars['code'])
1023
    surf = adapt_format('ase', inp_vars['surf_file'], inp_vars['special_atoms'])
1024
    surf_ads_list = adsorb_confs(selected_confs, surf, inp_vars)
1025
    if len(surf_ads_list) > inp_vars['max_structures']:
1026
        surf_ads_list = random.sample(surf_ads_list, inp_vars['max_structures'])
1027
    logger.info(f'Generated {len(surf_ads_list)} adsorbate-surface atomic '
1028
                f'configurations to carry out a calculation of.')
1029

    
1030
    run_calc('screening', inp_vars, surf_ads_list)
1031
    logger.info('Finished the procedures for the screening of adsorbate-surface'
1032
                ' structures section.')