• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

bjmorgan / vasppy / 18837628687

27 Oct 2025 10:22AM UTC coverage: 43.4% (+0.03%) from 43.375%
18837628687

push

github

bjmorgan
Fixing bug

983 of 2265 relevant lines covered (43.4%)

1.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.31
/vasppy/scripts/convergence_testing.py
1
"""Generate a series of VASP inputs for convergence testing."""
2
import argparse
4✔
3
import os
4✔
4
import numpy as np
4✔
5
import shutil
4✔
6
from pymatgen.core import Structure
4✔
7
from pymatgen.io.vasp import Incar, Potcar
4✔
8
from vasppy.kpoints import get_convergence_testing_kspacing
4✔
9
from pathlib import Path
4✔
10
from dataclasses import dataclass
4✔
11
from typing import Any
4✔
12

13

14
def parse_args() -> argparse.Namespace:
4✔
15
    """Parse command line arguments.
16
    
17
    Returns:
18
        Parsed command line arguments.
19
        
20
    """
21
    parser = argparse.ArgumentParser(
4✔
22
        description='Generate a series of VASP inputs for convergence testing. '
23
                    'Requires a template INCAR, the geometry of the system (a POSCAR), '
24
                    'and pseudopotentials (via --pseudopotentials or --potcar-file).'
25
    )
26
    parser.add_argument('-i', '--incar', required=True, help='Specify the template INCAR.')
4✔
27
    parser.add_argument('-p', '--poscar', required=True, help='Specify the geometry of the system (POSCAR).')
4✔
28
    potcar_group = parser.add_mutually_exclusive_group(required=True)
4✔
29
    potcar_group.add_argument(
4✔
30
        '--pseudopotentials', nargs='+', default=None,
31
        help='Specify the pseudopotentials (POTCARs) to be used e.g. Li_sv Mn_pv O. '
32
             'Mutually exclusive with --potcar-file. '
33
             'Defaults to None i.e. no POTCARs will be written. '
34
             'This functionality requires Pymatgen to be set up for VASP POTCARs.'
35
    )
36
    potcar_group.add_argument(
4✔
37
        '--potcar-file', default=None,
38
        help='Path to existing POTCAR file to use for all calculations. '
39
             'Mutually exclusive with --pseudopotentials.'
40
    )
41
    parser.add_argument(
4✔
42
        '-e', '--encut', type=int, nargs=3, default=(100, 700, 50),
43
        help='Set the upper/lower bounds and step size for the basis set size (ENCUT). Defaults to 100 700 50.'
44
    )
45
    parser.add_argument(
4✔
46
        '-k', '--kspacing', type=float, nargs=3, default=(0.1, 0.8, 0.02),
47
        help='Set the upper/lower bounds and step size for the minimum allowed distance between k-points (KSPACING). '
48
             'Defaults to 0.1 0.8 0.02'
49
    )
50
    parser.add_argument(
4✔
51
        '-d', '--directory', default='./convergence_testing',
52
        help='Specify the directory in which to place the generated VASP inputs. Defaults to ./convergence_testing.'
53
    )
54
    parser.add_argument(
4✔
55
        '--base-encut', type=int, default=400,
56
        help='Set the value of ENCUT for the KSPACING convergence tests. Defaults to 400.'
57
    )
58
    parser.add_argument(
4✔
59
        '--base-kspacing', type=float, default=0.3,
60
        help='Set the value of KSPACING for the ENCUT convergence tests. Defaults to 0.3.'
61
    )
62
    parser.add_argument(
4✔
63
        '--job-script', default=None,
64
        help='Path to job submission script to copy into each convergence test directory.'
65
    )
66
    parser.add_argument(
4✔
67
        '--dry-run', action='store_true',
68
        help='Show what would be created without actually creating files or directories.'
69
    )
70
    args = parser.parse_args()
4✔
71
    args.encut = tuple(args.encut)
4✔
72
    args.kspacing = tuple(args.kspacing)
4✔
73
    return args
4✔
74

75
@dataclass
4✔
76
class ConvergenceTarget:
4✔
77
    """Represents a single convergence test calculation.
78
    
79
    Attributes:
80
        path: Directory path for this calculation.
81
        encut: ENCUT value to test (None if not varying).
82
        kspacing: KSPACING value to test (None if not varying).
83
        base_incar: Base INCAR parameters shared across tests.
84
        
85
    """
86
    path: Path
4✔
87
    encut: int | None
4✔
88
    kspacing: float | None
4✔
89
    base_incar: dict[str, Any]
4✔
90
    
91
    # Parameters that should be included in INCAR
92
    CONVERGENCE_PARAMS = ['encut', 'kspacing']
4✔
93
    
