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

dockonsurf / modules / dos_input.py @ fe91ddb2

Historique | Voir | Annoter | Télécharger (33,5 ko)

1
"""Functions to deal with DockOnSurf input files.
2

3
Functions
4
try_command:Tries to run a command and logs its exceptions (expected and not).
5
str2lst: Converts a string of integers, and groups of them, to a list of lists.
6
check_expect_val: Checks whether the value of an option has an adequate value.
7
read_input: Sets up the calculation by reading the parameters from input file.
8
get_run_type: Gets 'run_type' value and checks that its value is acceptable.
9
get_code: Gets 'code' value and checks that its value is acceptable.
10
get_batch_q_sys: Gets 'batch_q_sys' value and checks that its value is
11
acceptable.
12
get_relaunch_err: Gets 'relaunch_err' value and checks that its value is
13
acceptable.
14
get_max_jobs: Gets 'max_jobs' value and checks that its value is acceptable.
15
get_special_atoms: Gets 'special_atoms' value and checks that its value is
16
acceptable.
17
get_isol_inp_file: Gets 'isol_inp_file' value and checks that its value is
18
acceptable.
19
get_cluster_magns: Gets 'cluster_magns' value and checks that its value is
20
acceptable.
21
get_num_conformers: Gets 'num_conformers' value and checks that its value is
22
acceptable.
23
get_num_prom_cand: Gets 'num_prom_cand' value and checks that its value is
24
acceptable.
25
get_iso_rmsd: Gets 'iso_rmsd' value and checks that its value is acceptable.
26
get_pre_opt: Gets 'pre_opt' value and checks that its value is acceptable.
27
get_screen_inp_file: Gets 'screen_inp_file' value and checks that its value is
28
acceptable.
29
get_sites: Gets 'sites' value and checks that its value is acceptable.
30
get_molec_ctrs: Gets 'molec_ctrs' value and checks that its value is
31
acceptable.
32
get_try_disso: Gets 'try_disso' value and checks that its value is acceptable.
33
get_pts_per_angle: Gets 'pts_per_angle' value and checks that its value is
34
acceptable.
35
get_coll_thrsld: Gets 'coll_thrsld' value and checks that its value is
36
acceptable.
37
get_screen_rmsd: Gets 'screen_rmsd' value and checks that its value is
38
acceptable.
39
get_coll_bottom_z: Gets 'coll_bottom_z' value and checks that its value is
40
acceptable.
41
get_refine_inp_file: Gets 'refine_inp_file' value and checks that its value is
42
acceptable.
43
get_energy_cutoff: Gets 'energy_cutoff' value and checks that its value is
44
acceptable.
45
"""
46
import os.path
47
import logging
48
from configparser import ConfigParser, NoSectionError, NoOptionError, \
49
    MissingSectionHeaderError, DuplicateOptionError
50
import numpy as np
51
from modules.utilities import try_command
52

    
53
logger = logging.getLogger('DockOnSurf')
54

    
55
dos_inp = ConfigParser(inline_comment_prefixes='#',
56
                       empty_lines_in_values=False)
57

    
58
new_answers = {'n': False, 'none': False, 'nay': False,
59
               'y': True, '': True, 'aye': True, 'sure': True}
60
for answer, val in new_answers.items():
61
    dos_inp.BOOLEAN_STATES[answer] = val
62
turn_false_answers = [answer for answer in dos_inp.BOOLEAN_STATES
63
                      if dos_inp.BOOLEAN_STATES[answer] is False]
64

    
65
no_sect_err = "Section '%s' not found on input file"
66
no_opt_err = "Option '%s' not found on section '%s'"
67
num_error = "'%s' value must be a %s"
68

    
69

    
70
def str2lst(cmplx_str, func=int):  # TODO: enable deeper level of nested lists
71
    # TODO Treat all-enclosing parenthesis as a list instead of list of lists.
72
    """Converts a string of integers, and groups of them, to a list.
73

74
    Keyword arguments:
75
    @param cmplx_str: str, string of integers (or floats) and groups of them
76
    enclosed by parentheses-like characters.
77
    - Group enclosers: '()' '[]' and '{}'.
78
    - Separators: ',' ';' and ' '.
79
    - Nested groups are not allowed: '3 ((6 7) 8) 4'.
80
    @param func: either to use int or float
81

82
    @return list, list of integers (or floats), or list of integers (or floats)
83
    in the case they were grouped. First, the singlets are placed, and then the
84
    groups in input order.
85

86
    eg. '128,(135 138;141] 87 {45, 68}' -> [128, 87, [135, 138, 141], [45, 68]]
87
    """
88

    
89
    # Checks
90
    error_msg = "Function argument should be a str, sequence of " \
91
                "numbers separated by ',' ';' or ' '." \
92
                "\nThey can be grouped in parentheses-like enclosers: '()', " \
93
                "'[]' or {}. Nested groups are not allowed. \n" \
