Source code for scm.plams.interfaces.molecule.ase

import numpy as np

from scm.plams.core.functions import add_to_class
from scm.plams.mol.molecule import Atom, Molecule, MoleculeError

__all__ = ["toASE", "fromASE"]
ase_present = False

try:
    import ase

    ase_present = True
except ImportError:
    __all__ = []


@add_to_class(Molecule)
def readase(self, f, **other):
    """Read Molecule using ASE engine

    The ``read`` function of the |Molecule| class passes a file descriptor into here, so in this case you must specify the *format* to be read by ASE::

        mol = Molecule('file.cif', inputformat='ase', format='cif')

    The ASE Atoms object then gets converted to a PLAMS Molecule and returned.
    All *other* options are passed to ``ASE.io.read()``.
    See https://wiki.fysik.dtu.dk/ase/ase/io/io.html on how to use it.

    .. note::

        The nomenclature of PLAMS and ASE is incompatible for reading multiple geometries, make sure that you only read single geometries with ASE! Reading multiple geometries is not supported, each geometry needs to be read individually.

    """
    try:
        from ase import io as aseIO
    except ImportError:
        raise MoleculeError("Asked for ASE IO engine but could not load ASE.io module")

    aseMol = aseIO.read(f, **other)
    mol = fromASE(aseMol)
    # update self with the molecule read without overwriting e.g. settings
    self += mol
    # lattice does not survive soft update
    self.lattice = mol.lattice
    return


@add_to_class(Molecule)
def writease(self, f, **other):
    """Write molecular coordinates using ASE engine.

    The ``write`` function of the |Molecule| class passes a file descriptor into here, so in this case you must specify the *format* to be written by ASE.
    All *other* options are passed to ``ASE.io.write()``.
    See https://wiki.fysik.dtu.dk/ase/ase/io/io.html on how to use it.

    These two write the same content to the respective files::

        molecule.write('filename.anyextension', outputformat='ase', format='gen')
        molecule.writease('filename.anyextension', format='gen')

    """
    aseMol = toASE(self)
    aseMol.write(f, **other)
    return


if ase_present:
    Molecule._readformat["ase"] = Molecule.readase
    Molecule._writeformat["ase"] = Molecule.writease


[docs]def toASE(molecule, set_atomic_charges=False): """Convert a PLAMS |Molecule| to an ASE molecule (``ase.Atoms`` instance). Translate coordinates, atomic numbers, and lattice vectors (if present). The order of atoms is preserved. set_atomic_charges: bool If True, set_initial_charges() will be called with the average atomic charge (taken from molecule.properties.charge). The purpose is to preserve the total charge, not to set any reasonable initial charges. """ # iterate over PLAMS atoms for atom in molecule: # check if coords only consists of floats or ints if not all(isinstance(x, (int, float)) for x in atom.coords): raise ValueError("Non-Number in Atomic Coordinates, not compatible with ASE") aseMol = ase.Atoms(numbers=molecule.numbers, positions=molecule.as_array()) # get lattice info if any lattice = np.zeros((3, 3)) pbc = [False, False, False] for i, vec in enumerate(molecule.lattice): # check if lattice only consists of floats or ints if not all(isinstance(x, (int, float)) for x in vec): raise ValueError("Non-Number in Lattice Vectors, not compatible with ASE") pbc[i] = True lattice[i] = np.array(vec) # save lattice info to aseMol if any(pbc): aseMol.set_pbc(pbc) aseMol.set_cell(lattice) if set_atomic_charges: charge = molecule.properties.get("charge", 0) if not charge: atomic_charges = [0.0] * len(molecule) else: atomic_charges = [float(charge)] + [0.0] * (len(molecule) - 1) aseMol.set_initial_charges(atomic_charges) return aseMol
[docs]def fromASE(molecule, properties=None, set_charge=False): """Convert an ASE molecule to a PLAMS |Molecule|. Translate coordinates, atomic numbers, and lattice vectors (if present). The order of atoms is preserved. Pass a |Settings| instance through the ``properties`` option to inherit them to the returned molecule. """ plamsMol = Molecule() # iterate over ASE atoms for atom in molecule: # add atom to plamsMol plamsMol.add_atom(Atom(atnum=atom.number, coords=tuple(atom.position))) # add Lattice if any if any(molecule.get_pbc()): lattice = [] # loop over three booleans for i, boolean in enumerate(molecule.get_pbc().tolist()): if boolean: lattice.append(tuple(molecule.get_cell()[i])) # write lattice to plamsMol plamsMol.lattice = lattice.copy() if properties: plamsMol.properties.update(properties) if (properties and "charge" not in properties or not properties) and set_charge: plamsMol.properties.charge = sum(molecule.get_initial_charges()) if "charge" in molecule.info: plamsMol.properties.charge += molecule.info["charge"] return plamsMol