94
    def __post_init__(self):
4✔
95
        """Validate that at least one parameter is set."""
96
        if self.encut is None and self.kspacing is None:
4✔
97
            raise ValueError(
4✔
98
                "At least one of encut or kspacing must be set for a convergence test"
99
            )
100
    
101
    @property
4✔
102
    def incar_params(self) -> dict[str, Any]:
4✔
103
        """Get complete INCAR parameters for writing.
104
        
105
        Returns:
106
            Dictionary of INCAR parameters including base params and test values.
107
            
108
        """
109
        params = self.base_incar.copy()
4✔
110
        
111
        for param_name in self.CONVERGENCE_PARAMS:
4✔
112
            value = getattr(self, param_name)
4✔
113
            if value is not None:
4✔
114
                params[param_name.upper()] = value
4✔
115
        
116
        return params
4✔
117
            
118
def create_directory_structure(base_dir: str | Path) -> None:
4✔
119
    """Create the base directory structure for convergence testing.
120
    
121
    Args:
122
        base_dir: Base directory path for convergence tests.
123
        
124
    """
125
    base_path = Path(base_dir)
4✔
126
    os.mkdir(str(base_path))
4✔
127
    os.mkdir(str(base_path / 'ENCUT'))
4✔
128
    os.mkdir(str(base_path / 'KSPACING'))
4✔
129
    
130
    
131
def write_vasp_input_files(
4✔
132
    directory: str | Path,
133
    incar_params: dict,
134
    structure: Structure,
135
    potcar: Potcar,
136
    job_script: str | None = None
137
) -> None:
138
    """Write VASP input files to a directory.
139
    
140
    Args:
141
        directory: Directory path to write files to.
142
        incar_params: Dictionary of INCAR parameters.
143
        structure: Pymatgen Structure object.
144
        potcar: Potcar object.
145
        job_script: Optional path to job script file to copy.
146
        
147
    """
148
    dir_path = Path(directory)
4✔
149
    incar = Incar.from_dict(incar_params)
4✔
150
    incar.write_file(str(dir_path / 'INCAR'))
4✔
151
    structure.to(fmt='poscar', filename=str(dir_path / 'POSCAR'))
4✔
152
    potcar.write_file(str(dir_path / 'POTCAR'))
4✔
153
    
154
    if job_script is not None:
4✔
155
        shutil.copy2(job_script, str(dir_path / Path(job_script).name))
4✔
156
    
157
def calculate_encut_targets(
4✔
158
    base_dir: str | Path,
159
    encut_values: np.ndarray,
160
    base_kspacing: float,
161
    incar_dict: dict
162
) -> list[ConvergenceTarget]:
163
    """Calculate convergence test targets for ENCUT.
164
    
165
    Args:
166
        base_dir: Base directory for convergence tests.
167
        encut_values: Array of ENCUT values to test.
168
        base_kspacing: Fixed KSPACING value to use.
169
        incar_dict: Base INCAR parameters dictionary.
170
        
171
    Returns:
172
        List of ConvergenceTarget objects for ENCUT tests.
173
        
174
    """
175
    base_path = Path(base_dir)
4✔
176
    targets = []
4✔
177
    
178
    # Add base KSPACING to base_incar for these tests
179
    base_incar_with_kspacing = incar_dict.copy()
4✔
180
    base_incar_with_kspacing['KSPACING'] = base_kspacing
4✔
181
    
182
    for energy_cutoff in encut_values:
4✔
183
        path = base_path / 'ENCUT' / str(energy_cutoff)
4✔
184
        target = ConvergenceTarget(
4✔
185
            path=path,
186
            encut=int(energy_cutoff),
187
            kspacing=None,
188
            base_incar=base_incar_with_kspacing
189
        )
190
        targets.append(target)
4✔
191
    
192
    return targets
4✔
193

194

195
def calculate_kspacing_targets(
4✔
196
    base_dir: str | Path,
197
    kspacing_values: tuple[float, ...],
198
    base_encut: int,
199
    incar_dict: dict
200
) -> list[ConvergenceTarget]:
201
    """Calculate convergence test targets for KSPACING.
202
    
203
    Args:
204
        base_dir: Base directory for convergence tests.
205
        kspacing_values: Tuple of KSPACING values to test.
206
        base_encut: Fixed ENCUT value to use.
207
        incar_dict: Base INCAR parameters dictionary.
208
        
209
    Returns:
210
        List of ConvergenceTarget objects for KSPACING tests.
211
        
212
    """
213
    base_path = Path(base_dir)
4✔
214
    targets = []
4✔
215
    
216
    # Add base ENCUT to base_incar for these tests
217
    base_incar_with_encut = incar_dict.copy()