94
                "eg. 128,(135 138;141) 87 {45, 68}"
95
    cmplx_str = try_command(cmplx_str.replace, [(AttributeError, error_msg)],
96
                            ',', ' ')
97

    
98
    cmplx_str = cmplx_str.replace(';', ' ').replace('[', '(').replace(
99
        ']', ')').replace('{', '(').replace('}', ')')
100

    
101
    try_command(list, [(ValueError, error_msg)], map(func, cmplx_str.replace(
102
        ')', '').replace('(', '').split()))
103

    
104
    deepness = 0
105
    for el in cmplx_str.split():
106
        if '(' in el:
107
            deepness += 1
108
        if ')' in el:
109
            deepness += -1
110
        if deepness > 1 or deepness < 0:
111
            logger.error(error_msg)
112
            raise ValueError(error_msg)
113

    
114
    init_list = cmplx_str.split()
115
    start_group = []
116
    end_group = []
117
    for i, element in enumerate(init_list):
118
        if '(' in element:
119
            start_group.append(i)
120
            init_list[i] = element.replace('(', '')
121
        if ')' in element:
122
            end_group.append(i)
123
            init_list[i] = element.replace(')', '')
124

    
125
    init_list = list(map(func, init_list))
126

    
127
    new_list = []
128
    for start_el, end_el in zip(start_group, end_group):
129
        new_list.append(init_list[start_el:end_el + 1])
130

    
131
    for v in new_list:
132
        for el in v:
133
            init_list.remove(el)
134
    return init_list + new_list
135

    
136

    
137
def check_expect_val(value, expect_vals, err_msg=None):
138
    """Checks whether an option lies within its expected values.
139

140
    Keyword arguments:
141
    @param value: The variable to check if its value lies within the expected
142
    ones
143
    @param expect_vals: list, list of values allowed for the present option.
144
    @param err_msg: The error message to be prompted in both log and screen.
145
    @raise ValueError: if the value is not among the expected ones.
146
    @return True if the value is among the expected ones.
147
    """
148
    if err_msg is None:
149
        err_msg = f"'{value}' is not an adequate value.\n" \
150
                  f"Adequate values: {expect_vals}"
151
    if not any([exp_val == value for exp_val in expect_vals]):
152
        logger.error(err_msg)
153
        raise ValueError(err_msg)
154

    
155
    return True
156

    
157

    
158
def check_inp_file(inp_file, code):
159
    if code == 'cp2k':
160
        from pycp2k import CP2K
161
        cp2k = CP2K()
162
        try_command(cp2k.parse,
163
                    [(UnboundLocalError, "Invalid CP2K input file")], inp_file)
164

    
165

    
166
# Global
167

    
168
def get_run_type():
169
    isolated, screening, refinement = (False, False, False)
170
    run_type_vals = ['isolated', 'screening', 'refinement', 'adsorption',
171
                     'full']
172
    run_types = dos_inp.get('Global', 'run_type').split()
173
    for run_type in run_types:
174
        check_expect_val(run_type.lower(), run_type_vals)
175
        if 'isol' in run_type.lower():
176
            isolated = True
177
        if 'screen' in run_type.lower():
178
            screening = True
179
        if 'refine' in run_type.lower():
180
            refinement = True
181
        if 'adsor' in run_type.lower():
182
            screening, refinement = (True, True)
183
        if 'full' in run_type.lower():
184
            isolated, screening, refinement = (True, True, True)
185

    
186
    return isolated, screening, refinement
187

    
188

    
189
def get_code():
190
    code_vals = ['cp2k']
191
    check_expect_val(dos_inp.get('Global', 'code').lower(), code_vals)
192
    code = dos_inp.get('Global', 'code').lower()
193
    return code
194

    
195

    
196
def get_batch_q_sys():
197
    batch_q_sys_vals = ['sge', 'lsf', 'irene', 'local'] + turn_false_answers
198
    check_expect_val(dos_inp.get('Global', 'batch_q_sys').lower(),
199
                     batch_q_sys_vals)
200
    batch_q_sys = dos_inp.get('Global', 'batch_q_sys').lower()
201
    if batch_q_sys.lower() in turn_false_answers:
202
        return False
203
    else:
204
        return batch_q_sys
205

    
206

    
207
def get_subm_script():
208
    subm_script = dos_inp.get('Global', 'subm_script', fallback=False)
209
    if subm_script and not os.path.isfile(subm_script):
210
        logger.error(f'File {subm_script} not found.')
211
        raise FileNotFoundError(f'File {subm_script} not found')
212
    return subm_script
213

    
214

    
215
def get_project_name():
216
    project_name = dos_inp.get('Global', 'project_name', fallback='')
217
    return project_name
218

    
219

    
220
def get_relaunch_err():
221
    relaunch_err_vals = ['geo_not_conv']
222
    relaunch_err = dos_inp.get('Global', 'relaunch_err',
223
                               fallback="False")
224
    if relaunch_err.lower() in turn_false_answers:
