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.
Files changed (42) hide show
  1. diffpy/Structure.py +35 -0
  2. diffpy/__init__.py +23 -0
  3. diffpy/structure/__init__.py +93 -0
  4. diffpy/structure/_legacy_importer.py +88 -0
  5. diffpy/structure/apps/__init__.py +17 -0
  6. diffpy/structure/apps/anyeye.py +284 -0
  7. diffpy/structure/apps/transtru.py +126 -0
  8. diffpy/structure/atom.py +544 -0
  9. diffpy/structure/expansion/__init__.py +27 -0
  10. diffpy/structure/expansion/makeellipsoid.py +129 -0
  11. diffpy/structure/expansion/shapeutils.py +44 -0
  12. diffpy/structure/expansion/supercell_mod.py +91 -0
  13. diffpy/structure/lattice.py +663 -0
  14. diffpy/structure/mmlibspacegroups.py +8154 -0
  15. diffpy/structure/parsers/__init__.py +83 -0
  16. diffpy/structure/parsers/p_auto.py +217 -0
  17. diffpy/structure/parsers/p_cif.py +876 -0
  18. diffpy/structure/parsers/p_discus.py +312 -0
  19. diffpy/structure/parsers/p_pdb.py +405 -0
  20. diffpy/structure/parsers/p_pdffit.py +290 -0
  21. diffpy/structure/parsers/p_rawxyz.py +149 -0
  22. diffpy/structure/parsers/p_xcfg.py +457 -0
  23. diffpy/structure/parsers/p_xyz.py +161 -0
  24. diffpy/structure/parsers/parser_index_mod.py +108 -0
  25. diffpy/structure/parsers/structureparser.py +80 -0
  26. diffpy/structure/pdffitstructure.py +109 -0
  27. diffpy/structure/sgtbxspacegroups.py +5198 -0
  28. diffpy/structure/spacegroupmod.py +329 -0
  29. diffpy/structure/spacegroups.py +1441 -0
  30. diffpy/structure/structure.py +866 -0
  31. diffpy/structure/structureerrors.py +35 -0
  32. diffpy/structure/symmetryutilities.py +1100 -0
  33. diffpy/structure/utils.py +126 -0
  34. diffpy/structure/version.py +26 -0
  35. diffpy.structure-3.2.0.dist-info/AUTHORS.rst +13 -0
  36. diffpy.structure-3.2.0.dist-info/LICENSE.rst +141 -0
  37. diffpy.structure-3.2.0.dist-info/LICENSE_DANSE.rst +50 -0
  38. diffpy.structure-3.2.0.dist-info/LICENSE_pymmlib.rst +203 -0
  39. diffpy.structure-3.2.0.dist-info/METADATA +197 -0
  40. diffpy.structure-3.2.0.dist-info/RECORD +42 -0
  41. diffpy.structure-3.2.0.dist-info/WHEEL +5 -0
  42. 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))