4✔
218
    base_incar_with_encut['ENCUT'] = base_encut
4✔
219
    
220
    for minimum_distance in kspacing_values:
4✔
221
        path = base_path / 'KSPACING' / str(minimum_distance)
4✔
222
        target = ConvergenceTarget(
4✔
223
            path=path,
224
            encut=None,
225
            kspacing=minimum_distance,
226
            base_incar=base_incar_with_encut
227
        )
228
        targets.append(target)
4✔
229
    
230
    return targets 
4✔
231

232
def execute_targets(
4✔
233
    targets: list[ConvergenceTarget],
234
    structure: Structure,
235
    potcar: Potcar,
236
    job_script: str | None = None
237
) -> None:
238
    """Execute convergence test targets by creating directories and writing files.
239
    
240
    Args:
241
        targets: List of ConvergenceTarget objects to execute.
242
        structure: Pymatgen Structure object.
243
        potcar: Potcar object.
244
        job_script: Optional path to job script file to copy.
245
        
246
    """
247
    for target in targets:
4✔
248
        os.mkdir(str(target.path))
4✔
249
        write_vasp_input_files(target.path, target.incar_params, structure, potcar, job_script)
4✔
250

251
def validate_inputs(
4✔
252
    poscar_path: str,
253
    incar_path: str,
254
    output_dir: str,
255
    job_script: str | None = None
256
) -> None:
257
    """Validate input files and output directory before starting work.
258
    
259
    Args:
260
        poscar_path: Path to POSCAR file.
261
        incar_path: Path to INCAR file.
262
        output_dir: Path to output directory.
263
        job_script: Optional path to job script file.
264
        
265
    Raises:
266
        FileNotFoundError: If POSCAR, INCAR, or job script file doesn't exist.
267
        FileExistsError: If output directory already exists.
268
        
269
    """
270
    # Check POSCAR exists
271
    if not os.path.isfile(poscar_path):
4✔
272
        raise FileNotFoundError(
4✔
273
            f"POSCAR file not found: {poscar_path}\n"
274
            f"Please provide a valid POSCAR file using the -p/--poscar argument."
275
        )
276
    
277
    # Check INCAR exists
278
    if not os.path.isfile(incar_path):
4✔
279
        raise FileNotFoundError(
4✔
280
            f"INCAR file not found: {incar_path}\n"
281
            f"Please provide a valid INCAR file using the -i/--incar argument."
282
        )
283
    
284
    # Check job script exists if provided
285
    if job_script is not None and not os.path.isfile(job_script):
4✔
286
        raise FileNotFoundError(
4✔
287
            f"Job script file not found: {job_script}\n"
288
            f"Please provide a valid job script file using the --job-script argument."
289
        )
290
    
291
    # Check output directory doesn't exist
292
    if os.path.exists(output_dir):
4✔
293
        raise FileExistsError(
4✔
294
            f"Output directory already exists: {output_dir}\n"
295
            f"Please choose a different directory using the -d/--directory argument."
296
        )
297
            
298
def load_inputs(poscar_path: str, incar_path: str) -> tuple[Structure, dict]:
4✔
299
    """Load structure and INCAR template.
300
    
301
    Args:
302
        poscar_path: Path to POSCAR file.
303
        incar_path: Path to INCAR file.
304
        
305
    Returns:
306
        Tuple of (Structure, INCAR dict).
307
        
308
    """
309
    print(f"Loading structure from {poscar_path}...")
4✔
310
    structure = Structure.from_file(poscar_path)
4✔
311
    print(f"Loading INCAR template from {incar_path}...")
4✔
312
    base_incar_dict = Incar.from_file(incar_path).as_dict()
4✔
313
    return structure, base_incar_dict
4✔
314

315
def print_dry_run_summary(
4✔
316
    directory: str,
317
    encut_targets: list[ConvergenceTarget],
318
    kspacing_targets: list[ConvergenceTarget],
319
    potcar: Potcar,
320
    job_script: str | None = None
321
) -> None:
322
    """Print summary of what would be created in dry-run mode.
323
    
324
    Args:
325
        directory: Base directory path.
326
        encut_targets: List of ENCUT test targets.
327
        kspacing_targets: List of KSPACING test targets.
328
        potcar: Potcar object.
329
        job_script: Optional path to job script file.
330
        
331
    """
332
    print("\n=== DRY RUN - No files will be created ===\n")
4✔
333
    print(f"Would create base directory: {directory}\n")
4✔
334
    print(f"Would create {len(encut_targets)} ENCUT test directories:")
4✔
335
    for target in encut_targets:
4✔
336
        print(f"  - {target.path} (ENCUT={target.encut})")
