Skip to content

Commit 6d0172a

Browse files
committed
Squashed commit of the following:
commit 6c9a31c81fa528000a28fb315338a6df40c5a3d2 Author: Viljar Femoen <viljar.femoen@hotmail.no> Date: Tue Jun 10 11:17:00 2025 +0200 Use new phase.expand_asymmetric_unit commit 496138e Author: Viljar Femoen <viljar.femoen@hotmail.no> Date: Thu Jun 5 15:21:10 2025 +0200 Formatting commit cde8647 Author: Viljar Femoen <viljar.femoen@hotmail.no> Date: Thu Jun 5 15:21:10 2025 +0200 Use numba for speedup commit cba08e1 Author: Viljar Femoen <viljar.femoen@hotmail.no> Date: Thu Jun 5 15:20:57 2025 +0200 Simplify `DiffractingVector.__getitem__` commit fce8e2b Author: Viljar Femoen <viljar.femoen@hotmail.no> Date: Thu Jun 5 15:20:57 2025 +0200 Use Phase.expand_asymmetric_unit commit 9c06e7d Author: Viljar Femoen <viljar.femoen@hotmail.no> Date: Thu Jun 5 15:20:39 2025 +0200 Fix test by using symmetrically unique reflections (4 degenerate were missing) commit 98dc783 Author: Viljar Femoen <viljar.femoen@hotmail.no> Date: Thu Jun 5 15:20:39 2025 +0200 Ensure compatible ordering of unique vectors commit 54b6f07 Author: Viljar Femoen <viljar.femoen@hotmail.no> Date: Thu Jun 5 15:20:39 2025 +0200 Getting closer, still some tests left commit be19d4f Author: Viljar Femoen <viljar.femoen@hotmail.no> Date: Thu Jun 5 15:20:39 2025 +0200 Adress #234 commit ccdb65a Author: Viljar Femoen <viljar.femoen@hotmail.no> Date: Thu Jun 5 15:20:16 2025 +0200 Return self.__class__ instead of explicit ReciprocalLatticeVector commit 44825d5 Author: Viljar Femoen <viljar.femoen@hotmail.no> Date: Thu Jun 5 15:20:16 2025 +0200 Use rotate_with_basis commit dd5b74e Author: Viljar Femoen <viljar.femoen@hotmail.no> Date: Thu Jun 5 15:19:44 2025 +0200 Fix bug where direct beam is added twice commit 38afe7b Author: Viljar Femoen <viljar.femoen@hotmail.no> Date: Thu Jun 5 15:19:44 2025 +0200 Fix formatting commit a00445d Author: Viljar Femoen <viljar.femoen@hotmail.no> Date: Thu Jun 5 15:19:11 2025 +0200 Support direct beam commit b759b5d Author: Viljar Femoen <viljar.femoen@hotmail.no> Date: Thu Jun 5 15:19:11 2025 +0200 Working precession, need to test for correctness commit c7d006a Author: Viljar Femoen <viljar.femoen@hotmail.no> Date: Thu Jun 5 15:19:10 2025 +0200 Support precession commit 5c59e4d Author: Viljar Femoen <viljar.femoen@hotmail.no> Date: Thu Jun 5 15:18:34 2025 +0200 Rotate Ewald's sphere instead of lattice commit 7aa207e Author: Viljar Femoen <viljar.femoen@hotmail.no> Date: Thu Jun 5 15:18:34 2025 +0200 Subclass Phase for faster initialization
1 parent 81f1147 commit 6d0172a

File tree

5 files changed

+393
-231
lines changed

5 files changed

+393
-231
lines changed

diffsims/crystallography/_diffracting_vector.py

Lines changed: 42 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,40 @@
1818

1919
from diffsims.crystallography import ReciprocalLatticeVector
2020
import numpy as np
21-
from orix.vector.miller import _transform_space
21+
from diffpy.structure import Structure
22+
from orix.crystal_map import Phase
2223
from orix.quaternion import Rotation
2324

2425