225
        return False
226
    else:
227
        check_expect_val(relaunch_err.lower(), relaunch_err_vals)
228
    return relaunch_err
229

    
230

    
231
def get_max_jobs():
232
    import re
233
    err_msg = "'max_jobs' must be a list of, number plus 'p', 'q' or 'r', or " \
234
              "a combination of them without repeating letters.\n" \
235
              "eg: '2r 3p 4pr', '5q' or '3r 3p'"
236
    max_jobs_str = dos_inp.get('Global', 'max_jobs', fallback="inf").lower()
237
    str_vals = ["r", "p", "q", "rp", "rq", "pr", "qr"]
238
    max_jobs = {"r": np.inf, "p": np.inf, "rp": np.inf}
239
    if "inf" == max_jobs_str:
240
        return {"r": np.inf, "p": np.inf, "rp": np.inf}
241
    # Iterate over the number of requirements:
242
    for req in max_jobs_str.split():
243
        # Split numbers from letters into a list
244
        req_parts = re.findall(r'[a-z]+|\d+', req)
245
        if len(req_parts) != 2:
246
            logger.error(err_msg)
247
            raise ValueError(err_msg)
248
        if req_parts[0].isdecimal():
249
            req_parts[1] = req_parts[1].replace('q', 'p').replace('pr', 'rp')
250
            if req_parts[1] in str_vals and max_jobs[req_parts[1]] == np.inf:
251
                max_jobs[req_parts[1]] = int(req_parts[0])
252
        elif req_parts[1].isdecimal():
253
            req_parts[0] = req_parts[0].replace('q', 'p').replace('pr', 'rp')
254
            if req_parts[0] in str_vals and max_jobs[req_parts[0]] == np.inf:
255
                max_jobs[req_parts[0]] = int(req_parts[1])
256
        else:
257
            logger.error(err_msg)
258
            raise ValueError(err_msg)
259

    
260
    return max_jobs
261

    
262

    
263
def get_special_atoms():
264
    from ase.data import chemical_symbols
265

    
266
    spec_at_err = '\'special_atoms\' does not have an adequate format.\n' \
267
                  'Adequate format: (Fe1 Fe) (O1 O)'
268
    special_atoms = dos_inp.get('Global', 'special_atoms', fallback="False")
269
    if special_atoms.lower() in turn_false_answers:
270
        special_atoms = []
271
    else:
272
        # Converts the string into a list of tuples
273
        lst_tple = [tuple(pair.replace("(", "").split()) for pair in
274
                    special_atoms.split(")")[:-1]]
275
        if len(lst_tple) == 0:
276
            logger.error(spec_at_err)
277
            raise ValueError(spec_at_err)
278
        for i, tup in enumerate(lst_tple):
279
            if not isinstance(tup, tuple) or len(tup) != 2:
280
                logger.error(spec_at_err)
281
                raise ValueError(spec_at_err)
282
            if tup[1].capitalize() not in chemical_symbols:
283
                elem_err = "The second element of the couple should be an " \
284
                           "actual element of the periodic table"
285
                logger.error(elem_err)
286
                raise ValueError(elem_err)
287
            if tup[0].capitalize() in chemical_symbols:
288
                elem_err = "The first element of the couple is already an " \
289
                           "actual element of the periodic table, "
290
                logger.error(elem_err)
291
                raise ValueError(elem_err)
292
            for j, tup2 in enumerate(lst_tple):
293
                if j <= i:
294
                    continue
295
                if tup2[0] == tup[0]:
296
                    label_err = f'You have specified the label {tup[0]} to ' \
297
                                f'more than one special atom'
298
                    logger.error(label_err)
299
                    raise ValueError(label_err)
300
        special_atoms = lst_tple
301
    return special_atoms
302

    
303

    
304
# Isolated
305

    
306
def get_isol_inp_file():
307
    isol_inp_file = dos_inp.get('Isolated', 'isol_inp_file')
308
    if not os.path.isfile(isol_inp_file):
309
        logger.error(f'File {isol_inp_file} not found.')
310
        raise FileNotFoundError(f'File {isol_inp_file} not found')
311
    return isol_inp_file
312

    
313

    
314
def get_molec_file():
315
    molec_file = dos_inp.get('Isolated', 'molec_file')
316
    if not os.path.isfile(molec_file):
317
        logger.error(f'File {molec_file} not found.')
318
        raise FileNotFoundError(f'File {molec_file} not found')
319
    return molec_file
320

    
321

    
322
def get_num_conformers():
323
    err_msg = num_error % ('num_conformers', 'positive integer')
324
    num_conformers = try_command(dos_inp.getint, [(ValueError, err_msg)],
325
                                 'Isolated', 'num_conformers', fallback=100)
326
    if num_conformers < 1:
327
        logger.error(err_msg)
328
        raise ValueError(err_msg)
329
    return num_conformers