4✔
337
    print(f"\nWould create {len(kspacing_targets)} KSPACING test directories:")
4✔
338
    for target in kspacing_targets:
4✔
339
        print(f"  - {target.path} (KSPACING={target.kspacing})")
4✔
340
    print(f"\nTotal: {len(encut_targets) + len(kspacing_targets)} convergence tests")
4✔
341
    print("POTCARs will be included in all calculations")
4✔
342
    if job_script:
4✔
343
        print(f"Job script '{job_script}' will be copied to each directory")
4✔
344
                    
345
def execute_convergence_tests(
4✔
346
    directory: str,
347
    encut_targets: list[ConvergenceTarget],
348
    kspacing_targets: list[ConvergenceTarget],
349
    structure: Structure,
350
    potcar: Potcar,
351
    job_script: str | None = None
352
) -> None:
353
    """Execute convergence tests by creating directories and writing files.
354
    
355
    Args:
356
        directory: Base directory path.
357
        encut_targets: List of ENCUT test targets.
358
        kspacing_targets: List of KSPACING test targets.
359
        structure: Pymatgen Structure object.
360
        potcar: Potcar object.
361
        job_script: Optional path to job script file to copy.
362
        
363
    """
364
    print(f"\nCreating directory structure at {directory}...")
4✔
365
    create_directory_structure(directory)
4✔
366
    
367
    print(f"Generating {len(encut_targets)} ENCUT convergence tests...")
4✔
368
    execute_targets(encut_targets, structure, potcar, job_script)
4✔
369
    
370
    print(f"Generating {len(kspacing_targets)} KSPACING convergence tests...")
4✔
371
    execute_targets(kspacing_targets, structure, potcar, job_script)
4✔
372
    
373
    print(f"\n✓ Complete! Created {len(encut_targets) + len(kspacing_targets)} tests in {directory}")
4✔
374

375
def load_potcar(pseudopotentials: list[str] | None, potcar_file: str | None) -> Potcar:
4✔
376
    """Load or create Potcar object.
377
    
378
    Args:
379
        pseudopotentials: List of pseudopotential names to create Potcar from.
380
        potcar_file: Path to existing POTCAR file to load.
381
        
382
    Returns:
383
        Potcar object.
384
        
385
    Raises:
386
        ValueError: If neither pseudopotentials nor potcar_file is provided.
387
        
388
    """
389
    potcar: Potcar
390
    if pseudopotentials:
4✔
391
        potcar = Potcar(pseudopotentials)
4✔
392
    elif potcar_file:
4✔
393
        potcar = Potcar.from_file(potcar_file)
4✔
394
    else:
395
        raise ValueError("Either pseudopotentials or potcar_file must be provided")
4✔
396
    return potcar
4✔
397
                    
398
def main() -> None:
4✔
399
    """Main entry point for convergence testing script."""
400
    args = parse_args()
4✔
401
    
402
    # Validate and load inputs
403
    print("Validating inputs...")
4✔
404
    validate_inputs(args.poscar, args.incar, args.directory, job_script=args.job_script)
4✔
405
    structure, base_incar_dict = load_inputs(args.poscar, args.incar)
4✔
406
    
407
    # Load potcar
408
    potcar = load_potcar(args.pseudopotentials, args.potcar_file)
4✔
409
    
410
    # Calculate parameter ranges
411
    print("Calculating convergence test parameters...")
4✔
412
    reciprocal_lattice_vectors = structure.lattice.reciprocal_lattice_crystallographic.matrix
4✔
413
    kspacing_min, kspacing_max, step = args.kspacing
4✔
414
    kspacing_values = get_convergence_testing_kspacing(
4✔
415
        reciprocal_lattice_vectors, 
416
        (kspacing_min, kspacing_max), 
417
        step
418
    )
419
    encut_min, encut_max, step = args.encut
4✔
420
    encut_values = np.arange(encut_min, encut_max + step, step)
4✔
421
    
422
    # Calculate targets
423
    encut_targets = calculate_encut_targets(
4✔
424
        args.directory, encut_values, args.base_kspacing, base_incar_dict
425
    )
426
    kspacing_targets = calculate_kspacing_targets(
4✔
427
        args.directory, kspacing_values, args.base_encut, base_incar_dict
428
    )
429
    
430
    # Execute or dry-run
431
    if args.dry_run:
4✔
432
        print_dry_run_summary(args.directory, encut_targets, kspacing_targets, potcar, args.job_script)
4✔
433
    else:
434
        execute_convergence_tests(args.directory, encut_targets, kspacing_targets, structure, potcar, args.job_script)
4✔
435

436

437
if __name__ == '__main__':
4✔
438
    main()
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc