File: nwwriter.py

package info (click to toggle)
python-ase 3.26.0-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 15,484 kB
  • sloc: python: 148,112; xml: 2,728; makefile: 110; javascript: 47
file content (607 lines) | stat: -rw-r--r-- 19,624 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
# fmt: off

import os
import random
import string
import warnings
from copy import deepcopy
from typing import List, Optional, Tuple

import numpy as np

from ase.calculators.calculator import KPoints, kpts2kpts

_special_kws = ['center', 'autosym', 'autoz', 'theory', 'basis', 'xc', 'task',
                'set', 'symmetry', 'label', 'geompar', 'basispar', 'kpts',
                'bandpath', 'restart_kw', 'pretasks', 'charge']

_system_type = {1: 'polymer', 2: 'surface', 3: 'crystal'}


def _render_geom(atoms, params: dict) -> List[str]:
    """Generate the geometry block

    Parameters
    ----------
    atoms : Atoms
        Geometry for the computation
    params : dict
        Parameter set for the computation

    Returns
    -------
    geom : [str]
        Geometry block to use in the computation
    """
    geom_header = ['geometry units angstrom']
    for geomkw in ['center', 'autosym', 'autoz']:
        geom_header.append(geomkw if params.get(geomkw) else 'no' + geomkw)
    if 'geompar' in params:
        geom_header.append(params['geompar'])
    geom = [' '.join(geom_header)]

    outpos = atoms.get_positions()
    pbc = atoms.pbc
    if np.any(pbc):
        scpos = atoms.get_scaled_positions()
        for i, pbci in enumerate(pbc):
            if pbci:
                outpos[:, i] = scpos[:, i]
        npbc = pbc.sum()
        cellpars = atoms.cell.cellpar()
        geom.append(f'  system {_system_type[npbc]} units angstrom')
        if npbc == 3:
            geom.append('    lattice_vectors')
            for row in atoms.cell:
                geom.append('      {:20.16e} {:20.16e} {:20.16e}'.format(*row))
        else:
            if pbc[0]:
                geom.append(f'    lat_a {cellpars[0]:20.16e}')
            if pbc[1]:
                geom.append(f'    lat_b {cellpars[1]:20.16e}')
            if pbc[2]:
                geom.append(f'    lat_c {cellpars[2]:20.16e}')
            if pbc[1] and pbc[2]:
                geom.append(f'    alpha {cellpars[3]:20.16e}')
            if pbc[0] and pbc[2]:
                geom.append(f'    beta {cellpars[4]:20.16e}')
            if pbc[1] and pbc[0]:
                geom.append(f'    gamma {cellpars[5]:20.16e}')
        geom.append('  end')

    for i, atom in enumerate(atoms):
        geom.append('  {:<2} {:20.16e} {:20.16e} {:20.16e}'
                    ''.format(atom.symbol, *outpos[i]))
    symm = params.get('symmetry')
    if symm is not None:
        geom.append(f'  symmetry {symm}')
    geom.append('end')
    return geom


def _render_basis(theory, params: dict) -> List[str]:
    """Infer the basis set block

    Arguments
    ---------
    theory : str
        Name of the theory used for the calculation
    params : dict
        Parameters for the computation

    Returns
    -------
    [str]
        List of input file lines for the basis block
    """

    # Break if no basis set is provided and non is applicable
    if 'basis' not in params:
        if theory in ['pspw', 'band', 'paw']:
            return []

    # Write the header section
    if 'basispar' in params:
        header = 'basis {} noprint'.format(params['basispar'])
    else:
        header = 'basis noprint'
    basis_out = [header]

    # Write the basis set for each atom type
    basis_in = params.get('basis', '3-21G')
    if isinstance(basis_in, str):
        basis_out.append(f'   * library {basis_in}')
    else:
        for symbol, ibasis in basis_in.items():
            basis_out.append(f'{symbol:>4} library {ibasis}')
    basis_out.append('end')
    return basis_out


_special_keypairs = [('nwpw', 'simulation_cell'),
                     ('nwpw', 'carr-parinello'),
                     ('nwpw', 'brillouin_zone'),
                     ('tddft', 'grad'),
                     ]


def _render_brillouin_zone(array, name=None) -> List[str]:
    out = ['  brillouin_zone']
    if name is not None:
        out += [f'    zone_name {name}']
    template = '    kvector' + ' {:20.16e}' * array.shape[1]
    for row in array:
        out.append(template.format(*row))
    out.append('  end')
    return out


def _render_bandpath(bp) -> List[str]:
    if bp is None:
        return []
    out = ['nwpw']
    out += _render_brillouin_zone(bp.kpts, name=bp.path)
    out += [f'  zone_structure_name {bp.path}',
            'end',
            'task band structure']
    return out


