diff --git a/src/quacc/runners/prep.py b/src/quacc/runners/prep.py index 3f4a7e9f19..27f4b398ce 100644 --- a/src/quacc/runners/prep.py +++ b/src/quacc/runners/prep.py @@ -81,7 +81,8 @@ def calc_setup( # for all threads in the current process. However, elsewhere in the code, # we use absolute paths to avoid issues. We keep this here for now because some # old ASE calculators do not support the `directory` keyword argument. - os.chdir(tmpdir) + if SETTINGS.CHDIR: + os.chdir(tmpdir) return tmpdir, job_results_dir @@ -126,7 +127,8 @@ def calc_cleanup(atoms: Atoms, tmpdir: Path | str, job_results_dir: Path | str) # for all threads in the current process. However, elsewhere in the code, # we use absolute paths to avoid issues. We keep this here for now because some # old ASE calculators do not support the `directory` keyword argument. - os.chdir(job_results_dir) + if SETTINGS.CHDIR: + os.chdir(job_results_dir) # Gzip files in tmpdir if SETTINGS.GZIP_FILES: diff --git a/src/quacc/settings.py b/src/quacc/settings.py index 48b73950d0..651b7d7c2e 100644 --- a/src/quacc/settings.py +++ b/src/quacc/settings.py @@ -103,6 +103,20 @@ class QuaccSettings(BaseSettings): """ ), ) + CHDIR: bool = Field( + True, + description=( + """ + Whether quacc will make `os.chdir` calls to change the working directory + to be the location where the calculation is run. By default, we leave this + as `True` because not all ASE calculators properly support a `directory` + parameter. In most cases, this is fine, but it breaks thread safety. + If you need to run multiple, parallel calculations in a single Python process, + such as in a multithreaded job execution mode, then this setting needs + to be `False`. Note that not all calculators properly support this, however. + """ + ), + ) GZIP_FILES: bool = Field( True, description="Whether generated files should be gzip'd." ) diff --git a/tests/core/recipes/lj_recipes/test_lj_recipes.py b/tests/core/recipes/lj_recipes/test_lj_recipes.py index ea11cba7a9..d7c023f35f 100644 --- a/tests/core/recipes/lj_recipes/test_lj_recipes.py +++ b/tests/core/recipes/lj_recipes/test_lj_recipes.py @@ -93,3 +93,18 @@ def test_freq_job(tmp_path, monkeypatch): assert len(output["results"]["vib_freqs"]) == 3 * len(atoms) - 6 assert len(output["parameters_thermo"]["vib_freqs"]) == 3 * len(atoms) - 6 assert output["parameters_thermo"]["n_imag"] == 0 + + +def test_freq_job_threads(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + + SETTINGS.CHDIR = False + + atoms = molecule("H2O") + + output = freq_job(relax_job(atoms)["atoms"]) + assert output["natoms"] == len(atoms) + assert len(output["results"]["vib_freqs_raw"]) == 3 * len(atoms) + assert output["parameters_thermo"]["n_imag"] == 0 + + SETTINGS.CHDIR = DEFAULT_SETTINGS.CHDIR = True diff --git a/tests/parsl/test_emt_recipes.py b/tests/parsl/test_emt_recipes.py index 2d6bf6644b..beaeed9e33 100644 --- a/tests/parsl/test_emt_recipes.py +++ b/tests/parsl/test_emt_recipes.py @@ -4,12 +4,19 @@ from ase.build import bulk +from quacc import SETTINGS from quacc.recipes.emt.core import relax_job # skipcq: PYL-C0412 from quacc.recipes.emt.slabs import bulk_to_slabs_flow # skipcq: PYL-C0412 +DEFAULT_SETTINGS = SETTINGS.model_copy() -def test_parsl_functools(tmp_path, monkeypatch): + +@pytest.mark.parametrize("chdir", [True, False]) +def test_parsl_functools(tmp_path, monkeypatch, chdir): monkeypatch.chdir(tmp_path) + + SETTINGS.CHDIR = chdir + atoms = bulk("Cu") result = bulk_to_slabs_flow( atoms, job_params={"relax_job": {"opt_params": {"fmax": 0.1}}}, run_static=False @@ -18,11 +25,16 @@ def test_parsl_functools(tmp_path, monkeypatch): assert "atoms" in result[-1] assert result[-1]["fmax"] == 0.1 + SETTINGS.CHDIR = DEFAULT_SETTINGS.CHDIR + -def test_phonon_flow(tmp_path, monkeypatch): +@pytest.mark.parametrize("chdir", [True, False]) +def test_phonon_flow(tmp_path, monkeypatch, chdir): pytest.importorskip("phonopy") from quacc.recipes.emt.phonons import phonon_flow + SETTINGS.CHDIR = chdir + monkeypatch.chdir(tmp_path) atoms = bulk("Cu") output = phonon_flow(atoms) @@ -30,11 +42,16 @@ def test_phonon_flow(tmp_path, monkeypatch): 101, ) + SETTINGS.CHDIR = DEFAULT_SETTINGS.CHDIR + -def test_phonon_flow_multistep(tmp_path, monkeypatch): +@pytest.mark.parametrize("chdir", [True, False]) +def test_phonon_flow_multistep(tmp_path, monkeypatch, chdir): pytest.importorskip("phonopy") from quacc.recipes.emt.phonons import phonon_flow + SETTINGS.CHDIR = chdir + monkeypatch.chdir(tmp_path) atoms = bulk("Cu") relaxed = relax_job(atoms) @@ -42,3 +59,5 @@ def test_phonon_flow_multistep(tmp_path, monkeypatch): assert output.result()["results"]["thermal_properties"]["temperatures"].shape == ( 101, ) + + SETTINGS.CHDIR = DEFAULT_SETTINGS.CHDIR