diff --git a/src/autoplex/misc/qe/__init__.py b/src/autoplex/misc/qe/__init__.py new file mode 100644 index 000000000..4ce69eee7 --- /dev/null +++ b/src/autoplex/misc/qe/__init__.py @@ -0,0 +1,20 @@ +from .jobs import QeStaticMaker +from .run import run_qe_static +from .schema import ( + InputDoc, + OutputDoc, + QeKpointsSettings, + QeRunSettings, + TaskDoc, +) +from .utils import QeStaticInputGenerator + +__all__ = [ + "QEStaticMaker", + "QeInputSet", + "QeKpointsSettings", + "QeRunResult", + "QeRunSettings", + "QeStaticInputGenerator", + "run_qe_static", +] diff --git a/src/autoplex/misc/qe/jobs.py b/src/autoplex/misc/qe/jobs.py new file mode 100644 index 000000000..e975701de --- /dev/null +++ b/src/autoplex/misc/qe/jobs.py @@ -0,0 +1,91 @@ +from __future__ import annotations + +import os +from dataclasses import dataclass + +from ase import Atoms +from jobflow import Flow, Maker +from pymatgen.core import Structure + +from .run import run_qe_static +from .schema import QeKpointsSettings, QeRunSettings +from .utils import QeStaticInputGenerator + + +@dataclass +class QeStaticMaker(Maker): + """ + StaticMaker for Quantum ESPRESSO: + - assemble and write one .pwi per structure using InputGenerator; + - create a `run_qe_static` job for each input; + - assemble flow with all jobs. + + Parameters + ---------- + name : str + Name of the Flow. + command : str + Command to execute QE (e.g. "pw.x" or "mpirun -np 4 pw.x -nk 2"). + workdir : str | None + Directory used to write input/output files. Default: "/qe_static". + run_settings : QeRunSettings | None + Update namelists (&control, &system, &electrons). + kpoints : QeKpointsSettings | None + Set up for K_POINTS if it is not contained in the template. + pseudo : dict[str, str] | None + Dictionary of atomic symbols and corresponding pseudopotential files. + """ + + name: str = "qe_static" + command: str = "pw.x" + workdir: str | None = None + run_settings: QeRunSettings | None = None + kpoints: QeKpointsSettings | None = None + pseudo: dict[str, str] | None = None + + def make( + self, + structures: Atoms | list[Atoms] | Structure | list[Structure] | str | list[str], + ) -> Flow: + """ + Create a Flow to run static SCF calculations with QE for given structures. + + Parameters + ---------- + structures : Atoms | list[Atoms] | Structure | list[Structure] | str | list[str] + Single or list of ASE Atoms, pymatgen Structures, or ASE-readable files. + + Returns + ------- + Flow + A jobflow Flow with one `run_qe_static` job per structure. + """ + workdir = self.workdir or os.path.join(os.getcwd(), "qe_static") + os.makedirs(workdir, exist_ok=True) + + # Generate one input per structure + generator = QeStaticInputGenerator( + run_settings=self.run_settings or QeRunSettings(), + kpoints=self.kpoints or QeKpointsSettings(), + pseudo=self.pseudo or {}, + ) + input_sets = generator.generate_for_structures( + structures=structures, workdir=workdir, seed_prefix="structure" + ) + + # If single structure, generate one job + if len(input_sets) == 1: + job = run_qe_static(input_sets[0], command=self.command) + job.name = self.name + return job + + # Else create one SCF job per structure and assemble flow + jobs = [] + tasks = [] + for i, inp in enumerate(input_sets): + j = run_qe_static(inp, command=self.command) + j.name = f"{self.name}_{i}" + jobs.append(j) + tasks.append(j.output) + + return Flow(jobs=jobs, output=tasks, name=self.name) diff --git a/src/autoplex/misc/qe/run.py b/src/autoplex/misc/qe/run.py new file mode 100644 index 000000000..e8221d852 --- /dev/null +++ b/src/autoplex/misc/qe/run.py @@ -0,0 +1,103 @@ +from __future__ import annotations + +import logging +import os +import re +import subprocess + +from ase.io import read +from ase.units import GPa +from jobflow import job +from pymatgen.io.ase import AseAtomsAdaptor + +from .schema import InputDoc, OutputDoc, TaskDoc + +logger = logging.getLogger(__name__) + + +_ENERGY_RE = re.compile(r"!\s+total energy\s+=\s+([-\d\.Ee+]+)\s+Ry") + + +def _parse_total_energy_ev(pwo_path: str) -> float | None: + """ + Extract and return total energy (eV) if found in QE output (.pwo) + + Parameters + ---------- + pwo_path : str + Path to QE output file (.pwo) + + Returns + ------- + float | None + Total energy in eV if found, else None + """ + if not os.path.exists(pwo_path): + return None + + try: + with open(pwo_path, errors="ignore") as fh: + for line in fh: + m = _ENERGY_RE.search(line) + if m: + # convert energy in eV + ry = float(m.group(1)) + return ry * 13.605693009 + except Exception: + return None + return None + + +@job +def run_qe_static(input: InputDoc, command: str) -> TaskDoc: + """ + Execute single QE SCF static calculation from .pwi file. + Parse output .pwo file to extract total energy, forces, stress, and final structure. + + Parameters + ---------- + input : InputDoc + Input document containing paths and settings for the QE run. + command : str + Command to execute QE (e.g. "pw.x" or "mpirun -np 4 pw.x -nk 2"). + + Returns + ------- + TaskDoc + Document containing input, output, and metadata of the QE run. + """ + pwi_path = input.pwi_path + pwo_path = pwi_path.replace(".pwi", ".pwo") + # Assemble pwscf run command e.g. "pw.x < input.pwi >> input.pwo" + run_cmd = f"{command} < {pwi_path} >> {pwo_path}" + + success = False + try: + subprocess.run(run_cmd, shell=True, check=True, executable="/bin/bash") + except subprocess.CalledProcessError as exc: + logger.error("QE failed for %s: %s", pwi_path, exc) + + # # Manual parse of total energy in eV from .pwo + # energy_ev = _parse_total_energy_ev(pwo_path) + + # Parse with ASE + atoms = read(pwo_path) + energy_ev = atoms.get_total_energy() + forces_evA = atoms.get_forces() + stress_kbar = atoms.get_stress(voigt=False) * (-10 / GPa) + final_structure = AseAtomsAdaptor().get_structure(atoms) + + output = OutputDoc( + energy=energy_ev, + forces=forces_evA.tolist(), + stress=stress_kbar.tolist(), + energy_per_atom=energy_ev / len(atoms) if energy_ev is not None else None, + ) + + return TaskDoc( + structure=final_structure, + dir_name=os.path.dirname(pwi_path), + task_label="qe_scf", + input=input, + output=output, + ) diff --git a/src/autoplex/misc/qe/schema.py b/src/autoplex/misc/qe/schema.py new file mode 100644 index 000000000..e88476449 --- /dev/null +++ b/src/autoplex/misc/qe/schema.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +from emmet.core.math import Matrix3D, Vector3D +from emmet.core.structure import StructureMetadata +from pydantic import BaseModel, Field, field_validator +from pymatgen.core import Structure + + +class QeRunSettings(BaseModel): + """ + Set QE namelists for static calculation SCF + Standard namelists: CONTROL, SYSTEM, ELECTRONS + """ + + control: dict[str, object] = Field(default_factory=dict) + system: dict[str, object] = Field(default_factory=dict) + electrons: dict[str, object] = Field(default_factory=dict) + + @field_validator("control", "system", "electrons") + @classmethod + def _lowercase_keys(cls, v: dict[str, object]) -> dict[str, object]: + # default lowercase keywords + return {str(k).lower(): v[k] for k in v} + + +class QeKpointsSettings(BaseModel): + """ + K-points: use k-space resoultion with automatic Monkhorst-Pack grid + k-points offset as in Quantum ESPRESSO manual: 0: False, 1: True + """ + + kspace_resolution: float | None = None # angstrom^-1 + koffset: list[bool] = Field(default_factory=lambda: [False, False, False]) + + @field_validator("koffset") + @classmethod + def _len3(cls, v: list[bool]) -> list[bool]: + if len(v) != 3: + raise ValueError("koffset must be a list of 3 booleans.") + return v + + +class InputDoc(BaseModel): + """ + Inputs and contexts used to run the static SCF job + """ + + workdir: str + pwi_path: str + seed: str + run_settings: QeRunSettings = Field( + None, description="QE namelist section with: &control, &system, &electrons" + ) + pseudo: dict[str, str] = Field( + None, + description="Dictionary of atomic symbols and corresponding pseudopotential files.", + ) + kpoints: QeKpointsSettings = Field(None, description="QE K_POINTS settings") + + +class OutputDoc(BaseModel): + """ + The outputs of this jobs + """ + + energy: float | None = Field(None, description="Total energy in units of eV.") + + energy_per_atom: float | None = Field( + None, + description="Energy per atom of the final molecule or structure " + "in units of eV/atom.", + ) + + forces: list[Vector3D] | None = Field( + None, + description=( + "The force on each atom in units of eV/A for the final molecule " + "or structure." + ), + ) + + # NOTE: units for stresses were converted to kbar (* -10 from standard output) + # to comply with MP convention + stress: Matrix3D | None = Field( + None, description="The stress on the cell in units of kbar." + ) + + +class TaskDoc(StructureMetadata): + """Document containing information on structure manipulation using Quantum ESPRESSO.""" + + structure: Structure = Field( + None, description="Final output structure from the task" + ) + + input: InputDoc = Field( + None, description="The input information used to run this job." + ) + + output: OutputDoc = Field(None, description="The output information from this job.") + + task_label: str = Field( + None, + description="Description of the QE task (e.g., static, relax)", + ) + + dir_name: str | None = Field( + None, description="Directory where the QE calculations are performed." + ) diff --git a/src/autoplex/misc/qe/utils.py b/src/autoplex/misc/qe/utils.py new file mode 100644 index 000000000..c663a313d --- /dev/null +++ b/src/autoplex/misc/qe/utils.py @@ -0,0 +1,389 @@ +from __future__ import annotations + +import logging +import os +from dataclasses import dataclass + +import numpy as np +from ase import Atom, Atoms +from ase.data import atomic_masses, atomic_numbers +from ase.io import read +from pymatgen.core import Structure +from pymatgen.io.ase import AseAtomsAdaptor + +from .schema import InputDoc, QeKpointsSettings, QeRunSettings + +logger = logging.getLogger(__name__) + + +@dataclass +class QeStaticInputGenerator: + """ + Input generator to run static SCF calculations with Quantum Espresso (.pwi). + Used to write one .pwi for each structure inside `workdir`. + + Parameters + ---------- + run_settings : QeRunSettings | None + Update namelists (&control, &system, &electrons). + kpoints : QeKpointsSettings | None + Set up for K_POINTS. + pseudo : dict[str, str] | None + Dictionary of atomic symbols and corresponding pseudopotential files. + """ + + def __init__( + self, + run_settings: QeRunSettings | None = None, + kpoints: QeKpointsSettings | None = None, + pseudo: dict[str, str] | None = None, + ) -> None: + self.run_settings = run_settings or QeRunSettings() + self.kpoints = kpoints or QeKpointsSettings() + self.pseudo = pseudo or {} + + def generate_for_structures( + self, + structures: Atoms | list[Atoms] | Structure | list[Structure] | str | list[str], + workdir: str, + seed_prefix: str = "structure", + ) -> list[InputDoc]: + """ + Generate one .pwi for each structure read from ASE-readable files. + + Parameters + ---------- + structures : Atoms | list[Atoms] | Structure | list[Structure] | str | list[str] + Single or list of ASE Atoms, pymatgen Structures, or ASE-readable files. + workdir : str + Directory used to write input/output files. + seed_prefix : str + Prefix for naming input/output QE files. + + Returns + ------- + list[InputDoc] + List of InputDoc containing path to generated .pwi files and metadata. + """ + os.makedirs(workdir, exist_ok=True) + atoms_list = _load_structures(structures) + input_sets: list[InputDoc] = [] + + for i, atoms in enumerate(atoms_list): + seed = f"{seed_prefix}_{i}" + pwi_path = os.path.join(workdir, f"{seed}.pwi") + self._write_pwi( + pwi_output=pwi_path, + atoms=atoms, + ) + input_sets.append( + InputDoc( + workdir=workdir, + pwi_path=pwi_path, + seed=seed, + run_settings=self.run_settings, + kpoints=self.kpoints, + pseudo=self.pseudo, + ) + ) + return input_sets + + def _write_pwi( + self, + *, + pwi_output: str, + atoms: Atoms, + ) -> None: + """ + Write .pwi file for the current Atoms. + + Parameters + ---------- + pwi_output : str + Path to output .pwi file. + atoms : Atoms + ASE Atoms object. + + Raises + ------ + ValueError + If required information is missing to write the .pwi file. + """ + # Minimal template with main QE namelists + template_lines = _render_minimal_namelists(self.run_settings) + pwi = list(template_lines) + + # Find indices of key lines in template + idx_diskio = None + idx_outdir = None + idx_nat = None + idx_ntyp = None + idx_kpts = None + for idx, line in enumerate(pwi): + ls = line.lower() + if "nat" in ls and "!" not in ls: + idx_nat = idx + elif "ntyp" in ls and "!" not in ls: + idx_ntyp = idx + elif "disk_io" in ls: + idx_diskio = idx + elif "outdir" in ls: + idx_outdir = idx + elif "k_points" in ls: + idx_kpts = idx + + # Number of atoms + nat = len(atoms) + if idx_nat is None: + raise ValueError("Template must define a 'nat = ' line (in &system).") + pwi[idx_nat] = f" nat = {nat}\n" + + # Output directory + structure_id = os.path.basename(pwi_output).replace(".pwi", "") + if idx_diskio is None or "none" not in pwi[idx_diskio].lower(): + if idx_outdir is None: + insert_at = (idx_diskio + 1) if idx_diskio is not None else len(pwi) + pwi.insert(insert_at, f" outdir = '{structure_id}'\n") + else: + pwi[idx_outdir] = f" outdir = '{structure_id}'\n" + else: + if idx_outdir is None: + insert_at = (idx_diskio + 1) if idx_diskio is not None else len(pwi) + pwi.insert(insert_at, " outdir = 'OUT'\n") + + # ATOMIC_SPECIES + if self.pseudo is None: + raise ValueError( + "Pseudo dictionary must be provided to write ATOMIC_SPECIES." + ) + species = set(atoms.get_chemical_symbols()) + ntyp = len(species) + pwi[idx_ntyp] = f" ntyp = {ntyp}\n" + species_lines = ["\nATOMIC_SPECIES\n"] + for s in sorted(species): + if s not in self.pseudo: + raise ValueError( + f"Missing pseudo for atomic symbol '{s}' in pseudo dictionary." + ) + mass = atomic_masses[atomic_numbers[s]] + species_lines.append(f"{s} {mass:.4f} {self.pseudo[s]}\n") + species_lines.append("\n") + + # K-POINTS + kpt_lines = _render_kpoints( + template_lines=pwi, idx_kpts=idx_kpts, atoms=atoms, kpoints=self.kpoints + ) + kpt_lines.append("\n") + + # CELL_PARAMETERS + cell = atoms.cell + cell_lines = ["\nCELL_PARAMETERS (angstrom)\n"] + for i in range(3): + cell_lines.append( + f"{cell[i, 0]:.10f} {cell[i, 1]:.10f} {cell[i, 2]:.10f}\n" + ) + cell_lines.append("\n") + + # ATOMIC_POSITIONS + pos_lines = ["\nATOMIC_POSITIONS (angstrom)\n"] + for i, atom in enumerate(atoms): + x, y, z = atoms.positions[i] + pos_lines.append(f"{atom.symbol} {x:.10f} {y:.10f} {z:.10f}\n") + pos_lines.append("\n") + + # Assemble and write + with open(pwi_output, "w") as fh: + fh.writelines(pwi) + fh.writelines(species_lines) + fh.writelines(kpt_lines) + fh.writelines(cell_lines) + fh.writelines(pos_lines) + + +# --------- Utils --------- + + +def _read_template(path: str | None) -> list[str]: + """Read template .pwi file if provided, else return empty list.""" + if path is None: + return [] + if not os.path.exists(path): + raise FileNotFoundError(f"template_pwi not found: {path}") + with open(path) as fh: + return fh.readlines() + + +def _load_structures( + structures: Atoms | list[Atoms] | Structure | list[Structure] | str | list[str], +) -> list[Atoms]: + """ + Load structures from various input types and return a list of ASE Atoms objects. + + Parameters + ---------- + structures : Atoms | list[Atoms] | Structure | list[Structure] | str | list[str] + Single or list of ASE Atoms, pymatgen Structures, or ASE-readable files. + + Returns + ------- + list[Atoms] + List of ASE Atoms objects. + """ + # ASE readable files + # Sinlge ASE-readable file + if isinstance(structures, str): + atoms_list = read(structures, index=":") + # List of ASE readable files + elif isinstance(structures, list) and all(isinstance(s, str) for s in structures): + atom_list = [] + for fname in structures: + atoms_list += read(fname, index=":") + + # ASE Atoms + elif isinstance(structures, Atoms): + # List of ASE Atoms objects + if isinstance(structures[0], Atoms): + atoms_list = structures + # Single ASE Atoms + elif isinstance(structures, Atom): + atoms_list = [structures] + else: + raise ValueError("Unsupported ASE Atoms input.") + # List of ASE Atoms objects + elif isinstance(structures, list) and all(isinstance(s, Atoms) for s in structures): + atoms_list = structures + + # Pymatgen Structures + # Single pymatgen structure + elif isinstance(structures, Structure): + atoms_list = [AseAtomsAdaptor().get_atoms(structures)] + # List of pymatgen structures + elif isinstance(structures, list) and all( + isinstance(s, Structure) for s in structures + ): + adaptor = AseAtomsAdaptor() + atoms_list = [adaptor.get_atoms(s) for s in structures] + + else: + raise ValueError("Unsupported type for structures input.") + + return atoms_list + + +def _render_minimal_namelists(settings: QeRunSettings) -> list[str]: + """ + Create draft lines for namelists &control, &system, &electrons. + If some namelist is missing in `settings`, it will be created with minimal entries. + + Parameters + ---------- + settings : QeRunSettings + QE namelist settings. + + Returns + ------- + list[str] + List of lines for the namelists to be written in the .pwi file. + """ + + def _render_block(name: str, kv: dict) -> list[str]: + lines = [f"&{name}\n"] + for k, v in kv.items(): + if isinstance(v, bool): + vv = ".true." if v else ".false." + elif isinstance(v, (int, float)): + vv = v + else: + vv = f"'{v}'" + lines.append(f" {k} = {vv}\n") + lines.append("/\n\n") + return lines + + out: list[str] = [] + out += _render_block("control", settings.control) + out += _render_block("system", settings.system) + out += _render_block("electrons", settings.electrons) + return out + + +def _render_kpoints( + *, + template_lines: list[str], + idx_kpts: int | None, + atoms: Atoms, + kpoints: QeKpointsSettings, +) -> list[str]: + """ + Create lines for K_POINTS section. + + Parameters + ---------- + template_lines : list[str] + Lines of the template .pwi file. + idx_kpts : int | None + Index of K_POINTS line in template_lines, or None if not present. + atoms : Atoms + ASE Atoms object. + kpoints : QeKpointsSettings + K-points settings. + + Returns + ------- + list[str] + Lines for K_POINTS section to be written in the .pwi file. + """ + # if K_POINTS in template keep it + if idx_kpts is not None and idx_kpts >= 0: + line = template_lines[idx_kpts].lower() + if "gamma" in line: + kpts = template_lines[idx_kpts : idx_kpts + 1] + elif "automatic" in line: + kpts = template_lines[idx_kpts : idx_kpts + 2] + elif "tpiba" in line or "crystal" in line: + n = int(template_lines[idx_kpts + 1].split()[0]) + kpts = template_lines[idx_kpts : idx_kpts + n + 2] + else: + raise ValueError( + f"Unknown K_POINTS format: {template_lines[idx_kpts].strip()}" + ) + del template_lines[idx_kpts:] + return ["\n"] + kpts + + # Otherwise create K_POINTS using kspace_resolution + if kpoints.kspace_resolution is None: + raise ValueError( + "K_POINTS not found in template and kspace_resolution is None. " + "Either add K_POINTS to template or set kspace_resolution." + ) + + # Generate Monkhorst–Pack mesh + mp_mesh = _compute_kpoints_grid(atoms.cell, kpoints.kspace_resolution) + line = f"{mp_mesh[0]} {mp_mesh[1]} {mp_mesh[2]}" + for off in kpoints.koffset: + line += " 1" if off else " 0" + return ["\nK_POINTS automatic\n", f"{line}\n"] + + +def _compute_kpoints_grid(cell: np.ndarray, kspace_resolution: float) -> list[int]: + """ + Compute Monkhorst-Pack mesh from cell and kspace_resolution (in angstrom^-1). + + Parameters + ---------- + cell : np.ndarray + 3x3 array with cell vectors in angstrom. + kspace_resolution : float + Desired k-space resolution in angstrom^-1. + + Returns + ------- + list[int] + List of 3 integers defining the Monkhorst-Pack mesh. + """ + rec = 2.0 * np.pi * np.linalg.inv(cell).T + lengths = np.linalg.norm(rec, axis=1) + mesh = [max(1, int(np.ceil(L / kspace_resolution))) for L in lengths] + logger.debug( + "QE MP mesh %s using k-resolution %s angstrom^-1", mesh, kspace_resolution + ) + return mesh diff --git a/tests/auto/QE/initial_dataset.extxyz b/tests/auto/QE/initial_dataset.extxyz new file mode 100644 index 000000000..617a82597 --- /dev/null +++ b/tests/auto/QE/initial_dataset.extxyz @@ -0,0 +1,23 @@ +5 +Lattice="4.305297245283376 0.0 1.1625217649309496 0.8909299950637738 4.280333028546382 1.2880608262677014 0.0 0.0 4.72729418447685" Properties=species:S:1:pos:R:3:forces:R:3:magmoms:R:1 energy=-2514.1464323047767 free_energy=-2514.1464323047767 stress="0.007359951443438806 0.00299778461362621 0.0052683883960144545 0.00299778461362621 0.0026837747092279974 0.0037396559667073666 0.0052683883960144545 0.0037396559667073666 0.0063628322733322" pbc="T T T" +O 4.50768846 1.33619342 2.92160180 -0.17530373 -0.10837200 -0.14322382 -0.20730000 +O 1.82101029 2.69224629 4.07320896 0.10518717 0.08744862 0.14952482 -0.00050000 +O 3.74062566 0.79659710 2.12389339 0.17508853 0.10835117 0.14262192 -0.20730000 +C 2.41609781 3.20583755 4.94720316 -0.00019309 0.00017149 -0.00040289 0.00000000 +O 3.01116349 3.71950374 5.82116302 -0.10477888 -0.08759928 -0.14852003 -0.00060000 +8 +Lattice="4.6056051052883795 0.0 2.029353509877331 1.811973285238557 4.241415722640877 2.024043819726518 0.0 0.0 6.196473833460105" Properties=species:S:1:pos:R:3:forces:R:3:magmoms:R:1 energy=-3894.545303604514 free_energy=-3894.545303604514 stress="0.02617208282126037 -0.0035165436662139 0.01209121763456158 -0.0035165436662139 0.02923413846795054 0.007873201550335298 0.01209121763456158 0.007873201550335298 0.004317177106667822" pbc="T T T" +C 3.52254256 3.39084463 3.80256102 0.02988496 -0.02485177 -0.00790871 -0.00000000 +C 4.92641658 1.26664697 6.90007421 -0.02382616 0.01373226 0.01277735 0.00000000 +O 5.38937929 2.34182500 7.41231336 -0.36160612 -0.84502596 -0.40126976 -0.00040000 +O 4.68875284 3.39517442 4.32552071 -0.92541382 -0.00182266 -0.41182953 0.00000000 +O 3.75714279 1.25867136 6.38448820 0.92343690 0.00715384 0.40076119 0.00030000 +O 3.06303694 2.31729625 3.28347615 0.35900468 0.84653006 0.40832589 0.00010000 +O 1.02422967 0.20052108 4.87576818 -0.54057470 0.82168523 -0.00692809 0.00010000 +O 1.00749081 0.21551202 1.77354824 0.53909426 -0.81740075 0.00607192 -0.00020000 +4 +Lattice="3.5430015722830794 0.0 1.3334437401239645 -0.19990356019914338 3.755621037089823 1.4851449742235037 0.0 0.0 4.927789704843149" Properties=species:S:1:pos:R:3:forces:R:3:magmoms:R:1 energy=-1947.2726323461177 free_energy=-1947.2726323461177 stress="0.018799277171208773 -0.009603010497371065 -0.01168631012625862 -0.009603010497371065 0.02233969293541786 -0.010452306291430558 -0.01168631012625862 -0.010452306291430558 0.022246040858667517" pbc="T T T" +O 2.62466719 0.38683228 3.18016181 -0.08298467 0.86162243 -0.71473377 0.00010000 +O -0.10657170 1.28243135 4.94944308 -0.81817131 0.06298020 0.80407420 0.00020000 +C 2.57548477 1.32559350 2.30699142 0.40906354 -0.33314401 -0.03192410 0.00000000 +O 1.72125422 2.26331329 2.38182181 0.49209243 -0.59145863 -0.05741608 -0.00030000 diff --git a/tests/auto/QE/test_qe.py b/tests/auto/QE/test_qe.py new file mode 100644 index 000000000..444dbf58e --- /dev/null +++ b/tests/auto/QE/test_qe.py @@ -0,0 +1,103 @@ +from ase.io import read +from pymatgen.io.ase import AseAtomsAdaptor +from jobflow_remote import submit_flow, set_run_config +from autoplex.misc.qe import QeStaticMaker, QeRunSettings, QeKpointsSettings + +# --------- QE namelists dictionaries --------- +control_dict = { + "calculation" : "scf", + "restart_mode" : "from_scratch", + "prefix" : "FeCOH", + "tprnfor" : True, + "tstress" : True, + "outdir" : "./OUT/", + "disk_io" : 'none', + "pseudo_dir" : '/leonardo/home/userexternal/apacini0/PSEUDO', + "max_seconds" : 86000 +} + +system_dict = { + "ibrav" : 0, + "nat" : 506, + "ntyp" : 4, + "ecutwfc" : 60, + "ecutrho" : 480, + "occupations" : "smearing", + "smearing" : "gaussian", + "degauss" : 0.00735, + "nosym" : True, + "vdw_corr" : "dft-d3", + "nspin" : 2, + "starting_magnetization(1)" : 0.4 +} + +electrons_dict = { + "diagonalization" : "david", + "mixing_beta" : 0.15, + "electron_maxstep" : 150, + "mixing_mode" : "local-TF", + "mixing_ndim" : 16, + "conv_thr" : 1.0e-6, +} + +pseudo_dict = { + "Fe": "Fe.pbe-sp-van.UPF", + "C": "C.pbe-n-kjpaw_psl.1.0.0.UPF", + "O": "O.pbe-n-kjpaw_psl.1.0.0.UPF", + "H": "H.pbe-kjpaw_psl.1.0.0.UPF", +} + +# --------- Resources --------- +parallel_gpu_resources = { + "account": "IscrB_CNT-HARV", + "partition": "boost_usr_prod", + "qos": "boost_qos_dbg", + "time": "00:30:00", + "nodes": 1, + "ntasks_per_node": 4, + "cpus_per_task": 8, + "gres": "gpu:4", + "mem": "480000", + "job_name": "qe_auto", + "qerr_path": "JOB.err", + "qout_path": "JOB.out", + } + + + +if __name__ == "__main__": + # QE command + qe_command = "mpirun -np 4 pw.x -nk 4" + + # QE run seetings (computational parameters namelist) + qe_run_settings = QeRunSettings( + control=control_dict, + system=system_dict, + electrons=electrons_dict, + ) + + # QE KPOINTS settings + k_points_settings = QeKpointsSettings( + kspace_resolution=0.25, # angstrom^-1 + koffset=[False, False, True], + ) + + # Instance of QEStaticMaker + qe_maker = QeStaticMaker( + name="static_qe", + command=qe_command, + workdir=None, # default /qe_static + run_settings=qe_run_settings, + kpoints=k_points_settings, + pseudo=pseudo_dict, + ) + + # Define QE scf workflow + structures_path="/leonardo_work/EUHPC_A04_113/Alberto/GenMLFF-progect/autoplex/tests/auto/QE/initial_dataset.extxyz" + qe_scf_workflow = qe_maker.make(structures_path) + + # Update flow config + set_run_config(qe_scf_workflow, name_filter="static_qe", exec_config="qe_config", worker="schedule_worker", resources=parallel_gpu_resources) + + # Submit flow + submit_flow(qe_scf_workflow, worker="local_worker", resources={}, project="GenMLFF") \ No newline at end of file