Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adding ASE compatibility to Crystal class #534

Closed
wants to merge 16 commits into from
14 changes: 14 additions & 0 deletions py4DSTEM/io/google_drive_downloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,12 @@
"downsample_Si_SiGe_analysis_braggdisks_cal.h5",
"1bYgDdAlnWHyFmY-SwN3KVpMutWBI5MhP",
),
"test_Si_cif": ("Si.cif", "1b5ZonO0Upcp6eoiE-UEzGe1ymSj4QBaO"),
"test_Si_cif_sym": ("Si-sym.cif", "1Q26anzuq9FRgjNPGHUyrQ3d1pmP1B--T"),
"test_Si_prismatic": ("Si.prismatic", "1h_tnmygDGin3rMMBYtV2-VCJbO9_4Q_F"),
"test_NaCl_cif": ("NaCl.cif", "1nfUTyHTwYM6S5mCxIgwzrtN9gS0ReOYb"),
"test_NaCl_prismatic": ("NaCl.prismatic", "1_fWx6C3PkT47fBJufYjM3DN7eeMIQqYj"),
"test_DRX_cif": ("DRX.cif", "1hiGqFW6BJMFl718h63jQybpWfY-lNmsr"),
}

# collections of files
Expand Down Expand Up @@ -163,6 +169,14 @@
),
"test_braggvectors": ("Au_sim",),
"strain": ("test_strain",),
"test_atomic_coords": (
"test_Si_cif",
"test_Si_cif_sym",
"test_Si_prismatic",
"test_NaCl_cif",
"test_NaCl_prismatic",
"test_DRX_cif",
),
}


Expand Down
185 changes: 169 additions & 16 deletions py4DSTEM/process/diffraction/crystal.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,19 @@
from py4DSTEM.process.diffraction.crystal_viz import plot_ring_pattern
from py4DSTEM.process.diffraction.utils import Orientation, calc_1D_profile

from importlib.util import find_spec

try:
from pymatgen.symmetry.analyzer import SpacegroupAnalyzer
from pymatgen.core.structure import Structure
except ImportError:
pass

try:
from ase.io import read
except ImportError:
pass
Comment on lines +25 to +28
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For an optional dependency that is not needed at the top level of the file, I think a better pattern is to move the import within the function that requires it. That way any import errors get raised at the appropriate moment.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to delete as its now a dependency



class Crystal:
"""
Expand Down Expand Up @@ -147,6 +154,9 @@ def __init__(
# Calculate lattice parameters
self.calculate_lattice()

# Set occupancy attribute to None
self.occupancy = None

def calculate_lattice(self):
if not hasattr(self, "lat_real"):
# calculate unit cell lattice vectors
Expand Down Expand Up @@ -265,27 +275,27 @@ def get_strained_crystal(
else:
return crystal_strained

def from_CIF(CIF, conventional_standard_structure=True):
"""
Create a Crystal object from a CIF file, using pymatgen to import the CIF
# def from_CIF(CIF, conventional_standard_structure=True):
# """
# Create a Crystal object from a CIF file, using pymatgen to import the CIF

Note that pymatgen typically prefers to return primitive unit cells,
which can be overridden by setting conventional_standard_structure=True.
# Note that pymatgen typically prefers to return primitive unit cells,
# which can be overridden by setting conventional_standard_structure=True.

Args:
CIF: (str or Path) path to the CIF File
conventional_standard_structure: (bool) if True, conventional standard unit cell will be returned
instead of the primitive unit cell pymatgen typically returns
"""
from pymatgen.io.cif import CifParser
# Args:
# CIF: (str or Path) path to the CIF File
# conventional_standard_structure: (bool) if True, conventional standard unit cell will be returned
# instead of the primitive unit cell pymatgen typically returns
# """
# from pymatgen.io.cif import CifParser

parser = CifParser(CIF)
# parser = CifParser(CIF)

structure = parser.get_structures()[0]
# structure = parser.get_structures()[0]

return Crystal.from_pymatgen_structure(
structure, conventional_standard_structure=conventional_standard_structure
)
# return Crystal.from_pymatgen_structure(
# structure, conventional_standard_structure=conventional_standard_structure
# )

