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

dockonsurf / modules / screening.py @ 1297d18b

Historique | Voir | Annoter | Télécharger (49,7 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().sum()
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_molec.info = {**slab_molec.info, **molec.info}
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 internal_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 bond_vector are aligned
629
    if np.allclose(np.cross(vect_surf, bond_vector), 0):
630
        do_dihedral = False
631
    # Check if requested bond angle is not flat
632
    if np.isclose((bond_angle_target + 90) % 180 - 90, 0):
633
        do_mol_dihedral = False
634
        dihedral_use_mol2 = True
635
    # Check if vect_mol and vect2_mol are not aligned
636
    if np.allclose(np.cross(vect_mol, vect2_mol), 0):
637
        do_mol_dihedral = False
638

    
639
    ###########################
640
    #       Translation       #
641
    ###########################
642

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

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

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

    
660
    ###########################
661
    #   Bond angle rotation   #
662
    ###########################
663

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

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

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

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

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

    
705
    ###########################
706
    # Dihedral angle rotation #
707
    ###########################
708

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

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

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

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

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

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

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

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

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

    
827

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

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

    
900
    return slab_ads_list
901

    
902

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

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

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

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

    
1002

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

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

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

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

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

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

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