def _format_line(key, val) -> str:
    if val is None:
        return key
    if isinstance(val, bool):
        return f'{key} .{str(val).lower()}.'
    else:
        return ' '.join([key, str(val)])


def _format_block(key, val, nindent=0) -> List[str]:
    prefix = '  ' * nindent
    prefix2 = '  ' * (nindent + 1)
    if val is None:
        return [prefix + key]

    if not isinstance(val, dict):
        return [prefix + _format_line(key, val)]

    out = [prefix + key]
    for subkey, subval in val.items():
        if (key, subkey) in _special_keypairs:
            if (key, subkey) == ('nwpw', 'brillouin_zone'):
                out += _render_brillouin_zone(subval)
            else:
                out += _format_block(subkey, subval, nindent + 1)
        else:
            if isinstance(subval, dict):
                subval = ' '.join([_format_line(a, b)
                                   for a, b in subval.items()])
            out.append(prefix2 + ' '.join([_format_line(subkey, subval)]))
    out.append(prefix + 'end')
    return out


def _render_other(params) -> List[str]:
    """Render other commands

    Parameters
    ----------
    params : dict
        Parameter set to be rendered

    Returns
    -------
    out : [str]
        Block defining other commands
    """
    out = []
    for kw, block in params.items():
        if kw in _special_kws:
            continue
        out += _format_block(kw, block)
    return out


def _render_set(set_params) -> List[str]:
    """Render the commands for the set parameters

    Parameters
    ----------
    set_params : dict
        Parameters being set

    Returns
    -------
    out : [str]
        Block defining set commands
    """
    return ['set ' + _format_line(key, val) for key, val in set_params.items()]


_gto_theories = ['tce', 'ccsd', 'tddft', 'scf', 'dft',
                 'direct_mp2', 'mp2', 'rimp2']
_pw_theories = ['band', 'pspw', 'paw']
_all_theories = _gto_theories + _pw_theories


def _get_theory(params: dict) -> str:
    """Infer the theory given the user-provided settings

    Parameters
    ----------
    params : dict
        Parameters for the computation

    Returns
    -------
    theory : str
        Theory directive to use
    """
    # Default: user-provided theory
    theory = params.get('theory')
    if theory is not None:
        return theory

    # Check if the user passed a theory to xc
    xc = params.get('xc')
    if xc in _all_theories:
        return xc

    # Check for input blocks that correspond to a particular level of
    # theory. Correlated theories (e.g. CCSD) are checked first.
    for kw in _gto_theories:
        if kw in params:
            return kw

    # If the user passed an 'nwpw' block, then they want a plane-wave
    # calculation, but what kind? If they request k-points, then
    # they want 'band', otherwise assume 'pspw' (if the user wants
    # to use 'paw', they will have to ask for it specifically).
    nwpw = params.get('nwpw')
    if nwpw is not None:
        if 'monkhorst-pack' in nwpw or 'brillouin_zone' in nwpw:
            return 'band'
        return 'pspw'

    # When all else fails, default to dft.
    return 'dft'


_xc_conv = dict(lda='slater pw91lda',
                pbe='xpbe96 cpbe96',
                revpbe='revpbe cpbe96',
                rpbe='rpbe cpbe96',
                pw91='xperdew91 perdew91',
                )


def _update_mult(magmom_tot: int, params: dict) -> None:
    """Update parameters for multiplicity given the magnetic moment

    For example, sets the number of open shells for SCF calculations
    and the multiplicity for DFT calculations.

    Parameters
    ----------
    magmom_tot : int
        Total magnetic moment of the system
    params : dict
        Current set of parameters, will be modified
    """
    # Determine theory and multiplicity
    theory = params['theory']
    if magmom_tot == 0:
        magmom_mult = 1
    else:
        magmom_mult = np.sign(magmom_tot) * (abs(magmom_tot) + 1)

    # Adjust the kwargs for each type of theory
    if 'scf' in params:
        for kw in ['nopen', 'singlet', 'doublet', 'triplet', 'quartet',
                   'quintet', 'sextet', 'septet', 'octet']:
            if kw in params['scf']:
                break
        else:
            params['scf']['nopen'] = magmom_tot
    elif theory in ['scf', 'mp2', 'direct_mp2', 'rimp2', 'ccsd', 'tce']:
        params['scf'] = dict(nopen=magmom_tot)

    if 'dft' in params:
        if 'mult' not in params['dft']:
            params['dft']['mult'] = magmom_mult
    elif theory in ['dft', 'tddft']:
        params['dft'] = dict(mult=magmom_mult)

    if 'nwpw' in params:
        if 'mult' not in params['nwpw']:
            params['nwpw']['mult'] = magmom_mult
    elif theory in ['pspw', 'band', 'paw']:
        params['nwpw'] = dict(mult=magmom_mult)