330

    
331

    
332
def get_pre_opt():
333
    pre_opt_vals = ['uff', 'mmff'] + turn_false_answers
334
    check_expect_val(dos_inp.get('Isolated', 'pre_opt').lower(), pre_opt_vals)
335
    pre_opt = dos_inp.get('Isolated', 'pre_opt').lower()
336
    if pre_opt in turn_false_answers:
337
        return False
338
    else:
339
        return pre_opt
340

    
341

    
342
# Screening
343

    
344
def get_screen_inp_file():
345
    screen_inp_file = dos_inp.get('Screening', 'screen_inp_file')
346
    if not os.path.isfile(screen_inp_file):
347
        logger.error(f'File {screen_inp_file} not found.')
348
        raise FileNotFoundError(f'File {screen_inp_file} not found')
349
    return screen_inp_file
350

    
351

    
352
def get_surf_file():
353
    surf_file = dos_inp.get('Screening', 'surf_file')
354
    if not os.path.isfile(surf_file):
355
        logger.error(f'File {surf_file} not found.')
356
        raise FileNotFoundError(f'File {surf_file} not found')
357
    return surf_file
358

    
359

    
360
def get_sites():
361
    err_msg = 'The value of sites should be a list of atom numbers ' \
362
              '(ie. positive integers) or groups of atom numbers ' \
363
              'grouped by parentheses-like enclosers. \n' \
364
              'eg. 128,(135 138;141) 87 {45, 68}'
365
    # Convert the string into a list of lists
366
    sites = try_command(str2lst,
367
                        [(ValueError, err_msg), (AttributeError, err_msg)],
368
                        dos_inp.get('Screening', 'sites'))
369
    # Check all elements of the list (of lists) are positive integers
370
    for site in sites:
371
        if type(site) is list:
372
            for atom in site:
373
                if atom < 0:
374
                    logger.error(err_msg)
375
                    raise ValueError(err_msg)
376
        elif type(site) is int:
377
            if site < 0:
378
                logger.error(err_msg)
379
                raise ValueError(err_msg)
380
        else:
381
            logger.error(err_msg)
382
            raise ValueError(err_msg)
383

    
384
    return sites
385

    
386

    
387
def get_surf_ctrs2():
388
    err_msg = 'The value of surf_ctrs2 should be a list of atom numbers ' \
389
              '(ie. positive integers) or groups of atom numbers ' \
390
              'grouped by parentheses-like enclosers. \n' \
391
              'eg. 128,(135 138;141) 87 {45, 68}'
392
    # Convert the string into a list of lists
393
    surf_ctrs2 = try_command(str2lst,
394
                             [(ValueError, err_msg), (AttributeError, err_msg)],
395
                             dos_inp.get('Screening', 'surf_ctrs2'))
396
    # Check all elements of the list (of lists) are positive integers
397
    for ctr in surf_ctrs2:
398
        if type(ctr) is list:
399
            for atom in ctr:
400
                if atom < 0:
401
                    logger.error(err_msg)
402
                    raise ValueError(err_msg)
403
        elif type(ctr) is int:
404
            if ctr < 0:
405
                logger.error(err_msg)
406
                raise ValueError(err_msg)
407
        else:
408
            logger.error(err_msg)
409
            raise ValueError(err_msg)
410

    
411
    return surf_ctrs2
412

    
413

    
414
def get_molec_ctrs():
415
    err_msg = 'The value of molec_ctrs should be a list of atom' \
416
              ' numbers (ie. positive integers) or groups of atom ' \
417
              'numbers enclosed by parentheses-like characters. \n' \
418
              'eg. 128,(135 138;141) 87 {45, 68}'
419
    # Convert the string into a list of lists
420
    molec_ctrs = try_command(str2lst,
421
                             [(ValueError, err_msg),
422
                              (AttributeError, err_msg)],
423
                             dos_inp.get('Screening', 'molec_ctrs'))
424
    # Check all elements of the list (of lists) are positive integers
425
    for ctr in molec_ctrs:
426
        if isinstance(ctr, list):
427
            for atom in ctr:
428
                if atom < 0:
429
                    logger.error(err_msg)
430
                    raise ValueError(err_msg)
431
        elif isinstance(ctr, int):
432
            if ctr < 0:
433
                logger.error(err_msg)
434
                raise ValueError(err_msg)
435
        else:
436
            logger.error(err_msg)
437
            raise ValueError(err_msg)
438

    
439
    return molec_ctrs
440

    
441

    
442
def get_molec_ctrs2():
443
    err_msg = 'The value of molec_ctrs2 should be a list of atom ' \
444
              'numbers (ie. positive integers) or groups of atom ' \
445
              'numbers enclosed by parentheses-like characters. \n' \
446
              'eg. 128,(135 138;141) 87 {45, 68}'
447
    # Convert the string into a list of lists
448
    molec_ctrs2 = try_command(str2lst, [(ValueError, err_msg),
449
                                        (AttributeError, err_msg)],
450
                              dos_inp.get('Screening', 'molec_ctrs2'))
