From c990fce6fb4498d1958e948bd5044777d15ac5c7 Mon Sep 17 00:00:00 2001 From: Mgcini Keith Phuthi Date: Thu, 9 Jan 2025 17:27:36 -0800 Subject: [PATCH 01/78] added refine cell in phonopy --- .../phonopy/generate_phonopy_displacements.py | 33 +++++++++++++++++-- .../phonopy/phonon_bands_and_dos.py | 11 +++---- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/asimtools/asimmodules/phonopy/generate_phonopy_displacements.py b/asimtools/asimmodules/phonopy/generate_phonopy_displacements.py index ce187ec..f12a051 100755 --- a/asimtools/asimmodules/phonopy/generate_phonopy_displacements.py +++ b/asimtools/asimmodules/phonopy/generate_phonopy_displacements.py @@ -7,14 +7,19 @@ from phonopy import Phonopy from phonopy.interface.calculator import read_crystal_structure, write_crystal_structure from phonopy.structure.atoms import PhonopyAtoms +from ase import Atoms +from ase.spacegroup.symmetrize import check_symmetry from asimtools.calculators import load_calc from asimtools.utils import get_atoms, get_logger +import spglib def generate_phonopy_displacements( image: Dict, supercell: Sequence[int], distance: float = 0.01, - phonopy_save_path: str = 'phonopy_params.yaml' + phonopy_save_path: str = 'phonopy_params.yaml', + refine_cell: bool = False, + symprec: float = 1e-4, ) -> Dict: """Generates displacements for phonopy calculations @@ -31,6 +36,26 @@ def generate_phonopy_displacements( """ atoms = get_atoms(**image) + unrefined_sg = check_symmetry(atoms, symprec=symprec).international + if refine_cell: + cell = ( + atoms.get_cell().array, + atoms.get_scaled_positions(), + atoms.get_atomic_numbers() + ) + + refined_cell = spglib.standardize_cell(cell, symprec=symprec) + atoms = Atoms( + cell=refined_cell[0], + scaled_positions=refined_cell[1], + numbers=refined_cell[2], + pbc=True, + ) + atoms.write('refined_atoms.cif') + sg = check_symmetry(atoms, symprec=1e-6).international + else: + sg = None + atoms.write('POSCAR-unitcell', format='vasp') unitcell, _ = read_crystal_structure( @@ -61,4 +86,8 @@ def generate_phonopy_displacements( phonon.save(phonopy_save_path) - return {} + results = { + 'unrefined_spacegroup': str(unrefined_sg), + 'refined_spacegroup': str(sg), + } + return results diff --git a/asimtools/asimmodules/phonopy/phonon_bands_and_dos.py b/asimtools/asimmodules/phonopy/phonon_bands_and_dos.py index 3bd9588..7721de8 100644 --- a/asimtools/asimmodules/phonopy/phonon_bands_and_dos.py +++ b/asimtools/asimmodules/phonopy/phonon_bands_and_dos.py @@ -4,13 +4,6 @@ import os import numpy as np from numpy.typing import ArrayLike -# import matplotlib.pyplot as plt -# from ase.io import read -# import phonopy -# from phonopy.phonon.band_structure import get_band_qpoints_and_path_connections -# from asimtools.calculators import load_calc -# from asimtools.asimmodules.workflows.image_array import image_array -# from asimtools.utils import get_str_btn, get_images from asimtools.job import UnitJob def phonon_bands_and_dos( @@ -26,6 +19,8 @@ def phonon_bands_and_dos( use_seekpath: Optional[bool] = True, npoints: Optional[int] = 51, mesh: Optional[Union[ArrayLike,float]] = [20, 20, 20], + refine_cell: bool = False, + symprec: float = 1e-4, ) -> Dict: """Workflow to calculate phonon bands and density of states using phonopy. @@ -77,6 +72,8 @@ def phonon_bands_and_dos( 'supercell': supercell, 'distance': distance, 'phonopy_save_path': phonopy_save_path, + 'refine_cell': refine_cell, + 'symprec': symprec, }, }, 'step-1': { From b24fa2aeb56c6ea57ed875814351e744663b5254 Mon Sep 17 00:00:00 2001 From: Mgcini Keith Phuthi Date: Fri, 10 Jan 2025 13:34:09 -0800 Subject: [PATCH 02/78] parity bug fix --- asimtools/asimmodules/benchmarking/parity.py | 11 ++++++----- tests/asimmodules/workflows/test_distributed.py | 2 -- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/asimtools/asimmodules/benchmarking/parity.py b/asimtools/asimmodules/benchmarking/parity.py index 1730cc9..898c848 100644 --- a/asimtools/asimmodules/benchmarking/parity.py +++ b/asimtools/asimmodules/benchmarking/parity.py @@ -53,6 +53,8 @@ def calc_parity_data( spvals = [] for i, atoms in enumerate(subset): calc = load_calc(calc_id) + patoms = atoms.copy() + patoms.calc = calc n_atoms = len(atoms) if 'energy' in properties: prop = 'energy' @@ -60,7 +62,7 @@ def calc_parity_data( [ervals, atoms.get_potential_energy()/n_atoms] ) epvals = np.hstack( - [epvals, float(calc.get_potential_energy(atoms)/n_atoms)] + [epvals, float(patoms.get_potential_energy()/n_atoms)] ) if 'forces' in properties: @@ -71,17 +73,16 @@ def calc_parity_data( ) fpvals = np.hstack( - [fpvals, np.array(calc.get_forces(atoms)).flatten()] + [fpvals, np.array(patoms.get_forces()).flatten()] ) if 'stress' in properties: prop = 'stress' srvals = np.hstack( - [srvals, np.array(atoms.get_stress(voigt=False)).flatten()] + [srvals, np.array(atoms.get_stress(voigt=True)).flatten()] ) - spvals = np.hstack( - [spvals, np.array(calc.get_stress(atoms)).flatten()] + [spvals, np.array(patoms.get_stress(voigt=True)).flatten()] ) res[prop] = {'ref': srvals, 'pred': spvals} diff --git a/tests/asimmodules/workflows/test_distributed.py b/tests/asimmodules/workflows/test_distributed.py index 0c59605..ded274c 100644 --- a/tests/asimmodules/workflows/test_distributed.py +++ b/tests/asimmodules/workflows/test_distributed.py @@ -86,5 +86,3 @@ def test_batch_distributed(env_input, calc_input, sim_input, tmp_path, request): uj = load_job_from_directory(d) print('job_info:', uj.workdir, uj.get_status()) assert uj.get_status()[1] == statuses[d_ind] - - # assert distjob.get_status(descend=False) == (True, 'complete') From 96aa10ca09762c1b1632585dac8b8a6e92497600 Mon Sep 17 00:00:00 2001 From: Mgcini Keith Phuthi Date: Mon, 20 Jan 2025 10:34:26 -0800 Subject: [PATCH 03/78] asimrun shouldn't need env_id --- asimtools/asimmodules/matgl/train_matgl.py | 50 ++++++++++++ .../transformations/delete_atoms.py | 79 +++++++++++++++++++ asimtools/job.py | 6 +- asimtools/scripts/asim_run.py | 2 +- 4 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 asimtools/asimmodules/matgl/train_matgl.py create mode 100644 asimtools/asimmodules/transformations/delete_atoms.py diff --git a/asimtools/asimmodules/matgl/train_matgl.py b/asimtools/asimmodules/matgl/train_matgl.py new file mode 100644 index 0000000..3ec8fc3 --- /dev/null +++ b/asimtools/asimmodules/matgl/train_matgl.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python +''' +Asimmodule for training MACE models + +Author: mkphuthi@github.com +''' +from typing import Dict, Optional, Union +import sys +from pathlib import Path +import logging +import warnings +warnings.filterwarnings("ignore") +import json +from numpy.random import randint +from mace.cli.run_train import main as mace_run_train_main +from mace.cli.create_lammps_model import main as create_lammps_model + +def train_mace( + config: Union[Dict,str], + randomize_seed: bool = False, + compile_lammps: bool = False, +) -> Dict: + """Runs MACE training + + :param config: MACE config dictionary or path to config file + :type config: Union[Dict,str] + :param randomize_seed: Whether to randomize the seed, defaults to False + :type randomize_seed: bool + :return: Dictionary of results + :rtype: Dict + """ + + if isinstance(config, str): + with open(config, 'r') as fp: + config = json.load(fp) + + if randomize_seed: + config['seed'] = randint(0, 1000000) + + config_file_path = str(Path("mace_config.yaml").resolve()) + with open(config_file_path, "w") as f: + json.dump(config, f, indent=2) + + logging.getLogger().handlers.clear() + sys.argv = ["program", "--config", config_file_path] + mace_run_train_main() + + if compile_lammps: + create_lammps_model('mace_test_compiled.model') + return {} diff --git a/asimtools/asimmodules/transformations/delete_atoms.py b/asimtools/asimmodules/transformations/delete_atoms.py new file mode 100644 index 0000000..9c49976 --- /dev/null +++ b/asimtools/asimmodules/transformations/delete_atoms.py @@ -0,0 +1,79 @@ +''' +Produce a set of images with unit cells scaled compared to the input + +author: mkphuthi@github.com +''' + +from typing import Dict, Optional, Sequence +import numpy as np +from ase.io import write +from asimtools.utils import ( + get_atoms, +) + +def apply_scale(old_atoms, scale): + ''' Applies a scaling factor to a unit cell ''' + atoms = old_atoms.copy() + new_cell = atoms.get_cell() * scale + atoms.set_cell(new_cell, scale_atoms=True) + atoms.info['scale'] = f'{scale:.3f}' + return atoms + +def scale_unit_cells( + image: Dict, + scales: Optional[Sequence] = None, + logspace: Optional[Sequence] = None, + linspace: Optional[Sequence] = None, + scale_by: str = 'a', +) -> Dict: + """Produce a set of images with unit cells scaled compared to the input + + :param image: Image specification, see :func:`asimtools.utils.get_atoms` + :type image: Dict + :param scales: Scaling values by which to scale cell, defaults to None + :type scales: Optional[Sequence], optional + :param logspace: Parameters to pass to np.logspace for scaling values, + defaults to None + :type logspace: Optional[Sequence], optional + :param linspace: Parameters to pass to np.linspace for scaling values, + defaults to None + :type linspace: Optional[Sequence], optional + :param scale_by: Scale either "volume" or "a" which is lattice parameter, + defaults to 'a' + :type scale_by: str, optional + :raises ValueError: If more than one of scales, linspace, logspace are + provided + :return: Path to xyz file + :rtype: Dict + """ + + assert scale_by in ['volume', 'a'], \ + 'Only scaling by "a" and "volume" allowed' + + if (scales is None and linspace is None and logspace is not None): + scales = np.logspace(*logspace) + elif (scales is None and linspace is not None and logspace is None): + scales = np.linspace(*linspace) + elif (scales is not None and linspace is None and logspace is None): + pass + else: + raise ValueError( + 'Provide only one of factors, factor_logspacem factor_linspace' + ) + + atoms = get_atoms(**image) + + scales = np.array(scales) + if scale_by == 'volume': + scales = scales**(1/3) + + # Make a database of structures with the volumes scaled appropriately + scaled_images = [] + for scale in scales: + new_atoms = apply_scale(atoms, scale) + scaled_images.append(new_atoms) + + scaled_images_file = 'scaled_unitcells_output.xyz' + write(scaled_images_file, scaled_images, format='extxyz') + + return {'files': {'images': scaled_images_file}} diff --git a/asimtools/job.py b/asimtools/job.py index dddc52a..46f4bbc 100644 --- a/asimtools/job.py +++ b/asimtools/job.py @@ -44,6 +44,7 @@ def __init__( sim_input: Dict, env_input: Union[Dict,None] = None, calc_input: Union[Dict,None] = None, + asimrun_mode: bool = False, ) -> None: if env_input is None: env_input = get_env_input() @@ -63,7 +64,7 @@ def __init__( self.sim_input['src_dir'] = self.launchdir self.env_id = self.sim_input.get('env_id', None) - if self.env_id is not None and self.env_input is not None: + if self.env_id is not None and not asimrun_mode: self.env = self.env_input[self.env_id] else: self.env = { @@ -1039,7 +1040,7 @@ def submit(self, dependency: Union[List,None] = None, debug: bool = False) -> Li return job_ids -def load_job_from_directory(workdir: os.PathLike) -> Job: +def load_job_from_directory(workdir: os.PathLike, asimrun_mode=False) -> Job: ''' Loads a job from a given directory ''' workdir = Path(workdir) assert workdir.exists(), f'Work directory "{workdir}" does not exist' @@ -1066,6 +1067,7 @@ def load_job_from_directory(workdir: os.PathLike) -> Job: sim_input=sim_input, env_input=env_input, calc_input=calc_input, + asimrun_mode=asimrun_mode, ) # This makes sure that wherever we may be loading the job from, we refer diff --git a/asimtools/scripts/asim_run.py b/asimtools/scripts/asim_run.py index 4fa90a4..8a8432a 100755 --- a/asimtools/scripts/asim_run.py +++ b/asimtools/scripts/asim_run.py @@ -132,7 +132,7 @@ def main(args=None) -> None: sim_func = getattr(sim_module, func_name) cwd = Path('.').resolve() - job = load_job_from_directory(cwd) + job = load_job_from_directory(cwd, asimrun_mode=True) job.start() try: From 9c92520a7ff5da4518d72fe1093859be9425b5eb Mon Sep 17 00:00:00 2001 From: Mgcini Keith Phuthi Date: Mon, 20 Jan 2025 10:35:34 -0800 Subject: [PATCH 04/78] precommand bug fix --- asimtools/job.py | 41 ++--------------------------------------- 1 file changed, 2 insertions(+), 39 deletions(-) diff --git a/asimtools/job.py b/asimtools/job.py index dddc52a..c6e375f 100644 --- a/asimtools/job.py +++ b/asimtools/job.py @@ -645,7 +645,7 @@ def _gen_array_script( # txt += ' WORKDIR=${fls[${SLURM_ARRAY_TASK_ID}]}\n' # txt += 'fi\n\n' # txt += 'cd ${WORKDIR}\n' - txt += ' ' + '\n'.join(slurm_params.get('precommands', [])) + txt += ' ' + '\n'.join(slurm_params.get('precommands', [])) + '\n' txt += '\n '.join( self.unitjobs[0].calc_params.get('precommands', []) ) + '\n' @@ -961,44 +961,7 @@ def submit(self, dependency: Union[List,None] = None, debug: bool = False) -> Li curjob.env['slurm']['flags']['-J'] = \ f'step-{step+i}' - # if i < len(self.unitjobs)-1: - # nextjob = self.unitjobs[i+1] - - # curworkdir = os.path.relpath( - # curjob.workdir, - # nextjob.workdir - # ) - # nextjob.sim_input['dependent_dir'] = str(curworkdir) - - # #### dep in job script implementation - # #if there is a following job - # if i < len(self.unitjobs)-1: - # nextjob = self.unitjobs[i+1] - - # nextworkdir = os.path.relpath( - # nextjob.workdir, - # curjob.workdir - # ) - # curworkdir = os.path.relpath( - # curjob.workdir, - # nextjob.workdir - # ) - # # Add postcommands to go into the next workdir - # postcommands = curjob.env['slurm'].get( - # 'postcommands', [] - # ) - # postcommands += ['\n#Submitting next step:'] - # postcommands += [f'cd {nextworkdir}'] - # # submit the next job dependent in the current - # # sh script - # submit_txt = 'asim-execute sim_input.yaml ' - # submit_txt += '-c calc_input.yaml ' - # submit_txt += '-e env_input.yaml ' - # postcommands += [submit_txt] - # postcommands += [f'cd {curworkdir}'] - # curjob.env['slurm']['postcommands'] = postcommands - # ##### - # submit the next job dependent on the current one + # submit the next job dependent on the current one # Previous working solution write_image = False # Write image first step in chain being run/continued From 114cb6597b3f8c2c17bf703c8d6e7685e0e23f14 Mon Sep 17 00:00:00 2001 From: Keith Phuthi Date: Fri, 14 Feb 2025 11:24:31 -0800 Subject: [PATCH 05/78] vasp,lammps,espresso --- CHANGELOG.md | 12 ++ .../geometry_optimization/cell_relax.py | 2 +- .../geometry_optimization/optimize.py | 2 +- .../symmetric_cell_relax.py | 2 +- asimtools/asimmodules/lammps/lammps.py | 10 +- asimtools/asimmodules/singlepoint.py | 2 +- asimtools/asimmodules/vasp/vasp.py | 142 ++++++++++++------ asimtools/calculators.py | 13 +- examples/external/VASP/calc_input.yaml | 6 +- .../external/VASP/vasp_ase_sim_input.yaml | 9 ++ .../external/VASP/vasp_mixed_sim_input.yaml | 18 +++ .../external/VASP/vasp_mpset_sim_input.yaml | 6 +- ...int_sim_input.yaml => vasp_sim_input.yaml} | 4 +- 13 files changed, 160 insertions(+), 68 deletions(-) create mode 100644 examples/external/VASP/vasp_ase_sim_input.yaml create mode 100644 examples/external/VASP/vasp_mixed_sim_input.yaml rename examples/external/VASP/{singlepoint_sim_input.yaml => vasp_sim_input.yaml} (69%) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6c1e2f..93a2f8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). + +## [develop] - 2025-2-14 + +### Added +- Can now specify whether to write velocities in lammps +### Changed +- VASP interface changed to align more with pymatgen principles + +### Fixed +- Minor bugs in geometry optimizations +- Updated EspressoProfile calculator to match ASE 3.25.0b1 + ## [0.1.0] - 2024-12-27 ### Added diff --git a/asimtools/asimmodules/geometry_optimization/cell_relax.py b/asimtools/asimmodules/geometry_optimization/cell_relax.py index 7c0d1b7..e69b24b 100755 --- a/asimtools/asimmodules/geometry_optimization/cell_relax.py +++ b/asimtools/asimmodules/geometry_optimization/cell_relax.py @@ -11,7 +11,7 @@ from typing import Dict, Optional, Sequence import ase.optimize -from ase.constraints import StrainFilter +from ase.filters import StrainFilter from ase.io.trajectory import Trajectory from asimtools.calculators import load_calc from asimtools.utils import get_atoms, join_names diff --git a/asimtools/asimmodules/geometry_optimization/optimize.py b/asimtools/asimmodules/geometry_optimization/optimize.py index 56850cf..6c38132 100755 --- a/asimtools/asimmodules/geometry_optimization/optimize.py +++ b/asimtools/asimmodules/geometry_optimization/optimize.py @@ -6,7 +6,7 @@ ''' from typing import Dict, Optional import ase.optimize -from ase.constraints import ExpCellFilter +from ase.filters import ExpCellFilter from ase.io.trajectory import Trajectory from asimtools.calculators import load_calc from asimtools.utils import get_atoms diff --git a/asimtools/asimmodules/geometry_optimization/symmetric_cell_relax.py b/asimtools/asimmodules/geometry_optimization/symmetric_cell_relax.py index d134808..f925bdd 100755 --- a/asimtools/asimmodules/geometry_optimization/symmetric_cell_relax.py +++ b/asimtools/asimmodules/geometry_optimization/symmetric_cell_relax.py @@ -6,7 +6,7 @@ ''' from typing import Dict, Optional import ase.optimize -from ase.constraints import ExpCellFilter +from ase.filters import ExpCellFilter from ase.spacegroup.symmetrize import FixSymmetry from ase.io.trajectory import Trajectory from asimtools.calculators import load_calc diff --git a/asimtools/asimmodules/lammps/lammps.py b/asimtools/asimmodules/lammps/lammps.py index 50d7f20..8d7d15c 100755 --- a/asimtools/asimmodules/lammps/lammps.py +++ b/asimtools/asimmodules/lammps/lammps.py @@ -22,6 +22,7 @@ def lammps( placeholders: Optional[Dict] = None, lmp_cmd: str = 'lmp', masses: bool = True, + velocities: bool = False, seed: Optional[int] = None, ) -> Dict: """Runs a lammps script based on a specified template, variables can be @@ -46,7 +47,9 @@ def lammps( :type lmp_cmd: str, optional :param masses: Whether to specify atomic masses in LAMMPS data input, requires ASE>3.23.0, defaults to True - :type masses: bool, optional + :param velocities: Whether to specify atomic velocities in LAMMPS data input, + requires ASE>3.23.0, defaults to False + :type velocities: bool, optional :param seed: Random seed for anywhere necessary in the template. You will need to put the 'SEED' placeholder anywhere you want a random seed to be placed, if seed=None, a random one is generated, @@ -62,15 +65,16 @@ def lammps( # in arbitrary image provided by asimtools if image is not None: atoms = get_atoms(**image) - if masses: + if masses or velocities: try: atoms.write( 'image_input.lmpdat', format='lammps-data', atom_style=atom_style, masses=masses, + velocities=velocities, ) - except TypeError as te: + except ValueError as te: err_txt = 'Need ASE version >=3.23 to support writing ' err_txt += 'masses to lammps input file. Add mass keyword to ' err_txt += 'lammps template instead' diff --git a/asimtools/asimmodules/singlepoint.py b/asimtools/asimmodules/singlepoint.py index 9347a6e..39903b0 100755 --- a/asimtools/asimmodules/singlepoint.py +++ b/asimtools/asimmodules/singlepoint.py @@ -32,7 +32,7 @@ def singlepoint( """ calc = load_calc(calc_id) atoms = get_atoms(**image) - atoms.set_calculator(calc) + atoms.calc = calc if prefix is not None: prefix = prefix + '_' diff --git a/asimtools/asimmodules/vasp/vasp.py b/asimtools/asimmodules/vasp/vasp.py index 3bf1346..00a98a7 100755 --- a/asimtools/asimmodules/vasp/vasp.py +++ b/asimtools/asimmodules/vasp/vasp.py @@ -15,84 +15,128 @@ import logging from ase.io import read from pymatgen.io.ase import AseAtomsAdaptor -from pymatgen.io.vasp import Poscar -from pymatgen.io.vasp.sets import ( - MPRelaxSet, MPStaticSet, MPNonSCFSet, MPScanRelaxSet -) +from pymatgen.io.vasp import Poscar, Incar, Potcar, Kpoints, VaspInput +import pymatgen.io.vasp.sets from asimtools.utils import ( get_atoms, ) def vasp( image: Optional[Dict], - vaspinput_args: Optional[Dict] = None, - command: str = 'vasp_std', + user_incar_settings: Optional[Dict] = None, + user_kpoints_settings: Optional[Dict] = None, + user_potcar_functional: str = 'PBE_64', + potcar: Optional[Dict] = None, + vaspinput_kwargs: Optional[Dict] = None, + command: str = 'srun vasp_std', mpset: Optional[str] = None, + prev_calc: Optional[os.PathLike] = None, write_image_output: bool = True, + run_vasp: bool = True, ) -> Dict: """Run VASP with given input files and specified image :param image: Initial image for VASP calculation. Image specification, see :func:`asimtools.utils.get_atoms` :type image: Dict - :param vaspinput_args: Dictionary of pymatgen's VASPInput arguments. + :param vaspinput_args: Dictionary of pymatgen's VaspInput arguments. See :class:`pymatgen.io.vasp.inputs.VaspInput` :type vaspinput_args: Dict :param command: Command with which to run VASP, defaults to 'vasp_std' :type command: str, optional - :param mpset: Materials Project VASP set to use, defaults to None + :param mpset: Materials Project VASP set to use see + :mod:`pymatgen.io.vasp.sets`, defaults to None :type mpset: str, optional :param write_image_output: Whether to write output image in standard asimtools format to file, defaults to False :type write_image_output: bool, optional """ - if vaspinput_args: - if image is not None: - atoms = get_atoms(**image) - struct = AseAtomsAdapter.get_structure(atoms) - vaspinput = VaspInput( - poscar=Poscar(struct), - **vaspinput_args - ) + if vaspinput_kwargs is None: + vaspinput_kwargs = {} + + atoms = get_atoms(**image) + struct = AseAtomsAdaptor.get_structure(atoms) + if mpset is not None: + # if not ((incar is None) or (potcar is None) or (kpoints is None)): + # raise ValueError( + # 'Provide either mpset or all of incar and kpoints' + # ) + try: + set_ = getattr(pymatgen.io.vasp.sets, mpset) + except: + raise ImportError( + f'Unknown mpset: {mpset}. See available sets in pymatgen.') + + if prev_calc is not None: + vasp_input = set_.from_prev_calc( + prev_calc, + user_incar_settings=user_incar_settings, + user_kpoints_settings=user_kpoints_settings, + user_potcar_functional=user_potcar_functional, + **vaspinput_kwargs + ) + else: + vasp_input = set_( + struct, + user_incar_settings=user_incar_settings, + user_kpoints_settings=user_kpoints_settings, + user_potcar_functional=user_potcar_functional, + **vaspinput_kwargs + ) + else: - atoms = get_atoms(**image) - struct = AseAtomsAdaptor.get_structure(atoms) - if mpset == 'MPRelaxSet': - vasp_input = MPRelaxSet(struct) - elif mpset == 'MPStaticSet': - vasp_input = MPStaticSet(struct) - elif mpset == 'MPNonSCFSet': - vasp_input = MPNonSCFSet(struct) - elif mpset == 'MPScanRelaxSet': - vasp_input = MPScanRelaxSet(struct) + + incar = Incar(user_incar_settings) + incar.check_params() + if potcar is not None: + potcar = Potcar(potcar) + if user_kpoints_settings is not None: + kpoints = Kpoints(user_kpoints_settings) else: - raise ValueError(f'Unknown MPSet: {mpset}') + kpoints=None - vasp_input.write_input("./") + if vaspinput_args is None: + vaspinput_args = {} - command = command.split(' ') - completed_process = subprocess.run( - command, check=False, capture_output=True, text=True, + vasp_input = VaspInput( + incar=incar, + kpoints=kpoints, + poscar=Poscar(struct), + potcar=potcar, + **vaspinput_kwargs ) - with open('vasp_stdout.txt', 'a+', encoding='utf-8') as f: - f.write(completed_process.stdout) - - if completed_process.returncode != 0: - err_txt = f'Failed to run VASP\n' - err_txt += 'See vasp_stderr.txt for details.' - logging.error(err_txt) - with open('vasp_stderr.txt', 'a+', encoding='utf-8') as f: - f.write(completed_process.stderr) - completed_process.check_returncode() - return {} - - if write_image_output: - atoms_output = read('OUTCAR') - atoms_output.write( - 'image_output.xyz', - format='extxyz', - ) + + vasp_input.write_input("./") + # if incar_kwargs is not None: + # with open('INCAR', 'a+') as fp: + # for k, v in incar_kwargs.items(): + # fp.write(f'\n{k} = {v}') + + if run_vasp: + command = command.split(' ') + completed_process = subprocess.run( + command, check=False, capture_output=True, text=True, + ) + + with open('vasp_stdout.txt', 'a+', encoding='utf-8') as f: + f.write(completed_process.stdout) + + if completed_process.returncode != 0: + err_txt = f'Failed to run VASP\n' + err_txt += 'See vasp_stderr.txt for details.' + logging.error(err_txt) + with open('vasp_stderr.txt', 'a+', encoding='utf-8') as f: + f.write(completed_process.stderr) + completed_process.check_returncode() + return {} + + if write_image_output: + atoms_output = read('OUTCAR') + atoms_output.write( + 'image_output.xyz', + format='extxyz', + ) return {} diff --git a/asimtools/calculators.py b/asimtools/calculators.py index 40704d4..700b29b 100644 --- a/asimtools/calculators.py +++ b/asimtools/calculators.py @@ -239,19 +239,20 @@ def load_espresso_profile(calc_params): if 'command' in calc_params['args']: calc_params = deepcopy(calc_params) command = calc_params['args'].pop('command') - command = command.split() - progind = command.index('pw.x') - argv = command[:progind+1] else: - argv = ['pw.x'] + command = 'pw.x' + if 'pseudo_dir' in calc_params['args']: + pseudo_dir = calc_params['args'].pop('pseudo_dir') + else: + pseudo_dir = None try: calc = Espresso( **calc_params['args'], - profile=EspressoProfile(argv=argv) + profile=EspressoProfile(command=command, pseudo_dir=pseudo_dir) ) except Exception: - logging.error("Failed to load MACE-OFF with parameters:\n %s", calc_params) + logging.error("Failed to load Espresso with parameters:\n %s", calc_params) raise return calc diff --git a/examples/external/VASP/calc_input.yaml b/examples/external/VASP/calc_input.yaml index 9939693..25c757a 100644 --- a/examples/external/VASP/calc_input.yaml +++ b/examples/external/VASP/calc_input.yaml @@ -7,5 +7,9 @@ vasp_PBE: prec: Normal xc: PBE lreal: False - command: srun vasp_std + command: srun --mpi=pmi2 vasp_std + # See ASE vasp kpoints specification + kpts: [1,1,1] + # other parameters not specified through ASE can be set directly + NCORE: 4 diff --git a/examples/external/VASP/vasp_ase_sim_input.yaml b/examples/external/VASP/vasp_ase_sim_input.yaml new file mode 100644 index 0000000..3c1191f --- /dev/null +++ b/examples/external/VASP/vasp_ase_sim_input.yaml @@ -0,0 +1,9 @@ +asimmodule: singlepoint +workdir: vasp_ase_results +env_id: n24 # Specify your own env +args: + calc_id: vasp_PBE + image: + name: Na + builder: bulk + properties: ['energy', 'forces'] diff --git a/examples/external/VASP/vasp_mixed_sim_input.yaml b/examples/external/VASP/vasp_mixed_sim_input.yaml new file mode 100644 index 0000000..90206a1 --- /dev/null +++ b/examples/external/VASP/vasp_mixed_sim_input.yaml @@ -0,0 +1,18 @@ +# This example shows how to use a mpset as a template then specify your own INCAR +asimmodule: vasp.vasp +workdir: vasp_mixed_results +env_id: n24 # Specify your own env +args: + image: + name: Na + builder: bulk + incar: + ENCUT: 480 + KSPACING: 0.7 + IBRION: -1 + kpoints: + kpts: [[1,1,1]] + potcar: + Na: PBE + + command: srun --mpi=pmi2 vasp_std diff --git a/examples/external/VASP/vasp_mpset_sim_input.yaml b/examples/external/VASP/vasp_mpset_sim_input.yaml index 2dd3880..b3ae4a4 100644 --- a/examples/external/VASP/vasp_mpset_sim_input.yaml +++ b/examples/external/VASP/vasp_mpset_sim_input.yaml @@ -1,9 +1,9 @@ asimmodule: vasp.vasp workdir: vasp_mpset_results -env_id: inline +env_id: n24 # Specify your own env args: image: name: Na builder: bulk - mpset: MPStaticSet - command: vasp_std + mpset: MatPESStaticSet + command: srun --mpi=pmi2 vasp_std diff --git a/examples/external/VASP/singlepoint_sim_input.yaml b/examples/external/VASP/vasp_sim_input.yaml similarity index 69% rename from examples/external/VASP/singlepoint_sim_input.yaml rename to examples/external/VASP/vasp_sim_input.yaml index acd4c33..cdee4e4 100644 --- a/examples/external/VASP/singlepoint_sim_input.yaml +++ b/examples/external/VASP/vasp_sim_input.yaml @@ -1,8 +1,8 @@ asimmodule: singlepoint workdir: vasp_results -env_id: inline +env_id: n24 # Specify your own env args: - calc_id: vasp + calc_id: vasp_PBE image: name: Na builder: bulk From e6fd240e2d523546e8faa8102a04853f214ea40f Mon Sep 17 00:00:00 2001 From: Keith Phuthi Date: Tue, 11 Mar 2025 13:39:03 -0700 Subject: [PATCH 06/78] placeholders --- CHANGELOG.md | 10 +- .../asimmodules/benchmarking/distribution.py | 102 ++++++++++-------- asimtools/asimmodules/data/collect_images.py | 50 ++++++++- asimtools/asimmodules/lammps/utils.py | 58 ++++++++++ asimtools/asimmodules/matgl/train_matgl.py | 50 --------- asimtools/asimmodules/workflows/sim_array.py | 17 ++- asimtools/calculators.py | 44 +++++++- asimtools/utils.py | 54 ++++++++-- examples/internal/sim_array/run.sh | 5 + ...rystalstructure_placeholder_sim_input.yaml | 18 ++++ .../sim_array_crystalstructure_sim_input.yaml | 2 +- tests/unit/test_utils.py | 12 +++ 12 files changed, 309 insertions(+), 113 deletions(-) create mode 100644 asimtools/asimmodules/lammps/utils.py delete mode 100644 asimtools/asimmodules/matgl/train_matgl.py create mode 100644 examples/internal/sim_array/sim_array_crystalstructure_placeholder_sim_input.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index 93a2f8b..2e33558 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,18 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [develop] - 2025-2-14 ### Added +- Can now use placehodlers in sim_array where the array_values replace part of +the string at the given key_sequence - Can now specify whether to write velocities in lammps +- Calculator: Now added DFTD3 calculator using the ASE interface, works with any calculator. Two things of note: 1. You need to install DFTD3 from +https://www.chemie.uni-bonn.de/grimme/de/software/dft-d3/get_dft-d3. 2. Some +calculators which return a 3x3 matrix for stress will break. One can modify +ASE source for this as ASIMTools can't go into the calculator code. ### Changed -- VASP interface changed to align more with pymatgen principles +- VASP interface changed to align more with pymatgen +- asimtools.utils.write_yaml now stops sorting keys to help with readability of +written yamls ### Fixed - Minor bugs in geometry optimizations diff --git a/asimtools/asimmodules/benchmarking/distribution.py b/asimtools/asimmodules/benchmarking/distribution.py index c02ea97..47ab913 100644 --- a/asimtools/asimmodules/benchmarking/distribution.py +++ b/asimtools/asimmodules/benchmarking/distribution.py @@ -5,7 +5,7 @@ Author: mkphuthi@github.com ''' -from typing import Dict, Optional +from typing import Dict, Optional, Sequence import numpy as np import matplotlib.pyplot as plt from ase.units import kg, m as meters @@ -20,8 +20,8 @@ def distribution( unit: str = 'eV', bins: int = 50, log: bool = True, + properties: Sequence[str] = ('energy', 'forces', 'stress', 'volume', 'pressure', 'enthalpy', 'density', 'mass', 'natoms'), remap_keys: Optional[Dict] = None, - skip_failed: bool = False, ) -> Dict: if remap_keys is None: remap_keys = {} @@ -40,55 +40,71 @@ def distribution( 'natoms': 'atoms', } images = get_images(**images) - results = {prop: [] for prop in unit_dict} + results = {prop: [] for prop in properties} for i, atoms in enumerate(images): - include = True results['natoms'].append(len(atoms)) - if remap_keys.get('energy', False): - energy = atoms.info[remap_keys['energy']] - else: - energy = atoms.get_potential_energy() - results['energy'].append(energy) - if remap_keys.get('forces', False): - forces = atoms.arrays[remap_keys['forces']] - else: - forces = atoms.get_forces() - results['forces'].extend( - list(np.array(forces).flatten()) - ) - results['volume'].append(atoms.get_volume()) - if remap_keys.get('stress', False): - stress = atoms.arrays[remap_keys['stress']] - elif remap_keys.get('virial', False): - try: - stress = atoms.info[remap_keys['virial']] / atoms.get_volume() - except KeyError: - print('idx:', i, atoms.info, atoms.arrays) - else: - stress = atoms.get_stress(voigt=True) - results['stress'].extend( - list(np.array(stress)) * unit_factor - ) - results['pressure'].append(-np.sum(stress[:3])/3) - mass = np.sum(atoms.get_masses()) - results['mass'].append(mass) + if 'energy' in properties: + if remap_keys.get('energy', False): + energy = atoms.info[remap_keys['energy']] + else: + energy = atoms.get_potential_energy() + + results['energy'].append(energy) + if 'forces' in properties: + if remap_keys.get('forces', False): + forces = atoms.arrays[remap_keys['forces']] + else: + forces = atoms.get_forces() + + results['forces'].extend( + list(np.array(forces).flatten()) + ) + if 'volume' in properties: + results['volume'].append(atoms.get_volume()) + if 'stress' in properties: + if remap_keys.get('stress', False): + stress = atoms.arrays[remap_keys['stress']] + elif remap_keys.get('virial', False): + try: + stress = atoms.info[remap_keys['virial']] / atoms.get_volume() + except KeyError: + print('idx:', i, atoms.info, atoms.arrays) + else: + stress = atoms.get_stress(voigt=True) + + results['stress'].extend( + list(np.array(stress)) + ) + if 'pressure' in properties: + results['pressure'].append(-np.sum(stress[:3])/3) + if 'mass' in properties: + mass = np.sum(atoms.get_masses()) + results['mass'].append(mass) - for prop in unit_dict: + for prop in properties: results[prop] = np.array(results[prop]) - results['density'] = ( - (results['mass'] * kg * 1000) / (results['volume'] / cm3) - ) - results['enthalpy'] = ( - results['energy'] + results['pressure'] * results['volume'] - ) + if 'density' in properties: + assert 'mass' in properties and 'volume' in properties, \ + 'Mass and volume must be included to calculate density' + results['density'] = ( + (results['mass'] * kg * 1000) / (results['volume'] / cm3) + ) + if 'enthalpy' in properties: + assert 'energy' in properties and 'pressure' in properties and 'volume' in properties, \ + 'Energy, pressure and volume must be included to calculate enthalpy' + results['enthalpy'] = ( + results['energy'] + results['pressure'] * results['volume'] + ) for prop in ['energy', 'volume', 'enthalpy']: - results[prop] = results[prop] / results['natoms'] + if prop in properties: + results[prop] = results[prop] / results['natoms'] for prop in ['forces', 'stress', 'pressure', 'energy', 'enthalpy']: - results[prop] = results[prop] * unit_factor - - for prop in unit_dict: + if prop in properties: + results[prop] = results[prop] * unit_factor + print(results['energy']) + for prop in properties: with open(f'summary.txt', 'a+') as f: f.write(f'{prop} distribution\n') f.write(f'Num. values: {len(results[prop])}\n') diff --git a/asimtools/asimmodules/data/collect_images.py b/asimtools/asimmodules/data/collect_images.py index a2a1e70..fbbd07e 100644 --- a/asimtools/asimmodules/data/collect_images.py +++ b/asimtools/asimmodules/data/collect_images.py @@ -6,6 +6,9 @@ from typing import Dict, Optional, Sequence import numpy as np +from copy import deepcopy +from pymatgen.io.ase import AseAtomsAdaptor as AAA +from pymatgen.analysis.structure_matcher import StructureMatcher from ase.io import write from asimtools.utils import ( get_atoms, get_images, new_db @@ -18,6 +21,8 @@ def collect_images( fnames: Sequence[str] = ['output_images.xyz'], splits: Optional[Sequence[float]] = (1,), shuffle: bool = True, + sort_by_energy_per_atom: bool = False, + remove_duplicates: Optional[bool] = False, rename_keys: Optional[Dict] = None, energy_per_atom_limits: Optional[Sequence[float]] = None, force_max: Optional[float] = None, @@ -36,9 +41,17 @@ def collect_images( :type splits: Optional[Sequence[float]], optional :param shuffle: shuffle images before splitting, defaults to True :type shuffle: bool, optional - :param rename_keys: keys to rename on writing to the output file, defaults to None + :param sort_by_energy_per_atom: sort images before splitting, defaults to + False + :type sort_by_energy_per_atom: bool, optional + :param remove_duplicates: Whether to search for and remove duplicates with + :class:`pymatgen.analysis.structure_matcher.StructureMatcher`. This is + quite slow, defaults to False + :param rename_keys: keys to rename on writing to the output file, defaults + to None :type rename_keys: Optional[Dict], optional - :param energy_per_atom_limits: energy limits for filtering images, defaults to None + :param energy_per_atom_limits: energy limits for filtering images, + defaults to None :type energy_per_atom_limits: Optional[Sequence[float]], optional :param force_max: forces maximimum for filtering images, defaults to None :type force_max: Optional[float], optional @@ -77,7 +90,6 @@ def collect_images( if (min_stress < stress_limits[0]): if (max_stress > stress_limits[1]): select = False - print(select) if select: if rename_keys is not None and out_format == 'extxyz': @@ -93,9 +105,39 @@ def collect_images( nonselected_atoms.append(atoms) write('nonselected_images.xyz', nonselected_atoms, format='extxyz') + + if remove_duplicates: + all_structs = [AAA.get_structure(atoms) for atoms in selected_atoms] + selected_inds = [0] + selected_structs = [all_structs[0]] + sm = StructureMatcher() + num_duplicates = 0 + for i, struct in enumerate(all_structs): + is_duplicate = False + for j, selected_struct in enumerate(selected_structs): + is_duplicate = sm.fit(struct, selected_struct) + if is_duplicate: + num_duplicates += 1 + break + if not is_duplicate: + selected_structs.append(struct) + selected_inds.append(i) + selected_atoms = [selected_atoms[i] for i in selected_inds] + if shuffle: np.random.shuffle(selected_atoms) - datasets = [] + elif sort_by_energy_per_atom: + assert not shuffle, 'Either sort or shuffle, not both' + e_per_atoms = [ + atoms.get_potential_energy() / len(atoms) for atoms \ + in selected_atoms + ] + sort_result = sorted( + zip(e_per_atoms, selected_atoms), key=lambda x: x[0] + ) + + selected_atoms = [x[1] for x in sort_result] + start_index = 0 index_ranges = [] for i, split in enumerate(splits): diff --git a/asimtools/asimmodules/lammps/utils.py b/asimtools/asimmodules/lammps/utils.py new file mode 100644 index 0000000..12a9f2d --- /dev/null +++ b/asimtools/asimmodules/lammps/utils.py @@ -0,0 +1,58 @@ +import os +from glob import glob +import pickle +from pathlib import Path +import pandas as pd +from ase.units import bar +import numpy as np + +def read_lammps_log(logfile, skip_failed=False): + with open(logfile, 'r') as f: + logtxt = f.readlines() + + starts = [] + stops = [] + natoms = None + for i, line in enumerate(logtxt): + if 'Step' in line: + starts.append(i + 1) + if len(starts) > len(stops): + if 'Loop time' in line or 'WARNING: Pair style restartinfo' in line: + stops.append(i) + if 'atoms' in line: + atoms_line = line.split() + if len(atoms_line) == 2: + try: + natoms = int(atoms_line[0]) + except: + pass + + if natoms is None: + if 'Loop time' in line: + try: + natoms = int(line.split()[-2]) + except: + pass + + if skip_failed and (len(starts) != len(stops) or len(starts) == 0): + print(f"Incomplete run for {Path(logfile).resolve()}") + return False + + assert len(starts) != 0, f"No data in {logfile}" + assert natoms is not None, f"Could not find natoms in {logfile}" + + if len(starts) > len(stops): + stops.append(-5) + + headings = logtxt[starts[0]-1].split() + data = np.empty(len(headings)) + for start, stop in zip(starts, stops): + for data_line in logtxt[start:stop]: + data = np.vstack([data, np.fromstring(data_line, dtype=float, sep=' ')]) + + metadata = { + 'natoms': natoms, + 'columns': headings, + } + + return data[1:,:], metadata \ No newline at end of file diff --git a/asimtools/asimmodules/matgl/train_matgl.py b/asimtools/asimmodules/matgl/train_matgl.py deleted file mode 100644 index 3ec8fc3..0000000 --- a/asimtools/asimmodules/matgl/train_matgl.py +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env python -''' -Asimmodule for training MACE models - -Author: mkphuthi@github.com -''' -from typing import Dict, Optional, Union -import sys -from pathlib import Path -import logging -import warnings -warnings.filterwarnings("ignore") -import json -from numpy.random import randint -from mace.cli.run_train import main as mace_run_train_main -from mace.cli.create_lammps_model import main as create_lammps_model - -def train_mace( - config: Union[Dict,str], - randomize_seed: bool = False, - compile_lammps: bool = False, -) -> Dict: - """Runs MACE training - - :param config: MACE config dictionary or path to config file - :type config: Union[Dict,str] - :param randomize_seed: Whether to randomize the seed, defaults to False - :type randomize_seed: bool - :return: Dictionary of results - :rtype: Dict - """ - - if isinstance(config, str): - with open(config, 'r') as fp: - config = json.load(fp) - - if randomize_seed: - config['seed'] = randint(0, 1000000) - - config_file_path = str(Path("mace_config.yaml").resolve()) - with open(config_file_path, "w") as f: - json.dump(config, f, indent=2) - - logging.getLogger().handlers.clear() - sys.argv = ["program", "--config", config_file_path] - mace_run_train_main() - - if compile_lammps: - create_lammps_model('mace_test_compiled.model') - return {} diff --git a/asimtools/asimmodules/workflows/sim_array.py b/asimtools/asimmodules/workflows/sim_array.py index 46de2ba..86c0615 100755 --- a/asimtools/asimmodules/workflows/sim_array.py +++ b/asimtools/asimmodules/workflows/sim_array.py @@ -21,6 +21,7 @@ def sim_array( file_pattern: Optional[str] = None, linspace_args: Optional[Sequence] = None, arange_args: Optional[Sequence] = None, + placeholder: Optional[str] = None, env_ids: Optional[Union[Sequence[str],str]] = None, calc_input: Optional[Dict] = None, env_input: Optional[Dict] = None, @@ -29,6 +30,7 @@ def sim_array( str_btn_args: Optional[Dict] = None, secondary_key_sequences: Optional[Sequence] = None, secondary_array_values: Optional[Sequence] = None, + secondary_placeholders: Optional[Sequence] = None, array_max: Optional[int] = None, skip_failed: Optional[bool] = False, group_size: int = 1, @@ -53,6 +55,9 @@ def sim_array( :param arange_args: arguments to pass to :func:`numpy.arange` to be iterated over in each simulation, defaults to None :type arange_args: Optional[Sequence], optional + :param placeholder: placeholder is string dict value to replace with the + array value + :type placeholder: Optional[str], optional :param labels: Custom labels to use for each simulation, defaults to None :type labels: Sequence, optional :param label_prefix: Prefix to add before labels which can make extracting @@ -70,6 +75,10 @@ def sim_array( over in tandem with array_values to allow changing multiple key-value pairs, defaults to None :type secondary_array_values: Sequence, optional + :param secondary_placeholders: list of other other placeholders to iterate + over in tandem with array_values to allow changing multiple key-value + pairs, defaults to None + :type secondary_placeholders: Sequence, optional :param array_max: Number of jobs to run at once in scheduler :type array_max: int, optional :param calc_input: calc_input file to use, defaults to None @@ -113,17 +122,23 @@ def sim_array( new_value=val, key_sequence=key_sequence, return_copy=True, + placeholder=placeholder, ) else: new_sim_input = deepcopy(template_sim_input) if secondary_array_values is not None: - for k, vs in zip(secondary_key_sequences, secondary_array_values): + for j, (k, vs) in enumerate( + zip(secondary_key_sequences, secondary_array_values) + ): + if secondary_placeholders is not None: + secondary_placeholder = secondary_placeholders[j] new_sim_input = change_dict_value( d=new_sim_input, new_value=vs[i], key_sequence=k, return_copy=False, + placeholder=secondary_placeholder, ) if env_ids is not None: diff --git a/asimtools/calculators.py b/asimtools/calculators.py index 700b29b..40d7c55 100644 --- a/asimtools/calculators.py +++ b/asimtools/calculators.py @@ -259,7 +259,7 @@ def load_espresso_profile(calc_params): def load_m3gnet(calc_params): - """Load and M3GNet calculator + """Load any M3GNet or MatGL calculator :param calc_params: parameters to be passed to matgl.ext.ase.M3GNetCalculator. Must include a key "model" that points to the model used to instantiate the potential :type calc_params: Dict @@ -283,10 +283,11 @@ def load_m3gnet(calc_params): return calc def load_omat24(calc_params): - """Load and OMAT24 calculator + """Load any OMAT24 calculator :param calc_params: parameters to be passed to fairchem.core.OCPCalculator. - Must include a key "model" that points to the model used to instantiate the potential + Must include a key "model" that points to the model files used to + instantiate the potential :type calc_params: Dict :return: OMAT24 calculator :rtype: :class:`fairchem.core.OCPCalculator` @@ -296,7 +297,41 @@ def load_omat24(calc_params): try: calc = OCPCalculator(**calc_params['args']) except Exception: - logging.error("Failed to load OMAT24 with parameters:\n %s", calc_params) + logging.error( + "Failed to load OMAT24 with parameters:\n %s", calc_params + ) + raise + + return calc + +def load_ase_dftd3(calc_params): + """Load any calculator with DFTD3 correction as implemented in ASE + + :param calc_params: Dictionary with 2 keys. First is `d3_args` which is + passed to ase.calculators.dftd3.DFTD3 except for the dft argument. The + second is dft_calc_id or dft_calc_params which loads the calculator + to be wrapped. + :type calc_params: Dict + :return: ASE calculator + :rtype: :class:`ase.calculators.calculators.Calculator` + """ + from ase.calculators.dftd3 import DFTD3 + d3_args = calc_params['args'].get('d3_args', {}) + if 'dft' in d3_args: + raise ValueError('Do not specify dft arg for DFTD3, specify calc_id') + + dft_calc_id = calc_params['args'].get('dft_calc_id', None) + dft_calc_params = calc_params['args'].get('dft_calc_params', None) + if ( (dft_calc_id is not None) and (dft_calc_params is not None) ): + raise ValueError('Provide only one of dft_calc_id or dft_calc_params') + + dft = load_calc(calc_id=dft_calc_id, calc_params=dft_calc_params) + try: + calc = DFTD3(dft=dft, **d3_args) + except Exception: + logging.error( + "Failed to load d3 calculator with parameters:\n %s", calc_params + ) raise return calc @@ -311,4 +346,5 @@ def load_omat24(calc_params): 'EspressoProfile': load_espresso_profile, 'M3GNet': load_m3gnet, 'OMAT24': load_omat24, + 'ASEDFTD3': load_ase_dftd3, } diff --git a/asimtools/utils.py b/asimtools/utils.py index d05ace3..c78c43e 100644 --- a/asimtools/utils.py +++ b/asimtools/utils.py @@ -14,6 +14,7 @@ import yaml from natsort import natsorted import numpy as np +import matplotlib.pyplot as plt import pandas as pd from ase.io import read from ase.parallel import paropen @@ -49,7 +50,7 @@ def write_yaml(yaml_path: str, yaml_Dict: Dict) -> None: """ # Use paropen so that only the master process is updating outputs with paropen(yaml_path, 'w', encoding='utf-8') as f: - yaml.dump(yaml_Dict, f) + yaml.dump(yaml_Dict, f, sort_keys=False) def get_axis_lims(x: Sequence, y: Sequence, padding: float=0.1): """Get an estimate of good limits for a plot axis""" @@ -59,6 +60,27 @@ def get_axis_lims(x: Sequence, y: Sequence, padding: float=0.1): lims = [data_min - padding * diff, data_max + padding * diff] return lims +def improve_plot(ax=None, fontsize=14): + if ax is None: + ax = plt.gca() + + ax.tick_params(labelsize=fontsize) + ax.set_xlabel(ax.get_xlabel(), fontsize=fontsize+2) + ax.set_ylabel(ax.get_ylabel(), fontsize=fontsize+2) + ax.set_title(ax.get_title(), fontsize=fontsize+4) + + # Format Legends + if ax.get_legend() is not None: + plt.rc('legend', fontsize=fontsize) + leg = ax.get_legend() + leg.fontsize = fontsize + if leg.get_title() is not None: + leg.set_title( + leg.get_title().get_text(), + prop={'size': fontsize+2} + ) + plt.tight_layout() + def write_csv_from_dict( fname: str, data: Dict, @@ -596,7 +618,8 @@ def change_dict_value( d: Dict, new_value, key_sequence: Sequence, - return_copy: bool = True + return_copy: Optional[bool] = True, + placeholder: Optional[str] = None, ) -> Dict: """Changes a value in the specified dictionary given by following the key sequence @@ -605,9 +628,11 @@ def change_dict_value( :type d: Dict :param new_value: The new value that will replace the old one :type new_value: _type_ - :param key_sequence: List of keys in the order in which they access the dictionary key + :param key_sequence: List of keys in the order in which they access the + dictionary key :type key_sequence: Sequence - :param return_copy: Whether to return a copy only or to modify the dictionary in-place as well, defaults to True + :param return_copy: Whether to return a copy only or to modify the + dictionary in-place as well, defaults to True :type return_copy: bool, optional :return: The changed dictionary :rtype: Dict @@ -615,14 +640,20 @@ def change_dict_value( if return_copy: d = deepcopy(d) if len(key_sequence) == 1: - d[key_sequence[0]] = new_value + if placeholder is None: + d[key_sequence[0]] = new_value + else: + d[key_sequence[0]] = d[key_sequence[0]].replace( + placeholder, new_value + ) return d else: new_d = change_dict_value( d[key_sequence[0]], new_value, key_sequence[1:], - return_copy=return_copy + return_copy=return_copy, + placeholder=placeholder, ) d[key_sequence[0]] = new_d return d @@ -640,9 +671,11 @@ def change_dict_values( :type d: Dict :param new_values: The new values that will replace the old one :type new_values: Sequence - :param key_sequence: List of list of keys in the order in which they access the dictionary key + :param key_sequence: List of list of keys in the order in which they + access the dictionary key :type key_sequence: Sequence - :param return_copy: Whether to return a copy only or to modify the dictionary in-place as well, defaults to True + :param return_copy: Whether to return a copy only or to modify the + dictionary in-place as well, defaults to True :type return_copy: bool, optional :return: The changed dictionary :rtype: Dict @@ -749,7 +782,10 @@ def expand_wildcards(d: Dict, root_path: os.PathLike = None) -> Dict: :rtype: Dict[str, Any] """ import os - def expand_value(value: str, root_path: os.PathLike = None) -> Union[str, list]: + def expand_value( + value: str, + root_path: os.PathLike = None + ) -> Union[str, list]: if '*' in value: if root_path is None: root_path = Path('./') diff --git a/examples/internal/sim_array/run.sh b/examples/internal/sim_array/run.sh index 96e0ed1..e4d8690 100644 --- a/examples/internal/sim_array/run.sh +++ b/examples/internal/sim_array/run.sh @@ -29,3 +29,8 @@ asim-execute sim_array_calc_id_sim_input.yaml -c ../calc_input.yaml -e ../env_in # It also autmatically names the result directories (ids) corresponding to the # name of the file. You can do this with any input file, not just structures asim-execute sim_array_image_file_sim_input.yaml -c ../calc_input.yaml -e ../env_input.yaml + +# Example 5: +# This example runs the same calculator on Cu with different +# crystal structures but uses a placeholder instead that is replaced by the array_values +asim-execute sim_array_crystalstructure_sim_input.yaml -c ../calc_input.yaml -e ../env_input.yaml \ No newline at end of file diff --git a/examples/internal/sim_array/sim_array_crystalstructure_placeholder_sim_input.yaml b/examples/internal/sim_array/sim_array_crystalstructure_placeholder_sim_input.yaml new file mode 100644 index 0000000..81eaecc --- /dev/null +++ b/examples/internal/sim_array/sim_array_crystalstructure_placeholder_sim_input.yaml @@ -0,0 +1,18 @@ +asimmodule: workflows.sim_array +workdir: crystalstructure_placeholder_results +args: + key_sequence: ['args', 'image', 'crystalstructure'] + labels: [fcc, bcc] # Check what happens in the results dir if you don't specify labels + placeholder: PLACEHOLDER + array_values: [f,b] + env_ids: inline + template_sim_input: + asimmodule: singlepoint + args: + calc_id: lj_Cu + image: + builder: bulk + name: Cu + crystalstructure: PLACEHOLDERcc + a: 3.655 + \ No newline at end of file diff --git a/examples/internal/sim_array/sim_array_crystalstructure_sim_input.yaml b/examples/internal/sim_array/sim_array_crystalstructure_sim_input.yaml index 93634a0..25e6fc2 100644 --- a/examples/internal/sim_array/sim_array_crystalstructure_sim_input.yaml +++ b/examples/internal/sim_array/sim_array_crystalstructure_sim_input.yaml @@ -2,7 +2,7 @@ asimmodule: workflows.sim_array workdir: crystalstructure_results args: key_sequence: ['args', 'image'] - labels: [fcc, bcc] # Check what happens in the results dir if you don't specify ids + labels: [fcc, bcc] # Check what happens in the results dir if you don't specify labels array_values: - builder: bulk name: Cu diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 7b3acce..b08e5dc 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -122,6 +122,18 @@ def test_change_dict_value(test_input, expected): assert new_d == expected assert new_d != d +@pytest.mark.parametrize("test_input, expected",[ + (['l1', 'l2', 'l3'], {'l1': {'l2': {'l3': 'l3_NEW'}}}), +]) +def test_change_dict_value_placeholder(test_input, expected): + ''' Test getting iterable of atoms from different inputs ''' + d = {'l1': {'l2': {'l3': 'l3_PLACEHOLDER'}}} + new_d = change_dict_value( + d, 'NEW', test_input, return_copy=True, placeholder='PLACEHOLDER', + ) + assert new_d == expected + assert new_d != d + @pytest.mark.parametrize("test_input, expected",[ ([['l1', 'l21', 'l31'], ['l1', 'l21', 'l32']], {'l1': {'l21': {'l31': 'v1', 'l32': 'v2'}, 'l22': 'l22v'}}), From 354ae346686454ec3722cc38829e86da7f5a1fc4 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Fri, 14 Mar 2025 17:01:43 -0700 Subject: [PATCH 07/78] placeholder bug fix, time_unit ase_md --- asimtools/asimmodules/ase_md/ase_md.py | 5 +++++ asimtools/asimmodules/workflows/sim_array.py | 2 ++ asimtools/asimmodules/workflows/utils.py | 8 +++++++- asimtools/utils.py | 5 ++++- 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/asimtools/asimmodules/ase_md/ase_md.py b/asimtools/asimmodules/ase_md/ase_md.py index 69a8a34..316dbfa 100644 --- a/asimtools/asimmodules/ase_md/ase_md.py +++ b/asimtools/asimmodules/ase_md/ase_md.py @@ -172,6 +172,7 @@ def ase_md( pfactor: Optional[float] = None, externalstress: Optional[float] = 0, plot: Optional[bool] = True, + time_unit: Optional[str] = 'ase', ) -> Dict: """Runs ASE MD simulations. This is only recommended for small systems and for testing. For larger systems, use LAMMPS or more purpose-built code @@ -207,6 +208,10 @@ def ase_md( atoms = get_atoms(**image) atoms.set_calculator(calc) + if time_unit == 'fs': + timestep *= fs + ttime *= fs + assert dynamics in ['nvt', 'langevin', 'npt'], 'Invalid dynamics' if dynamics == 'langevin': atoms, _ = langevin_nvt( diff --git a/asimtools/asimmodules/workflows/sim_array.py b/asimtools/asimmodules/workflows/sim_array.py index 86c0615..18eaa46 100755 --- a/asimtools/asimmodules/workflows/sim_array.py +++ b/asimtools/asimmodules/workflows/sim_array.py @@ -133,6 +133,8 @@ def sim_array( ): if secondary_placeholders is not None: secondary_placeholder = secondary_placeholders[j] + else: + secondary_placeholder = None new_sim_input = change_dict_value( d=new_sim_input, new_value=vs[i], diff --git a/asimtools/asimmodules/workflows/utils.py b/asimtools/asimmodules/workflows/utils.py index d1b6913..f97b9fe 100644 --- a/asimtools/asimmodules/workflows/utils.py +++ b/asimtools/asimmodules/workflows/utils.py @@ -1,5 +1,6 @@ from typing import Dict, Sequence, Optional, Union from glob import glob +from pathlib import Path from natsort import natsorted import numpy as np from asimtools.utils import get_str_btn @@ -65,6 +66,7 @@ def prepare_array_vals( if file_pattern is not None: array_values = natsorted(glob(str(file_pattern))) + assert len(array_values) > 0, f'No file_pattern matching {file_pattern}' elif linspace_args is not None: array_values = np.linspace(*linspace_args) array_values = [float(v) for v in array_values] @@ -72,7 +74,7 @@ def prepare_array_vals( array_values = np.arange(*arange_args) array_values = [float(v) for v in array_values] - assert len(array_values) > 0, 'No array values or files found' + assert len(array_values) > 0, f'No array_values found' if labels == 'str_btn': assert str_btn_args is not None, 'Provide str_btn_args for labels' @@ -94,6 +96,10 @@ def prepare_array_vals( assert len(labels) == len(array_values), \ f'Num. of array_values ({len(array_values)}) must match num.'\ f'of labels ({len(labels)})' + + # File patterns should be resolved fully for placeholders to work + if file_pattern is not None: + array_values = [str(Path(v).resolve()) for v in array_values] if secondary_array_values is not None: nvals = len(secondary_array_values) diff --git a/asimtools/utils.py b/asimtools/utils.py index c78c43e..9928008 100644 --- a/asimtools/utils.py +++ b/asimtools/utils.py @@ -516,7 +516,10 @@ def get_images( images = [] if not skip_failed: - assert len(images) > 0, 'No images found' + addontxt = '' + if image_file: + addontxt = f' in image_file: {image_file}' + assert len(images) > 0, 'No images found' + addontxt return images From e2dbce2f9cdf1db1ea841b671c31127ed623d9d8 Mon Sep 17 00:00:00 2001 From: Mgcini Keith Phuthi Date: Tue, 18 Mar 2025 11:34:11 -0700 Subject: [PATCH 08/78] ase_md update --- asimtools/asimmodules/ase_md/ase_md.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/asimtools/asimmodules/ase_md/ase_md.py b/asimtools/asimmodules/ase_md/ase_md.py index 316dbfa..4fc0eaa 100644 --- a/asimtools/asimmodules/ase_md/ase_md.py +++ b/asimtools/asimmodules/ase_md/ase_md.py @@ -62,7 +62,7 @@ def langevin_nvt( traj_file, atoms=atoms, mode='w', - properties=['energy', 'forces', 'stress'] + properties=['energy', 'forces'] ) dyn.attach(traj.write) dyn.run(nsteps) @@ -77,6 +77,7 @@ def npt( traj_file: str = None, ttime: float = 25*fs, pfactor: Optional[float] = None, # (75*fs)**2 * 14*GPa, #Replace 14 with bulk modulus of material + properties: Optional[Sequence] = ('energy', 'forces', 'stress'), ): """Does NPT dynamics @@ -107,12 +108,13 @@ def npt( pfactor=pfactor, # mask=np.diag([1, 1, 1]), ) + if traj_file is not None: traj = Trajectory( traj_file, atoms=atoms, mode='w', - properties=['energy', 'forces', 'stress'], + properties=properties, ) dyn.attach(traj.write) dyn.run(nsteps) @@ -173,6 +175,7 @@ def ase_md( externalstress: Optional[float] = 0, plot: Optional[bool] = True, time_unit: Optional[str] = 'ase', + plot_args: Optional[dict] = None, ) -> Dict: """Runs ASE MD simulations. This is only recommended for small systems and for testing. For larger systems, use LAMMPS or more purpose-built code @@ -242,11 +245,15 @@ def ase_md( timestep=timestep, pfactor=None, ttime=ttime, + properties=['energy', 'forces'], ) if plot: - plot_thermo(images={'image_file': 'output.traj'}) + plot_thermo( + images={'image_file': 'output.traj'}, + **plot_args + ) results = {} return results From 52572a39c13d40d46336ef958575c09f63474de6 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Tue, 18 Mar 2025 11:35:13 -0700 Subject: [PATCH 09/78] minor bug fixes --- asimtools/asimmodules/benchmarking/distribution.py | 11 +++++++---- asimtools/asimmodules/lammps/lammps.py | 5 +++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/asimtools/asimmodules/benchmarking/distribution.py b/asimtools/asimmodules/benchmarking/distribution.py index 47ab913..23f0607 100644 --- a/asimtools/asimmodules/benchmarking/distribution.py +++ b/asimtools/asimmodules/benchmarking/distribution.py @@ -13,16 +13,19 @@ get_images, ) -cm3 = (meters * 100)**3 +cm = (meters / 100) def distribution( images: Dict, unit: str = 'eV', bins: int = 50, log: bool = True, - properties: Sequence[str] = ('energy', 'forces', 'stress', 'volume', 'pressure', 'enthalpy', 'density', 'mass', 'natoms'), + properties: Sequence[str] = ('energy', 'forces', 'stress', 'pressure', 'enthalpy'), remap_keys: Optional[Dict] = None, ) -> Dict: + noncalc_properties = ['density', 'mass', 'natoms', 'volume'] + properties = list(properties) + properties += noncalc_properties if remap_keys is None: remap_keys = {} unit_factors = {'meV': 1000, 'eV': 1, 'kcal/mol': 23.0621} @@ -88,7 +91,7 @@ def distribution( assert 'mass' in properties and 'volume' in properties, \ 'Mass and volume must be included to calculate density' results['density'] = ( - (results['mass'] * kg * 1000) / (results['volume'] / cm3) + (results['mass'] / results['volume']) / (kg * 0.001) * (cm**3) ) if 'enthalpy' in properties: assert 'energy' in properties and 'pressure' in properties and 'volume' in properties, \ @@ -103,7 +106,7 @@ def distribution( for prop in ['forces', 'stress', 'pressure', 'energy', 'enthalpy']: if prop in properties: results[prop] = results[prop] * unit_factor - print(results['energy']) + for prop in properties: with open(f'summary.txt', 'a+') as f: f.write(f'{prop} distribution\n') diff --git a/asimtools/asimmodules/lammps/lammps.py b/asimtools/asimmodules/lammps/lammps.py index 8d7d15c..6750da7 100755 --- a/asimtools/asimmodules/lammps/lammps.py +++ b/asimtools/asimmodules/lammps/lammps.py @@ -109,8 +109,9 @@ def lammps( seed = str(randint(0, 100000)) line = line.replace('SEED', seed) - for placeholder in placeholders: - line = line.replace(placeholder, placeholders[placeholder]) + if placeholders is not None: + for placeholder in placeholders: + line = line.replace(placeholder, placeholders[placeholder]) lmp_txt += line if image is not None: From cac8f4693d6df3675e0850d69837db2441ff4d88 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Fri, 28 Mar 2025 14:09:09 -0700 Subject: [PATCH 10/78] ase_md enhancements, write_atoms, minor cleanup --- asimtools/asimmodules/ase_md/ase_md.py | 49 +++++++++++++-- .../cubic_energy_expansion.py | 2 +- .../geometry_optimization/atom_relax.py | 2 +- .../geometry_optimization/cell_relax.py | 2 +- .../geometry_optimization/optimize.py | 2 +- .../symmetric_cell_relax.py | 2 +- .../vacancy_formation_energy.py | 2 +- asimtools/job.py | 5 +- asimtools/utils.py | 62 ++++++++++++++++++- tests/unit/test_utils.py | 1 + 10 files changed, 114 insertions(+), 15 deletions(-) diff --git a/asimtools/asimmodules/ase_md/ase_md.py b/asimtools/asimmodules/ase_md/ase_md.py index 4fc0eaa..d6eb5e1 100644 --- a/asimtools/asimmodules/ase_md/ase_md.py +++ b/asimtools/asimmodules/ase_md/ase_md.py @@ -18,6 +18,7 @@ from ase.md.velocitydistribution import MaxwellBoltzmannDistribution from ase.md.langevin import Langevin from ase.md.npt import NPT +from ase.md import MDLogger from asimtools.calculators import load_calc from asimtools.utils import ( get_atoms, @@ -31,6 +32,9 @@ def langevin_nvt( traj_file: str = None, friction: float = 1e-2, timestep: float = 1*fs, + properties: Optional[Sequence] = ('energy', 'forces', 'stress'), + log_interval: int = 1, + traj_interval: Optional[int] = 1, ): """Does Langevin dynamics @@ -64,7 +68,19 @@ def langevin_nvt( mode='w', properties=['energy', 'forces'] ) - dyn.attach(traj.write) + dyn.attach(traj.write, interval=traj_interval) + stress = False + if 'stress' in properties: + stress = True + dyn.attach(MDLogger( + dyn, + atoms, + 'md.log', + header=True, + stress=stress, + peratom=True, + mode="a" + ), interval=log_interval) dyn.run(nsteps) return atoms, traj @@ -78,6 +94,8 @@ def npt( ttime: float = 25*fs, pfactor: Optional[float] = None, # (75*fs)**2 * 14*GPa, #Replace 14 with bulk modulus of material properties: Optional[Sequence] = ('energy', 'forces', 'stress'), + log_interval: int = 1, + traj_interval: Optional[int] = 1, ): """Does NPT dynamics @@ -116,7 +134,20 @@ def npt( mode='w', properties=properties, ) - dyn.attach(traj.write) + dyn.attach(traj.write, interval=traj_interval) + + stress = False + if 'stress' in properties: + stress = True + dyn.attach(MDLogger( + dyn, + atoms, + 'md.log', + header=True, + stress=stress, + peratom=True, + mode="a" + ), interval=log_interval) dyn.run(nsteps) traj = Trajectory(traj_file, 'r') return atoms, traj @@ -176,6 +207,9 @@ def ase_md( plot: Optional[bool] = True, time_unit: Optional[str] = 'ase', plot_args: Optional[dict] = None, + properties: Optional[Sequence] = ('energy', 'forces', 'stress'), + log_interval: Optional[int] = 1, + traj_interval: Optional[int] = 1, ) -> Dict: """Runs ASE MD simulations. This is only recommended for small systems and for testing. For larger systems, use LAMMPS or more purpose-built code @@ -209,7 +243,7 @@ def ase_md( calc = load_calc(**calc_spec) atoms = get_atoms(**image) - atoms.set_calculator(calc) + atoms.calc = calc if time_unit == 'fs': timestep *= fs @@ -224,6 +258,8 @@ def ase_md( traj_file='output.traj', timestep=timestep, friction=friction, + log_interval=log_interval, + traj_interval=traj_interval, ) elif dynamics == 'npt': atoms, _ = npt( @@ -235,6 +271,9 @@ def ase_md( pfactor=pfactor, externalstress=externalstress, ttime=ttime, + properties=properties, + log_interval=log_interval, + traj_interval=traj_interval, ) elif dynamics == 'nvt': atoms, _ = npt( @@ -245,7 +284,9 @@ def ase_md( timestep=timestep, pfactor=None, ttime=ttime, - properties=['energy', 'forces'], + properties=properties, + log_interval=log_interval, + traj_interval=traj_interval, ) diff --git a/asimtools/asimmodules/elastic_constants/cubic_energy_expansion.py b/asimtools/asimmodules/elastic_constants/cubic_energy_expansion.py index d186cf3..819e38c 100755 --- a/asimtools/asimmodules/elastic_constants/cubic_energy_expansion.py +++ b/asimtools/asimmodules/elastic_constants/cubic_energy_expansion.py @@ -101,7 +101,7 @@ def cubic_energy_expansion( """ calc = load_calc(calc_id) atoms = get_atoms(**image) - atoms.set_calculator(calc) + atoms.calc = calc # Start by getting the Bulk modulus and optimized cell from the EOS logging.info('Calculating EOS') diff --git a/asimtools/asimmodules/geometry_optimization/atom_relax.py b/asimtools/asimmodules/geometry_optimization/atom_relax.py index 684ee55..ab1c37d 100755 --- a/asimtools/asimmodules/geometry_optimization/atom_relax.py +++ b/asimtools/asimmodules/geometry_optimization/atom_relax.py @@ -40,7 +40,7 @@ def atom_relax( """ calc = load_calc(calc_id) atoms = get_atoms(**image) - atoms.set_calculator(calc) + atoms.calc = calc logger = get_logger() if prefix is not None: diff --git a/asimtools/asimmodules/geometry_optimization/cell_relax.py b/asimtools/asimmodules/geometry_optimization/cell_relax.py index e69b24b..1a20c64 100755 --- a/asimtools/asimmodules/geometry_optimization/cell_relax.py +++ b/asimtools/asimmodules/geometry_optimization/cell_relax.py @@ -46,7 +46,7 @@ def cell_relax( """ calc = load_calc(calc_id) atoms = get_atoms(**image) - atoms.set_calculator(calc) + atoms.calc = calc if prefix is not None: prefix = prefix + '_' diff --git a/asimtools/asimmodules/geometry_optimization/optimize.py b/asimtools/asimmodules/geometry_optimization/optimize.py index 6c38132..e1cb30a 100755 --- a/asimtools/asimmodules/geometry_optimization/optimize.py +++ b/asimtools/asimmodules/geometry_optimization/optimize.py @@ -46,7 +46,7 @@ def optimize( calc = load_calc(calc_id) atoms = get_atoms(**image) - atoms.set_calculator(calc) + atoms.calc = calc ecf = ExpCellFilter(atoms, **expcellfilter_args) diff --git a/asimtools/asimmodules/geometry_optimization/symmetric_cell_relax.py b/asimtools/asimmodules/geometry_optimization/symmetric_cell_relax.py index f925bdd..62f697d 100755 --- a/asimtools/asimmodules/geometry_optimization/symmetric_cell_relax.py +++ b/asimtools/asimmodules/geometry_optimization/symmetric_cell_relax.py @@ -47,7 +47,7 @@ def symmetric_cell_relax( calc = load_calc(calc_id) atoms = get_atoms(**image) - atoms.set_calculator(calc) + atoms.calc = calc atoms.set_constraint(FixSymmetry(atoms, **fixsymmetry_args)) ecf = ExpCellFilter(atoms, **expcellfilter_args) diff --git a/asimtools/asimmodules/vacancy_formation_energy/vacancy_formation_energy.py b/asimtools/asimmodules/vacancy_formation_energy/vacancy_formation_energy.py index 9cf1aab..c8a8f13 100755 --- a/asimtools/asimmodules/vacancy_formation_energy/vacancy_formation_energy.py +++ b/asimtools/asimmodules/vacancy_formation_energy/vacancy_formation_energy.py @@ -49,7 +49,7 @@ def vacancy_formation_energy( calc = load_calc(calc_id) bulk = get_atoms(**image).repeat(repeat) - bulk.set_calculator(calc) + bulk.calc = calc vacant = bulk.copy() del vacant[vacancy_index] diff --git a/asimtools/job.py b/asimtools/job.py index b58ff99..c456f12 100644 --- a/asimtools/job.py +++ b/asimtools/job.py @@ -21,6 +21,7 @@ from asimtools.utils import ( read_yaml, write_yaml, + write_atoms, join_names, get_atoms, get_images, @@ -396,9 +397,9 @@ def gen_input_files( if image and write_image: atoms = get_atoms(**image) input_image_file = 'image_input.xyz' # Relative to workdir - atoms.write( + write_atoms( self.workdir / input_image_file, - format='extxyz' + atoms, ) sim_input['args']['image'] = { 'image_file': str(input_image_file), diff --git a/asimtools/utils.py b/asimtools/utils.py index 9928008..3353044 100644 --- a/asimtools/utils.py +++ b/asimtools/utils.py @@ -16,7 +16,7 @@ import numpy as np import matplotlib.pyplot as plt import pandas as pd -from ase.io import read +from ase.io import read, write from ase.parallel import paropen import ase.db import ase.build @@ -149,6 +149,52 @@ def join_names(substrs: Sequence[str]) -> str: name = '__'.join(final_substrs) + '__' return name +def write_atoms( + image_file: str, + atoms: Atoms, + fmt: str = 'extxyz', + write_info: bool = True, + columns: Optional[Sequence] = None, + **kwargs +): + """ + + """ + if kwargs.get('format', False): + fmt = kwargs.pop('format') + + if fmt in ['extxyz']: + if kwargs.get('write_info', False): + write_info = kwargs.pop('write_info') + if kwargs.get('columns', False): + write_info = kwargs.pop('columns') + else: + reserved_ks = ['symbols', 'positions', 'numbers', 'species', 'pos'] + columns = ['symbols', 'positions'] + kwargs.get( + 'columns', + [k for k in atoms.arrays.keys() if k not in reserved_ks] + ) + + if len(atoms.constraints) > 0: + columns.append('move_mask') + + write( + image_file, + atoms, + format=fmt, + write_info=write_info, + columns=columns, + **kwargs + ) + else: + write( + image_file, + atoms, + format=fmt, + **kwargs + ) + + def get_atoms( image_file: Optional[str] = None, interface: str = 'ase', @@ -742,11 +788,21 @@ def get_str_btn( s = s[start_index:stop_index] while occurence - j >= 0: if s1 is not None: - i1 = s.index(s1) + len(s1) + try: + i1 = s.index(s1) + len(s1) + except: + raise ValueError( + f'substring {s1} not found in {s}' + ) else: i1 = 0 if s2 is not None: - i2 = s[i1:].index(s2) + i1 + try: + i2 = s[i1:].index(s2) + i1 + except: + raise ValueError( + f'substring {s2} not found in {s}' + ) else: i2 = len(s) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index b08e5dc..f624db0 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -20,6 +20,7 @@ get_nth_label, get_str_btn, expand_wildcards, + write_atoms, ) import ase.build From 73a84705e85d99e2e1adc7070e35ac2b994cacef Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Sat, 29 Mar 2025 15:23:19 -0700 Subject: [PATCH 11/78] write_images --- asimtools/utils.py | 54 +++++++++++++++++++++++++++++---- tests/unit/test_utils.py | 64 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 6 deletions(-) diff --git a/asimtools/utils.py b/asimtools/utils.py index 3353044..77ba6d7 100644 --- a/asimtools/utils.py +++ b/asimtools/utils.py @@ -20,7 +20,7 @@ from ase.parallel import paropen import ase.db import ase.build -from pymatgen.core import Structure +from pymatgen.core import Structure, Lattice from pymatgen.io.ase import AseAtomsAdaptor Atoms = TypeVar('Atoms') @@ -151,18 +151,44 @@ def join_names(substrs: Sequence[str]) -> str: def write_atoms( image_file: str, - atoms: Atoms, + atoms: Union[Atoms,list[Atoms]], fmt: str = 'extxyz', write_info: bool = True, columns: Optional[Sequence] = None, **kwargs ): """ - + Writes image/images to a file. The default format is extxyz + and the default columns are symbols and positions. + All the images should have the same metadata + + :param image_file: Path to file to write to + :type image_file: str + :param atoms: Atoms object or list of atoms objects to write + :type atoms: Atoms or list[Atoms] + :param fmt: Format to write, defaults to 'extxyz' + :type fmt: str + :param write_info: Whether to write info, defaults to True + :type write_info: bool + :param columns: Columns to write, defaults to None + :type columns: Sequence, optional + :param kwargs: Extra keyword arguments passed to :func:`ase.io.write` + :type kwargs: Any + :raises ValueError: If the format is not supported + :return: None + :rtype: None """ if kwargs.get('format', False): fmt = kwargs.pop('format') + if isinstance(atoms, list): + if len(atoms) == 0: + raise ValueError('No images to write') + images = atoms + atoms = images[0] + else: + images = [atoms] + if fmt in ['extxyz']: if kwargs.get('write_info', False): write_info = kwargs.pop('write_info') @@ -180,7 +206,7 @@ def write_atoms( write( image_file, - atoms, + images, format=fmt, write_info=write_info, columns=columns, @@ -189,7 +215,7 @@ def write_atoms( else: write( image_file, - atoms, + images, format=fmt, **kwargs ) @@ -318,6 +344,18 @@ def get_atoms( Lattice abc : 2.7718585822512662 2.7718585822512662 2.7718585822512662 ... + + You can also specify a builder from pymatgen.core.structure or + pymatgen.core.molecule, for example pymatgen.core.surface.Structure. + The lattice paramters are passed as an ArrayLike with shape [3,3] to + :class:`pymatgen.core.lattice.Lattice` or dictionary to the + :func:`pymatgen.core.lattice.Lattice.from_parameters` function. + >>> image = {'builder': 'pymatgen.core.surface.Structure', 'lattice': {'a': 2.7, 'b': 2.7, 'c': 2.7, 'alpha': 90, 'beta': 90, 'gamma': 90}} + >>> get_atoms(**image) + Structure Summary + Lattice + abc : 2.7 2.7 2.7 + ... """ if interface == 'ase': assert image_file is not None or \ @@ -347,7 +385,11 @@ def get_atoms( with MPRester(user_api_key) as mpr: struct = mpr.get_structure_by_material_id(mp_id, **kwargs) elif builder is not None: - builder_func = getattr(Structure, builder) + import pymatgen.core + builder_func = getattr(pymatgen.core, builder) + lattice = kwargs.get('lattice', False) + if isinstance(lattice, dict): + kwargs['lattice'] = Lattice.from_parameters(**lattice) try: struct = builder_func(**kwargs) except ValueError: diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index f624db0..36d56fa 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -5,6 +5,7 @@ import os import pytest from ase.io import read +from pymatgen.core import Structure, Molecule, IStructure, IMolecule from asimtools.utils import ( join_names, get_atoms, @@ -62,6 +63,69 @@ def test_join_names(test_input, expected): ({'image_file': STRUCT_DIR / 'Ar', 'format': 'cfg'}, ase.build.bulk('Ar')), ({'atoms': ase.build.bulk('Ar')}, ase.build.bulk('Ar')), + ({ + 'interface': 'pymatgen', + 'builder': 'Structure', + 'lattice': [[3, 0, 0], [0, 3, 0], [0, 0, 3]], + 'coords': [[0, 0, 0]], + 'species': ['Ar'], + 'return_type': 'pymatgen', + }, Structure( + lattice=[[3, 0, 0], [0, 3, 0], [0, 0, 3]], + coords=[[0, 0, 0]], + species=['Ar'], + coords_are_cartesian=False, + )), + ({ + 'interface': 'pymatgen', + 'builder': 'Structure', + 'lattice': {'a': 3, 'b': 4, 'c': 5, 'alpha': 90, 'beta': 90, 'gamma': 90}, + 'coords': [[0, 0, 0]], + 'species': ['Ar'], + 'return_type': 'pymatgen', + }, Structure( + lattice=[[3, 0, 0], [0, 4, 0], [0, 0, 5]], + coords=[[0, 0, 0]], + species=['Ar'], + coords_are_cartesian=False, + )), + ({ + 'interface': 'pymatgen', + 'builder': 'IStructure', + 'lattice': {'a': 3, 'b': 4, 'c': 5, 'alpha': 90, 'beta': 90, 'gamma': 90}, + 'coords': [[0, 0, 0]], + 'species': ['Ar'], + 'return_type': 'pymatgen', + }, IStructure( + lattice=[[3, 0, 0], [0, 4, 0], [0, 0, 5]], + coords=[[0, 0, 0]], + species=['Ar'], + coords_are_cartesian=False, + )), + ({ + 'interface': 'pymatgen', + 'builder': 'Molecule', + 'coords': [[0, 0, 0], [1.5, 1.5, 1.5], [1.5, -1.5, -1.5]], + 'species': ['O', 'H', 'H'], + 'spin_multiplicity': 1, + 'return_type': 'pymatgen', + }, Molecule( + species=['O', 'H', 'H'], + coords=[[0, 0, 0], [1.5, 1.5, 1.5], [1.5, -1.5, -1.5]], + spin_multiplicity=1, + )), + ({ + 'interface': 'pymatgen', + 'builder': 'IMolecule', + 'coords': [[0, 0, 0], [1.5, 1.5, 1.5], [1.5, -1.5, -1.5]], + 'species': ['O', 'H', 'H'], + 'spin_multiplicity': 1, + 'return_type': 'pymatgen', + }, IMolecule( + species=['O', 'H', 'H'], + coords=[[0, 0, 0], [1.5, 1.5, 1.5], [1.5, -1.5, -1.5]], + spin_multiplicity=1, + )), ]) def test_get_atoms(test_input, expected): ''' Test getting atoms from different inputs ''' From 7c36145de3ab5cd323ef42aeb5d8621f22269e74 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Sat, 29 Mar 2025 18:38:45 -0700 Subject: [PATCH 12/78] return molecules --- asimtools/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/asimtools/utils.py b/asimtools/utils.py index 77ba6d7..df2e20b 100644 --- a/asimtools/utils.py +++ b/asimtools/utils.py @@ -415,7 +415,10 @@ def get_atoms( if return_type == 'ase' and interface == 'ase': return atoms elif return_type == 'pymatgen' and interface == 'ase': - return AseAtomsAdaptor.get_structure(atoms) + if builder == 'molecule': + return AseAtomsAdaptor.get_molecule(atoms) + else: + return AseAtomsAdaptor.get_structure(atoms) elif return_type == 'pymatgen' and interface == 'pymatgen': return struct elif return_type == 'ase' and interface == 'pymatgen': From 528402e1b382e323253ead11e64a843ad1f0c565 Mon Sep 17 00:00:00 2001 From: Mgcini Keith Phuthi Date: Tue, 1 Apr 2025 16:22:09 -0700 Subject: [PATCH 13/78] cleanup --- asimtools/asimmodules/vasp/vasp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/asimtools/asimmodules/vasp/vasp.py b/asimtools/asimmodules/vasp/vasp.py index 00a98a7..b66f55b 100755 --- a/asimtools/asimmodules/vasp/vasp.py +++ b/asimtools/asimmodules/vasp/vasp.py @@ -14,6 +14,7 @@ import subprocess import logging from ase.io import read +from ase import Atoms from pymatgen.io.ase import AseAtomsAdaptor from pymatgen.io.vasp import Poscar, Incar, Potcar, Kpoints, VaspInput import pymatgen.io.vasp.sets @@ -55,8 +56,7 @@ def vasp( if vaspinput_kwargs is None: vaspinput_kwargs = {} - atoms = get_atoms(**image) - struct = AseAtomsAdaptor.get_structure(atoms) + struct = get_atoms(**image, return_type='pymatgen') if mpset is not None: # if not ((incar is None) or (potcar is None) or (kpoints is None)): # raise ValueError( From 1f607412373cf313ef1df0438c6967b28b394dbb Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Fri, 18 Apr 2025 12:16:49 -0700 Subject: [PATCH 14/78] use write_atoms, lammps bug fix --- asimtools/asimmodules/geometry_optimization/atom_relax.py | 7 +++---- asimtools/asimmodules/geometry_optimization/cell_relax.py | 7 +++---- asimtools/asimmodules/geometry_optimization/optimize.py | 7 +++---- .../geometry_optimization/symmetric_cell_relax.py | 7 +++---- asimtools/asimmodules/lammps/lammps.py | 4 ++-- 5 files changed, 14 insertions(+), 18 deletions(-) diff --git a/asimtools/asimmodules/geometry_optimization/atom_relax.py b/asimtools/asimmodules/geometry_optimization/atom_relax.py index ab1c37d..a777fcf 100755 --- a/asimtools/asimmodules/geometry_optimization/atom_relax.py +++ b/asimtools/asimmodules/geometry_optimization/atom_relax.py @@ -9,7 +9,7 @@ import ase.optimize from ase.io.trajectory import Trajectory from asimtools.calculators import load_calc -from asimtools.utils import get_atoms, get_logger +from asimtools.utils import get_atoms, get_logger, write_atoms def atom_relax( calc_id: str, @@ -64,11 +64,10 @@ def atom_relax( raise image_file = prefix + 'image_output.xyz' - atoms.write( + write_atoms( image_file, + atoms, format='extxyz', - write_info=False, - write_results=True, ) energy = float(atoms.get_potential_energy()) diff --git a/asimtools/asimmodules/geometry_optimization/cell_relax.py b/asimtools/asimmodules/geometry_optimization/cell_relax.py index 1a20c64..d51b1ba 100755 --- a/asimtools/asimmodules/geometry_optimization/cell_relax.py +++ b/asimtools/asimmodules/geometry_optimization/cell_relax.py @@ -14,7 +14,7 @@ from ase.filters import StrainFilter from ase.io.trajectory import Trajectory from asimtools.calculators import load_calc -from asimtools.utils import get_atoms, join_names +from asimtools.utils import get_atoms, join_names, write_atoms def cell_relax( calc_id: str, @@ -70,11 +70,10 @@ def cell_relax( raise image_file = join_names([prefix, 'image_output.xyz'])[:-2] - atoms.write( + write_atoms( image_file, + atoms, format='extxyz', - write_info=False, - write_results=True, ) energy = float(atoms.get_potential_energy()) diff --git a/asimtools/asimmodules/geometry_optimization/optimize.py b/asimtools/asimmodules/geometry_optimization/optimize.py index e1cb30a..e9a5d78 100755 --- a/asimtools/asimmodules/geometry_optimization/optimize.py +++ b/asimtools/asimmodules/geometry_optimization/optimize.py @@ -9,7 +9,7 @@ from ase.filters import ExpCellFilter from ase.io.trajectory import Trajectory from asimtools.calculators import load_calc -from asimtools.utils import get_atoms +from asimtools.utils import get_atoms, write_atoms def optimize( calc_id: str, @@ -66,11 +66,10 @@ def optimize( raise image_file = 'image_output.xyz' - atoms.write( + write_atoms( image_file, + atoms, format='extxyz', - write_info=False, - write_results=True, ) energy = float(atoms.get_potential_energy()) diff --git a/asimtools/asimmodules/geometry_optimization/symmetric_cell_relax.py b/asimtools/asimmodules/geometry_optimization/symmetric_cell_relax.py index 62f697d..d2a419c 100755 --- a/asimtools/asimmodules/geometry_optimization/symmetric_cell_relax.py +++ b/asimtools/asimmodules/geometry_optimization/symmetric_cell_relax.py @@ -10,7 +10,7 @@ from ase.spacegroup.symmetrize import FixSymmetry from ase.io.trajectory import Trajectory from asimtools.calculators import load_calc -from asimtools.utils import get_atoms +from asimtools.utils import get_atoms, write_atoms def symmetric_cell_relax( calc_id: str, @@ -68,11 +68,10 @@ def symmetric_cell_relax( raise image_file = 'image_output.xyz' - atoms.write( + write_atoms( image_file, + atoms, format='extxyz', - write_info=False, - write_results=True, ) energy = float(atoms.get_potential_energy()) diff --git a/asimtools/asimmodules/lammps/lammps.py b/asimtools/asimmodules/lammps/lammps.py index 6750da7..7129936 100755 --- a/asimtools/asimmodules/lammps/lammps.py +++ b/asimtools/asimmodules/lammps/lammps.py @@ -107,11 +107,11 @@ def lammps( if 'SEED' in line and 'SEED' not in placeholders: if seed is None: seed = str(randint(0, 100000)) - line = line.replace('SEED', seed) + line = line.replace('SEED', str(seed)) if placeholders is not None: for placeholder in placeholders: - line = line.replace(placeholder, placeholders[placeholder]) + line = line.replace(placeholder, str(placeholders[placeholder])) lmp_txt += line if image is not None: From cc040347891bf628dffd9bd587c2efd7bbb306d3 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Fri, 25 Apr 2025 15:23:31 -0700 Subject: [PATCH 15/78] write_atoms test --- asimtools/utils.py | 8 +++++ tests/data/structures/adslab.xyz | 57 ++++++++++++++++++++++++++++++++ tests/unit/test_utils.py | 14 +++++++- 3 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 tests/data/structures/adslab.xyz diff --git a/asimtools/utils.py b/asimtools/utils.py index df2e20b..0b567f6 100644 --- a/asimtools/utils.py +++ b/asimtools/utils.py @@ -204,6 +204,14 @@ def write_atoms( if len(atoms.constraints) > 0: columns.append('move_mask') + if 'initial_magmoms' in columns: + for atoms in images: + atoms.arrays['initial_magmoms'] = np.where( + np.isnan(atoms.arrays['initial_magmoms']), + 0, + atoms.arrays['initial_magmoms'], + ) + write( image_file, images, diff --git a/tests/data/structures/adslab.xyz b/tests/data/structures/adslab.xyz new file mode 100644 index 0000000..fe4ea29 --- /dev/null +++ b/tests/data/structures/adslab.xyz @@ -0,0 +1,57 @@ +55 +Lattice="10.989862374024527 0.0 6.729349889709534e-16 -3.6632874580081776 10.361341611972845 6.729349889709536e-16 0.0 0.0 29.910616844190965" Properties=species:S:1:pos:R:3:initial_magmoms:R:1:surface_properties:S:1:bulk_wyckoff:S:1:bulk_equivalent:S:1:move_mask:L:1 pbc="T T T" +Na 0.00000000 0.00000000 4.48659253 nan subsurface a 0 F +Na -1.22109582 3.45378054 4.48659253 nan subsurface a 0 F +Na -2.44219164 6.90756107 4.48659253 nan subsurface a 0 F +Na 3.66328746 0.00000000 4.48659253 nan subsurface a 0 F +Na 2.44219164 3.45378054 4.48659253 nan subsurface a 0 F +Na 1.22109582 6.90756107 4.48659253 nan subsurface a 0 F +Na 7.32657492 0.00000000 4.48659253 nan subsurface a 0 F +Na 6.10547910 3.45378054 4.48659253 nan subsurface a 0 F +Na 4.88438328 6.90756107 4.48659253 nan subsurface a 0 F +Na 1.22109582 1.72689027 1.49553084 nan subsurface a 0 F +Na -0.00000000 5.18067081 1.49553084 nan subsurface a 0 F +Na -1.22109582 8.63445134 1.49553084 nan subsurface a 0 F +Na 4.88438328 1.72689027 1.49553084 nan subsurface a 0 F +Na 3.66328746 5.18067081 1.49553084 nan subsurface a 0 F +Na 2.44219164 8.63445134 1.49553084 nan subsurface a 0 F +Na 8.54767074 1.72689027 1.49553084 nan subsurface a 0 F +Na 7.32657492 5.18067081 1.49553084 nan subsurface a 0 F +Na 6.10547910 8.63445134 1.49553084 nan subsurface a 0 F +Na 0.05040356 0.07128126 10.61153565 nan subsurface a 0 T +Na -1.22088381 3.45363060 10.47942977 nan subsurface a 0 T +Na -2.43700358 6.91641549 10.46332553 nan subsurface a 0 T +Na 3.66307537 0.00014975 10.47942998 nan subsurface a 0 T +Na 2.39178795 3.38249920 10.61153531 nan subsurface a 0 T +Na 1.21590808 6.89870710 10.46332525 nan subsurface a 0 T +Na 7.33319379 0.00784251 10.46332545 nan subsurface a 0 T +Na 6.09886082 3.44593812 10.46332545 nan subsurface a 0 T +Na 4.88438293 6.90756134 10.52503220 nan subsurface a 0 T +Na 1.22109579 1.72689044 7.49352623 nan subsurface a 0 T +Na 0.00009148 5.17841763 7.48202082 nan subsurface a 0 T +Na -1.22118690 8.63670456 7.48202064 nan subsurface a 0 T +Na 4.88222840 1.72622555 7.48202032 nan subsurface a 0 T +Na 3.66029354 5.17643697 7.50198302 nan subsurface a 0 T +Na 2.44173987 8.63477030 7.48098737 nan subsurface a 0 T +Na 8.54982542 1.72755533 7.48202044 nan subsurface a 0 T +Na 7.32702677 5.18035170 7.48098729 nan subsurface a 0 T +Na 6.10847301 8.63868512 7.50198293 nan subsurface a 0 T +Na -0.05716541 -0.08084452 16.64749813 nan surface a 0 T +Na -0.66173064 3.05824944 16.35385328 nan surface a 0 T +Na -2.36879258 6.61089299 16.32311991 nan surface a 0 T +Na 3.10392211 0.39553082 16.35385370 nan surface a 0 T +Na 2.49935741 3.53462570 16.64749940 nan surface a 0 T +Na 1.14769723 7.20422919 16.32311967 nan surface a 0 T +Na 7.02240734 -0.02968818 16.32312005 nan surface a 0 T +Na 6.40964675 3.48346858 16.32312000 nan surface a 0 T +Na 4.88438260 6.90756112 16.41278280 nan surface a 0 T +Na 1.22109540 1.72689040 13.93937426 nan subsurface a 0 T +Na -0.06036830 5.24521596 13.41401878 nan subsurface a 0 T +Na -1.16072730 8.56990626 13.41401807 nan subsurface a 0 T +Na 4.96535977 1.69148936 13.41401871 nan subsurface a 0 T +Na 3.64865650 5.15997986 13.52384317 nan subsurface a 0 T +Na 2.48037426 8.60745143 13.39723386 nan subsurface a 0 T +Na 8.46669414 1.76229130 13.41401834 nan subsurface a 0 T +Na 7.28839235 5.20767012 13.39723382 nan subsurface a 0 T +Na 6.12011011 8.65514262 13.52384285 nan subsurface a 0 T +O 1.22109586 1.72688973 16.17505098 2.00000000 adsorbate None None T \ No newline at end of file diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 36d56fa..cf9d2bc 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -4,6 +4,7 @@ from pathlib import Path import os import pytest +import numpy as np from ase.io import read from pymatgen.core import Structure, Molecule, IStructure, IMolecule from asimtools.utils import ( @@ -60,7 +61,7 @@ def test_join_names(test_input, expected): ase.build.fcc111('Al', size=(2,2,3))), ({'image_file': STRUCT_DIR / 'Ar.xyz'}, ase.build.bulk('Ar')), - ({'image_file': STRUCT_DIR / 'Ar', 'format': 'cfg'}, + ({'image_file': STRUCT_DIR / 'Ar.xyz'}, ase.build.bulk('Ar')), ({'atoms': ase.build.bulk('Ar')}, ase.build.bulk('Ar')), ({ @@ -126,6 +127,7 @@ def test_join_names(test_input, expected): coords=[[0, 0, 0], [1.5, 1.5, 1.5], [1.5, -1.5, -1.5]], spin_multiplicity=1, )), + ]) def test_get_atoms(test_input, expected): ''' Test getting atoms from different inputs ''' @@ -173,6 +175,16 @@ def test_get_images(test_input, expected): for image in input_images: assert image in expected +def test_write_atoms(tmp_path): + ''' Test write_atoms. + Also test that when magmoms are provided by ASE which makes them nans + and kills jobs using VASP etc., we change them to zero ''' + slab_ = read(STRUCT_DIR / 'adslab.xyz') + write_atoms(tmp_path / 'slab.xyz', slab_) + slab = read(tmp_path / 'slab.xyz') + assert not np.any(np.isnan(slab.arrays['initial_magmoms'])) + assert 'surface_properties' in slab.arrays + @pytest.mark.parametrize("test_input, expected",[ (['l1', 'l2', 'l3'], {'l1': {'l2': {'l3': 'new_value'}}}), (['l1', 'l2'], {'l1': {'l2': 'new_value'}}), From 14b8d1484d2007b47627e34fcc8dfc3c857605a2 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Tue, 3 Jun 2025 13:04:26 -0700 Subject: [PATCH 16/78] FixSymmetry bugs related ASE update --- .../asimmodules/geometry_optimization/symmetric_cell_relax.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/asimtools/asimmodules/geometry_optimization/symmetric_cell_relax.py b/asimtools/asimmodules/geometry_optimization/symmetric_cell_relax.py index d2a419c..838108e 100755 --- a/asimtools/asimmodules/geometry_optimization/symmetric_cell_relax.py +++ b/asimtools/asimmodules/geometry_optimization/symmetric_cell_relax.py @@ -7,7 +7,7 @@ from typing import Dict, Optional import ase.optimize from ase.filters import ExpCellFilter -from ase.spacegroup.symmetrize import FixSymmetry +from ase.constraints import FixSymmetry from ase.io.trajectory import Trajectory from asimtools.calculators import load_calc from asimtools.utils import get_atoms, write_atoms @@ -68,6 +68,7 @@ def symmetric_cell_relax( raise image_file = 'image_output.xyz' + atoms.constraints = [] # write doesn't work with FixSymmetry constraint write_atoms( image_file, atoms, From c32eb35afdc901121104e8d3eeda6044d612220a Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Tue, 3 Jun 2025 13:04:56 -0700 Subject: [PATCH 17/78] write_atoms tests --- tests/unit/test_utils.py | 41 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index cf9d2bc..b2b4e0e 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -6,6 +6,7 @@ import pytest import numpy as np from ase.io import read +from ase.calculators.emt import EMT from pymatgen.core import Structure, Molecule, IStructure, IMolecule from asimtools.utils import ( join_names, @@ -168,14 +169,48 @@ def test_get_atoms(test_input, expected): ]) def test_get_images(test_input, expected): ''' Test getting iterable of atoms from different inputs ''' - print('++input:', get_images(**test_input)) - print('++expected:', expected) input_images = get_images(**test_input) assert len(input_images) == len(expected) for image in input_images: assert image in expected -def test_write_atoms(tmp_path): +@pytest.mark.parametrize("test_input, expected",[ + ( + {'image_file': str(STRUCT_DIR / 'adslab.xyz')}, + ('surface_properties', 'bulk_wyckoff'), + ), +]) +def test_write_atoms(test_input, expected, tmp_path): + init_atoms = get_atoms(**test_input) + testfile = tmp_path / 'test.xyz' + write_atoms(testfile, init_atoms) + + with open(testfile, 'r') as f: + lines = f.readlines() + + for prop in expected: + assert prop in lines[1], f'"{prop}" not in file header' + +@pytest.mark.parametrize("test_input, expected",[ + ( + {'name': 'Cu', 'interface': 'ase', 'builder': 'bulk'}, + ('forces', 'energy', 'stress'), + ), +]) +def test_write_atoms_calc(test_input, expected, tmp_path): + init_atoms = get_atoms(**test_input) + init_atoms.calc = EMT() + init_atoms.get_potential_energy() + testfile = tmp_path / 'test.xyz' + write_atoms(testfile, init_atoms) + + with open(testfile, 'r') as f: + lines = f.readlines() + + for prop in expected: + assert prop in lines[1], f'"{prop}" not in file header' + +def test_write_atoms_magmoms(tmp_path): ''' Test write_atoms. Also test that when magmoms are provided by ASE which makes them nans and kills jobs using VASP etc., we change them to zero ''' From 8e902c27400066215a9ca514b873968c71d008cf Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Tue, 3 Jun 2025 13:05:26 -0700 Subject: [PATCH 18/78] Cleanup for DFT apps --- .../surface_energies/surface_energies.py | 87 ++++++++++--------- 1 file changed, 46 insertions(+), 41 deletions(-) diff --git a/asimtools/asimmodules/surface_energies/surface_energies.py b/asimtools/asimmodules/surface_energies/surface_energies.py index 2d389eb..1ebe77b 100755 --- a/asimtools/asimmodules/surface_energies/surface_energies.py +++ b/asimtools/asimmodules/surface_energies/surface_energies.py @@ -14,6 +14,7 @@ from pymatgen.io.ase import AseAtomsAdaptor as AAA from asimtools.calculators import load_calc from asimtools.asimmodules.geometry_optimization.atom_relax import atom_relax +from asimtools.asimmodules.singlepoint import singlepoint from asimtools.utils import ( get_atoms, ) @@ -36,8 +37,8 @@ def get_surface_energy(slab, calc, bulk_e_per_atom): return converged, surf_en, slab_en, area def surface_energies( - calc_id: str, image: Dict, + calc_id: str = None, millers: Union[str,Sequence] = 'all', atom_relax_args: Optional[Dict] = None, generate_all_slabs_args: Optional[Dict] = None, @@ -45,7 +46,8 @@ def surface_energies( """Calculates surface energies of slabs defined by args specified for pymatgen.core.surface.generate_all_slabs() - :param calc_id: calc_id specification + :param calc_id: Optional calc_id specification, See + :func:`asimtools.calculators.load_calc` :type calc_id: str :param image: Image specification, see :func:`asimtools.utils.get_atoms` :type image: Dict @@ -65,10 +67,7 @@ def surface_energies( calc = load_calc(calc_id) bulk = get_atoms(**image) - bulk.set_calculator(calc) - - if atom_relax_args is None: - atom_relax_args = {} + bulk.calc = calc default_pymatgen_kwargs = { 'max_index': 3, @@ -86,55 +85,61 @@ def surface_energies( bulk_struct, **pymargs ) - + print('Generated %s slabs', len(slabs)) logging.info('Generated %s distinct slabs', len(slabs)) n_bulk = len(bulk) bulk_e_per_atom = bulk.get_potential_energy() / n_bulk bulk.write('bulk.xyz') slab_dict = {} - for s, slab in enumerate(slabs): + for _, slab in enumerate(slabs): big_slab = slab.copy() mindex = slab.miller_index miller = f'{mindex[0]}{mindex[1]}{mindex[2]}' if millers == 'all' or miller in millers: logging.info('Calculating for %s', miller) atoms = AAA.get_atoms(big_slab) - atoms.write(f'{miller}.xyz') - - relax_results = atom_relax( - calc_id=calc_id, - image={'atoms': atoms}, - optimizer=atom_relax_args.get('optimizer', 'BFGS'), - properties=('energy','forces'), - fmax=atom_relax_args.get('fmax', 0.01), - prefix=f'{miller}_relaxed' - ) - atoms = get_atoms( - image_file = relax_results.get('files', {}).get('image') - ) - - assert np.allclose(atoms.pbc, (True, True, True)), \ - f'Check pbcs for {miller}: {atoms.pbc}' - assert atoms.cell[2][2] > pymargs['min_vacuum_size'] + \ - pymargs['min_slab_size'], \ - f'Check layer number and vacuum for {miller}' - assert miller not in slab_dict, \ - f'Multiple terminations for {miller}' - + atoms.write(f'miller-{miller}.xyz') slab_dict[miller] = {} - converged, surf_en, slab_en, area = get_surface_energy( - atoms, load_calc(calc_id), bulk_e_per_atom - ) - - if converged: - slab_dict[miller]['surf_energy'] = float(surf_en) - slab_dict[miller]['natoms'] = len(atoms) - slab_dict[miller]['slab_energy'] = float(slab_en) - slab_dict['bulk_energy_per_atom'] = float(bulk_e_per_atom) - slab_dict[miller]['area'] = float(area) - atoms.write(f'{miller}.xyz') + if atom_relax_args is not None: + relax_results = atom_relax( + calc_id=calc_id, + image={'atoms': atoms}, + optimizer=atom_relax_args.get('optimizer', 'BFGS'), + properties=('energy','forces'), + fmax=atom_relax_args.get('fmax', 0.01), + prefix=f'{miller}_relaxed' + ) + + atoms = get_atoms( + image_file = relax_results.get('files', {}).get('image') + ) + + assert np.allclose(atoms.pbc, (True, True, True)), \ + f'Check pbcs for {miller}: {atoms.pbc}' + assert atoms.cell[2][2] > pymargs['min_vacuum_size'] + \ + pymargs['min_slab_size'], \ + f'Check layer number and vacuum for {miller}' + assert miller not in slab_dict, \ + f'Multiple terminations for {miller}' + + converged, surf_en, slab_en, area = get_surface_energy( + atoms, load_calc(calc_id), bulk_e_per_atom + ) + + if converged: + slab_dict[miller]['surf_energy'] = float(surf_en) + slab_dict[miller]['natoms'] = len(atoms) + slab_dict[miller]['slab_energy'] = float(slab_en) + slab_dict['bulk_energy_per_atom'] = float(bulk_e_per_atom) + slab_dict[miller]['area'] = float(area) + atoms.write(f'{miller}.xyz') + else: + logging.warning('Slab %s not converged', miller) + + assert len(slab_dict) > 0, \ + 'No slabs generated. Check your args for miller indices' results = { 'surface_energies': slab_dict, } From ec788f0eaa6aac71a768bfc4ff4e4209147aa55b Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Tue, 3 Jun 2025 13:05:43 -0700 Subject: [PATCH 19/78] Cleanup matgl --- asimtools/calculators.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/asimtools/calculators.py b/asimtools/calculators.py index 40d7c55..42f251a 100644 --- a/asimtools/calculators.py +++ b/asimtools/calculators.py @@ -282,6 +282,30 @@ def load_m3gnet(calc_params): return calc +def load_matgl(calc_params): + """Load any MatGL calculator + + :param calc_params: parameters to be passed to matgl.ext.ase.PESCalculator. Must include a key "model" that points to the model used to instantiate the potential + :type calc_params: Dict + :return: MatGL calculator + :rtype: :class:`matgl.ext.ase.PESCalculator` + """ + from matgl.ext.ase import PESCalculator + import matgl + calc_params = deepcopy(calc_params) + model = calc_params['args'].pop("model") + try: + pot = matgl.load_model(model) + calc = PESCalculator( + pot, + **calc_params['args'], + ) + except Exception: + logging.error("Failed to load M3GNet with parameters:\n %s", calc_params) + raise + + return calc + def load_omat24(calc_params): """Load any OMAT24 calculator @@ -345,6 +369,7 @@ def load_ase_dftd3(calc_params): 'MACECalculator': load_mace, 'EspressoProfile': load_espresso_profile, 'M3GNet': load_m3gnet, + 'MatGL': load_matgl, 'OMAT24': load_omat24, 'ASEDFTD3': load_ase_dftd3, } From 44bba9f107df3abd5f5e66ce65dcfcf23242839c Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Tue, 3 Jun 2025 13:09:15 -0700 Subject: [PATCH 20/78] write_atoms modifications --- CHANGELOG.md | 3 ++ asimtools/asimmodules/singlepoint.py | 9 +++--- asimtools/asimmodules/workflows/utils.py | 12 ++++++-- asimtools/utils.py | 38 ++++++++++++++---------- 4 files changed, 39 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e33558..64af58f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [develop] - 2025-2-14 ### Added +- Can now specify an integer N to labels keyword add will use the N-th label for array workflows - Can now use placehodlers in sim_array where the array_values replace part of the string at the given key_sequence - Can now specify whether to write velocities in lammps @@ -19,10 +20,12 @@ ASE source for this as ASIMTools can't go into the calculator code. - VASP interface changed to align more with pymatgen - asimtools.utils.write_yaml now stops sorting keys to help with readability of written yamls +- write_atoms now more universally used and recommended in asimmodules ### Fixed - Minor bugs in geometry optimizations - Updated EspressoProfile calculator to match ASE 3.25.0b1 +- FixSymmetry now imported from ase.constraints ## [0.1.0] - 2024-12-27 diff --git a/asimtools/asimmodules/singlepoint.py b/asimtools/asimmodules/singlepoint.py index 39903b0..e430189 100755 --- a/asimtools/asimmodules/singlepoint.py +++ b/asimtools/asimmodules/singlepoint.py @@ -10,6 +10,7 @@ from asimtools.calculators import load_calc from asimtools.utils import ( get_atoms, + write_atoms, ) def singlepoint( @@ -38,7 +39,7 @@ def singlepoint( prefix = prefix + '_' else: prefix = '' - import time; time.sleep(20) + columns_append = [] if 'energy' in properties: try: energy = atoms.get_potential_energy() @@ -54,6 +55,7 @@ def singlepoint( except Exception: logging.error('Failed to calculate forces') raise + columns_append += ['forces'] if 'stress' in properties: try: @@ -64,11 +66,10 @@ def singlepoint( raise image_file = prefix + 'image_output.xyz' - atoms.write( + write_atoms( image_file, + atoms, format='extxyz', - write_info=False, - write_results=True, ) results = { diff --git a/asimtools/asimmodules/workflows/utils.py b/asimtools/asimmodules/workflows/utils.py index f97b9fe..c775ef7 100644 --- a/asimtools/asimmodules/workflows/utils.py +++ b/asimtools/asimmodules/workflows/utils.py @@ -3,7 +3,7 @@ from pathlib import Path from natsort import natsorted import numpy as np -from asimtools.utils import get_str_btn +from asimtools.utils import get_str_btn, get_nth_label def prepare_array_vals( key_sequence: Optional[Sequence[str]] = None, @@ -35,7 +35,10 @@ def prepare_array_vals( :param arange_args: arguments to pass to :func:`numpy.arange` to be iterated over in each simulation, defaults to None :type arange_args: Optional[Sequence], optional - :param labels: Custom labels to use for each simulation, defaults to None + :param labels: Custom labels to use for each simulation. If "str_btn" + provide arguments to :func:`asimtools.utils.get_str_btn` as additional + str_btn_args keyword. If labels is an integer N, the Nth label in the + file_pattern or array_values is used, defaults to None :type labels: Sequence, optional :param label_prefix: Prefix to add before labels which can make extracting data from file paths easier, defaults to None @@ -84,11 +87,14 @@ def prepare_array_vals( labels = [f'{key_sequence[-1]}-{val}' for val in array_values] else: labels = [f'value-{val}' for val in array_values] + elif isinstance(labels, int): + labels = [get_nth_label(s, labels) for s in array_values] elif labels is None: labels = [str(i) for i in range(len(array_values))] - # In case the label is a file path with / characters + # In case the label is a file path with / or space characters labels = [label.replace('/', '+') for label in labels] + labels = [label.replace(' ', '+') for label in labels] if label_prefix is not None: labels = [label_prefix + '-' + label for label in labels] diff --git a/asimtools/utils.py b/asimtools/utils.py index 0b567f6..ef0c239 100644 --- a/asimtools/utils.py +++ b/asimtools/utils.py @@ -17,6 +17,7 @@ import matplotlib.pyplot as plt import pandas as pd from ase.io import read, write +from ase.io.extxyz import save_calc_results from ase.parallel import paropen import ase.db import ase.build @@ -170,7 +171,8 @@ def write_atoms( :type fmt: str :param write_info: Whether to write info, defaults to True :type write_info: bool - :param columns: Columns to write, defaults to None + :param columns: Columns to write, mostly for debugging, if used, specify + all columns including positions, symbols etc. defaults to None, :type columns: Sequence, optional :param kwargs: Extra keyword arguments passed to :func:`ase.io.write` :type kwargs: Any @@ -192,23 +194,26 @@ def write_atoms( if fmt in ['extxyz']: if kwargs.get('write_info', False): write_info = kwargs.pop('write_info') - if kwargs.get('columns', False): - write_info = kwargs.pop('columns') - else: - reserved_ks = ['symbols', 'positions', 'numbers', 'species', 'pos'] - columns = ['symbols', 'positions'] + kwargs.get( - 'columns', - [k for k in atoms.arrays.keys() if k not in reserved_ks] - ) - - if len(atoms.constraints) > 0: - columns.append('move_mask') - - if 'initial_magmoms' in columns: - for atoms in images: + # if kwargs.get('columns', False): + # columns = kwargs.pop('columns') + # else: + # reserved_ks = ['symbols', 'positions', 'numbers', 'species', 'pos'] + # columns = ['symbols', 'positions'] + kwargs.get( + # 'columns', + # [k for k in atoms.arrays.keys() if k not in reserved_ks] + # ) + # if columns_append is not None: + # columns += columns_append + + # if len(atoms.constraints) > 0: + # columns.append('move_mask') + + for atoms in images: + # Current workaround magmoms being NaNs is to replace NaNs with 0.0 + if 'initial_magmoms' in atoms.arrays: atoms.arrays['initial_magmoms'] = np.where( np.isnan(atoms.arrays['initial_magmoms']), - 0, + 0.0, atoms.arrays['initial_magmoms'], ) @@ -217,6 +222,7 @@ def write_atoms( images, format=fmt, write_info=write_info, + write_results=True, columns=columns, **kwargs ) From d15d3f4386f57243454869cf97895f019a236d0c Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Wed, 18 Jun 2025 16:30:40 -0700 Subject: [PATCH 21/78] get_atoms constraints, asim-check job_ids --- CHANGELOG.md | 2 ++ asimtools/scripts/asim_check.py | 6 ++-- asimtools/utils.py | 54 ++++++++++++++++++++++++--------- tests/unit/test_utils.py | 15 +++++++++ 4 files changed, 61 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64af58f..95dc069 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ the string at the given key_sequence https://www.chemie.uni-bonn.de/grimme/de/software/dft-d3/get_dft-d3. 2. Some calculators which return a 3x3 matrix for stress will break. One can modify ASE source for this as ASIMTools can't go into the calculator code. +- asim_check now also reports the job_ids +- get_atoms now allows addition of FixAtoms constraint ### Changed - VASP interface changed to align more with pymatgen diff --git a/asimtools/scripts/asim_check.py b/asimtools/scripts/asim_check.py index f9e4e3d..7732d0c 100755 --- a/asimtools/scripts/asim_check.py +++ b/asimtools/scripts/asim_check.py @@ -133,8 +133,9 @@ def print_job_tree( workdir = job_tree['workdir_name'] status, color = get_status_and_color(job_tree['job']) asimmodule = job_tree['job'].sim_input['asimmodule'] + job_ids = job_tree['job'].get_output().get('job_ids', 'none') print(color + f'{indent_str}{workdir}, asimmodule: {asimmodule},' + \ - f'status: {status}' + reset) + f'status: {status}, job_ids: {job_ids}' + reset) if level > 0: indent_str = '| ' + ' ' * level for subjob_id in subjobs: @@ -149,9 +150,10 @@ def print_job_tree( subjob_dir = job_tree['workdir_name'] subjob = job_tree['job'] asimmodule = subjob.sim_input['asimmodule'] + job_ids = job_tree['job'].get_output().get('job_ids', 'none') status, color = get_status_and_color(subjob) print(color + f'{indent_str}{subjob_dir}, asimmodule: {asimmodule}, '+\ - f'status: {status}' + reset) + f'status: {status}, job_ids: {job_ids}' + reset) if __name__ == "__main__": main(sys.argv[1:]) diff --git a/asimtools/utils.py b/asimtools/utils.py index ef0c239..217d627 100644 --- a/asimtools/utils.py +++ b/asimtools/utils.py @@ -21,6 +21,7 @@ from ase.parallel import paropen import ase.db import ase.build +from ase.constraints import FixAtoms from pymatgen.core import Structure, Lattice from pymatgen.io.ase import AseAtomsAdaptor @@ -194,19 +195,22 @@ def write_atoms( if fmt in ['extxyz']: if kwargs.get('write_info', False): write_info = kwargs.pop('write_info') - # if kwargs.get('columns', False): - # columns = kwargs.pop('columns') - # else: - # reserved_ks = ['symbols', 'positions', 'numbers', 'species', 'pos'] - # columns = ['symbols', 'positions'] + kwargs.get( - # 'columns', - # [k for k in atoms.arrays.keys() if k not in reserved_ks] - # ) - # if columns_append is not None: - # columns += columns_append - - # if len(atoms.constraints) > 0: - # columns.append('move_mask') + + reserved_ks = ['symbols', 'positions', 'numbers', 'species', 'pos'] + columns = ['symbols', 'positions'] + [ + k for k in atoms.arrays.keys() if k not in reserved_ks + ] + + if len(atoms.constraints) > 0: + columns.append('move_mask') + + if images[0].calc is not None: + if 'forces' in images[0].calc.results: + columns.append('forces') + + user_columns = kwargs.get('columns', None) + if user_columns is not None: + columns = list(set(columns + user_columns)) for atoms in images: # Current workaround magmoms being NaNs is to replace NaNs with 0.0 @@ -245,6 +249,7 @@ def get_atoms( mp_id: Optional[str] = None, user_api_key: Optional[str] = None, return_type: str = 'ase', + constraints: Sequence[dict] = None, **kwargs ) -> Union[Atoms, Structure]: """Return an ASE Atoms or pymatgen Structure object based on specified @@ -272,6 +277,9 @@ def get_atoms( :param user_api_key: Material Project API key, must be provided to get structures from Materials Project, defaults to None :type user_api_key: str, optional + :param constraints: List of constraints to apply to the atoms object, + currently only supports FixAtoms constraints, defaults to None + :type constraints: Sequence[dict], optional :param return_type: When set to `ase` returns a :class:`ase.Atoms` object, when set to `pymatgen` returns a :class:`pymatgen.core.structure.Structure` object, defaults to 'ase' @@ -298,8 +306,13 @@ def get_atoms( Atoms(symbols='Cu', pbc=True, cell=[[0.0, 1.805, 1.805], [1.805, 0.0, 1.805], [1.805, 1.805, 0.0]]) >>> get_atoms(builder='bulk', name='Ar', crystalstructure='fcc', a=3.4, cubic=True) Atoms(symbols='Ar4', pbc=True, cell=[3.4, 3.4, 3.4]) - >>> get_atoms(builder='fcc100', symbol='Fe', vacuum=8, size=[4,4, 5]) + >>> get_atoms(builder='fcc100', symbol='Fe', vacuum=8, size=[4,4,5]) Atoms(symbols='Cu80', pbc=[True, True, False], cell=[10.210621920333747, 10.210621920333747, 23.22], tags=...) + + You can also specify constraints to fix atoms in place, for example + >>> get_atoms(builder='fcc100', symbol='Fe', vacuum=8, size=[4,4,5], constraints=[{'constraint': 'FixAtoms', 'indices': [0,1]}]) + Atoms(symbols='Cu80', pbc=[True, True, False], cell=[10.210621920333747, 10.210621920333747, 23.22], tags=...) + Some examples for reading an image from a file using :func:`ase.io.read` are given below. All ``**kwargs`` are passed to :func:`ase.io.read` @@ -426,6 +439,19 @@ def get_atoms( elif rattle_stdev is not None and interface == 'pymatgen': struct.perturb(distance=rattle_stdev, min_distance=0) + if constraints is not None and interface == 'ase': + consts = [] + for constraint_args in constraints: + name = constraint_args.pop('constraint') + assert name == 'FixAtoms', \ + 'Only FixAtoms constraints are supported for ASE interface' + constraint_cls = getattr(ase.constraints, name, None) + const = constraint_cls(**constraint_args) + consts.append(const) + + for const in consts: + atoms.set_constraint(const) + if return_type == 'ase' and interface == 'ase': return atoms elif return_type == 'pymatgen' and interface == 'ase': diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index b2b4e0e..0eb6685 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -134,6 +134,14 @@ def test_get_atoms(test_input, expected): ''' Test getting atoms from different inputs ''' assert get_atoms(**test_input) == expected +def test_get_atoms_constraints(tmp_path): + atoms = ase.build.bulk('Cu').repeat((2,2,2)) + constrained_atoms = get_atoms( + atoms=atoms, + constraints=[{'constraint': 'FixAtoms', 'indices': [0, 1]}] + ) + assert len(constrained_atoms.constraints) == 1 + @pytest.mark.parametrize("test_input, expected",[ ({'image_file': str(STRUCT_DIR / 'images.xyz')}, [ase.build.bulk('Ar'), ase.build.bulk('Cu'), ase.build.bulk('Fe')]), @@ -191,6 +199,13 @@ def test_write_atoms(test_input, expected, tmp_path): for prop in expected: assert prop in lines[1], f'"{prop}" not in file header' +def test_write_atoms_constraints(tmp_path): + atoms = ase.build.bulk('Cu').repeat((2,2,2)) + atoms.set_constraint(ase.constraints.FixAtoms(indices=[0, 1])) + write_atoms(tmp_path / 'test_constraints.xyz', atoms) + read_atoms = read(tmp_path / 'test_constraints.xyz') + assert len(read_atoms.constraints) > 0 + @pytest.mark.parametrize("test_input, expected",[ ( {'name': 'Cu', 'interface': 'ase', 'builder': 'bulk'}, From 2ff282760d669c02e420c4ddee48c31fc9ecc054 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Mon, 21 Jul 2025 15:31:57 -0700 Subject: [PATCH 22/78] vasp ibrion,fairchem calc --- CHANGELOG.md | 1 + asimtools/asimmodules/vasp/vasp.py | 82 +++++++++----- asimtools/asimmodules/vasp/vasp.py.bak | 142 +++++++++++++++++++++++++ asimtools/calculators.py | 51 ++++++++- 4 files changed, 245 insertions(+), 31 deletions(-) create mode 100755 asimtools/asimmodules/vasp/vasp.py.bak diff --git a/CHANGELOG.md b/CHANGELOG.md index 95dc069..40de5a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ ASE source for this as ASIMTools can't go into the calculator code. - asimtools.utils.write_yaml now stops sorting keys to help with readability of written yamls - write_atoms now more universally used and recommended in asimmodules +- VASP calculation now fails if the max number of iterations is reached for ibrion=1,2,3 ### Fixed - Minor bugs in geometry optimizations diff --git a/asimtools/asimmodules/vasp/vasp.py b/asimtools/asimmodules/vasp/vasp.py index b66f55b..c3ab02c 100755 --- a/asimtools/asimmodules/vasp/vasp.py +++ b/asimtools/asimmodules/vasp/vasp.py @@ -13,6 +13,7 @@ from numpy.random import randint import subprocess import logging +import shutil from ase.io import read from ase import Atoms from pymatgen.io.ase import AseAtomsAdaptor @@ -20,8 +21,40 @@ import pymatgen.io.vasp.sets from asimtools.utils import ( get_atoms, + get_str_btn, ) +def execute_vasp_run_command( + command: str, +) -> None: + command = command.split(' ') + completed_process = subprocess.run( + command, check=False, capture_output=True, text=True, + ) + + with open('vasp_stdout.txt', 'a+', encoding='utf-8') as f: + f.write(completed_process.stdout) + + if completed_process.returncode != 0: + err_txt = f'VASP failed with error code: {completed_process.returncode}' + err_txt += '\nSee vasp_stderr.txt for details.' + logging.error(err_txt) + with open('vasp_stderr.txt', 'a+', encoding='utf-8') as f: + f.write(completed_process.stderr) + completed_process.check_returncode() + if "ZBRENT: fatal error in bracketing" in completed_process.stderr: + filenum = 0 + while os.path.exists(f'OUTCAR.{filenum}'): + filenum += 1 + shutil.move('OUTCAR', f'OUTCAR.{filenum}') + shutil.move('POSCAR', f'POSCAR.{filenum}') + shutil.move('CONTCAR', f'POSCAR') + execute_vasp_run_command(command) + else: + logging.info( + f'VASP run completed with code {completed_process.returncode}.' + ) + def vasp( image: Optional[Dict], user_incar_settings: Optional[Dict] = None, @@ -58,10 +91,6 @@ def vasp( struct = get_atoms(**image, return_type='pymatgen') if mpset is not None: - # if not ((incar is None) or (potcar is None) or (kpoints is None)): - # raise ValueError( - # 'Provide either mpset or all of incar and kpoints' - # ) try: set_ = getattr(pymatgen.io.vasp.sets, mpset) except: @@ -107,32 +136,31 @@ def vasp( **vaspinput_kwargs ) - vasp_input.write_input("./") - # if incar_kwargs is not None: - # with open('INCAR', 'a+') as fp: - # for k, v in incar_kwargs.items(): - # fp.write(f'\n{k} = {v}') if run_vasp: - command = command.split(' ') - completed_process = subprocess.run( - command, check=False, capture_output=True, text=True, - ) - - with open('vasp_stdout.txt', 'a+', encoding='utf-8') as f: - f.write(completed_process.stdout) - - if completed_process.returncode != 0: - err_txt = f'Failed to run VASP\n' - err_txt += 'See vasp_stderr.txt for details.' - logging.error(err_txt) - with open('vasp_stderr.txt', 'a+', encoding='utf-8') as f: - f.write(completed_process.stderr) - completed_process.check_returncode() - return {} - - if write_image_output: + optimization_failed = False + execute_vasp_run_command(command) + incar = vasp_input.incar + ibrion = incar.get('IBRION', -1) + nsw = incar.get('NSW', 0) + # Don't write result if running a relaxation that didn't finish + if ibrion in (1,2,3): + if nsw > 0: + with open('OUTCAR', 'r') as f: + lines = f.readlines() + lines = lines[::-1] + for line in lines: + if 'Ionic step' in line: + last_step = int(get_str_btn(line, 'Ionic step', '--')) + if last_step == nsw: + optimization_failed = True + raise RuntimeError( + 'VASP relaxation did not complete. ' + 'Check OUTCAR for details.' + ) + + if write_image_output and not optimization_failed: atoms_output = read('OUTCAR') atoms_output.write( 'image_output.xyz', diff --git a/asimtools/asimmodules/vasp/vasp.py.bak b/asimtools/asimmodules/vasp/vasp.py.bak new file mode 100755 index 0000000..b66f55b --- /dev/null +++ b/asimtools/asimmodules/vasp/vasp.py.bak @@ -0,0 +1,142 @@ +#!/usr/bin/env python +''' +Runs VASP based on input files and optionally MP settings. +Heavily uses pymatgen for IO and MP settings. +VASP must be installed + +Author: mkphuthi@github.com +''' +from typing import Dict, Optional, Sequence +import os +import sys +from pathlib import Path +from numpy.random import randint +import subprocess +import logging +from ase.io import read +from ase import Atoms +from pymatgen.io.ase import AseAtomsAdaptor +from pymatgen.io.vasp import Poscar, Incar, Potcar, Kpoints, VaspInput +import pymatgen.io.vasp.sets +from asimtools.utils import ( + get_atoms, +) + +def vasp( + image: Optional[Dict], + user_incar_settings: Optional[Dict] = None, + user_kpoints_settings: Optional[Dict] = None, + user_potcar_functional: str = 'PBE_64', + potcar: Optional[Dict] = None, + vaspinput_kwargs: Optional[Dict] = None, + command: str = 'srun vasp_std', + mpset: Optional[str] = None, + prev_calc: Optional[os.PathLike] = None, + write_image_output: bool = True, + run_vasp: bool = True, +) -> Dict: + """Run VASP with given input files and specified image + + :param image: Initial image for VASP calculation. Image specification, + see :func:`asimtools.utils.get_atoms` + :type image: Dict + :param vaspinput_args: Dictionary of pymatgen's VaspInput arguments. + See :class:`pymatgen.io.vasp.inputs.VaspInput` + :type vaspinput_args: Dict + :param command: Command with which to run VASP, defaults to 'vasp_std' + :type command: str, optional + :param mpset: Materials Project VASP set to use see + :mod:`pymatgen.io.vasp.sets`, defaults to None + :type mpset: str, optional + :param write_image_output: Whether to write output image in standard + asimtools format to file, defaults to False + :type write_image_output: bool, optional + """ + + if vaspinput_kwargs is None: + vaspinput_kwargs = {} + + struct = get_atoms(**image, return_type='pymatgen') + if mpset is not None: + # if not ((incar is None) or (potcar is None) or (kpoints is None)): + # raise ValueError( + # 'Provide either mpset or all of incar and kpoints' + # ) + try: + set_ = getattr(pymatgen.io.vasp.sets, mpset) + except: + raise ImportError( + f'Unknown mpset: {mpset}. See available sets in pymatgen.') + + if prev_calc is not None: + vasp_input = set_.from_prev_calc( + prev_calc, + user_incar_settings=user_incar_settings, + user_kpoints_settings=user_kpoints_settings, + user_potcar_functional=user_potcar_functional, + **vaspinput_kwargs + ) + else: + vasp_input = set_( + struct, + user_incar_settings=user_incar_settings, + user_kpoints_settings=user_kpoints_settings, + user_potcar_functional=user_potcar_functional, + **vaspinput_kwargs + ) + + else: + + incar = Incar(user_incar_settings) + incar.check_params() + if potcar is not None: + potcar = Potcar(potcar) + if user_kpoints_settings is not None: + kpoints = Kpoints(user_kpoints_settings) + else: + kpoints=None + + if vaspinput_args is None: + vaspinput_args = {} + + vasp_input = VaspInput( + incar=incar, + kpoints=kpoints, + poscar=Poscar(struct), + potcar=potcar, + **vaspinput_kwargs + ) + + + vasp_input.write_input("./") + # if incar_kwargs is not None: + # with open('INCAR', 'a+') as fp: + # for k, v in incar_kwargs.items(): + # fp.write(f'\n{k} = {v}') + + if run_vasp: + command = command.split(' ') + completed_process = subprocess.run( + command, check=False, capture_output=True, text=True, + ) + + with open('vasp_stdout.txt', 'a+', encoding='utf-8') as f: + f.write(completed_process.stdout) + + if completed_process.returncode != 0: + err_txt = f'Failed to run VASP\n' + err_txt += 'See vasp_stderr.txt for details.' + logging.error(err_txt) + with open('vasp_stderr.txt', 'a+', encoding='utf-8') as f: + f.write(completed_process.stderr) + completed_process.check_returncode() + return {} + + if write_image_output: + atoms_output = read('OUTCAR') + atoms_output.write( + 'image_output.xyz', + format='extxyz', + ) + + return {} diff --git a/asimtools/calculators.py b/asimtools/calculators.py index 42f251a..19951f5 100644 --- a/asimtools/calculators.py +++ b/asimtools/calculators.py @@ -306,14 +306,14 @@ def load_matgl(calc_params): return calc -def load_omat24(calc_params): - """Load any OMAT24 calculator +def load_fairchemV1(calc_params): + """Load any fairchemV1 calculator :param calc_params: parameters to be passed to fairchem.core.OCPCalculator. Must include a key "model" that points to the model files used to instantiate the potential :type calc_params: Dict - :return: OMAT24 calculator + :return: fairchem calculator :rtype: :class:`fairchem.core.OCPCalculator` """ from fairchem.core import OCPCalculator @@ -328,6 +328,46 @@ def load_omat24(calc_params): return calc +def load_fairchemV2(calc_params): + """Load any fairchemV1 calculator + + :param calc_params: parameters to be passed to fairchem.core.FAIRChemCalculator. + Must include a key "model" that points to the model files used to + instantiate the potential + :type calc_params: Dict + :return: fairchem calculator + :rtype: :class:`fairchem.core.FAIRChemCalculator` + + Examples + -------- + >>> from asimtools.calculators import load_calc + >>> calc_params = { + ... 'name': 'fairchem', + ... 'args': { + ... 'model_name': 'uma-s-1', + ... 'device': 'cuda', + ... 'task_name': 'oc20' # Also 'omol','omat','oc20','odac' or 'omc' + ... } + ... } + >>> calc = load_calc(calc_params=calc_params) + + """ + from fairchem.core import pretrained_mlip, FAIRChemCalculator + og_calc_params = deepcopy(calc_params) + task_name = calc_params['args'].pop('task_name', None) + predictor = pretrained_mlip.get_predict_unit(**calc_params['args']) + + try: + calc = FAIRChemCalculator(predictor, task_name=task_name) + except Exception: + logging.error( + "Failed to load FAIRChemCalculator with parameters:\n %s", \ + og_calc_params + ) + raise + + return calc + def load_ase_dftd3(calc_params): """Load any calculator with DFTD3 correction as implemented in ASE @@ -370,6 +410,9 @@ def load_ase_dftd3(calc_params): 'EspressoProfile': load_espresso_profile, 'M3GNet': load_m3gnet, 'MatGL': load_matgl, - 'OMAT24': load_omat24, + 'OMAT24': load_fairchemV1, + 'fairchemV1': load_fairchemV1, + 'fairchemV2': load_fairchemV2, + 'fairchem': load_fairchemV2, 'ASEDFTD3': load_ase_dftd3, } From 97470a1d4fb7d80e2ef2a291a6b24864e2010ca9 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Mon, 21 Jul 2025 15:34:30 -0700 Subject: [PATCH 23/78] hostname --- CHANGELOG.md | 1 + asimtools/scripts/asim_run.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95dc069..4051daa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ calculators which return a 3x3 matrix for stress will break. One can modify ASE source for this as ASIMTools can't go into the calculator code. - asim_check now also reports the job_ids - get_atoms now allows addition of FixAtoms constraint +- output.yaml now includes hostname ### Changed - VASP interface changed to align more with pymatgen diff --git a/asimtools/scripts/asim_run.py b/asimtools/scripts/asim_run.py index 8a8432a..ea2521a 100755 --- a/asimtools/scripts/asim_run.py +++ b/asimtools/scripts/asim_run.py @@ -8,6 +8,7 @@ import importlib import sys import os +import socket from pathlib import Path import argparse import subprocess @@ -165,6 +166,8 @@ def main(args=None) -> None: job_ids = os.getenv('SLURM_JOB_ID') results['job_ids'] = job_ids + results['hostname'] = socket.gethostname() + job.update_output(results) job.complete() From a16ae2e83731afcb38d041bdb49aeb2e14af97de Mon Sep 17 00:00:00 2001 From: Mgcini Keith Phuthi Date: Mon, 11 Aug 2025 14:26:22 -0700 Subject: [PATCH 24/78] template_sim_input for image_array --- asimtools/asimmodules/workflows/image_array.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/asimtools/asimmodules/workflows/image_array.py b/asimtools/asimmodules/workflows/image_array.py index 7c3705c..529c84d 100755 --- a/asimtools/asimmodules/workflows/image_array.py +++ b/asimtools/asimmodules/workflows/image_array.py @@ -14,7 +14,8 @@ def image_array( images: Dict, - subsim_input: Dict, + subsim_input: Optional[Dict] = None, + template_sim_input: Optional[Dict] = None, calc_input: Optional[Dict] = None, env_input: Optional[Dict] = None, array_max: Optional[int] = None, @@ -33,8 +34,14 @@ def image_array( :param images: Images specification, see :func:`asimtools.utils.get_images` :type images: Dict - :param subsim_input: sim_input of asimmodule to be run - :type subsim_input: Dict + :param subsim_input: sim_input of asimmodule to be run, included for backward + compatibility, please use template_sim_input instead, + defaults to None + :type subsim_input: Optional[Dict], optional + :param template_sim_input: sim_input of asimmodule to be run, defaults to None + :type template_sim_input: Optional[Dict], optional + :param str_btn_args: args to pass to :func:`asimtools.utils.get_str_btn` + :type str_btn_args: Optional[Sequence], optional :param calc_input: calc_input to override global file, defaults to None :type calc_input: Optional[Dict], optional :param env_input: env_input to override global file, defaults to None From c4df25348a5cd2a0c7e5f53495f9a70f53cae5ed Mon Sep 17 00:00:00 2001 From: Mgcini Keith Phuthi Date: Wed, 5 Nov 2025 16:16:37 -0800 Subject: [PATCH 25/78] lammps restart, image_array args --- asimtools/asimmodules/lammps/lammps.py | 24 ++++++++- .../asimmodules/workflows/image_array.py | 2 + asimtools/utils.py | 53 ++++++++++++++++++- tests/unit/test_utils.py | 16 +++++- 4 files changed, 92 insertions(+), 3 deletions(-) diff --git a/asimtools/asimmodules/lammps/lammps.py b/asimtools/asimmodules/lammps/lammps.py index 7129936..f752d8e 100755 --- a/asimtools/asimmodules/lammps/lammps.py +++ b/asimtools/asimmodules/lammps/lammps.py @@ -4,7 +4,7 @@ Author: mkphuthi@github.com ''' -from typing import Dict, Optional +from typing import Dict, Optional, Sequence import sys from pathlib import Path from numpy.random import randint @@ -24,6 +24,8 @@ def lammps( masses: bool = True, velocities: bool = False, seed: Optional[int] = None, + restart_template: Optional[str] = None, + specorder: Sequence[str] = None, ) -> Dict: """Runs a lammps script based on a specified template, variables can be specified as arguments to be defined in the final LAMMPS input file if @@ -55,6 +57,12 @@ def lammps( seed to be placed, if seed=None, a random one is generated, defaults to None :type seed: int, optional + :param restart_template: Optional lammps input template to be used to + generate a restart.lammps file, defaults to None + :type restart_template: str, optional + :param specorder: Optional list of atomic species in the order they + should appear in the LAMMPS data input file, defaults to None + :type specorder: Sequence[str], optional :return: LAMMPS out file names :rtype: Dict """ @@ -73,6 +81,7 @@ def lammps( atom_style=atom_style, masses=masses, velocities=velocities, + specorder=specorder, ) except ValueError as te: err_txt = 'Need ASE version >=3.23 to support writing ' @@ -92,6 +101,7 @@ def lammps( variables['IMAGE_FILE'] = 'image_input.lmpdat' lmp_txt = '' + for variable, value in variables.items(): lmp_txt += f'variable {variable} equal {value}\n' @@ -103,6 +113,18 @@ def lammps( if placeholders is None: placeholders = {} + if restart_template is not None: + restart_txt = lmp_txt + with open(restart_template, 'r', encoding='utf-8') as f: + restart_lines = f.readlines() + for rline in restart_lines: + if placeholders is not None: + for placeholder in placeholders: + rline = rline.replace(placeholder, str(placeholders[placeholder])) + restart_txt += rline + with open('restart.lammps', 'w', encoding='utf-8') as f: + f.write(restart_txt) + for line in lines: if 'SEED' in line and 'SEED' not in placeholders: if seed is None: diff --git a/asimtools/asimmodules/workflows/image_array.py b/asimtools/asimmodules/workflows/image_array.py index 529c84d..9489d4d 100755 --- a/asimtools/asimmodules/workflows/image_array.py +++ b/asimtools/asimmodules/workflows/image_array.py @@ -90,6 +90,8 @@ def image_array( secondary_array_values=secondary_array_values, ) + if template_sim_input is not None: + subsim_input = template_sim_input if key_sequence is None: key_sequence = ['args', 'image'] # For backwards compatibility where we don't have to specify image diff --git a/asimtools/utils.py b/asimtools/utils.py index 217d627..02b60b4 100644 --- a/asimtools/utils.py +++ b/asimtools/utils.py @@ -245,6 +245,7 @@ def get_atoms( builder: Optional[str] = 'bulk', atoms: Optional[Atoms] = None, repeat: Optional[Tuple[int, int, int]] = None, + repeat_to_N_args: Optional[Dict] = None, rattle_stdev: Optional[float] = None, mp_id: Optional[str] = None, user_api_key: Optional[str] = None, @@ -329,7 +330,7 @@ def get_atoms( >>> get_atoms(image_file='molecules.xyz', index=0) # Pick out one structure using indexing Atoms(symbols='OH2', pbc=False) - You can also make supercells and rattle the atoms + You can also make supercells and rattle the atoms or repeat to a target >>> li_bulk = get_atoms(name='Li') >>> li_bulk.write('POSCAR', format='vasp') @@ -337,6 +338,8 @@ def get_atoms( Atoms(symbols='Li27', pbc=True, cell=[[-5.235, 5.235, 5.235], [5.235, -5.235, 5.235], [5.235, 5.235, -5.235]]) >>> get_atoms(builder='bulk', name='Li', repeat=[2,2,2], rattle_stdev=0.01) Atoms(symbols='Li8', pbc=True, cell=[[-3.49, 3.49, 3.49], [3.49, -3.49, 3.49], [3.49, 3.49, -3.49]]) + >>> get_atoms(builder='bulk', name='Li', repeat_to_N_args={'N': 16, 'max_dim': 10.0}) + Atoms(symbols='Li16', pbc=True, cell=[[-6.98, 6.98, 6.98], [6.98, -6.98, 6.98], [6.98, 6.98, -6.98]]) Mostly for internal use and use in asimmodules, one can specify atoms directly @@ -439,6 +442,13 @@ def get_atoms( elif rattle_stdev is not None and interface == 'pymatgen': struct.perturb(distance=rattle_stdev, min_distance=0) + if repeat_to_N_args is not None and interface == 'ase': + atoms = repeat_to_N(atoms, **repeat_to_N_args) + elif repeat_to_N_args is not None and interface == 'pymatgen': + raise NotImplementedError( + 'repeat_to_N_args is only implemented for ASE interface' + ) + if constraints is not None and interface == 'ase': consts = [] for constraint_args in constraints: @@ -491,6 +501,47 @@ def parse_slice(value: str, bash: bool = False) -> slice: parts.append('1') return f'$(seq {parts[0]} {parts[2]} {parts[1]})' +def repeat_to_N( + atoms: Atoms, + N: int, + max_dim: float = 50.0, +) -> Atoms: + """Scale a structure to have approximately N atoms in the unit cell. + The function repeats the shortest axis of the unit cell until the + number of atoms >= N or the longest axis of the unit cell is > max_dim. + + :param atoms: Input atoms object + :type atoms: Atoms + :param N: Target number of atoms + :type N: int + :param max_dim: Maximum length of the longest axis of the unit cell, + defaults to 50.0 + :type max_dim: float, optional + :raises ValueError: If it fails to scale the structure to N atoms + :return: Scaled atoms object + :rtype: Atoms + """ + cell = atoms.get_cell() + lengths = [np.linalg.norm(vec) for vec in cell] + num_atoms = len(atoms) + new_atoms = atoms.copy() + + while num_atoms < N and np.max(lengths) < max_dim: + shortest_axis = np.argmin(lengths) + repeat_vec = [1, 1, 1] + repeat_vec[shortest_axis] += 1 + new_atoms = new_atoms.repeat(repeat_vec) + cell = new_atoms.get_cell() + lengths = [np.linalg.norm(vec) for vec in cell] + num_atoms = len(new_atoms) + + if num_atoms < N: + raise ValueError( + f'Failed to scale structure to {N} atoms without exceeding \ + max_dim of {max_dim} Angstroms' + ) + return new_atoms + def get_images( image_file: str = None, pattern: str = None, diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 0eb6685..be43e69 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -24,6 +24,7 @@ get_str_btn, expand_wildcards, write_atoms, + repeat_to_N, ) import ase.build @@ -132,6 +133,7 @@ def test_join_names(test_input, expected): ]) def test_get_atoms(test_input, expected): ''' Test getting atoms from different inputs ''' + print('e', expected) assert get_atoms(**test_input) == expected def test_get_atoms_constraints(tmp_path): @@ -373,4 +375,16 @@ def test_expand_wildcards(test_input, expected, tmp_path): f.write('') print(f'Found paths in {os.getcwd()}: {[f for f in Path(tmp_path).glob("*")]}') - assert expand_wildcards(test_input, root_path=tmp_path) == expected \ No newline at end of file + assert expand_wildcards(test_input, root_path=tmp_path) == expected + +def test_repeat_to_N(): + ''' Test repeating unit cell to at least N atoms ''' + atoms = ase.build.bulk('Cu', crystalstructure='fcc', cubic=True, a=2.0) + repeated_atoms = repeat_to_N(atoms, 16) + assert len(repeated_atoms) == 16 + assert np.abs(repeated_atoms.get_cell()[0][0] - 2*2.0) < 1e-6 + assert np.abs(repeated_atoms.get_cell()[1][1] - 2*2.0) < 1e-6 + assert np.abs(repeated_atoms.get_cell()[2][2] - 1*2.0) < 1e-6 + assert len(repeat_to_N(atoms, 15)) == 16 + with pytest.raises(ValueError): + repeat_to_N(atoms, 16, max_dim=4) \ No newline at end of file From c1988864d97611f6a860128f166a562130de2a56 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Wed, 5 Nov 2025 16:18:01 -0800 Subject: [PATCH 26/78] aqcat, vasp optional run --- asimtools/asimmodules/ase_md/ase_md.py | 2 ++ asimtools/asimmodules/data/collect_images.py | 3 ++- asimtools/asimmodules/vasp/vasp.py | 22 +++++++++++++++-- asimtools/calculators.py | 25 +++++++++++++++++++- 4 files changed, 48 insertions(+), 4 deletions(-) diff --git a/asimtools/asimmodules/ase_md/ase_md.py b/asimtools/asimmodules/ase_md/ase_md.py index d6eb5e1..cbfbebf 100644 --- a/asimtools/asimmodules/ase_md/ase_md.py +++ b/asimtools/asimmodules/ase_md/ase_md.py @@ -290,6 +290,8 @@ def ase_md( ) + if plot_args is None: + plot_args = {} if plot: plot_thermo( images={'image_file': 'output.traj'}, diff --git a/asimtools/asimmodules/data/collect_images.py b/asimtools/asimmodules/data/collect_images.py index fbbd07e..8a82717 100644 --- a/asimtools/asimmodules/data/collect_images.py +++ b/asimtools/asimmodules/data/collect_images.py @@ -61,6 +61,7 @@ def collect_images( :rtype: Dict """ + write_kwargs = {} if fnames == (1): fnames = [f'{fnames}-{i:03d}' for i in range(len(splits))] @@ -125,6 +126,7 @@ def collect_images( selected_atoms = [selected_atoms[i] for i in selected_inds] if shuffle: + assert not sort_by_energy_per_atom, 'Either sort or shuffle, not both' np.random.shuffle(selected_atoms) elif sort_by_energy_per_atom: assert not shuffle, 'Either sort or shuffle, not both' @@ -135,7 +137,6 @@ def collect_images( sort_result = sorted( zip(e_per_atoms, selected_atoms), key=lambda x: x[0] ) - selected_atoms = [x[1] for x in sort_result] start_index = 0 diff --git a/asimtools/asimmodules/vasp/vasp.py b/asimtools/asimmodules/vasp/vasp.py index c3ab02c..de08b50 100755 --- a/asimtools/asimmodules/vasp/vasp.py +++ b/asimtools/asimmodules/vasp/vasp.py @@ -73,9 +73,24 @@ def vasp( :param image: Initial image for VASP calculation. Image specification, see :func:`asimtools.utils.get_atoms` :type image: Dict - :param vaspinput_args: Dictionary of pymatgen's VaspInput arguments. + :param user_incar_settings: Dictionary of INCAR settings to override + defaults or MP settings, defaults to None + :type user_incar_settings: Dict, optional + :param user_kpoints_settings: Dictionary of KPOINTS settings to override + defaults or MP settings, defaults to None + :type user_kpoints_settings: Dict, optional + :param user_potcar_functional: Potcar functional to use in case of MP + settings, defaults to 'PBE_64' + :type user_potcar_functional: str, optional + :param potcar: Dictionary specifying Potcar settings, see + :class:`pymatgen.io.vasp.inputs.Potcar`, defaults to None + :type potcar: Dict, optional + :param prev_calc: Path to previous VASP calculation to use as starting + point for MP settings, defaults to None + :type prev_calc: os.PathLike, optional + :param vaspinput_kwargs: Dictionary of pymatgen's VaspInput arguments. See :class:`pymatgen.io.vasp.inputs.VaspInput` - :type vaspinput_args: Dict + :type vaspinput_kwargs: Dict :param command: Command with which to run VASP, defaults to 'vasp_std' :type command: str, optional :param mpset: Materials Project VASP set to use see @@ -84,6 +99,9 @@ def vasp( :param write_image_output: Whether to write output image in standard asimtools format to file, defaults to False :type write_image_output: bool, optional + :param run_vasp: Whether to run VASP after writing input files, + defaults to True + :type run_vasp: bool, optional """ if vaspinput_kwargs is None: diff --git a/asimtools/calculators.py b/asimtools/calculators.py index 19951f5..43b9d32 100644 --- a/asimtools/calculators.py +++ b/asimtools/calculators.py @@ -38,7 +38,7 @@ def load_calc( calc_params = calc_input[calc_id] except KeyError as exc: msg = f'Calculator with calc_id: {calc_id} not found in' - msg += f'calc_input {calc_input}' + msg += f'calc_input {[c for c in calc_input]}' raise KeyError(msg) from exc except AttributeError as exc: raise AttributeError('No calc_input found') from exc @@ -400,6 +400,28 @@ def load_ase_dftd3(calc_params): return calc +def load_aqcat(calc_params): + """Load AQCat Calculator + :param calc_params: args to pass to loader, including checkpoint_path + :type calc_params: Dict + :return: AQCat calculator + :rtype: :class:`fairchem.core.common.relaxation.ase_utils.patched_calc` + """ + from fairchem.core.common.relaxation.ase_utils import patched_calc + + CHECKPOINT_PATH = "aqcat25/checkpoints_aqcat_ev2/ev2-in+midFiLM-AQCat25+OC20-20M_20251008_223220.pt" + + try: + calc = patched_calc(**calc_params) + except Exception: + logging.error( + "Failed to load AQCat FAIRChemCalculator with parameters:\n %s", \ + calc_params + ) + raise + + return calc + external_calcs = { 'NequIP': load_nequip, 'Allegro': load_nequip, @@ -415,4 +437,5 @@ def load_ase_dftd3(calc_params): 'fairchemV2': load_fairchemV2, 'fairchem': load_fairchemV2, 'ASEDFTD3': load_ase_dftd3, + 'AQCat': load_aqcat } From 3afdbb1c7e4e39237805f69b0294e66680735a73 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Tue, 25 Nov 2025 20:04:14 -0800 Subject: [PATCH 27/78] minor --- asimtools/asimmodules/data/collect_images.py | 1 - asimtools/asimmodules/workflows/utils.py | 1 + asimtools/scripts/asim_check.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/asimtools/asimmodules/data/collect_images.py b/asimtools/asimmodules/data/collect_images.py index fbbd07e..674b6b0 100644 --- a/asimtools/asimmodules/data/collect_images.py +++ b/asimtools/asimmodules/data/collect_images.py @@ -135,7 +135,6 @@ def collect_images( sort_result = sorted( zip(e_per_atoms, selected_atoms), key=lambda x: x[0] ) - selected_atoms = [x[1] for x in sort_result] start_index = 0 diff --git a/asimtools/asimmodules/workflows/utils.py b/asimtools/asimmodules/workflows/utils.py index c775ef7..713ddc9 100644 --- a/asimtools/asimmodules/workflows/utils.py +++ b/asimtools/asimmodules/workflows/utils.py @@ -95,6 +95,7 @@ def prepare_array_vals( # In case the label is a file path with / or space characters labels = [label.replace('/', '+') for label in labels] labels = [label.replace(' ', '+') for label in labels] + labels = [label.replace('*', '') for label in labels] if label_prefix is not None: labels = [label_prefix + '-' + label for label in labels] diff --git a/asimtools/scripts/asim_check.py b/asimtools/scripts/asim_check.py index 7732d0c..9d6b3c2 100755 --- a/asimtools/scripts/asim_check.py +++ b/asimtools/scripts/asim_check.py @@ -86,7 +86,7 @@ def load_job_tree( else: subjob_dict = None - job = load_job_from_directory(workdir) + job = load_job_from_directory(workdir, asimrun_mode=True) job_dict = { 'workdir_name': workdir.name, 'job': job, From be3f15f8c5abd002c43ec85e9733e2edefbbf920 Mon Sep 17 00:00:00 2001 From: Mgcini Keith Phuthi Date: Mon, 15 Dec 2025 15:11:39 -0800 Subject: [PATCH 28/78] properties in collect --- asimtools/asimmodules/data/collect_images.py | 48 +++++++++++--------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/asimtools/asimmodules/data/collect_images.py b/asimtools/asimmodules/data/collect_images.py index 8a82717..e6255c9 100644 --- a/asimtools/asimmodules/data/collect_images.py +++ b/asimtools/asimmodules/data/collect_images.py @@ -6,12 +6,11 @@ from typing import Dict, Optional, Sequence import numpy as np -from copy import deepcopy from pymatgen.io.ase import AseAtomsAdaptor as AAA from pymatgen.analysis.structure_matcher import StructureMatcher from ase.io import write from asimtools.utils import ( - get_atoms, get_images, new_db + get_images, new_db ) @@ -27,6 +26,7 @@ def collect_images( energy_per_atom_limits: Optional[Sequence[float]] = None, force_max: Optional[float] = None, stress_limits: Optional[Sequence[float]] = None, + properties: Optional[tuple] = ('energy', 'forces', 'stress'), ) -> Dict: """Collects images into one file/database and can split them into multiple files/databases for ML tasks @@ -57,6 +57,8 @@ def collect_images( :type force_max: Optional[float], optional :param stress_limits: stress limits for filtering images, defaults to None :type stress_limits: Optional[Sequence[float]], optional + :param properties: which of energy, force, stress to consider + :type Sequence, optional :return: results :rtype: Dict @@ -72,34 +74,36 @@ def collect_images( selected_atoms = [] nonselected_atoms = [] for atoms in images: - energy = atoms.get_potential_energy() - forces = atoms.get_forces() - stress = atoms.get_stress() - select = True - if energy_per_atom_limits is not None: - if (energy < energy_per_atom_limits[0]): - if (energy > energy_per_atom_limits[1]): - select = False - if force_max is not None: - max_force = np.max(np.linalg.norm(forces, axis=1)) - if (max_force > force_max): - select = False - if stress_limits is not None: - max_stress = np.max(stress) - min_stress = np.min(stress) - if (min_stress < stress_limits[0]): - if (max_stress > stress_limits[1]): + if 'energy' in properties: + energy = atoms.get_potential_energy() + if energy_per_atom_limits is not None: + if (energy < energy_per_atom_limits[0]): + if (energy > energy_per_atom_limits[1]): + select = False + if 'forces' in properties: + forces = atoms.get_forces() + if force_max is not None: + max_force = np.max(np.linalg.norm(forces, axis=1)) + if (max_force > force_max): select = False + if 'stress' in properties: + stress = atoms.get_stress() + if stress_limits is not None: + max_stress = np.max(stress) + min_stress = np.min(stress) + if (min_stress < stress_limits[0]): + if (max_stress > stress_limits[1]): + select = False if select: if rename_keys is not None and out_format == 'extxyz': write_kwargs['write_info'] = True - if 'energy' in rename_keys: + if 'energy' in rename_keys and 'energy' in properties: atoms.info[rename_keys['energy']] = energy - if 'forces' in rename_keys: + if 'forces' in rename_keys and 'forces' in properties: atoms.arrays[rename_keys['forces']] = forces - if 'stress' in rename_keys: + if 'stress' in rename_keys and 'stress' in properties: atoms.info[rename_keys['stress']] = stress selected_atoms.append(atoms) else: From c6bed3808a759acabc531d2a76a1d9f8bc8b69a8 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Tue, 13 Jan 2026 14:25:34 -0800 Subject: [PATCH 29/78] aqcat calculator --- asimtools/calculators.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/asimtools/calculators.py b/asimtools/calculators.py index 43b9d32..786f0b3 100644 --- a/asimtools/calculators.py +++ b/asimtools/calculators.py @@ -389,7 +389,10 @@ def load_ase_dftd3(calc_params): if ( (dft_calc_id is not None) and (dft_calc_params is not None) ): raise ValueError('Provide only one of dft_calc_id or dft_calc_params') - dft = load_calc(calc_id=dft_calc_id, calc_params=dft_calc_params) + if ( (dft_calc_id is not None) or (dft_calc_params is not None) ): + dft = load_calc(calc_id=dft_calc_id, calc_params=dft_calc_params) + else: + dft = None try: calc = DFTD3(dft=dft, **d3_args) except Exception: @@ -409,14 +412,12 @@ def load_aqcat(calc_params): """ from fairchem.core.common.relaxation.ase_utils import patched_calc - CHECKPOINT_PATH = "aqcat25/checkpoints_aqcat_ev2/ev2-in+midFiLM-AQCat25+OC20-20M_20251008_223220.pt" - try: - calc = patched_calc(**calc_params) + calc = patched_calc(**calc_params['args']) except Exception: logging.error( "Failed to load AQCat FAIRChemCalculator with parameters:\n %s", \ - calc_params + calc_params['args'] ) raise From a7d648e1f7e9fabb03a67014efccfa4826da76bc Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Mon, 2 Feb 2026 17:45:22 -0800 Subject: [PATCH 30/78] fairchem calc fix --- asimtools/asimmodules/benchmarking/parity.py | 3 ++- asimtools/calculators.py | 17 ++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/asimtools/asimmodules/benchmarking/parity.py b/asimtools/asimmodules/benchmarking/parity.py index 898c848..d656aea 100644 --- a/asimtools/asimmodules/benchmarking/parity.py +++ b/asimtools/asimmodules/benchmarking/parity.py @@ -11,6 +11,7 @@ from functools import partial from multiprocessing import Pool import numpy as np +from tqdm import tqdm import matplotlib.pyplot as plt from asimtools.calculators import load_calc from asimtools.utils import ( @@ -51,7 +52,7 @@ def calc_parity_data( fpvals = [] srvals = [] spvals = [] - for i, atoms in enumerate(subset): + for i, atoms in enumerate(tqdm(subset)): calc = load_calc(calc_id) patoms = atoms.copy() patoms.calc = calc diff --git a/asimtools/calculators.py b/asimtools/calculators.py index 786f0b3..1854554 100644 --- a/asimtools/calculators.py +++ b/asimtools/calculators.py @@ -294,6 +294,7 @@ def load_matgl(calc_params): import matgl calc_params = deepcopy(calc_params) model = calc_params['args'].pop("model") + try: pot = matgl.load_model(model) calc = PESCalculator( @@ -352,10 +353,24 @@ def load_fairchemV2(calc_params): >>> calc = load_calc(calc_params=calc_params) """ + from fairchem.core.units.mlip_unit import load_predict_unit from fairchem.core import pretrained_mlip, FAIRChemCalculator og_calc_params = deepcopy(calc_params) task_name = calc_params['args'].pop('task_name', None) - predictor = pretrained_mlip.get_predict_unit(**calc_params['args']) + try: + predictor = pretrained_mlip.get_predict_unit(**calc_params['args']) + except Exception as exc: + logging.error( + "Failed to load pretrained model trying predict unit" + ) + try: + predictor = load_predict_unit(**calc_params['args']) + except Exception: + logging.error( + "Failed to load predictor unit with parameters:\n %s", \ + og_calc_params + ) + raise exc from exc try: calc = FAIRChemCalculator(predictor, task_name=task_name) From fdc231bae0ae6da6910b3c44b120b0c0c7d2db88 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Fri, 3 Apr 2026 15:17:45 -0700 Subject: [PATCH 31/78] minor aesthetic things, as_integers --- asimtools/asimmodules/lammps/lammps.py | 1 + asimtools/asimmodules/phonopy/thermal_properties.py | 4 ++++ asimtools/asimmodules/workflows/iterative.py | 3 --- asimtools/asimmodules/workflows/sim_array.py | 5 +++++ asimtools/asimmodules/workflows/utils.py | 9 ++++++++- asimtools/calculators.py | 8 ++++++-- asimtools/job.py | 7 +++++-- tests/asimmodules/workflows/test_sim_array.py | 12 ++++++++++++ 8 files changed, 41 insertions(+), 8 deletions(-) diff --git a/asimtools/asimmodules/lammps/lammps.py b/asimtools/asimmodules/lammps/lammps.py index f752d8e..9888e6c 100755 --- a/asimtools/asimmodules/lammps/lammps.py +++ b/asimtools/asimmodules/lammps/lammps.py @@ -161,6 +161,7 @@ def lammps( logging.error(err_txt) with open('lmp_stderr.txt', 'a+', encoding='utf-8') as f: f.write(completed_process.stderr) + completed_process.check_returncode() return {} diff --git a/asimtools/asimmodules/phonopy/thermal_properties.py b/asimtools/asimmodules/phonopy/thermal_properties.py index 4cd3993..3824fbb 100644 --- a/asimtools/asimmodules/phonopy/thermal_properties.py +++ b/asimtools/asimmodules/phonopy/thermal_properties.py @@ -22,8 +22,11 @@ def thermal_properties( if run_thermal_properties_kwargs is None: run_thermal_properties_kwargs = {} + print('Loading phonopy save...') phonon = phonopy.load(phonopy_save_path) + print('Running mesh...') phonon.run_mesh(mesh, **run_mesh_kwargs) + print('Running thermal properties...') phonon.run_thermal_properties( t_step=t_step, t_max=t_max, @@ -33,6 +36,7 @@ def thermal_properties( phonon.save(phonopy_save_path) # Write and plot thermal properties + print('Write and plot thermal properties...') if suffix != '': suffix = '-' + suffix tp_label = 'thermal_properties' + suffix diff --git a/asimtools/asimmodules/workflows/iterative.py b/asimtools/asimmodules/workflows/iterative.py index 069c232..286c53e 100755 --- a/asimtools/asimmodules/workflows/iterative.py +++ b/asimtools/asimmodules/workflows/iterative.py @@ -27,12 +27,9 @@ def iterative( env_ids: Optional[Union[Sequence[str],str]] = None, calc_input: Optional[Dict] = None, env_input: Optional[Dict] = None, - # labels: Optional[Union[Sequence,str]] = 'values', - # label_prefix: Optional[str] = None, str_btn_args: Optional[Dict] = None, secondary_key_sequences: Optional[Sequence] = None, secondary_array_values: Optional[Sequence] = None, - # array_max: Optional[int] = None, ) -> Dict: """Runs the same asimmodule, iterating over multiple values of a specified argument based on a sim_input template provided by the user diff --git a/asimtools/asimmodules/workflows/sim_array.py b/asimtools/asimmodules/workflows/sim_array.py index 18eaa46..e3346c3 100755 --- a/asimtools/asimmodules/workflows/sim_array.py +++ b/asimtools/asimmodules/workflows/sim_array.py @@ -33,6 +33,7 @@ def sim_array( secondary_placeholders: Optional[Sequence] = None, array_max: Optional[int] = None, skip_failed: Optional[bool] = False, + as_integers: Optional[bool] = False, group_size: int = 1, ) -> Dict: """Runs the same asimmodule, iterating over multiple values of a specified @@ -89,6 +90,9 @@ def sim_array( :type skip_failed: Optional[bool], optional :param group_size: Number of jobs to group together, defaults to 1 :type group_size: int, optional + :param as_integers: Whether to return the values as integers, useful for + indexing + :type as_integers: bool, False :return: Results :rtype: Dict """ @@ -105,6 +109,7 @@ def sim_array( str_btn_args=str_btn_args, secondary_key_sequences=secondary_key_sequences, secondary_array_values=secondary_array_values, + as_integers=as_integers, ) array_values = results['array_values'] labels = results['labels'] diff --git a/asimtools/asimmodules/workflows/utils.py b/asimtools/asimmodules/workflows/utils.py index 713ddc9..ce4cd10 100644 --- a/asimtools/asimmodules/workflows/utils.py +++ b/asimtools/asimmodules/workflows/utils.py @@ -17,6 +17,7 @@ def prepare_array_vals( str_btn_args: Optional[Dict] = None, secondary_key_sequences: Optional[Sequence] = None, secondary_array_values: Optional[Sequence] = None, + as_integers: Optional[bool] = False, ): """Helper function for preparing things needed for the different arrays @@ -55,6 +56,9 @@ def prepare_array_vals( over in tandem with array_values to allow changing multiple key-value pairs, defaults to None :type secondary_array_values: Sequence, optional + :param as_integers: Whether to return the values as integers, useful for + indexing + :type as_integers: bool, False :return: Results :rtype: Dict """ @@ -75,7 +79,10 @@ def prepare_array_vals( array_values = [float(v) for v in array_values] elif arange_args is not None: array_values = np.arange(*arange_args) - array_values = [float(v) for v in array_values] + if as_integers: + array_values = [int(v) for v in array_values] + else: + array_values = [float(v) for v in array_values] assert len(array_values) > 0, f'No array_values found' diff --git a/asimtools/calculators.py b/asimtools/calculators.py index 1854554..69fe2c4 100644 --- a/asimtools/calculators.py +++ b/asimtools/calculators.py @@ -32,8 +32,12 @@ def load_calc( assert calc_id is not None or calc_params is not None, \ 'Provide one of calc_id or calc_id and calc_input or calc_params' if calc_id is not None: - if calc_input is None: - calc_input = get_calc_input() + if isinstance(calc_id, dict): + calc_input = {'custom': calc_id} + calc_id = 'custom' + else: + if calc_input is None: + calc_input = get_calc_input() try: calc_params = calc_input[calc_id] except KeyError as exc: diff --git a/asimtools/job.py b/asimtools/job.py index c456f12..0cab78b 100644 --- a/asimtools/job.py +++ b/asimtools/job.py @@ -34,8 +34,8 @@ Atoms = TypeVar('Atoms') -START_MESSAGE = '+' * 15 + ' ASIMTOOLS START' + '+' * 15 + '\n' -STOP_MESSAGE = '+' * 15 + ' ASIMTOOLS STOP' + '+' * 15 + '\n' +START_MESSAGE = '+' * 15 + ' ASIMTOOLS START ' + '+' * 15 + '\n' +STOP_MESSAGE = '+' * 15 + ' ASIMTOOLS STOP ' + '+' * 16 + '\n' class Job(): ''' Abstract class for the job object ''' @@ -272,6 +272,9 @@ def __init__( # get precommands, postcommands, run_prefixes and run_suffixes self.calc_id = self.sim_input.get('args', {}).get('calc_id', None) if self.calc_id is not None: + if isinstance(self.calc_id, dict): + self.calc_input = {'custom': self.calc_id} + self.calc_id = 'custom' self.calc_params = self.calc_input[self.calc_id] else: self.calc_params = {} diff --git a/tests/asimmodules/workflows/test_sim_array.py b/tests/asimmodules/workflows/test_sim_array.py index c02e467..0ea29fc 100644 --- a/tests/asimmodules/workflows/test_sim_array.py +++ b/tests/asimmodules/workflows/test_sim_array.py @@ -59,6 +59,18 @@ 'id-0003__value-3.0__' ] ), + ( + { + "arange_args": (0,4,1), + "as_integers": True, + }, + [ + 'id-0000__value-0__', + 'id-0001__value-1__', + 'id-0002__value-2__', + 'id-0003__value-3__' + ] + ), ( { "linspace_args": (0,3,4) From 7b1cc197dc0e1ba1e0c4f35b809771756c213d0a Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Sat, 4 Apr 2026 11:39:03 -0700 Subject: [PATCH 32/78] distance arg in phonopy --- asimtools/asimmodules/phonopy/full_qha.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/asimtools/asimmodules/phonopy/full_qha.py b/asimtools/asimmodules/phonopy/full_qha.py index d6850c5..e1967be 100644 --- a/asimtools/asimmodules/phonopy/full_qha.py +++ b/asimtools/asimmodules/phonopy/full_qha.py @@ -12,6 +12,7 @@ def full_qha( supercell: Sequence = [5,5,5], t_max: float = 1000, pressure: Optional[float] = None, + distance: Optional[float] = 0.02, ) -> Dict: """Perform a full Quasiharmonic Approximation and predict thermal properties of a given structure. Calculated properties include @@ -41,6 +42,8 @@ def full_qha( :type t_max: float, optional :param pressure: Pressure to optimize to, defaults to None :type pressure: Optional[float], optional + :param distance: Dispacement distance for phonons, defaults to 0.02 + :type distance: Optional[float], optional :return: Nothing :rtype: Dict """ @@ -84,7 +87,7 @@ def full_qha( 'index': {} }, 'supercell': supercell, - 'distance': 0.02, + 'distance': distance, 'phonopy_save_path': phonopy_save_path, }, }, From aa9b22cf7c7528adec3f9142d564c98a1a654e13 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Sat, 4 Apr 2026 13:18:29 -0700 Subject: [PATCH 33/78] pylintrc --- .pylintrc | 304 +++++++++--------------------------------------------- 1 file changed, 47 insertions(+), 257 deletions(-) diff --git a/.pylintrc b/.pylintrc index 9abf5d4..bd1b1fe 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,285 +1,75 @@ [MASTER] - -# Specify a configuration file. -#rcfile= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook='import sys; sys.path.extend(["/Users/xiez/.virtualenvs/seahub/lib/python2.7/site-packages", "/usr/local/lib/python2.7/site-packages/"])' - -# Profiled execution. -profile=no - -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore=CVS - -# Pickle collected data for later comparisons. +ignore=CVS,.git,__pycache__ persistent=yes - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. load-plugins= - [MESSAGES CONTROL] - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time. -#enable= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). -# -# These warnings are annoying, disable them: -# C0111: Missing docstring -# C0301: line too long -# C0302: Too many lines in module (NN) -# I0011: Locally disabling W0000 -# R0201: Method could be a function -# R0801: Similar lines in N files -# R0902: Too many instance attributes (N/7) -# R0903: Too few public methods (N/2) -# R0904: Too many public methods (N/20) -# R0911: Too many return statements (N/6) -# R0912: Too many branches (NN/12) -# R0913: Too many arguments (NN/5) -# R0914: Too many local variables (NN/15) -# R0915: Too many statements (NN/50) -# W0511: TODO -# W0401: Wildcard import FOO -# W0141: Used builtin function 'map' -# W0142: Used * or ** magic -# W0232: Class has no __init__ method -# W0603: Using the global statement -# W0614: Unused import FOO from wildcard import -# W0703: Catch "Exception" -# W1201: Specify string format arguments as logging function parameters -# E1121: Too many positional arguments for function call -# -# These warnings are genuine, we should add them back, by potentially only -# disabling at the lines generating a false positive: -# C0103: Invalid name "FOO" (should match [a-z_][a-z0-9_]{2,30}$) -# E1103: Instance of 'FOO' has no 'BAR' member (but some types could not be inferred) -# W0621: Redefining name 'FOO' from outer scope (line NN) -# W0622: Redefining built-in 'FOO' -# W0702: No exception type(s) specified -# -disable=C0103,C0111,C0302,E1103,I0011,R0201,R0801,R0904,R0911,R0912,R0913,R0914,R0915,W0141,W0142,W0232,W0401,W0603,W0613,W0614,W0621,W0622,W0702,W0703,W1201,E1121 - +disable= + R0801, # Similar lines in N files + W0511, # TODO/FIXME notes [REPORTS] - -# Set the output format. Available formats are text, parseable, colorized, msvs -# (visual studio) and html -output-format=text - -# Include message's id in output -include-ids=yes - -# Put messages in a separate file for each module / package specified on the -# command line instead of printing them on stdout. Reports (if any) will be -# written in a file name "pylint_global.[txt|html]". -files-output=no - -# Tells whether to display a full report or only the messages +output-format=colorized reports=no - -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Add a comment according to your evaluation note. This is used by the global -# evaluation report (RP0004). -comment=no - - -[VARIABLES] - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# A regular expression matching the beginning of the name of dummy variables -# (i.e. not used). -dummy-variables-rgx=_|dummy - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid to define new builtins when possible. -additional-builtins= - - -[TYPECHECK] - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# List of classes names for which member attributes should not be checked -# (useful for classes with attributes dynamically set). -ignored-classes=django.db.models.Model,django.forms.Form,seahub.avatar.models.AvatarBase - -# When zope mode is activated, add a predefined set of Zope acquired attributes -# to generated-members. -zope=no - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E0201 when accessed. Python regular -# expressions are accepted. -generated-members=objects,DoesNotExist,cleaned_data,is_valid,errors - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME,XXX,TODO - - -[SIMILARITIES] - -# Minimum lines number of a similarity. -min-similarity-lines=4 - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - +score=yes [FORMAT] - -# Maximum number of characters on a single line. -max-line-length=80 - -# Maximum number of lines in a module -max-module-lines=1000 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -# In rietveld, 2 spaces indents are used. +max-line-length=100 +max-module-lines=1500 indent-string=' ' - +indent-after-paren=4 [BASIC] +# Module names: lowercase with underscores +module-rgx=[a-z_][a-z0-9_]*$ -# Required attributes for module, separated by a comma -required-attributes= - -# List of builtins function names that should not be used, separated by a comma -bad-functions=map,filter,apply,input - -# Regular expression which should only match correct module names -module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - -# Regular expression which should only match correct module level names -const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ - -# Regular expression which should only match correct class names -class-rgx=[A-Z_][a-zA-Z0-9]+$ - -# Regular expression which should only match correct function names -function-rgx=[a-z_][a-z0-9_]{2,30}$ +# Constants: ALL_CAPS or TypeVar-style (e.g. Atoms = TypeVar('Atoms')) +const-rgx=(([A-Z_][A-Z0-9_]*)|([A-Z][a-zA-Z0-9]+)|(__.*__))$ -# Regular expression which should only match correct method names -method-rgx=[a-z_][a-z0-9_]{2,30}$ +# Classes: PascalCase +class-rgx=[A-Z][a-zA-Z0-9]+$ -# Regular expression which should only match correct instance attribute names -attr-rgx=[a-z_][a-z0-9_]{2,30}$ +# Functions and methods: snake_case, up to 40 chars +function-rgx=[a-z_][a-z0-9_]{1,40}$ +method-rgx=(_?[a-z_][a-z0-9_]{1,40}|__[a-z]+__)$ -# Regular expression which should only match correct argument names -argument-rgx=[a-z_][a-z0-9_]{2,30}$ +# Attributes, arguments, variables: snake_case +attr-rgx=[a-z_][a-z0-9_]{1,40}$ +argument-rgx=[a-z_][a-z0-9_]{1,40}$ +variable-rgx=[a-z_][a-z0-9_]{1,40}$ -# Regular expression which should only match correct variable names -variable-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression which should only match correct list comprehension / -# generator expression variable names -inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ - -# Good variable names which should always be accepted, separated by a comma -good-names=i,j,k,ex,Run,_ - -# Bad variable names which should always be refused, separated by a comma -bad-names=foo,bar,baz,toto,tutu,tata - -# Regular expression which should only match functions or classes name which do -# not require a docstring -no-docstring-rgx=__.*__ +# Short names allowed in loops and comprehensions +good-names=i,j,k,v,ex,Run,_,f,n,x,y,z +no-docstring-rgx=^_(?!_) [DESIGN] - -# Maximum number of arguments for function / method -max-args=5 - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore -ignored-argument-names=_.* - -# Maximum number of locals for function / method body -max-locals=15 - -# Maximum number of return / yield for function / method body -max-returns=6 - -# Maximum number of branch for function / method body -max-branchs=12 - -# Maximum number of statements in function / method body -max-statements=50 - -# Maximum number of parents for a class (see R0901). +max-args=8 +max-locals=20 +max-returns=8 +max-branches=15 +max-statements=60 max-parents=7 +max-attributes=15 +min-public-methods=1 +max-public-methods=30 -# Maximum number of attributes for a class (see R0902). -max-attributes=7 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - - -[CLASSES] - -# List of interface methods to ignore, separated by a comma. This is used for -# instance to not check methods defines in Zope's Interface base class. -ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by +[VARIABLES] +init-import=no +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+$)|dummy|^ignored_|^unused_ -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__,__new__,setUp +[TYPECHECK] +ignore-mixin-members=yes +ignored-classes= -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls +[SIMILARITIES] +min-similarity-lines=6 +ignore-comments=yes +ignore-docstrings=yes +ignore-imports=yes +[MISCELLANEOUS] +notes=FIXME,XXX,TODO [IMPORTS] - -# Deprecated modules which should not be used, separated by a comma -deprecated-modules=regsub,string,TERMIOS,Bastion,rexec - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled) -import-graph= - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled) -ext-import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled) -int-import-graph= - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "Exception" -overgeneral-exceptions=Exception +allow-reexport-from-package=yes From a05f574fe3852cb5027951b34259c8dc6802512f Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Sat, 4 Apr 2026 13:31:44 -0700 Subject: [PATCH 34/78] utils.py linting --- asimtools/utils.py | 320 ++++++++++++++++++++++----------------------- 1 file changed, 157 insertions(+), 163 deletions(-) diff --git a/asimtools/utils.py b/asimtools/utils.py index 02b60b4..77b026f 100644 --- a/asimtools/utils.py +++ b/asimtools/utils.py @@ -17,11 +17,9 @@ import matplotlib.pyplot as plt import pandas as pd from ase.io import read, write -from ase.io.extxyz import save_calc_results from ase.parallel import paropen import ase.db import ase.build -from ase.constraints import FixAtoms from pymatgen.core import Structure, Lattice from pymatgen.io.ase import AseAtomsAdaptor @@ -42,17 +40,17 @@ def read_yaml(yaml_path: str) -> Dict: return {} return output -def write_yaml(yaml_path: str, yaml_Dict: Dict) -> None: +def write_yaml(yaml_path: str, yaml_dict: Dict) -> None: """Write a dictionary to a yaml file :param yaml_path: Path to write yaml to :type yaml_path: str - :param yaml_Dict: Dictionary to write - :type yaml_Dict: Dict - """ + :param yaml_dict: Dictionary to write + :type yaml_dict: Dict + """ # Use paropen so that only the master process is updating outputs with paropen(yaml_path, 'w', encoding='utf-8') as f: - yaml.dump(yaml_Dict, f, sort_keys=False) + yaml.dump(yaml_dict, f, sort_keys=False) def get_axis_lims(x: Sequence, y: Sequence, padding: float=0.1): """Get an estimate of good limits for a plot axis""" @@ -63,6 +61,7 @@ def get_axis_lims(x: Sequence, y: Sequence, padding: float=0.1): return lims def improve_plot(ax=None, fontsize=14): + ''' Apply standard formatting improvements to a matplotlib axis ''' if ax is None: ax = plt.gca() @@ -212,13 +211,13 @@ def write_atoms( if user_columns is not None: columns = list(set(columns + user_columns)) - for atoms in images: + for image in images: # Current workaround magmoms being NaNs is to replace NaNs with 0.0 - if 'initial_magmoms' in atoms.arrays: - atoms.arrays['initial_magmoms'] = np.where( - np.isnan(atoms.arrays['initial_magmoms']), + if 'initial_magmoms' in image.arrays: + image.arrays['initial_magmoms'] = np.where( + np.isnan(image.arrays['initial_magmoms']), 0.0, - atoms.arrays['initial_magmoms'], + image.arrays['initial_magmoms'], ) write( @@ -239,13 +238,13 @@ def write_atoms( ) -def get_atoms( +def get_atoms( # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals,too-many-branches,too-many-statements image_file: Optional[str] = None, interface: str = 'ase', builder: Optional[str] = 'bulk', atoms: Optional[Atoms] = None, repeat: Optional[Tuple[int, int, int]] = None, - repeat_to_N_args: Optional[Dict] = None, + repeat_to_n_args: Optional[Dict] = None, rattle_stdev: Optional[float] = None, mp_id: Optional[str] = None, user_api_key: Optional[str] = None, @@ -291,8 +290,8 @@ def get_atoms( There are three options one could use to specify and image or an atoms objects: - #. image_file + \*\*kwargs - #. builder + \*\*kwargs. + #. image_file + ``**kwargs`` + #. builder + ``**kwargs``. #. atoms Examples @@ -303,16 +302,18 @@ def get_atoms( >>> get_atoms(builder='molecule', name='H2O') Atoms(symbols='OH2', pbc=False) - >>> get_atoms(builder='bulk', name='Cu') - Atoms(symbols='Cu', pbc=True, cell=[[0.0, 1.805, 1.805], [1.805, 0.0, 1.805], [1.805, 1.805, 0.0]]) + >>> get_atoms(builder='bulk', name='Cu') # doctest: +ELLIPSIS + Atoms(symbols='Cu', pbc=True, ...) >>> get_atoms(builder='bulk', name='Ar', crystalstructure='fcc', a=3.4, cubic=True) Atoms(symbols='Ar4', pbc=True, cell=[3.4, 3.4, 3.4]) - >>> get_atoms(builder='fcc100', symbol='Fe', vacuum=8, size=[4,4,5]) - Atoms(symbols='Cu80', pbc=[True, True, False], cell=[10.210621920333747, 10.210621920333747, 23.22], tags=...) + >>> get_atoms(builder='fcc100', symbol='Fe', vacuum=8, size=[4,4,5]) # doctest: +ELLIPSIS + Atoms(symbols='Cu80', pbc=[True, True, False], ...) You can also specify constraints to fix atoms in place, for example - >>> get_atoms(builder='fcc100', symbol='Fe', vacuum=8, size=[4,4,5], constraints=[{'constraint': 'FixAtoms', 'indices': [0,1]}]) - Atoms(symbols='Cu80', pbc=[True, True, False], cell=[10.210621920333747, 10.210621920333747, 23.22], tags=...) + >>> get_atoms( # doctest: +ELLIPSIS + ... builder='fcc100', symbol='Fe', vacuum=8, size=[4,4,5], + ... constraints=[{'constraint': 'FixAtoms', 'indices': [0,1]}]) + Atoms(symbols='Cu80', pbc=[True, True, False], ...) Some examples for reading an image from a file using :func:`ase.io.read` @@ -325,7 +326,8 @@ def get_atoms( >>> get_atoms(image_file='h2o.cif', format='cif') Atoms(symbols='OH2', pbc=False) >>> from ase.io import write - >>> molecules = [get_atoms(builder='molecule', name='H2O'), get_atoms(builder='molecule', name='H2')] + >>> molecules = [get_atoms(builder='molecule', name='H2O'), + ... get_atoms(builder='molecule', name='H2')] >>> write('molecules.xyz', molecules, format='extxyz') >>> get_atoms(image_file='molecules.xyz', index=0) # Pick out one structure using indexing Atoms(symbols='OH2', pbc=False) @@ -334,19 +336,21 @@ def get_atoms( >>> li_bulk = get_atoms(name='Li') >>> li_bulk.write('POSCAR', format='vasp') - >>> get_atoms(image_file='POSCAR', repeat=[3,3,3]) - Atoms(symbols='Li27', pbc=True, cell=[[-5.235, 5.235, 5.235], [5.235, -5.235, 5.235], [5.235, 5.235, -5.235]]) - >>> get_atoms(builder='bulk', name='Li', repeat=[2,2,2], rattle_stdev=0.01) - Atoms(symbols='Li8', pbc=True, cell=[[-3.49, 3.49, 3.49], [3.49, -3.49, 3.49], [3.49, 3.49, -3.49]]) - >>> get_atoms(builder='bulk', name='Li', repeat_to_N_args={'N': 16, 'max_dim': 10.0}) - Atoms(symbols='Li16', pbc=True, cell=[[-6.98, 6.98, 6.98], [6.98, -6.98, 6.98], [6.98, 6.98, -6.98]]) + >>> get_atoms(image_file='POSCAR', repeat=[3,3,3]) # doctest: +ELLIPSIS + Atoms(symbols='Li27', pbc=True, ...) + >>> get_atoms( # doctest: +ELLIPSIS + ... builder='bulk', name='Li', repeat=[2,2,2], rattle_stdev=0.01) + Atoms(symbols='Li8', pbc=True, ...) + >>> get_atoms( # doctest: +ELLIPSIS + ... builder='bulk', name='Li', repeat_to_n_args={'n': 16, 'max_dim': 10.0}) + Atoms(symbols='Li16', pbc=True, ...) Mostly for internal use and use in asimmodules, one can specify atoms directly >>> li_bulk = get_atoms(name='Li') - >>> get_atoms(atoms=li_bulk) - Atoms(symbols='Li', pbc=True, cell=[[-1.745, 1.745, 1.745], [1.745, -1.745, 1.745], [1.745, 1.745, -1.745]]) + >>> get_atoms(atoms=li_bulk) # doctest: +ELLIPSIS + Atoms(symbols='Li', pbc=True, ...) In an asimmodule, the ``image`` argument is always given as a dictionary, you therefore have to expand it before passing it to ``get_atoms`` @@ -368,7 +372,9 @@ def get_atoms( You can also specify whether you want the primitive(default) or conventional unit cell as a keyword argument - >>> {'mp_id': 'mp-14', 'interface': 'pymatgen', 'user_api_key': "USER_API_KEY", 'conventional_unit_cell': True}, + >>> image = { + ... 'mp_id': 'mp-14', 'interface': 'pymatgen', + ... 'user_api_key': "USER_API_KEY", 'conventional_unit_cell': True} >>> get_atoms(**image) Structure Summary Lattice @@ -380,7 +386,9 @@ def get_atoms( The lattice paramters are passed as an ArrayLike with shape [3,3] to :class:`pymatgen.core.lattice.Lattice` or dictionary to the :func:`pymatgen.core.lattice.Lattice.from_parameters` function. - >>> image = {'builder': 'pymatgen.core.surface.Structure', 'lattice': {'a': 2.7, 'b': 2.7, 'c': 2.7, 'alpha': 90, 'beta': 90, 'gamma': 90}} + >>> image = { # doctest: +ELLIPSIS + ... 'builder': 'pymatgen.core.surface.Structure', + ... 'lattice': {'a': 2.7, 'b': 2.7, 'c': 2.7, 'alpha': 90, 'beta': 90, 'gamma': 90}} >>> get_atoms(**image) Structure Summary Lattice @@ -408,14 +416,15 @@ def get_atoms( raise else: assert atoms is not None, 'Specify an input structure' - + if interface == 'pymatgen': + # pylint: disable=import-outside-toplevel if mp_id is not None: from pymatgen.ext.matproj import MPRester with MPRester(user_api_key) as mpr: - struct = mpr.get_structure_by_material_id(mp_id, **kwargs) + struct = mpr.get_structure_by_material_id(mp_id, **kwargs) elif builder is not None: - import pymatgen.core + import pymatgen.core # pylint: disable=redefined-outer-name builder_func = getattr(pymatgen.core, builder) lattice = kwargs.get('lattice', False) if isinstance(lattice, dict): @@ -442,11 +451,11 @@ def get_atoms( elif rattle_stdev is not None and interface == 'pymatgen': struct.perturb(distance=rattle_stdev, min_distance=0) - if repeat_to_N_args is not None and interface == 'ase': - atoms = repeat_to_N(atoms, **repeat_to_N_args) - elif repeat_to_N_args is not None and interface == 'pymatgen': + if repeat_to_n_args is not None and interface == 'ase': + atoms = repeat_to_n(atoms, **repeat_to_n_args) + elif repeat_to_n_args is not None and interface == 'pymatgen': raise NotImplementedError( - 'repeat_to_N_args is only implemented for ASE interface' + 'repeat_to_n_args is only implemented for ASE interface' ) if constraints is not None and interface == 'ase': @@ -458,21 +467,21 @@ def get_atoms( constraint_cls = getattr(ase.constraints, name, None) const = constraint_cls(**constraint_args) consts.append(const) - + for const in consts: atoms.set_constraint(const) if return_type == 'ase' and interface == 'ase': return atoms - elif return_type == 'pymatgen' and interface == 'ase': + if return_type == 'pymatgen' and interface == 'ase': if builder == 'molecule': return AseAtomsAdaptor.get_molecule(atoms) - else: - return AseAtomsAdaptor.get_structure(atoms) - elif return_type == 'pymatgen' and interface == 'pymatgen': + return AseAtomsAdaptor.get_structure(atoms) + if return_type == 'pymatgen' and interface == 'pymatgen': return struct - elif return_type == 'ase' and interface == 'pymatgen': + if return_type == 'ase' and interface == 'pymatgen': return AseAtomsAdaptor.get_atoms(struct, msonable=False) + return None def parse_slice(value: str, bash: bool = False) -> slice: """Parses a :func:`slice` from string, like `start:stop:step`. @@ -492,32 +501,31 @@ def parse_slice(value: str, bash: bool = False) -> slice: parts.append(index) if not bash: return slice(*[int(p) if p else None for p in parts]) - else: - if not parts[0]: - parts[0] = '0' - if not parts[1]: - parts[1] = '$END' - if len(parts) == 2: - parts.append('1') - return f'$(seq {parts[0]} {parts[2]} {parts[1]})' - -def repeat_to_N( + if not parts[0]: + parts[0] = '0' + if not parts[1]: + parts[1] = '$END' + if len(parts) == 2: + parts.append('1') + return f'$(seq {parts[0]} {parts[2]} {parts[1]})' + +def repeat_to_n( atoms: Atoms, - N: int, + n: int, max_dim: float = 50.0, ) -> Atoms: - """Scale a structure to have approximately N atoms in the unit cell. + """Scale a structure to have approximately n atoms in the unit cell. The function repeats the shortest axis of the unit cell until the - number of atoms >= N or the longest axis of the unit cell is > max_dim. + number of atoms >= n or the longest axis of the unit cell is > max_dim. :param atoms: Input atoms object :type atoms: Atoms - :param N: Target number of atoms - :type N: int + :param n: Target number of atoms + :type n: int :param max_dim: Maximum length of the longest axis of the unit cell, defaults to 50.0 :type max_dim: float, optional - :raises ValueError: If it fails to scale the structure to N atoms + :raises ValueError: If it fails to scale the structure to n atoms :return: Scaled atoms object :rtype: Atoms """ @@ -526,7 +534,7 @@ def repeat_to_N( num_atoms = len(atoms) new_atoms = atoms.copy() - while num_atoms < N and np.max(lengths) < max_dim: + while num_atoms < n and np.max(lengths) < max_dim: shortest_axis = np.argmin(lengths) repeat_vec = [1, 1, 1] repeat_vec[shortest_axis] += 1 @@ -535,14 +543,14 @@ def repeat_to_N( lengths = [np.linalg.norm(vec) for vec in cell] num_atoms = len(new_atoms) - if num_atoms < N: + if num_atoms < n: raise ValueError( - f'Failed to scale structure to {N} atoms without exceeding \ - max_dim of {max_dim} Angstroms' + f'Failed to scale structure to {n} atoms without exceeding ' + f'max_dim of {max_dim} Angstroms' ) return new_atoms -def get_images( +def get_images( # pylint: disable=too-many-positional-arguments image_file: str = None, pattern: str = None, patterns: List[str] = None, @@ -603,12 +611,12 @@ def get_images( >>> molecules.append(get_atoms(builder='molecule', name='H2')) >>> molecules.append(get_atoms(builder='molecule', name='N2')) >>> write('molecules.xyz', molecules, format='extxyz') - >>> get_images(image_file='molecules.xyz') - [Atoms(symbols='OH2', pbc=False), Atoms(symbols='H2', pbc=False), Atoms(symbols='N2', pbc=False)] + >>> get_images(image_file='molecules.xyz') # doctest: +ELLIPSIS + [Atoms(symbols='OH2', pbc=False), ...] >>> get_images(image_file='molecules.xyz', index=':2') [Atoms(symbols='OH2', pbc=False), Atoms(symbols='H2', pbc=False)] - You can also use a wildcard (\*) by specifying the pattern argument. Notice + You can also use a wildcard (``*``) by specifying the pattern argument. Notice that the files don't have to be the same format if ASE can guess all the file formats, otherwise you can specify the format argument which should apply to all the images. @@ -619,28 +627,27 @@ def get_images( >>> fe.write('bulk_fe.cif') >>> pt = get_atoms(name='Pt') >>> pt.write('bulk_pt.cfg') - >>> get_images(pattern='bulk*') - [Atoms(symbols='Cu', pbc=True, cell=[[0.0, 1.805, 1.805], [1.805, 0.0, 1.805], [1.805, 1.805, 0.0]], masses=..., momenta=...), Atoms(symbols='Fe', pbc=True, cell=[[2.48549, 0.0, 0.0], [-0.8284876429214074, 2.3433456351179887, 0.0], [-0.8284876429214074, -1.171653675382785, 2.0294079014797743]], spacegroup_kinds=...), Atoms(symbols='Pt', pbc=True, cell=[[0.0, 1.96, 1.96], [1.96, 0.0, 1.96], [1.96, 1.96, 0.0]], masses=..., momenta=...)] - Atoms(symbols='OH2', pbc=False) - >>> get_images(pattern='bulk*.cfg', format='cfg') - [Atoms(symbols='Cu', pbc=True, cell=[[0.0, 1.805, 1.805], [1.805, 0.0, 1.805], [1.805, 1.805, 0.0]], masses=..., momenta=...), Atoms(symbols='Pt', pbc=True, cell=[[0.0, 1.96, 1.96], [1.96, 0.0, 1.96], [1.96, 1.96, 0.0]], masses=..., momenta=...)] - + >>> get_images(pattern='bulk*') # doctest: +ELLIPSIS + [Atoms(symbols='Cu', ...), ...] + >>> get_images(pattern='bulk*.cfg', format='cfg') # doctest: +ELLIPSIS + [Atoms(symbols='Cu', ...), ...] + You can also specify multiple patterns - >>> get_images(patterns=['bulk*.cfg', 'bulk\*.cif']) - [Atoms(symbols='Cu', pbc=True, cell=[[0.0, 1.805, 1.805], [1.805, 0.0, 1.805], [1.805, 1.805, 0.0]], masses=..., momenta=...), Atoms(symbols='Pt', pbc=True, cell=[[0.0, 1.96, 1.96], [1.96, 0.0, 1.96], [1.96, 1.96, 0.0]], masses=..., momenta=...), Atoms(symbols='Fe', pbc=True, cell=[[2.48549, 0.0, 0.0], [-0.8284876429214074, 2.3433456351179887, 0.0], [-0.8284876429214074, -1.171653675382785, 2.0294079014797743]], spacegroup_kinds=...)] - + >>> get_images(patterns=['bulk*.cfg', 'bulk*.cif']) # doctest: +ELLIPSIS + [Atoms(symbols='Cu', ...), ...] + Or you can directly pass a list of Atoms, mostly for internal use - >>> get_images(images=molecules) - [Atoms(symbols='OH2', pbc=False), Atoms(symbols='H2', pbc=False), Atoms(symbols='N2', pbc=False)] + >>> get_images(images=molecules) # doctest: +ELLIPSIS + [Atoms(symbols='OH2', pbc=False), ...] In an asimmodule, the ``images`` argument is always given as a dictionary, you therefore have to expand it before passing it to ``get_images`` >>> images = {'pattern': 'bulk*'} - >>> get_images(**images) - [Atoms(symbols='Cu', pbc=True, cell=[[0.0, 1.805, 1.805], [1.805, 0.0, 1.805], [1.805, 1.805, 0.0]], masses=..., momenta=...), Atoms(symbols='Fe', pbc=True, cell=[[2.48549, 0.0, 0.0], [-0.8284876429214074, 2.3433456351179887, 0.0], [-0.8284876429214074, -1.171653675382785, 2.0294079014797743]], spacegroup_kinds=...), Atoms(symbols='Pt', pbc=True, cell=[[0.0, 1.96, 1.96], [1.96, 0.0, 1.96], [1.96, 1.96, 0.0]], masses=..., momenta=...)] + >>> get_images(**images) # doctest: +ELLIPSIS + [Atoms(symbols='Cu', ...), ...] """ assert (image_file is not None) or \ (pattern is not None) or \ @@ -659,21 +666,16 @@ def get_images( f'No images matching pattern "{pattern}" from "{os.getcwd()}"' images = [] - for image_file in image_files: - image_file = Path(image_file).resolve() + for fpath in image_files: + fpath = Path(fpath).resolve() try: - new_images = read( - image_file, - index=index, - **kwargs - ) - except Exception as exc: + new_images = read(fpath, index=index, **kwargs) + except Exception as exc: # pylint: disable=broad-exception-caught if not skip_failed: raise IOError( - f"Failed to read {image_file} from {os.getcwd()}" + f"Failed to read {fpath} from {os.getcwd()}" ) from exc - else: - new_images = [] + new_images = [] # Output of read can either be list of atoms or Atoms, depending on index if not isinstance(new_images, list): @@ -682,12 +684,12 @@ def get_images( elif patterns is not None: images = [] - for pattern in patterns: - image_files = natsorted(glob(pattern)) - assert len(image_files) > 0, \ - f'Don\'t include pattern "{pattern}" if no files match' + for pat in patterns: + pat_files = natsorted(glob(pat)) + assert len(pat_files) > 0, \ + f'Don\'t include pattern "{pat}" if no files match' images += get_images( - pattern=pattern, + pattern=pat, index=index, skip_failed=skip_failed, **kwargs @@ -698,9 +700,7 @@ def get_images( images = [] if not skip_failed: - addontxt = '' - if image_file: - addontxt = f' in image_file: {image_file}' + addontxt = f' in image_file: {image_file}' if image_file else '' assert len(images) > 0, 'No images found' + addontxt return images @@ -788,29 +788,26 @@ def check_if_slurm_job_is_running(slurm_job_id: Union[str,int]): """ slurm_job_id = str(slurm_job_id) completed_process = subprocess.run( - ['squeue', '--job', slurm_job_id], + ['squeue', '--job', slurm_job_id], check=False, capture_output=True, text=True, ) stdout = completed_process.stdout - if str(' '+slurm_job_id) in stdout: - return True - else: - return False + return str(' ' + slurm_job_id) in stdout def change_dict_value( - d: Dict, + dct: Dict, new_value, key_sequence: Sequence, return_copy: Optional[bool] = True, placeholder: Optional[str] = None, ) -> Dict: - """Changes a value in the specified dictionary given by following the + """Changes a value in the specified dictionary given by following the key sequence - :param d: dictionary to be changed - :type d: Dict + :param dct: dictionary to be changed + :type dct: Dict :param new_value: The new value that will replace the old one :type new_value: _type_ :param key_sequence: List of keys in the order in which they access the @@ -823,37 +820,36 @@ def change_dict_value( :rtype: Dict """ if return_copy: - d = deepcopy(d) + dct = deepcopy(dct) if len(key_sequence) == 1: if placeholder is None: - d[key_sequence[0]] = new_value + dct[key_sequence[0]] = new_value else: - d[key_sequence[0]] = d[key_sequence[0]].replace( + dct[key_sequence[0]] = dct[key_sequence[0]].replace( placeholder, new_value ) - return d - else: - new_d = change_dict_value( - d[key_sequence[0]], - new_value, - key_sequence[1:], - return_copy=return_copy, - placeholder=placeholder, - ) - d[key_sequence[0]] = new_d - return d + return dct + new_d = change_dict_value( + dct[key_sequence[0]], + new_value, + key_sequence[1:], + return_copy=return_copy, + placeholder=placeholder, + ) + dct[key_sequence[0]] = new_d + return dct def change_dict_values( - d: Dict, + dct: Dict, new_values: Sequence, key_sequences: Sequence, return_copy: bool = True ) -> Dict: - """Changes values in the specified dictionary given by following the + """Changes values in the specified dictionary given by following the key sequences. Key-value pairs are set in the given order - :param d: dictionary to be changed - :type d: Dict + :param dct: dictionary to be changed + :type dct: Dict :param new_values: The new values that will replace the old one :type new_values: Sequence :param key_sequence: List of list of keys in the order in which they @@ -866,9 +862,8 @@ def change_dict_values( :rtype: Dict """ for key_sequence, new_value in zip(key_sequences, new_values): - d = change_dict_value(d, new_value, key_sequence, return_copy) - - return d + dct = change_dict_value(dct, new_value, key_sequence, return_copy) + return dct def get_logger( logfile='job.log', @@ -893,16 +888,16 @@ def get_logger( def get_str_btn( - s: Union[str,os.PathLike], + string: Union[str, os.PathLike], s1: str, s2: str, occurence: Optional[int] = 0, start_index: Optional[int] = 0, ): - """Returns the substring between strings s1 and s2 from s + """Returns the substring between strings s1 and s2 from string - :param s: string/path from which to extract substring - :type s: str + :param string: string/path from which to extract substring + :type string: str :param s1: substring before the desired substring, None starts from the beginning of s :type s1: str @@ -918,35 +913,36 @@ def get_str_btn( :return: substring :rtype: _type_ """ - s = str(s) + string = str(string) j = 0 - stop_index = len(s) + 1 - s = s[start_index:stop_index] + stop_index = len(string) + 1 + string = string[start_index:stop_index] while occurence - j >= 0: if s1 is not None: try: - i1 = s.index(s1) + len(s1) - except: + i1 = string.index(s1) + len(s1) + except ValueError as exc: raise ValueError( - f'substring {s1} not found in {s}' - ) + f'substring {s1} not found in {string}' + ) from exc else: i1 = 0 if s2 is not None: try: - i2 = s[i1:].index(s2) + i1 - except: + i2 = string[i1:].index(s2) + i1 + except ValueError as exc: raise ValueError( - f'substring {s2} not found in {s}' - ) + f'substring {s2} not found in {string}' + ) from exc else: - i2 = len(s) + i2 = len(string) if occurence - j == 0: - return s[i1:i2] + return string[i1:i2] - s = s[i1:] + string = string[i1:] j += 1 + return None def find_nth(haystack: str, needle: str, n: int) -> int: ''' Return index of nth occurence of substring in string ''' @@ -957,26 +953,25 @@ def find_nth(haystack: str, needle: str, n: int) -> int: return start def get_nth_label( - s: os.PathLike, + string: os.PathLike, n: int = 1, ): ''' Return nth label in a string potentially containing multiple labels, indexing starts from 0 ''' - s = str(s) - start = find_nth(s, '__', n=(n*2+1)) - return get_str_btn(s, '__', '__', start_index=start) + string = str(string) + start = find_nth(string, '__', n=n*2+1) + return get_str_btn(string, '__', '__', start_index=start) -def expand_wildcards(d: Dict, root_path: os.PathLike = None) -> Dict: +def expand_wildcards(dct: Dict, root_path: os.PathLike = None) -> Dict: """Expands paths in a dictionary - :param d: Dictionary to expand paths in - :type d: Dict[str, Any] + :param dct: Dictionary to expand paths in + :type dct: Dict[str, Any] :param root_path: Root path to expand paths from :type root_path: os.PathLike :return: Dictionary with expanded paths :rtype: Dict[str, Any] """ - import os def expand_value( value: str, root_path: os.PathLike = None @@ -993,11 +988,10 @@ def expand_value( value = os.path.relpath(value, root_path) return value - for key, value in d.items(): + for key, value in dct.items(): if isinstance(value, str): - d[key] = expand_value(value, root_path=root_path) + dct[key] = expand_value(value, root_path=root_path) elif isinstance(value, dict): - d[key] = expand_wildcards(value, root_path=root_path) - + dct[key] = expand_wildcards(value, root_path=root_path) - return d \ No newline at end of file + return dct From 898927703d72dd2a61ad73abe2b81fa6e0efd9e3 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Sat, 4 Apr 2026 13:32:09 -0700 Subject: [PATCH 35/78] calculators.py linting --- asimtools/calculators.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/asimtools/calculators.py b/asimtools/calculators.py index 69fe2c4..e1e6d0c 100644 --- a/asimtools/calculators.py +++ b/asimtools/calculators.py @@ -42,7 +42,7 @@ def load_calc( calc_params = calc_input[calc_id] except KeyError as exc: msg = f'Calculator with calc_id: {calc_id} not found in' - msg += f'calc_input {[c for c in calc_input]}' + msg += f'calc_input {list(calc_input)}' raise KeyError(msg) from exc except AttributeError as exc: raise AttributeError('No calc_input found') from exc @@ -265,7 +265,8 @@ def load_espresso_profile(calc_params): def load_m3gnet(calc_params): """Load any M3GNet or MatGL calculator - :param calc_params: parameters to be passed to matgl.ext.ase.M3GNetCalculator. Must include a key "model" that points to the model used to instantiate the potential + :param calc_params: parameters to be passed to matgl.ext.ase.M3GNetCalculator. + Must include a key "model" that points to the model used to instantiate the potential :type calc_params: Dict :return: M3GNet calculator :rtype: :class:`matgl.ext.ase.M3GNetCalculator` @@ -289,7 +290,8 @@ def load_m3gnet(calc_params): def load_matgl(calc_params): """Load any MatGL calculator - :param calc_params: parameters to be passed to matgl.ext.ase.PESCalculator. Must include a key "model" that points to the model used to instantiate the potential + :param calc_params: parameters to be passed to matgl.ext.ase.PESCalculator. + Must include a key "model" that points to the model used to instantiate the potential :type calc_params: Dict :return: MatGL calculator :rtype: :class:`matgl.ext.ase.PESCalculator` @@ -311,7 +313,7 @@ def load_matgl(calc_params): return calc -def load_fairchemV1(calc_params): +def load_fairchem_v1(calc_params): """Load any fairchemV1 calculator :param calc_params: parameters to be passed to fairchem.core.OCPCalculator. @@ -333,7 +335,7 @@ def load_fairchemV1(calc_params): return calc -def load_fairchemV2(calc_params): +def load_fairchem_v2(calc_params): """Load any fairchemV1 calculator :param calc_params: parameters to be passed to fairchem.core.FAIRChemCalculator. @@ -363,7 +365,7 @@ def load_fairchemV2(calc_params): task_name = calc_params['args'].pop('task_name', None) try: predictor = pretrained_mlip.get_predict_unit(**calc_params['args']) - except Exception as exc: + except Exception as exc: # pylint: disable=broad-exception-caught logging.error( "Failed to load pretrained model trying predict unit" ) @@ -402,12 +404,12 @@ def load_ase_dftd3(calc_params): d3_args = calc_params['args'].get('d3_args', {}) if 'dft' in d3_args: raise ValueError('Do not specify dft arg for DFTD3, specify calc_id') - + dft_calc_id = calc_params['args'].get('dft_calc_id', None) dft_calc_params = calc_params['args'].get('dft_calc_params', None) if ( (dft_calc_id is not None) and (dft_calc_params is not None) ): raise ValueError('Provide only one of dft_calc_id or dft_calc_params') - + if ( (dft_calc_id is not None) or (dft_calc_params is not None) ): dft = load_calc(calc_id=dft_calc_id, calc_params=dft_calc_params) else: @@ -452,10 +454,10 @@ def load_aqcat(calc_params): 'EspressoProfile': load_espresso_profile, 'M3GNet': load_m3gnet, 'MatGL': load_matgl, - 'OMAT24': load_fairchemV1, - 'fairchemV1': load_fairchemV1, - 'fairchemV2': load_fairchemV2, - 'fairchem': load_fairchemV2, + 'OMAT24': load_fairchem_v1, + 'fairchemV1': load_fairchem_v1, + 'fairchemV2': load_fairchem_v2, + 'fairchem': load_fairchem_v2, 'ASEDFTD3': load_ase_dftd3, 'AQCat': load_aqcat } From 0db9fbb3792f14e399bf10b3a3f615cae24fb59e Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Sat, 4 Apr 2026 13:32:43 -0700 Subject: [PATCH 36/78] lammps utils.py linting --- asimtools/asimmodules/lammps/utils.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/asimtools/asimmodules/lammps/utils.py b/asimtools/asimmodules/lammps/utils.py index 12a9f2d..3386767 100644 --- a/asimtools/asimmodules/lammps/utils.py +++ b/asimtools/asimmodules/lammps/utils.py @@ -1,13 +1,11 @@ -import os -from glob import glob -import pickle +'''Utility functions for reading and processing LAMMPS output files.''' from pathlib import Path -import pandas as pd -from ase.units import bar import numpy as np + def read_lammps_log(logfile, skip_failed=False): - with open(logfile, 'r') as f: + '''Read a LAMMPS log file and return thermodynamic data and metadata.''' + with open(logfile, 'r', encoding='utf-8') as f: logtxt = f.readlines() starts = [] @@ -24,14 +22,14 @@ def read_lammps_log(logfile, skip_failed=False): if len(atoms_line) == 2: try: natoms = int(atoms_line[0]) - except: + except ValueError: pass if natoms is None: if 'Loop time' in line: try: natoms = int(line.split()[-2]) - except: + except ValueError: pass if skip_failed and (len(starts) != len(stops) or len(starts) == 0): @@ -55,4 +53,4 @@ def read_lammps_log(logfile, skip_failed=False): 'columns': headings, } - return data[1:,:], metadata \ No newline at end of file + return data[1:,:], metadata From 3ff35c4c5a5e0276f839af0ac3f17160d3373086 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Sat, 4 Apr 2026 13:33:04 -0700 Subject: [PATCH 37/78] utils.py linting --- tests/unit/test_utils.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index be43e69..15138ca 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -24,7 +24,7 @@ get_str_btn, expand_wildcards, write_atoms, - repeat_to_N, + repeat_to_n, ) import ase.build @@ -377,14 +377,14 @@ def test_expand_wildcards(test_input, expected, tmp_path): assert expand_wildcards(test_input, root_path=tmp_path) == expected -def test_repeat_to_N(): +def test_repeat_to_n(): ''' Test repeating unit cell to at least N atoms ''' atoms = ase.build.bulk('Cu', crystalstructure='fcc', cubic=True, a=2.0) - repeated_atoms = repeat_to_N(atoms, 16) + repeated_atoms = repeat_to_n(atoms, 16) assert len(repeated_atoms) == 16 assert np.abs(repeated_atoms.get_cell()[0][0] - 2*2.0) < 1e-6 assert np.abs(repeated_atoms.get_cell()[1][1] - 2*2.0) < 1e-6 assert np.abs(repeated_atoms.get_cell()[2][2] - 1*2.0) < 1e-6 - assert len(repeat_to_N(atoms, 15)) == 16 + assert len(repeat_to_n(atoms, 15)) == 16 with pytest.raises(ValueError): - repeat_to_N(atoms, 16, max_dim=4) \ No newline at end of file + repeat_to_n(atoms, 16, max_dim=4) \ No newline at end of file From 7b11aac45240ae2d4aaa1c481240d53a6a9aef10 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Sat, 4 Apr 2026 13:47:32 -0700 Subject: [PATCH 38/78] job.py linting --- asimtools/job.py | 80 ++++++++++++++++++++++-------------------------- 1 file changed, 37 insertions(+), 43 deletions(-) diff --git a/asimtools/job.py b/asimtools/job.py index 0cab78b..d66924c 100644 --- a/asimtools/job.py +++ b/asimtools/job.py @@ -11,8 +11,7 @@ from pathlib import Path from datetime import datetime import logging -from glob import glob -from typing import List, TypeVar, Dict, Tuple, Union, Sequence +from typing import List, TypeVar, Dict, Tuple, Union from copy import deepcopy from colorama import Fore import ase.io @@ -29,7 +28,6 @@ get_calc_input, get_logger, check_if_slurm_job_is_running, - parse_slice, ) Atoms = TypeVar('Atoms') @@ -40,13 +38,14 @@ class Job(): ''' Abstract class for the job object ''' # pylint: disable=too-many-instance-attributes - def __init__( + def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments self, sim_input: Dict, env_input: Union[Dict,None] = None, calc_input: Union[Dict,None] = None, asimrun_mode: bool = False, ) -> None: + ''' Initialise Job with simulation, environment and calculator inputs ''' if env_input is None: env_input = get_env_input() if calc_input is None: @@ -149,8 +148,7 @@ def get_output(self) -> Dict: output_yaml = self.get_output_yaml() if output_yaml.exists(): return read_yaml(output_yaml) - else: - return {} + return {} def update_sim_input(self, new_params) -> None: ''' Update simulation parameters ''' @@ -433,7 +431,7 @@ def gen_input_files( self._gen_slurm_script() return None - def submit( + def submit( # pylint: disable=too-many-locals,too-many-branches,too-many-statements self, dependency: Union[List,None,str] = None, write_image: bool = True, @@ -603,7 +601,7 @@ def __init__( self.use_slurm = True else: self.use_slurm = False - + if all_sh: self.use_sh = True else: @@ -639,7 +637,7 @@ def _gen_array_script( txt += 'echo "LAUNCHDIR: ${CUR_DIR}"\n' txt += f'G={group_size} #Group size\n' txt += 'N=${SLURM_ARRAY_TASK_ID}\n' - txt += f'WORKDIRS=($(ls -dv ./id-*))\n' + txt += 'WORKDIRS=($(ls -dv ./id-*))\n' seqtxt = '$(seq $(($G*$N)) $(($G*$N+$G-1)) )' txt += f'for i in {seqtxt}; do\n' txt += ' WORKDIR=${WORKDIRS[$i]}\n' @@ -676,13 +674,13 @@ def _gen_sh_script( ''' txt = '#!/usr/bin/env sh\n\n' - txt += f'for WORKDIR in id-*; do\n' + txt += 'for WORKDIR in id-*; do\n' txt += ' cd ${WORKDIR};\n' txt += '\n'.join(self.unitjobs[0].calc_params.get('precommands', [])) txt += '\n asim-run sim_input.yaml -c calc_input.yaml ' txt += '>stdout.txt 2>stderr.txt\n' txt += '\n'.join(self.unitjobs[0].calc_params.get('precommands', [])) - txt += f'\n cd ../;\n' + txt += '\n cd ../;\n' txt += 'done' if write: @@ -714,9 +712,9 @@ def submit_jobs( write_image=kwargs.get('write_image', True) ) job_ids.append(job_id) - except Exception as exc: + except Exception as exc: # pylint: disable=broad-exception-caught logger = self.get_logger() - logger.error(f'Error submitting job in {unitjob.workdir}') + logger.error('Error submitting job in %s', unitjob.workdir) logger.error(exc) if not kwargs.get('skip_failed', False): raise exc @@ -724,7 +722,7 @@ def submit_jobs( def submit_sh_array( self, - **kwargs, + **_kwargs, ) -> Union[None,List[int]]: ''' Submits jobs using a sh script. Proceeds even if some jobs fail @@ -757,7 +755,7 @@ def submit_sh_array( if completed_process.stderr is not None: with paropen('stderr.txt', 'a+', encoding='utf-8') as err_file: err_file.write(completed_process.stderr) - print(completed_process.stderr) + print(completed_process.stderr) if completed_process.returncode != 0: err_msg = f'See {self.workdir / "stderr.txt"} for traceback.' @@ -770,13 +768,13 @@ def submit_sh_array( job_ids = None return job_ids - def submit_slurm_array( + def submit_slurm_array( # pylint: disable=too-many-locals,too-many-branches self, array_max=None, dependency: Union[List[str],None] = None, group_size: int = 1, debug: bool = False, - **kwargs, + **_kwargs, ) -> Union[None,List[int]]: ''' Submits a job array if all the jobs have the same env and use slurm @@ -844,7 +842,7 @@ def submit_slurm_array( if completed_process.stderr is not None: with paropen('stderr.txt', 'a+', encoding='utf-8') as err_file: - err_file.write(completed_process.stderr) + err_file.write(completed_process.stderr) if completed_process.returncode != 0: err_msg = f'See {self.workdir / "stderr.txt"} for traceback.' @@ -852,8 +850,7 @@ def submit_slurm_array( completed_process.check_returncode() if debug: - # logging.error('STDOUT:'+f'{completed_process.stdout}') - logging.error('STDERR:'+f'{completed_process.stderr}') + logging.error('STDERR: %s', completed_process.stderr) job_ids = None else: job_ids = [int(completed_process.stdout.split(' ')[-1])] @@ -912,14 +909,16 @@ def get_last_output(self) -> Dict: ''' Returns the output of the last job in the chain ''' return self.unitjobs[-1].get_output() - def submit(self, dependency: Union[List,None] = None, debug: bool = False) -> List: + def submit( # pylint: disable=too-many-locals,too-many-branches,too-many-statements,too-many-nested-blocks + self, dependency: Union[List,None] = None, debug: bool = False) -> List: ''' Submit a job using slurm, interactively or in the terminal ''' cur_dir = Path('.').resolve() os.chdir(self.workdir) logger = self.get_logger() - step = 0 #self.get_current_step() TODO: This feature is not used yet + step = 0 # self.get_current_step() TODO: This feature is not used yet + status = 'unknown' are_interactive_jobs = [ uj.env['mode'].get('interactive', False) \ for uj in self.unitjobs @@ -966,7 +965,7 @@ def submit(self, dependency: Union[List,None] = None, debug: bool = False) -> Li curjob.env['slurm']['flags']['-J'] = \ f'step-{step+i}' - # submit the next job dependent on the current one + # submit the next job dependent on the current one # Previous working solution write_image = False # Write image first step in chain being run/continued @@ -990,16 +989,12 @@ def submit(self, dependency: Union[List,None] = None, debug: bool = False) -> Li job_ids = dependency # Otherwise just submit them one after the other - # We only write the image if it's the first job, otherwise we refer to + # We only write the image if it's the first job, otherwise we refer to # wherever the image comes from in case it has to come from a previous # step else: for i, unitjob in enumerate(self.unitjobs[step:]): - if i == 0: - write_image = True - else: - write_image = False - + write_image = i == 0 job_ids = unitjob.submit(write_image=write_image) os.chdir(cur_dir) @@ -1103,21 +1098,20 @@ def load_job_tree( return job_dict def check_job_tree_complete(job_tree: Dict, skip_failed: bool=False) -> bool: + ''' Recursively check if all jobs in a job tree are complete ''' if job_tree['subjobs'] is None: status = job_tree['job'].get_status()[1] - if status == 'complete' or status =='discard' or skip_failed: + if status == 'complete' or status == 'discard' or skip_failed: return True, status - else: + return False, status + complete_so_far = True + for subjob_id in job_tree['subjobs']: + subjob = job_tree['subjobs'][subjob_id] + complete, status = check_job_tree_complete( + subjob, + skip_failed=skip_failed + ) + complete_so_far = complete_so_far and complete + if not complete_so_far: return False, status - else: - complete_so_far = True - for subjob_id in job_tree['subjobs']: - subjob = job_tree['subjobs'][subjob_id] - complete, status = check_job_tree_complete( - subjob, - skip_failed=skip_failed - ) - complete_so_far = complete_so_far and complete - if not complete_so_far: - return False, status - return True, status + return True, status From 6037d3973f7e9503e15eb3b70bd4c48cc3f16535 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Sat, 4 Apr 2026 14:26:51 -0700 Subject: [PATCH 39/78] src refactor --- asimtools/__init__.py | 1 - pyproject.toml | 4 +- src/asimtools/__init__.py | 39 +++++++++++++++++++ {asimtools => src/asimtools}/_version.py | 0 .../asimtools}/asimmodules/__init__.py | 0 .../asimmodules/active_learning/__init__.py | 0 .../asimmodules/active_learning/ase_md.py | 0 .../active_learning/compute_deviation.py | 0 .../active_learning/direct_sample.py | 0 .../active_learning/select_images.py | 0 .../asimtools}/asimmodules/ase_md/ase_md.py | 0 .../asimmodules/benchmarking/__init__.py | 0 .../asimmodules/benchmarking/distribution.py | 0 .../asimmodules/benchmarking/parity.py | 0 .../asimtools}/asimmodules/data/__init__.py | 0 .../asimmodules/data/collect_images.py | 0 .../asimtools}/asimmodules/do_nothing.py | 0 .../asimmodules/elastic_constants/__init__.py | 0 .../cubic_energy_expansion.py | 0 .../asimtools}/asimmodules/eos/__init__.py | 0 .../asimtools}/asimmodules/eos/postprocess.py | 0 .../geometry_optimization/__init__.py | 0 .../ase_cubic_eos_optimization.py | 0 .../geometry_optimization/atom_relax.py | 0 .../geometry_optimization/cell_relax.py | 0 .../geometry_optimization/optimize.py | 0 .../symmetric_cell_relax.py | 0 .../asimtools}/asimmodules/lammps/__init__.py | 0 .../asimtools}/asimmodules/lammps/lammps.py | 0 .../asimtools}/asimmodules/lammps/utils.py | 0 .../asimtools}/asimmodules/mace/train_mace.py | 0 .../asimmodules/phonons/__init__.py | 0 .../asimmodules/phonons/ase_phonons.py | 0 .../asimmodules/phonopy/__init__.py | 0 .../asimtools}/asimmodules/phonopy/forces.py | 0 .../asimmodules/phonopy/full_qha.py | 0 .../phonopy/generate_phonopy_displacements.py | 0 .../phonopy/phonon_bands_and_dos.py | 0 .../phonon_bands_and_dos_from_forces.py | 0 .../asimmodules/phonopy/qha_properties.py | 0 .../phonopy/read_force_constants.py | 0 .../asimmodules/phonopy/thermal_properties.py | 0 .../asimtools}/asimmodules/singlepoint.py | 0 .../asimmodules/surface_energies/__init__.py | 0 .../surface_energies/surface_energies.py | 0 .../asimmodules/transformations/__init__.py | 0 .../transformations/delete_atoms.py | 0 .../transformations/scale_unit_cells.py | 0 .../vacancy_formation_energy/__init__.py | 0 .../vacancy_formation_energy.py | 0 .../asimtools}/asimmodules/vasp/__init__.py | 0 .../asimtools}/asimmodules/vasp/vasp.py | 0 .../asimtools}/asimmodules/vasp/vasp.py.bak | 0 .../asimmodules/workflows/__init__.py | 0 .../asimmodules/workflows/calc_array.py | 4 +- .../asimmodules/workflows/chained.py | 0 .../asimmodules/workflows/distributed.py | 0 .../asimmodules/workflows/image_array.py | 4 +- .../asimmodules/workflows/iterative.py | 6 +-- .../asimmodules/workflows/sim_array.py | 4 +- .../workflows/update_dependencies.py | 0 .../asimtools}/asimmodules/workflows/utils.py | 0 {asimtools => src/asimtools}/calculators.py | 0 {asimtools => src/asimtools}/job.py | 0 .../asimtools}/scripts/__init__.py | 0 .../asimtools}/scripts/asim_check.py | 0 .../asimtools}/scripts/asim_execute.py | 0 .../asimtools}/scripts/asim_run.py | 0 {asimtools => src/asimtools}/utils.py | 0 69 files changed, 50 insertions(+), 12 deletions(-) delete mode 100644 asimtools/__init__.py create mode 100644 src/asimtools/__init__.py rename {asimtools => src/asimtools}/_version.py (100%) rename {asimtools => src/asimtools}/asimmodules/__init__.py (100%) rename {asimtools => src/asimtools}/asimmodules/active_learning/__init__.py (100%) rename {asimtools => src/asimtools}/asimmodules/active_learning/ase_md.py (100%) rename {asimtools => src/asimtools}/asimmodules/active_learning/compute_deviation.py (100%) rename {asimtools => src/asimtools}/asimmodules/active_learning/direct_sample.py (100%) rename {asimtools => src/asimtools}/asimmodules/active_learning/select_images.py (100%) rename {asimtools => src/asimtools}/asimmodules/ase_md/ase_md.py (100%) rename {asimtools => src/asimtools}/asimmodules/benchmarking/__init__.py (100%) rename {asimtools => src/asimtools}/asimmodules/benchmarking/distribution.py (100%) rename {asimtools => src/asimtools}/asimmodules/benchmarking/parity.py (100%) rename {asimtools => src/asimtools}/asimmodules/data/__init__.py (100%) rename {asimtools => src/asimtools}/asimmodules/data/collect_images.py (100%) rename {asimtools => src/asimtools}/asimmodules/do_nothing.py (100%) rename {asimtools => src/asimtools}/asimmodules/elastic_constants/__init__.py (100%) rename {asimtools => src/asimtools}/asimmodules/elastic_constants/cubic_energy_expansion.py (100%) rename {asimtools => src/asimtools}/asimmodules/eos/__init__.py (100%) rename {asimtools => src/asimtools}/asimmodules/eos/postprocess.py (100%) rename {asimtools => src/asimtools}/asimmodules/geometry_optimization/__init__.py (100%) rename {asimtools => src/asimtools}/asimmodules/geometry_optimization/ase_cubic_eos_optimization.py (100%) rename {asimtools => src/asimtools}/asimmodules/geometry_optimization/atom_relax.py (100%) rename {asimtools => src/asimtools}/asimmodules/geometry_optimization/cell_relax.py (100%) rename {asimtools => src/asimtools}/asimmodules/geometry_optimization/optimize.py (100%) rename {asimtools => src/asimtools}/asimmodules/geometry_optimization/symmetric_cell_relax.py (100%) rename {asimtools => src/asimtools}/asimmodules/lammps/__init__.py (100%) rename {asimtools => src/asimtools}/asimmodules/lammps/lammps.py (100%) rename {asimtools => src/asimtools}/asimmodules/lammps/utils.py (100%) rename {asimtools => src/asimtools}/asimmodules/mace/train_mace.py (100%) rename {asimtools => src/asimtools}/asimmodules/phonons/__init__.py (100%) rename {asimtools => src/asimtools}/asimmodules/phonons/ase_phonons.py (100%) rename {asimtools => src/asimtools}/asimmodules/phonopy/__init__.py (100%) rename {asimtools => src/asimtools}/asimmodules/phonopy/forces.py (100%) rename {asimtools => src/asimtools}/asimmodules/phonopy/full_qha.py (100%) rename {asimtools => src/asimtools}/asimmodules/phonopy/generate_phonopy_displacements.py (100%) rename {asimtools => src/asimtools}/asimmodules/phonopy/phonon_bands_and_dos.py (100%) rename {asimtools => src/asimtools}/asimmodules/phonopy/phonon_bands_and_dos_from_forces.py (100%) rename {asimtools => src/asimtools}/asimmodules/phonopy/qha_properties.py (100%) rename {asimtools => src/asimtools}/asimmodules/phonopy/read_force_constants.py (100%) rename {asimtools => src/asimtools}/asimmodules/phonopy/thermal_properties.py (100%) rename {asimtools => src/asimtools}/asimmodules/singlepoint.py (100%) rename {asimtools => src/asimtools}/asimmodules/surface_energies/__init__.py (100%) rename {asimtools => src/asimtools}/asimmodules/surface_energies/surface_energies.py (100%) rename {asimtools => src/asimtools}/asimmodules/transformations/__init__.py (100%) rename {asimtools => src/asimtools}/asimmodules/transformations/delete_atoms.py (100%) rename {asimtools => src/asimtools}/asimmodules/transformations/scale_unit_cells.py (100%) rename {asimtools => src/asimtools}/asimmodules/vacancy_formation_energy/__init__.py (100%) rename {asimtools => src/asimtools}/asimmodules/vacancy_formation_energy/vacancy_formation_energy.py (100%) rename {asimtools => src/asimtools}/asimmodules/vasp/__init__.py (100%) rename {asimtools => src/asimtools}/asimmodules/vasp/vasp.py (100%) rename {asimtools => src/asimtools}/asimmodules/vasp/vasp.py.bak (100%) rename {asimtools => src/asimtools}/asimmodules/workflows/__init__.py (100%) rename {asimtools => src/asimtools}/asimmodules/workflows/calc_array.py (98%) rename {asimtools => src/asimtools}/asimmodules/workflows/chained.py (100%) rename {asimtools => src/asimtools}/asimmodules/workflows/distributed.py (100%) rename {asimtools => src/asimtools}/asimmodules/workflows/image_array.py (98%) rename {asimtools => src/asimtools}/asimmodules/workflows/iterative.py (98%) rename {asimtools => src/asimtools}/asimmodules/workflows/sim_array.py (98%) rename {asimtools => src/asimtools}/asimmodules/workflows/update_dependencies.py (100%) rename {asimtools => src/asimtools}/asimmodules/workflows/utils.py (100%) rename {asimtools => src/asimtools}/calculators.py (100%) rename {asimtools => src/asimtools}/job.py (100%) rename {asimtools => src/asimtools}/scripts/__init__.py (100%) rename {asimtools => src/asimtools}/scripts/asim_check.py (100%) rename {asimtools => src/asimtools}/scripts/asim_execute.py (100%) rename {asimtools => src/asimtools}/scripts/asim_run.py (100%) rename {asimtools => src/asimtools}/utils.py (100%) diff --git a/asimtools/__init__.py b/asimtools/__init__.py deleted file mode 100644 index 8dee4bf..0000000 --- a/asimtools/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from ._version import __version__ diff --git a/pyproject.toml b/pyproject.toml index 9344421..4c27c13 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,10 +38,10 @@ dependencies = [ ] [tool.setuptools.packages.find] +where = ["src"] include = [ "asimtools", "asimtools.*", - "asimtools.scripts.*.*", ] [project.optional-dependencies] @@ -69,5 +69,5 @@ documentation = "https://battmodels.github.io/asimtools/" [tool.pytest.ini_options] log_cli_level = "warn" -pythonpath = "asimtools" +pythonpath = ["src"] testpaths = ["tests"] diff --git a/src/asimtools/__init__.py b/src/asimtools/__init__.py new file mode 100644 index 0000000..3826359 --- /dev/null +++ b/src/asimtools/__init__.py @@ -0,0 +1,39 @@ +from asimtools._version import __version__ +from asimtools.job import Job, UnitJob, DistributedJob, ChainedJob +from asimtools.calculators import load_calc +from asimtools.utils import ( + get_atoms, + get_images, + read_yaml, + write_yaml, + write_atoms, + repeat_to_n, + get_str_btn, + join_names, + change_dict_value, + change_dict_values, + expand_wildcards, +) + +__all__ = [ + "__version__", + # Job classes + "Job", + "UnitJob", + "DistributedJob", + "ChainedJob", + # Calculators + "load_calc", + # Utilities + "get_atoms", + "get_images", + "read_yaml", + "write_yaml", + "write_atoms", + "repeat_to_n", + "get_str_btn", + "join_names", + "change_dict_value", + "change_dict_values", + "expand_wildcards", +] diff --git a/asimtools/_version.py b/src/asimtools/_version.py similarity index 100% rename from asimtools/_version.py rename to src/asimtools/_version.py diff --git a/asimtools/asimmodules/__init__.py b/src/asimtools/asimmodules/__init__.py similarity index 100% rename from asimtools/asimmodules/__init__.py rename to src/asimtools/asimmodules/__init__.py diff --git a/asimtools/asimmodules/active_learning/__init__.py b/src/asimtools/asimmodules/active_learning/__init__.py similarity index 100% rename from asimtools/asimmodules/active_learning/__init__.py rename to src/asimtools/asimmodules/active_learning/__init__.py diff --git a/asimtools/asimmodules/active_learning/ase_md.py b/src/asimtools/asimmodules/active_learning/ase_md.py similarity index 100% rename from asimtools/asimmodules/active_learning/ase_md.py rename to src/asimtools/asimmodules/active_learning/ase_md.py diff --git a/asimtools/asimmodules/active_learning/compute_deviation.py b/src/asimtools/asimmodules/active_learning/compute_deviation.py similarity index 100% rename from asimtools/asimmodules/active_learning/compute_deviation.py rename to src/asimtools/asimmodules/active_learning/compute_deviation.py diff --git a/asimtools/asimmodules/active_learning/direct_sample.py b/src/asimtools/asimmodules/active_learning/direct_sample.py similarity index 100% rename from asimtools/asimmodules/active_learning/direct_sample.py rename to src/asimtools/asimmodules/active_learning/direct_sample.py diff --git a/asimtools/asimmodules/active_learning/select_images.py b/src/asimtools/asimmodules/active_learning/select_images.py similarity index 100% rename from asimtools/asimmodules/active_learning/select_images.py rename to src/asimtools/asimmodules/active_learning/select_images.py diff --git a/asimtools/asimmodules/ase_md/ase_md.py b/src/asimtools/asimmodules/ase_md/ase_md.py similarity index 100% rename from asimtools/asimmodules/ase_md/ase_md.py rename to src/asimtools/asimmodules/ase_md/ase_md.py diff --git a/asimtools/asimmodules/benchmarking/__init__.py b/src/asimtools/asimmodules/benchmarking/__init__.py similarity index 100% rename from asimtools/asimmodules/benchmarking/__init__.py rename to src/asimtools/asimmodules/benchmarking/__init__.py diff --git a/asimtools/asimmodules/benchmarking/distribution.py b/src/asimtools/asimmodules/benchmarking/distribution.py similarity index 100% rename from asimtools/asimmodules/benchmarking/distribution.py rename to src/asimtools/asimmodules/benchmarking/distribution.py diff --git a/asimtools/asimmodules/benchmarking/parity.py b/src/asimtools/asimmodules/benchmarking/parity.py similarity index 100% rename from asimtools/asimmodules/benchmarking/parity.py rename to src/asimtools/asimmodules/benchmarking/parity.py diff --git a/asimtools/asimmodules/data/__init__.py b/src/asimtools/asimmodules/data/__init__.py similarity index 100% rename from asimtools/asimmodules/data/__init__.py rename to src/asimtools/asimmodules/data/__init__.py diff --git a/asimtools/asimmodules/data/collect_images.py b/src/asimtools/asimmodules/data/collect_images.py similarity index 100% rename from asimtools/asimmodules/data/collect_images.py rename to src/asimtools/asimmodules/data/collect_images.py diff --git a/asimtools/asimmodules/do_nothing.py b/src/asimtools/asimmodules/do_nothing.py similarity index 100% rename from asimtools/asimmodules/do_nothing.py rename to src/asimtools/asimmodules/do_nothing.py diff --git a/asimtools/asimmodules/elastic_constants/__init__.py b/src/asimtools/asimmodules/elastic_constants/__init__.py similarity index 100% rename from asimtools/asimmodules/elastic_constants/__init__.py rename to src/asimtools/asimmodules/elastic_constants/__init__.py diff --git a/asimtools/asimmodules/elastic_constants/cubic_energy_expansion.py b/src/asimtools/asimmodules/elastic_constants/cubic_energy_expansion.py similarity index 100% rename from asimtools/asimmodules/elastic_constants/cubic_energy_expansion.py rename to src/asimtools/asimmodules/elastic_constants/cubic_energy_expansion.py diff --git a/asimtools/asimmodules/eos/__init__.py b/src/asimtools/asimmodules/eos/__init__.py similarity index 100% rename from asimtools/asimmodules/eos/__init__.py rename to src/asimtools/asimmodules/eos/__init__.py diff --git a/asimtools/asimmodules/eos/postprocess.py b/src/asimtools/asimmodules/eos/postprocess.py similarity index 100% rename from asimtools/asimmodules/eos/postprocess.py rename to src/asimtools/asimmodules/eos/postprocess.py diff --git a/asimtools/asimmodules/geometry_optimization/__init__.py b/src/asimtools/asimmodules/geometry_optimization/__init__.py similarity index 100% rename from asimtools/asimmodules/geometry_optimization/__init__.py rename to src/asimtools/asimmodules/geometry_optimization/__init__.py diff --git a/asimtools/asimmodules/geometry_optimization/ase_cubic_eos_optimization.py b/src/asimtools/asimmodules/geometry_optimization/ase_cubic_eos_optimization.py similarity index 100% rename from asimtools/asimmodules/geometry_optimization/ase_cubic_eos_optimization.py rename to src/asimtools/asimmodules/geometry_optimization/ase_cubic_eos_optimization.py diff --git a/asimtools/asimmodules/geometry_optimization/atom_relax.py b/src/asimtools/asimmodules/geometry_optimization/atom_relax.py similarity index 100% rename from asimtools/asimmodules/geometry_optimization/atom_relax.py rename to src/asimtools/asimmodules/geometry_optimization/atom_relax.py diff --git a/asimtools/asimmodules/geometry_optimization/cell_relax.py b/src/asimtools/asimmodules/geometry_optimization/cell_relax.py similarity index 100% rename from asimtools/asimmodules/geometry_optimization/cell_relax.py rename to src/asimtools/asimmodules/geometry_optimization/cell_relax.py diff --git a/asimtools/asimmodules/geometry_optimization/optimize.py b/src/asimtools/asimmodules/geometry_optimization/optimize.py similarity index 100% rename from asimtools/asimmodules/geometry_optimization/optimize.py rename to src/asimtools/asimmodules/geometry_optimization/optimize.py diff --git a/asimtools/asimmodules/geometry_optimization/symmetric_cell_relax.py b/src/asimtools/asimmodules/geometry_optimization/symmetric_cell_relax.py similarity index 100% rename from asimtools/asimmodules/geometry_optimization/symmetric_cell_relax.py rename to src/asimtools/asimmodules/geometry_optimization/symmetric_cell_relax.py diff --git a/asimtools/asimmodules/lammps/__init__.py b/src/asimtools/asimmodules/lammps/__init__.py similarity index 100% rename from asimtools/asimmodules/lammps/__init__.py rename to src/asimtools/asimmodules/lammps/__init__.py diff --git a/asimtools/asimmodules/lammps/lammps.py b/src/asimtools/asimmodules/lammps/lammps.py similarity index 100% rename from asimtools/asimmodules/lammps/lammps.py rename to src/asimtools/asimmodules/lammps/lammps.py diff --git a/asimtools/asimmodules/lammps/utils.py b/src/asimtools/asimmodules/lammps/utils.py similarity index 100% rename from asimtools/asimmodules/lammps/utils.py rename to src/asimtools/asimmodules/lammps/utils.py diff --git a/asimtools/asimmodules/mace/train_mace.py b/src/asimtools/asimmodules/mace/train_mace.py similarity index 100% rename from asimtools/asimmodules/mace/train_mace.py rename to src/asimtools/asimmodules/mace/train_mace.py diff --git a/asimtools/asimmodules/phonons/__init__.py b/src/asimtools/asimmodules/phonons/__init__.py similarity index 100% rename from asimtools/asimmodules/phonons/__init__.py rename to src/asimtools/asimmodules/phonons/__init__.py diff --git a/asimtools/asimmodules/phonons/ase_phonons.py b/src/asimtools/asimmodules/phonons/ase_phonons.py similarity index 100% rename from asimtools/asimmodules/phonons/ase_phonons.py rename to src/asimtools/asimmodules/phonons/ase_phonons.py diff --git a/asimtools/asimmodules/phonopy/__init__.py b/src/asimtools/asimmodules/phonopy/__init__.py similarity index 100% rename from asimtools/asimmodules/phonopy/__init__.py rename to src/asimtools/asimmodules/phonopy/__init__.py diff --git a/asimtools/asimmodules/phonopy/forces.py b/src/asimtools/asimmodules/phonopy/forces.py similarity index 100% rename from asimtools/asimmodules/phonopy/forces.py rename to src/asimtools/asimmodules/phonopy/forces.py diff --git a/asimtools/asimmodules/phonopy/full_qha.py b/src/asimtools/asimmodules/phonopy/full_qha.py similarity index 100% rename from asimtools/asimmodules/phonopy/full_qha.py rename to src/asimtools/asimmodules/phonopy/full_qha.py diff --git a/asimtools/asimmodules/phonopy/generate_phonopy_displacements.py b/src/asimtools/asimmodules/phonopy/generate_phonopy_displacements.py similarity index 100% rename from asimtools/asimmodules/phonopy/generate_phonopy_displacements.py rename to src/asimtools/asimmodules/phonopy/generate_phonopy_displacements.py diff --git a/asimtools/asimmodules/phonopy/phonon_bands_and_dos.py b/src/asimtools/asimmodules/phonopy/phonon_bands_and_dos.py similarity index 100% rename from asimtools/asimmodules/phonopy/phonon_bands_and_dos.py rename to src/asimtools/asimmodules/phonopy/phonon_bands_and_dos.py diff --git a/asimtools/asimmodules/phonopy/phonon_bands_and_dos_from_forces.py b/src/asimtools/asimmodules/phonopy/phonon_bands_and_dos_from_forces.py similarity index 100% rename from asimtools/asimmodules/phonopy/phonon_bands_and_dos_from_forces.py rename to src/asimtools/asimmodules/phonopy/phonon_bands_and_dos_from_forces.py diff --git a/asimtools/asimmodules/phonopy/qha_properties.py b/src/asimtools/asimmodules/phonopy/qha_properties.py similarity index 100% rename from asimtools/asimmodules/phonopy/qha_properties.py rename to src/asimtools/asimmodules/phonopy/qha_properties.py diff --git a/asimtools/asimmodules/phonopy/read_force_constants.py b/src/asimtools/asimmodules/phonopy/read_force_constants.py similarity index 100% rename from asimtools/asimmodules/phonopy/read_force_constants.py rename to src/asimtools/asimmodules/phonopy/read_force_constants.py diff --git a/asimtools/asimmodules/phonopy/thermal_properties.py b/src/asimtools/asimmodules/phonopy/thermal_properties.py similarity index 100% rename from asimtools/asimmodules/phonopy/thermal_properties.py rename to src/asimtools/asimmodules/phonopy/thermal_properties.py diff --git a/asimtools/asimmodules/singlepoint.py b/src/asimtools/asimmodules/singlepoint.py similarity index 100% rename from asimtools/asimmodules/singlepoint.py rename to src/asimtools/asimmodules/singlepoint.py diff --git a/asimtools/asimmodules/surface_energies/__init__.py b/src/asimtools/asimmodules/surface_energies/__init__.py similarity index 100% rename from asimtools/asimmodules/surface_energies/__init__.py rename to src/asimtools/asimmodules/surface_energies/__init__.py diff --git a/asimtools/asimmodules/surface_energies/surface_energies.py b/src/asimtools/asimmodules/surface_energies/surface_energies.py similarity index 100% rename from asimtools/asimmodules/surface_energies/surface_energies.py rename to src/asimtools/asimmodules/surface_energies/surface_energies.py diff --git a/asimtools/asimmodules/transformations/__init__.py b/src/asimtools/asimmodules/transformations/__init__.py similarity index 100% rename from asimtools/asimmodules/transformations/__init__.py rename to src/asimtools/asimmodules/transformations/__init__.py diff --git a/asimtools/asimmodules/transformations/delete_atoms.py b/src/asimtools/asimmodules/transformations/delete_atoms.py similarity index 100% rename from asimtools/asimmodules/transformations/delete_atoms.py rename to src/asimtools/asimmodules/transformations/delete_atoms.py diff --git a/asimtools/asimmodules/transformations/scale_unit_cells.py b/src/asimtools/asimmodules/transformations/scale_unit_cells.py similarity index 100% rename from asimtools/asimmodules/transformations/scale_unit_cells.py rename to src/asimtools/asimmodules/transformations/scale_unit_cells.py diff --git a/asimtools/asimmodules/vacancy_formation_energy/__init__.py b/src/asimtools/asimmodules/vacancy_formation_energy/__init__.py similarity index 100% rename from asimtools/asimmodules/vacancy_formation_energy/__init__.py rename to src/asimtools/asimmodules/vacancy_formation_energy/__init__.py diff --git a/asimtools/asimmodules/vacancy_formation_energy/vacancy_formation_energy.py b/src/asimtools/asimmodules/vacancy_formation_energy/vacancy_formation_energy.py similarity index 100% rename from asimtools/asimmodules/vacancy_formation_energy/vacancy_formation_energy.py rename to src/asimtools/asimmodules/vacancy_formation_energy/vacancy_formation_energy.py diff --git a/asimtools/asimmodules/vasp/__init__.py b/src/asimtools/asimmodules/vasp/__init__.py similarity index 100% rename from asimtools/asimmodules/vasp/__init__.py rename to src/asimtools/asimmodules/vasp/__init__.py diff --git a/asimtools/asimmodules/vasp/vasp.py b/src/asimtools/asimmodules/vasp/vasp.py similarity index 100% rename from asimtools/asimmodules/vasp/vasp.py rename to src/asimtools/asimmodules/vasp/vasp.py diff --git a/asimtools/asimmodules/vasp/vasp.py.bak b/src/asimtools/asimmodules/vasp/vasp.py.bak similarity index 100% rename from asimtools/asimmodules/vasp/vasp.py.bak rename to src/asimtools/asimmodules/vasp/vasp.py.bak diff --git a/asimtools/asimmodules/workflows/__init__.py b/src/asimtools/asimmodules/workflows/__init__.py similarity index 100% rename from asimtools/asimmodules/workflows/__init__.py rename to src/asimtools/asimmodules/workflows/__init__.py diff --git a/asimtools/asimmodules/workflows/calc_array.py b/src/asimtools/asimmodules/workflows/calc_array.py similarity index 98% rename from asimtools/asimmodules/workflows/calc_array.py rename to src/asimtools/asimmodules/workflows/calc_array.py index 3bf15c2..288025c 100755 --- a/asimtools/asimmodules/workflows/calc_array.py +++ b/src/asimtools/asimmodules/workflows/calc_array.py @@ -126,7 +126,7 @@ def calc_array( for i, val in enumerate(array_values): new_calc_params = change_dict_value( - d=calc_params, + dct=calc_params, new_value=val, key_sequence=key_sequence, return_copy=True, @@ -134,7 +134,7 @@ def calc_array( if secondary_array_values is not None: for k, vs in zip(secondary_key_sequences, secondary_array_values): new_calc_params = change_dict_value( - d=new_calc_params, + dct=new_calc_params, new_value=vs[i], key_sequence=k, return_copy=False, diff --git a/asimtools/asimmodules/workflows/chained.py b/src/asimtools/asimmodules/workflows/chained.py similarity index 100% rename from asimtools/asimmodules/workflows/chained.py rename to src/asimtools/asimmodules/workflows/chained.py diff --git a/asimtools/asimmodules/workflows/distributed.py b/src/asimtools/asimmodules/workflows/distributed.py similarity index 100% rename from asimtools/asimmodules/workflows/distributed.py rename to src/asimtools/asimmodules/workflows/distributed.py diff --git a/asimtools/asimmodules/workflows/image_array.py b/src/asimtools/asimmodules/workflows/image_array.py similarity index 98% rename from asimtools/asimmodules/workflows/image_array.py rename to src/asimtools/asimmodules/workflows/image_array.py index 9489d4d..16c6e6d 100755 --- a/asimtools/asimmodules/workflows/image_array.py +++ b/src/asimtools/asimmodules/workflows/image_array.py @@ -109,7 +109,7 @@ def image_array( array_sim_input = {} for i, val in enumerate(array_values): new_sim_input = change_dict_value( - d=subsim_input, + dct=subsim_input, new_value=val, key_sequence=key_sequence, return_copy=True, @@ -118,7 +118,7 @@ def image_array( if secondary_array_values is not None: for k, vs in zip(secondary_key_sequences, secondary_array_values): new_sim_input = change_dict_value( - d=new_sim_input, + dct=new_sim_input, new_value=vs[i], key_sequence=k, return_copy=False, diff --git a/asimtools/asimmodules/workflows/iterative.py b/src/asimtools/asimmodules/workflows/iterative.py similarity index 98% rename from asimtools/asimmodules/workflows/iterative.py rename to src/asimtools/asimmodules/workflows/iterative.py index 286c53e..388292c 100755 --- a/asimtools/asimmodules/workflows/iterative.py +++ b/src/asimtools/asimmodules/workflows/iterative.py @@ -104,7 +104,7 @@ def iterative( for i, val in enumerate(array_values): if key_sequence is not None: new_sim_input = change_dict_value( - d=template_sim_input, + dct=template_sim_input, new_value=val, key_sequence=key_sequence, return_copy=True, @@ -115,7 +115,7 @@ def iterative( if dependent_file_key_sequence is not None and i > 0: dep_arg = str(Path(f'../step-{i-1}') / dependent_file) new_sim_input = change_dict_value( - d=new_sim_input, + dct=new_sim_input, new_value=dep_arg, key_sequence=dependent_file_key_sequence, return_copy=False, @@ -126,7 +126,7 @@ def iterative( if secondary_array_values is not None: for k, vs in zip(secondary_key_sequences, secondary_array_values): new_sim_input = change_dict_value( - d=new_sim_input, + dct=new_sim_input, new_value=vs[i], key_sequence=k, return_copy=False, diff --git a/asimtools/asimmodules/workflows/sim_array.py b/src/asimtools/asimmodules/workflows/sim_array.py similarity index 98% rename from asimtools/asimmodules/workflows/sim_array.py rename to src/asimtools/asimmodules/workflows/sim_array.py index e3346c3..b64d368 100755 --- a/asimtools/asimmodules/workflows/sim_array.py +++ b/src/asimtools/asimmodules/workflows/sim_array.py @@ -123,7 +123,7 @@ def sim_array( for i, val in enumerate(array_values): if key_sequence is not None: new_sim_input = change_dict_value( - d=template_sim_input, + dct=template_sim_input, new_value=val, key_sequence=key_sequence, return_copy=True, @@ -141,7 +141,7 @@ def sim_array( else: secondary_placeholder = None new_sim_input = change_dict_value( - d=new_sim_input, + dct=new_sim_input, new_value=vs[i], key_sequence=k, return_copy=False, diff --git a/asimtools/asimmodules/workflows/update_dependencies.py b/src/asimtools/asimmodules/workflows/update_dependencies.py similarity index 100% rename from asimtools/asimmodules/workflows/update_dependencies.py rename to src/asimtools/asimmodules/workflows/update_dependencies.py diff --git a/asimtools/asimmodules/workflows/utils.py b/src/asimtools/asimmodules/workflows/utils.py similarity index 100% rename from asimtools/asimmodules/workflows/utils.py rename to src/asimtools/asimmodules/workflows/utils.py diff --git a/asimtools/calculators.py b/src/asimtools/calculators.py similarity index 100% rename from asimtools/calculators.py rename to src/asimtools/calculators.py diff --git a/asimtools/job.py b/src/asimtools/job.py similarity index 100% rename from asimtools/job.py rename to src/asimtools/job.py diff --git a/asimtools/scripts/__init__.py b/src/asimtools/scripts/__init__.py similarity index 100% rename from asimtools/scripts/__init__.py rename to src/asimtools/scripts/__init__.py diff --git a/asimtools/scripts/asim_check.py b/src/asimtools/scripts/asim_check.py similarity index 100% rename from asimtools/scripts/asim_check.py rename to src/asimtools/scripts/asim_check.py diff --git a/asimtools/scripts/asim_execute.py b/src/asimtools/scripts/asim_execute.py similarity index 100% rename from asimtools/scripts/asim_execute.py rename to src/asimtools/scripts/asim_execute.py diff --git a/asimtools/scripts/asim_run.py b/src/asimtools/scripts/asim_run.py similarity index 100% rename from asimtools/scripts/asim_run.py rename to src/asimtools/scripts/asim_run.py diff --git a/asimtools/utils.py b/src/asimtools/utils.py similarity index 100% rename from asimtools/utils.py rename to src/asimtools/utils.py From 37bcf3db452043e4fdfea73acf59c4434e9bedaf Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Sat, 4 Apr 2026 15:02:38 -0700 Subject: [PATCH 40/78] precommit config --- .pre-commit-config.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..642c344 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,18 @@ +repos: + - repo: local + hooks: + - id: pylint + name: pylint + entry: conda run -n asimtools-dev pylint + language: system + types: [python] + files: ^src/asimtools/ + exclude: ^src/asimtools/asimmodules/ + args: [--rcfile=.pylintrc] + + - id: pytest + name: pytest + entry: conda run -n asimtools-dev pytest tests/unit/ + language: system + pass_filenames: false + always_run: true From f9bd21104f373a742c215fb53bc38fb69b395e6c Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Sat, 4 Apr 2026 15:03:55 -0700 Subject: [PATCH 41/78] general linting --- src/asimtools/__init__.py | 1 + src/asimtools/_version.py | 1 + src/asimtools/scripts/asim_check.py | 2 +- src/asimtools/scripts/asim_run.py | 14 +++++++------- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/asimtools/__init__.py b/src/asimtools/__init__.py index 3826359..6bb430d 100644 --- a/src/asimtools/__init__.py +++ b/src/asimtools/__init__.py @@ -1,3 +1,4 @@ +'''asimtools: lightweight atomic simulation workflow manager.''' from asimtools._version import __version__ from asimtools.job import Job, UnitJob, DistributedJob, ChainedJob from asimtools.calculators import load_calc diff --git a/src/asimtools/_version.py b/src/asimtools/_version.py index 9167125..7e210bc 100644 --- a/src/asimtools/_version.py +++ b/src/asimtools/_version.py @@ -1,4 +1,5 @@ # See Python packaging guide # https://packaging.python.org/guides/single-sourcing-package-version/ +'''Package version string.''' __version__ = "0.0.2" diff --git a/src/asimtools/scripts/asim_check.py b/src/asimtools/scripts/asim_check.py index 9d6b3c2..e87d2fb 100755 --- a/src/asimtools/scripts/asim_check.py +++ b/src/asimtools/scripts/asim_check.py @@ -41,7 +41,7 @@ def main(args=None) -> None: :param args: cmdline args, defaults to None :type args: _type_, optional - """ + """ sim_input, rootdir, max_level = parse_command_line(args) workdir = sim_input.get('workdir', 'results') if not workdir.startswith('/'): diff --git a/src/asimtools/scripts/asim_run.py b/src/asimtools/scripts/asim_run.py index ea2521a..c5aa552 100755 --- a/src/asimtools/scripts/asim_run.py +++ b/src/asimtools/scripts/asim_run.py @@ -51,7 +51,7 @@ def parse_command_line(args) -> Tuple[Dict, str]: return sim_input, calc_input_file -def main(args=None) -> None: +def main(args=None) -> None: # pylint: disable=too-many-locals,too-many-branches,too-many-statements ''' Main ''' sim_input, calc_input_file = parse_command_line(args) @@ -123,12 +123,12 @@ def main(args=None) -> None: try: spec.loader.exec_module(sim_module) except Exception as exc: - t = f'Failed to load asimmodule "{asimmodule}". Possible causes:\n' - t += '* The asimmodule has a bug, see traceback above\n' - t += '* ASIMTOOLS_ASIMMODULE_DIR variable not set properly.\n' - t += '* You can provide the full path to the asimmodule' - logger.error(t) - raise FileNotFoundError(t) from exc + txt = f'Failed to load asimmodule "{asimmodule}". Possible causes:\n' + txt += '* The asimmodule has a bug, see traceback above\n' + txt += '* ASIMTOOLS_ASIMMODULE_DIR variable not set properly.\n' + txt += '* You can provide the full path to the asimmodule' + logger.error(txt) + raise FileNotFoundError(txt) from exc sim_func = getattr(sim_module, func_name) From a30aab7000dfa9073c7d2774e913bd4acffc96c8 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Sat, 4 Apr 2026 15:04:53 -0700 Subject: [PATCH 42/78] add more utils tests --- tests/unit/test_utils.py | 114 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 113 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 15138ca..67c7f35 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -2,9 +2,14 @@ Tests for utils.py ''' from pathlib import Path +from unittest.mock import patch, MagicMock import os import pytest import numpy as np +import matplotlib +matplotlib.use('Agg') +import matplotlib.pyplot as plt +import pandas as pd from ase.io import read from ase.calculators.emt import EMT from pymatgen.core import Structure, Molecule, IStructure, IMolecule @@ -25,6 +30,12 @@ expand_wildcards, write_atoms, repeat_to_n, + strip_symbols, + get_axis_lims, + improve_plot, + write_csv_from_dict, + new_db, + check_if_slurm_job_is_running, ) import ase.build @@ -387,4 +398,105 @@ def test_repeat_to_n(): assert np.abs(repeated_atoms.get_cell()[2][2] - 1*2.0) < 1e-6 assert len(repeat_to_n(atoms, 15)) == 16 with pytest.raises(ValueError): - repeat_to_n(atoms, 16, max_dim=4) \ No newline at end of file + repeat_to_n(atoms, 16, max_dim=4) + + +@pytest.mark.parametrize("test_input, expected", [ + ('hello', 'hello'), + ('_hello_', 'hello'), + ('-hello-', 'hello'), + ('.hello.', 'hello'), + (' hello ', 'hello'), + ('_-hello-_', 'hello'), + ('', ''), +]) +def test_strip_symbols(test_input, expected): + ''' Test stripping leading/trailing bad symbols from a string ''' + assert strip_symbols(test_input) == expected + + +@pytest.mark.parametrize("x, y, padding, expected_min, expected_max", [ + ([0, 1], [0, 1], 0.1, -0.1, 1.1), + ([0, 2], [1, 3], 0.0, 0.0, 3.0), + ([-1, 1], [-1, 1], 0.5, -2.0, 2.0), +]) +def test_get_axis_lims(x, y, padding, expected_min, expected_max): + ''' Test axis limit computation with padding ''' + lims = get_axis_lims(x, y, padding=padding) + assert np.isclose(lims[0], expected_min) + assert np.isclose(lims[1], expected_max) + + +def test_improve_plot(): + ''' Test that improve_plot sets label and tick fontsizes ''' + fig, ax = plt.subplots() + ax.set_xlabel('X') + ax.set_ylabel('Y') + ax.set_title('Title') + fontsize = 16 + improve_plot(ax=ax, fontsize=fontsize) + assert ax.xaxis.label.get_fontsize() == fontsize + 2 + assert ax.yaxis.label.get_fontsize() == fontsize + 2 + assert ax.title.get_fontsize() == fontsize + 4 + plt.close(fig) + + +def test_improve_plot_with_legend(): + ''' Test that improve_plot handles legends ''' + fig, ax = plt.subplots() + ax.plot([0, 1], [0, 1], label='line') + ax.legend(title='legend_title') + improve_plot(ax=ax, fontsize=12) + leg = ax.get_legend() + assert leg is not None + plt.close(fig) + + +@pytest.mark.parametrize("data, columns, header", [ + ({'a': [1, 2, 3], 'b': [4, 5, 6]}, None, ''), + ({'a': [1, 2], 'b': [3, 4], 'c': [5, 6]}, ['a', 'c'], 'my header'), +]) +def test_write_csv_from_dict(data, columns, header, tmp_path): + ''' Test writing a dict to CSV and reading it back ''' + fpath = tmp_path / 'test.csv' + result_df = write_csv_from_dict(fpath, data, columns=columns, header=header) + + used_columns = columns if columns is not None else list(data.keys()) + assert list(result_df.columns) == used_columns + + read_df = pd.read_csv(fpath, comment='#') + assert list(read_df.columns) == used_columns + for col in used_columns: + assert list(read_df[col]) == data[col] + + if header: + with open(fpath, 'r', encoding='utf-8') as f: + first_line = f.readline() + assert header in first_line + + +def test_new_db(tmp_path): + ''' Test creating a new ASE database ''' + import ase.db + dbpath = tmp_path / 'test.db' + db = new_db(str(dbpath)) + assert dbpath.exists() + atoms = ase.build.bulk('Cu') + db.write(atoms) + assert db.count() == 1 + + +def test_check_if_slurm_job_is_running_true(): + ''' Test that a job ID found in squeue stdout returns True ''' + mock_result = MagicMock() + mock_result.stdout = ' 12345 some other text' + with patch('asimtools.utils.subprocess.run', return_value=mock_result): + assert check_if_slurm_job_is_running(12345) is True + + +def test_check_if_slurm_job_is_running_false(): + ''' Test that a job ID not in squeue stdout returns False ''' + mock_result = MagicMock() + mock_result.stdout = ' 99999 some other text' + with patch('asimtools.utils.subprocess.run', return_value=mock_result): + assert check_if_slurm_job_is_running(12345) is False \ No newline at end of file From a4effe7a2e58d68513631f59e45ba77ac65bca0a Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Sat, 4 Apr 2026 16:12:14 -0700 Subject: [PATCH 43/78] job.py tests --- tests/unit/test_job.py | 259 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 255 insertions(+), 4 deletions(-) diff --git a/tests/unit/test_job.py b/tests/unit/test_job.py index 7691699..e6e0be4 100644 --- a/tests/unit/test_job.py +++ b/tests/unit/test_job.py @@ -4,10 +4,20 @@ #pylint: disable=missing-function-docstring #pylint: disable=redefined-outer-name +import logging +import os from pathlib import Path import pytest -from asimtools.job import UnitJob, check_job_tree_complete -from asimtools.utils import read_yaml +from asimtools.job import ( + UnitJob, + DistributedJob, + ChainedJob, + check_job_tree_complete, + load_job_from_directory, + get_subjobs, + load_job_tree, +) +from asimtools.utils import read_yaml, write_yaml def create_unitjob(sim_input, env_input, workdir, calc_input=None, status=None): """Helper for making a generic UnitJob object""" @@ -427,5 +437,246 @@ def test_check_job_tree_complete(tmp_path, test_input, expected): ''' Test check_job_tree_complete ''' assert check_job_tree_complete(test_input) == expected - assert check_job_tree_complete(test_input, skip_failed=True)[0] == True - \ No newline at end of file + assert check_job_tree_complete(test_input, skip_failed=True)[0] is True + + +# --------------------------------------------------------------------------- +# Job getter / updater methods +# --------------------------------------------------------------------------- + +def test_get_sim_input_is_deepcopy(inline_env_input, do_nothing_sim_input, tmp_path): + unitjob = create_unitjob(do_nothing_sim_input, inline_env_input, tmp_path / 'wdir') + sim_input = unitjob.get_sim_input() + sim_input['asimmodule'] = 'mutated' + assert unitjob.sim_input['asimmodule'] != 'mutated' + + +def test_get_calc_input_is_deepcopy(inline_env_input, do_nothing_sim_input, lj_argon_calc_input, tmp_path): + unitjob = create_unitjob( + do_nothing_sim_input, inline_env_input, tmp_path / 'wdir', calc_input=lj_argon_calc_input + ) + calc_input = unitjob.get_calc_input() + calc_id = list(lj_argon_calc_input.keys())[0] + calc_input[calc_id]['name'] = 'mutated' + assert unitjob.calc_input[calc_id]['name'] != 'mutated' + + +def test_get_env_input_is_deepcopy(inline_env_input, do_nothing_sim_input, tmp_path): + unitjob = create_unitjob(do_nothing_sim_input, inline_env_input, tmp_path / 'wdir') + env_input = unitjob.get_env_input() + env_id = list(inline_env_input.keys())[0] + env_input[env_id]['mode']['use_slurm'] = True + assert unitjob.env_input[env_id]['mode']['use_slurm'] is False + + +def test_update_sim_input(inline_env_input, do_nothing_sim_input, tmp_path): + unitjob = create_unitjob(do_nothing_sim_input, inline_env_input, tmp_path / 'wdir') + unitjob.update_sim_input({'new_key': 'new_value'}) + assert unitjob.get_sim_input()['new_key'] == 'new_value' + + +def test_add_output_files(inline_env_input, do_nothing_sim_input, tmp_path): + wdir = tmp_path / 'wdir' + unitjob = create_unitjob(do_nothing_sim_input, inline_env_input, wdir) + unitjob.gen_input_files() + unitjob.add_output_files({'result': 'out.xyz'}) + assert unitjob.get_output().get('files', {}).get('result') == 'out.xyz' + + +def test_get_logger(inline_env_input, do_nothing_sim_input, tmp_path): + wdir = tmp_path / 'wdir' + unitjob = create_unitjob(do_nothing_sim_input, inline_env_input, wdir) + unitjob.gen_input_files() + logger = unitjob.get_logger() + assert isinstance(logger, logging.Logger) + + +# --------------------------------------------------------------------------- +# UnitJob.gen_run_command +# --------------------------------------------------------------------------- + +def test_gen_run_command_basic(inline_env_input, do_nothing_sim_input, tmp_path): + unitjob = create_unitjob(do_nothing_sim_input, inline_env_input, tmp_path / 'wdir') + cmd = unitjob.gen_run_command() + assert 'asim-run' in cmd + assert 'sim_input.yaml' in cmd + + +def test_gen_run_command_prefix_suffix(tmp_path): + env_input = { + 'inline': { + 'mode': {'use_slurm': False, 'interactive': True}, + } + } + calc_input = { + 'lj': { + 'name': 'LennardJones', + 'module': 'ase.calculators.lj', + 'run_prefix': 'mpirun -n 4', + 'run_suffix': '--some-flag', + 'args': {}, + } + } + # calc_id must live in sim_input['args'] for UnitJob to pick up calc_params + sim_input = { + 'asimmodule': 'do_nothing', + 'env_id': 'inline', + 'workdir': str(tmp_path / 'wdir'), + 'args': {'calc_id': 'lj'}, + } + unitjob = UnitJob(sim_input, env_input=env_input, calc_input=calc_input) + cmd = unitjob.gen_run_command() + assert cmd.index('mpirun -n 4') < cmd.index('asim-run') + assert cmd.index('asim-run') < cmd.index('--some-flag') + + +# --------------------------------------------------------------------------- +# DistributedJob +# --------------------------------------------------------------------------- + +def _make_distributed_sim_input(env_id, n=3): + subsim = {'asimmodule': 'do_nothing', 'env_id': env_id} + return {f'job{i}': dict(subsim) for i in range(n)} + + +def test_distributed_job_init_inline(inline_env_input): + sim_input = _make_distributed_sim_input('inline', n=3) + djob = DistributedJob(sim_input, env_input=inline_env_input) + assert len(djob.unitjobs) == 3 + assert djob.use_slurm is False + # Workdir names must be prefixed with 'id-' + for uj in djob.unitjobs: + assert uj.workdir.name.startswith('id-') + + +def test_distributed_job_init_slurm(batch_env_input): + sim_input = _make_distributed_sim_input('batch', n=2) + djob = DistributedJob(sim_input, env_input=batch_env_input) + assert djob.use_slurm is True + + +def test_distributed_job_init_too_many_jobs(inline_env_input): + sim_input = {f'job{i}': {'asimmodule': 'do_nothing', 'env_id': 'inline'} for i in range(1000)} + with pytest.raises(AssertionError): + DistributedJob(sim_input, env_input=inline_env_input) + + +def test_distributed_job_gen_input_files(inline_env_input, tmp_path): + original_dir = Path('.').resolve() + os.chdir(tmp_path) + try: + sim_input = _make_distributed_sim_input('inline', n=2) + djob = DistributedJob(sim_input, env_input=inline_env_input) + djob.gen_input_files() + for uj in djob.unitjobs: + assert (uj.workdir / 'sim_input.yaml').exists() + finally: + os.chdir(original_dir) + + +def test_distributed_job_submit_inline(inline_env_input, tmp_path): + original_dir = Path('.').resolve() + os.chdir(tmp_path) + try: + sim_input = _make_distributed_sim_input('inline', n=2) + djob = DistributedJob(sim_input, env_input=inline_env_input) + djob.submit() + for uj in djob.unitjobs: + assert uj.get_status()[1] == 'complete' + finally: + os.chdir(original_dir) + + +# --------------------------------------------------------------------------- +# ChainedJob +# --------------------------------------------------------------------------- + +def _make_chained_sim_input(env_id, n=2): + return {f'step-{i}': {'asimmodule': 'do_nothing', 'env_id': env_id} for i in range(n)} + + +def test_chained_job_init(inline_env_input): + sim_input = _make_chained_sim_input('inline', n=3) + cjob = ChainedJob(sim_input, env_input=inline_env_input) + assert len(cjob.unitjobs) == 3 + for i, uj in enumerate(cjob.unitjobs): + assert str(uj.workdir) == f'step-{i}' + + +def test_chained_job_init_bad_keys(inline_env_input): + sim_input = {'bad_key': {'asimmodule': 'do_nothing', 'env_id': 'inline'}} + with pytest.raises(AssertionError): + ChainedJob(sim_input, env_input=inline_env_input) + + +def test_chained_job_get_last_output(tmp_path): + sim_input = _make_chained_sim_input('inline', n=2) + env_input = {'inline': {'mode': {'use_slurm': False, 'interactive': True}}} + chain_dir = tmp_path / 'chain' + chain_dir.mkdir() + cjob = ChainedJob(sim_input, env_input=env_input) + cjob.set_workdir(chain_dir) + cjob.unitjobs[-1].set_workdir(chain_dir / 'step-1') + cjob.unitjobs[-1].gen_input_files() + cjob.unitjobs[-1].update_output({'my_result': 42}) + assert cjob.get_last_output()['my_result'] == 42 + + +# --------------------------------------------------------------------------- +# load_job_from_directory, get_subjobs, load_job_tree +# --------------------------------------------------------------------------- + +def test_load_job_from_directory(inline_env_input, do_nothing_sim_input, tmp_path): + wdir = tmp_path / 'wdir' + unitjob = create_unitjob(do_nothing_sim_input, inline_env_input, wdir) + unitjob.gen_input_files(write_env_input=True) + job = load_job_from_directory(wdir) + assert job.sim_input['asimmodule'] == do_nothing_sim_input['asimmodule'] + assert job.workdir == wdir + + +def test_load_job_from_directory_missing(tmp_path): + with pytest.raises(AssertionError): + load_job_from_directory(tmp_path / 'nonexistent') + + +def test_get_subjobs(inline_env_input, do_nothing_sim_input, tmp_path): + root = tmp_path / 'root' + root.mkdir() + write_yaml(root / 'sim_input.yaml', do_nothing_sim_input) + + # Two subdirs with sim_input.yaml, one without + for name in ('sub_a', 'sub_b', 'empty_sub'): + (root / name).mkdir() + write_yaml(root / 'sub_a' / 'sim_input.yaml', do_nothing_sim_input) + write_yaml(root / 'sub_b' / 'sim_input.yaml', do_nothing_sim_input) + + subjobs = get_subjobs(root) + assert len(subjobs) == 2 + assert all(p.name in ('sub_a', 'sub_b') for p in subjobs) + # Must be sorted + assert subjobs == sorted(subjobs) + + +def test_load_job_tree_flat(inline_env_input, do_nothing_sim_input, tmp_path): + wdir = tmp_path / 'wdir' + unitjob = create_unitjob(do_nothing_sim_input, inline_env_input, wdir) + unitjob.gen_input_files(write_env_input=True) + tree = load_job_tree(wdir) + assert tree['workdir_name'] == wdir.name + assert tree['subjobs'] is None + + +def test_load_job_tree_nested(inline_env_input, do_nothing_sim_input, tmp_path): + root = tmp_path / 'root' + root.mkdir() + write_yaml(root / 'sim_input.yaml', do_nothing_sim_input) + for name in ('sub_a', 'sub_b'): + subdir = root / name + subdir.mkdir() + write_yaml(subdir / 'sim_input.yaml', do_nothing_sim_input) + + tree = load_job_tree(root) + assert tree['subjobs'] is not None + assert set(tree['subjobs'].keys()) == {'sub_a', 'sub_b'} + assert tree['subjobs']['sub_a']['subjobs'] is None From 7ef9206fb6932cbd55313284e0e7d92ba567247e Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Sat, 4 Apr 2026 16:38:27 -0700 Subject: [PATCH 44/78] docs update --- .../asimtools.asimmodules.active_learning.rst | 42 ++ docs/asimtools.asimmodules.ase_md.rst | 18 + docs/asimtools.asimmodules.data.rst | 18 + docs/asimtools.asimmodules.mace.rst | 18 + docs/asimtools.asimmodules.phonopy.rst | 74 +++ docs/asimtools.asimmodules.rst | 13 +- docs/asimtools.asimmodules.vasp.rst | 18 + docs/asimtools.asimmodules.workflows.rst | 26 +- docs/conf.py | 14 +- docs/include_contributing.rst | 2 +- docs/include_readme.rst | 2 +- docs/index.rst | 11 +- docs/workflows.rst | 548 +++++++++++++++++- 13 files changed, 759 insertions(+), 45 deletions(-) create mode 100644 docs/asimtools.asimmodules.active_learning.rst create mode 100644 docs/asimtools.asimmodules.ase_md.rst create mode 100644 docs/asimtools.asimmodules.data.rst create mode 100644 docs/asimtools.asimmodules.mace.rst create mode 100644 docs/asimtools.asimmodules.phonopy.rst create mode 100644 docs/asimtools.asimmodules.vasp.rst diff --git a/docs/asimtools.asimmodules.active_learning.rst b/docs/asimtools.asimmodules.active_learning.rst new file mode 100644 index 0000000..3cca83c --- /dev/null +++ b/docs/asimtools.asimmodules.active_learning.rst @@ -0,0 +1,42 @@ +asimtools.asimmodules.active\_learning package +============================================== + +asimtools.asimmodules.active\_learning.ase\_md module +----------------------------------------------------- + +.. automodule:: asimtools.asimmodules.active_learning.ase_md + :members: + :undoc-members: + :show-inheritance: + +asimtools.asimmodules.active\_learning.compute\_deviation module +---------------------------------------------------------------- + +.. automodule:: asimtools.asimmodules.active_learning.compute_deviation + :members: + :undoc-members: + :show-inheritance: + +asimtools.asimmodules.active\_learning.direct\_sample module +------------------------------------------------------------ + +.. automodule:: asimtools.asimmodules.active_learning.direct_sample + :members: + :undoc-members: + :show-inheritance: + +asimtools.asimmodules.active\_learning.select\_images module +------------------------------------------------------------ + +.. automodule:: asimtools.asimmodules.active_learning.select_images + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: asimtools.asimmodules.active_learning + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/asimtools.asimmodules.ase_md.rst b/docs/asimtools.asimmodules.ase_md.rst new file mode 100644 index 0000000..a743975 --- /dev/null +++ b/docs/asimtools.asimmodules.ase_md.rst @@ -0,0 +1,18 @@ +asimtools.asimmodules.ase\_md package +===================================== + +asimtools.asimmodules.ase\_md.ase\_md module +-------------------------------------------- + +.. automodule:: asimtools.asimmodules.ase_md.ase_md + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: asimtools.asimmodules.ase_md + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/asimtools.asimmodules.data.rst b/docs/asimtools.asimmodules.data.rst new file mode 100644 index 0000000..b825555 --- /dev/null +++ b/docs/asimtools.asimmodules.data.rst @@ -0,0 +1,18 @@ +asimtools.asimmodules.data package +================================== + +asimtools.asimmodules.data.collect\_images module +------------------------------------------------- + +.. automodule:: asimtools.asimmodules.data.collect_images + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: asimtools.asimmodules.data + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/asimtools.asimmodules.mace.rst b/docs/asimtools.asimmodules.mace.rst new file mode 100644 index 0000000..9fda97b --- /dev/null +++ b/docs/asimtools.asimmodules.mace.rst @@ -0,0 +1,18 @@ +asimtools.asimmodules.mace package +================================== + +asimtools.asimmodules.mace.train\_mace module +--------------------------------------------- + +.. automodule:: asimtools.asimmodules.mace.train_mace + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: asimtools.asimmodules.mace + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/asimtools.asimmodules.phonopy.rst b/docs/asimtools.asimmodules.phonopy.rst new file mode 100644 index 0000000..4007ac3 --- /dev/null +++ b/docs/asimtools.asimmodules.phonopy.rst @@ -0,0 +1,74 @@ +asimtools.asimmodules.phonopy package +===================================== + +asimtools.asimmodules.phonopy.forces module +------------------------------------------- + +.. automodule:: asimtools.asimmodules.phonopy.forces + :members: + :undoc-members: + :show-inheritance: + +asimtools.asimmodules.phonopy.full\_qha module +---------------------------------------------- + +.. automodule:: asimtools.asimmodules.phonopy.full_qha + :members: + :undoc-members: + :show-inheritance: + +asimtools.asimmodules.phonopy.generate\_phonopy\_displacements module +--------------------------------------------------------------------- + +.. automodule:: asimtools.asimmodules.phonopy.generate_phonopy_displacements + :members: + :undoc-members: + :show-inheritance: + +asimtools.asimmodules.phonopy.phonon\_bands\_and\_dos module +------------------------------------------------------------ + +.. automodule:: asimtools.asimmodules.phonopy.phonon_bands_and_dos + :members: + :undoc-members: + :show-inheritance: + +asimtools.asimmodules.phonopy.phonon\_bands\_and\_dos\_from\_forces module +-------------------------------------------------------------------------- + +.. automodule:: asimtools.asimmodules.phonopy.phonon_bands_and_dos_from_forces + :members: + :undoc-members: + :show-inheritance: + +asimtools.asimmodules.phonopy.qha\_properties module +---------------------------------------------------- + +.. automodule:: asimtools.asimmodules.phonopy.qha_properties + :members: + :undoc-members: + :show-inheritance: + +asimtools.asimmodules.phonopy.read\_force\_constants module +----------------------------------------------------------- + +.. automodule:: asimtools.asimmodules.phonopy.read_force_constants + :members: + :undoc-members: + :show-inheritance: + +asimtools.asimmodules.phonopy.thermal\_properties module +-------------------------------------------------------- + +.. automodule:: asimtools.asimmodules.phonopy.thermal_properties + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: asimtools.asimmodules.phonopy + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/asimtools.asimmodules.rst b/docs/asimtools.asimmodules.rst index 74e4281..4d6b823 100644 --- a/docs/asimtools.asimmodules.rst +++ b/docs/asimtools.asimmodules.rst @@ -6,15 +6,21 @@ asimtools.asimmodules package .. toctree:: :maxdepth: 4 + asimtools.asimmodules.active_learning + asimtools.asimmodules.ase_md asimtools.asimmodules.benchmarking + asimtools.asimmodules.data asimtools.asimmodules.elastic_constants asimtools.asimmodules.eos asimtools.asimmodules.geometry_optimization asimtools.asimmodules.lammps + asimtools.asimmodules.mace asimtools.asimmodules.phonons + asimtools.asimmodules.phonopy asimtools.asimmodules.surface_energies asimtools.asimmodules.transformations asimtools.asimmodules.vacancy_formation_energy + asimtools.asimmodules.vasp asimtools.asimmodules.workflows @@ -35,13 +41,6 @@ asimtools.asimmodules.singlepoint module :undoc-members: :show-inheritance: -asimtools.asimmodules.template module -------------------------------------- - -.. automodule:: asimtools.asimmodules.template - :members: - :undoc-members: - :show-inheritance: Module contents --------------- diff --git a/docs/asimtools.asimmodules.vasp.rst b/docs/asimtools.asimmodules.vasp.rst new file mode 100644 index 0000000..b3c2b69 --- /dev/null +++ b/docs/asimtools.asimmodules.vasp.rst @@ -0,0 +1,18 @@ +asimtools.asimmodules.vasp package +================================== + +asimtools.asimmodules.vasp.vasp module +-------------------------------------- + +.. automodule:: asimtools.asimmodules.vasp.vasp + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: asimtools.asimmodules.vasp + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/asimtools.asimmodules.workflows.rst b/docs/asimtools.asimmodules.workflows.rst index 137c318..a200d80 100644 --- a/docs/asimtools.asimmodules.workflows.rst +++ b/docs/asimtools.asimmodules.workflows.rst @@ -1,8 +1,6 @@ asimtools.asimmodules.workflows package ======================================= - - asimtools.asimmodules.workflows.calc\_array module -------------------------------------------------- @@ -35,6 +33,14 @@ asimtools.asimmodules.workflows.image\_array module :undoc-members: :show-inheritance: +asimtools.asimmodules.workflows.iterative module +------------------------------------------------ + +.. automodule:: asimtools.asimmodules.workflows.iterative + :members: + :undoc-members: + :show-inheritance: + asimtools.asimmodules.workflows.sim\_array module ------------------------------------------------- @@ -43,6 +49,22 @@ asimtools.asimmodules.workflows.sim\_array module :undoc-members: :show-inheritance: +asimtools.asimmodules.workflows.update\_dependencies module +----------------------------------------------------------- + +.. automodule:: asimtools.asimmodules.workflows.update_dependencies + :members: + :undoc-members: + :show-inheritance: + +asimtools.asimmodules.workflows.utils module +-------------------------------------------- + +.. automodule:: asimtools.asimmodules.workflows.utils + :members: + :undoc-members: + :show-inheritance: + Module contents --------------- diff --git a/docs/conf.py b/docs/conf.py index 553e067..f2e2a46 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,7 +12,7 @@ import os import sys -sys.path.insert(0, os.path.abspath('../asimtools')) +sys.path.insert(0, os.path.abspath('../src')) sys.path.insert(0, os.path.abspath('../')) @@ -48,6 +48,16 @@ # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +# Mock optional heavy dependencies so autodoc can import all modules +autodoc_mock_imports = [ + 'maml', + 'mace', + 'matgl', + 'chgnet', + 'phonopy', + 'seekpath', +] + # -- Options for HTML output ------------------------------------------------- @@ -59,7 +69,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = [] # Intersphinx Mappings intersphinx_mapping = { diff --git a/docs/include_contributing.rst b/docs/include_contributing.rst index 78dbb13..ee11058 100644 --- a/docs/include_contributing.rst +++ b/docs/include_contributing.rst @@ -1,5 +1,5 @@ Contributing to ASIMTools ========================= -.. include:: CONTRIBUTING.md +.. include:: ../CONTRIBUTING.md :parser: myst_parser.sphinx_ diff --git a/docs/include_readme.rst b/docs/include_readme.rst index f2253cd..8f9ef40 100644 --- a/docs/include_readme.rst +++ b/docs/include_readme.rst @@ -1,5 +1,5 @@ README ====== -.. include:: README.md +.. include:: ../README.md :parser: myst_parser.sphinx_ diff --git a/docs/index.rst b/docs/index.rst index 4e56338..634430f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,23 +1,16 @@ -.. asimtools documentation master file, created by - sphinx-quickstart on Sat Jul 1 18:22:31 2023. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. +.. asimtools documentation master file -Welcome to asimtools's documentation! -===================================== +.. include:: include_readme.rst .. toctree:: :maxdepth: 12 :caption: Contents: - README Running asimmodules Custom asimmodules Workflows API Docs Contributing - - Indices and tables diff --git a/docs/workflows.rst b/docs/workflows.rst index 4867ded..e47a9d4 100644 --- a/docs/workflows.rst +++ b/docs/workflows.rst @@ -1,33 +1,535 @@ Using built-in workflow tools ============================= -ASIMTools offers some standard tools for performing common workflows. These -are: +ASIMTools provides six built-in workflow asimmodules for the most common +simulation patterns. They are all wrappers around the +:class:`~asimtools.job.DistributedJob` and :class:`~asimtools.job.ChainedJob` +classes and can be nested arbitrarily. -#. :func:`asimtools.asimmodules.sim_array.sim_array` - Run the same asimmodule - with one or more specified arguments of the asimmodule iterated over. This - is the most useful asimmodule and can substitute most others except - ``chained`` +.. contents:: Workflows + :local: + :depth: 1 -#. :func:`asimtools.asimmodules.image_array.image_array` - Run the same - asimmodule on multiple images, e.g. a repeat calculation on a database +---- -#. :func:`asimtools.asimmodules.calc_array.calc_array` - Run the same - asimmodule using different calc_ids or calculator parameters based on a - template. e.g. to converge cutoffs in DFT or benchmark many force fields +sim\_array +---------- -#. :func:`asimtools.asimmodules.distributed.distributed` - Run multiple - sim_inputs in parallel +**Module:** :func:`asimtools.asimmodules.workflows.sim_array.sim_array` -#. :func:`asimtools.asimmodules.chained.chained` - Run asimmodules one after - the other, e.g. if step 2 results depend on step 1 etc. This allows building - multi-step workflows. +Run the **same asimmodule in parallel** over a sweep of values for a single +argument (or a paired set of arguments). This is the most generally useful +workflow and covers parameter sweeps, convergence tests, and ensemble runs. -#. :func:`asimtools.asimmodules.iterative.iterative` - Run the same asimmodule - over and over until some condition is reached. This asimmodule is still - under active development +When to use +~~~~~~~~~~~ -Examples for each type of workflow are given in the examples directory and -documentation can be found in :mod:`asimtools.asimmodules`. They also serve as -templates for you to build your own workflows directly using -:func:`asimtools.job.Job` objects as an advanced user. +- Sweep a lattice constant, cutoff energy, temperature, etc. +- Vary an integer index (e.g. run the same relaxation on structures 0-99). +- Sweep multiple parameters simultaneously (``secondary_key_sequences``). + +Key parameters +~~~~~~~~~~~~~~ + ++-----------------------------+--------------------------------------------------+ +| Parameter | Description | ++=============================+==================================================+ +| ``template_sim_input`` | Base sim_input copied for every job. | ++-----------------------------+--------------------------------------------------+ +| ``key_sequence`` | Dot-path into the nested dict being swept, | +| | e.g. ``[args, a]`` to change ``args.a``. | ++-----------------------------+--------------------------------------------------+ +| ``array_values`` | Explicit list of values. | ++-----------------------------+--------------------------------------------------+ +| ``linspace_args`` | ``[start, stop, n]`` — passed to | +| | :func:`numpy.linspace`. | ++-----------------------------+--------------------------------------------------+ +| ``arange_args`` | ``[start, stop, step]`` — passed to | +| | :func:`numpy.arange`. | ++-----------------------------+--------------------------------------------------+ +| ``file_pattern`` | Glob pattern; files matching it become values. | ++-----------------------------+--------------------------------------------------+ +| ``labels`` | ``"values"`` (default) uses the value itself; | +| | ``"files"`` extracts a label from the file path; | +| | a list provides explicit directory names. | ++-----------------------------+--------------------------------------------------+ +| ``placeholder`` | Replace this sentinel string inside the value | +| | rather than the whole value. | ++-----------------------------+--------------------------------------------------+ +| ``secondary_key_sequences`` | Additional keys to sweep in lock-step. | ++-----------------------------+--------------------------------------------------+ +| ``group_size`` | Pack N jobs into each Slurm array task. | ++-----------------------------+--------------------------------------------------+ + +Example — sweep lattice constants +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: yaml + + # sim_input.yaml + asimmodule: workflows.sim_array + env_id: batch + workdir: results/lc_sweep + args: + key_sequence: [args, image, a] + linspace_args: [3.4, 4.2, 9] # 9 values from 3.4 to 4.2 Å + template_sim_input: + asimmodule: singlepoint + env_id: batch + args: + calc_id: my_mlip + image: + name: Cu + crystalstructure: fcc + a: PLACEHOLDER + properties: [energy, forces, stress] + +Example — sweep over a set of structure files +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: yaml + + asimmodule: workflows.sim_array + env_id: batch + workdir: results/structure_sweep + args: + key_sequence: [args, image, image_file] + file_pattern: data/structures/*.xyz + labels: files # label taken from the filename + template_sim_input: + asimmodule: geometry_optimization.cell_relax + env_id: batch + args: + calc_id: my_mlip + image: + image_file: PLACEHOLDER + +Example — sweep two parameters simultaneously +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: yaml + + asimmodule: workflows.sim_array + env_id: batch + workdir: results/dual_sweep + args: + key_sequence: [args, temperature] + array_values: [300, 600, 900, 1200] + secondary_key_sequences: + - [args, pressure] + secondary_array_values: + - [1.0, 2.0, 3.0, 4.0] + template_sim_input: + asimmodule: ase_md.ase_md + env_id: batch + args: + calc_id: my_mlip + image: {name: Fe} + temperature: 300 + pressure: 1.0 + +---- + +image\_array +------------ + +**Module:** :func:`asimtools.asimmodules.workflows.image_array.image_array` + +Run the **same asimmodule in parallel on multiple structures**. The image +input specification for each job is taken from +:func:`asimtools.utils.get_images`, so any combination of file patterns, +explicit lists, or ASE database queries is supported. + +When to use +~~~~~~~~~~~ + +- Relaxation or single-point calculation across a database of structures. +- Benchmark a force field against a set of reference structures. +- Compute phonons for every structure in a dataset. + +Key parameters +~~~~~~~~~~~~~~ + ++-----------------------------+---------------------------------------------------+ +| Parameter | Description | ++=============================+===================================================+ +| ``images`` | Dict for :func:`~asimtools.utils.get_images`; | +| | can use ``image_file``, ``pattern``, | +| | ``patterns``, or ``images``. | ++-----------------------------+---------------------------------------------------+ +| ``template_sim_input`` | Base sim_input; image key is injected per job. | ++-----------------------------+---------------------------------------------------+ +| ``key_sequence`` | Where in ``template_sim_input`` to inject the | +| | image path (default ``[args, image, | +| | image_file]``). | ++-----------------------------+---------------------------------------------------+ +| ``labels`` | Source of directory name for each job. | ++-----------------------------+---------------------------------------------------+ +| ``env_ids`` | Per-image environments (or a single string). | ++-----------------------------+---------------------------------------------------+ + +Example — single-point energy on a dataset +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: yaml + + asimmodule: workflows.image_array + env_id: batch + workdir: results/dataset_sp + args: + images: + pattern: data/structures/*.xyz + template_sim_input: + asimmodule: singlepoint + env_id: batch + args: + calc_id: my_mlip + properties: [energy, forces, stress] + labels: files + +Example — relax all structures in an xyz file +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: yaml + + asimmodule: workflows.image_array + env_id: batch + workdir: results/relax_all + args: + images: + image_file: data/structures.xyz + template_sim_input: + asimmodule: geometry_optimization.atom_relax + env_id: batch + args: + calc_id: my_mlip + +---- + +calc\_array +----------- + +**Module:** :func:`asimtools.asimmodules.workflows.calc_array.calc_array` + +Run the **same asimmodule in parallel with different calculators** (or +different calculator hyperparameters). Useful for benchmarking, convergence +studies, or comparing multiple potentials. + +When to use +~~~~~~~~~~~ + +- Compare DFT, ML potentials, and empirical force fields on the same system. +- Converge a DFT parameter (cutoff energy, k-point density) across many values. +- Benchmark a new potential against a reference. + +Key parameters +~~~~~~~~~~~~~~ + ++-----------------------------+--------------------------------------------------+ +| Parameter | Description | ++=============================+==================================================+ +| ``subsim_input`` | The sim_input for the asimmodule to run. | ++-----------------------------+--------------------------------------------------+ +| ``calc_ids`` | Explicit list of calculator IDs. | ++-----------------------------+--------------------------------------------------+ +| ``template_calc_id`` | Base calc_id; ``key_sequence`` is swept inside | +| | its parameters. | ++-----------------------------+--------------------------------------------------+ +| ``key_sequence`` | Path into the calculator dict to sweep. | ++-----------------------------+--------------------------------------------------+ +| ``array_values`` / | Values to sweep (same semantics as | +| ``linspace_args`` / | ``sim_array``). | +| ``arange_args`` | | ++-----------------------------+--------------------------------------------------+ + +Example — benchmark multiple calculators +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: yaml + + asimmodule: workflows.calc_array + env_id: batch + workdir: results/calc_benchmark + args: + calc_ids: [emt, lj_argon, mace_mp] + subsim_input: + asimmodule: singlepoint + env_id: batch + args: + image: {name: Ar} + properties: [energy, forces] + +Example — converge DFT cutoff energy +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: yaml + + asimmodule: workflows.calc_array + env_id: batch + workdir: results/ecut_convergence + args: + template_calc_id: vasp_base + key_sequence: [args, encut] + linspace_args: [200, 600, 9] # 200, 250, …, 600 eV + subsim_input: + asimmodule: singlepoint + env_id: batch + args: + image: {name: Fe} + properties: [energy] + +---- + +distributed +----------- + +**Module:** :func:`asimtools.asimmodules.workflows.distributed.distributed` + +Submit an **arbitrary collection of heterogeneous sim_inputs in parallel**. +Unlike ``sim_array`` / ``image_array`` / ``calc_array``, each sub-job can be +a completely different asimmodule with different parameters. + +When to use +~~~~~~~~~~~ + +- Run a heterogeneous set of jobs that do not share a common template. +- Fan out to many independent calculations and collect results later. +- Use as the parallel layer in a custom workflow script. + +Key parameters +~~~~~~~~~~~~~~ + ++-----------------------------+--------------------------------------------------+ +| Parameter | Description | ++=============================+==================================================+ +| ``subsim_inputs`` | Dict mapping job IDs to individual sim_inputs. | ++-----------------------------+--------------------------------------------------+ +| ``array_max`` | Max concurrent jobs in Slurm array. | ++-----------------------------+--------------------------------------------------+ +| ``group_size`` | Pack N jobs per Slurm array task. | ++-----------------------------+--------------------------------------------------+ +| ``skip_failed`` | Continue even if some sub-jobs fail. | ++-----------------------------+--------------------------------------------------+ + +Example +~~~~~~~ + +.. code-block:: yaml + + asimmodule: workflows.distributed + env_id: batch + workdir: results/mixed_jobs + args: + subsim_inputs: + eos_fe: + asimmodule: eos.postprocess + env_id: batch + args: + image: {name: Fe} + calc_id: my_mlip + relax_cu: + asimmodule: geometry_optimization.cell_relax + env_id: batch + args: + image: {name: Cu} + calc_id: my_mlip + sp_ar: + asimmodule: singlepoint + env_id: batch + args: + image: {name: Ar} + calc_id: emt + properties: [energy] + +---- + +chained +------- + +**Module:** :func:`asimtools.asimmodules.workflows.chained.chained` + +Run asimmodules **one after the other**, where each step can depend on files +produced by the previous step. When Slurm is used, job dependencies are set +automatically via ``sbatch --dependency``. + +When to use +~~~~~~~~~~~ + +- Multi-step workflows: relax → single-point → post-process. +- Any workflow where step N reads output files from step N-1. +- Chain a ``sim_array`` with a post-processing script. + +Key parameters +~~~~~~~~~~~~~~ + ++-----------------------------+--------------------------------------------------+ +| Parameter | Description | ++=============================+==================================================+ +| ``steps`` | Ordered dict (``step-0``, ``step-1``, ...) of | +| | sim_inputs to execute in sequence. | ++-----------------------------+--------------------------------------------------+ + +.. note:: + + Keys must follow the pattern ``step-N`` (zero-indexed integers). If a step + internally launches additional Slurm jobs (e.g. via ``sim_array``), use + ``update_dependencies`` as an intermediate step to wire up the Slurm + dependency chain correctly. + +Example — relax then compute phonons +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: yaml + + asimmodule: workflows.chained + env_id: batch + workdir: results/relax_then_phonons + args: + steps: + step-0: + asimmodule: geometry_optimization.cell_relax + env_id: batch + args: + calc_id: my_mlip + image: {name: Cu} + step-1: + asimmodule: phonons.ase_phonons + env_id: batch + args: + calc_id: my_mlip + image: + image_file: ../step-0/final.xyz + +Example — three-step pipeline with a parallel middle step +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: yaml + + asimmodule: workflows.chained + env_id: batch + workdir: results/pipeline + args: + steps: + step-0: + asimmodule: geometry_optimization.cell_relax + env_id: batch + args: + calc_id: my_mlip + image: {name: Al} + step-1: + asimmodule: workflows.sim_array + env_id: batch + args: + key_sequence: [args, image, a] + linspace_args: [3.9, 4.1, 5] + template_sim_input: + asimmodule: singlepoint + env_id: batch + args: + calc_id: my_mlip + image: + name: Al + a: PLACEHOLDER + step-2: + asimmodule: eos.postprocess + env_id: batch + args: + workdir: ../step-1 + +---- + +iterative +--------- + +**Module:** :func:`asimtools.asimmodules.workflows.iterative.iterative` + +Run the **same asimmodule sequentially** over a sweep of values where each +job must complete before the next begins. Unlike ``sim_array``, jobs run +one at a time. Optionally, a file produced by each step can be fed as input +to the next (``dependent_file``). + +When to use +~~~~~~~~~~~ + +- Each step's output is input to the next (e.g. active-learning loops). +- A sequential sweep where order matters. +- Iterative fitting or refinement workflows. + +Key parameters +~~~~~~~~~~~~~~ + ++-----------------------------------+----------------------------------------------+ +| Parameter | Description | ++===================================+==============================================+ +| ``template_sim_input`` | Base sim_input for each step. | ++-----------------------------------+----------------------------------------------+ +| ``key_sequence`` | Key path swept across steps. | ++-----------------------------------+----------------------------------------------+ +| ``array_values`` / ``linspace_args`` | Values to iterate over. | +| / ``arange_args`` | | ++-----------------------------------+----------------------------------------------+ +| ``dependent_file`` | Filename (relative to each step's workdir) | +| | to pass as input to the next step. | ++-----------------------------------+----------------------------------------------+ +| ``dependent_file_key_sequence`` | Key path in the next step's sim_input where | +| | the file path is injected. | ++-----------------------------------+----------------------------------------------+ + +Example — sequential geometry relaxations at increasing pressures +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: yaml + + asimmodule: workflows.iterative + env_id: batch + workdir: results/pressure_series + args: + key_sequence: [args, pressure] + array_values: [0, 5, 10, 20, 50] + dependent_file: final.xyz + dependent_file_key_sequence: [args, image, image_file] + template_sim_input: + asimmodule: geometry_optimization.cell_relax + env_id: batch + args: + calc_id: my_mlip + image: {name: Fe} + pressure: 0 + +---- + +update\_dependencies +-------------------- + +**Module:** +:func:`asimtools.asimmodules.workflows.update_dependencies.update_dependencies` + +An **internal helper** used by ``chained`` to wire up Slurm job dependencies +when one step internally launches additional Slurm jobs (e.g. via +``sim_array``). It reads the job IDs written by the previous step and calls +``scontrol update`` to add the correct ``afterok`` dependency on the next +step's jobs. + +Most users will not need to call this directly; it is inserted automatically +by ``chained`` when needed. It has no effect outside of a Slurm environment. + +.. code-block:: yaml + + # Example use inside a chained workflow (advanced) + step-1: + asimmodule: workflows.update_dependencies + env_id: batch + args: + prev_step_dir: ../step-0 + next_step_dir: ../step-2 + skip_failed: false + +---- + +API reference +------------- + +.. toctree:: + :maxdepth: 1 + + asimtools.asimmodules.workflows From 6b8bb7911eae3d0ff655a65b12981154c846c301 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Mon, 6 Apr 2026 11:38:08 -0700 Subject: [PATCH 45/78] optimizations and version bump --- CHANGELOG.md | 33 ++++++++++++ docs/conf.py | 2 +- docs/include_changelog.rst | 5 ++ docs/index.rst | 1 + pyproject.toml | 2 +- src/asimtools/_version.py | 2 +- src/asimtools/job.py | 74 ++++++++++++--------------- src/asimtools/scripts/asim_check.py | 41 +++++++++------ src/asimtools/scripts/asim_execute.py | 1 - src/asimtools/scripts/asim_run.py | 1 - tests/unit/test_job.py | 20 +++----- 11 files changed, 106 insertions(+), 76 deletions(-) create mode 100644 docs/include_changelog.rst diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d56e5b..8a0609f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,39 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [0.2.0] - 2026-04-06 + +### Breaking changes +- `change_dict_value`, `change_dict_values`, `expand_wildcards`: parameter `d` renamed to `dct` +- `get_str_btn`, `get_nth_label`: parameter `s` renamed to `string` +- `repeat_to_N` renamed to `repeat_to_n` +- All call sites in `asimmodules/workflows/` updated to use the new parameter names + +### Added +- Pre-commit hooks: pylint (10.00/10 on `src/asimtools/` excluding asimmodules) and pytest run on every commit +- Tests for previously untested utilities: `strip_symbols`, `get_axis_lims`, `improve_plot`, `write_csv_from_dict`, `new_db`, `check_if_slurm_job_is_running` +- Tests for previously untested job functionality: `Job` getters/updaters, `DistributedJob` init and inline submit, `ChainedJob` init and last output, `load_job_from_directory`, `get_subjobs`, `load_job_tree` +- Sphinx docs now include all previously missing asimmodule packages: `active_learning`, `ase_md`, `data`, `mace`, `phonopy`, `vasp` +- Workflows documentation page with parameter tables and YAML examples for all 7 workflow modules: `sim_array`, `image_array`, `calc_array`, `distributed`, `chained`, `iterative`, `update_dependencies` +- `autodoc_mock_imports` in Sphinx config for optional heavy dependencies (`maml`, `mace`, `matgl`, `chgnet`, `phonopy`, `seekpath`) + +### Changed +- Package moved to `src/` layout; `pyproject.toml` and pytest config updated accordingly +- Sphinx docs home page is now the project README +- `get_sim_input`, `get_calc_input`, `get_env_input` now return internal references instead of deepcopies +- `Job.__init__` still deepcopies inputs at construction time to preserve isolation from callers + +### Fixed +- `asim_run.py`: precommands were executed twice (once discarding output, once capturing); now runs once +- `asim_execute.py`: `calc_input` was assigned twice before the conditional read +- `job.py` `start()` and `complete()`: two sequential `update_output` calls (2 reads + 2 writes) collapsed into one +- `asim_check.py`: each job tree node read `output.yaml` twice per print call; now reads once per node + +### Performance +- `get_status()`: running-job status update no longer triggers a redundant `output.yaml` re-read +- `DistributedJob.__init__`: `use_slurm`/`use_sh` classification reduced from 3 O(n) passes to a single pass with early exit +- `load_job_from_directory`: replaced `exists()` + `read_yaml` (2 filesystem calls) with try/except around `read_yaml` (1 call) + ## [develop] - 2025-2-14 ### Added diff --git a/docs/conf.py b/docs/conf.py index f2e2a46..15a6cb0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -23,7 +23,7 @@ author = 'Mgcini Keith Phuthi' # The full version, including alpha/beta/rc tags -release = '1.0.0' +release = '0.2.0' # -- General configuration --------------------------------------------------- diff --git a/docs/include_changelog.rst b/docs/include_changelog.rst new file mode 100644 index 0000000..08efb85 --- /dev/null +++ b/docs/include_changelog.rst @@ -0,0 +1,5 @@ +Changelog +========= + +.. include:: ../CHANGELOG.md + :parser: myst_parser.sphinx_ diff --git a/docs/index.rst b/docs/index.rst index 634430f..3cbd526 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -10,6 +10,7 @@ Custom asimmodules Workflows API Docs + Changelog Contributing diff --git a/pyproject.toml b/pyproject.toml index 4c27c13..e099f70 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "asimtools" description = "A lightweight python package for managing and running atomic simulation workflows" -version = "0.1.0" +version = "0.2.0" readme = "README.md" license = { text = "MIT" } authors = [ diff --git a/src/asimtools/_version.py b/src/asimtools/_version.py index 7e210bc..6daea9f 100644 --- a/src/asimtools/_version.py +++ b/src/asimtools/_version.py @@ -2,4 +2,4 @@ # https://packaging.python.org/guides/single-sourcing-package-version/ '''Package version string.''' -__version__ = "0.0.2" +__version__ = "0.2.0" diff --git a/src/asimtools/job.py b/src/asimtools/job.py index d66924c..b755a05 100644 --- a/src/asimtools/job.py +++ b/src/asimtools/job.py @@ -86,16 +86,16 @@ def set_status(self, status: str) -> None: def start(self) -> None: ''' Updates the output to signal that the job was started ''' self.update_output({ - 'start_time': datetime.now().strftime('%H:%M:%S, %m/%d/%y') + 'start_time': datetime.now().strftime('%H:%M:%S, %m/%d/%y'), + 'status': 'started', }) - self.set_status('started') def complete(self) -> None: - ''' Updates the output to signal that the job was started ''' + ''' Updates the output to signal that the job completed ''' self.update_output({ - 'end_time': datetime.now().strftime('%H:%M:%S, %m/%d/%y') + 'end_time': datetime.now().strftime('%H:%M:%S, %m/%d/%y'), + 'status': 'complete', }) - self.set_status('complete') def fail(self) -> None: ''' Updates status to failed ''' @@ -123,15 +123,15 @@ def add_output_files(self, file_dict: Dict) -> None: def get_sim_input(self) -> Dict: ''' Get simulation input ''' - return deepcopy(self.sim_input) + return self.sim_input def get_calc_input(self) -> Dict: ''' Get calculator input ''' - return deepcopy(self.calc_input) + return self.calc_input def get_env_input(self) -> Dict: ''' Get environment input ''' - return deepcopy(self.env_input) + return self.env_input def get_output_yaml(self) -> Dict: ''' Get current output file ''' @@ -180,7 +180,9 @@ def get_status(self, descend=False, display=False) -> Tuple[bool,str]: running = check_if_slurm_job_is_running(job_id) if running: status = 'started' - self.set_status(status) + # Write status without re-reading output.yaml + output['status'] = status + write_yaml(self.get_output_yaml(), output) complete = False else: if not descend: @@ -538,7 +540,7 @@ def submit( # pylint: disable=too-many-locals,too-many-branches,too-many-statem class DistributedJob(Job): ''' Array job object with ability to submit simultaneous jobs ''' - def __init__( + def __init__( # pylint: disable=too-many-locals self, sim_input: Dict, env_input: Union[None,Dict] = None, @@ -553,7 +555,7 @@ def __init__( assert njobs < 1000, \ f'ASIMTools id_nums are limited to {num_digits} digits, found \ {njobs} jobs! Having that many jobs is not very efficient. Try \ - grouping jobs together.' + grouping jobs together using the group_size argument to workflows.' sim_id_changed = False for i, (sim_id, subsim_input) in enumerate(sim_input.items()): @@ -584,28 +586,22 @@ def __init__( unitjobs.append(unitjob) # If all the jobs have the same config and use slurm, use a job array - env_id = unitjobs[0].env_id - same_env = np.all( - [(uj.env_id == env_id) for uj in unitjobs] - ) - - all_slurm = np.all( - [uj.env['mode'].get('use_slurm', False) for uj in unitjobs] - ) - - all_sh = np.all( - [uj.env['mode'].get('use_sh', False) for uj in unitjobs] - ) - - if same_env and all_slurm: - self.use_slurm = True - else: - self.use_slurm = False + first_env_id = unitjobs[0].env_id + same_env = True + all_slurm = True + all_sh = True + for uj in unitjobs: + if uj.env_id != first_env_id: + same_env = False + if not uj.env['mode'].get('use_slurm', False): + all_slurm = False + if not uj.env['mode'].get('use_sh', False): + all_sh = False + if not same_env and not all_slurm and not all_sh: + break - if all_sh: - self.use_sh = True - else: - self.use_sh = False + self.use_slurm = same_env and all_slurm + self.use_sh = all_sh self.unitjobs = unitjobs @@ -1013,16 +1009,14 @@ def load_job_from_directory(workdir: os.PathLike, asimrun_mode=False) -> Job: logger.error('sim_input.yaml not found in %s', {str(workdir)}) raise exc - env_input_file = workdir / 'env_input.yaml' - if env_input_file.exists(): - env_input = read_yaml(env_input_file) - else: + try: + env_input = read_yaml(workdir / 'env_input.yaml') + except FileNotFoundError: env_input = None - calc_input_file = workdir / 'calc_input.yaml' - if calc_input_file.exists(): - calc_input = read_yaml(calc_input_file) - else: + try: + calc_input = read_yaml(workdir / 'calc_input.yaml') + except FileNotFoundError: calc_input = None job = Job( diff --git a/src/asimtools/scripts/asim_check.py b/src/asimtools/scripts/asim_check.py index e87d2fb..6d74216 100755 --- a/src/asimtools/scripts/asim_check.py +++ b/src/asimtools/scripts/asim_check.py @@ -94,18 +94,21 @@ def load_job_tree( } return job_dict -def get_status_and_color(job): - ''' Helper to get printing colors ''' - status = job.get_status()[1] +def _status_color(status): + ''' Map a status string to a colorama color ''' if status == 'complete': - color = Fore.GREEN - elif status == 'failed': - color = Fore.RED - elif status == 'started': - color = Fore.BLUE - else: - color = Fore.WHITE - return status, color + return Fore.GREEN + if status == 'failed': + return Fore.RED + if status == 'started': + return Fore.BLUE + return Fore.WHITE + +def get_status_and_color(job): + ''' Helper to get printing colors — reads output.yaml once ''' + output = job.get_output() + status = output.get('status', 'clean') + return status, _status_color(status) def print_job_tree( job_tree: Dict, @@ -131,9 +134,12 @@ def print_job_tree( pass elif subjobs is not None: workdir = job_tree['workdir_name'] - status, color = get_status_and_color(job_tree['job']) - asimmodule = job_tree['job'].sim_input['asimmodule'] - job_ids = job_tree['job'].get_output().get('job_ids', 'none') + job = job_tree['job'] + output = job.get_output() # single read per node + status = output.get('status', 'clean') + color = _status_color(status) + asimmodule = job.sim_input['asimmodule'] + job_ids = output.get('job_ids', 'none') print(color + f'{indent_str}{workdir}, asimmodule: {asimmodule},' + \ f'status: {status}, job_ids: {job_ids}' + reset) if level > 0: @@ -145,13 +151,14 @@ def print_job_tree( level=level+1, max_level=max_level, ) - subjob = job_tree['job'] else: subjob_dir = job_tree['workdir_name'] subjob = job_tree['job'] + output = subjob.get_output() # single read per node + status = output.get('status', 'clean') + color = _status_color(status) asimmodule = subjob.sim_input['asimmodule'] - job_ids = job_tree['job'].get_output().get('job_ids', 'none') - status, color = get_status_and_color(subjob) + job_ids = output.get('job_ids', 'none') print(color + f'{indent_str}{subjob_dir}, asimmodule: {asimmodule}, '+\ f'status: {status}, job_ids: {job_ids}' + reset) diff --git a/src/asimtools/scripts/asim_execute.py b/src/asimtools/scripts/asim_execute.py index 02c3b0f..b0152d7 100755 --- a/src/asimtools/scripts/asim_execute.py +++ b/src/asimtools/scripts/asim_execute.py @@ -71,7 +71,6 @@ def parse_command_line(args) -> Tuple[Dict, Dict, Dict]: else: dependency = None - calc_input = args.calc env_input = args.env if env_input is not None: env_input = read_yaml(env_input) diff --git a/src/asimtools/scripts/asim_run.py b/src/asimtools/scripts/asim_run.py index c5aa552..6e5dcde 100755 --- a/src/asimtools/scripts/asim_run.py +++ b/src/asimtools/scripts/asim_run.py @@ -71,7 +71,6 @@ def main(args=None) -> None: # pylint: disable=too-many-locals,too-many-branche precommands = sim_input.get('precommands', []) for precommand in precommands: command = precommand.split() - completed_process = subprocess.run(command, check=True) completed_process = subprocess.run( command, check=False, capture_output=True, text=True, ) diff --git a/tests/unit/test_job.py b/tests/unit/test_job.py index e6e0be4..45e5ea9 100644 --- a/tests/unit/test_job.py +++ b/tests/unit/test_job.py @@ -444,29 +444,21 @@ def test_check_job_tree_complete(tmp_path, test_input, expected): # Job getter / updater methods # --------------------------------------------------------------------------- -def test_get_sim_input_is_deepcopy(inline_env_input, do_nothing_sim_input, tmp_path): +def test_get_sim_input_returns_internal(inline_env_input, do_nothing_sim_input, tmp_path): unitjob = create_unitjob(do_nothing_sim_input, inline_env_input, tmp_path / 'wdir') - sim_input = unitjob.get_sim_input() - sim_input['asimmodule'] = 'mutated' - assert unitjob.sim_input['asimmodule'] != 'mutated' + assert unitjob.get_sim_input() is unitjob.sim_input -def test_get_calc_input_is_deepcopy(inline_env_input, do_nothing_sim_input, lj_argon_calc_input, tmp_path): +def test_get_calc_input_returns_internal(inline_env_input, do_nothing_sim_input, lj_argon_calc_input, tmp_path): unitjob = create_unitjob( do_nothing_sim_input, inline_env_input, tmp_path / 'wdir', calc_input=lj_argon_calc_input ) - calc_input = unitjob.get_calc_input() - calc_id = list(lj_argon_calc_input.keys())[0] - calc_input[calc_id]['name'] = 'mutated' - assert unitjob.calc_input[calc_id]['name'] != 'mutated' + assert unitjob.get_calc_input() is unitjob.calc_input -def test_get_env_input_is_deepcopy(inline_env_input, do_nothing_sim_input, tmp_path): +def test_get_env_input_returns_internal(inline_env_input, do_nothing_sim_input, tmp_path): unitjob = create_unitjob(do_nothing_sim_input, inline_env_input, tmp_path / 'wdir') - env_input = unitjob.get_env_input() - env_id = list(inline_env_input.keys())[0] - env_input[env_id]['mode']['use_slurm'] = True - assert unitjob.env_input[env_id]['mode']['use_slurm'] is False + assert unitjob.get_env_input() is unitjob.env_input def test_update_sim_input(inline_env_input, do_nothing_sim_input, tmp_path): From b33ff5a75c4ac543d9fddf9e633135618bf43b04 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Thu, 7 May 2026 13:58:55 -0700 Subject: [PATCH 46/78] Add calculator dict interface to load_calc and UnitJob load_calc now accepts a `calculator` dict with `calc_id` or `calc_params` keys, mirroring the `image`/`get_atoms` pattern. Legacy calc_id and calc_params kwargs are preserved for backward compatibility. UnitJob.__init__ updated to extract calc_params from the new calculator dict in sim_input args. Adds test_calculators.py with 15 unit tests covering both interfaces. Co-Authored-By: Claude Sonnet 4.6 --- src/asimtools/calculators.py | 26 +++++-- src/asimtools/job.py | 17 ++++- tests/unit/test_calculators.py | 135 +++++++++++++++++++++++++++++++++ 3 files changed, 168 insertions(+), 10 deletions(-) create mode 100644 tests/unit/test_calculators.py diff --git a/src/asimtools/calculators.py b/src/asimtools/calculators.py index e1e6d0c..3c28061 100644 --- a/src/asimtools/calculators.py +++ b/src/asimtools/calculators.py @@ -11,24 +11,36 @@ # pylint: disable=import-error def load_calc( + calculator: Optional[Dict] = None, calc_id: Optional[str] = None, calc_input: Optional[Dict] = None, calc_params: Optional[Dict] = None, ): - """Loads a calculator using given calc_id or calc_input. Provide only one of - calc_id or calc_input and calc_id or calc_params - - :param calc_id: ID/key to use to load calculator from the supplied or \ - global calc_input file, defaults to None + """Loads a calculator using a calculator dict or legacy calc_id/calc_params + arguments. + + :param calculator: Dictionary with either a ``calc_id`` key (to look up + from the global or supplied calc_input) or a ``calc_params`` key + (to use directly). Takes precedence over the legacy arguments if + provided, defaults to None + :type calculator: Optional[Dict], optional + :param calc_id: Deprecated — use ``calculator={'calc_id': ...}`` instead. + ID/key to use to load calculator from the supplied or global + calc_input file, defaults to None :type calc_id: str, optional :param calc_input: calc_input dictionary, same form as calc_input yaml \ :type calc_input: Optional[Dict], optional - :param calc_params: calc_params dictionary for a single calculator \ - calc_params, defaults to None + :param calc_params: Deprecated — use ``calculator={'calc_params': ...}`` + instead. calc_params dictionary for a single calculator, + defaults to None :type calc_params: Optional[Dict], optional :return: ASE calculator instance :rtype: :class:`ase.calculators.calculators.Calculator` """ + if calculator is not None: + calc_id = calculator.get('calc_id', calc_id) + calc_params = calculator.get('calc_params', calc_params) + assert calc_id is not None or calc_params is not None, \ 'Provide one of calc_id or calc_id and calc_input or calc_params' if calc_id is not None: diff --git a/src/asimtools/job.py b/src/asimtools/job.py index b755a05..e82f271 100644 --- a/src/asimtools/job.py +++ b/src/asimtools/job.py @@ -268,10 +268,21 @@ def __init__( ) -> None: super().__init__(sim_input, env_input, calc_input) - # Check if the asimmodule being called uses a calc_id to + # Check if the asimmodule being called uses a calculator/calc_id to # get precommands, postcommands, run_prefixes and run_suffixes - self.calc_id = self.sim_input.get('args', {}).get('calc_id', None) - if self.calc_id is not None: + args = self.sim_input.get('args', {}) + calculator = args.get('calculator', None) + self.calc_id = args.get('calc_id', None) + + if calculator is not None: + self.calc_id = calculator.get('calc_id', self.calc_id) + calc_params_direct = calculator.get('calc_params', None) + else: + calc_params_direct = None + + if calc_params_direct is not None: + self.calc_params = calc_params_direct + elif self.calc_id is not None: if isinstance(self.calc_id, dict): self.calc_input = {'custom': self.calc_id} self.calc_id = 'custom' diff --git a/tests/unit/test_calculators.py b/tests/unit/test_calculators.py new file mode 100644 index 0000000..6d808b2 --- /dev/null +++ b/tests/unit/test_calculators.py @@ -0,0 +1,135 @@ +''' +Tests for calculators.py +''' +import pytest +from ase.calculators.emt import EMT +from asimtools.calculators import load_calc, load_ase_calc + +EMT_PARAMS = { + 'name': 'EMT', + 'module': 'ase.calculators.emt', + 'args': {}, +} + +CALC_INPUT = {'emt': EMT_PARAMS} + + +# ── load_ase_calc ───────────────────────────────────────────────────────────── + +def test_load_ase_calc_returns_correct_type(): + ''' load_ase_calc with EMT params returns an EMT instance ''' + calc = load_ase_calc(EMT_PARAMS) + assert isinstance(calc, EMT) + + +def test_load_ase_calc_with_args(): + ''' load_ase_calc passes args to the calculator constructor ''' + params = { + 'name': 'LennardJones', + 'module': 'ase.calculators.lj', + 'args': {'epsilon': 2.0, 'sigma': 1.5}, + } + from ase.calculators.lj import LennardJones + calc = load_ase_calc(params) + assert isinstance(calc, LennardJones) + assert calc.parameters['epsilon'] == 2.0 + assert calc.parameters['sigma'] == 1.5 + + +def test_load_ase_calc_bad_module(): + ''' load_ase_calc raises when module cannot be imported ''' + params = {'name': 'EMT', 'module': 'nonexistent.module', 'args': {}} + with pytest.raises(ModuleNotFoundError): + load_ase_calc(params) + + +def test_load_ase_calc_bad_name(): + ''' load_ase_calc raises when class name is not found in module ''' + params = {'name': 'NotAClass', 'module': 'ase.calculators.emt', 'args': {}} + with pytest.raises(AttributeError): + load_ase_calc(params) + + +# ── load_calc: new calculator dict interface ────────────────────────────────── + +def test_load_calc_calculator_with_calc_params(): + ''' calculator={"calc_params": ...} loads without a calc_input lookup ''' + calc = load_calc(calculator={'calc_params': EMT_PARAMS}) + assert isinstance(calc, EMT) + + +def test_load_calc_calculator_with_calc_id(): + ''' calculator={"calc_id": ...} looks up params from supplied calc_input ''' + calc = load_calc( + calculator={'calc_id': 'emt'}, + calc_input=CALC_INPUT, + ) + assert isinstance(calc, EMT) + + +def test_load_calc_calculator_with_calc_id_global(tmp_path, monkeypatch): + ''' calculator={"calc_id": ...} falls back to global calc_input env var ''' + from asimtools.utils import write_yaml + calc_input_file = tmp_path / 'calc_input.yaml' + write_yaml(calc_input_file, CALC_INPUT) + monkeypatch.setenv('ASIMTOOLS_CALC_INPUT', str(calc_input_file)) + calc = load_calc(calculator={'calc_id': 'emt'}) + assert isinstance(calc, EMT) + + +def test_load_calc_calculator_sets_label(): + ''' load_calc sets calc.label from calc_params name when no label given ''' + calc = load_calc(calculator={'calc_params': EMT_PARAMS}) + assert calc.label == 'EMT' + + +def test_load_calc_calculator_sets_custom_label(): + ''' load_calc sets calc.label from explicit label key in calc_params ''' + params = dict(EMT_PARAMS, label='my_emt') + calc = load_calc(calculator={'calc_params': params}) + assert calc.label == 'my_emt' + + +# ── load_calc: legacy interface (backward compatibility) ────────────────────── + +def test_load_calc_legacy_calc_params(): + ''' Legacy calc_params kwarg still works ''' + calc = load_calc(calc_params=EMT_PARAMS) + assert isinstance(calc, EMT) + + +def test_load_calc_legacy_calc_id(): + ''' Legacy calc_id kwarg still works with explicit calc_input ''' + calc = load_calc(calc_id='emt', calc_input=CALC_INPUT) + assert isinstance(calc, EMT) + + +# ── load_calc: error cases ──────────────────────────────────────────────────── + +def test_load_calc_no_args_raises(): + ''' load_calc raises AssertionError when called with no identifying args ''' + with pytest.raises(AssertionError): + load_calc() + + +def test_load_calc_missing_calc_id_raises(): + ''' load_calc raises KeyError when calc_id is not in calc_input ''' + with pytest.raises(KeyError): + load_calc(calc_id='missing', calc_input=CALC_INPUT) + + +def test_load_calc_no_module_or_external_name_raises(): + ''' load_calc raises KeyError when calc_params has unknown name and no module ''' + params = {'name': 'UnknownCalc', 'args': {}} + with pytest.raises(KeyError): + load_calc(calc_params=params) + + +def test_load_calc_calculator_takes_precedence_over_legacy(): + ''' calculator kwarg overrides legacy calc_params when both provided ''' + other_params = {'name': 'LennardJones', 'module': 'ase.calculators.lj', 'args': {}} + calc = load_calc( + calculator={'calc_params': EMT_PARAMS}, + calc_params=other_params, + ) + assert isinstance(calc, EMT) From 2f86a7b53eaaa3c2494c0814e309d1490496fc98 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Thu, 7 May 2026 14:02:11 -0700 Subject: [PATCH 47/78] Update tests to use new calculator dict interface Replace calc_id in sim_input['args'] with calculator={'calc_id': ...} across conftest.py fixtures, test_job.py helpers and inline sim_inputs, and test_distributed.py helper. Also cleans up the create_unitjob/ create_distjob helpers which previously set calc_id at top-level sim_input (dead code) instead of in args. Co-Authored-By: Claude Sonnet 4.6 --- .../asimmodules/workflows/test_distributed.py | 2 +- tests/conftest.py | 20 +++++++++---------- tests/unit/test_job.py | 7 +++---- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/tests/asimmodules/workflows/test_distributed.py b/tests/asimmodules/workflows/test_distributed.py index ded274c..6ff3039 100644 --- a/tests/asimmodules/workflows/test_distributed.py +++ b/tests/asimmodules/workflows/test_distributed.py @@ -17,7 +17,7 @@ def create_distjob(sim_input, env_input, workdir, calc_input=None): sim_input['env_id'] = env_id if calc_input is not None: calc_id = list(calc_input.keys())[0] - sim_input['calc_id'] = calc_id + sim_input.setdefault('args', {})['calculator'] = {'calc_id': calc_id} sim_input['workdir'] = workdir distjob = DistributedJob( sim_input['args']['subsim_inputs'], diff --git a/tests/conftest.py b/tests/conftest.py index 2e799f0..3654aa7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -189,14 +189,14 @@ def do_nothing_sim_input(): @pytest.fixture def lj_distributed_sim_input(): - ''' + ''' Sim input for a distributed job that does some lj calculations ''' subsim_input = { 'asimmodule': 'singlepoint', 'env_id': 'inline', 'args': { - 'calc_id': 'lj', + 'calculator': {'calc_id': 'lj'}, 'image': { 'name': 'Ar', }, @@ -219,7 +219,7 @@ def lj_distributed_sim_input(): @pytest.fixture def lj_distributed_skip_failed_sim_input(): - ''' + ''' Sim input for a distributed job that does some lj calculations but the first job fails ''' @@ -227,7 +227,7 @@ def lj_distributed_skip_failed_sim_input(): 'asimmodule': 'singlepoint', 'env_id': 'inline', 'args': { - 'calc_id': 'lj', + 'calculator': {'calc_id': 'lj'}, 'image': { 'name': 'Ar', }, @@ -254,14 +254,14 @@ def lj_distributed_skip_failed_sim_input(): @pytest.fixture def lj_distributed_custom_name_sim_input(): - ''' + ''' Sim input for a distributed job that does some lj calculations ''' subsim_input = { 'asimmodule': 'singlepoint', 'env_id': 'inline', 'args': { - 'calc_id': 'lj', + 'calculator': {'calc_id': 'lj'}, 'image': { 'name': 'Ar', }, @@ -285,14 +285,14 @@ def lj_distributed_custom_name_sim_input(): @pytest.fixture def lj_distributed_batch_sim_input(): - ''' + ''' Sim input for a distributed job that does some lj calculations ''' subsim_input = { 'asimmodule': 'singlepoint', 'env_id': 'batch', 'args': { - 'calc_id': 'lj', + 'calculator': {'calc_id': 'lj'}, 'image': { 'name': 'Ar', }, @@ -316,14 +316,14 @@ def lj_distributed_batch_sim_input(): @pytest.fixture def lj_distributed_group_batch_sim_input(): - ''' + ''' Sim input for a distributed job that does some lj calculations ''' subsim_input = { 'asimmodule': 'singlepoint', 'env_id': 'batch', # This should be overwrriten by the group env 'args': { - 'calc_id': 'lj', + 'calculator': {'calc_id': 'lj'}, 'image': { 'name': 'Ar', }, diff --git a/tests/unit/test_job.py b/tests/unit/test_job.py index 45e5ea9..b571b36 100644 --- a/tests/unit/test_job.py +++ b/tests/unit/test_job.py @@ -25,7 +25,7 @@ def create_unitjob(sim_input, env_input, workdir, calc_input=None, status=None): sim_input['env_id'] = env_id if calc_input is not None: calc_id = list(calc_input.keys())[0] - sim_input['calc_id'] = calc_id + sim_input.setdefault('args', {})['calculator'] = {'calc_id': calc_id} sim_input['workdir'] = workdir unitjob = UnitJob( sim_input, @@ -217,7 +217,7 @@ def test_slurm_asimmodule(flags, tmp_path): 'env_id': 'test_batch', 'workdir': wdir, 'job_name': jobname, - 'args': {'calc_id': 'test_calc_id'}, + 'args': {'calculator': {'calc_id': 'test_calc_id'}}, } env_input = { @@ -509,12 +509,11 @@ def test_gen_run_command_prefix_suffix(tmp_path): 'args': {}, } } - # calc_id must live in sim_input['args'] for UnitJob to pick up calc_params sim_input = { 'asimmodule': 'do_nothing', 'env_id': 'inline', 'workdir': str(tmp_path / 'wdir'), - 'args': {'calc_id': 'lj'}, + 'args': {'calculator': {'calc_id': 'lj'}}, } unitjob = UnitJob(sim_input, env_input=env_input, calc_input=calc_input) cmd = unitjob.gen_run_command() From 9e076463624f659a0e36909290130fb9e47d97d6 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Thu, 7 May 2026 14:10:50 -0700 Subject: [PATCH 48/78] Update singlepoint to use calculator dict interface Replace calc_id parameter with calculator dict, matching the new load_calc interface. Callers now pass calculator={'calc_id': ...} or calculator={'calc_params': ...}. Co-Authored-By: Claude Sonnet 4.6 --- src/asimtools/asimmodules/singlepoint.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/asimtools/asimmodules/singlepoint.py b/src/asimtools/asimmodules/singlepoint.py index e430189..55151e4 100755 --- a/src/asimtools/asimmodules/singlepoint.py +++ b/src/asimtools/asimmodules/singlepoint.py @@ -14,7 +14,7 @@ ) def singlepoint( - calc_id: str, + calculator: Dict, image: Dict, properties: Tuple[str] = ('energy', 'forces'), prefix: Optional[str] = None, @@ -22,8 +22,10 @@ def singlepoint( """Evaluates the properties of a single image, currently implemented properties are energy, forces and stress - :param calc_id: calc_id specification - :type calc_id: str + :param calculator: Calculator specification with either a ``calc_id`` key + to look up from the global calc_input or a ``calc_params`` key to use + directly, see :func:`asimtools.calculators.load_calc` + :type calculator: Dict :param image: Image specification, see :func:`asimtools.utils.get_atoms` :type image: Dict :param properties: properties to evaluate, defaults to ('energy', 'forces') @@ -31,7 +33,7 @@ def singlepoint( :return: Dictionary of results :rtype: Dict """ - calc = load_calc(calc_id) + calc = load_calc(calculator=calculator) atoms = get_atoms(**image) atoms.calc = calc From 903f01b6a6dad4ab5626350bb2aba697cebb5f64 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Thu, 7 May 2026 14:11:11 -0700 Subject: [PATCH 49/78] Remove dead calc_id assignment from create_unitjob helper sim_input['calc_id'] at top level was never read by UnitJob (which reads from sim_input['args']). The calc_input is still passed directly to UnitJob so it remains available for lookup. Co-Authored-By: Claude Sonnet 4.6 --- src/asimtools/job.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/asimtools/job.py b/src/asimtools/job.py index e82f271..2f96f3d 100644 --- a/src/asimtools/job.py +++ b/src/asimtools/job.py @@ -1047,9 +1047,6 @@ def create_unitjob(sim_input, env_input, workdir, calc_input=None): """Helper for making a generic UnitJob object, mostly for testing""" env_id = list(env_input.keys())[0] sim_input['env_id'] = env_id - if calc_input is not None: - calc_id = list(calc_input.keys())[0] - sim_input['calc_id'] = calc_id sim_input['workdir'] = workdir unitjob = UnitJob( sim_input, From 7c3fa9af3f27669f62bdfd7d131f23f4eb369c88 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Thu, 7 May 2026 14:26:06 -0700 Subject: [PATCH 50/78] Update atom_relax to use calculator dict interface Co-Authored-By: Claude Sonnet 4.6 --- .../asimmodules/geometry_optimization/atom_relax.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/asimtools/asimmodules/geometry_optimization/atom_relax.py b/src/asimtools/asimmodules/geometry_optimization/atom_relax.py index a777fcf..f39732a 100755 --- a/src/asimtools/asimmodules/geometry_optimization/atom_relax.py +++ b/src/asimtools/asimmodules/geometry_optimization/atom_relax.py @@ -12,7 +12,7 @@ from asimtools.utils import get_atoms, get_logger, write_atoms def atom_relax( - calc_id: str, + calculator: Dict, image: Dict, optimizer: str = 'GPMin', #GPMin is fast in many cases according to ASE docs properties: Tuple[str] = ('energy', 'forces'), @@ -23,8 +23,8 @@ def atom_relax( """Relaxes the given tomic structure using ASE's built-in structure optimizers - :param calc_id: calc_id specification - :type calc_id: str + :param calculator: Calculator specification, see :func:`asimtools.calculators.load_calc` + :type calculator: Dict :param image: Image specification, see :func:`asimtools.utils.get_atoms` :type image: Dict :param prefix: Prefix of output files, defaults to '' @@ -38,7 +38,7 @@ def atom_relax( :return: Dictionary of results :rtype: Dict """ - calc = load_calc(calc_id) + calc = load_calc(calculator=calculator) atoms = get_atoms(**image) atoms.calc = calc logger = get_logger() From 497a23c66ed4990943e1c458fd2756cdfaff994c Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Thu, 7 May 2026 14:26:51 -0700 Subject: [PATCH 51/78] Update cell_relax to use calculator dict interface Co-Authored-By: Claude Sonnet 4.6 --- .../asimmodules/geometry_optimization/cell_relax.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/asimtools/asimmodules/geometry_optimization/cell_relax.py b/src/asimtools/asimmodules/geometry_optimization/cell_relax.py index d51b1ba..a0946a1 100755 --- a/src/asimtools/asimmodules/geometry_optimization/cell_relax.py +++ b/src/asimtools/asimmodules/geometry_optimization/cell_relax.py @@ -17,7 +17,7 @@ from asimtools.utils import get_atoms, join_names, write_atoms def cell_relax( - calc_id: str, + calculator: Dict, image: Dict, optimizer: str = 'BFGS', fmax: float = 0.002, @@ -27,8 +27,8 @@ def cell_relax( ) -> Dict: """Relax cell using ASE Optimizer - :param calc_id: calc_id specification - :type calc_id: str + :param calculator: Calculator specification, see :func:`asimtools.calculators.load_calc` + :type calculator: Dict :param image: Image specification, see :func:`asimtools.utils.get_atoms` :type image: Dict :param prefix: Prefix to output files, defaults to '' @@ -44,7 +44,7 @@ def cell_relax( :return: Dictionary of results including, final energy, stress and output files :rtype: Dict """ - calc = load_calc(calc_id) + calc = load_calc(calculator=calculator) atoms = get_atoms(**image) atoms.calc = calc From 61a22c554b41bae744fc7511b4f9e6065b3c4d1e Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Thu, 7 May 2026 14:27:42 -0700 Subject: [PATCH 52/78] Update symmetric_cell_relax to use calculator dict interface Co-Authored-By: Claude Sonnet 4.6 --- .../geometry_optimization/symmetric_cell_relax.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/asimtools/asimmodules/geometry_optimization/symmetric_cell_relax.py b/src/asimtools/asimmodules/geometry_optimization/symmetric_cell_relax.py index 838108e..221c0b3 100755 --- a/src/asimtools/asimmodules/geometry_optimization/symmetric_cell_relax.py +++ b/src/asimtools/asimmodules/geometry_optimization/symmetric_cell_relax.py @@ -13,7 +13,7 @@ from asimtools.utils import get_atoms, write_atoms def symmetric_cell_relax( - calc_id: str, + calculator: Dict, image: Dict, optimizer: str = 'BFGS', fmax: float = 0.003, #Roughly 0.48GPa @@ -23,8 +23,8 @@ def symmetric_cell_relax( ) -> Dict: """Relaxes cell (and atoms) using ase.constraints.ExpCellFilter while retaining symmetry - :param calc_id: calc_id specification - :type calc_id: str + :param calculator: Calculator specification, see :func:`asimtools.calculators.load_calc` + :type calculator: Dict :param image: Image specification, see :func:`asimtools.utils.get_atoms` :type image: Dict :param optimizer: Any optimizer from ase.optimize, defaults to 'BFGS' @@ -45,7 +45,7 @@ def symmetric_cell_relax( if optimizer_args is None: optimizer_args = {} - calc = load_calc(calc_id) + calc = load_calc(calculator=calculator) atoms = get_atoms(**image) atoms.calc = calc From 2b6aadd67cbc09df9d2234feb04139fba9aa88a4 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Thu, 7 May 2026 14:28:43 -0700 Subject: [PATCH 53/78] Update ase_cubic_eos_optimization to use calculator dict interface Co-Authored-By: Claude Sonnet 4.6 --- .../geometry_optimization/ase_cubic_eos_optimization.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/asimtools/asimmodules/geometry_optimization/ase_cubic_eos_optimization.py b/src/asimtools/asimmodules/geometry_optimization/ase_cubic_eos_optimization.py index ef85f67..b0cd268 100644 --- a/src/asimtools/asimmodules/geometry_optimization/ase_cubic_eos_optimization.py +++ b/src/asimtools/asimmodules/geometry_optimization/ase_cubic_eos_optimization.py @@ -17,7 +17,7 @@ from asimtools.utils import get_atoms def ase_cubic_eos_optimization( - calc_id: str, + calculator: Dict, image: Dict, npoints: Optional[int] = 5, eos_string: Optional[str] = 'sj', @@ -27,8 +27,8 @@ def ase_cubic_eos_optimization( ) -> Dict: """Generate the energy-volume equation of state (energy calculations not parallelized) - :param calc_id: calc_id specification - :type calc_id: str + :param calculator: Calculator specification, see :func:`asimtools.calculators.load_calc` + :type calculator: Dict :param image: Image specification, see :func:`asimtools.utils.get_atoms` :type image: Dict :param npoints: Number of energy points to calculate, must be >5, defaults to 5 @@ -44,7 +44,7 @@ def ase_cubic_eos_optimization( :return: Equilibrium energy, volume, bulk modulus and factor by which to scale lattice parameter to get equilibrium structure :rtype: Dict """ - calc = load_calc(calc_id) + calc = load_calc(calculator=calculator) atoms = get_atoms(**image) atoms.calc = calc v_init = atoms.get_volume() From 5654a6a5b732fa9cdca813bc575d2e1f93d198a6 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Thu, 7 May 2026 14:31:50 -0700 Subject: [PATCH 54/78] refactor(optimize): replace calc_id with calculator dict --- .../asimmodules/geometry_optimization/optimize.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/asimtools/asimmodules/geometry_optimization/optimize.py b/src/asimtools/asimmodules/geometry_optimization/optimize.py index e9a5d78..804e0f5 100755 --- a/src/asimtools/asimmodules/geometry_optimization/optimize.py +++ b/src/asimtools/asimmodules/geometry_optimization/optimize.py @@ -12,7 +12,7 @@ from asimtools.utils import get_atoms, write_atoms def optimize( - calc_id: str, + calculator: Dict, image: Dict, optimizer: str = 'BFGS', fmax: float = 0.003, #Roughly 0.48GPa @@ -22,8 +22,8 @@ def optimize( ) -> Dict: """Relaxes cell (and atoms) using ase.constraints.ExpCellFilter while retaining symmetry - :param calc_id: calc_id specification - :type calc_id: str + :param calculator: Calculator specification, see :func:`asimtools.calculators.load_calc` + :type calculator: Dict :param image: Image specification, see :func:`asimtools.utils.get_atoms` :type image: Dict :param optimizer: Any optimizer from ase.optimize, defaults to 'BFGS' @@ -44,7 +44,7 @@ def optimize( if optimizer_args is None: optimizer_args = {} - calc = load_calc(calc_id) + calc = load_calc(calculator=calculator) atoms = get_atoms(**image) atoms.calc = calc From 557ee9f93a1f8d92a0bea9f0ef19d27447c368ee Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Thu, 7 May 2026 14:32:01 -0700 Subject: [PATCH 55/78] refactor(ase_phonons): replace calc_id with calculator dict --- src/asimtools/asimmodules/phonons/ase_phonons.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/asimtools/asimmodules/phonons/ase_phonons.py b/src/asimtools/asimmodules/phonons/ase_phonons.py index 9a4638f..1f29c40 100755 --- a/src/asimtools/asimmodules/phonons/ase_phonons.py +++ b/src/asimtools/asimmodules/phonons/ase_phonons.py @@ -14,7 +14,7 @@ from asimtools.utils import get_atoms def ase_phonons( - calc_id: str, + calculator: Dict, image: Dict, path: str, delta: float = 0.01, @@ -23,8 +23,8 @@ def ase_phonons( ) -> Dict: """Calculates phonon spectrum and DOS using ASE - :param calc_id: calc_id specification - :type calc_id: str + :param calculator: Calculator specification, see :func:`asimtools.calculators.load_calc` + :type calculator: Dict :param image: Image specification, see :func:`asimtools.utils.get_atoms` :type image: Dict :param path: Path in BZ for plot @@ -40,7 +40,7 @@ def ase_phonons( """ atoms = get_atoms(**image) - calc = load_calc(calc_id) + calc = load_calc(calculator=calculator) ph = Phonons(atoms, calc, supercell=supercell, delta=delta) try: From a79cd347f767e302826406bdbb447ff9ece22869 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Thu, 7 May 2026 14:34:16 -0700 Subject: [PATCH 56/78] refactor(phonopy/forces): replace calc_id with calculator dict --- src/asimtools/asimmodules/phonopy/forces.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/asimtools/asimmodules/phonopy/forces.py b/src/asimtools/asimmodules/phonopy/forces.py index 41e47aa..fdc51cc 100644 --- a/src/asimtools/asimmodules/phonopy/forces.py +++ b/src/asimtools/asimmodules/phonopy/forces.py @@ -8,7 +8,7 @@ def forces( images: Dict, - calc_id: str, + calculator: Dict, calc_env_id: Optional[str] = None, **kwargs, ) -> Dict: @@ -16,8 +16,8 @@ def forces( :param images: Images specification, see :func:`asimtools.utils.get_images` :type images: Dict - :param calc_id: calc_id specification, see :func:`asimtools.utils.get_calc` - :type calc_id: str + :param calculator: Calculator specification, see :func:`asimtools.calculators.load_calc` + :type calculator: Dict :param calc_env_id: env_id to use for the calculator, defaults to None :type calc_env_id: Optional[str], optional :param kwargs: Additional keyword arguments to pass to image_array @@ -29,7 +29,7 @@ def forces( singlepoint_input={ 'asimmodule': 'singlepoint', 'args': { - 'calc_id': calc_id, + 'calculator': calculator, 'properties': ['energy', 'forces'] }, } From a3f00c515bc27725927e3118e2695cf062f51781 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Thu, 7 May 2026 14:34:31 -0700 Subject: [PATCH 57/78] refactor(phonopy/phonon_bands_and_dos): replace calc_id with calculator dict --- src/asimtools/asimmodules/phonopy/phonon_bands_and_dos.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/asimtools/asimmodules/phonopy/phonon_bands_and_dos.py b/src/asimtools/asimmodules/phonopy/phonon_bands_and_dos.py index 7721de8..f8b9b99 100644 --- a/src/asimtools/asimmodules/phonopy/phonon_bands_and_dos.py +++ b/src/asimtools/asimmodules/phonopy/phonon_bands_and_dos.py @@ -8,7 +8,7 @@ def phonon_bands_and_dos( image: Dict, - calc_id: str, + calculator: Dict, calc_env_id: str, process_env_id: str, supercell: ArrayLike = [10,10,10], @@ -26,8 +26,8 @@ def phonon_bands_and_dos( :param image: Image specification. See :ref:`asimtools.utils.get_image`. :type image: Dict - :param calc_id: calc_id of the calculator to use. - :type calc_id: str + :param calculator: Calculator specification, see :func:`asimtools.calculators.load_calc` + :type calculator: Dict :param calc_env_id: env_id of the calculator to use. :type calc_env_id: str :param process_env_id: env_id for pre- and post-processing. @@ -84,7 +84,7 @@ def phonon_bands_and_dos( 'pattern': '../step-0/supercell-*', 'format': 'vasp', }, - 'calc_id': calc_id, + 'calculator': calculator, }, }, 'step-2': { From 88379479aaea8cd9fbe1a81712dd0dfbe9675e86 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Thu, 7 May 2026 14:34:48 -0700 Subject: [PATCH 58/78] refactor(phonopy/full_qha): replace calc_id with calculator dict --- src/asimtools/asimmodules/phonopy/full_qha.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/asimtools/asimmodules/phonopy/full_qha.py b/src/asimtools/asimmodules/phonopy/full_qha.py index e1967be..8fedf53 100644 --- a/src/asimtools/asimmodules/phonopy/full_qha.py +++ b/src/asimtools/asimmodules/phonopy/full_qha.py @@ -4,7 +4,7 @@ def full_qha( image: Dict, - calc_id: str, + calculator: Dict, phonopy_save_path: Optional[str] = None, calc_env_id: Optional[str] = None, process_env_id: Optional[str] = None, @@ -20,8 +20,8 @@ def full_qha( :param image: Image specification, see :func:`asimtools.utils.get_atoms` :type image: Dict - :param calc_id: calc_id specification - :type calc_id: str + :param calculator: Calculator specification, see :func:`asimtools.calculators.load_calc` + :type calculator: Dict :param phonopy_save_path: Path where phonopy save yaml is saved, this file is important to keep for easier postprocessing/analsyis, we recommend keeping the default, defaults to None @@ -53,7 +53,7 @@ def full_qha( else: phonopy_save_path = str(Path(phonopy_save_path).resolve()) ase_cubic_eos_args['image'] = image - ase_cubic_eos_args['calc_id'] = calc_id + ase_cubic_eos_args['calculator'] = calculator scales = ase_cubic_eos_args.get('scales', False) if scales: npoints = len(scales) @@ -99,7 +99,7 @@ def full_qha( 'pattern': '../step-0/supercell-*', 'format': 'vasp', }, - 'calc_id': calc_id, + 'calculator': calculator, 'calc_env_id': calc_env_id, }, }, From 912249ac72366cfaa14f75084c5b4829fdcd6c51 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Thu, 7 May 2026 14:35:05 -0700 Subject: [PATCH 59/78] refactor(vacancy_formation_energy): replace calc_id with calculator dict --- .../vacancy_formation_energy.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/asimtools/asimmodules/vacancy_formation_energy/vacancy_formation_energy.py b/src/asimtools/asimmodules/vacancy_formation_energy/vacancy_formation_energy.py index c8a8f13..e72b332 100755 --- a/src/asimtools/asimmodules/vacancy_formation_energy/vacancy_formation_energy.py +++ b/src/asimtools/asimmodules/vacancy_formation_energy/vacancy_formation_energy.py @@ -15,7 +15,7 @@ ) def vacancy_formation_energy( - calc_id: str, + calculator: Dict, image: Dict, vacancy_index: int = 0, atom_relax_args: Optional[Dict] = None, @@ -24,8 +24,8 @@ def vacancy_formation_energy( ) -> Dict: """Calculates the monovacancy formation energy from a bulk structure - :param calc_id: calc_id specification - :type calc_id: str + :param calculator: Calculator specification, see :func:`asimtools.calculators.load_calc` + :type calculator: Dict :param image: Image specification, see :func:`asimtools.utils.get_atoms` :type image: Dict :param vacancy_index: Index of atom to remove, defaults to 0 @@ -47,7 +47,7 @@ def vacancy_formation_energy( :rtype: Dict """ - calc = load_calc(calc_id) + calc = load_calc(calculator=calculator) bulk = get_atoms(**image).repeat(repeat) bulk.calc = calc @@ -67,7 +67,7 @@ def vacancy_formation_energy( if atom_relax_args is not None: try: relax_results = atom_relax( - calc_id=calc_id, + calculator=calculator, image={'atoms': vacant}, optimizer=atom_relax_args.get('optimizer', 'BFGS'), properties=('energy','forces'), @@ -82,7 +82,7 @@ def vacancy_formation_energy( else: try: relax_results = optimize( - calc_id=calc_id, + calculator=calculator, image={'atoms': vacant}, # optimizer=optimize_args.get('optimizer', 'BFGS'), # fmax=atom_relax_args.get('fmax', 0.003), From 03955adcdc91f8ead263ad55b0c7b80d8bd29cd0 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Thu, 7 May 2026 14:35:34 -0700 Subject: [PATCH 60/78] refactor(benchmarking/parity): replace calc_id with calculator dict --- .../asimmodules/benchmarking/parity.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/asimtools/asimmodules/benchmarking/parity.py b/src/asimtools/asimmodules/benchmarking/parity.py index d656aea..780fd63 100644 --- a/src/asimtools/asimmodules/benchmarking/parity.py +++ b/src/asimtools/asimmodules/benchmarking/parity.py @@ -24,7 +24,7 @@ Calculator = TypeVar('Calculator') def calc_parity_data( subset: List, - calc_id: str, + calculator: Dict, properties: Sequence = ('energy', 'forces', 'stress'), force_prob: float = 1.0, ) -> Dict: @@ -32,8 +32,8 @@ def calc_parity_data( :param subset: List of atoms instances :type subset: List - :param calc_id: calc_id specification - :type calc_id: str + :param calculator: Calculator specification, see :func:`asimtools.calculators.load_calc` + :type calculator: Dict :param properties: Properties to evaluate, choose from "energy", \ "forces" and "stress", defaults to ('energy', 'forces', 'stress') :type properties: List, optional @@ -53,7 +53,7 @@ def calc_parity_data( srvals = [] spvals = [] for i, atoms in enumerate(tqdm(subset)): - calc = load_calc(calc_id) + calc = load_calc(calculator=calculator) patoms = atoms.copy() patoms.calc = calc n_atoms = len(atoms) @@ -128,7 +128,7 @@ def rmse(yhat: Sequence, y: Sequence) -> float: def parity( images: Dict, - calc_id: str, + calculator: Dict, force_prob: float = 1.0, nprocs: int = 1, unit: str = 'meV', @@ -140,8 +140,8 @@ def parity( :param images: Images specification, see :func:`asimtools.utils.get_images` :type images: Dict - :param calc_id: ID of calculator provided in calc_input or global file - :type calc_id: str + :param calculator: Calculator specification, see :func:`asimtools.calculators.load_calc` + :type calculator: Dict :param force_prob: Fraction of forces to consider in force parity, \ can be used for speeding up large structures by only subsampling\ randomly, defaults to 1.0 @@ -180,7 +180,7 @@ def parity( with Pool(nprocs) as pool: reses = pool.map(partial( calc_parity_data, - calc_id=calc_id, + calculator=calculator, properties=properties, force_prob=force_prob, ), @@ -189,7 +189,7 @@ def parity( else: reses = [calc_parity_data( subset, - calc_id=calc_id, + calculator=calculator, properties=properties, force_prob=force_prob, ) for subset in subsets From b167d7e83b08d3bb62b1ffd400c7c11c4329e76a Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Thu, 7 May 2026 14:35:50 -0700 Subject: [PATCH 61/78] refactor(elastic_constants/cubic_energy_expansion): replace calc_id with calculator dict --- .../elastic_constants/cubic_energy_expansion.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/asimtools/asimmodules/elastic_constants/cubic_energy_expansion.py b/src/asimtools/asimmodules/elastic_constants/cubic_energy_expansion.py index 819e38c..d97d52f 100755 --- a/src/asimtools/asimmodules/elastic_constants/cubic_energy_expansion.py +++ b/src/asimtools/asimmodules/elastic_constants/cubic_energy_expansion.py @@ -80,16 +80,16 @@ def get_strained_atoms(atoms, strain: str, delta: float): return strained_atoms def cubic_energy_expansion( - calc_id: str, + calculator: Dict, image: Dict, deltas: Sequence[float] = (-0.01,-0.0075,-0.005,0.00,0.005,0.0075,0.01), ase_cubic_eos_args: Optional[Dict] = None, ) -> Dict: - """Calculates B (Bulk modulus), C11, C12 and C44 elastic constants of + """Calculates B (Bulk modulus), C11, C12 and C44 elastic constants of a structure with cubic symmetry - :param calc_id: calc_id specification - :type calc_id: str + :param calculator: Calculator specification, see :func:`asimtools.calculators.load_calc` + :type calculator: Dict :param image: Image specification, see :func:`asimtools.utils.get_atoms` :type image: Dict :param deltas: strains to apply in each direction, defaults to (-0.01,-0.0075,-0.005,0.00,0.005,0.0075,0.01) @@ -99,13 +99,13 @@ def cubic_energy_expansion( :return: Elastic constant results :rtype: Dict """ - calc = load_calc(calc_id) + calc = load_calc(calculator=calculator) atoms = get_atoms(**image) atoms.calc = calc # Start by getting the Bulk modulus and optimized cell from the EOS logging.info('Calculating EOS') - eos_kwargs = {'image': image, 'calc_id': calc_id} + eos_kwargs = {'image': image, 'calculator': calculator} if ase_cubic_eos_args is not None: eos_kwargs.update(ase_cubic_eos_args) eos_results = eos(**eos_kwargs) @@ -122,7 +122,7 @@ def cubic_energy_expansion( c44_atoms = get_strained_atoms( atoms.copy(), 'mono_vol_cons', delta ) - calc = load_calc(calc_id) + calc = load_calc(calculator=calculator) c44_atoms.calc = calc c44_en = c44_atoms.get_potential_energy() c44_ens.append(c44_en) @@ -134,7 +134,7 @@ def cubic_energy_expansion( c11min12_atoms = get_strained_atoms( atoms.copy(), 'orth_vol_cons', delta ) - calc = load_calc(calc_id) + calc = load_calc(calculator=calculator) c11min12_atoms.calc = calc c11min12_en = c11min12_atoms.get_potential_energy() c11min12_ens.append(c11min12_en) From c59a54f20a79275e7cf76dfbb9dbcb9d32c29cf2 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Thu, 7 May 2026 14:36:08 -0700 Subject: [PATCH 62/78] refactor(surface_energies): replace calc_id with calculator dict --- .../surface_energies/surface_energies.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/asimtools/asimmodules/surface_energies/surface_energies.py b/src/asimtools/asimmodules/surface_energies/surface_energies.py index 1ebe77b..9585c33 100755 --- a/src/asimtools/asimmodules/surface_energies/surface_energies.py +++ b/src/asimtools/asimmodules/surface_energies/surface_energies.py @@ -38,7 +38,7 @@ def get_surface_energy(slab, calc, bulk_e_per_atom): def surface_energies( image: Dict, - calc_id: str = None, + calculator: Dict = None, millers: Union[str,Sequence] = 'all', atom_relax_args: Optional[Dict] = None, generate_all_slabs_args: Optional[Dict] = None, @@ -46,9 +46,8 @@ def surface_energies( """Calculates surface energies of slabs defined by args specified for pymatgen.core.surface.generate_all_slabs() - :param calc_id: Optional calc_id specification, See - :func:`asimtools.calculators.load_calc` - :type calc_id: str + :param calculator: Calculator specification, see :func:`asimtools.calculators.load_calc` + :type calculator: Dict :param image: Image specification, see :func:`asimtools.utils.get_atoms` :type image: Dict :param millers: List of miller indices to consider in the form 'xyz', @@ -65,7 +64,7 @@ def surface_energies( :rtype: Dict """ - calc = load_calc(calc_id) + calc = load_calc(calculator=calculator) bulk = get_atoms(**image) bulk.calc = calc @@ -104,7 +103,7 @@ def surface_energies( if atom_relax_args is not None: relax_results = atom_relax( - calc_id=calc_id, + calculator=calculator, image={'atoms': atoms}, optimizer=atom_relax_args.get('optimizer', 'BFGS'), properties=('energy','forces'), @@ -125,7 +124,7 @@ def surface_energies( f'Multiple terminations for {miller}' converged, surf_en, slab_en, area = get_surface_energy( - atoms, load_calc(calc_id), bulk_e_per_atom + atoms, load_calc(calculator=calculator), bulk_e_per_atom ) if converged: From 1ef633f7487291d9cf4e76e14bb7668b2a5717b7 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Thu, 7 May 2026 14:36:32 -0700 Subject: [PATCH 63/78] refactor(active_learning/compute_deviation): replace calc_ids with calculators list of dicts --- .../active_learning/compute_deviation.py | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/asimtools/asimmodules/active_learning/compute_deviation.py b/src/asimtools/asimmodules/active_learning/compute_deviation.py index f57c61f..ef62cbf 100644 --- a/src/asimtools/asimmodules/active_learning/compute_deviation.py +++ b/src/asimtools/asimmodules/active_learning/compute_deviation.py @@ -17,7 +17,7 @@ def compute_deviation( template_calc_params: Optional[Dict] = None, model_weights_key_sequence: Optional[Sequence] = None, model_weights_pattern: Optional[os.PathLike] = None, - calc_ids: Optional[Sequence] = None, + calculators: Optional[Sequence] = None, ) -> Dict: """Computes variance of properties from a trajectory file @@ -31,13 +31,14 @@ def compute_deviation( :param model_weights_pattern: Pattern of model weights files, defaults to None :type model_weights_pattern: Optional[os.PathLike] - :param calc_ids: List of calc_ids to use, if provided, all other arguments - are ignored, defaults to None - :type calc_ids: Optional[Sequence] + :param calculators: List of calculator dicts, each with 'calc_id' or + 'calc_params' key. If provided, all other arguments are ignored, + defaults to None + :type calculators: Optional[Sequence] """ properties = ['energy', 'forces', 'stress', 'energy_per_atom'] - if calc_ids is None: + if calculators is None: model_weights_files = natsorted(glob(model_weights_pattern)) calc_dict = {} @@ -49,9 +50,12 @@ def compute_deviation( return_copy=True ) - calc_dict[f'calc-{i}'] = new_calc_params + calc_dict[f'calc-{i}'] = {'calc_params': new_calc_params} else: - calc_dict = {calc_id: calc_id for calc_id in calc_ids} + calc_dict = {} + for i, calculator in enumerate(calculators): + label = calculator.get('calc_id', f'calc-{i}') + calc_dict[label] = calculator variances = {prop: {} for prop in properties} @@ -74,10 +78,7 @@ def compute_deviation( atom_results = {prop: [] for prop in properties} for calc_id in calc_dict: # Some calculators behave badly if not reloaded unfortunately - if isinstance(calc_dict[calc_id], str): - calc = load_calc(calc_id=calc_dict[calc_id]) - else: - calc = load_calc(calc_params=calc_dict[calc_id].copy()) + calc = load_calc(calculator=calc_dict[calc_id]) atoms.set_calculator(calc) energy = atoms.get_potential_energy(atoms) From 6bd0cea5d46d60e1cffd884861e6dff0e378bbc8 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Thu, 7 May 2026 14:54:33 -0700 Subject: [PATCH 64/78] refactor(workflows/calc_array): add calculator/template_calculator with backward compat for calc_ids/template_calc_id --- .../asimmodules/workflows/calc_array.py | 67 ++++++++++++------- 1 file changed, 43 insertions(+), 24 deletions(-) diff --git a/src/asimtools/asimmodules/workflows/calc_array.py b/src/asimtools/asimmodules/workflows/calc_array.py index 288025c..b94c7f8 100755 --- a/src/asimtools/asimmodules/workflows/calc_array.py +++ b/src/asimtools/asimmodules/workflows/calc_array.py @@ -19,6 +19,8 @@ def calc_array( subsim_input: Dict, calc_ids: Sequence[str] = None, template_calc_id: Optional[str] = None, + calculators: Sequence[Dict] = None, + template_calculator: Optional[Dict] = None, key_sequence: Optional[Sequence[str]] = None, array_values: Optional[Sequence] = None, file_pattern: Optional[str] = None, @@ -39,8 +41,20 @@ def calc_array( """Apply the same asimmodule using different calculators and if necessary different environments - :param calc_ids: Iterable with calc_ids, defaults to None + :param calc_ids: Deprecated. Use calculators instead. Iterable with + calc_ids, defaults to None :type calc_ids: Sequence, optional + :param template_calc_id: Deprecated. Use template_calculator instead. + calc_id of the template calculator, defaults to None + :type template_calc_id: str, optional + :param calculators: Sequence of calculator dicts, each with a 'calc_id' + or 'calc_params' key, see :func:`asimtools.calculators.load_calc`, + defaults to None + :type calculators: Sequence[Dict], optional + :param template_calculator: Calculator dict with 'calc_id' or 'calc_params' + key used as template when iterating over key_sequence values, see + :func:`asimtools.calculators.load_calc`, defaults to None + :type template_calculator: Optional[Dict], optional :param calc_input: Dictionary of calculator inputs :type calc_input: Dictionary, optional :param labels: Iterable with custom labels for each calc, defaults to None @@ -82,28 +96,36 @@ def calc_array( :return: Dictionary of results :rtype: Dict """ + # Backward compatibility: convert old-style params to new interface + if calculators is None and calc_ids is not None: + calculators = [{'calc_id': cid} for cid in calc_ids] + if template_calculator is None and template_calc_id is not None: + template_calculator = {'calc_id': template_calc_id} + print([ array_values, linspace_args, arange_args, file_pattern ]) using_array_values = key_sequence is not None\ - and template_calc_id is not None\ + and template_calculator is not None\ and [ array_values, linspace_args, arange_args, file_pattern ].count(None) == 3 - err_txt = 'Specify either a sequence of "calc_ids" or all of ' - err_txt += '"key_sequence", "template_calc_id" and one of [' + err_txt = 'Specify either a sequence of "calculators" or all of ' + err_txt += '"key_sequence", "template_calculator" and one of [' err_txt += '"array_values", "linspace_args", "arange_args", "file_pattern"' err_txt += '] to iterate over' - assert calc_ids is not None or using_array_values, err_txt + assert calculators is not None or using_array_values, err_txt if using_array_values: - if calc_input is None: - calc_input = get_calc_input() - calc_params = calc_input[template_calc_id] - new_calc_input = {} + if template_calculator.get('calc_params') is not None: + calc_params = template_calculator['calc_params'] + else: + if calc_input is None: + calc_input = get_calc_input() + calc_params = calc_input[template_calculator['calc_id']] - if calc_ids is not None and labels is None: - labels = calc_ids + if calculators is not None and labels is None: + labels = [c.get('calc_id', f'calc-{i}') for i, c in enumerate(calculators)] results = prepare_array_vals( key_sequence=key_sequence, @@ -124,6 +146,7 @@ def calc_array( secondary_array_values = results['secondary_array_values'] secondary_key_sequences = results['secondary_key_sequences'] + calculators = [] for i, val in enumerate(array_values): new_calc_params = change_dict_value( dct=calc_params, @@ -139,31 +162,27 @@ def calc_array( key_sequence=k, return_copy=False, ) + calculators.append({'calc_params': new_calc_params}) - new_calc_input[labels[i]] = new_calc_params - - calc_input = new_calc_input - calc_ids = labels - - elif calc_ids is not None: + elif calculators is not None: assert labels != 'get_str_btn', \ 'get_str_btn only works when using the key_sequence argument.' if labels is None or labels == 'values': - labels = calc_ids + labels = [c.get('calc_id', f'calc-{i}') for i, c in enumerate(calculators)] - assert len(labels) == len(calc_ids), \ - 'Num. of calc_ids or array_values must match num. of labels' + assert len(labels) == len(calculators), \ + 'Num. of calculators or array_values must match num. of labels' if env_ids is not None: - assert len(env_ids) == len(calc_ids) or isinstance(env_ids, str), \ - 'Provide one env_id or as many as there are calc_ids/array_values' + assert len(env_ids) == len(calculators) or isinstance(env_ids, str), \ + 'Provide one env_id or as many as there are calculators/array_values' array_sim_input = {} # Make individual sim_inputs for each calc - for i, calc_id in enumerate(calc_ids): + for i, calculator in enumerate(calculators): new_subsim_input = deepcopy(subsim_input) - new_subsim_input['args']['calc_id'] = calc_id + new_subsim_input['args']['calculator'] = calculator array_sim_input[f'{labels[i]}'] = new_subsim_input if env_ids is not None: From 094a35c799ff247b06fe34cf09df49974ae8b4dd Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Thu, 7 May 2026 15:45:17 -0700 Subject: [PATCH 65/78] refactor(examples): update all sim_input.yaml files to use calculator dict instead of calc_id --- .../external/CHGNet/atom_relax_sim_input.yaml | 3 ++- .../external/CHGNet/relax_results/sim_input.yaml | 3 ++- examples/external/GPAW/singlepoint_sim_input.yaml | 3 ++- .../external/M3GNEt/atom_relax_sim_input.yaml | 3 ++- .../external/M3GNEt/relax_results/sim_input.yaml | 3 ++- examples/external/MACE/atom_relax_sim_input.yaml | 3 ++- examples/external/MACE/full_qha_sim_input.yaml | 3 ++- .../MACE/mace-off_atom_relax_sim_input.yaml | 3 ++- .../external/OMAT24/atom_relax_sim_input.yaml | 3 ++- .../scf_singlepoint_sim_input.yaml | 3 ++- .../vc-relax_singlepoint_sim_input.yaml | 3 ++- examples/external/VASP/vasp_ase_sim_input.yaml | 3 ++- examples/external/VASP/vasp_sim_input.yaml | 3 ++- .../active_learning_sim_input.yaml | 3 ++- .../ase_cubic_eos/ase_cubic_eos_sim_input.yaml | 3 ++- .../array_ase_eos_sim_input.yaml | 3 ++- .../ase_cubic_eos_custom/ase_eos_sim_input.yaml | 3 ++- .../internal/atom_relax/atom_relax_sim_input.yaml | 3 ++- .../calc_array/calc_array_calc_id_sim_input.yaml | 4 +++- .../calc_array_elastic_constant_sim_input.yaml | 9 ++++++--- .../calc_array_secondary_sim_input.yaml | 3 ++- .../calc_array/calc_array_sigma_sim_input.yaml | 3 ++- .../internal/cell_relax/cell_relax_sim_input.yaml | 3 ++- .../chained/chained_batch_nested_sim_input.yaml | 9 ++++++--- .../internal/chained/chained_batch_sim_input.yaml | 9 ++++++--- examples/internal/chained/chained_sim_input.yaml | 9 ++++++--- .../internal/chained/chained_srun_sim_input.yaml | 9 ++++++--- .../chained/eos_chained_array_sim_input.yaml | 3 ++- .../distributed/distributed_bash_sim_input.yaml | 15 ++++++++++----- .../distributed/distributed_batch_sim_input.yaml | 15 ++++++++++----- .../distributed/distributed_fail_sim_input.yaml | 15 ++++++++++----- .../distributed_group_bash_sim_input.yaml | 15 ++++++++++----- .../distributed/distributed_mixed_sim_input.yaml | 15 ++++++++++----- .../distributed/distributed_sim_input.yaml | 15 ++++++++++----- .../elastic_constants_sim_input.yaml | 3 ++- .../image_array/image_array_batch_sim_input.yaml | 3 ++- .../image_array_key_sequence_sim_input.yaml | 3 ++- .../image_array/image_array_sim_input.yaml | 3 ++- .../iterative/array_cell_relax_sim_input.yaml | 3 ++- .../iterative_lattice_parameters_sim_input.yaml | 3 ++- .../sequential_cell_relax_sim_input.yaml | 3 ++- .../optimize_geometry/optimize_sim_input.yaml | 3 ++- examples/internal/phonons/phonons_sim_input.yaml | 3 ++- examples/internal/phonopy/full_qha_sim_input.yaml | 3 ++- .../phonopy/phonon_bands_and_dos_sim_input.yaml | 3 ++- .../sim_array_batch_calc_id_sim_input.yaml | 3 ++- ...im_array_batch_crystalstructure_sim_input.yaml | 3 ++- .../sim_array_batch_image_file_sim_input.yaml | 3 ++- ..._array_batch_lattice_parameters_sim_input.yaml | 3 ++- .../sim_array/sim_array_calc_id_sim_input.yaml | 5 +++-- ...ay_crystalstructure_placeholder_sim_input.yaml | 3 ++- ...rray_crystalstructure_secondary_sim_input.yaml | 3 ++- .../sim_array_crystalstructure_sim_input.yaml | 3 ++- .../sim_array/sim_array_image_file_sim_input.yaml | 3 ++- .../sim_array_lattice_parameters_sim_input.yaml | 3 ++- .../singlepoint/singlepoint_batch_sim_input.yaml | 3 ++- .../singlepoint/singlepoint_sim_input.yaml | 3 ++- .../singlepoint/singlepoint_srun_sim_input.yaml | 3 ++- .../surface_energies_sim_input.yaml | 3 ++- .../symmetric_cell_relax_sim_input.yaml | 3 ++- .../vacancy_formation_energy/vfe_sim_input.yaml | 3 ++- 61 files changed, 192 insertions(+), 96 deletions(-) diff --git a/examples/external/CHGNet/atom_relax_sim_input.yaml b/examples/external/CHGNet/atom_relax_sim_input.yaml index 54b0601..e85cf6f 100644 --- a/examples/external/CHGNet/atom_relax_sim_input.yaml +++ b/examples/external/CHGNet/atom_relax_sim_input.yaml @@ -2,7 +2,8 @@ asimmodule: geometry_optimization.atom_relax env_id: workdir: relax_results args: - calc_id: CHGNet + calculator: + calc_id: CHGNet image: name: NaCl builder: bulk diff --git a/examples/external/CHGNet/relax_results/sim_input.yaml b/examples/external/CHGNet/relax_results/sim_input.yaml index 0a7a0e6..ecd04cf 100644 --- a/examples/external/CHGNet/relax_results/sim_input.yaml +++ b/examples/external/CHGNet/relax_results/sim_input.yaml @@ -1,5 +1,6 @@ args: - calc_id: CHGNet + calculator: + calc_id: CHGNet fmax: 0.01 image: image_file: image_input.xyz diff --git a/examples/external/GPAW/singlepoint_sim_input.yaml b/examples/external/GPAW/singlepoint_sim_input.yaml index 5526cc9..27df8f2 100644 --- a/examples/external/GPAW/singlepoint_sim_input.yaml +++ b/examples/external/GPAW/singlepoint_sim_input.yaml @@ -3,7 +3,8 @@ env_id: batch workdir: gpaw_results overwrite: true args: - calc_id: gpaw + calculator: + calc_id: gpaw image: name: Ar builder: bulk diff --git a/examples/external/M3GNEt/atom_relax_sim_input.yaml b/examples/external/M3GNEt/atom_relax_sim_input.yaml index d8b2959..1e17f35 100644 --- a/examples/external/M3GNEt/atom_relax_sim_input.yaml +++ b/examples/external/M3GNEt/atom_relax_sim_input.yaml @@ -2,7 +2,8 @@ asimmodule: geometry_optimization.atom_relax env_id: workdir: relax_results args: - calc_id: M3GNet + calculator: + calc_id: M3GNet image: name: NaCl builder: bulk diff --git a/examples/external/M3GNEt/relax_results/sim_input.yaml b/examples/external/M3GNEt/relax_results/sim_input.yaml index e05508a..5c467cf 100644 --- a/examples/external/M3GNEt/relax_results/sim_input.yaml +++ b/examples/external/M3GNEt/relax_results/sim_input.yaml @@ -1,5 +1,6 @@ args: - calc_id: M3GNet + calculator: + calc_id: M3GNet fmax: 0.01 image: image_file: image_input.xyz diff --git a/examples/external/MACE/atom_relax_sim_input.yaml b/examples/external/MACE/atom_relax_sim_input.yaml index e5125a4..2bee1bd 100644 --- a/examples/external/MACE/atom_relax_sim_input.yaml +++ b/examples/external/MACE/atom_relax_sim_input.yaml @@ -1,7 +1,8 @@ asimmodule: geometry_optimization.atom_relax workdir: nacl_results args: - calc_id: MACE_cpu + calculator: + calc_id: MACE_cpu image: name: NaCl builder: bulk diff --git a/examples/external/MACE/full_qha_sim_input.yaml b/examples/external/MACE/full_qha_sim_input.yaml index 532ddbe..428c085 100644 --- a/examples/external/MACE/full_qha_sim_input.yaml +++ b/examples/external/MACE/full_qha_sim_input.yaml @@ -6,7 +6,8 @@ args: name: NaCl crystalstructure: rocksalt a: 5.641 - calc_id: MACE64_cpu + calculator: + calc_id: MACE64_cpu calc_env_id: inline process_env_id: inline phonopy_save_path: phonopy_save.yaml # Specify path relative to this asimmodule's workdir diff --git a/examples/external/MACE/mace-off_atom_relax_sim_input.yaml b/examples/external/MACE/mace-off_atom_relax_sim_input.yaml index f14cbda..314e29c 100644 --- a/examples/external/MACE/mace-off_atom_relax_sim_input.yaml +++ b/examples/external/MACE/mace-off_atom_relax_sim_input.yaml @@ -2,7 +2,8 @@ asimmodule: geometry_optimization.atom_relax env_id: workdir: cyclobutane_relax_results args: - calc_id: MACE-OFF + calculator: + calc_id: MACE-OFF image: name: cyclobutane builder: molecule diff --git a/examples/external/OMAT24/atom_relax_sim_input.yaml b/examples/external/OMAT24/atom_relax_sim_input.yaml index 5590d93..67f7873 100644 --- a/examples/external/OMAT24/atom_relax_sim_input.yaml +++ b/examples/external/OMAT24/atom_relax_sim_input.yaml @@ -1,7 +1,8 @@ asimmodule: geometry_optimization.atom_relax workdir: nacl_results args: - calc_id: OMAT24_eqV2_153M + calculator: + calc_id: OMAT24_eqV2_153M image: name: NaCl builder: bulk diff --git a/examples/external/QuantumEspresso/scf_singlepoint_sim_input.yaml b/examples/external/QuantumEspresso/scf_singlepoint_sim_input.yaml index 6cb3f34..b38cd44 100644 --- a/examples/external/QuantumEspresso/scf_singlepoint_sim_input.yaml +++ b/examples/external/QuantumEspresso/scf_singlepoint_sim_input.yaml @@ -1,7 +1,8 @@ asimmodule: singlepoint workdir: scf_results args: - calc_id: QE_Li + calculator: + calc_id: QE_Li image: name: Li builder: bulk diff --git a/examples/external/QuantumEspresso/vc-relax_singlepoint_sim_input.yaml b/examples/external/QuantumEspresso/vc-relax_singlepoint_sim_input.yaml index 5f0e4af..6a1bad0 100644 --- a/examples/external/QuantumEspresso/vc-relax_singlepoint_sim_input.yaml +++ b/examples/external/QuantumEspresso/vc-relax_singlepoint_sim_input.yaml @@ -1,7 +1,8 @@ asimmodule: singlepoint workdir: vc-relax_results args: - calc_id: QE_Li_vc-relax + calculator: + calc_id: QE_Li_vc-relax image: name: Li builder: bulk diff --git a/examples/external/VASP/vasp_ase_sim_input.yaml b/examples/external/VASP/vasp_ase_sim_input.yaml index 3c1191f..c3a3272 100644 --- a/examples/external/VASP/vasp_ase_sim_input.yaml +++ b/examples/external/VASP/vasp_ase_sim_input.yaml @@ -2,7 +2,8 @@ asimmodule: singlepoint workdir: vasp_ase_results env_id: n24 # Specify your own env args: - calc_id: vasp_PBE + calculator: + calc_id: vasp_PBE image: name: Na builder: bulk diff --git a/examples/external/VASP/vasp_sim_input.yaml b/examples/external/VASP/vasp_sim_input.yaml index cdee4e4..2134f74 100644 --- a/examples/external/VASP/vasp_sim_input.yaml +++ b/examples/external/VASP/vasp_sim_input.yaml @@ -2,7 +2,8 @@ asimmodule: singlepoint workdir: vasp_results env_id: n24 # Specify your own env args: - calc_id: vasp_PBE + calculator: + calc_id: vasp_PBE image: name: Na builder: bulk diff --git a/examples/external/active_learning/active_learning_sim_input.yaml b/examples/external/active_learning/active_learning_sim_input.yaml index 7befdae..3bad5e3 100644 --- a/examples/external/active_learning/active_learning_sim_input.yaml +++ b/examples/external/active_learning/active_learning_sim_input.yaml @@ -80,6 +80,7 @@ args: subsim_input: asimmodule: singlepoint args: - calc_id: lj_Ar + calculator: + calc_id: lj_Ar image: image_file: {} \ No newline at end of file diff --git a/examples/internal/ase_cubic_eos/ase_cubic_eos_sim_input.yaml b/examples/internal/ase_cubic_eos/ase_cubic_eos_sim_input.yaml index e175500..da20da2 100644 --- a/examples/internal/ase_cubic_eos/ase_cubic_eos_sim_input.yaml +++ b/examples/internal/ase_cubic_eos/ase_cubic_eos_sim_input.yaml @@ -1,7 +1,8 @@ asimmodule: geometry_optimization.ase_cubic_eos_optimization workdir: eos_results args: - calc_id: emt + calculator: + calc_id: emt image: name: Cu a: 3.59 diff --git a/examples/internal/ase_cubic_eos_custom/array_ase_eos_sim_input.yaml b/examples/internal/ase_cubic_eos_custom/array_ase_eos_sim_input.yaml index 4dbc5b9..84892ee 100644 --- a/examples/internal/ase_cubic_eos_custom/array_ase_eos_sim_input.yaml +++ b/examples/internal/ase_cubic_eos_custom/array_ase_eos_sim_input.yaml @@ -6,7 +6,8 @@ args: template_sim_input: asimmodule: ../../ase_eos.py args: - calc_id: emt + calculator: + calc_id: emt image: builder: bulk crystalstructure: fcc diff --git a/examples/internal/ase_cubic_eos_custom/ase_eos_sim_input.yaml b/examples/internal/ase_cubic_eos_custom/ase_eos_sim_input.yaml index ee1f78c..5ddd238 100644 --- a/examples/internal/ase_cubic_eos_custom/ase_eos_sim_input.yaml +++ b/examples/internal/ase_cubic_eos_custom/ase_eos_sim_input.yaml @@ -2,7 +2,8 @@ asimmodule: ../ase_eos.py env_id: inline workdir: Cu_results args: - calc_id: emt + calculator: + calc_id: emt image: name: Cu cubic: true diff --git a/examples/internal/atom_relax/atom_relax_sim_input.yaml b/examples/internal/atom_relax/atom_relax_sim_input.yaml index 36b32c6..f5ca60d 100644 --- a/examples/internal/atom_relax/atom_relax_sim_input.yaml +++ b/examples/internal/atom_relax/atom_relax_sim_input.yaml @@ -2,7 +2,8 @@ asimmodule: geometry_optimization.atom_relax env_id: workdir: relax_results args: - calc_id: lj_Ar + calculator: + calc_id: lj_Ar image: name: Ar builder: bulk diff --git a/examples/internal/calc_array/calc_array_calc_id_sim_input.yaml b/examples/internal/calc_array/calc_array_calc_id_sim_input.yaml index 986271f..0566b5e 100644 --- a/examples/internal/calc_array/calc_array_calc_id_sim_input.yaml +++ b/examples/internal/calc_array/calc_array_calc_id_sim_input.yaml @@ -2,7 +2,9 @@ asimmodule: workflows.calc_array overwrite: true workdir: calc_id_results args: - calc_ids: [emt, lj_Cu] + calculators: + - calc_id: emt + - calc_id: lj_Cu env_ids: [inline, inline] subsim_input: asimmodule: geometry_optimization.atom_relax diff --git a/examples/internal/calc_array/calc_array_elastic_constant_sim_input.yaml b/examples/internal/calc_array/calc_array_elastic_constant_sim_input.yaml index e6ac647..568f7f1 100644 --- a/examples/internal/calc_array/calc_array_elastic_constant_sim_input.yaml +++ b/examples/internal/calc_array/calc_array_elastic_constant_sim_input.yaml @@ -1,12 +1,15 @@ asimmodule: workflows.calc_array workdir: env_results args: - calc_ids: [emt, lj_Cu] - env_ids: [inline, inline] # Must correspond to calc_id if list is given + calculators: + - calc_id: emt + - calc_id: lj_Cu + env_ids: [inline, inline] # Must correspond to calculator if list is given subsim_input: asimmodule: elastic_constants.cubic_energy_expansion args: - calc_id: emt + calculator: + calc_id: emt image: name: Cu a: 3.6 diff --git a/examples/internal/calc_array/calc_array_secondary_sim_input.yaml b/examples/internal/calc_array/calc_array_secondary_sim_input.yaml index 794b19e..8ae72e8 100644 --- a/examples/internal/calc_array/calc_array_secondary_sim_input.yaml +++ b/examples/internal/calc_array/calc_array_secondary_sim_input.yaml @@ -1,7 +1,8 @@ asimmodule: workflows.calc_array workdir: secondary_results args: - template_calc_id: lj_Cu + template_calculator: + calc_id: lj_Cu key_sequence: ['args', 'sigma'] linspace_args: [2.3, 2.36, 3] secondary_key_sequences: [[args, 'epsilon']] diff --git a/examples/internal/calc_array/calc_array_sigma_sim_input.yaml b/examples/internal/calc_array/calc_array_sigma_sim_input.yaml index ffce902..f0f82a0 100644 --- a/examples/internal/calc_array/calc_array_sigma_sim_input.yaml +++ b/examples/internal/calc_array/calc_array_sigma_sim_input.yaml @@ -1,7 +1,8 @@ asimmodule: workflows.calc_array workdir: sigma_results args: - template_calc_id: lj_Cu + template_calculator: + calc_id: lj_Cu key_sequence: ['args', 'sigma'] array_values: [2.30, 2.33, 2.36] subsim_input: diff --git a/examples/internal/cell_relax/cell_relax_sim_input.yaml b/examples/internal/cell_relax/cell_relax_sim_input.yaml index fefaabe..4836641 100644 --- a/examples/internal/cell_relax/cell_relax_sim_input.yaml +++ b/examples/internal/cell_relax/cell_relax_sim_input.yaml @@ -1,7 +1,8 @@ asimmodule: geometry_optimization.cell_relax # The symmetry might change! workdir: cell_relax_results args: - calc_id: lj_Ar + calculator: + calc_id: lj_Ar image: name: Ar builder: bulk diff --git a/examples/internal/chained/chained_batch_nested_sim_input.yaml b/examples/internal/chained/chained_batch_nested_sim_input.yaml index f87a24f..b0423a9 100644 --- a/examples/internal/chained/chained_batch_nested_sim_input.yaml +++ b/examples/internal/chained/chained_batch_nested_sim_input.yaml @@ -12,7 +12,8 @@ args: env_id: batch job_name: relax args: - calc_id: lj_Ar + calculator: + calc_id: lj_Ar image: name: Ar crystalstructure: fcc @@ -37,7 +38,8 @@ args: asimmodule: geometry_optimization.cell_relax env_id: batch args: - calc_id: lj_Ar + calculator: + calc_id: lj_Ar image: image_file: ../step-0/id-0001__relax__/image_output.xyz fmax: 0.01 @@ -50,6 +52,7 @@ args: asimmodule: geometry_optimization.ase_cubic_eos_optimization env_id: batch args: - calc_id: lj_Ar + calculator: + calc_id: lj_Ar image: image_file: ../step-1/image_output.xyz diff --git a/examples/internal/chained/chained_batch_sim_input.yaml b/examples/internal/chained/chained_batch_sim_input.yaml index 20c9f58..94e3311 100644 --- a/examples/internal/chained/chained_batch_sim_input.yaml +++ b/examples/internal/chained/chained_batch_sim_input.yaml @@ -6,7 +6,8 @@ args: asimmodule: geometry_optimization.atom_relax env_id: batch args: - calc_id: lj_Ar + calculator: + calc_id: lj_Ar image: name: Ar crystalstructure: fcc @@ -19,7 +20,8 @@ args: asimmodule: geometry_optimization.cell_relax env_id: batch args: - calc_id: lj_Ar + calculator: + calc_id: lj_Ar image: image_file: ../step-0/image_output.xyz fmax: 0.01 @@ -32,6 +34,7 @@ args: asimmodule: geometry_optimization.ase_cubic_eos_optimization env_id: batch args: - calc_id: lj_Ar + calculator: + calc_id: lj_Ar image: image_file: ../step-1/image_output.xyz diff --git a/examples/internal/chained/chained_sim_input.yaml b/examples/internal/chained/chained_sim_input.yaml index 374b4ac..5cc58b2 100644 --- a/examples/internal/chained/chained_sim_input.yaml +++ b/examples/internal/chained/chained_sim_input.yaml @@ -5,7 +5,8 @@ args: step-0: asimmodule: geometry_optimization.atom_relax args: - calc_id: lj_Ar + calculator: + calc_id: lj_Ar image: name: Ar crystalstructure: fcc @@ -17,13 +18,15 @@ args: step-1: asimmodule: geometry_optimization.cell_relax args: - calc_id: lj_Ar + calculator: + calc_id: lj_Ar image: image_file: step-0/image_output.xyz fmax: 0.006 step-2: asimmodule: geometry_optimization.ase_cubic_eos_optimization args: - calc_id: lj_Ar + calculator: + calc_id: lj_Ar image: image_file: step-1/image_output.xyz diff --git a/examples/internal/chained/chained_srun_sim_input.yaml b/examples/internal/chained/chained_srun_sim_input.yaml index ae63f5c..a8a5b96 100644 --- a/examples/internal/chained/chained_srun_sim_input.yaml +++ b/examples/internal/chained/chained_srun_sim_input.yaml @@ -6,7 +6,8 @@ args: asimmodule: geometry_optimization.atom_relax env_id: srun args: - calc_id: lj_Ar + calculator: + calc_id: lj_Ar image: name: Ar crystalstructure: fcc @@ -19,7 +20,8 @@ args: asimmodule: geometry_optimization.cell_relax env_id: srun args: - calc_id: lj_Ar + calculator: + calc_id: lj_Ar image: image_file: step-0/image_output.xyz # for srun, files must be relative to chain root fmax: 0.006 @@ -32,6 +34,7 @@ args: asimmodule: geometry_optimization.ase_cubic_eos_optimization env_id: srun args: - calc_id: lj_Ar + calculator: + calc_id: lj_Ar image: image_file: step-1/image_output.xyz diff --git a/examples/internal/chained/eos_chained_array_sim_input.yaml b/examples/internal/chained/eos_chained_array_sim_input.yaml index aaac5cb..8834d41 100644 --- a/examples/internal/chained/eos_chained_array_sim_input.yaml +++ b/examples/internal/chained/eos_chained_array_sim_input.yaml @@ -20,7 +20,8 @@ args: asimmodule: singlepoint env_id: batch args: - calc_id: emt + calculator: + calc_id: emt # This step is introduced to connect step 1 and 3 since step # one submits jobs internally step-2: diff --git a/examples/internal/distributed/distributed_bash_sim_input.yaml b/examples/internal/distributed/distributed_bash_sim_input.yaml index be81ed5..62d3bb7 100644 --- a/examples/internal/distributed/distributed_bash_sim_input.yaml +++ b/examples/internal/distributed/distributed_bash_sim_input.yaml @@ -6,7 +6,8 @@ args: asimmodule: singlepoint env_id: bash #If all env_ids are slurm batch jobs using the same configuration, we automatically use arrays args: - calc_id: lj_Ar + calculator: + calc_id: lj_Ar image: name: Ar id-0001: @@ -14,7 +15,8 @@ args: asimmodule: geometry_optimization.atom_relax env_id: bash args: - calc_id: lj_Ar + calculator: + calc_id: lj_Ar image: name: Ar id-0002: @@ -22,12 +24,15 @@ args: asimmodule: workflows.calc_array env_id: bash args: - calc_ids: [emt, lj_Cu] - env_ids: [inline, inline] # Must correspond to calc_id if list is given + calculators: + - calc_id: emt + - calc_id: lj_Cu + env_ids: [inline, inline] # Must correspond to calculator if list is given subsim_input: asimmodule: elastic_constants.cubic_energy_expansion args: - calc_id: emt + calculator: + calc_id: emt image: name: Cu a: 3.6 diff --git a/examples/internal/distributed/distributed_batch_sim_input.yaml b/examples/internal/distributed/distributed_batch_sim_input.yaml index ce7fdf6..7d7b86a 100644 --- a/examples/internal/distributed/distributed_batch_sim_input.yaml +++ b/examples/internal/distributed/distributed_batch_sim_input.yaml @@ -7,7 +7,8 @@ args: asimmodule: singlepoint env_id: batch #If all env_ids are slurm batch jobs using the same configuration, we automatically use arrays args: - calc_id: lj_Ar + calculator: + calc_id: lj_Ar image: name: Ar id-0001: @@ -15,7 +16,8 @@ args: asimmodule: geometry_optimization.atom_relax env_id: batch args: - calc_id: lj_Ar + calculator: + calc_id: lj_Ar image: name: Ar id-0002: @@ -23,12 +25,15 @@ args: asimmodule: workflows.calc_array env_id: batch args: - calc_ids: [emt, lj_Cu] - env_ids: [inline, inline] # Must correspond to calc_id if list is given + calculators: + - calc_id: emt + - calc_id: lj_Cu + env_ids: [inline, inline] # Must correspond to calculator if list is given subsim_input: asimmodule: elastic_constants.cubic_energy_expansion args: - calc_id: emt + calculator: + calc_id: emt image: name: Cu a: 3.6 diff --git a/examples/internal/distributed/distributed_fail_sim_input.yaml b/examples/internal/distributed/distributed_fail_sim_input.yaml index aad93ec..3824f32 100644 --- a/examples/internal/distributed/distributed_fail_sim_input.yaml +++ b/examples/internal/distributed/distributed_fail_sim_input.yaml @@ -6,7 +6,8 @@ args: id-0000: asimmodule: singlepoint args: - calc_id: lj_Ar + calculator: + calc_id: lj_Ar image: name: Ar properties: [bad_property] @@ -14,7 +15,8 @@ args: # Doesn't have to be same asimmodule asimmodule: geometry_optimization.atom_relax args: - calc_id: lj_Ar + calculator: + calc_id: lj_Ar image: name: Ar optimizer: BFGS @@ -22,12 +24,15 @@ args: # This is just the calc_array example copied and pasted! asimmodule: workflows.calc_array args: - calc_ids: [emt, lj_Cu] - env_ids: [inline, inline] # Must correspond to calc_id if list is given + calculators: + - calc_id: emt + - calc_id: lj_Cu + env_ids: [inline, inline] # Must correspond to calculator if list is given subsim_input: asimmodule: elastic_constants.cubic_energy_expansion args: - calc_id: emt + calculator: + calc_id: emt image: name: Cu a: 3.6 diff --git a/examples/internal/distributed/distributed_group_bash_sim_input.yaml b/examples/internal/distributed/distributed_group_bash_sim_input.yaml index 1fc42ca..0e0c7f4 100644 --- a/examples/internal/distributed/distributed_group_bash_sim_input.yaml +++ b/examples/internal/distributed/distributed_group_bash_sim_input.yaml @@ -8,7 +8,8 @@ args: asimmodule: singlepoint env_id: batch #If all env_ids are slurm batch jobs using the same configuration, we automatically use arrays args: - calc_id: lj_Ar + calculator: + calc_id: lj_Ar image: name: Ar id-0001: @@ -16,7 +17,8 @@ args: asimmodule: geometry_optimization.atom_relax env_id: batch args: - calc_id: lj_Ar + calculator: + calc_id: lj_Ar image: name: Ar id-0002: @@ -24,12 +26,15 @@ args: asimmodule: workflows.calc_array env_id: batch args: - calc_ids: [emt, lj_Cu] - env_ids: [inline, inline] # Must correspond to calc_id if list is given + calculators: + - calc_id: emt + - calc_id: lj_Cu + env_ids: [inline, inline] # Must correspond to calculator if list is given subsim_input: asimmodule: elastic_constants.cubic_energy_expansion args: - calc_id: emt + calculator: + calc_id: emt image: name: Cu a: 3.6 diff --git a/examples/internal/distributed/distributed_mixed_sim_input.yaml b/examples/internal/distributed/distributed_mixed_sim_input.yaml index a9319cb..52276c6 100644 --- a/examples/internal/distributed/distributed_mixed_sim_input.yaml +++ b/examples/internal/distributed/distributed_mixed_sim_input.yaml @@ -6,7 +6,8 @@ args: asimmodule: singlepoint env_id: inline # No array since one of them is not a slurm batch job args: - calc_id: lj_Ar + calculator: + calc_id: lj_Ar image: name: Ar id-0001: @@ -14,7 +15,8 @@ args: asimmodule: geometry_optimization.atom_relax env_id: batch args: - calc_id: lj_Ar + calculator: + calc_id: lj_Ar image: name: Ar id-0002: @@ -22,12 +24,15 @@ args: asimmodule: workflows.calc_array env_id: batch args: - calc_ids: [emt, lj_Cu] - env_ids: [inline, inline] # Must correspond to calc_id if list is given + calculators: + - calc_id: emt + - calc_id: lj_Cu + env_ids: [inline, inline] # Must correspond to calculator if list is given subsim_input: asimmodule: elastic_constants.cubic_energy_expansion args: - calc_id: emt + calculator: + calc_id: emt image: name: Cu a: 3.6 diff --git a/examples/internal/distributed/distributed_sim_input.yaml b/examples/internal/distributed/distributed_sim_input.yaml index 60e1c39..f6a93df 100644 --- a/examples/internal/distributed/distributed_sim_input.yaml +++ b/examples/internal/distributed/distributed_sim_input.yaml @@ -5,14 +5,16 @@ args: id-0000: asimmodule: singlepoint args: - calc_id: lj_Ar + calculator: + calc_id: lj_Ar image: name: Ar id-0001: # Doesn't have to be same asimmodule asimmodule: geometry_optimization.atom_relax args: - calc_id: lj_Ar + calculator: + calc_id: lj_Ar image: name: Ar optimizer: BFGS @@ -20,12 +22,15 @@ args: # This is just the calc_array example copied and pasted! asimmodule: workflows.calc_array args: - calc_ids: [emt, lj_Cu] - env_ids: [inline, inline] # Must correspond to calc_id if list is given + calculators: + - calc_id: emt + - calc_id: lj_Cu + env_ids: [inline, inline] # Must correspond to calculator if list is given subsim_input: asimmodule: elastic_constants.cubic_energy_expansion args: - calc_id: emt + calculator: + calc_id: emt image: name: Cu a: 3.6 diff --git a/examples/internal/elastic_constants/elastic_constants_sim_input.yaml b/examples/internal/elastic_constants/elastic_constants_sim_input.yaml index 3b935fb..463053a 100644 --- a/examples/internal/elastic_constants/elastic_constants_sim_input.yaml +++ b/examples/internal/elastic_constants/elastic_constants_sim_input.yaml @@ -1,7 +1,8 @@ asimmodule: elastic_constants.cubic_energy_expansion workdir: elastic_constant_results args: - calc_id: emt + calculator: + calc_id: emt image: # Does not need to be relaxed, will use energy minimum volume from eos name: Cu a: 3.59 diff --git a/examples/internal/image_array/image_array_batch_sim_input.yaml b/examples/internal/image_array/image_array_batch_sim_input.yaml index fb638c8..0f2e444 100644 --- a/examples/internal/image_array/image_array_batch_sim_input.yaml +++ b/examples/internal/image_array/image_array_batch_sim_input.yaml @@ -9,6 +9,7 @@ args: env_id: batch args: image: {} - calc_id: lj_Ar + calculator: + calc_id: lj_Ar properties: ['energy', 'forces'] diff --git a/examples/internal/image_array/image_array_key_sequence_sim_input.yaml b/examples/internal/image_array/image_array_key_sequence_sim_input.yaml index 9680553..1e9023b 100644 --- a/examples/internal/image_array/image_array_key_sequence_sim_input.yaml +++ b/examples/internal/image_array/image_array_key_sequence_sim_input.yaml @@ -12,6 +12,7 @@ args: asimmodule: singlepoint args: image: {} - calc_id: lj_Ar + calculator: + calc_id: lj_Ar properties: ['energy', 'forces'] diff --git a/examples/internal/image_array/image_array_sim_input.yaml b/examples/internal/image_array/image_array_sim_input.yaml index 4960c3a..451ddb8 100644 --- a/examples/internal/image_array/image_array_sim_input.yaml +++ b/examples/internal/image_array/image_array_sim_input.yaml @@ -7,6 +7,7 @@ args: subsim_input: asimmodule: singlepoint args: - calc_id: lj_Ar + calculator: + calc_id: lj_Ar properties: ['energy', 'forces'] diff --git a/examples/internal/iterative/array_cell_relax_sim_input.yaml b/examples/internal/iterative/array_cell_relax_sim_input.yaml index 8742953..6b48ae6 100644 --- a/examples/internal/iterative/array_cell_relax_sim_input.yaml +++ b/examples/internal/iterative/array_cell_relax_sim_input.yaml @@ -7,7 +7,8 @@ args: template_sim_input: asimmodule: geometry_optimization.symmetric_cell_relax args: - calc_id: lj_Ar + calculator: + calc_id: lj_Ar image: image_file: ../Ar.cif optimizer: BFGS diff --git a/examples/internal/iterative/iterative_lattice_parameters_sim_input.yaml b/examples/internal/iterative/iterative_lattice_parameters_sim_input.yaml index 7371fd3..ecfe0d8 100644 --- a/examples/internal/iterative/iterative_lattice_parameters_sim_input.yaml +++ b/examples/internal/iterative/iterative_lattice_parameters_sim_input.yaml @@ -7,7 +7,8 @@ args: template_sim_input: asimmodule: singlepoint args: - calc_id: lj_Cu + calculator: + calc_id: lj_Cu image: builder: bulk name: Cu diff --git a/examples/internal/iterative/sequential_cell_relax_sim_input.yaml b/examples/internal/iterative/sequential_cell_relax_sim_input.yaml index e84e8eb..2af4697 100644 --- a/examples/internal/iterative/sequential_cell_relax_sim_input.yaml +++ b/examples/internal/iterative/sequential_cell_relax_sim_input.yaml @@ -9,7 +9,8 @@ args: template_sim_input: asimmodule: geometry_optimization.symmetric_cell_relax args: - calc_id: lj_Ar + calculator: + calc_id: lj_Ar image: image_file: ../Ar.cif optimizer: BFGS diff --git a/examples/internal/optimize_geometry/optimize_sim_input.yaml b/examples/internal/optimize_geometry/optimize_sim_input.yaml index c32268f..e8ed169 100644 --- a/examples/internal/optimize_geometry/optimize_sim_input.yaml +++ b/examples/internal/optimize_geometry/optimize_sim_input.yaml @@ -1,7 +1,8 @@ asimmodule: geometry_optimization.optimize workdir: optimize_results args: - calc_id: lj_Ar + calculator: + calc_id: lj_Ar image: name: Ar crystalstructure: fcc diff --git a/examples/internal/phonons/phonons_sim_input.yaml b/examples/internal/phonons/phonons_sim_input.yaml index e53e9c4..a76a349 100644 --- a/examples/internal/phonons/phonons_sim_input.yaml +++ b/examples/internal/phonons/phonons_sim_input.yaml @@ -1,7 +1,8 @@ asimmodule: phonons.ase_phonons workdir: phonons_results args: - calc_id: emt + calculator: + calc_id: emt image: name: Cu cubic: true diff --git a/examples/internal/phonopy/full_qha_sim_input.yaml b/examples/internal/phonopy/full_qha_sim_input.yaml index 4210476..4c9e788 100644 --- a/examples/internal/phonopy/full_qha_sim_input.yaml +++ b/examples/internal/phonopy/full_qha_sim_input.yaml @@ -3,7 +3,8 @@ workdir: full_qha_results args: image: name: Cu - calc_id: emt + calculator: + calc_id: emt calc_env_id: inline process_env_id: inline phonopy_save_path: phonopy_save.yaml # Specify path relative to this asimmodule's workdir diff --git a/examples/internal/phonopy/phonon_bands_and_dos_sim_input.yaml b/examples/internal/phonopy/phonon_bands_and_dos_sim_input.yaml index 9471a86..521402a 100644 --- a/examples/internal/phonopy/phonon_bands_and_dos_sim_input.yaml +++ b/examples/internal/phonopy/phonon_bands_and_dos_sim_input.yaml @@ -7,7 +7,8 @@ args: distance: 0.02 phonopy_save_path: phonopy.save calc_env_id: inline - calc_id: emt + calculator: + calc_id: emt process_env_id: inline # paths: ['GAMMA', 'X', 'GAMMA', 'L'] # labels: Optional[Sequence] = None, diff --git a/examples/internal/sim_array/sim_array_batch_calc_id_sim_input.yaml b/examples/internal/sim_array/sim_array_batch_calc_id_sim_input.yaml index 3f533d4..8a11a00 100644 --- a/examples/internal/sim_array/sim_array_batch_calc_id_sim_input.yaml +++ b/examples/internal/sim_array/sim_array_batch_calc_id_sim_input.yaml @@ -7,7 +7,8 @@ args: template_sim_input: asimmodule: singlepoint args: - calc_id: lj_Cu + calculator: + calc_id: lj_Cu image: builder: bulk name: Cu diff --git a/examples/internal/sim_array/sim_array_batch_crystalstructure_sim_input.yaml b/examples/internal/sim_array/sim_array_batch_crystalstructure_sim_input.yaml index 71baba5..7044d71 100644 --- a/examples/internal/sim_array/sim_array_batch_crystalstructure_sim_input.yaml +++ b/examples/internal/sim_array/sim_array_batch_crystalstructure_sim_input.yaml @@ -16,7 +16,8 @@ args: template_sim_input: asimmodule: singlepoint args: - calc_id: lj_Cu + calculator: + calc_id: lj_Cu image: {} # You can set a placeholder here or not specify. # I usually put something that would cause an error to make sure # it is substituted correctly diff --git a/examples/internal/sim_array/sim_array_batch_image_file_sim_input.yaml b/examples/internal/sim_array/sim_array_batch_image_file_sim_input.yaml index f9cecfc..517e0bc 100644 --- a/examples/internal/sim_array/sim_array_batch_image_file_sim_input.yaml +++ b/examples/internal/sim_array/sim_array_batch_image_file_sim_input.yaml @@ -8,7 +8,8 @@ args: template_sim_input: asimmodule: singlepoint args: - calc_id: lj_Cu + calculator: + calc_id: lj_Cu image: image_file: {} \ No newline at end of file diff --git a/examples/internal/sim_array/sim_array_batch_lattice_parameters_sim_input.yaml b/examples/internal/sim_array/sim_array_batch_lattice_parameters_sim_input.yaml index b253ffa..30434dd 100644 --- a/examples/internal/sim_array/sim_array_batch_lattice_parameters_sim_input.yaml +++ b/examples/internal/sim_array/sim_array_batch_lattice_parameters_sim_input.yaml @@ -7,7 +7,8 @@ args: template_sim_input: asimmodule: singlepoint args: - calc_id: lj_Cu + calculator: + calc_id: lj_Cu image: builder: bulk name: Cu diff --git a/examples/internal/sim_array/sim_array_calc_id_sim_input.yaml b/examples/internal/sim_array/sim_array_calc_id_sim_input.yaml index 02357bf..ca03f79 100644 --- a/examples/internal/sim_array/sim_array_calc_id_sim_input.yaml +++ b/examples/internal/sim_array/sim_array_calc_id_sim_input.yaml @@ -1,13 +1,14 @@ asimmodule: workflows.sim_array workdir: calc_id_results args: - key_sequence: [args, calc_id] + key_sequence: [args, calculator, calc_id] array_values: [emt, lj_Cu] env_ids: [inline, inline] template_sim_input: asimmodule: singlepoint args: - calc_id: {} + calculator: + calc_id: {} image: builder: bulk name: Cu diff --git a/examples/internal/sim_array/sim_array_crystalstructure_placeholder_sim_input.yaml b/examples/internal/sim_array/sim_array_crystalstructure_placeholder_sim_input.yaml index 81eaecc..a01b7db 100644 --- a/examples/internal/sim_array/sim_array_crystalstructure_placeholder_sim_input.yaml +++ b/examples/internal/sim_array/sim_array_crystalstructure_placeholder_sim_input.yaml @@ -9,7 +9,8 @@ args: template_sim_input: asimmodule: singlepoint args: - calc_id: lj_Cu + calculator: + calc_id: lj_Cu image: builder: bulk name: Cu diff --git a/examples/internal/sim_array/sim_array_crystalstructure_secondary_sim_input.yaml b/examples/internal/sim_array/sim_array_crystalstructure_secondary_sim_input.yaml index 3dcfdcf..a95b784 100644 --- a/examples/internal/sim_array/sim_array_crystalstructure_secondary_sim_input.yaml +++ b/examples/internal/sim_array/sim_array_crystalstructure_secondary_sim_input.yaml @@ -12,7 +12,8 @@ args: template_sim_input: asimmodule: singlepoint args: - calc_id: lj_Cu + calculator: + calc_id: lj_Cu image: name: Cu builder: bulk diff --git a/examples/internal/sim_array/sim_array_crystalstructure_sim_input.yaml b/examples/internal/sim_array/sim_array_crystalstructure_sim_input.yaml index 25e6fc2..86b6ff0 100644 --- a/examples/internal/sim_array/sim_array_crystalstructure_sim_input.yaml +++ b/examples/internal/sim_array/sim_array_crystalstructure_sim_input.yaml @@ -16,7 +16,8 @@ args: template_sim_input: asimmodule: singlepoint args: - calc_id: lj_Cu + calculator: + calc_id: lj_Cu image: {} # You can set a placeholder here or not specify. # I usually put something that would cause an error to make sure # it is substituted correctly diff --git a/examples/internal/sim_array/sim_array_image_file_sim_input.yaml b/examples/internal/sim_array/sim_array_image_file_sim_input.yaml index f9cecfc..517e0bc 100644 --- a/examples/internal/sim_array/sim_array_image_file_sim_input.yaml +++ b/examples/internal/sim_array/sim_array_image_file_sim_input.yaml @@ -8,7 +8,8 @@ args: template_sim_input: asimmodule: singlepoint args: - calc_id: lj_Cu + calculator: + calc_id: lj_Cu image: image_file: {} \ No newline at end of file diff --git a/examples/internal/sim_array/sim_array_lattice_parameters_sim_input.yaml b/examples/internal/sim_array/sim_array_lattice_parameters_sim_input.yaml index bc10f1e..2f77fec 100644 --- a/examples/internal/sim_array/sim_array_lattice_parameters_sim_input.yaml +++ b/examples/internal/sim_array/sim_array_lattice_parameters_sim_input.yaml @@ -7,7 +7,8 @@ args: template_sim_input: asimmodule: singlepoint args: - calc_id: lj_Cu + calculator: + calc_id: lj_Cu image: builder: bulk name: Cu diff --git a/examples/internal/singlepoint/singlepoint_batch_sim_input.yaml b/examples/internal/singlepoint/singlepoint_batch_sim_input.yaml index dc34214..0766f3a 100644 --- a/examples/internal/singlepoint/singlepoint_batch_sim_input.yaml +++ b/examples/internal/singlepoint/singlepoint_batch_sim_input.yaml @@ -2,7 +2,8 @@ asimmodule: singlepoint workdir: batch_results env_id: batch args: - calc_id: lj_Ar + calculator: + calc_id: lj_Ar image: name: Ar builder: bulk diff --git a/examples/internal/singlepoint/singlepoint_sim_input.yaml b/examples/internal/singlepoint/singlepoint_sim_input.yaml index df4329e..ca3f423 100644 --- a/examples/internal/singlepoint/singlepoint_sim_input.yaml +++ b/examples/internal/singlepoint/singlepoint_sim_input.yaml @@ -1,7 +1,8 @@ asimmodule: singlepoint workdir: inline_results args: - calc_id: lj_Ar + calculator: + calc_id: lj_Ar image: name: Ar builder: bulk diff --git a/examples/internal/singlepoint/singlepoint_srun_sim_input.yaml b/examples/internal/singlepoint/singlepoint_srun_sim_input.yaml index f9973f1..c48e291 100644 --- a/examples/internal/singlepoint/singlepoint_srun_sim_input.yaml +++ b/examples/internal/singlepoint/singlepoint_srun_sim_input.yaml @@ -2,7 +2,8 @@ asimmodule: singlepoint workdir: srun_results env_id: srun args: - calc_id: lj_Ar + calculator: + calc_id: lj_Ar image: name: Ar builder: bulk diff --git a/examples/internal/surface_energies/surface_energies_sim_input.yaml b/examples/internal/surface_energies/surface_energies_sim_input.yaml index 55645a6..b057dc8 100644 --- a/examples/internal/surface_energies/surface_energies_sim_input.yaml +++ b/examples/internal/surface_energies/surface_energies_sim_input.yaml @@ -1,7 +1,8 @@ asimmodule: surface_energies.surface_energies workdir: surface_energies_results args: - calc_id: emt + calculator: + calc_id: emt image: name: Cu cubic: true # This is really important! You must use the conventional cell! diff --git a/examples/internal/symmetric_cell_relax/symmetric_cell_relax_sim_input.yaml b/examples/internal/symmetric_cell_relax/symmetric_cell_relax_sim_input.yaml index 6fe30e6..269da5b 100644 --- a/examples/internal/symmetric_cell_relax/symmetric_cell_relax_sim_input.yaml +++ b/examples/internal/symmetric_cell_relax/symmetric_cell_relax_sim_input.yaml @@ -1,7 +1,8 @@ asimmodule: geometry_optimization.symmetric_cell_relax workdir: symmetric_cell_relax_results args: - calc_id: lj_Ar + calculator: + calc_id: lj_Ar image: name: Ar crystalstructure: sc diff --git a/examples/internal/vacancy_formation_energy/vfe_sim_input.yaml b/examples/internal/vacancy_formation_energy/vfe_sim_input.yaml index efa55fb..3aa619f 100644 --- a/examples/internal/vacancy_formation_energy/vfe_sim_input.yaml +++ b/examples/internal/vacancy_formation_energy/vfe_sim_input.yaml @@ -1,7 +1,8 @@ asimmodule: vacancy_formation_energy.vacancy_formation_energy workdir: vfe_results args: - calc_id: emt + calculator: + calc_id: emt image: # Does not need to be relaxed, will use energy minimum volume from eos name: Cu a: 3.59 From dbdae3a2f0f24e0e126e2328cf9f177174f7721a Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Thu, 7 May 2026 16:05:03 -0700 Subject: [PATCH 66/78] refactor(ase_md): replace calc_spec with calculator dict --- src/asimtools/asimmodules/ase_md/ase_md.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/asimtools/asimmodules/ase_md/ase_md.py b/src/asimtools/asimmodules/ase_md/ase_md.py index cbfbebf..beaae5e 100644 --- a/src/asimtools/asimmodules/ase_md/ase_md.py +++ b/src/asimtools/asimmodules/ase_md/ase_md.py @@ -194,7 +194,7 @@ def plot_thermo( plt.close(fig='all') def ase_md( - calc_spec: Dict, + calculator: Dict, image: Dict, timestep: float, nsteps: int = 100, @@ -214,8 +214,8 @@ def ase_md( """Runs ASE MD simulations. This is only recommended for small systems and for testing. For larger systems, use LAMMPS or more purpose-built code - :param calc_spec: calc specification - :type calc_spec: Dict + :param calculator: Calculator specification, see :func:`asimtools.calculators.load_calc` + :type calculator: Dict :param image: Image specification, see :func:`asimtools.utils.get_atoms` :type image: Dict :param timestep: Timestep in ASE time units @@ -241,7 +241,7 @@ def ase_md( :rtype: Dict """ - calc = load_calc(**calc_spec) + calc = load_calc(calculator=calculator) atoms = get_atoms(**image) atoms.calc = calc From 05121f8e09f31cd3e7369d6d4aeef837f649d76d Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Thu, 7 May 2026 16:05:25 -0700 Subject: [PATCH 67/78] refactor(examples/active_learning): replace calc_spec with calculator in ase_md examples --- .../active_learning/active_learning_sim_input.yaml | 2 +- .../external/active_learning/ase_md_sim_input.yaml | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/examples/external/active_learning/active_learning_sim_input.yaml b/examples/external/active_learning/active_learning_sim_input.yaml index 3bad5e3..bbb3134 100644 --- a/examples/external/active_learning/active_learning_sim_input.yaml +++ b/examples/external/active_learning/active_learning_sim_input.yaml @@ -37,7 +37,7 @@ args: step-1: asimmodule: active_learning.ase_md args: - calc_spec: + calculator: calc_params: name: MACE args: diff --git a/examples/external/active_learning/ase_md_sim_input.yaml b/examples/external/active_learning/ase_md_sim_input.yaml index 17fe6e0..9e517e6 100644 --- a/examples/external/active_learning/ase_md_sim_input.yaml +++ b/examples/external/active_learning/ase_md_sim_input.yaml @@ -1,12 +1,13 @@ asimmodule: ase_md.ase_md.py workdir: ase_md_results args: - calc_spec: + calculator: calc_id: lj_Ar - # name: MACE - # args: - # model: /Users/keith/dev/asimtools/examples/external/active_learning/train_mace_results/MACE_models/mace_test_compiled.model - # use_device: cpu + # calc_params: + # name: MACE + # args: + # model: /Users/keith/dev/asimtools/examples/external/active_learning/train_mace_results/MACE_models/mace_test_compiled.model + # use_device: cpu image: name: Ar cubic: True From f9b1fd3b5b2e56de1d9862f94409b57a871835d3 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Thu, 7 May 2026 16:38:26 -0700 Subject: [PATCH 68/78] ase_eos bug fixing --- examples/internal/ase_cubic_eos_custom/ase_eos.py | 12 ++++++------ examples/internal/chained/chained_sim_input.yaml | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/internal/ase_cubic_eos_custom/ase_eos.py b/examples/internal/ase_cubic_eos_custom/ase_eos.py index f99cbd0..542402e 100644 --- a/examples/internal/ase_cubic_eos_custom/ase_eos.py +++ b/examples/internal/ase_cubic_eos_custom/ase_eos.py @@ -15,22 +15,22 @@ def ase_eos( image: Dict, - calc_id: str, + calculator: dict, db_file: 'bulk.db' ) -> Dict: """Calculate the equation of state, fit it and extract the equilibrium volume, energy and bulk modulus :param image: image to use - :type image: Dict - :param calc_id: calculator id - :type calc_id: str + :type image: dict + :param calculator: calculator spec + :type calculator: dict :param db_file: ASE database in which to store results :type db_file: bulk.db :return: results including equilibrium volume, energy and bulk modulus - :rtype: Dict + :rtype: dict """ - calc = load_calc(calc_id) + calc = load_calc(calculator) db = connect(db_file) atoms = get_atoms(**image) atoms.calc = calc diff --git a/examples/internal/chained/chained_sim_input.yaml b/examples/internal/chained/chained_sim_input.yaml index 5cc58b2..815bf1e 100644 --- a/examples/internal/chained/chained_sim_input.yaml +++ b/examples/internal/chained/chained_sim_input.yaml @@ -21,7 +21,7 @@ args: calculator: calc_id: lj_Ar image: - image_file: step-0/image_output.xyz + image_file: ../step-0/image_output.xyz fmax: 0.006 step-2: asimmodule: geometry_optimization.ase_cubic_eos_optimization @@ -29,4 +29,4 @@ args: calculator: calc_id: lj_Ar image: - image_file: step-1/image_output.xyz + image_file: ../step-1/image_output.xyz From 0faaac80831bbc370a9b5d220bc4ff9e638e4cfb Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Thu, 7 May 2026 16:50:21 -0700 Subject: [PATCH 69/78] refactor(job): modernize type annotations, fix wrong return types --- src/asimtools/job.py | 111 ++++++++++++++++++++++--------------------- 1 file changed, 58 insertions(+), 53 deletions(-) diff --git a/src/asimtools/job.py b/src/asimtools/job.py index 2f96f3d..8cd1e80 100644 --- a/src/asimtools/job.py +++ b/src/asimtools/job.py @@ -11,7 +11,7 @@ from pathlib import Path from datetime import datetime import logging -from typing import List, TypeVar, Dict, Tuple, Union +from typing import TypeVar from copy import deepcopy from colorama import Fore import ase.io @@ -40,9 +40,9 @@ class Job(): # pylint: disable=too-many-instance-attributes def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments self, - sim_input: Dict, - env_input: Union[Dict,None] = None, - calc_input: Union[Dict,None] = None, + sim_input: dict, + env_input: dict | None = None, + calc_input: dict | None = None, asimrun_mode: bool = False, ) -> None: ''' Initialise Job with simulation, environment and calculator inputs ''' @@ -115,25 +115,25 @@ def leave_workdir(self) -> None: ''' goes to directory from which job was launched ''' os.chdir(self.launchdir) - def add_output_files(self, file_dict: Dict) -> None: + def add_output_files(self, file_dict: dict) -> None: ''' Adds a file to the output file list ''' files = self.get_output().get('files', {}) files.update(file_dict) self.update_output({'files': file_dict}) - def get_sim_input(self) -> Dict: + def get_sim_input(self) -> dict: ''' Get simulation input ''' return self.sim_input - def get_calc_input(self) -> Dict: + def get_calc_input(self) -> dict: ''' Get calculator input ''' return self.calc_input - def get_env_input(self) -> Dict: + def get_env_input(self) -> dict: ''' Get environment input ''' return self.env_input - def get_output_yaml(self) -> Dict: + def get_output_yaml(self) -> Path: ''' Get current output file ''' out_fname = 'output.yaml' output_yaml = self.workdir / out_fname @@ -143,35 +143,35 @@ def get_workdir(self) -> Path: ''' Get working directory ''' return self.workdir - def get_output(self) -> Dict: + def get_output(self) -> dict: ''' Get values in output.yaml ''' output_yaml = self.get_output_yaml() if output_yaml.exists(): return read_yaml(output_yaml) return {} - def update_sim_input(self, new_params) -> None: + def update_sim_input(self, new_params: dict) -> None: ''' Update simulation parameters ''' self.sim_input.update(new_params) - def update_calc_input(self, new_params) -> None: + def update_calc_input(self, new_params: dict) -> None: ''' Update calculator parameters ''' self.calc_input.update(new_params) - def update_env_input(self, new_params) -> None: + def update_env_input(self, new_params: dict) -> None: ''' Update calculator parameters ''' self.env_input.update(new_params) self.env_id = self.sim_input.get('env_id', None) if self.env_id is not None: self.env = self.env_input[self.env_id] - def set_workdir(self, workdir) -> None: + def set_workdir(self, workdir: str | Path) -> None: ''' Set working directory both in sim_input and instance ''' workdir = Path(workdir) self.sim_input['workdir'] = str(workdir.resolve()) self.workdir = workdir - def get_status(self, descend=False, display=False) -> Tuple[bool,str]: + def get_status(self, descend: bool = False, display: bool = False) -> tuple[bool, str]: ''' Check job status ''' output = self.get_output() job_id = output.get('job_id', False) @@ -196,13 +196,13 @@ def get_status(self, descend=False, display=False) -> Tuple[bool,str]: print(f'Status: {status}') return complete, status - def update_output(self, output_update: Dict) -> None: + def update_output(self, output_update: dict) -> None: ''' Update output.yaml if it exists or write a new one ''' output = self.get_output() output.update(output_update) write_yaml(self.get_output_yaml(), output) - def get_logger(self, logfilename='stdout.txt', level='info'): + def get_logger(self, logfilename: str = 'stdout.txt', level: str = 'info') -> logging.Logger: ''' Get the logger ''' assert self.workdir.exists(), 'Work directory does not exist yet' logger = get_logger(logfile=self.workdir / logfilename, level=level) @@ -210,9 +210,9 @@ def get_logger(self, logfilename='stdout.txt', level='info'): def _gen_slurm_batch_preamble( self, - slurm_params=None, - extra_flags=None - ) -> None: + slurm_params: dict | None = None, + extra_flags: list | None = None, + ) -> str: ''' Generate the txt with job configuration but no run commands ''' if slurm_params is None: slurm_params = self.env.get('slurm', {}) @@ -254,17 +254,17 @@ def _gen_slurm_batch_preamble( return txt class UnitJob(Job): - ''' + ''' Unit job object with ability to submit a slurm/interactive job. - Unit jobs run in a specific environment and if required, run a + Unit jobs run in a specific environment and if required, run a specific calculator. More complex workflows are built up of unit jobs ''' def __init__( self, - sim_input: Dict, - env_input: Union[Dict,None] = None, - calc_input: Union[Dict,None] = None, + sim_input: dict, + env_input: dict | None = None, + calc_input: dict | None = None, ) -> None: super().__init__(sim_input, env_input, calc_input) @@ -446,9 +446,9 @@ def gen_input_files( def submit( # pylint: disable=too-many-locals,too-many-branches,too-many-statements self, - dependency: Union[List,None,str] = None, + dependency: list | str | None = None, write_image: bool = True, - ) -> Union[None,List[str]]: + ) -> list[str] | None: ''' Submit a job using slurm, interactively or in the terminal ''' @@ -553,9 +553,9 @@ class DistributedJob(Job): ''' Array job object with ability to submit simultaneous jobs ''' def __init__( # pylint: disable=too-many-locals self, - sim_input: Dict, - env_input: Union[None,Dict] = None, - calc_input: Union[None,Dict] = None, + sim_input: dict, + env_input: dict | None = None, + calc_input: dict | None = None, ) -> None: super().__init__(sim_input, env_input, calc_input) # Set a standard for all subdirectories to start @@ -707,7 +707,7 @@ def gen_input_files(self, **kwargs) -> None: def submit_jobs( self, **kwargs, - ) -> Union[None,List[int]]: + ) -> list[int] | None: ''' Submits the jobs. If submitting lots of batch jobs, we recommend using DistributedJob.submit_slurm_array @@ -730,7 +730,7 @@ def submit_jobs( def submit_sh_array( self, **_kwargs, - ) -> Union[None,List[int]]: + ) -> list[int] | None: ''' Submits jobs using a sh script. Proceeds even if some jobs fail ''' @@ -777,12 +777,12 @@ def submit_sh_array( def submit_slurm_array( # pylint: disable=too-many-locals,too-many-branches self, - array_max=None, - dependency: Union[List[str],None] = None, + array_max: int | None = None, + dependency: list[str] | None = None, group_size: int = 1, debug: bool = False, **_kwargs, - ) -> Union[None,List[int]]: + ) -> list[int] | None: ''' Submits a job array if all the jobs have the same env and use slurm ''' @@ -863,8 +863,8 @@ def submit_slurm_array( # pylint: disable=too-many-locals,too-many-branches job_ids = [int(completed_process.stdout.split(' ')[-1])] return job_ids - def submit(self, **kwargs) -> None: - ''' + def submit(self, **kwargs) -> list[int] | None: + ''' Submit a job using slurm, interactively or in the current console ''' @@ -890,9 +890,9 @@ class ChainedJob(Job): ''' def __init__( self, - sim_input: Dict, - env_input: Union[None,Dict] = None, - calc_input: Union[None,Dict] = None, + sim_input: dict, + env_input: dict | None = None, + calc_input: dict | None = None, ) -> None: super().__init__(sim_input, env_input, calc_input) @@ -912,12 +912,12 @@ def __init__( self.unitjobs = unitjobs - def get_last_output(self) -> Dict: + def get_last_output(self) -> dict: ''' Returns the output of the last job in the chain ''' return self.unitjobs[-1].get_output() def submit( # pylint: disable=too-many-locals,too-many-branches,too-many-statements,too-many-nested-blocks - self, dependency: Union[List,None] = None, debug: bool = False) -> List: + self, dependency: list | None = None, debug: bool = False) -> list: ''' Submit a job using slurm, interactively or in the terminal ''' @@ -1009,7 +1009,7 @@ def submit( # pylint: disable=too-many-locals,too-many-branches,too-many-statem return job_ids -def load_job_from_directory(workdir: os.PathLike, asimrun_mode=False) -> Job: +def load_job_from_directory(workdir: str | Path, asimrun_mode: bool = False) -> Job: ''' Loads a job from a given directory ''' workdir = Path(workdir) assert workdir.exists(), f'Work directory "{workdir}" does not exist' @@ -1043,7 +1043,12 @@ def load_job_from_directory(workdir: os.PathLike, asimrun_mode=False) -> Job: job.workdir = Path(workdir) return job -def create_unitjob(sim_input, env_input, workdir, calc_input=None): +def create_unitjob( + sim_input: dict, + env_input: dict, + workdir: str | Path, + calc_input: dict | None = None, +) -> UnitJob: """Helper for making a generic UnitJob object, mostly for testing""" env_id = list(env_input.keys())[0] sim_input['env_id'] = env_id @@ -1055,13 +1060,13 @@ def create_unitjob(sim_input, env_input, workdir, calc_input=None): ) return unitjob -def get_subjobs(workdir): +def get_subjobs(workdir: Path) -> list[Path]: """Get all the directories with jobs in them - :param workdir: _description_ - :type workdir: _type_ - :return: _description_ - :rtype: _type_ + :param workdir: directory to search for subjobs + :type workdir: Path + :return: sorted list of subdirectory paths containing sim_input.yaml + :rtype: list[Path] """ subjob_dirs = [] for subdir in workdir.iterdir(): @@ -1072,14 +1077,14 @@ def get_subjobs(workdir): return sorted(subjob_dirs) def load_job_tree( - workdir: str = './', -) -> Dict: + workdir: str | Path = './', +) -> dict: """Loads all the jobs in a directory in a tree format using recursion :param workdir: root directory from which to recursively find jobs, defaults to './' :type workdir: str, optional :return: dictionary mimicking the job tree - :rtype: Dict + :rtype: dict """ workdir = Path(workdir).resolve() @@ -1099,7 +1104,7 @@ def load_job_tree( } return job_dict -def check_job_tree_complete(job_tree: Dict, skip_failed: bool=False) -> bool: +def check_job_tree_complete(job_tree: dict, skip_failed: bool = False) -> tuple[bool, str]: ''' Recursively check if all jobs in a job tree are complete ''' if job_tree['subjobs'] is None: status = job_tree['job'].get_status()[1] From 268cde76c2b2a43d8400f3642535f49c8dc2fcd0 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Thu, 7 May 2026 17:00:09 -0700 Subject: [PATCH 70/78] Add dry_run option to UnitJob: writes inputs but skips submission dry_run=True in sim_input writes all input files to disk without submitting the job. The flag is stripped from the written sim_input.yaml so subsequent runs execute normally without manual cleanup. Co-Authored-By: Claude Sonnet 4.6 --- src/asimtools/job.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/asimtools/job.py b/src/asimtools/job.py index 8cd1e80..b586042 100644 --- a/src/asimtools/job.py +++ b/src/asimtools/job.py @@ -377,6 +377,7 @@ def gen_input_files( # This is the sim_input that will be written to disk so it will have # slightly different paths and the image(s) will be image_files sim_input = deepcopy(self.sim_input) + sim_input.pop('dry_run', None) # The sim_input file will already be in the work directory sim_input['workdir'] = '.' # Collect useful variables @@ -478,6 +479,13 @@ def submit( # pylint: disable=too-many-locals,too-many-branches,too-many-statem %s', self.workdir) return None + if self.sim_input.get('dry_run', False): + logger.warning( + 'dry_run=True: input files written but job not submitted in %s', + self.workdir + ) + return None + cur_dir = Path('.').resolve() os.chdir(self.workdir) mode_params = self.env.get('mode', {}) From 5942d5a8fcf79d04c338ea8d2e218322869a0bed Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Thu, 7 May 2026 17:54:47 -0700 Subject: [PATCH 71/78] Bump version to 0.3.0; update docs and changelog for calculator interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CHANGELOG: document breaking calculator dict change, dry_run addition, calc_array backward compat, and job.py type annotation modernization - usage.rst: add dry_run param, add Specifying Calculators section with calc_id/calc_params examples, update Python code examples - workflows.rst: update all YAML examples to use calculator: calc_id: x; update calc_array docs to document new calculators/template_calculator params - pyproject.toml: 0.2.0 → 0.3.0 Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 22 +++++++++++++++++ docs/usage.rst | 56 ++++++++++++++++++++++++++++++++++++++----- docs/workflows.rst | 60 ++++++++++++++++++++++++++++++++-------------- pyproject.toml | 2 +- 4 files changed, 115 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a0609f..ed6d128 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,28 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [0.3.0] - 2026-05-07 + +### Breaking changes +- All asimmodules: `calc_id: str` parameter replaced by `calculator: Dict` with + keys `calc_id` (reference into `calc_input.yaml`) or `calc_params` (inline + calculator specification). Mirrors the existing `image`/`get_atoms` pattern. +- `ase_md.ase_md`: `calc_spec` parameter renamed to `calculator` +- All YAML example files updated to use the new `calculator:` interface + +### Added +- `dry_run` option in `sim_input.yaml`: writes input files to the work directory + without submitting the job. Unlike `submit: false`, the flag is stripped from + the written `sim_input.yaml` so subsequent runs execute normally without + manual cleanup. +- `calc_array`: new `calculators` and `template_calculator` parameters accepting + dicts instead of plain strings. Old `calc_ids` / `template_calc_id` string + parameters are retained for backward compatibility and auto-converted. + +### Changed +- `job.py`: type annotations modernized to use built-in `dict/list/tuple` and + `X | Y` union syntax (Python 3.10+) throughout, removing most `typing` imports + ## [0.2.0] - 2026-04-06 ### Breaking changes diff --git a/docs/usage.rst b/docs/usage.rst index 6b7a632..f67c1e3 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -250,6 +250,39 @@ and M3GNet force fields are implemented. the arguments are passed as ``calc = LennardJones(**{'sigma':3.2, 'epsilon':3})`` +.. _specifyingcalculators: + +Specifying Calculators +---------------------- + +Asimmodules that require a calculator accept a ``calculator`` dictionary as part +of their ``args`` section. There are two ways to specify a calculator: + +**By reference** — look up a named entry from ``calc_input.yaml``: + +.. code-block:: yaml + + args: + calculator: + calc_id: lj_argon + +**Inline** — provide the full calculator specification directly: + +.. code-block:: yaml + + args: + calculator: + calc_params: + name: MACE + args: + model: medium + use_device: cpu + +The ``calc_id`` form is preferred for production workflows because the same +calculator can be shared across many sim_inputs and updated in one place. The +``calc_params`` form is useful for one-off jobs or when the calculator should +travel with the sim_input (e.g. in active-learning workflows). + .. _siminput: sim_input.yaml @@ -288,7 +321,10 @@ The parameters are: - **submit**: (bool, optional) whether to run the asimmodule. If set to false it will just write the input files which is very useful for testing before submitting large workflows. You can go in and test one example before - resubmitting with ``submit=True``, defaults to true + resubmitting with ``submit=True``, defaults to true +- **dry_run**: (bool, optional) like ``submit: false`` but the flag is stripped + from the written ``sim_input.yaml``, so the next ``asim-execute`` call runs + normally without any manual cleanup, defaults to false - **workdir**: (str, optional) The directory in which the asimmodule will be run, `asim-execute` will create the directory whereas `asim-run` ignores this parameter entirely, defaults to './results' @@ -511,17 +547,25 @@ import them and use them in any other code for example, you can import :func:`asimtools.asimmodules.singlepoint` and use it as below. .. code-block:: python - + from asimtools.asimmodules.singlepoint import singlepoint - results = singlepoint(image={'name': 'Ar'}, calc_id='lj') + results = singlepoint( + image={'name': 'Ar'}, + calculator={'calc_id': 'lj'}, + ) print(results) -You can also use the utils and tools e.g. to load a calculator using just a -``calc_id`` +You can also use the utils and tools e.g. to load a calculator: .. code-block:: python from asimtools.calculators import load_calc - calc = load_calc('lj') + # By reference to calc_input.yaml + calc = load_calc(calculator={'calc_id': 'lj'}) + + # Inline specification + calc = load_calc(calculator={'calc_params': {'name': 'LennardJones', + 'module': 'ase.calculators.lj', + 'args': {'sigma': 3.54}}}) diff --git a/docs/workflows.rst b/docs/workflows.rst index e47a9d4..8896d02 100644 --- a/docs/workflows.rst +++ b/docs/workflows.rst @@ -77,7 +77,8 @@ Example — sweep lattice constants asimmodule: singlepoint env_id: batch args: - calc_id: my_mlip + calculator: + calc_id: my_mlip image: name: Cu crystalstructure: fcc @@ -100,7 +101,8 @@ Example — sweep over a set of structure files asimmodule: geometry_optimization.cell_relax env_id: batch args: - calc_id: my_mlip + calculator: + calc_id: my_mlip image: image_file: PLACEHOLDER @@ -123,7 +125,8 @@ Example — sweep two parameters simultaneously asimmodule: ase_md.ase_md env_id: batch args: - calc_id: my_mlip + calculator: + calc_id: my_mlip image: {name: Fe} temperature: 300 pressure: 1.0 @@ -183,7 +186,8 @@ Example — single-point energy on a dataset asimmodule: singlepoint env_id: batch args: - calc_id: my_mlip + calculator: + calc_id: my_mlip properties: [energy, forces, stress] labels: files @@ -202,7 +206,8 @@ Example — relax all structures in an xyz file asimmodule: geometry_optimization.atom_relax env_id: batch args: - calc_id: my_mlip + calculator: + calc_id: my_mlip ---- @@ -230,10 +235,11 @@ Key parameters +=============================+==================================================+ | ``subsim_input`` | The sim_input for the asimmodule to run. | +-----------------------------+--------------------------------------------------+ -| ``calc_ids`` | Explicit list of calculator IDs. | +| ``calculators`` | Explicit list of calculator dicts, each with | +| | a ``calc_id`` or ``calc_params`` key. | +-----------------------------+--------------------------------------------------+ -| ``template_calc_id`` | Base calc_id; ``key_sequence`` is swept inside | -| | its parameters. | +| ``template_calculator`` | Base calculator dict; ``key_sequence`` is swept | +| | inside its parameters. | +-----------------------------+--------------------------------------------------+ | ``key_sequence`` | Path into the calculator dict to sweep. | +-----------------------------+--------------------------------------------------+ @@ -241,6 +247,12 @@ Key parameters | ``linspace_args`` / | ``sim_array``). | | ``arange_args`` | | +-----------------------------+--------------------------------------------------+ +| ``calc_ids`` | *Deprecated* — list of calc_id strings. | +| | Auto-converted to ``calculators``. | ++-----------------------------+--------------------------------------------------+ +| ``template_calc_id`` | *Deprecated* — base calc_id string. | +| | Auto-converted to ``template_calculator``. | ++-----------------------------+--------------------------------------------------+ Example — benchmark multiple calculators ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -251,7 +263,10 @@ Example — benchmark multiple calculators env_id: batch workdir: results/calc_benchmark args: - calc_ids: [emt, lj_argon, mace_mp] + calculators: + - calc_id: emt + - calc_id: lj_argon + - calc_id: mace_mp subsim_input: asimmodule: singlepoint env_id: batch @@ -268,7 +283,8 @@ Example — converge DFT cutoff energy env_id: batch workdir: results/ecut_convergence args: - template_calc_id: vasp_base + template_calculator: + calc_id: vasp_base key_sequence: [args, encut] linspace_args: [200, 600, 9] # 200, 250, …, 600 eV subsim_input: @@ -326,19 +342,22 @@ Example env_id: batch args: image: {name: Fe} - calc_id: my_mlip + calculator: + calc_id: my_mlip relax_cu: asimmodule: geometry_optimization.cell_relax env_id: batch args: image: {name: Cu} - calc_id: my_mlip + calculator: + calc_id: my_mlip sp_ar: asimmodule: singlepoint env_id: batch args: image: {name: Ar} - calc_id: emt + calculator: + calc_id: emt properties: [energy] ---- @@ -390,13 +409,15 @@ Example — relax then compute phonons asimmodule: geometry_optimization.cell_relax env_id: batch args: - calc_id: my_mlip + calculator: + calc_id: my_mlip image: {name: Cu} step-1: asimmodule: phonons.ase_phonons env_id: batch args: - calc_id: my_mlip + calculator: + calc_id: my_mlip image: image_file: ../step-0/final.xyz @@ -414,7 +435,8 @@ Example — three-step pipeline with a parallel middle step asimmodule: geometry_optimization.cell_relax env_id: batch args: - calc_id: my_mlip + calculator: + calc_id: my_mlip image: {name: Al} step-1: asimmodule: workflows.sim_array @@ -426,7 +448,8 @@ Example — three-step pipeline with a parallel middle step asimmodule: singlepoint env_id: batch args: - calc_id: my_mlip + calculator: + calc_id: my_mlip image: name: Al a: PLACEHOLDER @@ -492,7 +515,8 @@ Example — sequential geometry relaxations at increasing pressures asimmodule: geometry_optimization.cell_relax env_id: batch args: - calc_id: my_mlip + calculator: + calc_id: my_mlip image: {name: Fe} pressure: 0 diff --git a/pyproject.toml b/pyproject.toml index e099f70..6a00279 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "asimtools" description = "A lightweight python package for managing and running atomic simulation workflows" -version = "0.2.0" +version = "0.3.0" readme = "README.md" license = { text = "MIT" } authors = [ From 0bba70ae523dc4617085f7e60f5403d43e7c7aee Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Thu, 7 May 2026 18:23:42 -0700 Subject: [PATCH 72/78] Single-source version from pyproject.toml via importlib.metadata; add pylint to dev deps Co-Authored-By: Claude Sonnet 4.6 --- pyproject.toml | 1 + src/asimtools/__init__.py | 3 ++- src/asimtools/_version.py | 5 ----- 3 files changed, 3 insertions(+), 6 deletions(-) delete mode 100644 src/asimtools/_version.py diff --git a/pyproject.toml b/pyproject.toml index 6a00279..7fad6bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,7 @@ include = [ [project.optional-dependencies] dev = [ "asimtools[tests, docs, phonons]", + "pylint", ] mlip = ["matgl>=1.1.2", "chgnet>=0.3.3", "mace-torch>=0.3.3"] phonons = ["phonopy>=2.20.0", "seekpath>=2.1.0"] diff --git a/src/asimtools/__init__.py b/src/asimtools/__init__.py index 6bb430d..a9612c5 100644 --- a/src/asimtools/__init__.py +++ b/src/asimtools/__init__.py @@ -1,5 +1,6 @@ '''asimtools: lightweight atomic simulation workflow manager.''' -from asimtools._version import __version__ +from importlib.metadata import version +__version__ = version("asimtools") from asimtools.job import Job, UnitJob, DistributedJob, ChainedJob from asimtools.calculators import load_calc from asimtools.utils import ( diff --git a/src/asimtools/_version.py b/src/asimtools/_version.py deleted file mode 100644 index 6daea9f..0000000 --- a/src/asimtools/_version.py +++ /dev/null @@ -1,5 +0,0 @@ -# See Python packaging guide -# https://packaging.python.org/guides/single-sourcing-package-version/ -'''Package version string.''' - -__version__ = "0.2.0" From 1bdc4b9d545716b9d10e009a0ed6512c440a3589 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Thu, 7 May 2026 18:35:40 -0700 Subject: [PATCH 73/78] Improve CI workflows: restrict release to tags, fix docs deps, remove redundant static workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - release.yaml: trigger only on v* tags, enable TestPyPI → PyPI pipeline, enable GitHub Release creation with Sigstore signing - docs.yaml: replace broken autodoc pip install with pip install -e ".[docs]", restrict triggers to main branch pushes/PRs, scope write permission to deploy step only - static.yaml: delete — redundant with docs.yaml gh-pages deployment Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/docs.yaml | 22 ++--- .github/workflows/release.yaml | 146 ++++++++++++++++----------------- .github/workflows/static.yaml | 43 ---------- 3 files changed, 81 insertions(+), 130 deletions(-) delete mode 100644 .github/workflows/static.yaml diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index ded6faf..7793d5f 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -1,26 +1,26 @@ name: documentation -on: [push, pull_request, workflow_dispatch] - -permissions: - contents: write +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + workflow_dispatch: jobs: docs: runs-on: ubuntu-latest + permissions: + contents: write steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: '3.9' + python-version: '3.10' - name: Install dependencies - run: | - pip install sphinx sphinx_rtd_theme myst_parser autodoc - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - pip install -e . + run: pip install -e ".[docs]" - name: Sphinx build - run: | - sphinx-build docs _build + run: sphinx-build docs _build - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v3 if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 3289313..9cf776a 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,6 +1,9 @@ name: Publish Python 🐍 distribution 📦 to PyPI and TestPyPI -on: push +on: + push: + tags: + - 'v*' jobs: build: @@ -14,11 +17,7 @@ jobs: with: python-version: "3.10" - name: Install pypa/build - run: >- - python3 -m - pip install - build - --user + run: python3 -m pip install build --user - name: Build a binary wheel and a source tarball run: python3 -m build - name: Store the distribution packages @@ -27,16 +26,16 @@ jobs: name: python-package-distributions path: dist/ - publish-to-pypi: - name: >- - Publish Python 🐍 distribution 📦 to PyPI - if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes + publish-to-testpypi: + name: Publish Python 🐍 distribution 📦 to TestPyPI needs: - build runs-on: ubuntu-latest + environment: - name: pypi - url: https://pypi.org/p/asimtools # Replace with your PyPI project name + name: testpypi + url: https://test.pypi.org/p/asimtools + permissions: id-token: write # IMPORTANT: mandatory for trusted publishing @@ -46,73 +45,68 @@ jobs: with: name: python-package-distributions path: dist/ - - name: Publish distribution 📦 to PyPI + - name: Publish distribution 📦 to TestPyPI uses: pypa/gh-action-pypi-publish@release/v1 + with: + verbose: true + repository-url: https://test.pypi.org/legacy/ - # github-release: - # name: >- - # Sign the Python 🐍 distribution 📦 with Sigstore - # and upload them to GitHub Release - # needs: - # - publish-to-pypi - # runs-on: ubuntu-latest - - # permissions: - # contents: write # IMPORTANT: mandatory for making GitHub Releases - # id-token: write # IMPORTANT: mandatory for sigstore - - # steps: - # - name: Download all the dists - # uses: actions/download-artifact@v4 - # with: - # name: python-package-distributions - # path: dist/ - # - name: Sign the dists with Sigstore - # uses: sigstore/gh-action-sigstore-python@v2.1.1 - # with: - # inputs: >- - # ./dist/*.tar.gz - # ./dist/*.whl - # - name: Create GitHub Release - # env: - # GITHUB_TOKEN: ${{ github.token }} - # run: >- - # gh release create - # '${{ github.ref_name }}' - # --repo '${{ github.repository }}' - # --notes "" - # - name: Upload artifact signatures to GitHub Release - # env: - # GITHUB_TOKEN: ${{ github.token }} - # # Upload to GitHub Release using the `gh` CLI. - # # `dist/` contains the built packages, and the - # # sigstore-produced signatures and certificates. - # run: >- - # gh release upload - # '${{ github.ref_name }}' dist/** - # --repo '${{ github.repository }}' + publish-to-pypi: + name: Publish Python 🐍 distribution 📦 to PyPI + needs: + - publish-to-testpypi + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/asimtools + permissions: + id-token: write # IMPORTANT: mandatory for trusted publishing - # publish-to-testpypi: - # name: Publish Python 🐍 distribution 📦 to TestPyPI - # needs: - # - build - # runs-on: ubuntu-latest + steps: + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + - name: Publish distribution 📦 to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 - # environment: - # name: testpypi - # url: https://test.pypi.org/p/asimtools + github-release: + name: >- + Sign the Python 🐍 distribution 📦 with Sigstore + and upload them to GitHub Release + needs: + - publish-to-pypi + runs-on: ubuntu-latest - # permissions: - # id-token: write # IMPORTANT: mandatory for trusted publishing + permissions: + contents: write # IMPORTANT: mandatory for making GitHub Releases + id-token: write # IMPORTANT: mandatory for sigstore - # steps: - # - name: Download all the dists - # uses: actions/download-artifact@v4 - # with: - # name: python-package-distributions - # path: dist/ - # - name: Publish distribution 📦 to TestPyPI - # uses: pypa/gh-action-pypi-publish@release/v1 - # with: - # verbose: true - # repository-url: https://test.pypi.org/legacy/ + steps: + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + - name: Sign the dists with Sigstore + uses: sigstore/gh-action-sigstore-python@v2.1.1 + with: + inputs: >- + ./dist/*.tar.gz + ./dist/*.whl + - name: Create GitHub Release + env: + GITHUB_TOKEN: ${{ github.token }} + run: >- + gh release create + '${{ github.ref_name }}' + --repo '${{ github.repository }}' + --notes "" + - name: Upload artifact signatures to GitHub Release + env: + GITHUB_TOKEN: ${{ github.token }} + run: >- + gh release upload + '${{ github.ref_name }}' dist/** + --repo '${{ github.repository }}' diff --git a/.github/workflows/static.yaml b/.github/workflows/static.yaml deleted file mode 100644 index 37a4402..0000000 --- a/.github/workflows/static.yaml +++ /dev/null @@ -1,43 +0,0 @@ -# Simple workflow for deploying static content to GitHub Pages -name: Deploy static content to Pages - -on: - # Runs on pushes targeting the default branch - push: - branches: ["gh-pages"] - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages -permissions: - contents: read - pages: write - id-token: write - -# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. -# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. -concurrency: - group: "pages" - cancel-in-progress: false - -jobs: - # Single deploy job since we're just deploying - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Setup Pages - uses: actions/configure-pages@v3 - - name: Upload artifact - uses: actions/upload-pages-artifact@v2 - with: - # Upload entire repository - path: 'build' - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v2 From 601df184097ebe0dd5e31e435fb9333bb566d084 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Fri, 8 May 2026 13:24:44 -0700 Subject: [PATCH 74/78] =?UTF-8?q?Update=20deprecated=20action=20versions:?= =?UTF-8?q?=20upload-artifact=20v3=E2=86=92v4,=20setup-python=20v3?= =?UTF-8?q?=E2=86=92v5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/draft-pdf.yaml | 2 +- .github/workflows/tests.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/draft-pdf.yaml b/.github/workflows/draft-pdf.yaml index fd18457..ffbc079 100644 --- a/.github/workflows/draft-pdf.yaml +++ b/.github/workflows/draft-pdf.yaml @@ -15,7 +15,7 @@ jobs: # This should be the path to the paper within your repo. paper-path: paper.md - name: Upload - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: paper # This is the output path where Pandoc will write the compiled diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 216eed7..8b4aa18 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -20,7 +20,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python 3.10 - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: "3.10" cache: 'pip' From 1a1fb19cf25766e43fd228f3e14e57a0342db3fb Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Fri, 8 May 2026 13:25:32 -0700 Subject: [PATCH 75/78] Remove draft-pdf.yaml workflow Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/draft-pdf.yaml | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 .github/workflows/draft-pdf.yaml diff --git a/.github/workflows/draft-pdf.yaml b/.github/workflows/draft-pdf.yaml deleted file mode 100644 index ffbc079..0000000 --- a/.github/workflows/draft-pdf.yaml +++ /dev/null @@ -1,30 +0,0 @@ -name: Draft PDF -on: [push] - -jobs: - paper: - runs-on: ubuntu-latest - name: Paper Draft - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Build draft PDF - uses: openjournals/openjournals-draft-action@master - with: - journal: joss - # This should be the path to the paper within your repo. - paper-path: paper.md - - name: Upload - uses: actions/upload-artifact@v4 - with: - name: paper - # This is the output path where Pandoc will write the compiled - # PDF. Note, this should be the same directory as the input - # paper.md - path: paper.pdf - - name: Commit PDF to repository - uses: EndBug/add-and-commit@v9 - with: - message: '(auto) Paper PDF Draft' - # This should be the path to the paper within your repo. - add: 'paper.pdf' # 'paper/*.pdf' to commit all PDFs in the paper directory From 99505af614e0f7a17af4066d93fe144a2561c8c3 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Fri, 8 May 2026 13:26:33 -0700 Subject: [PATCH 76/78] Remove pip cache from tests workflow to fix cache service 400 error Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/tests.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 8b4aa18..147603e 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -23,7 +23,6 @@ jobs: uses: actions/setup-python@v5 with: python-version: "3.10" - cache: 'pip' - name: Install dependencies run: | python -m pip install --upgrade pip From f7c3541b5182b09ba54699b31e32da190967ce36 Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Fri, 8 May 2026 13:42:41 -0700 Subject: [PATCH 77/78] Fix job_array.sh failing on Ubuntu: use bash instead of sh The script uses bash array syntax (WORKDIRS=(...), ${WORKDIRS[$i]}) which is incompatible with dash (the default sh on Ubuntu/GitHub Actions), causing exit code 2. Switch shebang and debug command to bash. Co-Authored-By: Claude Sonnet 4.6 --- src/asimtools/job.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/asimtools/job.py b/src/asimtools/job.py index b586042..cf30ba1 100644 --- a/src/asimtools/job.py +++ b/src/asimtools/job.py @@ -217,7 +217,7 @@ def _gen_slurm_batch_preamble( if slurm_params is None: slurm_params = self.env.get('slurm', {}) - txt = '#!/usr/bin/sh\n\n' + txt = '#!/usr/bin/env bash\n\n' flags = slurm_params.get('flags', []) if isinstance(flags, dict): flag_list = [] @@ -846,7 +846,7 @@ def submit_slurm_array( # pylint: disable=too-many-locals,too-many-branches # Only for testing purposes print('SLURM command:', command) os.environ['SLURM_ARRAY_TASK_ID'] = '0' - command = ['sh', 'job_array.sh'] + command = ['bash', 'job_array.sh'] completed_process = subprocess.run( command, check=False, capture_output=True, text=True, From 593091c6d9caa1c2807057dafd3247357f0cc88e Mon Sep 17 00:00:00 2001 From: mkphuthi Date: Fri, 8 May 2026 13:44:51 -0700 Subject: [PATCH 78/78] Rewrite job_array.sh generation to use POSIX sh instead of bash arrays Replaces bash-specific array syntax (WORKDIRS=(...), ${WORKDIRS[$i]}) with a POSIX-compatible loop and counter, making the script portable to systems where sh is dash or another non-bash shell. Co-Authored-By: Claude Sonnet 4.6 --- src/asimtools/job.py | 43 ++++++++++++++++++++----------------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/src/asimtools/job.py b/src/asimtools/job.py index cf30ba1..950937a 100644 --- a/src/asimtools/job.py +++ b/src/asimtools/job.py @@ -217,7 +217,7 @@ def _gen_slurm_batch_preamble( if slurm_params is None: slurm_params = self.env.get('slurm', {}) - txt = '#!/usr/bin/env bash\n\n' + txt = '#!/usr/bin/env sh\n\n' flags = slurm_params.get('flags', []) if isinstance(flags, dict): flag_list = [] @@ -647,32 +647,29 @@ def _gen_array_script( ] ) - txt += '\necho "Job started on `hostname` at `date`"\n' - txt += 'CUR_DIR=`pwd`\n' + txt += '\necho "Job started on $(hostname) at $(date)"\n' + txt += 'CUR_DIR=$(pwd)\n' txt += 'echo "LAUNCHDIR: ${CUR_DIR}"\n' - txt += f'G={group_size} #Group size\n' + txt += f'G={group_size}\n' txt += 'N=${SLURM_ARRAY_TASK_ID}\n' - txt += 'WORKDIRS=($(ls -dv ./id-*))\n' - seqtxt = '$(seq $(($G*$N)) $(($G*$N+$G-1)) )' - txt += f'for i in {seqtxt}; do\n' - txt += ' WORKDIR=${WORKDIRS[$i]}\n' - txt += ' cd ${WORKDIR};\n' - # else: - # txt += '\nif [[ ! -z ${SLURM_ARRAY_TASK_ID} ]]; then\n' - # txt += ' fls=( id-* )\n' - # txt += ' WORKDIR=${fls[${SLURM_ARRAY_TASK_ID}]}\n' - # txt += 'fi\n\n' - # txt += 'cd ${WORKDIR}\n' - txt += ' ' + '\n'.join(slurm_params.get('precommands', [])) + '\n' - txt += '\n '.join( + txt += 'START=$((G*N))\n' + txt += 'END=$((G*N+G-1))\n' + txt += 'i=0\n' + txt += 'for WORKDIR in $(ls -dv ./id-*); do\n' + txt += ' if [ $i -ge $START ] && [ $i -le $END ]; then\n' + txt += ' cd ${WORKDIR}\n' + txt += ' ' + '\n '.join(slurm_params.get('precommands', [])) + '\n' + txt += ' ' + '\n '.join( self.unitjobs[0].calc_params.get('precommands', []) ) + '\n' - txt += ' echo "WORKDIR: ${WORKDIR}"\n' - txt += ' ' + self.unitjobs[0].gen_run_command() + '\n' - txt += ' ' + '\n'.join(slurm_params.get('postcommands', [])) + '\n' - txt += ' cd ${CUR_DIR}\n' + txt += ' echo "WORKDIR: ${WORKDIR}"\n' + txt += ' ' + self.unitjobs[0].gen_run_command() + '\n' + txt += ' ' + '\n '.join(slurm_params.get('postcommands', [])) + '\n' + txt += ' cd ${CUR_DIR}\n' + txt += ' fi\n' + txt += ' i=$((i+1))\n' txt += 'done\n' - txt += 'echo "Job ended at `date`"' + txt += 'echo "Job ended at $(date)"' if write: slurm_file = self.workdir / 'job_array.sh' @@ -846,7 +843,7 @@ def submit_slurm_array( # pylint: disable=too-many-locals,too-many-branches # Only for testing purposes print('SLURM command:', command) os.environ['SLURM_ARRAY_TASK_ID'] = '0' - command = ['bash', 'job_array.sh'] + command = ['sh', 'job_array.sh'] completed_process = subprocess.run( command, check=False, capture_output=True, text=True,