def _update_kpts(atoms, params) -> None:
    """Converts top-level 'kpts' argument to native keywords

    Parameters
    ----------
    atoms : Atoms
        Input structure
    params : dict
        Current parameter set, will be updated
    """
    kpts = params.get('kpts')
    if kpts is None:
        return

    nwpw = params.get('nwpw', {})

    if 'monkhorst-pack' in nwpw or 'brillouin_zone' in nwpw:
        raise ValueError("Redundant k-points specified!")

    if isinstance(kpts, KPoints):
        nwpw['brillouin_zone'] = kpts.kpts
    elif isinstance(kpts, dict):
        if kpts.get('gamma', False) or 'size' not in kpts:
            nwpw['brillouin_zone'] = kpts2kpts(kpts, atoms).kpts
        else:
            nwpw['monkhorst-pack'] = ' '.join(map(str, kpts['size']))
    elif isinstance(kpts, np.ndarray):
        nwpw['brillouin_zone'] = kpts
    else:
        nwpw['monkhorst-pack'] = ' '.join(map(str, kpts))

    params['nwpw'] = nwpw


def _render_pretask(
        this_step: dict,
        previous_basis: Optional[List[str]],
        wfc_path: str,
        next_steps: List[dict],
) -> Tuple[List[str], List[str]]:
    """Generate input file lines that perform a cheaper method first

    Parameters
    ----------
    this_step: dict
        Input parameters used to define the computation
    previous_basis: [str], optional
        Basis set block used in the previous step
    wfc_path: str
        Name of the wavefunction path
    next_steps: [dict]
        Parameters for the next steps in the calculation.
        This function will adjust the next steps to read
        and project from the wave functions written to disk by this step
        if the basis set changes between this step and the next.

    Returns
    -------
    output: [str]
        Output lines for this task
    this_basis: [str]
        Basis set block used for this task
    """

    # Get the theory for the next step
    next_step = next_steps[0]
    next_theory = _get_theory(next_step)
    next_step['theory'] = next_theory
    out = []
    if next_theory not in ['dft', 'mp2', 'direct_mp2', 'rimp2', 'scf']:
        raise ValueError(f'Initial guesses not supported for {next_theory}')

    # Determine the theory for this step
    this_theory = _get_theory(this_step)
    this_step['theory'] = this_theory
    if this_theory not in ['dft', 'scf']:
        raise ValueError('Initial guesses must use either dft or scf')

    # Determine which basis set to use for this step. Our priorities
    #  1. Basis defined explicitly in this step
    #  2. Basis set of the previous step
    #  3. Basis set of the target computation level
    if 'basis' in this_step:
        this_basis = _render_basis(this_theory, this_step)
    elif previous_basis is not None:
        this_basis = previous_basis.copy()
    else:
        # Use the basis for the last step
        this_basis = _render_basis(next_theory, next_steps[-1])

    # Determine the basis for the next step
    #  If not defined, it'll be the same as this step
    if 'basis' in next_step:
        next_basis = _render_basis(next_theory, next_step)
    else:
        next_basis = this_basis

    # Set up projections if needed
    if this_basis == next_basis:
        out.append('\n'.join(this_basis))
        proj_from = None  # We do not need to project basis
    else:
        # Check for known limitations of NWChem
        if this_theory != next_theory:
            msg = 'Theories must be the same if basis are different. ' \
                  f'This step: {this_theory}//{this_basis} ' \
                  f'Next step: {next_theory}//{next_basis}'
            if 'basis' not in this_step:
                msg += f". Consider specifying basis in {this_step}"
            raise ValueError(msg)
        if not any('* library' in x for x in this_basis):
            raise ValueError('We can only support projecting from systems '
                             'where all atoms share the same basis')

        # Append a new name to this basis function by
        #  appending it as the first argument of the basis block
        proj_from = f"smb_{len(next_steps)}"
        this_basis[0] = f'basis {proj_from} {this_basis[0][6:]}'
        out.append('\n'.join(this_basis))

        # Point ao basis (the active basis set) to this new basis set
        out.append(f'set "ao basis" {proj_from}')

    # Insert a command to write wfcs from this computation to disk
    if this_theory not in this_step:
        this_step[this_theory] = {}
    if 'vectors' not in this_step[this_theory]:
        this_step[this_theory]['vectors'] = {}
    this_step[this_theory]['vectors']['output'] = wfc_path

    # Check if the initial theory changes
    if this_theory != next_theory and \
            'lindep:n_dep' not in this_step.get('set', {}):
        warnings.warn('Loading initial guess may fail if you do not specify'
                      ' the number of linearly-dependent basis functions.'
                      ' Consider adding {"set": {"lindep:n_dep": 0}} '
                      f' to the step: {this_step}.')

    # Add this to the input file along with a "task * ignore" command
    out.extend(['\n'.join(_render_other(this_step)),
                '\n'.join(_render_set(this_step.get('set', {}))),
                f'task {this_theory} ignore'])

    # Command to read the wavefunctions in the next step
    #  Theory used to get the wavefunctions may be different (mp2 uses SCF)
    wfc_theory = 'scf' if 'mp2' in next_theory else next_theory
    next_step = next_step
    if wfc_theory not in next_step:
        next_step[wfc_theory] = {}
    if 'vectors' not in next_step[wfc_theory]:
        next_step[wfc_theory]['vectors'] = {}

    if proj_from is None:
        # No need for projection
        next_step[wfc_theory]['vectors']['input'] = wfc_path
    else:
        # Define that we should project from our basis set
        next_step[wfc_theory]['vectors']['input'] \
            = f'project {proj_from} {wfc_path}'

        # Replace the name of the basis set to the default
        out.append('set "ao basis" "ao basis"')

    return out, this_basis