451

    
452
    # Check all elements of the list (of lists) are positive integers
453
    for ctr in molec_ctrs2:
454
        if isinstance(ctr, list):
455
            for atom in ctr:
456
                if atom < 0:
457
                    logger.error(err_msg)
458
                    raise ValueError(err_msg)
459
        elif isinstance(ctr, int):
460
            if ctr < 0:
461
                logger.error(err_msg)
462
                raise ValueError(err_msg)
463
        else:
464
            logger.error(err_msg)
465
            raise ValueError(err_msg)
466

    
467
    return molec_ctrs2
468

    
469

    
470
def get_molec_ctrs3():
471
    err_msg = 'The value of molec_ctrs3 should be a list of atom ' \
472
              'numbers (ie. positive integers) or groups of atom ' \
473
              'numbers enclosed by parentheses-like characters. \n' \
474
              'eg. 128,(135 138;141) 87 {45, 68}'
475
    # Convert the string into a list of lists
476
    molec_ctrs3 = try_command(str2lst, [(ValueError, err_msg),
477
                                        (AttributeError, err_msg)],
478
                              dos_inp.get('Screening', 'molec_ctrs3'))
479

    
480
    # Check all elements of the list (of lists) are positive integers
481
    for ctr in molec_ctrs3:
482
        if isinstance(ctr, list):
483
            for atom in ctr:
484
                if atom < 0:
485
                    logger.error(err_msg)
486
                    raise ValueError(err_msg)
487
        elif isinstance(ctr, int):
488
            if ctr < 0:
489
                logger.error(err_msg)
490
                raise ValueError(err_msg)
491
        else:
492
            logger.error(err_msg)
493
            raise ValueError(err_msg)
494

    
495
    return molec_ctrs3
496

    
497

    
498
def get_max_helic_angle():
499
    err_msg = "'max_helic_angle' must be a positive number in degrees"
500
    max_helic_angle = try_command(dos_inp.getfloat, [(ValueError, err_msg)],
501
                                  'Screening', 'max_helic_angle',
502
                                  fallback=180.0)
503
    if max_helic_angle < 0:
504
        logger.error(err_msg)
505
        raise ValueError(err_msg)
506

    
507
    return max_helic_angle
508

    
509

    
510
def get_select_magns():
511
    select_magns_vals = ['energy', 'moi']
512
    select_magns_str = dos_inp.get('Screening', 'select_magns',
513
                                   fallback='energy')
514
    select_magns_str.replace(',', ' ').replace(';', ' ')
515
    select_magns = select_magns_str.split(' ')
516
    select_magns = [m.lower() for m in select_magns]
517
    for m in select_magns:
518
        check_expect_val(m, select_magns_vals)
519
    return select_magns
520

    
521

    
522
def get_confs_per_magn():
523
    err_msg = num_error % ('confs_per_magn', 'positive integer')
524
    confs_per_magn = try_command(dos_inp.getint, [(ValueError, err_msg)],
525
                                 'Screening', 'confs_per_magn', fallback=2)
526
    if confs_per_magn <= 0:
527
        logger.error(err_msg)
528
        raise ValueError(err_msg)
529
    return confs_per_magn
530

    
531

    
532
def get_pbc_cell():
533
    err = "'pbc_cell' must be either 3 vectors of size 3 or False"
534
    pbc_cell_str = dos_inp.get('Screening', 'pbc_cell', fallback="False")
535
    if pbc_cell_str.lower() in turn_false_answers:
536
        return False
537
    else:
538
        pbc_cell = np.array(try_command(str2lst, [(ValueError, err)],
539
                                        pbc_cell_str, float))
540
        if pbc_cell.shape != (3, 3):
541
            logger.error(err)
542
            raise ValueError(err)
543
        return pbc_cell
544

    
545

    
546
def get_surf_norm_vect():
547
    err = "'surf_norm_vect' must be a 3 component vector, 'x', 'y' or 'z', " \
548
          "'auto' or 'asann'."
549
    cart_axes = {'x': [1.0, 0.0, 0.0], '-x': [-1.0, 0.0, 0.0],
550
                 'y': [0.0, 1.0, 0.0], '-y': [0.0, -1.0, 0.0],
551
                 'z': [0.0, 0.0, 1.0], '-z': [0.0, 0.0, -1.0]}
552
    surf_norm_vect_str = dos_inp.get('Screening', 'surf_norm_vect',
553
                                     fallback="auto").lower()
554
    if surf_norm_vect_str == "asann" or surf_norm_vect_str == "auto":
555
        return 'auto'
556
    if surf_norm_vect_str in cart_axes:
557
        return np.array(cart_axes[surf_norm_vect_str])
558
    surf_norm_vect = try_command(str2lst, [(ValueError, err)],
559
                                 surf_norm_vect_str, float)
560
    if len(surf_norm_vect) != 3:
