diff --git a/tests/calculators/test_calc_generic.py b/tests/calculators/test_calc_generic.py index 8a7421cf..0cc4b0ac 100644 --- a/tests/calculators/test_calc_generic.py +++ b/tests/calculators/test_calc_generic.py @@ -70,6 +70,18 @@ def test_default_properties(): assert "dummy_forces" in mol_out.arrays.keys() +def test_config_specific_calculator(tmp_path): + mol_in = [molecule("CH4"), molecule("CH4"), molecule("CH4")] + mol_in[1].info["WFL_CALCULATOR_KWARGS"] = {'epsilon':2.0} + mol_in[2].info["WFL_CALCULATOR_INITIALIZER"] = EMT + calculator = [LennardJones, [], {}] + mol_out = generic.calculate(mol_in, OutputSpec(tmp_path / "run.xyz"), calculator, properties=["energy", "forces"], output_prefix="dummy_") + + energies = [] + for at in mol_out: + energies.append(at.info['dummy_energy']) + assert energies[0] == energies[1]/2 != energies[2] + #################################################################################################### class EMT_override_def_autopara(EMT): diff --git a/tests/calculators/test_vasp.py b/tests/calculators/test_vasp.py index c9e1144c..73360750 100644 --- a/tests/calculators/test_vasp.py +++ b/tests/calculators/test_vasp.py @@ -1,6 +1,7 @@ from pathlib import Path import glob import os +import copy import numpy as np @@ -244,9 +245,50 @@ def test_vasp_scratchdir(tmp_path, monkeypatch): assert nfiles == 18 scratch_dir = Path("/tmp") / str(run_dir[0].resolve()).replace("/", "", 1).replace("/", "_") - assert not os.path.exists(scratch_dir) + assert not scratch_dir.is_dir() ats = list(configs_eval) assert 'TEST_energy' in ats[0].info assert 'TEST_forces' in ats[0].arrays # ase.io.write(sys.stdout, list(configs_eval), format='extxyz') + + +def test_vasp_per_configuration(tmp_path): + vasp_kwargs = { + "encut": 200.0, # kinetic energy cutoff + "ediff": 1.0e-3, + "kspacing": 1.0, + "pp": os.environ['PYTEST_VASP_POTCAR_DIR'], + "workdir": tmp_path + } + + atoms = [Atoms('Si', cell=(2, 2, 2), pbc=[True] * 3), + Atoms('Si', cell=(2, 2, 2), pbc=[True] * 3), + Atoms('Si', cell=(2, 2, 2), pbc=[True] * 3)] + + tmp = copy.deepcopy(vasp_kwargs) + tmp['encut'] = 220.0 + atoms[1].info["WFL_CALCULATOR_INITIALIZER"] = Vasp + atoms[1].info["WFL_CALCULATOR_KWARGS"] = tmp + + tmp = copy.deepcopy(vasp_kwargs) + tmp['encut'] = 240.0 + atoms[2].info["WFL_CALCULATOR_KWARGS"] = tmp + + calculator = (Vasp, [], vasp_kwargs) + + configs_eval = generic.calculate( + inputs=ConfigSet(atoms), + outputs=OutputSpec('vasp_out.regular.xyz', file_root=tmp_path), + calculator=calculator, + output_prefix='TEST_') + + ats = list(configs_eval) + + with open(tmp_path / ats[2].info['vasp_rundir'] / 'INCAR', 'r') as fincar: + for l in fincar: + if l.split('=')[0].strip() == 'ENCUT': + assert float(l.split('=')[1]) == 240.0 + + assert ats[0].info['TEST_energy'] > ats[1].info['TEST_energy'] > ats[2].info['TEST_energy'] + # ase.io.write(sys.stdout, list(configs_eval), format='extxyz') diff --git a/wfl/calculators/generic.py b/wfl/calculators/generic.py index b0bc1394..578c46d8 100644 --- a/wfl/calculators/generic.py +++ b/wfl/calculators/generic.py @@ -11,18 +11,27 @@ def _run_autopara_wrappable(atoms, calculator, properties=None, output_prefix='_auto_', verbose=False, raise_calc_exceptions=False): - """evaluates configs using an arbitrary calculator and store results in SinglePointCalculator + """evaluates configs using an arbitrary calculator and store results in info/arrays entries + or `SinglePointCalculator`. Defaults to wfl_num_inputs_per_python_subprocess=10, to avoid recreating the calculator for each configuration, unless calculator class defines a wfl_generic_default_autopara_info attribute in which case that value is used for the default. + If `Atoms.info` contains 'WFL\_CALCULATOR\_INITIALIZER', 'WFL\_CALCULATOR\_ARGS' or + 'WFL\_CALCULATOR\_KWARGS', an individual calculator will be created for that `Atoms` object. + The `initializer` and `*args` will be _overridden_ by the corresponding `Atoms.info` entries, but + the `**kwargs` will be _modified_ (`dict.update`) by the `Atoms.info` entry. + Parameters ---------- atoms: ase.atoms.Atoms / list(Atoms) input configuration(s) - calculator: Calculator / (initializer, args, kwargs) - ASE calculator or routine to call to create calculator + calculator: Calculator / (initializer (callable), args (list), kwargs (dict)) + ASE calculator or routine to call to create calculator. If 'WFL\_CALCULATOR\_ARGS' + `...\_INITIALIZER`, or `...\_KWARGS` are present in any `Atoms.info` dicts, calculator + _must_ be a 3-tuple so that those `initializer`, `*args` or `**kwargs` can be used to + override defaults. properties: list(str), default ['energy', 'forces', stress'] Properties to request from calculator. If any are not present after calculation (e.g. stress for nonperiodic configurations), a warning will be printed. @@ -36,14 +45,41 @@ def _run_autopara_wrappable(atoms, calculator, properties=None, output_prefix='_ if properties is None: properties = ['energy', 'forces', 'stress'] - calculator = construct_calculator_picklesafe(calculator) + try: + calculator_default = construct_calculator_picklesafe(calculator) + calculator_failure_message = None + except Exception as exc: + # if calculator constructor failed, it may still be fine if every atoms object has + # enough info to construct its own calculator, but we won't know until later + calculator_failure_message = str(exc) + calculator_default = None if output_prefix == '_auto_': output_prefix = calculator.__class__.__name__ + '_' at_out = [] for at in atoms_to_list(atoms): - at.calc = calculator + calculator_use = calculator_default + if ("WFL_CALCULATOR_INITIALIZER" in at.info or + "WFL_CALCULATOR_ARGS" in at.info or + "WFL_CALCULATOR_KWARGS" in at.info): + # create per-config Calculator + try: + initializer_use = at.info.get("WFL_CALCULATOR_INITIALIZER", calculator[0]) + args_use = at.info.get("WFL_CALCULATOR_ARGS", calculator[1]) + kwargs_use = calculator[2].copy() + kwargs_use.update(at.info.get("WFL_CALCULATOR_KWARGS", {})) + calculator_use = construct_calculator_picklesafe((initializer_use, args_use, kwargs_use)) + except Exception as exc: + raise TypeError("calculators.generic.calculate got WFL_CALCULATOR_INITIALIZER, _ARGS, or _KWARGS " + f"but constructor failed, most likely because calculator wasn't a tuple (TypeError) " + "or original tuple had invalid element that wasn't overridden by `Atoms.info` entry. " + f"Constructor exception was '{exc}'") + + if calculator_use is None: + raise ValueError(f"Failed to construct calculator, original attempt's exception was '{calculator_failure_message}'") + at.calc = calculator_use + calculation_succeeded = False try: # explicitly pass system_changes=all_changes because some calculators, e.g. ace.ACECalculator,