Skip to content

Commit bd4a6c0

Browse files
committed
add get_lattice_vectors, get_lattice_vector_angles and their respective tests
1 parent 1f51263 commit bd4a6c0

3 files changed

Lines changed: 184 additions & 19 deletions

File tree

src/diffpy/structure/structure.py

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,31 @@ def get_occupancies(self):
250250
occupancies = numpy.array([a.occupancy for a in self])
251251
return occupancies
252252

253+
def get_lattice_vectors(self):
254+
"""Return array of lattice vectors for this structure.
255+
256+
Returns
257+
-------
258+
numpy.ndarray
259+
The array of lattice vectors for this structure.
260+
"""
261+
lattice_vectors = self.lattice.base
262+
return lattice_vectors
263+
264+
def get_lattice_vector_angles(self):
265+
"""Return array of lattice vector angles for this structure.
266+
267+
Returns
268+
-------
269+
numpy.ndarray
270+
The array of lattice vector angles for this structure.
271+
"""
272+
a, b, c = self.lattice.base
273+
alpha = self.lattice.angle(b, c)
274+
beta = self.lattice.angle(a, c)
275+
gamma = self.lattice.angle(a, b)
276+
return numpy.array([alpha, beta, gamma])
277+
253278
def convert_ase_to_diffpy_structure(
254279
self,
255280
ase_atoms: ASEAtoms,
@@ -269,8 +294,6 @@ def convert_ase_to_diffpy_structure(
269294
270295
Returns
271296
-------
272-
Structure
273-
Reference to this `Structure` object with updated attributes and `Atom` instances.
274297
lost_info : dict, optional
275298
The dictionary containing any information from the ASE `Atoms`
276299
object that is not currently available in the `Structure` class.
@@ -317,31 +340,24 @@ def convert_ase_to_diffpy_structure(
317340
318341
will return a dictionary with the magnetic moments of the atoms in the ASE `Atoms` object.
319342
"""
343+
# clear structure before populating it with new atoms
344+
del self[:]
320345
if not isinstance(ase_atoms, ASEAtoms):
321-
raise TypeError("Input must be an instance of ase.Atoms.")
322-
# --- structure conversion ---
346+
raise TypeError(f"Input must be an instance of ase.Atoms but got type {ase_atoms}.")
347+
cell = ase_atoms.get_cell()
348+
self.lattice = Lattice(base=numpy.array(cell))
323349
symbols = ase_atoms.get_chemical_symbols()
324350
scaled_positions = ase_atoms.get_scaled_positions()
325351
for sym, xyz in zip(symbols, scaled_positions):
326352
self.append(Atom(sym, xyz=xyz))
327-
# --- optional extraction ---
328353
if lost_info is None:
329354
return
330355
extracted_info = {}
331356
for name in lost_info:
332357
if not hasattr(ase_atoms, name):
333358
raise ValueError(f"ASE.Atoms object has no attribute '{name}'.")
334-
try:
335-
attr = getattr(ase_atoms, name)
336-
value = attr() if callable(attr) else attr
337-
# try to copy (safe for numpy arrays, dicts, etc.)
338-
try:
339-
value = value.copy()
340-
except Exception:
341-
pass
342-
extracted_info[name] = value
343-
except Exception as e:
344-
extracted_info[name] = f"ERROR: {type(e).__name__}: {e}"
359+
attr = getattr(ase_atoms, name)
360+
extracted_info[name] = attr() if callable(attr) else attr
345361
return extracted_info
346362

347363
def assign_unique_labels(self):

tests/conftest.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import json
22
from pathlib import Path
33

4+
import numpy as np
45
import pytest
6+
from ase import Atoms
7+
8+
from diffpy.structure import Atom, Lattice, Structure
59

610

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

2933
return _load
34+
35+
36+
@pytest.fixture
37+
def build_ase_atom_object():
38+
"""Helper function to build an ASE.Atoms object for testing."""
39+
a = 5.409
40+
frac_coords = np.array(
41+
[
42+
[0.0, 0.0, 0.0],
43+
[0.5, 0.5, 0.5],
44+
[0.25, 0.25, 0.25],
45+
[0.75, 0.75, 0.75],
46+
]
47+
)
48+
cart_coords = frac_coords * a
49+
symbols = ["Zn", "Zn", "S", "S"]
50+
ase_zb = Atoms(symbols=symbols, positions=cart_coords, cell=[[a, 0, 0], [0, a, 0], [0, 0, a]], pbc=True)
51+
return ase_zb
52+
53+
54+
@pytest.fixture
55+
def build_diffpy_structure_object():
56+
"""Helper function to build a diffpy.structure.Structure object for
57+
testing."""
58+
a = 5.409
59+
frac_coords = np.array(
60+
[
61+
[0.0, 0.0, 0.0],
62+
[0.5, 0.5, 0.5],
63+
[0.25, 0.25, 0.25],
64+
[0.75, 0.75, 0.75],
65+
]
66+
)
67+
lattice = Lattice(base=[[a, 0, 0], [0, a, 0], [0, 0, a]])
68+
atoms = [
69+
Atom("Zn", frac_coords[0]),
70+
Atom("Zn", frac_coords[1]),
71+
Atom("S", frac_coords[2]),
72+
Atom("S", frac_coords[3]),
73+
]
74+
diffpy_zb = Structure(atoms=atoms, lattice=lattice)
75+
return diffpy_zb

tests/test_structure.py

Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -686,9 +686,112 @@ def test_get_occupancies(datafile):
686686
assert numpy.allclose(actual_occupancies, expected_occupancies)
687687

688688

689-
# def test_convert_ase_to_diffpy_structure(datafile):
690-
# """Check convert_ase_to_diffpy_structure()"""
691-
# assert False
689+
def test_get_lattice_vectors(datafile):
690+
"""Check Structure.get_lattice_vectors()"""
691+
pbte_stru = Structure(filename=datafile("PbTe.cif"))
692+
actual_lattice_vectors = pbte_stru.get_lattice_vectors()
693+
expected_lattice_vectors = numpy.array([[6.461, 0.0, 0.0], [0.0, 6.461, 0.0], [0.0, 0.0, 6.461]])
694+
assert numpy.allclose(actual_lattice_vectors, expected_lattice_vectors)
695+
696+
697+
def test_get_lattice_vector_angles(datafile):
698+
"""Check Structure.get_lattice_vector_angles()"""
699+
pbte_stru = Structure(filename=datafile("PbTe.cif"))
700+
actual_lattice_vector_angles = pbte_stru.get_lattice_vector_angles()
701+
expected_lattice_vector_angles = numpy.array([90.0, 90.0, 90.0])
702+
assert numpy.allclose(actual_lattice_vector_angles, expected_lattice_vector_angles)
703+
704+
705+
@pytest.mark.parametrize(
706+
"input",
707+
[
708+
# case: user calls the conversion function on a Structure object that already contains
709+
# a structure
710+
# expected: the structure is wiped clean and replaced with the converted structure
711+
# we use the fixture to create a Structure object that already contains a structure.
712+
"use_diffpy_structure_fixture",
713+
# case: user calls the conversion function on an empty Structure object
714+
# expected: the converted structure is added to the empty Structure object without issue
715+
Structure(),
716+
],
717+
)
718+
def test_convert_ase_to_diffpy_structure(input, build_ase_atom_object, build_diffpy_structure_object):
719+
"""Check convert_ase_to_diffpy_structure()"""
720+
# input: User wants to convert an ASE.Atoms object to a diffpy.structure.Structure object
721+
# expected: All similar data is transferred correctly,
722+
# including chemical symbols, fractional coordinates, and lattice parameters.
723+
724+
# Create an ASE.Atoms object
725+
ase_zb = build_ase_atom_object
726+
# Create an identical expected diffpy Structure object
727+
expected_structure = build_diffpy_structure_object
728+
729+
# Create new Structure object and convert ase to diffpy structure.
730+
# Use the string input to determine which type of Structure object to create for the test
731+
if isinstance(input, str):
732+
actual_structure = build_diffpy_structure_object
733+
else:
734+
actual_structure = input
735+
# set the lost_info variable, which gets the attribute of method from ASE.Atoms object, gets the values
736+
# and stores it in a dict. This is used because ASE.Atoms stores more/different
737+
# info that a diffpy.structure object
738+
lost_info_dict = actual_structure.convert_ase_to_diffpy_structure(ase_zb, lost_info=["get_masses"])
739+
actual_masses = lost_info_dict["get_masses"]
740+
expected_masses = ase_zb.get_masses()
741+
assert numpy.allclose(actual_masses, expected_masses)
742+
743+
# Compare the actual and expected values
744+
expected_lattice_vectors = expected_structure.get_lattice_vectors()
745+
actual_lattice_vectors = actual_structure.get_lattice_vectors()
746+
assert numpy.allclose(expected_lattice_vectors, actual_lattice_vectors)
747+
748+
expected_lattice_angle = expected_structure.get_lattice_vector_angles()
749+
actual_lattice_angle = actual_structure.get_lattice_vector_angles()
750+
assert numpy.allclose(expected_lattice_angle, actual_lattice_angle)
751+
752+
expected_symbols = expected_structure.get_chemical_symbols()
753+
actual_symbols = actual_structure.get_chemical_symbols()
754+
assert actual_symbols == expected_symbols
755+
756+
expected_coords = expected_structure.get_fractional_coordinates()
757+
actual_coords = actual_structure.get_fractional_coordinates()
758+
assert numpy.allclose(actual_coords, expected_coords)
759+
760+
761+
def test_convert_ase_to_diffpy_structure_bad_typeerror():
762+
"""Check convert_ase_to_diffpy_structure() with bad input."""
763+
bad_input = "string" # pass a string instead of ase.Atoms
764+
expected_error_msg = "Input must be an instance of ase.Atoms but got type str."
765+
actual_structure = Structure()
766+
with pytest.raises(TypeError, match=expected_error_msg):
767+
actual_structure.convert_ase_to_diffpy_structure(bad_input)
768+
769+
770+
@pytest.mark.parametrize(
771+
"bad_lost_info,error,expected_error_msg",
772+
[ # case: User provides an ASE.Atoms object but requests lost_info that is not an attribute of ASE.Atoms
773+
# expected: A ValueError is raised with a clear error message indicating the requested lost_info
774+
# attribute is invalid.
775+
(["invalid_method"], ValueError, "ASE.Atoms object has no attribute 'invalid_method'"),
776+
# case: User provides an ASE.Atoms object but requests lost_info that is an attribute of ASE.Atoms
777+
# but has not been set yet.
778+
# expected: The error message from ase is raised indicating the specific issue with the
779+
# requested lost_info attribute.
780+
# We set the expected error message to None because this expectation is
781+
# out of our control, but it is good to make sure that we are
782+
# raising the error from ASE.
783+
(["get_magnetic_moments"], RuntimeError, None),
784+
],
785+
)
786+
def test_convert_ase_to_diffpy_structure_bad_valueerror(
787+
bad_lost_info, error, expected_error_msg, build_ase_atom_object
788+
):
789+
"""Check convert_ase_to_diffpy_structure() with bad lost_info."""
790+
ase_zb = build_ase_atom_object
791+
actual_structure = Structure()
792+
with pytest.raises(error, match=expected_error_msg):
793+
actual_structure.convert_ase_to_diffpy_structure(ase_zb, lost_info=bad_lost_info)
794+
692795

693796
# ----------------------------------------------------------------------------
694797

0 commit comments

Comments
 (0)