561
        logger.error(err)
562
        raise ValueError(err)
563

    
564
    return np.array(surf_norm_vect) / np.linalg.norm(surf_norm_vect)
565

    
566

    
567
def get_adsorption_height():
568
    err_msg = num_error % ('adsorption_height', 'positive number')
569
    ads_height = try_command(dos_inp.getfloat, [(ValueError, err_msg)],
570
                             'Screening', 'adsorption_height', fallback=2.5)
571
    if ads_height <= 0:
572
        logger.error(err_msg)
573
        raise ValueError(err_msg)
574
    return ads_height
575

    
576

    
577
def get_set_angles():
578
    set_vals = ['euler', 'internal']
579
    check_expect_val(dos_inp.get('Screening', 'set_angles').lower(), set_vals)
580
    set_angles = dos_inp.get('Screening', 'set_angles',
581
                             fallback='euler').lower()
582
    return set_angles
583

    
584

    
585
def get_pts_per_angle():
586
    err_msg = num_error % ('sample_points_per_angle', 'positive integer')
587
    pts_per_angle = try_command(dos_inp.getint,
588
                                [(ValueError, err_msg)],
589
                                'Screening', 'sample_points_per_angle',
590
                                fallback=3)
591
    if pts_per_angle <= 0:
592
        logger.error(err_msg)
593
        raise ValueError(err_msg)
594
    return pts_per_angle
595

    
596

    
597
def get_max_structures():
598
    err_msg = num_error % ('max_structures', 'positive integer')
599
    max_structs = dos_inp.get('Screening', 'max_structures', fallback="False")
600
    if max_structs.lower() in turn_false_answers:
601
        return np.inf
602
    if try_command(int, [(ValueError, err_msg)], max_structs) <= 0:
603
        logger.error(err_msg)
604
        raise ValueError(err_msg)
605
    return int(max_structs)
606

    
607

    
608
def get_coll_thrsld():
609
    err_msg = num_error % ('collision_threshold', 'positive number')
610
    coll_thrsld_str = dos_inp.get('Screening', 'collision_threshold',
611
                                  fallback="1.3")
612
    if coll_thrsld_str.lower() in turn_false_answers:
613
        return False
614
    coll_thrsld = try_command(float, [(ValueError, err_msg)], coll_thrsld_str)
615

    
616
    if coll_thrsld <= 0:
617
        logger.error(err_msg)
618
        raise ValueError(err_msg)
619

    
620
    return coll_thrsld
621

    
622

    
623
def get_min_coll_height(norm_vect):
624
    err_msg = num_error % ('min_coll_height', 'decimal number')
625
    min_coll_height = dos_inp.get('Screening', 'min_coll_height',
626
                                  fallback="False")
627
    if min_coll_height.lower() in turn_false_answers:
628
        return False
629
    min_coll_height = try_command(float, [(ValueError, err_msg)],
630
                                  min_coll_height)
631
    cart_axes = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0],
632
                 [-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, -1.0]]
633
    err_msg = "'min_coll_height' option is only implemented for " \
634
              "'surf_norm_vect' to be one of the x, y or z axes. "
635
    if not isinstance(norm_vect, str) or norm_vect != 'auto':
636
        check_expect_val(norm_vect.tolist(), cart_axes, err_msg)
637
    return min_coll_height
638

    
639

    
640
def get_H_donor(spec_atoms):
641
    from ase.data import chemical_symbols
642
    err_msg = "The value of 'h_donor' must be either False, a chemical symbol " \
643
              "or an atom index"
644
    h_donor_str = dos_inp.get('Screening', 'h_donor', fallback="False")
645
    h_donor = []
646
    if h_donor_str.lower() in turn_false_answers:
647
        return False
648
    err = False
649
    for el in h_donor_str.split():
650
        try:
651
            h_donor.append(int(el))
652
        except ValueError:
653
            if el not in chemical_symbols + [nw_sym for pairs in spec_atoms
654
                                             for nw_sym in pairs]:
655
                err = True
656
            else:
657
                h_donor.append(el)
658
        finally:
659
            if err:
660
                logger.error(err_msg)
661
                ValueError(err_msg)
662
    return h_donor
663

    
664

    
665
def get_H_acceptor(spec_atoms):
666
    from ase.data import chemical_symbols
667
    err_msg = "The value of 'h_acceptor' must be either 'all', a chemical " \
668
              "symbol or an atom index"
669
    h_acceptor_str = dos_inp.get('Screening', 'h_acceptor', fallback="all")
670
    if h_acceptor_str.lower() == "all":
671
        return "all"
672
    h_acceptor = []
673
    err = False
674
    for el in h_acceptor_str.split():
675
        try:
676
            h_acceptor.append(int(el))
677
        except ValueError:
678
            if el not in chemical_symbols + [nw_sym for pairs in spec_atoms
679
                                             for nw_sym in pairs]:
680
                err = True
681
            else:
682
                h_acceptor.append(el)