def write_nwchem_in(fd, atoms, properties=None, echo=False, **params):
    """Writes NWChem input file.

    See :class:`~ase.calculators.nwchem.NWChem` for available params.

    Parameters
    ----------
    fd
        file descriptor
    atoms
        atomic configuration
    properties
        list of properties to compute; by default only the
        calculation of the energy is requested
    echo
        if True include the `echo` keyword at the top of the file,
        which causes the content of the input file to be included
        in the output file
    params
        dict of instructions blocks to be included
    """
    # Copy so we can alter params w/o changing it for the function caller
    params = deepcopy(params)

    if properties is None:
        properties = ['energy']

    if 'stress' in properties:
        if 'set' not in params:
            params['set'] = {}
        params['set']['includestress'] = True

    task = params.get('task')
    if task is None:
        if 'stress' in properties or 'forces' in properties:
            task = 'gradient'
        else:
            task = 'energy'

    _update_kpts(atoms, params)

    # Determine the theory for each step
    #  We determine the theory ahead of time because it is
    #  used when generating other parts of the input file (e.g., _get_mult)
    theory = _get_theory(params)
    params['theory'] = theory

    for pretask in params.get('pretasks', []):
        pretask['theory'] = _get_theory(pretask)

    if 'xc' in params:
        xc = _xc_conv.get(params['xc'].lower(), params['xc'])
        if theory in ['dft', 'tddft']:
            if 'dft' not in params:
                params['dft'] = {}
            params['dft']['xc'] = xc
        elif theory in ['pspw', 'band', 'paw']:
            if 'nwpw' not in params:
                params['nwpw'] = {}
            params['nwpw']['xc'] = xc

    # Update the multiplicity given the charge of the system
    magmom_tot = int(atoms.get_initial_magnetic_moments().sum())
    _update_mult(magmom_tot, params)
    for pretask in params.get('pretasks', []):
        _update_mult(magmom_tot, pretask)

    # Determine the header options
    label = params.get('label', 'nwchem')
    perm = os.path.abspath(params.pop('perm', label))
    scratch = os.path.abspath(params.pop('scratch', label))
    restart_kw = params.get('restart_kw', 'start')
    if restart_kw not in ('start', 'restart'):
        raise ValueError("Unrecognised restart keyword: {}!"
                         .format(restart_kw))
    short_label = label.rsplit('/', 1)[-1]
    if echo:
        out = ['echo']
    else:
        out = []

    # Defines the geometry and global options
    out.extend([f'title "{short_label}"',
                f'permanent_dir {perm}',
                f'scratch_dir {scratch}',
                f'{restart_kw} {short_label}',
                '\n'.join(_render_set(params.get('set', {}))),
                '\n'.join(_render_geom(atoms, params))])

    # Add the charge if provided
    if 'charge' in params:
        out.append(f'charge {params["charge"]}')

    # Define the memory separately from the other options so that it
    #  is defined before any initial wfc guesses are performed
    memory_opts = params.pop('memory', None)
    if memory_opts is not None:
        out.extend(_render_other(dict(memory=memory_opts)))

    # If desired, output commands to generate the initial wavefunctions
    if 'pretasks' in params:
        wfc_path = f'tmp-{"".join(random.choices(string.hexdigits, k=8))}.wfc'
        pretasks = params['pretasks']
        previous_basis = None
        for this_ind, this_step in enumerate(pretasks):
            new_out, previous_basis = _render_pretask(
                this_step,
                previous_basis,
                wfc_path,
                pretasks[this_ind + 1:] + [params]
            )
            out.extend(new_out)

    # Finish output file with the commands to perform the desired computation
    out.extend(['\n'.join(_render_basis(theory, params)),
                '\n'.join(_render_other(params)),
                f'task {theory} {task}',
                '\n'.join(_render_bandpath(params.get('bandpath', None)))])

    fd.write('\n\n'.join(out))