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

dockonsurf / modules / screening.py @ b516a1d6

Historique | Voir | Annoter | Télécharger (23,76 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=False):
70
    """Computes the angle between two vectors.
71

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

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

    
91

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

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

    
109

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

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

    
141
            logger.error(err)
142
            raise ValueError
143
    return np.array(coords)
144

    
145

    
146
def add_adsorbate(slab, adsorbate, site_coord, ctr_coord, height, offset=None,
147
                  norm_vect=(0, 0, 1)):
148
    """Add an adsorbate to a surface.
149

150
    This function extends the functionality of ase.build.add_adsorbate
151
    (https://wiki.fysik.dtu.dk/ase/ase/build/surface.html#ase.build.add_adsorbate)
152
    by enabling to change the z coordinate and the axis perpendicular to the
153
    surface.
154
    @param slab: ase.Atoms object containing the coordinates of the surface
155
    @param adsorbate: ase.Atoms object containing the coordinates of the
156
        adsorbate.
157
    @param site_coord: The coordinates of the adsorption site on the surface.
158
    @param ctr_coord: The coordinates of the adsorption center in the molecule.
159
    @param height: The height above the surface where to adsorb.
160
    @param offset: Offsets the adsorbate by a number of unit cells. Mostly
161
        useful when adding more than one adsorbate.
162
    @param norm_vect: The vector perpendicular to the surface.
163
    """
164
    from copy import deepcopy
165
    info = slab.info.get('adsorbate_info', {})
166
    pos = np.array([0.0, 0.0, 0.0])  # part of absolute coordinates
167
    spos = np.array([0.0, 0.0, 0.0])  # part relative to unit cell
168
    norm_vect_u = np.array(norm_vect) / np.linalg.norm(norm_vect)
169
    if offset is not None:
170
        spos += np.asarray(offset, float)
171
    if isinstance(site_coord, str):
172
        # A site-name:
173
        if 'sites' not in info:
174
            raise TypeError('If the atoms are not made by an ase.build '
175
                            'function, position cannot be a name.')
176
        if site_coord not in info['sites']:
177
            raise TypeError('Adsorption site %s not supported.' % site_coord)
178
        spos += info['sites'][site_coord]
179
    else:
180
        pos += site_coord
181
    if 'cell' in info:
182
        cell = info['cell']
183
    else:
184
        cell = slab.get_cell()
185
    pos += np.dot(spos, cell)
186
    # Convert the adsorbate to an Atoms object
187
    if isinstance(adsorbate, ase.Atoms):
188
        ads = deepcopy(adsorbate)
189
    elif isinstance(adsorbate, ase.Atom):
190
        ads = ase.Atoms([adsorbate])
191
    else:
192
        # Assume it is a string representing a single Atom
193
        ads = ase.Atoms([ase.Atom(adsorbate)])
194
    pos += height * norm_vect_u
195
    # Move adsorbate into position
196
    ads.translate(pos - ctr_coord)
197
    # Attach the adsorbate
198
    slab.extend(ads)
199

    
200

    
201
def check_collision(slab_molec, slab_num_atoms, min_height, vect, nn_slab=0,
202
                    nn_molec=0, coll_coeff=0.9):
203
    """Checks whether a slab and a molecule collide or not.
204

205
    @param slab_molec: The system of adsorbate-slab for which to detect if there
206
        are collisions.
207
    @param nn_slab: Number of neigbors in the surface.
208
    @param nn_molec: Number of neighbors in the molecule.
209
    @param coll_coeff: The coefficient that multiplies the covalent radius of
210
        atoms resulting in a distance that two atoms being closer to that is
211
        considered as atomic collision.
212
    @param slab_num_atoms: Number of atoms of the bare slab.
213
    @param min_height: The minimum height atoms can have to not be considered as
214
        colliding.
215
    @param vect: The vector perpendicular to the slab.
216
    @return: bool, whether the surface and the molecule collide.
217
    """
218
    from ase.neighborlist import natural_cutoffs, neighbor_list
219
    if min_height is not False:
220
        cart_axes = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0],
221
                     [-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, -1.0]]
222
        if vect.tolist() in cart_axes:
223
            for atom in slab_molec[slab_num_atoms:]:
224
                for i, coord in enumerate(vect):
225
                    if coord == 0:
226
                        continue
227
                    if atom.position[i] * coord < min_height:
228
                        return True
229
            return False
230
    else:
231
        slab_molec_cutoffs = natural_cutoffs(slab_molec, mult=coll_coeff)
232
        slab_molec_nghbs = len(
233
            neighbor_list("i", slab_molec, slab_molec_cutoffs))
234
        if slab_molec_nghbs > nn_slab + nn_molec:
235
            return True
236
        else:
237
            return False
238

    
239

    
240
def dissociate_h(slab_molec_orig, h_idx, num_atoms_slab, neigh_cutoff=1):
241
    # TODO rethink
242
    """Tries to dissociate a H from the molecule and adsorbs it on the slab.
243

244
    Tries to dissociate a H atom from the molecule and adsorb in on top of the
245
    surface if the distance is shorter than two times the neigh_cutoff value.
246
    @param slab_molec_orig: The ase.Atoms object of the system adsorbate-slab.
247
    @param h_idx: The index of the hydrogen atom to carry out adsorption of.
248
    @param num_atoms_slab: The number of atoms of the slab without adsorbate.
249
    @param neigh_cutoff: half the maximum distance between the surface and the
250
        H for it to carry out dissociation.
251
    @return: An ase.Atoms object of the system adsorbate-surface with H
252
    """
253
    from copy import deepcopy
254
    from ase.neighborlist import NeighborList
255
    slab_molec = deepcopy(slab_molec_orig)
256
    cutoffs = len(slab_molec) * [neigh_cutoff]
257
    nl = NeighborList(cutoffs, self_interaction=False, bothways=True)
258
    nl.update(slab_molec)
259
    surf_h_vect = np.array([np.infty] * 3)
260
    for neigh_idx in nl.get_neighbors(h_idx)[0]:
261
        if neigh_idx < num_atoms_slab:
262
            dist = np.linalg.norm(slab_molec[neigh_idx].position -
263
                                  slab_molec[h_idx].position)
264
            if dist < np.linalg.norm(surf_h_vect):
265
                surf_h_vect = slab_molec[neigh_idx].position \
266
                              - slab_molec[h_idx].position
267
    if np.linalg.norm(surf_h_vect) != np.infty:
268
        trans_vect = surf_h_vect - surf_h_vect / np.linalg.norm(surf_h_vect)
269
        slab_molec[h_idx].position = slab_molec[h_idx].position + trans_vect
270
        return slab_molec
271

    
272

    
273
def dissociation(slab_molec, disso_atoms, num_atoms_slab):
274
    # TODO rethink
275
    # TODO multiple dissociation
276
    """Decides which H atoms to dissociate according to a list of atoms.
277

278
    Given a list of chemical symbols or atom indices it checks for every atom
279
    or any of its neighbor if it's a H and calls dissociate_h to try to carry
280
    out dissociation of that H. For atom indices, it checks both whether
281
    the atom index or its neighbors are H, for chemical symbols, it only checks
282
    if there is a neighbor H.
283
    @param slab_molec: The ase.Atoms object of the system adsorbate-slab.
284
    @param disso_atoms: The indices or chemical symbols of the atoms
285
    @param num_atoms_slab:
286
    @return:
287
    """
288
    from ase.neighborlist import natural_cutoffs, NeighborList
289
    molec = slab_molec[num_atoms_slab:]
290
    cutoffs = natural_cutoffs(molec)
291
    nl = NeighborList(cutoffs, self_interaction=False, bothways=True)
292
    nl.update(molec)
293
    disso_structs = []
294
    for el in disso_atoms:
295
        if isinstance(el, int):
296
            if molec[el].symbol == 'H':
297
                disso_struct = dissociate_h(slab_molec, el + num_atoms_slab,
298
                                            num_atoms_slab)
299
                if disso_struct is not None:
300
                    disso_structs.append(disso_struct)
301
            else:
302
                for neigh_idx in nl.get_neighbors(el)[0]:
303
                    if molec[neigh_idx].symbol == 'H':
304
                        disso_struct = dissociate_h(slab_molec, neigh_idx +
305
                                                    num_atoms_slab,
306
                                                    num_atoms_slab)
307
                        if disso_struct is not None:
308
                            disso_structs.append(disso_struct)
309
        else:
310
            for atom in molec:
311
                if atom.symbol.lower() == el.lower():
312
                    for neigh_idx in nl.get_neighbors(atom.index)[0]:
313
                        if molec[neigh_idx].symbol == 'H':
314
                            disso_struct = dissociate_h(slab_molec, neigh_idx \
315
                                                        + num_atoms_slab,
316
                                                        num_atoms_slab)
317
                            if disso_struct is not None:
318
                                disso_structs.append(disso_struct)
319
    return disso_structs
320

    
321

    
322
def correct_coll(molec, slab, ctr_coord, site_coord, num_pts,
323
                 min_coll_height, norm_vect, slab_nghbs, molec_nghbs,
324
                 coll_coeff, height=2.5):
325
    # TODO Rethink this function
326
    """Tries to adsorb a molecule on a slab trying to avoid collisions by doing
327
    small rotations.
328

329
    @param molec: ase.Atoms object of the molecule to adsorb
330
    @param slab: ase.Atoms object of the surface on which to adsorb the
331
        molecule
332
    @param ctr_coord: The coordinates of the molecule to use as adsorption
333
        center.
334
    @param site_coord: The coordinates of the surface on which to adsorb the
335
        molecule
336
    @param num_pts: Number on which to sample Euler angles.
337
    @param min_coll_height: The lowermost height for which to detect a collision
338
    @param norm_vect: The vector perpendicular to the surface.
339
    @param slab_nghbs: Number of neigbors in the surface.
340
    @param molec_nghbs: Number of neighbors in the molecule.
341
    @param coll_coeff: The coefficient that multiplies the covalent radius of
342
        atoms resulting in a distance that two atoms being closer to that is
343
        considered as atomic collision.
344
    @param height: Height on which to try adsorption
345
    @return collision: bool, whether the structure generated has collisions
346
        between slab and adsorbate.
347
    """
348
    from copy import deepcopy
349
    slab_num_atoms = len(slab)
350
    collision = True
351
    max_corr = 6  # Should be an even number
352
    d_angle = 180 / ((max_corr / 2.0) * num_pts)
353
    num_corr = 0
354
    while collision and num_corr <= max_corr:
355
        k = num_corr * (-1) ** num_corr
356
        slab_molec = deepcopy(slab)
357
        molec.euler_rotate(k * d_angle, k * d_angle / 2, k * d_angle,
358
                           center=ctr_coord)
359
        add_adsorbate(slab_molec, molec, site_coord, ctr_coord, height,
360
                      norm_vect=norm_vect)
361
        collision = check_collision(slab_molec, slab_num_atoms, min_coll_height,
362
                                    norm_vect, slab_nghbs, molec_nghbs,
363
                                    coll_coeff)
364
        num_corr += 1
365
    return slab_molec, collision
366

    
367

    
368
def ads_euler(orig_molec, slab, ctr_coord, site_coord, num_pts,
369
              min_coll_height, coll_coeff, norm_vect, slab_nghbs, molec_nghbs,
370
              disso_atoms):
371
    """Generates adsorbate-surface structures by sampling over Euler angles.
372

373
    This function generates a number of adsorbate-surface structures at
374
    different orientations of the adsorbate sampled at multiple Euler (zxz)
375
    angles.
376
    @param orig_molec: ase.Atoms object of the molecule to adsorb
377
    @param slab: ase.Atoms object of the surface on which to adsorb the molecule
378
    @param ctr_coord: The coordinates of the molecule to use as adsorption
379
        center.
380
    @param site_coord: The coordinates of the surface on which to adsorb the
381
        molecule
382
    @param num_pts: Number on which to sample Euler angles.
383
    @param min_coll_height: The lowermost height for which to detect a collision
384
    @param coll_coeff: The coefficient that multiplies the covalent radius of
385
        atoms resulting in a distance that two atoms being closer to that is
386
        considered as atomic collision.
387
    @param norm_vect: The vector perpendicular to the surface.
388
    @param slab_nghbs: Number of neigbors in the surface.
389
    @param molec_nghbs: Number of neighbors in the molecule.
390
    @param disso_atoms: List of atom types or atom numbers to try to dissociate.
391
    @return: list of ase.Atoms object conatining all the orientations of a given
392
        conformer
393
    """
394
    from copy import deepcopy
395
    slab_ads_list = []
396
    # rotation around z
397
    for alpha in np.arange(0, 360, 360 / num_pts):
398
        # rotation around x'
399
        for beta in np.arange(0, 180, 180 / num_pts):
400
            # rotation around z'
401
            for gamma in np.arange(0, 360, 360 / num_pts):
402
                molec = deepcopy(orig_molec)
403
                molec.euler_rotate(alpha, beta, gamma, center=ctr_coord)
404
                slab_molec, collision = correct_coll(molec, slab,
405
                                                     ctr_coord, site_coord,
406
                                                     num_pts, min_coll_height,
407
                                                     norm_vect,
408
                                                     slab_nghbs, molec_nghbs,
409
                                                     coll_coeff)
410
                if not collision and \
411
                        not any([(slab_molec.positions == conf.positions).all()
412
                                 for conf in slab_ads_list]):
413
                    slab_ads_list.append(slab_molec)
414
                    slab_ads_list.extend(dissociation(slab_molec, disso_atoms,
415
                                                      len(slab)))
416

    
417
    return slab_ads_list
418

    
419

    
420
def ads_chemcat(site, ctr, pts_angle):
421
    return "TO IMPLEMENT"
422

    
423

    
424
def adsorb_confs(conf_list, surf, molec_ctrs, sites, algo, num_pts, neigh_ctrs,
425
                 norm_vect, min_coll_height, coll_coeff, disso_atoms):
426
    """Generates a number of adsorbate-surface structure coordinates.
427

428
    Given a list of conformers, a surface, a list of atom indices (or list of
429
    list of indices) of both the surface and the adsorbate, it generates a
430
    number of adsorbate-surface structures for every possible combination of
431
    them at different orientations.
432
    @param conf_list: list of ase.Atoms of the different conformers
433
    @param surf: the ase.Atoms object of the surface
434
    @param molec_ctrs: the list atom indices of the adsorbate.
435
    @param sites: the list of atom indices of the surface.
436
    @param algo: the algorithm to use for the generation of adsorbates.
437
    @param num_pts: the number of points per angle orientation to sample
438
    @param neigh_ctrs: the indices of the neighboring atoms to the adsorption
439
        atoms.
440
    @param norm_vect: The vector perpendicular to the surface.
441
    @param min_coll_height: The lowermost height for which to detect a collision
442
    @param coll_coeff: The coefficient that multiplies the covalent radius of
443
        atoms resulting in a distance that two atoms being closer to that is
444
        considered as atomic collision.
445
    @param disso_atoms: List of atom types or atom numbers to try to dissociate.
446
    @return: list of ase.Atoms for the adsorbate-surface structures
447
    """
448
    from ase.neighborlist import natural_cutoffs, neighbor_list
449
    surf_ads_list = []
450
    sites_coords = get_atom_coords(surf, sites)
451
    if min_coll_height is False:
452
        surf_cutoffs = natural_cutoffs(surf, mult=coll_coeff)
453
        surf_nghbs = len(neighbor_list("i", surf, surf_cutoffs))
454
    else:
455
        surf_nghbs = 0
456
    for conf in conf_list:
457
        molec_ctr_coords = get_atom_coords(conf, molec_ctrs)
458
        molec_neigh_coords = get_atom_coords(conf, neigh_ctrs)
459
        if min_coll_height is False:
460
            conf_cutoffs = natural_cutoffs(conf, mult=coll_coeff)
461
            molec_nghbs = len(neighbor_list("i", conf, conf_cutoffs))
462
        else:
463
            molec_nghbs = 0
464
        for site in sites_coords:
465
            for ctr in molec_ctr_coords:
466
                if algo == 'euler':
467
                    surf_ads_list.extend(ads_euler(conf, surf, ctr, site,
468
                                                   num_pts, min_coll_height,
469
                                                   coll_coeff, norm_vect,
470
                                                   surf_nghbs, molec_nghbs,
471
                                                   disso_atoms))
472
                elif algo == 'chemcat':
473
                    surf_ads_list.extend(ads_chemcat(site, ctr, num_pts))
474
    return surf_ads_list
475

    
476

    
477
def run_screening(inp_vars):
478
    """Carries out the screening of adsorbate structures on a surface.
479

480
    @param inp_vars: Calculation parameters from input file.
481
    """
482
    import os
483
    import random
484
    from modules.formats import collect_coords, adapt_format
485
    from modules.calculation import run_calc, check_finished_calcs
486

    
487
    logger.info('Carrying out procedures for the screening of adsorbate-surface'
488
                ' structures.')
489
    if not os.path.isdir("isolated"):
490
        err = "'isolated' directory not found. It is needed in order to carry "
491
        "out the screening of structures to be adsorbed"
492
        logger.error(err)
493
        raise FileNotFoundError(err)
494

    
495
    finished_calcs, unfinished_calcs = check_finished_calcs('isolated',
496
                                                            inp_vars['code'])
497
    logger.info(f"Found {len(finished_calcs)} structures of isolated "
498
                f"conformers whose calculation finished normally.")
499
    if len(unfinished_calcs) != 0:
500
        logger.warning(f"Found {len(unfinished_calcs)} calculations more that "
501
                       f"did not finish normally: {unfinished_calcs}. \n"
502
                       f"Using only the ones that finished normally: "
503
                       f"{finished_calcs}.")
504

    
505
    conformer_atoms_list = collect_coords(finished_calcs, inp_vars['code'],
506
                                          'isolated', inp_vars['special_atoms'])
507
    selected_confs = select_confs(conformer_atoms_list, finished_calcs,
508
                                  inp_vars['select_magns'],
509
                                  inp_vars['confs_per_magn'],
510
                                  inp_vars['code'])
511
    surf = adapt_format('ase', inp_vars['surf_file'], inp_vars['special_atoms'])
512
    surf_ads_list = adsorb_confs(selected_confs, surf,
513
                                 inp_vars['molec_ads_ctrs'], inp_vars['sites'],
514
                                 inp_vars['ads_algo'],
515
                                 inp_vars['sample_points_per_angle'],
516
                                 inp_vars['molec_neigh_ctrs'],
517
                                 inp_vars['surf_norm_vect'],
518
                                 inp_vars['min_coll_height'],
519
                                 inp_vars['collision_threshold'],
520
                                 inp_vars['disso_atoms'])
521
    if len(surf_ads_list) > inp_vars['max_structures']:
522
        surf_ads_list = random.sample(surf_ads_list, inp_vars['max_structures'])
523
    logger.info(f'Generated {len(surf_ads_list)} adsorbate-surface atomic '
524
                f'configurations to carry out a calculation of.')
525

    
526
    run_calc('screening', inp_vars, surf_ads_list)
527
    logger.info('Finished the procedures for the screening of adsorbate-surface'
528
                ' structures section.')