683
        finally:
684
            if err:
685
                logger.error(err_msg)
686
                ValueError(err_msg)
687
    return h_acceptor
688

    
689

    
690
def get_use_molec_file():
691
    use_molec_file = dos_inp.get('Screening', 'use_molec_file',
692
                                 fallback='False')
693
    if use_molec_file.lower() in turn_false_answers:
694
        return False
695
    if not os.path.isfile(use_molec_file):
696
        logger.error(f'File {use_molec_file} not found.')
697
        raise FileNotFoundError(f'File {use_molec_file} not found')
698

    
699
    return use_molec_file
700

    
701

    
702
# Refinement
703

    
704
def get_refine_inp_file():
705
    refine_inp_file = dos_inp.get('Refinement', 'refine_inp_file')
706
    if not os.path.isfile(refine_inp_file):
707
        logger.error(f'File {refine_inp_file} not found.')
708
        raise FileNotFoundError(f'File {refine_inp_file} not found')
709

    
710
    return refine_inp_file
711

    
712

    
713
def get_energy_cutoff():
714
    err_msg = num_error % ('energy_cutoff', 'positive decimal number')
715
    energy_cutoff = try_command(dos_inp.getfloat,
716
                                [(ValueError, err_msg)],
717
                                'Refinement', 'energy_cutoff', fallback=0.5)
718
    if energy_cutoff < 0:
719
        logger.error(err_msg)
720
        raise ValueError(err_msg)
721
    return energy_cutoff
722

    
723

    
724
# Read input parameters
725

    
726
def read_input(in_file):
727
    err = False
728
    try:
729
        dos_inp.read(in_file)
730
    except MissingSectionHeaderError as e:
731
        logger.error('There are options in the input file without a Section '
732
                     'header.')
733
        err = e
734
    except DuplicateOptionError as e:
735
        logger.error('There is an option in the input file that has been '
736
                     'specified more than once.')
737
        err = e
738
    except Exception as e:
739
        err = e
740
    else:
741
        err = False
742
    finally:
743
        if isinstance(err, BaseException):
744
            raise err
745

    
746
    inp_vars = {}
747

    
748
    # Global
749
    if not dos_inp.has_section('Global'):
750
        logger.error(no_sect_err % 'Global')
751
        raise NoSectionError('Global')
752

    
753
    # Mandatory options
754
    # Checks whether the mandatory options 'run_type', 'code', etc. are present.
755
    glob_mand_opts = ['run_type', 'batch_q_sys']
756
    for opt in glob_mand_opts:
757
        if not dos_inp.has_option('Global', opt):
758
            logger.error(no_opt_err % (opt, 'Global'))
759
            raise NoOptionError(opt, 'Global')
760

    
761
    # Gets which sections are to be carried out
762
    isolated, screening, refinement = get_run_type()
763
    inp_vars['isolated'] = isolated
764
    inp_vars['screening'] = screening
765
    inp_vars['refinement'] = refinement
766
    inp_vars['batch_q_sys'] = get_batch_q_sys()
767

    
768
    # Dependent options:
769
    inp_vars['code'] = get_code()
770
    if inp_vars['batch_q_sys']:
771
        inp_vars['max_jobs'] = get_max_jobs()
772
        if inp_vars['batch_q_sys'] != 'local':
773
            if not dos_inp.has_option('Global', 'subm_script'):
774
                logger.error(no_opt_err % ('subm_script', 'Global'))
775
                raise NoOptionError('subm_script', 'Global')
776
            inp_vars['subm_script'] = get_subm_script()
777

    
778
    # Facultative options (Default/Fallback value present)
779
    inp_vars['project_name'] = get_project_name()
780
    # inp_vars['relaunch_err'] = get_relaunch_err()
781
    inp_vars['special_atoms'] = get_special_atoms()
782

    
783
    # Isolated
784
    if isolated:
785
        if not dos_inp.has_section('Isolated'):
786
            logger.error(no_sect_err % 'Isolated')
787
            raise NoSectionError('Isolated')
788
        # Mandatory options
789
        # Checks whether the mandatory options are present.
790
        iso_mand_opts = ['isol_inp_file', 'molec_file']
791
        for opt in iso_mand_opts:
792
            if not dos_inp.has_option('Isolated', opt):
793
                logger.error(no_opt_err % (opt, 'Isolated'))
794
                raise NoOptionError(opt, 'Isolated')
795
        inp_vars['isol_inp_file'] = get_isol_inp_file()
796
        if 'code ' in inp_vars:
797
            check_inp_file(inp_vars['isol_inp_file'], inp_vars['code'])
798
        inp_vars['molec_file'] = get_molec_file()
799

    
800
        # Facultative options (Default/Fallback value present)
801
        inp_vars['num_conformers'] = get_num_conformers()
802
        # inp_vars['num_prom_cand'] = get_num_prom_cand()
803
        # inp_vars['iso_rmsd'] = get_iso_rmsd()