26+
class RotatedPhase(Phase):
27+
"""Helper class to speed up rotating the basis of a Phase object.
28+
The speedup comes from avoiding a deepcopy of the phase.
29+
30+
Parameters
31+
----------
32+
phase : orix.crystal_map.Phase
33+
A phase with a crystal lattice and symmetry.
34+
rotation : orix.quaternion.Rotation
35+
Rotation to apply to the basis of the phase
36+
"""
37+
38+
def __init__(self, phase: Phase, rotation: Rotation):
39+
# Copy structure to not override the input phase.
40+
# Use private members to avoid re-computing
41+
self._structure = Structure(phase.structure)
42+
self._diffpy_lattice = phase._diffpy_lattice
43+
self.name = phase.name
44+
self.space_group = phase.space_group
45+
self.point_group = phase.point_group
46+
self.color = phase.color
47+
48+
# Perform basis rotation
49+
br = self.structure.lattice.baserot
50+
# In case the base rotation is set already
51+
new_br = br @ rotation.to_matrix().squeeze()
52+
self.structure.lattice.setLatPar(baserot=new_br)
53+
54+
2555
class DiffractingVector(ReciprocalLatticeVector):
2656
r"""Reciprocal lattice vectors :math:`(hkl)` for use in electron
2757
diffraction analysis and simulation.
@@ -93,27 +123,9 @@ def __init__(self, phase, xyz=None, hkl=None, hkil=None, intensity=None):
93123
def __getitem__(self, key):
94124
new_data = self.data[key]
95125
dv_new = self.__class__(self.phase, xyz=new_data)
96-
97-
if np.isnan(self.structure_factor).all():
98-
dv_new._structure_factor = np.full(dv_new.shape, np.nan, dtype="complex128")
99-
100-
else:
101-
dv_new._structure_factor = self.structure_factor[key]
102-
if np.isnan(self.theta).all():
103-
dv_new._theta = np.full(dv_new.shape, np.nan)
104-
else:
105-
dv_new._theta = self.theta[key]
106-
if np.isnan(self.intensity).all():
107-
dv_new._intensity = np.full(dv_new.shape, np.nan)
108-
else:
109-
slic = self.intensity[key]
110-
if not hasattr(slic, "__len__"):
111-
slic = np.array(
112-
[
113-
slic,
114-
]
115-
)
116-
dv_new._intensity = slic
126+
dv_new._structure_factor = self._structure_factor[key]
127+
dv_new._theta = self._theta[key]
128+
dv_new._intensity = self._intensity[key]
117129

118130
return dv_new
119131

@@ -151,14 +163,11 @@ def rotate_with_basis(self, rotation):
151163
if rotation.size != 1:
152164
raise ValueError("Rotation must be a single rotation")
153165
# rotate basis
154-
new_phase = self.phase.deepcopy()
155-
br = new_phase.structure.lattice.baserot
156-
# In case the base rotation is set already
157-
new_br = br @ rotation.to_matrix().squeeze()
158-
new_phase.structure.lattice.setLatPar(baserot=new_br)
166+
new_phase = RotatedPhase(self.phase, rotation)
167+
159168
# rotate vectors
160169
vecs = ~rotation * self.to_miller()
161-
return ReciprocalLatticeVector(new_phase, xyz=vecs.data)
170+
return self.__class__(new_phase, xyz=vecs.data)
162171

163172
@property
164173
def intensity(self):
@@ -177,11 +186,11 @@ def intensity(self, value):
177186
raise ValueError("Length of intensity array must match number of vectors")
178187
self._intensity = np.array(value)
179188

180-
def calculate_structure_factor(self):
181-
raise NotImplementedError(
182-
"Structure factor calculation not implemented for DiffractionVector. "
183-
"Use ReciprocalLatticeVector instead."
184-
)
189+
# def calculate_structure_factor(self):
190+
# raise NotImplementedError(
191+
# "Structure factor calculation not implemented for DiffractionVector. "
192+
# "Use ReciprocalLatticeVector instead."
193+
# )
185194

186195
def to_flat_polar(self):
187196
"""Return the vectors in polar coordinates as projected onto the x,y plane"""

diffsims/crystallography/reciprocal_lattice_vector.py

Lines changed: 35 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@
1919
from collections import defaultdict
2020
from copy import deepcopy
2121

22-
from diffpy.structure.symmetryutilities import expandPosition
23-
from diffpy.structure import Structure
2422
import numba as nb
2523
import numpy as np
2624
from orix.vector import Miller, Vector3d
@@ -644,7 +642,9 @@ def allowed(self):
644642

645643
# ------------------------- Custom methods ----------------------- #
646644

647-
def calculate_structure_factor(self, scattering_params="xtables"):
645+
def calculate_structure_factor(
646+
self, scattering_params="xtables", debye_waller_factors=None
647+
):
648648
r"""Populate :attr:`structure_factor` with the complex
649649
kinematical structure factor :math:`F_{hkl}` for each vector.
650650
@@ -653,6 +653,8 @@ def calculate_structure_factor(self, scattering_params="xtables"):
653653
scattering_params : str
654654
Which atomic scattering factors to use, either ``"xtables"``
655655
(default) or ``"lobato"``.
656+
debye_waller_factors: dict
657+
Debye-Waller factors for atoms in the structure.
656658
657659
Examples
658660
--------
@@ -692,32 +694,17 @@ def calculate_structure_factor(self, scattering_params="xtables"):
692694
"""
693695

694696
# Compute one structure factor per set {hkl}
695-
hkl_sets = self.get_hkl_sets()
696-
697-
# For each set, get the indices of the first vector in the
698-
# present vectors, accounting for potential multiple dimensions
699-
# and avoding computing the unique vectors again
700-
first_idx = []
701-
for arr in list(hkl_sets.values()):
702-
i = []
703-
for arr_i in arr:
704-
i.append(arr_i[0])
705-
first_idx.append(i)
706-
first_idx_arr = np.array(first_idx).T
707-
708-
# Get 2D array of unique vectors, one for each set
709-
hkl_unique = self.hkl[tuple(first_idx_arr)]
710-
697+
unique, inds = self.unique(use_symmetry=True, return_inverse=True)
698+
hkl_unique = unique.hkl
711699
structure_factor = _get_kinematical_structure_factor(
712700
structure=self.phase.structure,
713701
g_indices=hkl_unique,
714702
g_hkls_array=self.phase.structure.lattice.rnorm(hkl_unique),
703+
debye_waller_factors=debye_waller_factors,
715704
scattering_params=scattering_params,
716705
)
717-
718706
# Set structure factors of all symmetrically equivalent vectors
719-
for i, idx in enumerate(hkl_sets.values()):
720-
self._structure_factor[idx] = structure_factor[i]
707+
self._structure_factor[:] = structure_factor[inds]
721708

722709
def calculate_theta(self, voltage):
723710
r"""Populate :attr:`theta` with the Bragg angle :math:`theta_B`
@@ -930,16 +917,12 @@ def sanitise_phase(self):
930917

