Source code for scm.plams.interfaces.adfsuite.quickjobs

import numpy as np

from scm.plams.core.functions import get_config, delete_job, finish, init
from scm.plams.core.settings import Settings
from scm.plams.core.errors import PlamsError
from scm.plams.interfaces.adfsuite.ams import AMSJob
from scm.plams.interfaces.adfsuite.amsworker import AMSWorker
from scm.plams.mol.molecule import Molecule
from typing import Any, Optional, Union, Sequence, overload, List, TYPE_CHECKING
from typing_extensions import TypeGuard

if TYPE_CHECKING:
    from scm.base import ChemicalSystem

    _has_scm_chemsys: bool
else:
    try:
        from scm.base import ChemicalSystem

        _has_scm_chemsys = True
    except ImportError:
        _has_scm_chemsys = False

__all__ = ["preoptimize", "refine_density", "refine_lattice", "shakemd"]


def _is_chemical_system(molecule: Union[Molecule, "ChemicalSystem"]) -> TypeGuard["ChemicalSystem"]:
    return _has_scm_chemsys and isinstance(molecule, ChemicalSystem)


def _get_main_structure(
    results: Any, molecule: Union[Molecule, "ChemicalSystem"]
) -> Optional[Union[Molecule, "ChemicalSystem"]]:
    if _is_chemical_system(molecule):
        return results.get_main_system()
    return results.get_main_molecule()


def _get_density(molecule: Union[Molecule, "ChemicalSystem"]) -> float:
    if _is_chemical_system(molecule):
        return molecule.density("kg/m3")
    return molecule.get_density()


def _set_density(molecule: Union[Molecule, "ChemicalSystem"], density: float) -> None:
    if _is_chemical_system(molecule):
        molecule.set_density(density, unit="kg/m3")
    else:
        molecule.set_density(density)


def _get_lattice_vectors(molecule: Union[Molecule, "ChemicalSystem"]) -> Any:
    if _is_chemical_system(molecule):
        return molecule.lattice.vectors
    return molecule.lattice


def _with_updated_geometry(
    molecule: Union[Molecule, "ChemicalSystem"], result_molecule: Molecule
) -> Union[Molecule, "ChemicalSystem"]:
    if not _is_chemical_system(molecule):
        return result_molecule

    # PES scan frames are exposed as Molecules, so update a copy to retain the
    # ChemicalSystem metadata that is not represented by a Molecule.
    result = molecule.copy()
    result.coords = result_molecule.as_array()
    if result_molecule.lattice:
        result.lattice.vectors = result_molecule.lattice
    return result


