Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions news/ase-adapter.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
**Added:**

* Added ``Structure.get_lattice_vectors()`` method to return the lattice vectors.
* Added ``Structure.get_lattice_vector_angles()`` method to return the angles between the lattice vectors.
* Added ``Structure.get_isotropic_displacement_parameters()`` method to return the isotropic displacement parameters.
* Added ``Structure.get_anisotropic_displacement_parameters()`` method to return the anisotropic displacement parameters.
* Added ``Structure.get_occupancies()`` method to return the occupancies of the sites.
* Added ``Structure.get_cartesian_coordinates()`` method to return the Cartesian coordinates of the sites.
* Added ``Structure.get_fractional_coordinates()`` method to return the fractional coordinates of the sites.
* Added ``Structure.get_chemical_symbols()`` method to return the chemical symbols of the sites.
* Added ``Structure.convert_ase_to_diffpy_structure()`` method to convert an ASE Atoms object to a DiffPy Structure object.

**Changed:**

* <news item>

**Deprecated:**

* <news item>

**Removed:**

* <news item>

**Fixed:**

* <news item>

**Security:**

* <news item>
1 change: 1 addition & 0 deletions requirements/conda.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
numpy
pycifrw
diffpy.utils
ase
1 change: 1 addition & 0 deletions requirements/pip.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
numpy
pycifrw
diffpy.utils
ase
225 changes: 225 additions & 0 deletions src/diffpy/structure/structure.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import copy as copymod

import numpy
from ase import Atoms as ASEAtoms

from diffpy.structure.atom import Atom
from diffpy.structure.lattice import Lattice
Expand Down Expand Up @@ -173,6 +174,230 @@ def getLastAtom(self):
last_atom = self[-1]
return last_atom

def get_chemical_symbols(self):
"""Return list of chemical symbols for all `Atoms` in this
structure.

Returns
-------
list of str
The list of chemical symbols for all `Atoms` in this structure.
"""
symbols_with_charge = [a.element for a in self]
symbols = [atomBareSymbol(sym) for sym in symbols_with_charge]
return symbols

def get_fractional_coordinates(self):
"""Return array of fractional coordinates of all `Atoms` in this
structure.

Returns
-------
numpy.ndarray
The array of fractional coordinates of all `Atoms` in this structure
in the same order as `Structure.get_chemical_symbols()`.
"""
coords = numpy.array([a.xyz for a in self])
return coords

def get_cartesian_coordinates(self):
"""Return array of Cartesian coordinates of all `Atoms` in this
structure.

Returns
-------
numpy.ndarray
The array of Cartesian coordinates of all `Atoms` in this structure
in the same order as `Structure.get_chemical_symbols()`.
"""
cartn_coords = numpy.array([a.xyz_cartn for a in self])
return cartn_coords

def get_anisotropic_displacement_parameters(self, return_array=False):
"""Return the anisotropic displacement parameters for all atoms.

Parameters
----------
return_array : bool, optional
If True, return anisotropic displacement parameters as a numpy array instead of a dictionary.

Returns
-------
dict
The dictionary of anisotropic displacement parameters for all atoms in this structure.
Keys are of the form 'Element_i_Ujk', e.g. 'C_0_11', 'C_0_12'.
"""
if return_array:
aniso_adps = numpy.array([a.U for a in self])
return aniso_adps
else:
adp_dict = {}
for i, atom in enumerate(self):
element = atomBareSymbol(atom.element)
adp_dict[f"{element}_{i}_11"] = self.U11[i]
adp_dict[f"{element}_{i}_22"] = self.U22[i]
adp_dict[f"{element}_{i}_33"] = self.U33[i]
adp_dict[f"{element}_{i}_12"] = self.U12[i]
adp_dict[f"{element}_{i}_13"] = self.U13[i]
adp_dict[f"{element}_{i}_23"] = self.U23[i]
return adp_dict

def get_isotropic_displacement_parameters(self, return_array=False):
"""Return a the isotropic displacement parameters for all atoms.

Parameters
----------
return_array : bool, optional
If True, return isotropic displacement parameters as a numpy array instead of a dictionary.
Default is False.

Returns
-------
dict
The dictionary of isotropic displacement parameters for all atoms in this structure.
Keys are of the form 'Element_i_Uiso', e.g. 'C_0_Uiso'.
"""
if return_array:
iso_adps = numpy.array([a.Uisoequiv for a in self])
return iso_adps
else:
iso_dict = {}
for i, atom in enumerate(self):
element = atomBareSymbol(atom.element)
iso_dict[f"{element}_{i+1}_Uiso"] = self.Uisoequiv[i]
return iso_dict

def get_occupancies(self):
"""Return array of occupancies of all `Atoms` in this structure.

Returns
-------
numpy.ndarray
The array of occupancies of all `Atoms` in this structure.
"""
occupancies = numpy.array([a.occupancy for a in self])
return occupancies

def get_lattice_vectors(self):
"""Return array of lattice vectors for this structure.

Returns
-------
numpy.ndarray
The array of lattice vectors for this structure.
"""
lattice_vectors = self.lattice.base
return lattice_vectors

def get_lattice_vector_angles(self):
"""Return array of lattice vector angles for this structure.

Returns
-------
numpy.ndarray
The array of lattice vector angles for this structure.
"""
a, b, c = self.lattice.base
alpha = self.lattice.angle(b, c)
beta = self.lattice.angle(a, c)
gamma = self.lattice.angle(a, b)
return numpy.array([alpha, beta, gamma])

