Skip to content

Commit 46e7bd2

Browse files
authored
πŸ”€ Merge pull request #8 from Baharis/development
✨ Correctly transform the U matrix when applying transformations
2 parents 227592e + 6faaae9 commit 46e7bd2

8 files changed

Lines changed: 439 additions & 7 deletions

File tree

β€Žpicometer/atom.pyβ€Ž

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,20 @@ def fract_xyz(self) -> np.ndarray:
119119
def cart_xyz(self) -> np.ndarray:
120120
return self.orthogonalise(self.fract_xyz)
121121

122+
@property
123+
def fract_uij(self) -> np.ndarray:
124+
"""Return a 3D array i.e. stack of 3x3 fract. displacement tensors."""
125+
t = self.table
126+
default = pd.Series([np.nan] * len(t), index=t.index)
127+
uij = np.zeros((len(t), 3, 3), dtype=np.float64)
128+
uij[:, 0, 0] = t.get('U11', default).to_numpy(dtype=np.float64)
129+
uij[:, 1, 1] = t.get('U22', default).to_numpy(dtype=np.float64)
130+
uij[:, 2, 2] = t.get('U33', default).to_numpy(dtype=np.float64)
131+
uij[:, 0, 1] = uij[:, 1, 0] = t.get('U12', default).to_numpy(dtype=np.float64)
132+
uij[:, 0, 2] = uij[:, 2, 0] = t.get('U13', default).to_numpy(dtype=np.float64)
133+
uij[:, 1, 2] = uij[:, 2, 1] = t.get('U23', default).to_numpy(dtype=np.float64)
134+
return uij
135+
122136
def fractionalise(self, cart_xyz: np.ndarray) -> np.ndarray:
123137
"""Multiply 3xN vector by crystallographic matrix to get fract coord"""
124138
return np.linalg.inv(self.base.A_d.T) @ cart_xyz
@@ -158,6 +172,17 @@ def transform(self, symm_op_code: str) -> 'AtomSet':
158172
data['fract_x'] = fract_xyz[:, 0]
159173
data['fract_y'] = fract_xyz[:, 1]
160174
data['fract_z'] = fract_xyz[:, 2]
175+
if {'U11', 'U22', 'U33', 'U12', 'U13', 'U23'}.issubset(data.columns):
176+
uij = self.fract_uij # shape: (n_atoms, 3, 3)
177+
mask = ~np.isnan(uij).all(axis=(1, 2)) # atoms with defined Uij
178+
if np.any(mask):
179+
uij_rot = (s := symm_op.tf) @ uij[mask] @ s.T
180+
data.loc[mask, 'U11'] = uij_rot[:, 0, 0]
181+
data.loc[mask, 'U22'] = uij_rot[:, 1, 1]
182+
data.loc[mask, 'U33'] = uij_rot[:, 2, 2]
183+
data.loc[mask, 'U12'] = uij_rot[:, 0, 1]
184+
data.loc[mask, 'U13'] = uij_rot[:, 0, 2]
185+
data.loc[mask, 'U23'] = uij_rot[:, 1, 2]
161186
return self.__class__(self.base, data)
162187

163188
@property

β€Žpicometer/instructions.pyβ€Ž

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from typing import Any, Union, Protocol
1616

1717
from numpy import rad2deg
18+
import numpy as np
1819
import pandas as pd
1920
import yaml
2021

@@ -218,6 +219,31 @@ def _load_model_state(self, cif_path, block_name):
218219
label = cif_path + (':' + block_name if block_name else '')
219220
self.processor.model_states[label] = ModelState(atoms=atoms)
220221
logger.info(f'Loaded model state {label}')
222+
223+
if self.processor.settings['complete_uiso_from_umatrix']:
224+
if 'U11' in atoms.table.columns:
225+
if 'Uiso' not in atoms.table.columns:
226+
atoms.table['Uiso'] = pd.NA
227+
u_equiv = atoms.table[['U11', 'U22', 'U33']].mean(axis=1)
228+
atoms.table['Uiso'].fillna(u_equiv, inplace=True)
229+
230+
if self.processor.settings['complete_umatrix_from_uiso']:
231+
u_columns = ['U11', 'U12', 'U13', 'U22', 'U23', 'U33']
232+
if 'Uiso' in atoms.table.columns:
233+
for col in u_columns:
234+
if col not in atoms.table.columns:
235+
atoms.table[col] = pd.NA
236+
mask1 = atoms.table['Uiso'].notna()
237+
mask2 = atoms.table[['U11', 'U22', 'U33']].isna().all(axis=1)
238+
# based on http://dx.doi.org/10.1107/S0021889802008580
239+
n_mat = np.diag([atoms.base.a_r, atoms.base.b_r, atoms.base.c_r])
240+
n_inv = np.linalg.inv(n_mat)
241+
u_star = (m := np.linalg.inv(atoms.base.A_d.T)) @ m.T
242+
u_cif = n_inv @ u_star @ n_inv.T
243+
for atom_label in atoms.table.index[mask1 & mask2]:
244+
u_atom = atoms.table.at[atom_label, 'Uiso'] * u_cif
245+
atoms.table.loc[atom_label, u_columns] = u_atom[np.triu_indices(3)]
246+
221247
if not self.processor.settings['auto_write_unit_cell']:
222248
return
223249
et = self.processor.evaluation_table
@@ -326,6 +352,7 @@ class DisplacementInstructionHandler(SerialInstructionHandler):
326352

327353
def handle_one(self, instruction: Instruction, ms_key: str, ms: ModelState) -> None:
328354
focus = ms.nodes.locate(self.processor.selection)
355+
assert len(focus) > 0
329356
for label, displacements in focus.table.iterrows():
330357
for suffix in 'Uiso U11 U22 U33 U23 U13 U12'.split():
331358
label_ = label + '_' + suffix

β€Žpicometer/settings.pyβ€Ž

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ class DefaultSettings:
1919
"""Store default values of all settings. Use `AnyValue` if no default."""
2020
auto_write_unit_cell: bool = True
2121
clear_selection_after_use: bool = True
22+
complete_uiso_from_umatrix: bool = False
23+
complete_umatrix_from_uiso: bool = False
2224

2325
@classmethod
2426
def get_field(cls, key: str) -> Field:

β€Žpicometer/settings.yamlβ€Ž

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
settings:
22
auto_write_unit_cell: True
33
clear_selection_after_use: True
4+
complete_uiso_from_umatrix: False
5+
complete_umatrix_from_uiso: False

0 commit comments

Comments
Β (0)