# ToDo: remove type ignore once mypy_path is enabled
@overload
def preoptimize(
    molecule: Sequence[Union[Molecule, "ChemicalSystem"]],
    model: str = "UFF",
    settings: Optional[Settings] = None,
    nproc: int = 1,
    maxiterations: int = 100,
) -> List[Union[Molecule, "ChemicalSystem"]]: ...
@overload
def preoptimize(
    molecule: Molecule,
    model: str = "UFF",
    settings: Optional[Settings] = None,
    nproc: int = 1,
    maxiterations: int = 100,
) -> Molecule: ...
@overload
def preoptimize(
    molecule: "ChemicalSystem",
    model: str = "UFF",
    settings: Optional[Settings] = None,
    nproc: int = 1,
    maxiterations: int = 100,
) -> "ChemicalSystem": ...
[docs]def preoptimize( molecule: Union[Molecule, "ChemicalSystem", Sequence[Union[Molecule, "ChemicalSystem"]]], model: str = "UFF", settings: Optional[Settings] = None, nproc: int = 1, maxiterations: int = 100, ) -> Union[Molecule, "ChemicalSystem", List[Union[Molecule, "ChemicalSystem"]]]: """ Returns an optimized structure, or a list of optimized structures. Each result has the same type as its corresponding input. molecule: Molecule, ChemicalSystem, or sequence of either Structure or structures to optimize model: str Shorthand for some model, e.g. 'UFF' settings: Settings Custom engine settings (overrides ``model``) nproc: int Number of processes maxiterations: int Maximum number of iterations for the geometry optimization. """ my_settings = _get_quick_settings(model, settings, nproc) single_molecule = isinstance(molecule, Molecule) or _is_chemical_system(molecule) input_molecules = [molecule] if single_molecule else molecule output_molecules: List[Union[Molecule, "ChemicalSystem"]] = [] with AMSWorker(my_settings) as worker: for i, mol in enumerate(input_molecules): results = worker.GeometryOptimization( name=f"preoptimization_{i}", molecule=mol, maxiterations=maxiterations, pretendconverged=True ) result_molecule = _get_main_structure(results, mol) if result_molecule is None: raise PlamsError(f"Preoptimization failed for molecule with index {i}.") output_molecules.append(result_molecule) if single_molecule: return output_molecules[0] else: return output_molecules
@overload def shakemd( molecule: Molecule, density: Optional[float] = None, # kg/m^3 nsteps: int = 4000, model: str = "UFF", settings: Optional[Settings] = None, nproc: int = 1, ) -> Molecule: ... @overload def shakemd( molecule: "ChemicalSystem", density: Optional[float] = None, nsteps: int = 4000, model: str = "UFF", settings: Optional[Settings] = None, nproc: int = 1, ) -> "ChemicalSystem": ...
[docs]def shakemd( molecule: Union[Molecule, "ChemicalSystem"], density: Optional[float] = None, # kg/m^3 nsteps: int = 4000, model: str = "UFF", settings: Optional[Settings] = None, nproc: int = 1, ) -> Union[Molecule, "ChemicalSystem"]: """ Performs SHAKE MD simulation with all bonds fixed. If ``density`` is specified, a lattice deformation will also be applied. The temperature is 100 K and the time step 0.5 fs. Returns a structure corresponding to the final MD step, with the same type as ``molecule``. molecule: Molecule or ChemicalSystem The initial structure density: float, optional If specified, the molecule must have a 3D lattice. Target density in kg/m^3 (1000x the density in g/cm^3) nsteps: int Number of MD steps. model: str e.g. 'UFF' settings: Settings Engine settings (overrides ``model``) """ called_plams_init = _ensure_init() s = _get_quick_settings(model, settings, nproc) s.input.ams.Task = "MolecularDynamics" s.input.ams.MolecularDynamics.NSteps = nsteps s.input.ams.MolecularDynamics.Thermostat.Type = "Berendsen" s.input.ams.MolecularDynamics.Thermostat.Tau = 100 s.input.ams.MolecularDynamics.Thermostat.Temperature = 100 s.input.ams.MolecularDynamics.TimeStep = 0.5 s.input.ams.MolecularDynamics.InitialVelocities.Temperature = 100 s.input.ams.MolecularDynamics.Shake.All = "bonds * *" if density is not None: temp_mol = molecule.copy() _set_density(temp_mol, density) l = _get_lattice_vectors(temp_mol) target_lattice_str = f""" {l[0][0]} {l[0][1]} {l[0][2]} {l[1][0]} {l[1][1]} {l[1][2]} {l[2][0]} {l[2][1]} {l[2][2]} """ s.input.ams.MolecularDynamics.Deformation.StartStep = max(nsteps - 3000, 1) s.input.ams.MolecularDynamics.Deformation.StopStep = nsteps s.input.ams.MolecularDynamics.Deformation.TargetLattice._1 = target_lattice_str job = AMSJob(settings=s, molecule=molecule, name="shakemd") job.run() out = _get_main_structure(job.results, molecule) delete_job(job) if called_plams_init: finish() return out
@overload def refine_density( molecule: Molecule, density: float, step_size: int = 50, model: str = "UFF", settings: Optional[Settings] = None, nproc: int = 1, maxiterations: int = 100, ) -> Molecule: ... @overload def refine_density( molecule: "ChemicalSystem", density: float, step_size: int = 50, model: str = "UFF", settings: Optional[Settings] = None, nproc: int = 1, maxiterations: int = 100, ) -> "ChemicalSystem": ...
[docs]def refine_density( molecule: Union[Molecule, "ChemicalSystem"], density: float, step_size: int = 50, model: str = "UFF", settings: Optional[Settings] = None, nproc: int = 1, maxiterations: int = 100, ) -> Union[Molecule, "ChemicalSystem"]: """ Performs a series of geometry optimizations with densities approaching ``density``. This can be useful if you want to compress a system to a given density, but cannot just use apply_strain() (because apply_strain() also scales the bond lengths). This function can be useful if for example packmol does not succeed to pack molecules with the desired density. Packmol can then generate a structure with a lower density, and this function can be used to increase the density to the desired value. Returns a structure with the requested density, with the same type as ``molecule``. molecule: Molecule or ChemicalSystem The structure must have a 3D lattice density: float Target density in kg/m^3 (1000x the density in g/cm^3) step_size: float Step size for the density (in kg/m^3). Set step_size to a large number to only use 1 step. model: str e.g. 'UFF' settings: Settings Engine settings (overrides ``model``) maxiterations: int maximum number of iterations for the geometry optimization. """ assert len(_get_lattice_vectors(molecule)) == 3 tolerance = 1e-3 my_settings = _get_quick_settings(model, settings, nproc) current_density = _get_density(molecule) # kg/m^3 output_molecule = molecule.copy() counter = 0 with AMSWorker(my_settings) as worker: while current_density < density - tolerance or current_density > density + tolerance: counter += 1 if current_density < density - step_size: new_density = current_density + step_size elif current_density > density + step_size: new_density = current_density - step_size else: new_density = density _set_density(output_molecule, new_density) results = worker.GeometryOptimization( name=f"preoptimization_{counter}", molecule=output_molecule, maxiterations=maxiterations, pretendconverged=True, ) result_molecule = _get_main_structure(results, output_molecule) if result_molecule is None: raise PlamsError(f"Density refinement failed for density '{new_density}'.") output_molecule = result_molecule current_density = _get_density(output_molecule) # kg/m^3 return output_molecule
@overload def refine_lattice( molecule: Molecule, lattice: List[List[float]], n_points: Optional[int] = None, max_strain: float = 0.15, model: str = "UFF", settings: Optional[Settings] = None, nproc: int = 1, maxiterations: int = 10, ) -> Optional[Molecule]: ... @overload def refine_lattice( molecule: "ChemicalSystem", lattice: List[List[float]], n_points: Optional[int] = None, max_strain: float = 0.15, model: str = "UFF", settings: Optional[Settings] = None, nproc: int = 1, maxiterations: int = 10, ) -> Optional["ChemicalSystem"]: ...
[docs]def refine_lattice( molecule: Union[Molecule, "ChemicalSystem"], lattice: List[List[float]], n_points: Optional[int] = None, max_strain: float = 0.15, model: str = "UFF", settings: Optional[Settings] = None, nproc: int = 1, maxiterations: int = 10, ) -> Optional[Union[Molecule, "ChemicalSystem"]]: """ Returns a structure for which the lattice of the ``molecule`` is transformed to ``lattice``, by performing short geometry optimizations (each for at most ``maxiterations``) on gradually distorted lattices (linearly interpolating from the original lattice to the new lattice using ``n_points`` points). This can be useful for transforming an orthorhombic box of a liquid into a non-orthorhombic box of a liquid, where the gradual transformation of the lattice ensures that the molecules do not become too distorted. If init() has been called before calling this function, the job will be run in the current PLAMS working directory and will be deleted when the job finishes. The returned structure has the same type as ``molecule``. If the refinement fails, ``None`` is returned. molecule: Molecule or ChemicalSystem The initial structure lattice: list of list of float List with 1, 2, or 3 elements. Each element is a list of float with 3 elements each. For example, ``lattice=[[10, 0, 0],[-5, 5, 0],[0, 0, 12]]``. n_points: None or int >=2 Number of points used for the linear interpolation. If None, n_points will be chosen such that the maximum strain for any step is at most ``max_strain`` compared to the original lattice vector lengths. max_strain: float Only if ``n_points=None``, use this value to determine the maximum allowed strain from one step to the next (as a fraction of the length of the original lattice vectors). model: str e.g. 'UFF' settings: Settings Engine settings (overrides ``model``) nproc: int Number of processes used by the job maxiterations: int maximum number of iterations for the geometry optimizations. """ molecule_lattice = _get_lattice_vectors(molecule) assert len(lattice) == len( molecule_lattice ), f"Different number of lattice vectors: len(molecule.lattice) = {len(molecule_lattice)}, len(lattice) = {len(lattice)}" assert all(len(x) == 3 for x in lattice), f"Lattice vectors must have three components. Lattice: {lattice}" assert all(len(x) == 3 for x in molecule_lattice), f"Lattice vectors must have three components. Lattice: {lattice}" assert ( len(lattice) >= 1 and len(lattice) <= 3 ), f"{len(lattice)} lattice vectors given but must be between 1 and 3. Lattice: {lattice}" def lattice2str(latt: List[List[float]]) -> str: return "\n".join(" ".join(str(j) for j in i) for i in latt) if n_points is None: lat1 = np.array(molecule_lattice) lat2 = np.array(lattice) strain = np.linalg.norm(lat2, axis=1) / np.linalg.norm(lat1, axis=1) n_points = np.ceil(np.max(np.abs(strain - 1) / max_strain)) n_points = int(max(n_points, 2)) assert n_points >= 2, f"Expected n_points >=2, but received {n_points}" called_plams_init = _ensure_init() s = _get_quick_settings(model, settings, nproc) s.input.ams.task = "PESScan" s.input.ams.PESScan.ScanCoordinate.FromLattice._1 = lattice2str(molecule_lattice) s.input.ams.PESScan.ScanCoordinate.ToLattice._1 = lattice2str(lattice) s.input.ams.PESScan.ScanCoordinate.NPoints = n_points s.input.ams.GeometryOptimization.MaxIterations = maxiterations s.input.ams.GeometryOptimization.PretendConverged = "Yes" job = AMSJob(settings=s, molecule=molecule, name="refine_density") job.run() final_molecule = None if job.ok(): results = job.results.get_pesscan_results(molecules=True) final_molecule = _with_updated_geometry(molecule, results["Molecules"][-1]) delete_job(job) if called_plams_init: finish() return final_molecule
def _ensure_init() -> bool: if get_config().init: called_plams_init = False else: s = Settings() s.erase_workdir = True s.log.stdout = 0 init(config_settings=s) called_plams_init = True return called_plams_init def model_to_settings(model: str) -> Settings: """ Returns Settings """ settings = Settings() model = model.lower() if model == "uff": settings.input.ForceField.Type = "UFF" elif model == "gaff": settings.input.ForceField.Type = "GAFF" settings.input.ForceField.AnteChamberIntegration = "Yes" elif model == "gfnff": settings.input.GFNFF elif model == "ani-2x" or model == "ani2x": settings.input.MLPotential.Model = "ANI-2x" elif model == "gfn1xtb" or model == "gfn1-xtb": settings.input.DFTB.Model = "GFN1-xTB" elif model == "m3gnet-up-2022" or model == "m3gnetup2022": settings.input.MLPotential.Model = "M3GNet-UP-2022" else: raise ValueError("Unknown model: {}".format(model)) return settings def _get_quick_settings(model: str, settings: Optional[Settings], nproc: int) -> Settings: if settings is None: my_settings = model_to_settings(model) else: my_settings = settings.copy() if nproc: my_settings.runscript.nproc = nproc return my_settings