Repository: Quantum-Accelerators/quacc
Pull request #1426 (2023-12-27 04:12:07)

[WIP] Refactor espresso calculator and recipes
#Summary

This PR seeks to streamline a few things related to the Espresso calculator and recipes.

1. I merged in the changes from #1415, which moves the `phonon_job` to its own module and makes the previous directory a required argument to better inform the user what is needed.

2. I removed the `calc_defaults` kwarg from the calculator. This shouldn't have been in there, as the way quacc works is that the `calc_defaults` are handled by the `_base.py` functions, not in the calculator itself. The functionality remains the same, but the logic for merging the keyword arguments is done _before_ instantiating the calculator.

3. Several aspects of the calculator were internally refactored without any change of functionality to the recipes. For instance, see the rewrite of `parse_pw_preset` to `parse_pseudopotential_info`.

4. There were a few instances before where the user-specified parameters were overwritten by a preset, which is a strict no-no. We should always respect the user's parameters. This was most notable with things like `ecutwfc`. If the user specified a value of `50`, but the preset found a maximum elemental `ecutwfc` of 100, it'd overwrite the user's `50` value. In practice, if the user asks for a value, they should see that value in the input file. In practice, if a user wants to rely on the max value from the preset (typically the case), they should not explicitly specify an `ecutwfc` at all.

5. The most notable change is that all parameters must be specified explicitly with respect to a given section in the input file. For instance, use `input_data = {"system": {"ecutwfc": 100}}` and not `input_data={"ecutfc": 100}`. There are several important reasons for this. The first is that ASE as a platform is trying to move away from any enumeration of input arguments since it is difficult to keep up with version updates, and we should do the same here. The second more important reason is that the existing approach makes it non-trivial to ensure the user-specified settings take priority.

For instance, consider a user-supplied argument of `input_data={"system": {"ecutfc": 50}}` and a preset containing

```yaml
input_data:
  ecutwfc: 100
```

You can't merge the resulting dictionaries because then you'd end up with `input_data = {"ecutwfc": 50, "system": {"ecutwfc": 100}}`. You can get around this by converting _both_ the list of keyword arguments and the presets to the same format with the list of acceptable keys, but it's messy. In general, I prefer to keep things as 1:1 comparable with the actual input file as possible, so it's easier to just remove the support for these kinds of shortcuts.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/Quantum-Accelerators/quacc/pull/1426?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Quantum-Accelerators) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`015bc69`)](https://app.codecov.io/gh/Quantum-Accelerators/quacc/commit/015bc699ccf5c2848450716650d7c1d0f65f99e0?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Quantum-Accelerators) 99.09% compared to head [(`1cb0ef2`)](https://app.codecov.io/gh/Quantum-Accelerators/quacc/pull/1426?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Quantum-Accelerators) 98.98%.


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #1426      +/-   ##
==========================================
- Coverage   99.09%   98.98%   -0.11%     
==========================================
  Files          76       76              
  Lines        2974     2958      -16     
==========================================
- Hits         2947     2928      -19     
- Misses         27       30       +3     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/Quantum-Accelerators/quacc/pull/1426?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Quantum-Accelerators).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Quantum-Accelerators).


Patch:
diff --git a/docs/user/recipes/recipes_list.md b/docs/user/recipes/recipes_list.md
index 2d24016b..83f23f55 100644
--- a/docs/user/recipes/recipes_list.md
+++ b/docs/user/recipes/recipes_list.md
@@ -171,10 +171,10 @@ The list of available quacc recipes is shown below. The "Req'd Extras" column sp
 
 <center>
 
-| Name            | Decorator       | Documentation                              | Req'd Extras |
-| --------------- | --------------- | ------------------------------------------ | ------------ |
-| Espresso Static | `#!Python @job` | [quacc.recipes.espresso.core.static_job][] |              |
-| Espresso Phonon | `#!Python @job` | [quacc.recipes.espresso.core.phonon_job][] |              |
+| Name            | Decorator       | Documentation                                 | Req'd Extras |
+| --------------- | --------------- | --------------------------------------------- | ------------ |
+| Espresso Static | `#!Python @job` | [quacc.recipes.espresso.core.static_job][]    |              |
+| Espresso Phonon | `#!Python @job` | [quacc.recipes.espresso.phonons.phonon_job][] |              |
 
 </center>
 
diff --git a/src/quacc/calculators/espresso/espresso.py b/src/quacc/calculators/espresso/espresso.py
index d135551d..daf0e301 100644
--- a/src/quacc/calculators/espresso/espresso.py
+++ b/src/quacc/calculators/espresso/espresso.py
@@ -8,12 +8,10 @@ from ase import Atoms
 from ase.calculators.espresso import Espresso as Espresso_
 from ase.calculators.espresso import EspressoProfile
 from ase.calculators.espresso import EspressoTemplate as EspressoTemplate_
-from ase.io.espresso import construct_namelist
 
 from quacc import SETTINGS
 from quacc.calculators.espresso.io import read, write
-from quacc.calculators.espresso.keys import ALL_KEYS
-from quacc.calculators.espresso.utils import parse_pw_preset
+from quacc.calculators.espresso.utils import get_pseudopotential_info
 from quacc.utils.dicts import recursive_dict_merge
 from quacc.utils.files import load_yaml_calc
 
@@ -143,7 +141,7 @@ class EspressoTemplate(EspressoTemplate_):
         input_data = parameters.get("input_data", {})
 
         for section in input_data:
-            for d_key in self.outdirs.keys():
+            for d_key in self.outdirs:
                 if d_key in input_data[section]:
                     path = Path(input_data[section][d_key])
                     path = path.expanduser().resolve()
@@ -171,10 +169,9 @@ class Espresso(Espresso_):
         self,
         input_atoms: Atoms | None = None,
         preset: str | None = None,
+        parallel_info: dict[str, Any] | None = None,
         template: EspressoTemplate | None = None,
         profile: EspressoProfile | None = None,