def from_pymatgen_structure(
structure=None,
Expand Down Expand Up @@ -400,6 +410,149 @@ def from_pymatgen_structure(

return Crystal(positions, numbers, cell)

def from_ase(
atoms,
):
"""
Create a py4DSTEM Crystal object from an ASE atoms object

Args:
atoms (ase.Atoms): an ASE atoms object

"""

xtal = Crystal(
positions=atoms.get_scaled_positions(), # fractional coords
numbers=atoms.numbers,
cell=atoms.cell.array,
)

# get occupancies
# ASE seems to have different ways of storing occupancies
# if ASE object created from prismatic file
if "occupancies" in atoms.arrays.keys():
# np.array with length number of atoms
xtal.occupancy = atoms.arrays["occupancies"]
# if created from cif file
elif "occupancy" in atoms.info.keys():
# python dict with occupancy per site
xtal.occupancy = atoms.info["occupancy"]
# TODO add in elif statement if other ways appear
else:
print(Warning("Could not find occupancies of crystal"))
return xtal

def from_prismatic(filepath):
"""
Create a py4DSTEM Crystal object from an prismatic style xyz co-ordinate file

Args:
filepath (str|Pathlib.Path): path to the prismatic format xyz file

"""

# check if ase is installed
if find_spec("ase") is None:
raise ImportWarning("Could not import ASE, please install, and try again")
else:
from ase.io import read

atoms = read(filepath, format="prismatic")

# create the crystal object
xtal = Crystal(
positions=atoms.get_scaled_positions(), # fractional coords
numbers=atoms.numbers,
cell=atoms.cell.array,
)
# add occupancies
# It should be this one but keeping other method in case
if "occupancies" in atoms.arrays.keys():
# np.array with length number of atoms
xtal.occupancy = atoms.arrays["occupancies"]
# if created from cif file
elif "occupancy" in atoms.info.keys():
# python dict with occupancy per site
xtal.occupancy = atoms.info["occupancy"]
# TODO add in elif statement if other ways appear
else:
print(Warning("Could not find occupancies of crystal"))
return xtal

def from_cif(
filepath,
):
"""
Create a py4DSTEM Crystal object from a cif file using ase.io.read function

Args:
filepath (str|Pathlib.Path): path to the file
"""

# check if ase is installed
if find_spec("ase") is None:
raise ImportWarning(
"Could not import ASE, please install, restart and try again"
)
else:
from ase.io import read
Comment on lines +493 to +498
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is all this extra logic necessary? If ase is missing it will raise a ModuleNotFoundError

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to delete as its now a dependency

# try loading the file using ase read and get required properties
atoms = read(filepath, format="cif")
xtal = Crystal(
positions=atoms.get_scaled_positions(), # fractional coords
numbers=atoms.numbers,
cell=atoms.cell.array,
)

# add occupancies
# It should be this one but keeping other method in case
if "occupancies" in atoms.arrays.keys():
# np.array with length number of atoms
xtal.occupancy = atoms.arrays["occupancies"]
# if created from cif file
elif "occupancy" in atoms.info.keys():
# python dict with occupancy per site
xtal.occupancy = atoms.info["occupancy"]
# TODO add in elif statement if other ways appear
# TODO when nested python dict, it needs to be unpacked. into numpy array like from_prismatic
else:
print(Warning("Could not find occupancies of crystal"))
return xtal

# def from_generic_file(filepath, **kwargs):
# """
# Create a py4DSTEM Crystal from a wide range of generic file types using
# `ase.io.read`, kwargs are passed to `ase.io.read` function. For more details
# and potentially compatible filetypes please see https://wiki.fysik.dtu.dk/ase/ase/io/io.html.
# Note this has not been tested extensively. The loaded file must have these three properties:
# .get_scaled_positions()
# .numbers
# .cell.array,

# Args:
# filepath (str|Pathlib.Path): path to the file
# kwargs: key word arguments to be passed to `ase.io.read`

# """

# # check if ase is installed
# if find_spec("ase") is None:
# raise ImportWarning(
# "Could not import ASE, please install, restart and try again"
# )
# else:
# from ase.io import read
# # try loading the file using ase read and get required properties
# try:
# atoms = read(filepath, **kwargs)
# return Crystal(
# positions=atoms.get_scaled_positions(), # fractional coords
# numbers=atoms.numbers,
# cell=atoms.cell.array,
# )
# except Exception as e:
# raise e

def from_unitcell_parameters(
latt_params,
elements,
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"emdfile >= 0.0.13",
"mpire >= 2.7.1",
"threadpoolctl >= 3.1.0",
"ase >= 3.22.0",
],
extras_require={
"ipyparallel": ["ipyparallel >= 6.2.4", "dill >= 0.3.3"],
Expand Down
Loading