def convert_ase_to_diffpy_structure(
self,
ase_atoms: ASEAtoms,
lost_info: list[str] | None = None,
) -> Structure | tuple[Structure, dict]: # noqa
"""Convert ASE `Atoms` object to this `Structure` instance.

The conversion process involves extracting the lattice parameters, chemical symbols,
and fractional coordinates from the ASE `Atoms` object and populating the corresponding
attributes of this `Structure` instance. The `lattice` attribute is set based on the
cell parameters of the ASE `Atoms` object, and each `Atom` in this `Structure` is created
with the chemical symbol and fractional coordinates extracted from the ASE `Atoms`.

Parameters
----------
ase_structure : ase.Atoms
The ASE `Atoms` object to be converted.
lost_info : str or list of str, optional
The method(s) or attribute(s) to extract from the ASE `Atoms`
object that do not have a direct equivalent in the `Structure` class.
This will be provided in a dictionary format where keys are the
method/attribute name(s) and value(s) are the corresponding data
extracted from the ASE `Atoms` object.
Default is None. See `Examples` for usage.

Returns
-------
lost_info : dict, optional
If specified, the dictionary containing any information from the ASE `Atoms`
object that is not currently available in the `Structure` class.
Default behavior is to return `None`.
If `lost_info` is provided, a dictionary containing
any information from the ASE `Atoms` will be returned.
This may include information such as magnetic moments, charge states,
or other ASE-specific properties that do not have a direct equivalent
in the `Structure` class.

Raises
------
TypeError
If the input `ase_structure` is not an instance of `ase.Atoms`.
ValueError
If any of the specified `lost_info` attributes are not present in the ASE `Atoms` object.

Examples
--------
An example of converting an `ASE.Atoms` instance to a `Structure` instance,

.. code-block:: python
from ase import Atoms
from diffpy.structure import Structure

# Create an ASE Atoms object
ase_atoms = Atoms('H2O', positions=[[0, 0, 0], [0, 0, 1], [1, 0, 0]])

# Convert to a diffpy Structure object
structure = Structure()
structure.convert_ase_to_diffpy(ase_atoms)


To extract additional information from the ASE `Atoms` object that is not
directly represented in the `Structure` class, such as magnetic moments,
you can specify an attribute or method of `ASE.Atoms` as
a string or list of strings in `lost_info` list. For example,

.. code-block:: python
lost_info = structure.convert_ase_to_diffpy(
ase_atoms,
lost_info='get_magnetic_moments'
)

will return a dictionary with the magnetic moments of the atoms in the ASE `Atoms` object.
"""
# clear structure before populating it with new atoms
del self[:]
if not isinstance(ase_atoms, ASEAtoms):
raise TypeError(f"Input must be an instance of ase.Atoms but got type {type(ase_atoms)}.")
cell = ase_atoms.get_cell()
self.lattice = Lattice(base=numpy.array(cell))
symbols = ase_atoms.get_chemical_symbols()
scaled_positions = ase_atoms.get_scaled_positions()
for atom_symbol, frac_coord in zip(symbols, scaled_positions):
self.append(Atom(atom_symbol, xyz=frac_coord))
if lost_info is None:
return
extracted_info = {}
if isinstance(lost_info, str):
lost_info = [lost_info]
for name in lost_info:
if not hasattr(ase_atoms, name):
raise ValueError(f"ASE.Atoms object has no attribute '{name}'.")
attr = getattr(ase_atoms, name)
extracted_info[name] = attr() if callable(attr) else attr
return extracted_info

def assign_unique_labels(self):
"""Set a unique label string for each `Atom` in this structure.

Expand Down
46 changes: 46 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import json
from pathlib import Path

import numpy as np
import pytest
from ase import Atoms

from diffpy.structure import Atom, Lattice, Structure


@pytest.fixture
Expand All @@ -27,3 +31,45 @@ def _load(filename):
return "tests/testdata/" + filename

return _load


@pytest.fixture
def build_ase_atom_object():
"""Helper function to build an ASE.Atoms object for testing."""
a = 5.409
frac_coords = np.array(
[
[0.0, 0.0, 0.0],
[0.5, 0.5, 0.5],
[0.25, 0.25, 0.25],
[0.75, 0.75, 0.75],
]
)
cart_coords = frac_coords * a
symbols = ["Zn", "Zn", "S", "S"]
ase_zb = Atoms(symbols=symbols, positions=cart_coords, cell=[[a, 0, 0], [0, a, 0], [0, 0, a]], pbc=True)
return ase_zb


@pytest.fixture
def build_diffpy_structure_object():
"""Helper function to build a diffpy.structure.Structure object for
testing."""
a = 5.409
frac_coords = np.array(
[
[0.0, 0.0, 0.0],
[0.5, 0.5, 0.5],
[0.25, 0.25, 0.25],
[0.75, 0.75, 0.75],
]
)
lattice = Lattice(base=[[a, 0, 0], [0, a, 0], [0, 0, a]])
atoms = [
Atom("Zn", frac_coords[0]),
Atom("Zn", frac_coords[1]),
Atom("S", frac_coords[2]),
Atom("S", frac_coords[3]),
]
diffpy_zb = Structure(atoms=atoms, lattice=lattice)
return diffpy_zb
Loading
Loading