931918
self._raise_if_no_space_group()
932919

933-
space_group = self.phase.space_group
934-
structure = self.phase.structure
920+
self.phase = self.phase.expand_asymmetric_unit()
935921

936-
new_structure = _expand_unit_cell(space_group, structure)
937-
for atom in new_structure:
922+
for atom in self.phase.structure:
938923
if np.issubdtype(type(atom.element), np.integer):
939924
atom.element = _get_string_from_element_id(atom.element)
940925

941-
self.phase.structure = new_structure
942-
943926
def symmetrise(self, return_multiplicity=False, return_index=False):
944927
"""Unique vectors symmetrically equivalent to the vectors.
945928
@@ -1506,7 +1489,7 @@ def transpose(self, *axes):
15061489
new._update_shapes()
15071490
return new
15081491

1509-
def unique(self, use_symmetry=False, return_index=False):
1492+
def unique(self, use_symmetry=False, return_index=False, return_inverse=False):
15101493
"""The unique vectors.
15111494
15121495
Parameters
@@ -1517,48 +1500,59 @@ def unique(self, use_symmetry=False, return_index=False):
15171500
return_index : bool, optional
15181501
Whether to return the indices of the (flattened) data where
15191502
the unique entries were found. Default is ``False``.
1503+
return_inverse: bool, optional
1504+
Whether to also return the indices to reconstruct the
1505+
(flattened) data from the unique data.
15201506
15211507
Returns
15221508
-------
15231509
ReciprocalLatticeVector
15241510
Flattened unique vectors.
15251511
idx : numpy.ndarray
15261512
Indices of the unique data in the (flattened) array.
1527-
1513+
inv : numpy.ndarray
1514+
Indices to reconstruct the original vectors from the unique
15281515
"""
15291516

15301517
# TODO: Reduce floating point precision in orix!
15311518
def miller_unique(miller, use_symmetry=False):
1532-
v, idx = Vector3d(miller).unique(return_index=True)
1519+
v, idx, inv = Vector3d(miller).unique(
1520+
return_index=True, return_inverse=True, ignore_zero=False
1521+
)
15331522

15341523
if use_symmetry:
15351524
operations = miller.phase.point_group
15361525
n_v = v.size
15371526
v2 = operations.outer(v).flatten().reshape(*(n_v, operations.size))
1538-
data = v2.data.round(6) # Reduced precision
1527+
data = v2.data.round(8) # Reduced precision
1528+
# To check for uniqueness, sort the equivalent vectors from each operation
15391529
data_sorted = np.zeros_like(data)
15401530
for i in range(n_v):
15411531
a = data[i]
15421532
order = np.lexsort(a.T) # Sort by column 1, 2, then 3
1543-
data_sorted[i] = a[order]
1544-
_, idx = np.unique(data_sorted, return_index=True, axis=0)
1545-
v = v[idx[::-1]]
1533+
data_sorted[i] = a[order[::-1]] # Reverse to preserve order
1534+
_, idx, inv = np.unique(
1535+
data_sorted, return_index=True, return_inverse=True, axis=0
1536+
)
1537+
v = v[idx]
15461538

15471539
m = miller.__class__(xyz=v.data, phase=miller.phase)
15481540
m.coordinate_format = miller.coordinate_format
1549-
return m, idx
15501541

1551-
# kwargs = dict(use_symmetry=use_symmetry, return_index=True)
1552-
# miller, idx = self.to_miller().unique(**kwargs)
1553-
miller, idx = miller_unique(self.to_miller(), use_symmetry)
1554-
idx = idx[::-1]
1542+
return m, idx, inv
1543+
1544+
miller, idx, inv = miller_unique(self.to_miller(), use_symmetry)
15551545

15561546
new_rlv = self.from_miller(miller)
15571547
new_rlv._structure_factor = self.structure_factor.ravel()[idx]
15581548
new_rlv._theta = self.theta.ravel()[idx]
15591549

1560-
if return_index:
1550+
if return_index and return_inverse:
1551+
return new_rlv, idx, inv
1552+
elif return_index:
15611553
return new_rlv, idx
1554+
elif return_inverse:
1555+
return new_rlv, inv
15621556
else:
15631557
return new_rlv
15641558

@@ -1596,62 +1590,6 @@ def stack(cls, sequence):
15961590
return new
15971591

15981592

1599-
# TODO: Upstream to diffpy.structure.Atom.__eq__()
1600-
def _atom_eq(atom1, atom2):
1601-
"""Determine whether two atoms are equal.
1602-
1603-
Parameters
1604-
----------
1605-
atom1, atom2 : diffpy.structure.Atom
1606-
1607-
Returns
1608-
-------
1609-
bool
1610-
1611-
"""
1612-
1613-
return (
1614-
atom1.element == atom2.element
1615-
and np.allclose(atom1.xyz, atom2.xyz, atol=1e-7)
1616-
and atom1.occupancy == atom2.occupancy
1617-
and np.allclose(atom1.U, atom2.U, atol=1e-7)
1618-
and np.allclose(atom1.Uisoequiv, atom2.Uisoequiv, atol=1e-7)
1619-
)
1620-
1621-
1622-
# TODO: Upstream to orix.crystal_map.Phase.expand_structure()
1623-
def _expand_unit_cell(space_group, structure):
1624-
"""Expand a unit cell with symmetrically equivalent atoms.
1625-
1626-
Parameters
1627-
----------
1628-
space_group : diffpy.structure.spacegroupmod.SpaceGroup
1629-
Space group describing the symmetry operations of the unit cell.
1630-
structure : diffpy.structure.Structure
1631-
Initial structure with atoms.
1632-
1633-
Returns
1634-
-------
1635-
new_structure : diffpy.structure.Structure
1636-
1637-
"""
1638-
1639-
new_structure = Structure(lattice=structure.lattice)
1640-
1641-
for atom in structure:
1642-
equal = []
1643-
for atom2 in new_structure:
1644-
equal.append(_atom_eq(atom, atom2))
1645-
if not any(equal):
1646-
new_positions = expandPosition(space_group, atom.xyz)[0]
1647-
for new_position in new_positions:
1648-
new_atom = deepcopy(atom)
1649-
new_atom.xyz = new_position
1650-
new_structure.append(new_atom)
1651-
1652-
return new_structure
1653-
1654-
16551593
@nb.njit(
16561594
"int64[:](float64[:, :], float64[:, :], int64[:])",
16571595
cache=True,

0 commit comments

Comments
 (0)