diffpy.structure 3.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- diffpy/Structure.py +35 -0
- diffpy/__init__.py +23 -0
- diffpy/structure/__init__.py +93 -0
- diffpy/structure/_legacy_importer.py +88 -0
- diffpy/structure/apps/__init__.py +17 -0
- diffpy/structure/apps/anyeye.py +284 -0
- diffpy/structure/apps/transtru.py +126 -0
- diffpy/structure/atom.py +544 -0
- diffpy/structure/expansion/__init__.py +27 -0
- diffpy/structure/expansion/makeellipsoid.py +129 -0
- diffpy/structure/expansion/shapeutils.py +44 -0
- diffpy/structure/expansion/supercell_mod.py +91 -0
- diffpy/structure/lattice.py +663 -0
- diffpy/structure/mmlibspacegroups.py +8154 -0
- diffpy/structure/parsers/__init__.py +83 -0
- diffpy/structure/parsers/p_auto.py +217 -0
- diffpy/structure/parsers/p_cif.py +876 -0
- diffpy/structure/parsers/p_discus.py +312 -0
- diffpy/structure/parsers/p_pdb.py +405 -0
- diffpy/structure/parsers/p_pdffit.py +290 -0
- diffpy/structure/parsers/p_rawxyz.py +149 -0
- diffpy/structure/parsers/p_xcfg.py +457 -0
- diffpy/structure/parsers/p_xyz.py +161 -0
- diffpy/structure/parsers/parser_index_mod.py +108 -0
- diffpy/structure/parsers/structureparser.py +80 -0
- diffpy/structure/pdffitstructure.py +109 -0
- diffpy/structure/sgtbxspacegroups.py +5198 -0
- diffpy/structure/spacegroupmod.py +329 -0
- diffpy/structure/spacegroups.py +1441 -0
- diffpy/structure/structure.py +866 -0
- diffpy/structure/structureerrors.py +35 -0
- diffpy/structure/symmetryutilities.py +1100 -0
- diffpy/structure/utils.py +126 -0
- diffpy/structure/version.py +26 -0
- diffpy.structure-3.2.0.dist-info/AUTHORS.rst +13 -0
- diffpy.structure-3.2.0.dist-info/LICENSE.rst +141 -0
- diffpy.structure-3.2.0.dist-info/LICENSE_DANSE.rst +50 -0
- diffpy.structure-3.2.0.dist-info/LICENSE_pymmlib.rst +203 -0
- diffpy.structure-3.2.0.dist-info/METADATA +197 -0
- diffpy.structure-3.2.0.dist-info/RECORD +42 -0
- diffpy.structure-3.2.0.dist-info/WHEEL +5 -0
- diffpy.structure-3.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1100 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
##############################################################################
|
|
3
|
+
#
|
|
4
|
+
# diffpy.structure by DANSE Diffraction group
|
|
5
|
+
# Simon J. L. Billinge
|
|
6
|
+
# (c) 2006 trustees of the Michigan State University.
|
|
7
|
+
# All rights reserved.
|
|
8
|
+
#
|
|
9
|
+
# File coded by: Pavol Juhas
|
|
10
|
+
#
|
|
11
|
+
# See AUTHORS.txt for a list of people who contributed.
|
|
12
|
+
# See LICENSE_DANSE.txt for license information.
|
|
13
|
+
#
|
|
14
|
+
##############################################################################
|
|
15
|
+
|
|
16
|
+
"""Symmetry utility functions such as expansion of asymmetric unit,
|
|
17
|
+
and generation of positional constraints.
|
|
18
|
+
|
|
19
|
+
Attributes
|
|
20
|
+
----------
|
|
21
|
+
epsilon : float
|
|
22
|
+
Default tolerance for equality of 2 positions, also
|
|
23
|
+
used for identification of special positions.
|
|
24
|
+
|
|
25
|
+
stdUsymbols : list
|
|
26
|
+
Standard symbols denoting elements of anisotropic thermal
|
|
27
|
+
displacement tensor.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import re
|
|
31
|
+
import sys
|
|
32
|
+
|
|
33
|
+
import numpy
|
|
34
|
+
|
|
35
|
+
from diffpy.structure.structureerrors import SymmetryError
|
|
36
|
+
|
|
37
|
+
# Constants ------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
epsilon = 1.0e-5
|
|
40
|
+
|
|
41
|
+
stdUsymbols = ["U11", "U22", "U33", "U12", "U13", "U23"]
|
|
42
|
+
|
|
43
|
+
# ----------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def isSpaceGroupLatPar(spacegroup, a, b, c, alpha, beta, gamma):
|
|
47
|
+
"""Check if space group allows passed lattice parameters.
|
|
48
|
+
|
|
49
|
+
Parameters
|
|
50
|
+
----------
|
|
51
|
+
spacegroup : SpaceGroup
|
|
52
|
+
Instance of `SpaceGroup`.
|
|
53
|
+
a, b, c, alpha, beta, gamma : float
|
|
54
|
+
`Lattice` parameters.
|
|
55
|
+
|
|
56
|
+
Return
|
|
57
|
+
------
|
|
58
|
+
bool
|
|
59
|
+
``True`` when lattice parameters are allowed by space group.
|
|
60
|
+
|
|
61
|
+
Note
|
|
62
|
+
----
|
|
63
|
+
Crystal system rules:
|
|
64
|
+
|
|
65
|
+
Benjamin, W. A., Introduction to crystallography, New York (1969), p.60.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
# crystal system rules
|
|
69
|
+
# ref: Benjamin, W. A., Introduction to crystallography,
|
|
70
|
+
# New York (1969), p.60
|
|
71
|
+
def check_triclinic():
|
|
72
|
+
return True
|
|
73
|
+
|
|
74
|
+
def check_monoclinic():
|
|
75
|
+
rv = (alpha == gamma == 90) or (alpha == beta == 90)
|
|
76
|
+
return rv
|
|
77
|
+
|
|
78
|
+
def check_orthorhombic():
|
|
79
|
+
return alpha == beta == gamma == 90
|
|
80
|
+
|
|
81
|
+
def check_tetragonal():
|
|
82
|
+
return a == b and alpha == beta == gamma == 90
|
|
83
|
+
|
|
84
|
+
def check_trigonal():
|
|
85
|
+
rv = (a == b == c and alpha == beta == gamma) or (a == b and alpha == beta == 90 and gamma == 120)
|
|
86
|
+
return rv
|
|
87
|
+
|
|
88
|
+
def check_hexagonal():
|
|
89
|
+
return a == b and alpha == beta == 90 and gamma == 120
|
|
90
|
+
|
|
91
|
+
def check_cubic():
|
|
92
|
+
return a == b == c and alpha == beta == gamma == 90
|
|
93
|
+
|
|
94
|
+
crystal_system_rules = {
|
|
95
|
+
"TRICLINIC": check_triclinic,
|
|
96
|
+
"MONOCLINIC": check_monoclinic,
|
|
97
|
+
"ORTHORHOMBIC": check_orthorhombic,
|
|
98
|
+
"TETRAGONAL": check_tetragonal,
|
|
99
|
+
"TRIGONAL": check_trigonal,
|
|
100
|
+
"HEXAGONAL": check_hexagonal,
|
|
101
|
+
"CUBIC": check_cubic,
|
|
102
|
+
}
|
|
103
|
+
rule = crystal_system_rules[spacegroup.crystal_system]
|
|
104
|
+
return rule()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# Constant regular expression used in isconstantFormula().
|
|
108
|
+
# isconstantFormula runs faster when regular expression is not
|
|
109
|
+
# compiled per every single call.
|
|
110
|
+
|
|
111
|
+
_rx_constant_formula = re.compile(r"[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)??(/[-+]?\d+)?$")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def isconstantFormula(s):
|
|
115
|
+
"""Check if formula string is constant.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
s : str
|
|
120
|
+
Formula string.
|
|
121
|
+
|
|
122
|
+
Return
|
|
123
|
+
------
|
|
124
|
+
bool
|
|
125
|
+
``True`` when argument is a floating point number or a fraction of float with integer.
|
|
126
|
+
"""
|
|
127
|
+
res = _rx_constant_formula.match(s.replace(" ", ""))
|
|
128
|
+
return bool(res)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# Helper class intended for this module only:
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class _Position2Tuple(object):
|
|
135
|
+
"""Create callable object that converts fractional coordinates to
|
|
136
|
+
a tuple of integers with given precision. For presision close to zero
|
|
137
|
+
it will return a tuples of double.
|
|
138
|
+
|
|
139
|
+
Note
|
|
140
|
+
----
|
|
141
|
+
Helper class intended for local use only.
|
|
142
|
+
|
|
143
|
+
Parameters
|
|
144
|
+
----------
|
|
145
|
+
eps : float
|
|
146
|
+
Cutoff for equivalent coordinates.
|
|
147
|
+
|
|
148
|
+
Attributes
|
|
149
|
+
----------
|
|
150
|
+
eps : float
|
|
151
|
+
Cutoff for equivalent coordinates. When two coordiantes map to the
|
|
152
|
+
same tuple, they are closer than `eps`.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
def __init__(self, eps=None):
|
|
156
|
+
if eps is None:
|
|
157
|
+
eps = epsilon
|
|
158
|
+
# ensure self.eps has exact machine representation
|
|
159
|
+
self.eps = eps + 1.0
|
|
160
|
+
self.eps = self.eps - 1.0
|
|
161
|
+
# no conversions for very small eps
|
|
162
|
+
if self.eps == 0.0 or 1.0 / self.eps > sys.maxsize:
|
|
163
|
+
self.eps = 0.0
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
def __call__(self, xyz):
|
|
167
|
+
"""Convert array of fractional coordinates to a tuple.
|
|
168
|
+
|
|
169
|
+
Parameters
|
|
170
|
+
----------
|
|
171
|
+
xyz : Iterable
|
|
172
|
+
Fractional coordinates.
|
|
173
|
+
|
|
174
|
+
Return
|
|
175
|
+
------
|
|
176
|
+
tuple
|
|
177
|
+
Tuple of 3 float when `eps` is zero, otherwise tuple of 3 int.
|
|
178
|
+
"""
|
|
179
|
+
# no conversion case
|
|
180
|
+
if self.eps == 0.0:
|
|
181
|
+
tpl = tuple(xyz % 1.0)
|
|
182
|
+
return tpl
|
|
183
|
+
# here we convert to integer
|
|
184
|
+
tpl = tuple([int((xi - numpy.floor(xi)) / self.eps) for xi in xyz])
|
|
185
|
+
return tpl
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# End of class _Position2Tuple
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def positionDifference(xyz0, xyz1):
|
|
192
|
+
"""Smallest difference between two coordinates in periodic lattice.
|
|
193
|
+
|
|
194
|
+
Parameters
|
|
195
|
+
----------
|
|
196
|
+
xyz0, xyz1 : array_like
|
|
197
|
+
Fractional coordinates.
|
|
198
|
+
|
|
199
|
+
Return
|
|
200
|
+
------
|
|
201
|
+
dxyz : numpy.ndarray
|
|
202
|
+
Smallest difference between two coordinates in periodic lattice
|
|
203
|
+
with ``0 <= dxyz <= 0.5``.
|
|
204
|
+
"""
|
|
205
|
+
dxyz = numpy.asarray(xyz0) - xyz1
|
|
206
|
+
# map differences to [0,0.5]
|
|
207
|
+
dxyz = dxyz - numpy.floor(dxyz)
|
|
208
|
+
mask = dxyz > 0.5
|
|
209
|
+
dxyz[mask] = 1.0 - dxyz[mask]
|
|
210
|
+
return dxyz
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def nearestSiteIndex(sites, xyz):
|
|
214
|
+
"""Index of the nearest site to a specified position.
|
|
215
|
+
|
|
216
|
+
Parameters
|
|
217
|
+
----------
|
|
218
|
+
sites : array_like
|
|
219
|
+
List of coordinates.
|
|
220
|
+
xyz : array_like
|
|
221
|
+
Single position.
|
|
222
|
+
|
|
223
|
+
Return
|
|
224
|
+
------
|
|
225
|
+
int
|
|
226
|
+
Index of the nearest site.
|
|
227
|
+
"""
|
|
228
|
+
# we use box distance to be consistent with _Position2Tuple conversion
|
|
229
|
+
dbox = positionDifference(sites, xyz).max(axis=1)
|
|
230
|
+
nearindex = numpy.argmin(dbox)
|
|
231
|
+
return nearindex
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def equalPositions(xyz0, xyz1, eps):
|
|
235
|
+
"""Equality of two coordinates with optional tolerance.
|
|
236
|
+
|
|
237
|
+
Parameters
|
|
238
|
+
----------
|
|
239
|
+
xyz0, xyz1 : array_like
|
|
240
|
+
Fractional coordinates.
|
|
241
|
+
eps : float
|
|
242
|
+
Tolerance for equality of coordinates.
|
|
243
|
+
|
|
244
|
+
Return
|
|
245
|
+
------
|
|
246
|
+
bool
|
|
247
|
+
``True`` when two coordinates are closer than `eps`.
|
|
248
|
+
"""
|
|
249
|
+
# we use box distance to be consistent with _Position2Tuple conversion
|
|
250
|
+
dxyz = positionDifference(xyz0, xyz1)
|
|
251
|
+
return numpy.all(dxyz <= eps)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def expandPosition(spacegroup, xyz, sgoffset=[0, 0, 0], eps=None):
|
|
255
|
+
"""Obtain unique equivalent positions and corresponding operations.
|
|
256
|
+
|
|
257
|
+
Parameters
|
|
258
|
+
----------
|
|
259
|
+
spacegroup : SpaceGroup
|
|
260
|
+
Instance of SpaceGroup.
|
|
261
|
+
xyz : list
|
|
262
|
+
Position to be expanded.
|
|
263
|
+
sgoffset : list, Optional
|
|
264
|
+
Offset of space group origin ``[0, 0, 0]``. Default is ``[0, 0, 0]``.
|
|
265
|
+
eps : float, Optional
|
|
266
|
+
Cutoff for equal positions, default is ``1.0e-5``.
|
|
267
|
+
|
|
268
|
+
Return
|
|
269
|
+
------
|
|
270
|
+
tuple
|
|
271
|
+
A tuple with ``(list of unique equivalent positions, nested
|
|
272
|
+
list of `SpaceGroups.SymOp` instances, site multiplicity)``.
|
|
273
|
+
"""
|
|
274
|
+
sgoffset = numpy.asarray(sgoffset, dtype=float)
|
|
275
|
+
if eps is None:
|
|
276
|
+
eps = epsilon
|
|
277
|
+
pos2tuple = _Position2Tuple(eps)
|
|
278
|
+
positions = []
|
|
279
|
+
site_symops = {} # position tuples with [related symops]
|
|
280
|
+
for symop in spacegroup.iter_symops():
|
|
281
|
+
# operate on coordinates in non-shifted spacegroup
|
|
282
|
+
pos = symop(xyz + sgoffset) - sgoffset
|
|
283
|
+
mask = numpy.logical_or(pos < 0.0, pos >= 1.0)
|
|
284
|
+
pos[mask] -= numpy.floor(pos[mask])
|
|
285
|
+
tpl = pos2tuple(pos)
|
|
286
|
+
if tpl not in site_symops:
|
|
287
|
+
pos_is_new = True
|
|
288
|
+
site_symops[tpl] = []
|
|
289
|
+
# double check if there is any position nearby
|
|
290
|
+
if positions:
|
|
291
|
+
nearpos = positions[nearestSiteIndex(positions, pos)]
|
|
292
|
+
# is it an equivalent position?
|
|
293
|
+
if equalPositions(nearpos, pos, eps):
|
|
294
|
+
# tpl should map to the same list as nearpos
|
|
295
|
+
site_symops[tpl] = site_symops[pos2tuple(nearpos)]
|
|
296
|
+
pos_is_new = False
|
|
297
|
+
if pos_is_new:
|
|
298
|
+
positions.append(pos)
|
|
299
|
+
# here tpl is inside site_symops
|
|
300
|
+
site_symops[tpl].append(symop)
|
|
301
|
+
# pos_symops is nested list of symops associated with each position
|
|
302
|
+
pos_symops = [site_symops[pos2tuple(p)] for p in positions]
|
|
303
|
+
multiplicity = len(positions)
|
|
304
|
+
return positions, pos_symops, multiplicity
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def nullSpace(A):
|
|
308
|
+
"""Null space of matrix A."""
|
|
309
|
+
from numpy import linalg
|
|
310
|
+
|
|
311
|
+
u, s, v = linalg.svd(A)
|
|
312
|
+
# s may have smaller dimension than v
|
|
313
|
+
vnrows = numpy.shape(v)[0]
|
|
314
|
+
mask = numpy.ones(vnrows, dtype=bool)
|
|
315
|
+
mask[s > epsilon] = False
|
|
316
|
+
null_space = numpy.compress(mask, v, axis=0)
|
|
317
|
+
return null_space
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _findInvariants(symops):
|
|
321
|
+
"""Find a list of symmetry operations which contains identity.
|
|
322
|
+
|
|
323
|
+
Parameters
|
|
324
|
+
----------
|
|
325
|
+
symops : list of SymOp
|
|
326
|
+
Nested list of `SymOp` instances.
|
|
327
|
+
|
|
328
|
+
Return
|
|
329
|
+
------
|
|
330
|
+
list
|
|
331
|
+
List-item in symops which contains identity.
|
|
332
|
+
|
|
333
|
+
Raise
|
|
334
|
+
-----
|
|
335
|
+
ValueError
|
|
336
|
+
When identity is not found.
|
|
337
|
+
"""
|
|
338
|
+
invrnts = None
|
|
339
|
+
R0 = numpy.identity(3, dtype=float)
|
|
340
|
+
t0 = numpy.zeros(3, dtype=float)
|
|
341
|
+
for ops in symops:
|
|
342
|
+
for op in ops:
|
|
343
|
+
if numpy.all(op.R == R0) and numpy.all(op.t == t0):
|
|
344
|
+
invrnts = ops
|
|
345
|
+
break
|
|
346
|
+
if invrnts:
|
|
347
|
+
break
|
|
348
|
+
if invrnts is None:
|
|
349
|
+
emsg = "Could not find identity operation."
|
|
350
|
+
raise ValueError(emsg)
|
|
351
|
+
return invrnts
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
# ----------------------------------------------------------------------------
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class GeneratorSite(object):
|
|
358
|
+
"""Storage of data related to a generator positions.
|
|
359
|
+
|
|
360
|
+
Parameters
|
|
361
|
+
----------
|
|
362
|
+
spacegroup : SpaceGroup
|
|
363
|
+
Instance of `SpaceGroup`.
|
|
364
|
+
xyz : array_like
|
|
365
|
+
Generating site. When `xyz` is close to special
|
|
366
|
+
position `self.xyz` will be adjusted.
|
|
367
|
+
Uij : array_like, Optional
|
|
368
|
+
Thermal factors at generator site. Yields `self.Uij`
|
|
369
|
+
after adjusting to spacegroup symmetry. Default is zeros.
|
|
370
|
+
sgoffset : list, Optional
|
|
371
|
+
Offset of space group origin ``[0, 0, 0]``. Default is ``[0, 0, 0]``.
|
|
372
|
+
eps : float, Optional
|
|
373
|
+
Cutoff for equal positions. Default is ``1.0e-5``.
|
|
374
|
+
|
|
375
|
+
Attributes
|
|
376
|
+
----------
|
|
377
|
+
xyz : numpy.ndarray
|
|
378
|
+
Fractional coordinates of generator site.
|
|
379
|
+
Uij : numpy.ndarray
|
|
380
|
+
Anisotropic thermal displacement at generator site.
|
|
381
|
+
sgoffset : numpy.ndarray
|
|
382
|
+
Offset of space group origin ``[0, 0, 0]``.
|
|
383
|
+
eps : float
|
|
384
|
+
Cutoff for equal positions.
|
|
385
|
+
eqxyz : list
|
|
386
|
+
List of equivalent positions.
|
|
387
|
+
eqUij : list
|
|
388
|
+
List of displacement matrices at equivalent positions.
|
|
389
|
+
symops : list
|
|
390
|
+
Nested list of operations per each `eqxyz`.
|
|
391
|
+
multiplicity : int
|
|
392
|
+
Generator site multiplicity.
|
|
393
|
+
Uisotropy : bool
|
|
394
|
+
Bool flag for isotropic thermal factors.
|
|
395
|
+
invariants : list
|
|
396
|
+
List of invariant operations for generator site.
|
|
397
|
+
null_space : numpy.ndarray
|
|
398
|
+
Null space of all possible differences of invariant
|
|
399
|
+
rotational matrices, this is a base of symmetry
|
|
400
|
+
allowed shifts.
|
|
401
|
+
Uspace : numpy.ndarray
|
|
402
|
+
3D array of independent components of U matrices.
|
|
403
|
+
pparameters : list
|
|
404
|
+
List of ``(xyz symbol, value)`` pairs.
|
|
405
|
+
Uparameters : list
|
|
406
|
+
List of ``(U symbol, value)`` pairs.
|
|
407
|
+
"""
|
|
408
|
+
|
|
409
|
+
Ucomponents = numpy.array(
|
|
410
|
+
[
|
|
411
|
+
[[1, 0, 0], [0, 0, 0], [0, 0, 0]],
|
|
412
|
+
[[0, 0, 0], [0, 1, 0], [0, 0, 0]],
|
|
413
|
+
[[0, 0, 0], [0, 0, 0], [0, 0, 1]],
|
|
414
|
+
[[0, 1, 0], [1, 0, 0], [0, 0, 0]],
|
|
415
|
+
[[0, 0, 1], [0, 0, 0], [1, 0, 0]],
|
|
416
|
+
[[0, 0, 0], [0, 0, 1], [0, 1, 0]],
|
|
417
|
+
],
|
|
418
|
+
dtype=float,
|
|
419
|
+
)
|
|
420
|
+
"""numpy.ndarray: 6x3x3 array of independent components of U matrices."""
|
|
421
|
+
|
|
422
|
+
idx2Usymbol = {0: "U11", 1: "U12", 2: "U13", 3: "U12", 4: "U22", 5: "U23", 6: "U13", 7: "U23", 8: "U33"}
|
|
423
|
+
"""dict: Mapping of index to standard U symbol."""
|
|
424
|
+
|
|
425
|
+
def __init__(self, spacegroup, xyz, Uij=numpy.zeros((3, 3)), sgoffset=[0, 0, 0], eps=None):
|
|
426
|
+
if eps is None:
|
|
427
|
+
eps = epsilon
|
|
428
|
+
# just declare the members
|
|
429
|
+
self.xyz = numpy.array(xyz, dtype=float)
|
|
430
|
+
self.Uij = numpy.array(Uij, dtype=float)
|
|
431
|
+
self.sgoffset = numpy.array(sgoffset, dtype=float)
|
|
432
|
+
self.eps = eps
|
|
433
|
+
self.eqxyz = []
|
|
434
|
+
self.eqUij = []
|
|
435
|
+
self.symops = None
|
|
436
|
+
self.multiplicity = None
|
|
437
|
+
self.Uisotropy = False
|
|
438
|
+
self.invariants = []
|
|
439
|
+
self.null_space = None
|
|
440
|
+
self.Uspace = None
|
|
441
|
+
self.pparameters = []
|
|
442
|
+
self.Uparameters = []
|
|
443
|
+
# fill in the values
|
|
444
|
+
sites, ops, mult = expandPosition(spacegroup, xyz, sgoffset, eps)
|
|
445
|
+
invariants = _findInvariants(ops)
|
|
446
|
+
# shift self.xyz exactly to the special position
|
|
447
|
+
if mult > 1:
|
|
448
|
+
xyzdups = numpy.array([op(xyz + self.sgoffset) - self.sgoffset for op in invariants])
|
|
449
|
+
dxyz = xyzdups - xyz
|
|
450
|
+
dxyz = numpy.mean(dxyz - dxyz.round(), axis=0)
|
|
451
|
+
# recalculate if needed
|
|
452
|
+
if numpy.any(dxyz != 0.0):
|
|
453
|
+
self.xyz = xyz + dxyz
|
|
454
|
+
self.xyz[numpy.fabs(self.xyz) < self.eps] = 0.0
|
|
455
|
+
sites, ops, mult = expandPosition(spacegroup, self.xyz, self.sgoffset, eps)
|
|
456
|
+
invariants = _findInvariants(ops)
|
|
457
|
+
# self.xyz, sites, ops are all adjusted here
|
|
458
|
+
self.eqxyz = sites
|
|
459
|
+
self.symops = ops
|
|
460
|
+
self.multiplicity = mult
|
|
461
|
+
self.invariants = invariants
|
|
462
|
+
self._findNullSpace()
|
|
463
|
+
self._findPosParameters()
|
|
464
|
+
self._findUSpace()
|
|
465
|
+
self._findUParameters()
|
|
466
|
+
self._findeqUij()
|
|
467
|
+
return
|
|
468
|
+
|
|
469
|
+
def signedRatStr(self, x):
|
|
470
|
+
"""Convert floating point number to signed rational representation.
|
|
471
|
+
|
|
472
|
+
Possible fractional are multiples of 1/3, 1/6, 1/7, 1/9, if these
|
|
473
|
+
are not close, return `%+g` format.
|
|
474
|
+
|
|
475
|
+
Parameters
|
|
476
|
+
----------
|
|
477
|
+
x : float
|
|
478
|
+
Floating point number.
|
|
479
|
+
|
|
480
|
+
Return
|
|
481
|
+
------
|
|
482
|
+
str
|
|
483
|
+
Signed rational representation of `x`.
|
|
484
|
+
"""
|
|
485
|
+
s = "{:.8g}".format(x)
|
|
486
|
+
if len(s) < 6:
|
|
487
|
+
return "%+g" % x
|
|
488
|
+
den = numpy.array([3.0, 6.0, 7.0, 9.0])
|
|
489
|
+
nom = x * den
|
|
490
|
+
idx = numpy.where(numpy.fabs(nom - nom.round()) < self.eps)[0]
|
|
491
|
+
if idx.size == 0:
|
|
492
|
+
return "%+g" % x
|
|
493
|
+
# here we have fraction
|
|
494
|
+
return "%+.0f/%.0f" % (nom[idx[0]], den[idx[0]])
|
|
495
|
+
|
|
496
|
+
def _findNullSpace(self):
|
|
497
|
+
"""Calculate `self.null_space` from `self.invariants`.
|
|
498
|
+
Try to represent `self.null_space` using small integers.
|
|
499
|
+
"""
|
|
500
|
+
R0 = self.invariants[0].R
|
|
501
|
+
Rdiff = [(symop.R - R0) for symop in self.invariants]
|
|
502
|
+
Rdiff = numpy.concatenate(Rdiff, axis=0)
|
|
503
|
+
self.null_space = nullSpace(Rdiff)
|
|
504
|
+
if self.null_space.size == 0:
|
|
505
|
+
return
|
|
506
|
+
# reverse sort rows of null_space rows by absolute value
|
|
507
|
+
key = tuple(numpy.fabs(numpy.transpose(self.null_space))[::-1])
|
|
508
|
+
order = numpy.lexsort(key)
|
|
509
|
+
self.null_space = self.null_space[order[::-1]]
|
|
510
|
+
# rationalize by the smallest element larger than cutoff
|
|
511
|
+
cutoff = 1.0 / 32
|
|
512
|
+
for row in self.null_space:
|
|
513
|
+
abrow = numpy.abs(row)
|
|
514
|
+
sgrow = numpy.sign(row)
|
|
515
|
+
# equalize items with round-off-equal absolute value
|
|
516
|
+
ii = abrow.argsort()
|
|
517
|
+
delta = 1e-8 * abrow[ii[-1]]
|
|
518
|
+
for k in ii[1:]:
|
|
519
|
+
if abrow[k] - abrow[k - 1] < delta:
|
|
520
|
+
abrow[k] = abrow[k - 1]
|
|
521
|
+
# find the smallest nonzero absolute element
|
|
522
|
+
jnz = numpy.flatnonzero(abrow > cutoff)
|
|
523
|
+
idx = jnz[abrow[jnz].argmin()]
|
|
524
|
+
row[:] = (sgrow * abrow) / sgrow[idx] / abrow[idx]
|
|
525
|
+
return
|
|
526
|
+
|
|
527
|
+
def _findPosParameters(self):
|
|
528
|
+
"""Find pparameters and their values for expressing `self.xyz`."""
|
|
529
|
+
usedsymbol = {}
|
|
530
|
+
# parameter values depend on offset of self.xyz
|
|
531
|
+
txyz = self.xyz
|
|
532
|
+
# define txyz such that most of its elements are zero
|
|
533
|
+
for nvec in self.null_space:
|
|
534
|
+
idx = numpy.where(numpy.fabs(nvec) >= epsilon)[0][0]
|
|
535
|
+
varvalue = txyz[idx] / nvec[idx]
|
|
536
|
+
txyz = txyz - varvalue * nvec
|
|
537
|
+
# determine standard parameter name
|
|
538
|
+
vname = [s for s in "xyz"[idx:] if s not in usedsymbol][0]
|
|
539
|
+
self.pparameters.append((vname, varvalue))
|
|
540
|
+
usedsymbol[vname] = True
|
|
541
|
+
return
|
|
542
|
+
|
|
543
|
+
def _findUSpace(self):
|
|
544
|
+
"""Find independent U components with respect to invariant
|
|
545
|
+
rotations.
|
|
546
|
+
"""
|
|
547
|
+
n = len(self.invariants)
|
|
548
|
+
R6zall = numpy.tile(-numpy.identity(6, dtype=float), (n, 1))
|
|
549
|
+
R6zall_iter = numpy.split(R6zall, n, axis=0)
|
|
550
|
+
i6kl = ((0, (0, 0)), (1, (1, 1)), (2, (2, 2)), (3, (0, 1)), (4, (0, 2)), (5, (1, 2)))
|
|
551
|
+
for op, R6z in zip(self.invariants, R6zall_iter):
|
|
552
|
+
R = op.R
|
|
553
|
+
for j, Ucj in enumerate(self.Ucomponents):
|
|
554
|
+
Ucj2 = numpy.dot(R, numpy.dot(Ucj, R.T))
|
|
555
|
+
for i, kl in i6kl:
|
|
556
|
+
R6z[i, j] += Ucj2[kl]
|
|
557
|
+
Usp6 = nullSpace(R6zall)
|
|
558
|
+
# normalize Usp6 by its maximum component
|
|
559
|
+
mxcols = numpy.argmax(numpy.fabs(Usp6), axis=1)
|
|
560
|
+
mxrows = numpy.arange(len(mxcols))
|
|
561
|
+
Usp6 /= Usp6[mxrows, mxcols].reshape(-1, 1)
|
|
562
|
+
Usp6 = numpy.around(Usp6, 2)
|
|
563
|
+
# normalize again after rounding to get correct signs
|
|
564
|
+
mxcols = numpy.argmax(numpy.fabs(Usp6), axis=1)
|
|
565
|
+
Usp6 /= Usp6[mxrows, mxcols].reshape(-1, 1)
|
|
566
|
+
self.Uspace = numpy.tensordot(Usp6, self.Ucomponents, axes=(1, 0))
|
|
567
|
+
self.Uisotropy = len(self.Uspace) == 1
|
|
568
|
+
return
|
|
569
|
+
|
|
570
|
+
def _findUParameters(self):
|
|
571
|
+
"""Find Uparameters and their values for expressing `self.Uij`."""
|
|
572
|
+
# permute indices as 00 11 22 01 02 12 10 20 21
|
|
573
|
+
diagorder = numpy.array((0, 4, 8, 1, 2, 5, 3, 6, 7))
|
|
574
|
+
Uijflat = self.Uij.flatten()
|
|
575
|
+
for Usp in self.Uspace:
|
|
576
|
+
Uspflat = Usp.flatten()
|
|
577
|
+
Uspnorm2 = numpy.dot(Uspflat, Uspflat)
|
|
578
|
+
permidx = next(i for i, x in enumerate(Uspflat[diagorder]) if x == 1)
|
|
579
|
+
idx = diagorder[permidx]
|
|
580
|
+
vname = self.idx2Usymbol[idx]
|
|
581
|
+
varvalue = numpy.dot(Uijflat, Uspflat) / Uspnorm2
|
|
582
|
+
self.Uparameters.append((vname, varvalue))
|
|
583
|
+
return
|
|
584
|
+
|
|
585
|
+
def _findeqUij(self):
|
|
586
|
+
"""Adjust `self.Uij` and `self.eqUij` to be consistent with spacegroup."""
|
|
587
|
+
self.Uij = numpy.zeros((3, 3), dtype=float)
|
|
588
|
+
for i in range(len(self.Uparameters)):
|
|
589
|
+
Usp = self.Uspace[i]
|
|
590
|
+
varvalue = self.Uparameters[i][1]
|
|
591
|
+
self.Uij += varvalue * Usp
|
|
592
|
+
# now determine eqUij
|
|
593
|
+
for ops in self.symops:
|
|
594
|
+
# take first rotation matrix
|
|
595
|
+
R = ops[0].R
|
|
596
|
+
Rt = R.transpose()
|
|
597
|
+
self.eqUij.append(numpy.dot(R, numpy.dot(self.Uij, Rt)))
|
|
598
|
+
return
|
|
599
|
+
|
|
600
|
+
def positionFormula(self, pos, xyzsymbols=("x", "y", "z")):
|
|
601
|
+
"""Formula of equivalent position with respect to generator site.
|
|
602
|
+
|
|
603
|
+
Parameters
|
|
604
|
+
----------
|
|
605
|
+
pos : array_like
|
|
606
|
+
Fractional coordinates of possibly equivalent site.
|
|
607
|
+
xyzsymbols : tuple, Optional
|
|
608
|
+
Symbols for parametrized coordinates.
|
|
609
|
+
|
|
610
|
+
Return
|
|
611
|
+
------
|
|
612
|
+
dict
|
|
613
|
+
Position formulas in a dictionary with keys equal ``("x", "y", "z")``
|
|
614
|
+
or an empty dictionary when pos is not equivalent to generator.
|
|
615
|
+
Formulas are formatted as ``[[-][%g*]{x|y|z}] [{+|-}%g]``, for example
|
|
616
|
+
``-x``, ``z +0.5``, ``0.25``.
|
|
617
|
+
"""
|
|
618
|
+
# find pos in eqxyz
|
|
619
|
+
idx = nearestSiteIndex(self.eqxyz, pos)
|
|
620
|
+
eqpos = self.eqxyz[idx]
|
|
621
|
+
if not equalPositions(eqpos, pos, self.eps):
|
|
622
|
+
return {}
|
|
623
|
+
# any rotation matrix should do fine
|
|
624
|
+
R = self.symops[idx][0].R
|
|
625
|
+
nsrotated = numpy.dot(self.null_space, numpy.transpose(R))
|
|
626
|
+
# build formulas using eqpos
|
|
627
|
+
# find offset
|
|
628
|
+
teqpos = numpy.array(eqpos)
|
|
629
|
+
for nvec, (vname, varvalue) in zip(nsrotated, self.pparameters):
|
|
630
|
+
teqpos -= nvec * varvalue
|
|
631
|
+
# map varnames to xyzsymbols
|
|
632
|
+
name2sym = dict(zip(("x", "y", "z"), xyzsymbols))
|
|
633
|
+
xyzformula = 3 * [""]
|
|
634
|
+
for nvec, (vname, ignore) in zip(nsrotated, self.pparameters):
|
|
635
|
+
for i in range(3):
|
|
636
|
+
if abs(nvec[i]) < epsilon:
|
|
637
|
+
continue
|
|
638
|
+
xyzformula[i] += "%s*%s " % (self.signedRatStr(nvec[i]), name2sym[vname])
|
|
639
|
+
# add constant offset teqpos to all formulas
|
|
640
|
+
for i in range(3):
|
|
641
|
+
if xyzformula[i] and abs(teqpos[i]) < epsilon:
|
|
642
|
+
continue
|
|
643
|
+
xyzformula[i] += self.signedRatStr(teqpos[i])
|
|
644
|
+
# reduce unnecessary +1* and -1*
|
|
645
|
+
xyzformula = [re.sub("^[+]1[*]|(?<=[+-])1[*]", "", f).strip() for f in xyzformula]
|
|
646
|
+
return dict(zip(("x", "y", "z"), xyzformula))
|
|
647
|
+
|
|
648
|
+
def UFormula(self, pos, Usymbols=stdUsymbols):
|
|
649
|
+
"""List of atom displacement formulas with custom parameter symbols.
|
|
650
|
+
|
|
651
|
+
Parameters
|
|
652
|
+
----------
|
|
653
|
+
pos : array_like
|
|
654
|
+
Fractional coordinates of possibly equivalent site.
|
|
655
|
+
Usymbols : list, Optional
|
|
656
|
+
6 symbols for possible U matrix parameters, default is
|
|
657
|
+
``["U11", "U22", "U33", "U12", "U13", "U23"]``.
|
|
658
|
+
|
|
659
|
+
Return
|
|
660
|
+
------
|
|
661
|
+
Uformula : dict
|
|
662
|
+
U element formulas in a dictionary where keys are from
|
|
663
|
+
``('U11','U22','U33','U12','U13','U23')`` or empty dictionary when
|
|
664
|
+
pos is not equivalent to generator.
|
|
665
|
+
"""
|
|
666
|
+
# find pos in eqxyz
|
|
667
|
+
idx = nearestSiteIndex(self.eqxyz, pos)
|
|
668
|
+
eqpos = self.eqxyz[idx]
|
|
669
|
+
if not equalPositions(eqpos, pos, self.eps):
|
|
670
|
+
return {}
|
|
671
|
+
# any rotation matrix should do fine
|
|
672
|
+
R = self.symops[idx][0].R
|
|
673
|
+
Rt = R.transpose()
|
|
674
|
+
Usrotated = [numpy.dot(R, numpy.dot(Us, Rt)) for Us in self.Uspace]
|
|
675
|
+
Uformula = dict.fromkeys(stdUsymbols, "")
|
|
676
|
+
name2sym = dict(zip(stdUsymbols, Usymbols))
|
|
677
|
+
for Usr, (vname, ignore) in zip(Usrotated, self.Uparameters):
|
|
678
|
+
# avoid adding off-diagonal elements twice
|
|
679
|
+
assert numpy.all(Usr == Usr.T)
|
|
680
|
+
Usr -= numpy.tril(Usr, -1)
|
|
681
|
+
Usrflat = Usr.flatten()
|
|
682
|
+
for i in numpy.where(Usrflat)[0]:
|
|
683
|
+
f = "%+g*%s" % (Usrflat[i], name2sym[vname])
|
|
684
|
+
smbl = self.idx2Usymbol[i]
|
|
685
|
+
Uformula[smbl] += f
|
|
686
|
+
for smbl, f in Uformula.items():
|
|
687
|
+
if not f:
|
|
688
|
+
f = "0"
|
|
689
|
+
f = re.sub(r"^[+]?1[*]|^[+](?=\d)|(?<=[+-])1[*]", "", f).strip()
|
|
690
|
+
Uformula[smbl] = f
|
|
691
|
+
return Uformula
|
|
692
|
+
|
|
693
|
+
def eqIndex(self, pos):
|
|
694
|
+
"""Index of the nearest generator equivalent site.
|
|
695
|
+
|
|
696
|
+
Parameters
|
|
697
|
+
----------
|
|
698
|
+
pos : array_like
|
|
699
|
+
Fractional coordinates.
|
|
700
|
+
|
|
701
|
+
Return
|
|
702
|
+
------
|
|
703
|
+
int
|
|
704
|
+
Index of the nearest generator equivalent site.
|
|
705
|
+
"""
|
|
706
|
+
return nearestSiteIndex(self.eqxyz, pos)
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
# End of class GeneratorSite
|
|
710
|
+
|
|
711
|
+
# ----------------------------------------------------------------------------
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
class ExpandAsymmetricUnit(object):
|
|
715
|
+
"""Expand asymmetric unit and anisotropic thermal displacement.
|
|
716
|
+
|
|
717
|
+
Parameters
|
|
718
|
+
----------
|
|
719
|
+
spacegroup : SpaceGroup
|
|
720
|
+
Instance of `SpaceGroup`.
|
|
721
|
+
corepos : array_like
|
|
722
|
+
List of positions in asymmetric unit,
|
|
723
|
+
it may contain duplicates.
|
|
724
|
+
coreUijs : numpy.ndarray, Optional
|
|
725
|
+
Thermal factors for `corepos`.
|
|
726
|
+
sgoffset : list, Optional
|
|
727
|
+
Offset of space group origin ``[0, 0, 0]``. Default is ``[0, 0, 0]``.
|
|
728
|
+
eps : float, Optional
|
|
729
|
+
Cutoff for duplicate positions. Default is ``1.0e-5``.
|
|
730
|
+
|
|
731
|
+
Attributes
|
|
732
|
+
----------
|
|
733
|
+
spacegroup : SpaceGroup
|
|
734
|
+
Instance of `SpaceGroup`.
|
|
735
|
+
corepos : array_like
|
|
736
|
+
List of positions in asymmetric unit,
|
|
737
|
+
it may contain duplicates.
|
|
738
|
+
coreUijs : numpy.ndarray
|
|
739
|
+
Thermal factors for `corepos`. Defaults to zeros.
|
|
740
|
+
sgoffset : numpy.ndarray
|
|
741
|
+
Offset of space group origin ``[0, 0, 0]``. Default to zeros.
|
|
742
|
+
eps : float
|
|
743
|
+
Cutoff for equivalent positions. Default is ``1.0e-5``.
|
|
744
|
+
multiplicity : list
|
|
745
|
+
Multiplicity of each site in `corepos`.
|
|
746
|
+
Uisotropy : list
|
|
747
|
+
Bool flags for isotropic sites in `corepos`.
|
|
748
|
+
expandedpos : list
|
|
749
|
+
List of equivalent positions per each site in `corepos`.
|
|
750
|
+
expandedUijs : list
|
|
751
|
+
List of thermal factors per each site in `corepos`.
|
|
752
|
+
"""
|
|
753
|
+
|
|
754
|
+
# By design Atom instances are not accepted as arguments to keep
|
|
755
|
+
# number of required imports low.
|
|
756
|
+
def __init__(self, spacegroup, corepos, coreUijs=None, sgoffset=[0, 0, 0], eps=None):
|
|
757
|
+
if eps is None:
|
|
758
|
+
eps = epsilon
|
|
759
|
+
# declare data members
|
|
760
|
+
self.spacegroup = spacegroup
|
|
761
|
+
self.corepos = corepos
|
|
762
|
+
self.coreUijs = None
|
|
763
|
+
self.sgoffset = numpy.array(sgoffset)
|
|
764
|
+
self.eps = eps
|
|
765
|
+
self.multiplicity = []
|
|
766
|
+
self.Uisotropy = []
|
|
767
|
+
self.expandedpos = []
|
|
768
|
+
self.expandedUijs = []
|
|
769
|
+
# obtain their values
|
|
770
|
+
corelen = len(self.corepos)
|
|
771
|
+
if coreUijs:
|
|
772
|
+
self.coreUijs = coreUijs
|
|
773
|
+
else:
|
|
774
|
+
self.coreUijs = numpy.zeros((corelen, 3, 3), dtype=float)
|
|
775
|
+
for cpos, cUij in zip(self.corepos, self.coreUijs):
|
|
776
|
+
gen = GeneratorSite(self.spacegroup, cpos, cUij, self.sgoffset, self.eps)
|
|
777
|
+
self.multiplicity.append(gen.multiplicity)
|
|
778
|
+
self.Uisotropy.append(gen.Uisotropy)
|
|
779
|
+
self.expandedpos.append(gen.eqxyz)
|
|
780
|
+
self.expandedUijs.append(gen.eqUij)
|
|
781
|
+
return
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
# End of class ExpandAsymmetricUnit
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
# Helper function for SymmetryConstraints class. It may be useful
|
|
788
|
+
# elsewhere therefore its name does not start with underscore.
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
def pruneFormulaDictionary(eqdict):
|
|
792
|
+
"""Remove constant items from formula dictionary.
|
|
793
|
+
|
|
794
|
+
Parameters
|
|
795
|
+
----------
|
|
796
|
+
eqdict : dict
|
|
797
|
+
Formula dictionary which maps standard variable symbols
|
|
798
|
+
``("x", "U11")`` to string formulas ``("0", "-x3", "z7 +0.5")``.
|
|
799
|
+
|
|
800
|
+
Return
|
|
801
|
+
------
|
|
802
|
+
dict
|
|
803
|
+
Pruned formula dictionary.
|
|
804
|
+
"""
|
|
805
|
+
pruned = {}
|
|
806
|
+
for smb, eq in eqdict.items():
|
|
807
|
+
if not isconstantFormula(eq):
|
|
808
|
+
pruned[smb] = eq
|
|
809
|
+
return pruned
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
class SymmetryConstraints(object):
|
|
813
|
+
"""Generate symmetry constraints for specified positions.
|
|
814
|
+
|
|
815
|
+
Parameters
|
|
816
|
+
----------
|
|
817
|
+
spacegroup : SpaceGroup
|
|
818
|
+
Instance of `SpaceGroup`.
|
|
819
|
+
positions : array_like
|
|
820
|
+
List of all positions to be constrained.
|
|
821
|
+
Uijs : array_like, Optional
|
|
822
|
+
List of U matrices for all constrained positions.
|
|
823
|
+
sgoffset : list, Optional
|
|
824
|
+
Offset of space group origin ``[0, 0, 0]``. Default is ``[0, 0, 0]``.
|
|
825
|
+
eps : float, Optional
|
|
826
|
+
Cutoff for duplicate positions. Default is ``1.0e-5``.
|
|
827
|
+
|
|
828
|
+
Attributes
|
|
829
|
+
----------
|
|
830
|
+
spacegroup : SpaceGroup
|
|
831
|
+
Instance of `SpaceGroup`.
|
|
832
|
+
positions : numpy.ndarray
|
|
833
|
+
All positions to be constrained.
|
|
834
|
+
Uijs : numpy.ndarray
|
|
835
|
+
Thermal factors for all positions. Defaults to zeros.
|
|
836
|
+
sgoffset : numpy.ndarray
|
|
837
|
+
Optional offset of space group origin ``[0, 0, 0]``.
|
|
838
|
+
eps : float
|
|
839
|
+
Cutoff for equivalent positions. Default is ``1.0e-5``.
|
|
840
|
+
corepos : list
|
|
841
|
+
List of of positions in the asymmetric unit.
|
|
842
|
+
coremap : dict
|
|
843
|
+
Dictionary mapping indices of asymmetric core positions
|
|
844
|
+
to indices of all symmetry related positions.
|
|
845
|
+
poseqns : list
|
|
846
|
+
List of coordinate formula dictionaries per each site.
|
|
847
|
+
Formula dictionary keys are from ``("x", "y", "z")`` and
|
|
848
|
+
the values are formatted as ``[[-]{x|y|z}%i] [{+|-}%g]``,
|
|
849
|
+
for example: ``x0``, ``-x3``, ``z7 +0.5``, ``0.25``.
|
|
850
|
+
pospars : list
|
|
851
|
+
List of ``(xyz symbol, value)`` pairs.
|
|
852
|
+
Ueqns : list
|
|
853
|
+
List of anisotropic atomic displacement formula
|
|
854
|
+
dictionaries per each position. Formula dictionary
|
|
855
|
+
keys are from ``('U11','U22','U33','U12','U13','U23')``
|
|
856
|
+
and the values are formatted as ``{[%g*][Uij%i]|0}``,
|
|
857
|
+
for example: ``U110``, ``0.5*U2213``, ``0``.
|
|
858
|
+
Upars : list
|
|
859
|
+
List of ``(U symbol, value)`` pairs.
|
|
860
|
+
Uisotropy : list
|
|
861
|
+
List of bool flags for isotropic thermal displacements.
|
|
862
|
+
"""
|
|
863
|
+
|
|
864
|
+
def __init__(self, spacegroup, positions, Uijs=None, sgoffset=[0, 0, 0], eps=None):
|
|
865
|
+
if eps is None:
|
|
866
|
+
eps = epsilon
|
|
867
|
+
# fill in data members
|
|
868
|
+
self.spacegroup = spacegroup
|
|
869
|
+
self.positions = None
|
|
870
|
+
self.Uijs = None
|
|
871
|
+
self.sgoffset = numpy.array(sgoffset)
|
|
872
|
+
self.eps = eps
|
|
873
|
+
self.corepos = []
|
|
874
|
+
self.coremap = {}
|
|
875
|
+
self.poseqns = None
|
|
876
|
+
self.pospars = []
|
|
877
|
+
self.Ueqns = None
|
|
878
|
+
self.Upars = []
|
|
879
|
+
self.Uisotropy = None
|
|
880
|
+
# handle list of lists returned by ExpandAsymmetricUnit
|
|
881
|
+
if len(positions) and isinstance(positions[0], list):
|
|
882
|
+
# concatenate lists before converting to Nx3 array
|
|
883
|
+
flatpos = sum(positions, [])
|
|
884
|
+
flatpos = numpy.array(flatpos, dtype=float).flatten()
|
|
885
|
+
self.positions = flatpos.reshape((-1, 3))
|
|
886
|
+
# otherwise convert to array
|
|
887
|
+
else:
|
|
888
|
+
flatpos = numpy.array(positions, dtype=float).flatten()
|
|
889
|
+
self.positions = flatpos.reshape((-1, 3))
|
|
890
|
+
# here self.positions should be a 2D numpy array
|
|
891
|
+
numpos = len(self.positions)
|
|
892
|
+
# adjust Uijs if not specified
|
|
893
|
+
if Uijs is not None:
|
|
894
|
+
self.Uijs = numpy.array(Uijs, dtype=float)
|
|
895
|
+
else:
|
|
896
|
+
self.Uijs = numpy.zeros((numpos, 3, 3), dtype=float)
|
|
897
|
+
self.poseqns = numpos * [None]
|
|
898
|
+
self.Ueqns = numpos * [None]
|
|
899
|
+
self.Uisotropy = numpos * [False]
|
|
900
|
+
# all members should be initialized here
|
|
901
|
+
self._findConstraints()
|
|
902
|
+
return
|
|
903
|
+
|
|
904
|
+
def _findConstraints(self):
|
|
905
|
+
"""Find constraints for positions and anisotropic displacements `Uij`."""
|
|
906
|
+
numpos = len(self.positions)
|
|
907
|
+
# canonical xyzsymbols and Usymbols
|
|
908
|
+
xyzsymbols = [smbl + str(i) for i in range(numpos) for smbl in "xyz"]
|
|
909
|
+
Usymbols = [smbl + str(i) for i in range(numpos) for smbl in stdUsymbols]
|
|
910
|
+
independent = set(range(numpos))
|
|
911
|
+
for genidx in range(numpos):
|
|
912
|
+
if genidx not in independent:
|
|
913
|
+
continue
|
|
914
|
+
# it is a generator
|
|
915
|
+
self.coremap[genidx] = []
|
|
916
|
+
genpos = self.positions[genidx]
|
|
917
|
+
genUij = self.Uijs[genidx]
|
|
918
|
+
gen = GeneratorSite(self.spacegroup, genpos, genUij, self.sgoffset, self.eps)
|
|
919
|
+
# append new pparameters if there are any
|
|
920
|
+
gxyzsymbols = xyzsymbols[3 * genidx : 3 * (genidx + 1)]
|
|
921
|
+
for k, v in gen.pparameters:
|
|
922
|
+
smbl = gxyzsymbols["xyz".index(k)]
|
|
923
|
+
self.pospars.append((smbl, v))
|
|
924
|
+
gUsymbols = Usymbols[6 * genidx : 6 * (genidx + 1)]
|
|
925
|
+
for k, v in gen.Uparameters:
|
|
926
|
+
smbl = gUsymbols[stdUsymbols.index(k)]
|
|
927
|
+
self.Upars.append((smbl, v))
|
|
928
|
+
# search for equivalents inside indies
|
|
929
|
+
indies = sorted(independent)
|
|
930
|
+
for indidx in indies:
|
|
931
|
+
indpos = self.positions[indidx]
|
|
932
|
+
formula = gen.positionFormula(indpos, gxyzsymbols)
|
|
933
|
+
# formula is empty when indidx is independent
|
|
934
|
+
if not formula:
|
|
935
|
+
continue
|
|
936
|
+
# indidx is dependent here
|
|
937
|
+
independent.remove(indidx)
|
|
938
|
+
self.coremap[genidx].append(indidx)
|
|
939
|
+
self.poseqns[indidx] = formula
|
|
940
|
+
self.Ueqns[indidx] = gen.UFormula(indpos, gUsymbols)
|
|
941
|
+
# make sure positions and Uijs are consistent with spacegroup
|
|
942
|
+
eqidx = gen.eqIndex(indpos)
|
|
943
|
+
dxyz = gen.eqxyz[eqidx] - indpos
|
|
944
|
+
self.positions[indidx] += dxyz - dxyz.round()
|
|
945
|
+
self.Uijs[indidx] = gen.eqUij[eqidx]
|
|
946
|
+
self.Uisotropy[indidx] = gen.Uisotropy
|
|
947
|
+
# all done here
|
|
948
|
+
coreidx = sorted(self.coremap.keys())
|
|
949
|
+
self.corepos = [self.positions[i] for i in coreidx]
|
|
950
|
+
return
|
|
951
|
+
|
|
952
|
+
def posparSymbols(self):
|
|
953
|
+
"""Return list of standard position parameter symbols."""
|
|
954
|
+
return [n for n, v in self.pospars]
|
|
955
|
+
|
|
956
|
+
def posparValues(self):
|
|
957
|
+
"""Return list of position parameters values."""
|
|
958
|
+
return [v for n, v in self.pospars]
|
|
959
|
+
|
|
960
|
+
def positionFormulas(self, xyzsymbols=None):
|
|
961
|
+
"""List of position formulas with custom parameter symbols.
|
|
962
|
+
|
|
963
|
+
Parameters
|
|
964
|
+
----------
|
|
965
|
+
xyzsymbols : list, Optional
|
|
966
|
+
List of custom symbols used in formula strings.
|
|
967
|
+
|
|
968
|
+
Return
|
|
969
|
+
------
|
|
970
|
+
list
|
|
971
|
+
List of coordinate formulas dictionaries. Formulas dictionary
|
|
972
|
+
keys are from ``("x", "y", "z")`` and the values are formatted as
|
|
973
|
+
``[[-]{symbol}] [{+|-}%g]``, for example: ``x0``, ``-sym``, ``@7 +0.5``, ``0.25``.
|
|
974
|
+
"""
|
|
975
|
+
if not xyzsymbols:
|
|
976
|
+
return list(self.poseqns)
|
|
977
|
+
# check xyzsymbols
|
|
978
|
+
if len(xyzsymbols) < len(self.pospars):
|
|
979
|
+
emsg = "Not enough symbols for %i position parameters" % len(self.pospars)
|
|
980
|
+
raise SymmetryError(emsg)
|
|
981
|
+
# build translation dictionary
|
|
982
|
+
trsmbl = dict(zip(self.posparSymbols(), xyzsymbols))
|
|
983
|
+
|
|
984
|
+
def translatesymbol(matchobj):
|
|
985
|
+
return trsmbl[matchobj.group(0)]
|
|
986
|
+
|
|
987
|
+
pat = re.compile(r"\b[xyz]\d+")
|
|
988
|
+
rv = []
|
|
989
|
+
for eqns in self.poseqns:
|
|
990
|
+
treqns = {}
|
|
991
|
+
for smbl, eq in eqns.items():
|
|
992
|
+
treqns[smbl] = re.sub(pat, translatesymbol, eq)
|
|
993
|
+
rv.append(treqns)
|
|
994
|
+
return rv
|
|
995
|
+
|
|
996
|
+
def positionFormulasPruned(self, xyzsymbols=None):
|
|
997
|
+
"""List of position formula dictionaries with constant items removed.
|
|
998
|
+
|
|
999
|
+
See also
|
|
1000
|
+
--------
|
|
1001
|
+
positionFormulas()
|
|
1002
|
+
|
|
1003
|
+
Parameters
|
|
1004
|
+
----------
|
|
1005
|
+
xyzsymbols : list, Optional
|
|
1006
|
+
List of custom symbols used in formula strings.
|
|
1007
|
+
|
|
1008
|
+
Return
|
|
1009
|
+
------
|
|
1010
|
+
list
|
|
1011
|
+
List of coordinate formula dictionaries.
|
|
1012
|
+
"""
|
|
1013
|
+
rv = [pruneFormulaDictionary(eqns) for eqns in self.positionFormulas(xyzsymbols)]
|
|
1014
|
+
return rv
|
|
1015
|
+
|
|
1016
|
+
def UparSymbols(self):
|
|
1017
|
+
"""Return list of standard atom displacement parameter symbols."""
|
|
1018
|
+
return [n for n, v in self.Upars]
|
|
1019
|
+
|
|
1020
|
+
def UparValues(self):
|
|
1021
|
+
"""Return list of atom displacement parameters values."""
|
|
1022
|
+
return [v for n, v in self.Upars]
|
|
1023
|
+
|
|
1024
|
+
def UFormulas(self, Usymbols=None):
|
|
1025
|
+
"""List of atom displacement formulas with custom parameter symbols.
|
|
1026
|
+
|
|
1027
|
+
Parameters
|
|
1028
|
+
----------
|
|
1029
|
+
Usymbols : list, Optional
|
|
1030
|
+
List of custom symbols used in formula strings.
|
|
1031
|
+
|
|
1032
|
+
Return
|
|
1033
|
+
------
|
|
1034
|
+
list
|
|
1035
|
+
List of atom displacement formula dictionaries per each site.
|
|
1036
|
+
Formula dictionary keys are from ``('U11','U22','U33','U12','U13','U23')``
|
|
1037
|
+
and the values are formatted as ``{[%g*][Usymbol]|0}``, for example:
|
|
1038
|
+
``U11``, ``0.5*@37``, ``0``.
|
|
1039
|
+
"""
|
|
1040
|
+
if not Usymbols:
|
|
1041
|
+
return list(self.Ueqns)
|
|
1042
|
+
# check Usymbols
|
|
1043
|
+
if len(Usymbols) < len(self.Upars):
|
|
1044
|
+
emsg = "Not enough symbols for %i U parameters" % len(self.Upars)
|
|
1045
|
+
raise SymmetryError(emsg)
|
|
1046
|
+
# build translation dictionary
|
|
1047
|
+
trsmbl = dict(zip(self.UparSymbols(), Usymbols))
|
|
1048
|
+
|
|
1049
|
+
def translatesymbol(matchobj):
|
|
1050
|
+
return trsmbl[matchobj.group(0)]
|
|
1051
|
+
|
|
1052
|
+
pat = re.compile(r"\bU\d\d\d+")
|
|
1053
|
+
rv = []
|
|
1054
|
+
for eqns in self.Ueqns:
|
|
1055
|
+
treqns = {}
|
|
1056
|
+
for smbl, eq in eqns.items():
|
|
1057
|
+
treqns[smbl] = re.sub(pat, translatesymbol, eq)
|
|
1058
|
+
rv.append(treqns)
|
|
1059
|
+
return rv
|
|
1060
|
+
|
|
1061
|
+
def UFormulasPruned(self, Usymbols=None):
|
|
1062
|
+
"""List of atom displacement formula dictionaries with constant items
|
|
1063
|
+
removed.
|
|
1064
|
+
|
|
1065
|
+
See Also
|
|
1066
|
+
--------
|
|
1067
|
+
UFormulas()
|
|
1068
|
+
|
|
1069
|
+
Parameters
|
|
1070
|
+
----------
|
|
1071
|
+
Usymbols : list, Optional
|
|
1072
|
+
List of custom symbols used in formula strings.
|
|
1073
|
+
|
|
1074
|
+
Return
|
|
1075
|
+
------
|
|
1076
|
+
list
|
|
1077
|
+
List of atom displacement formulas in tuples of
|
|
1078
|
+
``(U11, U22, U33, U12, U13, U23)``.
|
|
1079
|
+
"""
|
|
1080
|
+
rv = [pruneFormulaDictionary(eqns) for eqns in self.UFormulas(Usymbols)]
|
|
1081
|
+
return rv
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
# End of class SymmetryConstraints
|
|
1085
|
+
|
|
1086
|
+
# ----------------------------------------------------------------------------
|
|
1087
|
+
|
|
1088
|
+
# basic demonstration
|
|
1089
|
+
if __name__ == "__main__":
|
|
1090
|
+
from diffpy.structure.spacegroups import sg100
|
|
1091
|
+
|
|
1092
|
+
site = [0.125, 0.625, 0.13]
|
|
1093
|
+
Uij = [[1, 2, 3], [2, 4, 5], [3, 5, 6]]
|
|
1094
|
+
g = GeneratorSite(sg100, site, Uij=Uij)
|
|
1095
|
+
fm100 = g.positionFormula(site)
|
|
1096
|
+
print("g = GeneratorSite(sg100, %r)" % site)
|
|
1097
|
+
print("g.positionFormula(%r) = %s" % (site, fm100))
|
|
1098
|
+
print("g.pparameters =", g.pparameters)
|
|
1099
|
+
print("g.Uparameters =", g.Uparameters)
|
|
1100
|
+
print("g.UFormula(%r) =" % site, g.UFormula(site))
|