-        calc_defaults: dict[str, Any] | None = None,
-        parallel_info: dict[str, Any] | None = None,
         **kwargs,
     ) -> None:
         """
@@ -190,6 +187,10 @@ class Espresso(Espresso_):
             `ESPRESSO_PRESET_DIR` (default: quacc/calculators/espresso/presets),
             The .yaml extension is not necessary. Any user-supplied calculator
             **kwargs will override any corresponding preset values.
+        parallel_info
+            parallel_info is a dictionary passed to the ASE Espresso calculator
+            profile. It is used to specify prefixes for the command line arguments.
+            See the ASE documentation for more details.
         template
             ASE calculator templace which can be used to specify which espresso
             binary will be used in the calculation. This is taken care of by recipe
@@ -198,94 +199,86 @@ class Espresso(Espresso_):
             ASE calculator profile which can be used to specify the location of
             the espresso binary and pseudopotential files. This is taken care of
             internally using quacc settings.
-        calc_defaults
-            A dictionary of default input_data parameters to pass to the Espresso
-            calculator. These will be overridden by any user-supplied calculator
-            **kwargs.
-        parallel_info
-            parallel_info is a dictionary passed to the ASE Espresso calculator
-            profile. It is used to specify prefixes for the command line arguments.
-            See the ASE documentation for more details.
         **kwargs
-            Additional arguments to be passed to the Espresso calculator, e.g.
-            `input_data`, `kpts`... Takes all valid ASE calculator arguments.
+            Additional arguments to be passed to the Espresso calculator. Takes all valid
+            ASE calculator arguments, such as `input_data` and `kpts`. Refer to
+            `ase.calculators.espresso.Espresso` for details. Note that the full input
+            must be described; use `{"system":{"ecutwfc": 60}}` and not the `{"ecutwfc": 60}`
+            short-hand.
 
         Returns
         -------
         None
         """
-        self.preset = preset
         self.input_atoms = input_atoms or Atoms()
-        self.calc_defaults = calc_defaults or {}
+        self.preset = preset
+        self.parallel_info = parallel_info
+        self.kwargs = kwargs
+        self._user_calc_params = {}
 
         template = template or EspressoTemplate("pw")