804
        inp_vars['pre_opt'] = get_pre_opt()
805

    
806
    # Screening
807
    if screening:
808
        if not dos_inp.has_section('Screening'):
809
            logger.error(no_sect_err % 'Screening')
810
            raise NoSectionError('Screening')
811
        # Mandatory options:
812
        # Checks whether the mandatory options are present.
813
        screen_mand_opts = ['screen_inp_file', 'surf_file', 'sites',
814
                            'molec_ctrs']
815
        for opt in screen_mand_opts:
816
            if not dos_inp.has_option('Screening', opt):
817
                logger.error(no_opt_err % (opt, 'Screening'))
818
                raise NoOptionError(opt, 'Screening')
819
        inp_vars['screen_inp_file'] = get_screen_inp_file()
820
        inp_vars['surf_file'] = get_surf_file()
821
        inp_vars['sites'] = get_sites()
822
        inp_vars['molec_ctrs'] = get_molec_ctrs()
823

    
824
        # Facultative options (Default value present)
825
        inp_vars['select_magns'] = get_select_magns()
826
        inp_vars['confs_per_magn'] = get_confs_per_magn()
827
        inp_vars['surf_norm_vect'] = get_surf_norm_vect()
828
        inp_vars['adsorption_height'] = get_adsorption_height()
829
        inp_vars['set_angles'] = get_set_angles()
830
        inp_vars['sample_points_per_angle'] = get_pts_per_angle()
831
        inp_vars['pbc_cell'] = get_pbc_cell()
832
        inp_vars['collision_threshold'] = get_coll_thrsld()
833
        inp_vars['min_coll_height'] = get_min_coll_height(
834
            inp_vars['surf_norm_vect'])
835
        if inp_vars['min_coll_height'] is False \
836
                and inp_vars['collision_threshold'] is False:
837
            logger.warning("Collisions are deactivated: Overlapping of "
838
                           "adsorbate and surface is possible")
839
        inp_vars['h_donor'] = get_H_donor(inp_vars['special_atoms'])
840
        inp_vars['max_structures'] = get_max_structures()
841
        inp_vars['use_molec_file'] = get_use_molec_file()
842

    
843
        # Options depending on the value of others
844
        if inp_vars['set_angles'] == "internal":
845
            internal_opts = ['molec_ctrs2', 'molec_ctrs3', 'surf_ctrs2',
846
                             'max_helic_angle']
847
            for opt in internal_opts:
848
                if not dos_inp.has_option('Screening', opt):
849
                    logger.error(no_opt_err % (opt, 'Screening'))
850
                    raise NoOptionError(opt, 'Screening')
851
            inp_vars['max_helic_angle'] = get_max_helic_angle()
852
            inp_vars['molec_ctrs2'] = get_molec_ctrs2()
853
            inp_vars['molec_ctrs3'] = get_molec_ctrs3()
854
            inp_vars['surf_ctrs2'] = get_surf_ctrs2()
855
            if len(inp_vars["molec_ctrs2"]) != len(inp_vars['molec_ctrs']) \
856
                    or len(inp_vars["molec_ctrs3"]) != \
857
                    len(inp_vars['molec_ctrs']) \
858
                    or len(inp_vars['surf_ctrs2']) != len(inp_vars['sites']):
859
                err = "'molec_ctrs' 'molec_ctrs2' and 'molec_ctrs2' must have " \
860
                      "the same number of indices "
861
                logger.error(err)
862
                raise ValueError(err)
863

    
864
        if inp_vars['h_donor'] is False:
865
            inp_vars['h_acceptor'] = False
866
        else:
867
            inp_vars['h_acceptor'] = get_H_acceptor(inp_vars['special_atoms'])
868

    
869
    # Refinement
870
    if refinement:
871
        if not dos_inp.has_section('Refinement'):
872
            logger.error(no_sect_err % 'Refinement')
873
            raise NoSectionError('Refinement')
874
        # Mandatory options
875
        # Checks whether the mandatory options are present.
876
        ref_mand_opts = ['refine_inp_file']
877
        for opt in ref_mand_opts:
878
            if not dos_inp.has_option('Refinement', opt):
879
                logger.error(no_opt_err % (opt, 'Refinement'))
880
                raise NoOptionError(opt, 'Refinement')
881
        inp_vars['refine_inp_file'] = get_refine_inp_file()
882

    
883
        # Facultative options (Default value present)
884
        inp_vars['energy_cutoff'] = get_energy_cutoff()
885
        # end energy_cutoff
886

    
887
    return_vars_str = "\n\t".join([str(key) + ": " + str(value)
888
                                   for key, value in inp_vars.items()])
889
    logger.info(
890
        f'Correctly read {in_file} parameters: \n\n\t{return_vars_str}\n')
891

    
892
    return inp_vars
893

    
894

    
895
if __name__ == "__main__":
896
    import sys
897

    
898
    print(read_input(sys.argv[1]))