-
-        kwargs = self._kwargs_handler(template.binary, **kwargs)
-
-        pseudo_path = (
-            kwargs["input_data"]
+        self._bin_path = str(SETTINGS.ESPRESSO_BIN_PATHS[template.binary])
+        self._binary = template.binary
+        self._cleanup_params()
+        self._pseudo_path = (
+            self._user_calc_params.get("input_data", {})
             .get("control", {})
             .get("pseudo_dir", str(SETTINGS.ESPRESSO_PSEUDO))
         )
-
-        bin_path = SETTINGS.ESPRESSO_BIN_PATHS[template.binary]
-        profile = profile or EspressoProfile(
-            binary=str(bin_path), parallel_info=parallel_info, pseudo_path=pseudo_path
+        self.profile = profile or EspressoProfile(
+            binary=self._bin_path,
+            parallel_info=parallel_info,
+            pseudo_path=self._pseudo_path,
         )
 
-        if kwargs.get("directory"):
-            raise ValueError("quacc does not support the directory argument.")
-
         super().__init__(
-            profile=profile, directory=".", parallel_info=parallel_info, **kwargs
+            profile=self.profile,
+            parallel_info=self.parallel_info,
+            **self._user_calc_params,
         )
 
         self.template = template
 
-    def _kwargs_handler(self, binary: str, **kwargs) -> dict[str, Any]:
+    def _cleanup_params(self) -> None:
         """
         Function that handles the kwargs. It will merge the user-supplied
-        kwargs with the defaults and preset values. Priority order is as follow:
-
-        User-supplied kwargs > preset > defaults
+        kwargs with the preset values, using the former as priority.
 
         Parameters
         ----------
-        binary
-            The espresso binary used to construct the namelist
-        **kwargs
-            User-supplied kwargs
+        None
 
         Returns
         -------
-        kwargs
-            The merged kwargs
+        None
         """
-        keys = ALL_KEYS[binary]
-        kwargs["input_data"] = construct_namelist(kwargs.get("input_data"), keys=keys)
-        self.calc_defaults["input_data"] = construct_namelist(
-            self.calc_defaults.get("input_data"), keys=keys
-        )
 
-        kpts = kwargs.get("kpts")
-        kspacing = kwargs.get("kspacing")
-
-        if kpts and kspacing:
-            raise ValueError("Cannot specify both kpts and kspacing.")
+        if self.kwargs.get("directory"):
+            raise ValueError("quacc does not support the directory argument.")
 
         if self.preset:
-            config = load_yaml_calc(SETTINGS.ESPRESSO_PRESET_DIR / f"{self.preset}")
-            preset = parse_pw_preset(config, self.input_atoms)
-            kwargs = recursive_dict_merge(preset, kwargs)
-
-        if kpts:
-            kwargs.pop("kspacing", None)
-        elif kspacing:
-            kwargs.pop("kpts", None)
-
-        kwargs = recursive_dict_merge(self.calc_defaults, kwargs)
-
-        if kwargs.get("kpts") == "gamma":
-            kwargs["kpts"] = None
-
-        return kwargs
+            calc_preset = load_yaml_calc(
+                SETTINGS.ESPRESSO_PRESET_DIR / f"{self.preset}"
+            )
+            ecutwfc, ecutrho, pseudopotentials = get_pseudopotential_info(
+                calc_preset, self.input_atoms
+            )
+            calc_preset.pop("pseudopotentials", None)
+            self._user_calc_params = recursive_dict_merge(
+                calc_preset,
+                {
+                    "input_data": {"system": {"ecutwfc": ecutwfc, "ecutrho": ecutrho}},
+                    "pseudopotentials": pseudopotentials,
+                },
+                self.kwargs,
+            )
+        else:
+            self._user_calc_params = self.kwargs
+
+        if self._user_calc_params.get("kpts") == "gamma":
+            self._user_calc_params["kpts"] = None
+
+        if self._user_calc_params.get("kpts") and self._user_calc_params.get(
+            "kspacing"
+        ):
+            raise ValueError("Cannot specify both kpts and kspacing.")
diff --git a/src/quacc/calculators/espresso/keys.py b/src/quacc/calculators/espresso/keys.py
deleted file mode 100644
index 82807800..00000000
--- a/src/quacc/calculators/espresso/keys.py
+++ /dev/null
@@ -1,376 +0,0 @@
-from __future__ import annotations
-
-from ase.io.espresso import PH_KEYS, Namelist
-
-NEB_KEYS = Namelist(
-    {
-        "PATH": [
-            "string_method",
-            "restart_mode",
-            "nstep_path",
-            "num_of_images",
-            "opt_scheme",
-            "CI_scheme",
-            "first_last_opt",
-            "minimum_image",
-            "temp_req",
-            "ds",
-            "k_max",
-            "k_min",
-            "path_thr",
-            "uses_masses",
-            "use_freezing",
-            "lfcp",
-            "fcp_mu",
-            "fcp_thr",
-            "fcp_scheme",
-        ]
-    }
-)
-
-Q2R_KEYS = Namelist({"INPUT": ["fildyn", "flfrc", "zasr", "loto_2d", "write_lr"]})
-
-MATDYN_KEYS = Namelist(
-    {
-        "INPUT": [
-            "flfrc",
-            "asr",
-            "huang",
-            "dos",
-            "nk1",
-            "nk2",
-            "nk3",
-            "deltaE",
-            "ndos",
-            "degauss",
-            "fldos",
-            "flfrq",
-            "flvec",
-            "fleig",
-            "fldyn",
-            "at",
-            "l1",
-            "l2",
-            "l3",
-            "ntyp",
-            "amass",
-            "readtau",
-            "fltau",
-            "la2F",
-            "q_in_band_form",
-            "q_in_cryst_coord",
-            "eigen_similarity",
-            "fd",
-            "na_ifc",
-            "nosym",
-            "loto_2d",
-            "loto_disable",
-            "read_lr",
-            "write_frc",
-        ]
-    }
-)
-
-DYNMAT_KEYS = Namelist(
-    {
-        "INPUT": [
-            "fildyn",
-            "q",
-            "amass",
-            "asr",
-            "remove_interaction_blocks",
-            "axis",
-            "lperm",
-            "lplasma",
-            "filout",
-            "fileig",
-            "filmol",
-            "filxsf",
-            "loto_2d",
-            "el_ph_nsig",
-            "el_ph_sigma",
-        ]
-    }
-)
-
-PROJWFC_KEYS = Namelist(
-    {
-        "PROJWFC": [
-            "prefix",
-            "outdir",
-            "ngauss",
-            "degauss",
-            "emin",
-            "emax",
-            "deltae",
-            "lsymdiag_basis",
-            "pawproj",
-            "filpdos",
-            "filproj",
-            "lwrite_overlaps",
-            "lbinary_data",
-            "kresolveddos",
-            "tdosinboxes",
-            "n_proj_boxes",
-            "irmin",
-            "irmax",
-            "plot_boxes",
-        ]
-    }
-)
-
-
-# Updated pw.x keys from my own branch
-
-PW_KEYS = Namelist(
-    (
-        (
-            "CONTROL",
-            [
-                "calculation",
-                "title",
-                "verbosity",
-                "restart_mode",
-                "wf_collect",
-                "nstep",
-                "iprint",
-                "tstress",
-                "tprnfor",
-                "dt",
-                "outdir",
-                "wfcdir",
-                "prefix",
-                "lkpoint_dir",
-                "max_seconds",
-                "etot_conv_thr",
-                "forc_conv_thr",
-                "disk_io",
-                "pseudo_dir",
-                "tefield",
-                "dipfield",
-                "lelfield",
-                "nberrycyc",
-                "lorbm",
-                "lberry",
-                "gdir",
-                "nppstr",
-                "gatelfcp",
-                "trism",
-            ],
-        ),
-        (
-            "SYSTEM",
-            [
-                "ibrav",
-                "celldm",
-                "a",
-                "b",
-                "c",
-                "cosAB",
-                "cosAC",
-                "cosBCnat",
-                "ntyp",
-                "nbnd",
-                "tot_charge",
-                "tot_magnetization",
-                "starting_magnetization",
-                "ecutwfc",
-                "ecutrho",
-                "ecutfock",
-                "nr1",
-                "nr2",
-                "nr3",
-                "nr1s",
-                "nr2s",
-                "nr3s",
-                "nosym",
-                "nosym_evc",
-                "noinv",
-                "no_t_rev",
-                "force_symmorphic",
-                "use_all_frac",
-                "occupations",
-                "one_atom_occupations",
-                "starting_spin_angle",
-                "degauss",
-                "smearing",
-                "nspin",
-                "noncolin",
-                "ecfixed",
-                "qcutz",
-                "q2sigma",
-                "input_dft",
-                "exx_fraction",
-                "screening_parameter",
-                "exxdiv_treatment",
-                "x_gamma_extrapolation",
-                "ecutvcut",
-                "nqx1",
-                "nqx2",
-                "nqx3",
-                "lda_plus_u",
-                "lda_plus_u_kind",
-                "Hubbard_U",
-                "Hubbard_J0",
-                "Hubbard_occ",
-                "Hubbard_alpha",
-                "Hubbard_beta",
-                "Hubbard_J",
-                "starting_ns_eigenvalue",
-                "U_projection_type",
-                "edir",
-                "emaxpos",
-                "eopreg",
-                "eamp",
-                "angle1",
-                "angle2",
-                "constrained_magnetization",
-                "fixed_magnetization",
-                "lambda",
-                "report",
-                "lspinorb",
-                "assume_isolated",
-                "esm_bc",
-                "esm_w",
-                "esm_efield",
-                "esm_nfit",
-                "fcp_mu",
-                "vdw_corr",
-                "london",
-                "london_s6",
-                "london_c6",
-                "london_rvdw",
-                "london_rcut",
-                "ts_vdw_econv_thr",
-                "ts_vdw_isolated",
-                "xdm",
-                "xdm_a1",
-                "xdm_a2",
-                "space_group",
-                "uniqueb",
-                "origin_choice",
-                "rhombohedral",
-                "zmon",
-                "realxz",
-                "block",
-                "block_1",
-                "block_2",
-                "block_height",
-            ],
-        ),
-        (
-            "ELECTRONS",
-            [
-                "electron_maxstep",
-                "scf_must_converge",
-                "conv_thr",
-                "adaptive_thr",
-                "conv_thr_init",
-                "conv_thr_multi",
-                "mixing_mode",
-                "mixing_beta",
-                "mixing_ndim",
-                "mixing_fixed_ns",
-                "diagonalization",
-                "ortho_para",
-                "diago_thr_init",
-                "diago_cg_maxiter",
-                "diago_david_ndim",
-                "diago_full_acc",
-                "efield",
-                "efield_cart",
-                "efield_phase",
-                "startingpot",
-                "startingwfc",
-                "tqr",
-            ],
-        ),
-        (
-            "IONS",
-            [
-                "ion_dynamics",
-                "ion_positions",
-                "pot_extrapolation",
-                "wfc_extrapolation",
-                "remove_rigid_rot",
-                "ion_temperature",
-                "tempw",
-                "tolp",
-                "delta_t",
-                "nraise",
-                "refold_pos",
-                "upscale",
-                "bfgs_ndim",
-                "trust_radius_max",
-                "trust_radius_min",
-                "trust_radius_ini",
-                "w_1",
-                "w_2",
-            ],
-        ),
-        (
-            "CELL",
-            [
-                "cell_dynamics",
-                "press",
-                "wmass",
-                "cell_factor",
-                "press_conv_thr",
-                "cell_dofree",
-            ],
-        ),
-        (
-            "RISM",
-            [
-                "nsolv",
-                "closure",
-                "tempv",
-                "ecutsolv",
-                "solute_lj",
-                "solute_epsilon",
-                "solute_sigma",
-                "starting1d",
-                "starting3d",
-                "smear1d",
-                "smear3d",
-                "rism1d_maxstep",
-                "rism3d_maxstep",
-                "rism1d_conv_thr",
-                "rism3d_conv_thr",
-                "mdiis1d_size",
-                "mdiis3d_size",
-                "mdiis1d_step",
-                "mdiis3d_step",
-                "rism1d_bond_width",
-                "rism1d_dielectric",
-                "rism1d_molesize",
-                "rism1d_nproc",
-                "rism3d_conv_level",
-                "rism3d_planar_average",
-                "laue_nfit",
-                "laue_expand_right",
-                "laue_expand_left",
-                "laue_starting_right",
-                "laue_starting_left",
-                "laue_buffer_right",
-                "laue_buffer_left",
-                "laue_both_hands",
-                "laue_wall",
-                "laue_wall_z",
-                "laue_wall_rho",
-                "laue_wall_epsilon",
-                "laue_wall_sigma",
-                "laue_wall_lj6",
-            ],
-        ),
-    )
-)
-
-ALL_KEYS = {
-    "pw": PW_KEYS,
-    "ph": PH_KEYS,
-    "neb": NEB_KEYS,
-    "q2r": Q2R_KEYS,
-    "dynmat": DYNMAT_KEYS,
-    "projwfc": PROJWFC_KEYS,
-}
diff --git a/src/quacc/calculators/espresso/utils.py b/src/quacc/calculators/espresso/utils.py
index f20c7726..afcd0a54 100644
--- a/src/quacc/calculators/espresso/utils.py
+++ b/src/quacc/calculators/espresso/utils.py
@@ -2,16 +2,15 @@ from __future__ import annotations
 
 from typing import TYPE_CHECKING
 
-from quacc.atoms.core import copy_atoms
-from quacc.utils.dicts import recursive_dict_merge
-
 if TYPE_CHECKING:
     from typing import Any
 
     from ase.atoms import Atoms
 
 
-def parse_pw_preset(config: dict[str, Any], atoms: Atoms) -> dict[str, Any]:
+def get_pseudopotential_info(
+    config: dict[str, Any], atoms: Atoms
+) -> tuple[float, float, dict[str, str]]:
     """
     Function that parses the pseudopotentials and cutoffs from a preset file.
     The cutoffs are taken from the largest value of the cutoffs among the elements
@@ -26,35 +25,24 @@ def parse_pw_preset(config: dict[str, Any], atoms: Atoms) -> dict[str, Any]:
 
     Returns
     -------
-    dict
-        A dictionary containing the pseudopotentials and cutoffs
+    float
+        The max(ecutwfc) value
+    float
+        The max(ecutrho) value
+    dict[str, str]
+        The pseudopotentials dictinoary, e.g. {"O": "O.pbe-n-kjpaw_psl.0.1.UPF"}
     """
 
-    atoms_copy = copy_atoms(atoms)
-
     if "pseudopotentials" in config:
         pp_dict = config["pseudopotentials"]
-        unique_elements = list(set(atoms_copy.symbols))
-        wfc_cutoff, rho_cutoff = 0, 0
+        unique_elements = list(set(atoms.get_chemical_symbols()))
+        ecutwfc, ecutrho = 0, 0
         pseudopotentials = {}
         for element in unique_elements:
-            if pp_dict[element]["cutoff_wfc"] > wfc_cutoff:
-                wfc_cutoff = pp_dict[element]["cutoff_wfc"]
-            if pp_dict[element]["cutoff_rho"] > rho_cutoff:
-                rho_cutoff = pp_dict[element]["cutoff_rho"]
+            if pp_dict[element]["cutoff_wfc"] > ecutwfc:
+                ecutwfc = pp_dict[element]["cutoff_wfc"]
+            if pp_dict[element]["cutoff_rho"] > ecutrho:
+                ecutrho = pp_dict[element]["cutoff_rho"]
             pseudopotentials[element] = pp_dict[element]["filename"]
-        pp_input_data = {"system": {"ecutwfc": wfc_cutoff, "ecutrho": rho_cutoff}}
-
-    input_data = config.get("input_data")
-    input_data = recursive_dict_merge(pp_input_data, input_data)
-
-    kspacing = config.get("kspacing")
-
-    kpts = config.get("kpts")
 
-    return {
-        "input_data": input_data,
-        "pseudopotentials": pseudopotentials,
-        "kspacing": kspacing,
-        "kpts": kpts,
-    }
+    return ecutwfc, ecutrho, pseudopotentials
diff --git a/src/quacc/recipes/espresso/_base.py b/src/quacc/recipes/espresso/_base.py
index 988d33de..d8237ccb 100644
--- a/src/quacc/recipes/espresso/_base.py
+++ b/src/quacc/recipes/espresso/_base.py
@@ -12,6 +12,7 @@ from quacc.calculators.espresso.espresso import (
 )
 from quacc.runners.ase import run_calc
 from quacc.schemas.ase import summarize_run
+from quacc.utils.dicts import recursive_dict_merge
 
 if TYPE_CHECKING:
     from typing import Any
@@ -63,15 +64,15 @@ def base_fn(
     """
 
     atoms = Atoms() if atoms is None else atoms
+    calc_flags = recursive_dict_merge(calc_defaults, calc_swaps)
 
     atoms.calc = Espresso(
         input_atoms=atoms,
         preset=preset,
+        parallel_info=parallel_info,
         template=template,
         profile=profile,
-        calc_defaults=calc_defaults,
-        parallel_info=parallel_info,
-        **calc_swaps,
+        **calc_flags,
     )
 
     final_atoms = run_calc(atoms, copy_files=copy_files)
diff --git a/src/quacc/recipes/espresso/core.py b/src/quacc/recipes/espresso/core.py
index ab379828..3bc2df63 100644
--- a/src/quacc/recipes/espresso/core.py
+++ b/src/quacc/recipes/espresso/core.py
@@ -77,72 +77,3 @@ def static_job(
         additional_fields={"name": "pw.x Static"},
         copy_files=copy_files,
     )
-
-
-@job
-def phonon_job(
-    preset: str | None = None,
-    copy_files: str | list[str] | None = None,
-    parallel_info: dict[str] | None = None,
-    **calc_kwargs,
-) -> RunSchema:
-    """
-    Function to carry out a basic ph.x calculation.
-
-    Parameters
-    ----------
-    preset
-        The name of a YAML file containing a list of parameters to use as
-        a "preset" for the calculator. quacc will automatically look in the
-        `ESPRESSO_PRESET_DIR` (default: quacc/calculators/espresso/presets).
-    copy_files
-        List of files to copy to the calculation directory. Almost always needed
-        for ph.x calculations. This parameter can either be a string or a list of
-        strings.
-
-        If a string is provided, it is assumed to be a path to a directory,
-        all of the child tree structure of that directory is going to be copied to the
-        scratch of this calculation. For phonon_job this is what most users will want to do.
-
-        If a list of strings is provided, each string point to a specific file. In this case
-        it is important to note that no directory structure is going to be copied, everything
-        is copied at the root of the temporary directory.
-    parallel_info
-        Dictionary containing information about the parallelization of the
-        calculation. See the ASE documentation for more information.
-    **calc_kwargs
-        calc_kwargs dictionary possibly containing the following keys:
-
-        - input_data: dict
-        - qpts: list[list[float]] | list[tuple[float]] | list[float]
-        - nat_todo: list[int]
-
-        See the docstring of [quacc.calculators.espresso.io.write_espresso_ph][] for more information.
-
-    Returns
-    -------
-    RunSchema
-        Dictionary of results from [quacc.schemas.ase.summarize_run][]
-    """
-
-    calc_defaults = {
-        "input_data": {
-            "inputph": {
-                "tr2_ph": 1e-12,
-                "alpha_mix(1)": 0.1,
-                "nmix_ph": 12,
-                "verbosity": "high",
-            }
-        },
-        "qpts": (0, 0, 0),
-    }
-
-    return base_fn(
-        preset=preset,
-        template=EspressoTemplate("ph"),
-        calc_defaults=calc_defaults,
-        calc_swaps=calc_kwargs,
-        parallel_info=parallel_info,
-        additional_fields={"name": "ph.x Phonon"},
-        copy_files=copy_files,
-    )
diff --git a/src/quacc/recipes/espresso/phonons.py b/src/quacc/recipes/espresso/phonons.py
new file mode 100644
index 00000000..d84aefab
--- /dev/null
+++ b/src/quacc/recipes/espresso/phonons.py
@@ -0,0 +1,74 @@
+"""Phonon recipes for espresso."""
+from __future__ import annotations
+
+from pathlib import Path
+from typing import TYPE_CHECKING
+
+from quacc import job
+from quacc.calculators.espresso.espresso import EspressoTemplate
+from quacc.recipes.espresso._base import base_fn
+
+if TYPE_CHECKING:
+    from quacc.schemas._aliases.ase import RunSchema
+
+
+@job
+def phonon_job(
+    prev_dir: str | Path,
+    preset: str | None = None,
+    parallel_info: dict[str] | None = None,
+    **calc_kwargs,
+) -> RunSchema:
+    """
+    Function to carry out a basic ph.x calculation.
+
+    Parameters
+    ----------
+    prev_dir
+        Previous directory where pw.x was run. This is used to copy the entire tree structure
+        of that directory to the working directory of this calculation.
+    preset
+        The name of a YAML file containing a list of parameters to use as
+        a "preset" for the calculator. quacc will automatically look in the
+        `ESPRESSO_PRESET_DIR` (default: quacc/calculators/espresso/presets).
+    parallel_info
+        Dictionary containing information about the parallelization of the
+        calculation. See the ASE documentation for more information.
+    **calc_kwargs
+        calc_kwargs dictionary possibly containing the following keys:
+
+        - input_data: dict
+        - qpts: list[list[float]] | list[tuple[float]] | list[float]
+        - nat_todo: list[int]
+
+        See the docstring of [quacc.calculators.espresso.io.write_espresso_ph][] for more information.
+
+    Returns
+    -------
+    RunSchema
+        Dictionary of results from [quacc.schemas.ase.summarize_run][]
+    """
+
+    calc_defaults = {
+        "input_data": {
+            "inputph": {
+                "tr2_ph": 1e-12,
+                "alpha_mix(1)": 0.1,
+                "nmix_ph": 12,
+                "verbosity": "high",
+            }
+        },
+        "qpts": (0, 0, 0),
+    }
+
+    template = EspressoTemplate("ph")
+
+    return base_fn(
+        preset=preset,
+        template=template,
+        calc_defaults=calc_defaults,
+        calc_swaps=calc_kwargs,
+        parallel_info=parallel_info,
+        additional_fields={"name": "ph.x Phonon"},
+        copy_files=prev_dir,
+    )


Test patch:
diff --git a/tests/core/calculators/espresso/test_espresso.py b/tests/core/calculators/espresso/test_espresso.py
index 3b5c9666..0edb31c3 100644
--- a/tests/core/calculators/espresso/test_espresso.py
+++ b/tests/core/calculators/espresso/test_espresso.py
@@ -9,33 +9,45 @@ from quacc.calculators.espresso.espresso import Espresso, EspressoTemplate
 
 
 def test_espresso_kwargs_handler():
-    calc_defaults = {
-        "input_data": {"system": {"ecutwfc": 20, "ecutrho": 80, "conv_thr": 1e-8}}
+    kwargs = {
+        "kpts": (1, 1, 1),
+        "input_data": {"system": {"ecutwfc": 30, "ecutrho": 80}},
     }
 
-    input_data = {"system": {"ecutwfc": 30}}
-
     preset = "sssp_1.3.0_pbe_efficiency"
 
     atoms = Atoms(symbols="LiLaOZr")
 
-    calc = Espresso(
-        input_atoms=atoms,
-        preset=preset,
-        calc_defaults=calc_defaults,
-        input_data=input_data,
-        kpts=(1, 1, 1),
-    )
+    calc = Espresso(input_atoms=atoms, preset=preset, **kwargs)
 
     expected_parameters = {
-        "input_data": {
-            "control": {},
-            "system": {"ecutwfc": 30, "ecutrho": 400, "conv_thr": 1e-8},
-            "electrons": {},
-            "ions": {},
-            "cell": {},
-            "rism": {},
+        "input_data": {"system": {"ecutwfc": 30, "ecutrho": 80}},
+        "kpts": (1, 1, 1),
+        "pseudopotentials": {
+            "O": "O.pbe-n-kjpaw_psl.0.1.UPF",
+            "Zr": "Zr_pbe_v1.uspp.F.UPF",
+            "Li": "li_pbe_v1.4.uspp.F.UPF",
+            "La": "La.paw.z_11.atompaw.wentzcovitch.v1.2.upf",
         },
+    }
+
+    assert calc.template.binary == "pw"
+    assert calc.parameters["kpts"] == expected_parameters["kpts"]
+    assert (
+        calc.parameters["input_data"]["system"]
+        == expected_parameters["input_data"]["system"]
+    )
+
+
+def test_espresso_kwargs_handler_v2():
+    preset = "sssp_1.3.0_pbe_efficiency"
+
+    atoms = Atoms(symbols="LiLaOZr")
+
+    calc = Espresso(input_atoms=atoms, preset=preset, kpts=(1, 1, 1))
+
+    expected_parameters = {
+        "input_data": {"system": {"ecutwfc": 50.0, "ecutrho": 400.0}},
         "kpts": (1, 1, 1),
         "pseudopotentials": {
             "O": "O.pbe-n-kjpaw_psl.0.1.UPF",
@@ -46,39 +58,37 @@ def test_espresso_kwargs_handler():
     }
 
     assert calc.template.binary == "pw"
-    assert calc.parameters == expected_parameters
+    assert calc.parameters["kpts"] == expected_parameters["kpts"]
+    assert (
+        calc.parameters["input_data"]["system"]
+        == expected_parameters["input_data"]["system"]
+    )
+    assert (
+        calc.parameters["pseudopotentials"] == expected_parameters["pseudopotentials"]
+    )
 
 
 def test_espresso_presets():
-    calc_defaults = {
-        "input_data": {
-            "system": {"ecutwfc": 20, "ecutrho": 80, "occupations": "fixed"},
-            "electrons": {"scf_must_converge": False},
-        }
+    input_data = {
+        "system": {"ecutwfc": 200, "occupations": "fixed"},
+        "electrons": {"scf_must_converge": False, "conv_thr": 1.0e-16},
     }
 
-    input_data = {"system": {"ecutwfc": 30}, "electrons": {"conv_thr": 1.0e-16}}
-
     preset = "esm_metal_slab_efficiency"
 
     atoms = Atoms(symbols="LiLaOZr")
     atoms.set_cell([5, 2, 10])
 
     calc = Espresso(
-        input_atoms=atoms,
-        preset=preset,
-        calc_defaults=calc_defaults,
-        input_data=input_data,
-        kpts=[7, 17, 1],
+        input_atoms=atoms, preset=preset, kpts=[7, 17, 1], input_data=input_data
     )
 
     expected_parameters = {
         "input_data": {
-            "control": {},
             "system": {
-                "ecutwfc": 30,
+                "ecutwfc": 200,
                 "ecutrho": 400.0,
-                "occupations": "smearing",
+                "occupations": "fixed",
                 "smearing": "marzari-vanderbilt",
                 "degauss": 0.01,
                 "assume_isolated": "esm",
@@ -90,9 +100,6 @@ def test_espresso_presets():
                 "mixing_mode": "local-TF",
                 "mixing_beta": 0.35,
             },
-            "ions": {},
-            "cell": {},
-            "rism": {},
         },
         "kpts": [7, 17, 1],
         "pseudopotentials": {
@@ -103,7 +110,18 @@ def test_espresso_presets():
         },
     }
     assert calc.template.binary == "pw"
-    assert calc.parameters == expected_parameters
+    assert (
+        calc.parameters["input_data"]["system"]
+        == expected_parameters["input_data"]["system"]
+    )
+    assert (
+        calc.parameters["input_data"]["electrons"]
+        == expected_parameters["input_data"]["electrons"]
+    )
+    assert calc.parameters["kpts"] == expected_parameters["kpts"]
+    assert (
+        calc.parameters["pseudopotentials"] == expected_parameters["pseudopotentials"]
+    )
 
 
 def test_espresso_presets_gamma():
@@ -129,16 +147,7 @@ def test_espresso_kpts():
 def test_outdir_handler(tmp_path, monkeypatch):
     monkeypatch.chdir(tmp_path)
 
-    parameters = {
-        "input_data": {
-            "control": {},
-            "system": {"outdir": "../test3/test2/test1"},
-            "electrons": {},
-            "ions": {},
-            "cell": {},
-            "rism": {},
-        }
-    }
+    parameters = {"input_data": {"system": {"outdir": "../test3/test2/test1"}}}
 
     test_path = "../test3/test2/test1"
     fake_template = EspressoTemplate()
@@ -168,19 +177,7 @@ def test_outdir_handler(tmp_path, monkeypatch):
 
 
 def test_bad_calculator_params():
-    calc_defaults = {
-        "input_data": {"system": {"ecutwfc": 20, "ecutrho": 80, "conv_thr": 1e-8}}
-    }
-
-    input_data = {"system": {"ecutwfc": 30}}
-
     atoms = Atoms(symbols="LiLaOZr")
 
     with pytest.raises(ValueError):
-        Espresso(
-            input_atoms=atoms,
-            calc_defaults=calc_defaults,
-            input_data=input_data,
-            kpts=(1, 1, 1),
-            directory="bad",
-        )
+        Espresso(input_atoms=atoms, kpts=(1, 1, 1), directory="bad")
diff --git a/tests/core/calculators/espresso/test_io.py b/tests/core/calculators/espresso/test_io.py
index eb4fb62d..814ef915 100644
--- a/tests/core/calculators/espresso/test_io.py
+++ b/tests/core/calculators/espresso/test_io.py
@@ -4,14 +4,8 @@ from pathlib import Path
 
 import numpy as np
 import pytest
-from ase.io.espresso import construct_namelist, read_fortran_namelist
 
-from quacc.calculators.espresso.io import (
-    read_espresso_ph,
-    write_espresso_io,
-    write_espresso_ph,
-)
-from quacc.calculators.espresso.keys import ALL_KEYS
+from quacc.calculators.espresso.io import read_espresso_ph
 
 RUN_PATH = Path(__file__).parent / "test_runs"
 
@@ -22,166 +16,6 @@ def _read_gzip_file(file_path):
     return content
 
 
-def test_write_espresso_io():
-    input_data = {
-        "degauss": 2,
-        "Emin": True,
-        "Emax": None,
-        "nbnd": 10,
-        "ecutwfc": 20,
-        "irmin(2, 6)": 0.1,
-        "outdir(5)": "/path/to/outdir",
-    }
-
-    input_data = construct_namelist(input_data, ALL_KEYS["projwfc"])
-
-    string_io = StringIO()
-
-    write_espresso_io(string_io, input_data=input_data)
-
-    expected = (
-        "&PROJWFC\n"
-        "   outdir(5)        = '/path/to/outdir'\n"
-        "   degauss          = 2\n"
-        "   emin             = .true.\n"
-        "   irmin(2, 6)      = 0.1\n"
-        "/\n"
-        "EOF"
-    )
-
-    string_io.seek(0)
-
-    recycled_input_data = read_fortran_namelist(string_io)[0]
-
-    assert recycled_input_data == input_data
-    assert string_io.getvalue() == expected
-
-
-def test_write_espresso_ph_single():
-    input_data = {
-        "amass(1)": 1.0,
-        "amass(2)": 2.0,
-        "prefix": "prefix",
-        "outdir": "/path/to/outdir",
-        "eth_rps": 0.1,
-    }
-
-    qpts = (0.5, -0.1, 1 / 3)
-
-    input_data = construct_namelist(input_data, ALL_KEYS["ph"])
-
-    string_io = StringIO()
-
-    write_espresso_ph(string_io, input_data=input_data, qpts=qpts)
-
-    expected = (
-        "&INPUTPH\n"
-        "   amass(1)         = 1.0\n"
-        "   amass(2)         = 2.0\n"
-        "   outdir           = '/path/to/outdir'\n"
-        "   prefix           = 'prefix'\n"
-        "   eth_rps          = 0.1\n"
-        "/\n"
-        "0.50000000 -0.10000000 0.33333333\n"
-    )
-
-    string_io.seek(0)
-
-    recycled_input_data = read_fortran_namelist(string_io)[0]
-
-    assert recycled_input_data == input_data
-    assert string_io.getvalue() == expected
-
-
-def test_write_espresso_ph_list():
-    input_data = {
-        "amass(1)": 1.0,
-        "amass(2)": 2.0,
-        "prefix": "prefix",
-        "outdir": "/path/to/outdir",
-        "eth_rps": 0.1,
-        "qplot": True,
-        "ldisp": True,
-    }
-
-    qpts = [(0.5, -0.1, 1 / 3, 2), (0.1, 0.2, 0.3, 10), (0.2, 0.3, 0.4, 1)]
-
-    input_data = construct_namelist(input_data, ALL_KEYS["ph"])
-
-    string_io = StringIO()
-
-    write_espresso_ph(string_io, input_data=input_data, qpts=qpts)
-
-    expected = (
-        "&INPUTPH\n"
-        "   amass(1)         = 1.0\n"
-        "   amass(2)         = 2.0\n"
-        "   outdir           = '/path/to/outdir'\n"
-        "   prefix           = 'prefix'\n"
-        "   eth_rps          = 0.1\n"
-        "   qplot            = .true.\n"
-        "   ldisp            = .true.\n"
-        "/\n"
-        "3\n"
-        "0.50000000 -0.10000000 0.33333333 2\n"
-        "0.10000000 0.20000000 0.30000000 10\n"
-        "0.20000000 0.30000000 0.40000000 1\n"
-    )
-
-    string_io.seek(0)
-
-    recycled_input_data = read_fortran_namelist(string_io)[0]
-
-    assert recycled_input_data == input_data
-    assert string_io.getvalue() == expected
-
-
-def test_write_espresso_ph_nat_todo():
-    input_data = {
-        "amass(1)": 1.0,
-        "amass(2)": 2.0,
-        "prefix": "prefix",
-        "outdir": "/path/to/outdir",
-        "eth_rps": 0.1,
-        "qplot": True,
-        "nat_todo": True,
-        "ldisp": True,
-    }
-
-    qpts = [(0.5, -0.1, 1 / 3, 1), (0.1, 0.2, 0.3, -1), (0.2, 0.3, 0.4, 4)]
-
-    input_data = construct_namelist(input_data, ALL_KEYS["ph"])
-
-    string_io = StringIO()
-
-    write_espresso_ph(string_io, input_data=input_data, qpts=qpts, nat_todo=[1, 2, 3])
-
-    expected = (
-        "&INPUTPH\n"
-        "   amass(1)         = 1.0\n"
-        "   amass(2)         = 2.0\n"
-        "   outdir           = '/path/to/outdir'\n"
-        "   prefix           = 'prefix'\n"
-        "   eth_rps          = 0.1\n"
-        "   qplot            = .true.\n"
-        "   ldisp            = .true.\n"
-        "   nat_todo         = .true.\n"
-        "/\n"
-        "3\n"
-        "0.50000000 -0.10000000 0.33333333 1\n"
-        "0.10000000 0.20000000 0.30000000 -1\n"
-        "0.20000000 0.30000000 0.40000000 4\n"
-        "1 2 3\n"
-    )
-
-    string_io.seek(0)
-
-    recycled_input_data = read_fortran_namelist(string_io)[0]
-
-    assert recycled_input_data == input_data
-    assert string_io.getvalue() == expected
-
-
 @pytest.mark.parametrize("i", range(1, 66, 5))
 def test_read_espresso_ph_all(i):
     file_contents = _read_gzip_file(RUN_PATH / f"run{i}.txt.gz")
diff --git a/tests/core/recipes/espresso_recipes/test_espresso.py b/tests/core/recipes/espresso_recipes/test_espresso.py
index 9546fdf7..310445ad 100644
--- a/tests/core/recipes/espresso_recipes/test_espresso.py
+++ b/tests/core/recipes/espresso_recipes/test_espresso.py
@@ -7,7 +7,8 @@ from ase.build import bulk
 from ase.io.espresso import construct_namelist
 
 from quacc import SETTINGS
-from quacc.recipes.espresso.core import phonon_job, static_job
+from quacc.recipes.espresso.core import static_job
+from quacc.recipes.espresso.phonons import phonon_job
 from quacc.utils.files import copy_decompress_files
 
 pytestmark = pytest.mark.skipif(which("pw.x") is None, reason="QE not installed")
@@ -25,13 +26,9 @@ def test_static_job(tmp_path, monkeypatch):
     atoms = bulk("Si")
 
     input_data = {
-        "occupations": "smearing",
-        "smearing": "gaussian",
-        "degauss": 0.005,
-        "mixing_mode": "plain",
-        "mixing_beta": 0.6,
-        "pseudo_dir": tmp_path,
-        "conv_thr": 1.0e-6,
+        "system": {"occupations": "smearing", "smearing": "gaussian", "degauss": 0.005},
+        "electrons": {"mixing_mode": "plain", "mixing_beta": 0.6, "conv_thr": 1.0e-6},
+        "control": {"pseudo_dir": tmp_path},
     }
 
     pseudopotentials = {"Si": "Si.upf"}
@@ -40,8 +37,6 @@ def test_static_job(tmp_path, monkeypatch):
         atoms, input_data=input_data, pseudopotentials=pseudopotentials, kspacing=0.5
     )
 
-    input_data = dict(construct_namelist(input_data))
-
     assert np.allclose(results["atoms"].positions, atoms.positions, atol=1.0e-4)
 
     assert np.allclose(results["atoms"].cell, atoms.cell, atol=1.0e-3)
@@ -65,14 +60,9 @@ def test_static_job_outdir(tmp_path, monkeypatch):
     atoms = bulk("Si")
 
     input_data = {
-        "occupations": "smearing",
-        "smearing": "gaussian",
-        "degauss": 0.005,
-        "mixing_mode": "plain",
-        "mixing_beta": 0.6,
-        "pseudo_dir": tmp_path,
-        "conv_thr": 1.0e-6,
-        "outdir": "test2/test3/test4",
+        "system": {"occupations": "smearing", "smearing": "gaussian", "degauss": 0.005},
+        "electrons": {"mixing_mode": "plain", "mixing_beta": 0.6, "conv_thr": 1.0e-6},
+        "control": {"pseudo_dir": tmp_path, "outdir": "test2/test3/test4"},
     }
 
     pseudopotentials = {"Si": "Si.upf"}
@@ -106,16 +96,13 @@ def test_static_job_outdir_abs(tmp_path, monkeypatch):
     atoms = bulk("Si")
 
     input_data = {
-        "occupations": "smearing",
-        "smearing": "gaussian",
-        "degauss": 0.005,
-        "mixing_mode": "plain",
-        "mixing_beta": 0.6,
-        "pseudo_dir": tmp_path,
-        "conv_thr": 1.0e-6,
-        "outdir": Path(tmp_path, "test2").resolve(),
+        "system": {"occupations": "smearing", "smearing": "gaussian", "degauss": 0.005},
+        "electrons": {"mixing_mode": "plain", "mixing_beta": 0.6, "conv_thr": 1.0e-6},
+        "control": {
+            "pseudo_dir": tmp_path,
+            "outdir": Path(tmp_path, "test2").resolve(),
+        },
     }
-
     pseudopotentials = {"Si": "Si.upf"}
 
     results = static_job(
@@ -146,27 +133,8 @@ def test_static_job_dir_fail(tmp_path, monkeypatch):
 
     atoms = bulk("Si")
 
-    input_data = {
-        "occupations": "smearing",
-        "smearing": "gaussian",
-        "degauss": 0.005,
-        "mixing_mode": "plain",
-        "mixing_beta": 0.6,
-        "pseudo_dir": tmp_path,
-        "conv_thr": 1.0e-6,
-        "outdir": Path(tmp_path, "test2").resolve(),
-    }
-
-    pseudopotentials = {"Si": "Si.upf"}
-
     with pytest.raises(ValueError):
-        results = static_job(
-            atoms,
-            input_data=input_data,
-            pseudopotentials=pseudopotentials,
-            kspacing=0.5,
-            directory=Path(tmp_path, "test2").resolve(),
-        )
+        static_job(atoms, directory=Path("fake_path"))
 
 
 def test_phonon_job(tmp_path, monkeypatch):
@@ -181,16 +149,12 @@ def test_phonon_job(tmp_path, monkeypatch):
     SETTINGS.ESPRESSO_PSEUDO = tmp_path
 
     input_data = {
-        "calculation": "scf",
-        "occupations": "smearing",
-        "smearing": "cold",
-        "degauss": 0.02,
-        "mixing_mode": "TF",
-        "mixing_beta": 0.7,
-        "conv_thr": 1.0e-6,
+        "control": {"calculation": "scf"},
+        "system": {"occupations": "smearing", "smearing": "cold", "degauss": 0.02},
+        "electrons": {"mixing_mode": "TF", "mixing_beta": 0.7, "conv_thr": 1.0e-6},
     }
 
-    ph_loose = {"tr2_ph": 1e-8}
+    ph_loose = {"inputph": {"tr2_ph": 1e-8}}
 
     pseudopotentials = {"Li": "Li.upf"}
 
@@ -198,7 +162,7 @@ def test_phonon_job(tmp_path, monkeypatch):
         atoms, input_data=input_data, pseudopotentials=pseudopotentials, kspacing=0.25
     )
 
-    ph_results = phonon_job(input_data=ph_loose, copy_files=pw_results["dir_name"])
+    ph_results = phonon_job(pw_results["dir_name"], input_data=ph_loose)
 
     assert (0, 0, 0) in ph_results["results"]
     assert np.allclose(
@@ -231,16 +195,14 @@ def test_phonon_job_list_to_do(tmp_path, monkeypatch):
     SETTINGS.ESPRESSO_PSEUDO = tmp_path
 
     input_data = {
-        "calculation": "scf",
-        "occupations": "smearing",
-        "smearing": "cold",
-        "degauss": 0.02,
-        "mixing_mode": "TF",
-        "mixing_beta": 0.7,
-        "conv_thr": 1.0e-6,
+        "control": {"calculation": "scf"},
+        "system": {"occupations": "smearing", "smearing": "cold", "degauss": 0.02},
+        "electrons": {"mixing_mode": "TF", "mixing_beta": 0.7, "conv_thr": 1.0e-6},
     }
 
-    ph_loose = {"tr2_ph": 1e-8, "qplot": True, "nat_todo": 1, "ldisp": True}
+    ph_loose = {
+        "inputph": {"tr2_ph": 1e-8, "qplot": True, "nat_todo": 1, "ldisp": True}
+    }
 
     pseudopotentials = {"Li": "Li.upf"}
 
@@ -253,10 +215,7 @@ def test_phonon_job_list_to_do(tmp_path, monkeypatch):
     nat_todo = [1]
 
     ph_results = phonon_job(
-        input_data=ph_loose,
-        copy_files=pw_results["dir_name"],
-        qpts=qpts,
-        nat_todo=nat_todo,
+        pw_results["dir_name"], input_data=ph_loose, qpts=qpts, nat_todo=nat_todo
     )
 
     assert (0, 0, 0) in ph_results["results"]