-
Notifications
You must be signed in to change notification settings - Fork 2
/
temCalcs.py
1901 lines (1596 loc) · 83.5 KB
/
temCalcs.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""These scripts serve as the backbone of TEM and crystallography calculations. They mainly serve for tilting help in the TEM."""
import numpy as np
import math as ma
import matplotlib.pyplot as plt
import re
from fractions import Fraction
import math
from scipy.optimize import fsolve
class Constants(object):
def __init__(self):
# Planck's constant
self.h=6.62607004e-34 #m^2*kg/s
# Elementary charge
self.e=1.60217662e-19 #couloms/electron
# Pi
self.pi = np.pi
# speed of light in vacuum
self.c = 299792458 #m/s
# electron rest mass
self.m0 = 9.10938356e-31 #kg
def gamma(self, v):
"""relativistic corrector"""
return 1/np.sqrt(1-v**2/self.c**2)
#Make sure all constants are calable simply from c
c=Constants()
#At the moment not all that in favor of a structureManager. So all available materials/structures are handled globally in the structures dictionary:
structures = {}
def addStructure(name = "structure"+str(len(structures)), **kwargs): ##doesn't add another structure if the name already exists but creates a new structure object
if name is None or name == "":
name = "structure"+str(len(structures))
structures[name] = Structure(name, **kwargs)
return structures[name]
def removeStructure(name):
#we also have to get rid of all the potential crystals that have this structure
#loop over the microscopes
for i in list(microscopes.keys()):
micr = getMicroscope(i)
for j in list(micr.stages.keys()):
stag = micr.getStage(j)
for k in list(stag.crystals.keys()):
crys = stag.getCrystal(k)
if crys.structure.name == name:
stag.removeCrystal(crys.name)
del structures[name]
def getStructure(name):
return structures[name]
def changeStructureName(oldname, newname):
structures[newname]=structures[oldname]
del structures[oldname]
#Slightly Easier ways of defining vectors
def miller(h, k, l):
return np.array([h, k, l])
def vector(h, k, l):
return np.array([h, k, l])
def sizeAngle(mag, direc):
"""Only valid for detector vectors. Returns X, Y, Z=0 given a magnitude and angle(degrees) with the x-axis"""
return mag*vector(np.cos(direc/360*2*np.pi),np.sin(direc/360*2*np.pi),0)
#Another fundamental list of objects is the number of TEMS. From tems, stages, detectors and crystals are defined.
microscopes = {}
def addMicroscope(name = None, **kwargs):
if name is None or name == "":
name = "microscope"+str(len(microscopes))
microscopes[name] = TEM(name, **kwargs)
return microscopes[name]
def removeMicroscope(name):
del microscopes[name]
def getMicroscope(name):
return microscopes[name]
def changeMicroscopeName(oldname, newname):
microscopes[newname]=microscopes[oldname]
del microscopes[oldname]
def saveSession(filname = "session.txt"):
#Open file
f = open(filname+".txt", "w")
#Loop through all structures
for i in list(structures.keys()):
i = getStructure(i)
a, b, c, alpha, beta, gamma = i.getCrystallography()
str = "structure;%s;%s;%s;%s;%s;%s;%s;" %(i.name, a, b, c, alpha, beta, gamma)
f.write(str+"\n")
#Loop through all microscopes
for i in list(microscopes.keys()):
i = getMicroscope(i)
str = "microscope;%s;%s;" %(i.name, i.getKv())
f.write(str+"\n")
#loop through all stages
for j in list(i.stages.keys()):
j = i.getStage(j)
str = "stage;%s;%s;%s;%s;%s;%s;%s;%s;%s;%s;" %(i.name, j.name, j.getAlpha(), j.getBeta(), j.getAlphaMin(), j.getAlphaMax(), j.getBetaMin(), j.getBetaMax(), j.alpharev, j.betarev)
f.write(str+"\n")
#loop through all the crystals
for k in list(j.crystals.keys()):
k = j.getCrystal(k)
str = "crystal;%s;%s;%s;%s;%s;%s;" %(i.name, j.name, k.name, k.getStructure().name, k.dumpOrient(), k.getComment())
f.write(str+"\n")
#loop through all detectors
for j in list(i.detectors.keys()):
j = i.getDetector(j)
str = "detector;%s;%s;imaging;%s;diffraction;%s;stem;%s;" %(i.name, j.name, j.getCalFileName("imaging"), j.getCalFileName("diffraction"), j.getCalFileName("stem"))
f.write(str+"\n")
f.close()
def clearSession():
"""Delete all objects (microscopes and structures)"""
microscopes.clear()
structures.clear()
def loadSession(filname = "session.txt"):
#Open file
f = open(filname)
text = f.readlines()
for i in text:
info = i.replace("\n","").split(";")
if info[0].lower() == "structure":
##create a structure
addStructure(name = info[1].strip(), a = float(info[2]), b = float(info[3]), c = float(info[4]), alpha = float(info[5]), beta = float(info[6]), gamma = float(info[7]))
elif info[0].lower() == "microscope":
addMicroscope(name = info[1].strip(), kv=float(info[2]))
elif info[0].lower() == "stage":
micr = getMicroscope(info[1].strip())
micr.addStage(name = info[2].strip(), alpha = float(info[3]), beta = float(info[4]), alphamin = float(info[5]), alphamax = float(info[6]), betamin = float(info[7]), betamax = float(info[8]), alpharev = info[9]=="True", betarev = info[10]=="True")
elif info[0].lower() == "detector":
micr = getMicroscope(info[1].strip())
ccd = micr.addDetector(name=info[2].strip())
#check if the calibrations were ever made
if info[4]: #imaging
ccd.setCalibration(info[4], mode = "imaging", type = "r")
if info[6]: #diffraction
ccd.setCalibration(info[6], mode = "diffraction", type = "r")
if info[8]: #stem
ccd.setCalibration(info[8], mode = "stem", type = "r")
elif info[0].lower() == "crystal":
micr = getMicroscope(info[1].strip())
stag = micr.getStage(info[2].strip())
crl = stag.addCrystal(info[4].strip(), info[3])
crl.reconstructOrient(info[5])
crl.setComment(info[6])
else:
pass
f.close()
"""Rotation matrixes"""
def XR(d, rnd=10):
d=float(d)/360*2*np.pi
mat=np.array([[1, 0, 0],[0, round(np.cos(d), rnd), round(-np.sin(d), rnd)], [0, round(np.sin(d), rnd), round(np.cos(d), rnd)]])
return mat
def YR(d, rnd=10):
d=float(d)/360*2*np.pi
mat=np.array([[round(np.cos(d), rnd), 0, round(np.sin(d), rnd)], [0, 1, 0], [round(-np.sin(d), rnd), 0, round(np.cos(d), rnd)]])
return mat
def ZR(d, rnd=10):
d=float(d)/360*2*np.pi
mat=np.array([[round(np.cos(d), rnd), round(-np.sin(d), rnd), 0], [round(np.sin(d), rnd), round(np.cos(d), rnd), 0], [0, 0, 1]])
return mat
def axisAngle(axis, d, rnd=10):
d=float(d)/360*2*np.pi
axis = np.array(normMatrix(axis).T)[0,:]
vx = axis[0]
vy = axis[1]
vz = axis[2]
mat = np.array([[vx**2 + (vy**2+vz**2)*np.cos(d), vx*vy*(1-np.cos(d))-vz*np.sin(d), vx*vz*(1-np.cos(d))+vy*np.sin(d)], [vx*vy*(1-np.cos(d))+vz*np.sin(d), vy**2 + (vx**2+vz**2)*np.cos(d), vy*vz*(1-np.cos(d))-vx*np.sin(d)], [vx*vz*(1-np.cos(d))-vy*np.sin(d), vy*vz*(1-np.cos(d))+vx*np.sin(d), vz**2 + (vx**2+vy**2)*np.cos(d)]])
return mat
def invmat():
return ZR(180)
class TEM(object):
def __init__(self, name, kv=300):
#name of the microscope
self.name = name
#Overpotential in volts
self.v = kv*1e3
self.wavelength = self.calcLambda(self.v)
self.ewaldR = 1/self.wavelength
#detectors is a dictionary name - detector object
self.detectors = {}
#Maybe should leave option open of having more than one stage
self.stages = {}
def setName(self, name):
#also change this in the TEM dictionary
microscopes[name] = microscopes.pop(self.name)
self.name = name
def addStage(self, name = "", **kwargs):
if name == "":
name = "stage"+str(len(self.stages))
self.stages[name] = Stage(self, name = name, **kwargs)
return self.stages[name]
def getStage(self, name):
return self.stages[name]
def removeStage(self, name):
del self.stages[name]
def changeStageName(self, oldname, newname):
self.stages[newname]=self.stages[oldname]
del self.stages[oldname]
def addDetector(self, name="", **kwargs):
if name == "":
name = "detector"+str(len(self.detectors))
self.detectors[name] = Detector(self, name = name, **kwargs)
return self.detectors[name]
def getDetector(self, name):
return self.detectors[name]
def removeDetector(self, name):
del self.detectors[name]
def changeDetectorName(self, oldname, newname):
self.detectors[newname]=self.detectors[oldname]
del self.detectors[oldname]
def setKv(self, kv):
#Overpotential in volts
kv = float(kv)
self.v = kv*1e3
self.wavelength = self.calcLambda(self.v)
self.ewaldR = 1/self.wavelength
def getKv(self):
return self.v/1e3
def getEwaldR(self, units = "m"):
if units == "m":
return self.ewaldR
elif units == "nm":
return self.ewaldR/1e9
elif units == "angstrom":
return self.ewaldR/1e10
else:
print("Those units are not recognized at the moment.")
return none
def getLambda(self):
return self.wavelength
def calcLambda(self, v):
"""Calculates the relativistic electron wavelength from the voltage"""
return c.h/np.sqrt(2*c.m0*c.e*v*(1+c.e*v/(2*c.m0*c.c**2)))
class Stage(object):
"""The stage of the TEM can tilt with alpha and beta and stores its own state."""
def __init__(self, TEM, name = "", alpha =0, beta = 0, alphamin = -30, alphamax = 30, betamin = -20, betamax = 20, alpharev = False, betarev = False):
#the stage needs to know which TEM it belongs to
self.TEM = TEM
self.name = name
self.alpha = alpha
self.beta = beta
self.alow = alphamin
self.atop = alphamax
self.blow = betamin
self.btop = betamax
self.alpharev = alpharev
self.betarev = betarev
#list of crystals (this determines how the crystal is oriented with respect to the stage)
self.crystals = {}
#it should be possible to store interesting orientations as name - [alpha, beta]
self.orientations = {}
def setName(self, name):
#also change this in the TEM dictionary
self.getTEM().stages[name] = self.getTEM().stages.pop(self.name)
self.name = name
def getAlphaMin(self):
return self.alow
def getAlphaMax(self):
return self.atop
def getBetaMin(self):
return self.blow
def getBetaMax(self):
return self.btop
def setAlphaRange(self, *args):
self.alow, self.atop = args
def setBetaRange(self, *args):
self.blow, self.btop = args
def setRev(self, *args):
self.alpharev, self.betarev = args
def getTEM(self):
return self.TEM
def addCrystal(self, structurename, name=""):
if name=="":
name = "crystal"+str(len(self.crystals))
self.crystals[name]=Crystal(name, self, structurename)
return self.crystals[name]
def changeCrystalName(self, oldname, newname):
self.crystals[newname]=self.crystals[oldname]
del self.crystals[oldname]
def removeCrystal(self, name):
del self.crystals[name]
def getCrystal(self, name):
return self.crystals[name]
def getAlpha(self):
return self.alpha
def getBeta(self):
return self.beta
def setAlpha(self, alpha):
self.alpha=alpha
def tiltTo(self, args):
alpha, beta = args
self.alpha = alpha
self.beta = beta
def tiltBy(self, args):
alpha, beta = args
self.alpha += alpha
self.beta += beta
def setBeta(self, beta):
self.beta=beta
def stageToAbs(self, vecs):
"""Turn the absolute coordinates into stage coordinates. This is identical to doing the active rotation. [010]->[0, cos(alpha), sin(alpha)] in stage coordinates with only alpha rotation."""
alpha=float(self.alpha)
beta=float(self.beta)
#first rotate around xf, then rotate around yf. Because yf rotates with xf we have to apply yr first then xr
xx = XR(alpha)
yy = YR(beta)
##Make sure reversed or not is considered in definining the rotation matrixes
#this correction made 18/03/12
if self.alpharev:
xx = XR(-alpha)
if self.betarev:
yy = YR(-beta)
rot = np.dot(xx, yy)
return np.dot(np.array(rot), np.array(vecs))
def absToStage(self, vecs):
"""Turn stage coordinates into absolute coordinates. This is identical to doing the reverse rotation. [010]->[0, cos(alpha), -sin(alpha)] in real with only alpha rotation."""
alpha=float(self.alpha)
beta=float(self.beta)
xx = XR(-alpha)
yy = YR(-beta)
###This correciton made 18/03/12
if self.alpharev:
xx = XR(alpha)
if self.betarev:
yy = YR(beta)
#The inverse of the absolute to stage
rot = np.dot(yy, xx)
return np.dot(np.array(rot), np.array(vecs))
def inrange(self, a, b, units = "radians"):
if units == "radians":
a=a/(2*np.pi)*360
b=b/(2*np.pi)*360
if units == "degrees":
pass #already good to compare
atop = self.atop
alow = self.alow
btop = self.btop
blow = self.blow
if a<=atop and a>=alow and b<=btop and b>=blow:
return True
else:
return False
def inrangeIncr(self, a, b):
return self.inrange(a + self.alpha/360*(2*np.pi), b + self.beta/360*(2*np.pi))
def calcAlphaBeta(self, tozall, rnd = 10):
"""A general rotation matrix is not possible with just alpha and beta. It can only be arranged that a certain vector is set to the optical axis.
The way to do this was calculated on paper and with sympy. Toz is the vector that needs to be turned on the Z-axis"""
return toAllCols(self.calcAlphaBetaVec, tozall, rnd = rnd)
def calcAlphaBetaVec(self, toz, rnd = 10):
"""18/03/12 The new simplified script to calc the alpha and beta one needs to go to for stage vector toz to be moved to the z-axis"""
#the whole of this script works with radians until the conversion at the end
#normalize
toz = toz/np.linalg.norm(toz)
c = toz[0]#+1e-15
d = toz[1]#+1e-15
f = toz[2]#+1e-15
length = np.sqrt(c**2+d**2+f**2)
#print("[%s %s %s] %s" %(c, d, f, length))
#we have to solve a particular equation, namely tilt(alpha, beta)*stage vec = abstoStage([001]) for alpha and beta
def equation(x, sv): #this is a wrapper for the next equation because the third element is redundant and should not be returned
alpha, beta = x
y = miller(0, 0, 1)
#y = self.absToStage(miller(0, 0, 1))
if self.alpharev:
alpha = -alpha
if self.betarev:
beta = -beta
A = np.dot(XR(alpha), YR(beta))
res = np.dot(A, sv) - y
return res
def tosolve(x, sv):
res = equation(x, sv)
return (res[0], res[1]) #apparently we can't use 3 things to evaluate, the third will be supplied as test
alpha, beta = fsolve(tosolve, (0,0), args = (toz)) #anser is already in degrees
#check the vector is in range
def test(a, b):
#is the third element also 0? - it may have mapped on -1, then the third element will be almost -2
thirdelem = equation((alpha, beta), toz)[2]
return (round(thirdelem,6)==0 and self.inrange(a, b, units = "degrees"))
if test(alpha, beta):
return np.array([np.round(alpha, rnd), np.round(beta, rnd)])
else:
return np.array([np.nan, np.nan])
def calcAlphaBetaVecOld(self, toz, verbose=False, rnd = 10):
"""Calc the alpha and beta one needs to go to for stage vector toz to be moved to the z-axis. This old script investigates all angle possibilities between 0-360 degrees, whereas the new one simplifies it."""
#normalize
toz = toz/np.linalg.norm(toz)
c = toz[0]#+1e-15
d = toz[1]#+1e-15
f = toz[2]#+1e-15
length = np.sqrt(c**2+d**2+f**2)
#there is a 180 degree uncertainty on the answer. If the angle is negative (fourth quadrant) it could be meant for the second quadrant.
beta = np.arctan(-c/f)
beta2 = 0
if beta<0:
beta2 = np.pi + beta
else:
beta2 = -np.pi + beta
#there is an exception: for all vectors in the y-z plane (c=0), beta can be any value. So it is best to set it to 0
if round(c,6)==0:
beta=0
beta2=np.pi
#this results in two possibilities for the solution of the second equation
denom1 = f*np.cos(beta) - c*np.sin(beta)
denom2 = f*np.cos(beta2) - c*np.sin(beta2)
alpha1a = np.arctan(d/denom1)
alpha1b = 0
if alpha1a<0:
alpha1b = np.pi + alpha1a
else:
alpha1b = -np.pi + alpha1a
alpha2a = np.arctan(d/denom2)
alpha2b = 0
if alpha2a<0:
alpha2b = np.pi + alpha2a
else:
alpha2b = -np.pi + alpha2a
#the third equation serves as a test - this needs to be 0 for a correct solution
def test(a, b):
return (np.round(-c*np.sin(b)*np.cos(a) + f*np.cos(a)*np.cos(b) + d*np.sin(a) - length, 5) == 0 and self.inrange(a, b))
possible = []
#test all the possibilities
if test(alpha1a, beta):
if verbose:
print("Option 1 is possible")
possible.append([np.round(alpha1a/(2*np.pi)*360, rnd), np.round(beta/(2*np.pi)*360, rnd)])
if test(alpha1b, beta):
if verbose:
print("Option 2 is possible")
possible.append([round(alpha1b/(2*np.pi)*360, rnd), round(beta/(2*np.pi)*360, rnd)])
if test(alpha2a, beta2):
if verbose:
print("Option 3 is possible")
possible.append([round(alpha2a/(2*np.pi)*360, rnd), round(beta2/(2*np.pi)*360, rnd)])
if test(alpha2b, beta2):
if verbose:
print("Option 4 is possible")
possible.append([round(alpha2b/(2*np.pi)*360, rnd), round(beta2/(2*np.pi)*360, rnd)])
#it is possible that nothing is possible
if not possible:
possible.append([np.nan, np.nan])
#Only return the first option
return np.array(possible[0])
class Detector(object):
"""A detector object stores it's own rotation with respect to the x-axis of the holder."""
def __init__(self, TEM, name = "", diffrot=None, imrot=None, stemrot=None, diffpixcal=None, impixcal=None, stempixcal=None):
#the detector needs to know which TEM it belongs to
self.TEM = TEM
#name
self.name = name
#the rotation of the diffraction pattern
self.diffrot = diffrot
self.diffrotfile = ""
#This is the rotation of the image on any detector except HAADF
self.imrot = imrot
self.imrotfile = ""
#this is the rotation of the HAADF image in STEM or the rotation of the Ronchigram on the TV or GIF CCD
self.stemrot = stemrot
self.stemrotfile = ""
#image mode size calibration
self.impixcal = impixcal
#diffraction mode size calibration
self.diffpixcal = diffpixcal
#Stem size calibration
self.stempixcal = stempixcal
def getMags(self, mod):
if mod == "Diffraction":
return sorted(list(self.diffrot.keys()))
elif mod == "STEM":
return sorted(list(self.stemrot.keys()))
elif mod == "Imaging":
return sorted(list(self.imrot.keys()))
else:
return None
def getTEM(self):
return self.TEM
def getName(self):
return self.name
def setName(self, name):
#also change this in the TEM dictionary
self.getTEM().detectors[name] = self.getTEM().detectors.pop(self.name)
self.name = name
def getCalibration(self, mode):
if mode=="STEM":
return self.stemrot
elif mode == "Diffraction":
return self.diffrot
elif mode == "Imaging":
return self.imrot
else:
return None
def getCalFileName(self, mode):
"""Return the file names of the particular mode"""
if mode == 0 or mode=="diffraction" or mode == "diff" or mode == "d":
return self.diffrotfile
if mode == 1 or mode=="imaging" or mode == "img" or mode=="i":
return self.imrotfile
if mode == 2 or mode=="STEM" or mode == "stem" or mode=="s":
return self.stemrotfile
def setCalibration(self, filename, mode = "diffraction", type = "rotation"):
"""This function sets the correct calibration dictionary in the right variables"""
calib = open(filename).read()
lin = re.compile("([1-9][0-9]*) *\t*: *\t*(\-?[0-9]+\.?[0-9]*) *\t*;")
lst = re.findall(lin, calib)
diction = dict([tuple(map(float, i)) for i in lst])
try:
if mode == 0 or mode=="diffraction" or mode == "diff" or mode == "d":
if type ==0 or type == "rotation" or type == "r" or type =="rot":
self.diffrot = diction
self.diffrotfile = filename
if type ==1 or type == "size" or type == "s" or type =="scale":
self.diffpixcal = diction
if mode == 1 or mode=="imaging" or mode == "img" or mode=="i":
if type ==0 or type == "rotation" or type == "r" or type =="rot":
self.imrot = diction
self.imrotfile = filename
if type ==1 or type == "size" or type == "s" or type =="scale":
self.impixcal = diction
if mode == 2 or mode=="STEM" or mode == "stem" or mode=="s":
if type ==0 or type == "rotation" or type == "r" or type =="rot":
self.stemrot = diction
self.stemrotfile = filename
if type ==1 or type == "size" or type == "s" or type =="scale":
self.stempixcal = diction
except:
print("This is not a valid calibration mode or type.")
def getRot(self, mode, setting):
"""This function gets the rotation angle of the detector. The rotation angle is how the absolute X and Y appear rotated on the detector. This must be tabulated and supplied."""
if mode == 0 or mode=="diffraction" or mode =="Diffraction" or mode == "diff" or mode == "d":
return self.diffrot[setting]
elif mode == 1 or mode=="imaging" or mode == "Imaging" or mode == "img" or mode=="i":
return self.imrot[setting]
elif mode == 2 or mode=="STEM" or mode == "stem" or mode=="s":
return self.stemrot[setting]
else:
print("This detector seems to not have a calibration for this setting")
return 0
def getRealSize(self, val, mode, setting, binning=1):
"""Rescale a vector or scalar quantity by the calibration"""
try:
if mode == 0 or mode=="diffraction" or mode == "diff" or mode == "d":
return val*self.diffpixcal[setting]*binning
if mode == 1 or mode=="imaging" or mode == "img" or mode=="i":
return val*self.impixcal[setting]*binning
if mode == 2 or mode=="STEM" or mode == "stem" or mode=="s":
return val*self.stempixcal[setting]*binning
except:
print("This detector seems to not have a calibration for these settings")
return 0
def detectorToAbs(self, vec, mode, setting):
"""Detector coordinates are transformed to absolute coordinates."""
ang = self.getRot(mode, setting)
#since ZR(ang) maps [100] -> [cos sin 0] which is the active rotation, we desire here the passive rotation -> to map [cos sin 0] (how X looks on the detector) -> [100] (the vector it should represent)
#This involves doing the inverse opperation
rm = ZR(ang).T
return np.dot(rm, vec)
def absToDetector(self, vec, mode, setting):
"""Absolute coordinates are transformed to detector coordinates."""
ang = self.getRot(mode, setting)
#ZR(ang) maps [100] -> [cos sin 0] This is precisely how the absolute X axis looks on the screen.
rm = ZR(ang)
return np.dot(rm, vec)
def plotRealAxes(self, mode, setting):
xaxis = np.array([1, 0, 0]);
yaxis = np.array([0, 1, 0]);
#Does the Z-axis go up the column or down the colunn ? This is to be determined from the way the tilts work. It determines which vector y is.
rotx = self.absToDetector(xaxis, mode, setting)
roty = self.absToDetector(yaxis, mode, setting)
fig, ax = plt.subplots(1)
#plot the x-axis as seen on the screen
ax.arrow(0, 0, rotx[0], rotx[1], color = "red")
#plot the y-axis as seen on the screen
ax.arrow(0, 0, roty[0], roty[1], color = "green")
sc = 1.1
wd = 0.005
#plot the x-axis as seen on the screen
ax.arrow(0, 0, sc*xaxis[0], sc*xaxis[1], width = wd, color = "black")
#plot the y-axis as seen on the screen
ax.arrow(0, 0, sc*yaxis[0], sc*yaxis[1], width = wd, color = "black")
#Label the detector axes
sc2 = 1.2
ax.text(sc2*xaxis[0], sc2*xaxis[1], "X", color = "black")
ax.text(sc2*yaxis[0], sc2*yaxis[1], "Y", color = "black")
plt.gca().set_aspect('equal', adjustable='box')
#Label the tilt axes
sc = 1.1
ax.text(sc*rotx[0], sc*rotx[1], r"$\alpha$", color = "red")
ax.text(sc*roty[0], sc*roty[1], r"$\beta$", color = "green")
#turn off labels of axes
ax.set_yticklabels([])
ax.set_xticklabels([])
sc2 = 1.3
ax.set_xlim([-sc2, sc2])
#this reverses the y-axis
ax.set_ylim([sc2, -sc2])
plt.show()
class Structure(object):
"""The structure class contains all the crystallographic data and calculations. The Crystal class can inherit from this class."""
def __init__(self, name, a=1, b=1, c=1, alpha=90, beta=90, gamma=90):
self.name = name
self.a=a
self.b=b
self.c=c
self.alpha=alpha
self.beta=beta
self.gamma=gamma
#self.typ = self.getCrystalClass()
#RM takes miller indices and transforms them to coordinates in the cartesian system stuck to the crystal
self.RM = self.getRealMatrix()
#invRM takes coordinates defined in the cartesian system stuck to the crystal and maps them to miller indices
self.invRM = np.linalg.inv(self.RM)
#RRM takes miller indices in recyprocal space and maps them to the cartesian system stuck to the crystal
self.RRM = self.getRecypMatrix()
#invRRM takes coordinates defined in the cartesian system stuck to the crystal and maps them to recyprocal space miller indices
self.invRRM = np.linalg.inv(self.RRM)
#direct metric tensor
self.G = self.getGmatrix()
#the recyprocal metric tensor is simply the inverse
self.recypG = np.linalg.inv(self.G)
def setName(self, name):
#also change this in the TEM dictionary
structures[name] = structures.pop(self.name)
self.name = name
def changeCrystallography(self, name, a, b, c, alpha, beta, gamma):
self.setName(name)
self.__init__(name, a=a, b=b, c=c, alpha=alpha, beta=beta, gamma=gamma)
def getCrystallography(self):
return self.a, self.b, self.c, self.alpha, self.beta, self.gamma
def millerToCartesian(self, vec, typ = "real"):
"""This function returns the coordinates in the cartesian coordinate system stuck to the crystal given a set of miller indices as columns or an array. Standard it will be assumed miller indices as defined by the real coordinate system, but recyp is also valid and must be supplied as typ = 'recyp' as argument"""
if typ=="real":
return np.dot(np.array(self.RM), np.array(vec))
elif typ=="recyp" or typ=="recyprocal":
return np.dot(np.array(self.RRM), np.array(vec))
else:
return None
def cartesianToMiller(self, vec, typ = "real"):
"""This function returns the miller indices given coordinates in the cartesian system stuck to the crystal. Standard it will be assumed miller indices as defined by the real coordinate system, but recyp is also valid and must be supplied as typ = 'recyp' as argument"""
if typ=="real":
return np.dot(np.array(self.invRM), np.array(vec))
elif typ=="recyp" or typ=="recyprocal":
return np.dot(np.array(self.invRRM), np.array(vec))
else:
return None
def recypToReal(self, vec):
"""get the coordinates of the real coordinates corresponding to a certain recyprocal space vector"""
#first convert to cartesian
car = self.millerToCartesian(vec, typ="recyp")
#convert the cartesian to real space miller
return self.cartesianToMiller(car)
def realToRecyp(self, vec):
"""get the coordinates of the recyprocal coordinates corresponding to a certain real space vector"""
#first convert to cartesian
car = self.millerToCartesian(vec)
#convert the cartesian to recyp space miller
return self.cartesianToMiller(car, typ="recyp")
@staticmethod
def getCrystalClass(a, b, c, alpha, beta, gamma):
typ = ""
if a==b and b==c and alpha==90 and beta==90 and gamma==90:
typ = "cubic"
elif a==b and alpha==90 and beta==90 and gamma==90:
typ = "tetragonal"
elif alpha==90 and beta==90 and gamma==90:
typ = "orthorhombic"
elif a==b and gamma==120 and alpha==90 and beta==90:
typ = "hexagonal"
elif a==b and b==c and alpha==beta and beta==gamma:
typ = "rhombohedral"
elif gamma == 90 and alpha==90:
typ = "monoclinic"
else:
typ = "triclinic"
return typ
def getRealMatrix(self, rnd=10):
"""returns in the general triclinic case the real space vectors in terms of cartesian axes
The cartesian axes stuck to the crystal is defined as: a is on x-axis, a-b plane is on x-y plane"""
a=float(self.a)
b=float(self.b)
c=float(self.c)
alpha=float(self.alpha)/360*2*np.pi
beta=float(self.beta)/360*2*np.pi
gamma=float(self.gamma)/360*2*np.pi
av=np.array([a, 0, 0])
bv=np.array([round(b*np.cos(gamma), rnd), round(b*np.sin(gamma), rnd), 0])
c1=round(c*np.cos(beta), rnd) #this can be solved from the dot product between c and a vectors
#c2=round(c*(np.cos(alpha)-np.cos(beta)*np.cos(gamma)), rnd) #this can be solved from the dot product between c and b vectors
c2 = (c*b*np.cos(alpha) - c1*bv[0])/(bv[1])
c3=round(np.sqrt(c**2-c1**2-c2**2), rnd)
cv=np.array([c1, c2, c3])
matR=np.array([av, bv, cv]).T
return matR
def getRecypMatrix(self, rnd=10):
"""Matrix containing the basis vectors of the recyprocal lattice. Based on the real matrix."""
a1 = self.RM[:, 0]
a2 = self.RM[:, 1]
a3 = self.RM[:, 2]
vol = np.dot(a1, np.cross(a2, a3))
astar = np.cross(a2, a3)/vol
bstar = np.cross(a3, a1)/vol
cstar = np.cross(a1, a2)/vol
return np.array([astar, bstar, cstar]).T
def getGmatrix(self, rnd=10):
"""metric tensor. This is the same as the RW.T * RW see definition on p 79 marc de graef"""
a=float(self.a)
b=float(self.b)
c=float(self.c)
alpha=float(self.alpha)/360*2*np.pi
beta=float(self.beta)/360*2*np.pi
gamma=float(self.gamma)/360*2*np.pi
mat=np.array([[a**2, round(a*b*np.cos(gamma), rnd), round(a*c*np.cos(beta), rnd)],[round(b*a*np.cos(gamma), rnd), b**2, round(b*c*np.cos(alpha), rnd)],[round(c*a*np.cos(beta), rnd), round(c*b*np.cos(alpha), rnd), c**2]])
return mat
def getSym(self, vec, checksame = False, typ = "real", unc = 1e-9):
"""Returns all the possible permutations of miller indices/vectors as columns in a matrix. Only returns the vectors that are crystallographically identical if checksame is set True. Real and recyprocal vectors are both valid"""
vec=np.array(vec) #make sure vec is an array. This way a list is also accepted.
tmpmat = np.matrix([vec,-vec]).T #-vec and vec can already be entered as columns of the permutation matrix
for i in range(3): #To make the permutations, the elements must be swapped.
val1 = i
val2 = (i+1)%3
val3 = (i+2)%3
vn = []
vn.append(np.array([vec[val1], vec[val2], vec[val3]])) #depending on i, the values are switched. 8 extra vectors per permutations must possibly be added: the one only with switched numbers.
vn.append(np.array([-vec[val1], vec[val2], vec[val3]])) #the one with the first element negative
vn.append(np.array([vec[val1], -vec[val2], vec[val3]])) #the one with the second element negative
vn.append(np.array([vec[val1], vec[val2], -vec[val3]])) #the one with the third element negative
vn.append(np.array([vec[val1], vec[val3], vec[val2]])) #depending on i, the values are switched. 8 extra vectors per permutations must possibly be added: the one only with switched numbers.
vn.append(np.array([-vec[val1], vec[val3], vec[val2]])) #the one with the first element negative
vn.append(np.array([vec[val1], -vec[val3], vec[val2]])) #the one with the second element negative
vn.append(np.array([vec[val1], vec[val3], -vec[val2]])) #the one with the third element negative
for j in vn: #all are checked to see whether they already exist in the matrix
if not isExist(tmpmat, j): #if they don't they get added
tmpmat = np.c_[tmpmat, j]
if not isExist(tmpmat, -j):
tmpmat = np.c_[tmpmat, -j]
if checksame and self.typ!="cubic":
#in case we only want those vectors that are crystallographically the same length. If the matrix is cubic we know we don't have to eliminate anything.
#tst is the length of the supplied vector
tst = self.getVectorLength(vec, typ=typ)
#others is the list of lengths of "equivalent" vectors
others = self.getVectorLength(tmpmat.T, typ=typ)
#get all the columns from tempmat where the difference between the length of the supplied vector and the equivalent vectors is negligible
tmpmat2 = tmpmat[:, abs(others-tst)<unc]
tmpmat = tmpmat2
return tmpmat
def getVectorDot(self, vec1, vec2, typ="real"):
"""Get the dot product between two miller indices. The vectors can be defined in real space or recyprocal space but must both be defined in the same space. Otherwise the normal dot product is applicable. A list of COLUMN vectors can also be supplied or 1 array"""
if typ=="real":
return np.array(np.dot(vec1.T,np.dot(self.G, vec2)))
elif typ=="recyp":
return np.array(np.dot(vec1.T,np.dot(self.recypG, vec2)))
else:
return None
def getVectorLength(self, vec, typ="real"):
"""The length of a certain real space vector or plane normal depending on the type, A list of ROW vectors can also be supplied, an array of 1 dimension will be returned"""
return np.array(np.matrix(np.sqrt(self.getVectorDot(vec, vec, typ=typ))).diagonal())
def getVectorAngle(self, vec1, vec2, typ="real", units="radians"):
#! still some strange behavior when testing when vec1 or vec2 is a one dimensional array and the other is not. Otherwise it works perfectly. This has to do with the way the division happens with matrix/arrays. Fix later.
"""The angle between two vectors in real or recyprocal space, A list of Column vectors can also be supplied, an array of 2 dimensions will be returned"""
num= self.getVectorDot(vec1, vec2, typ=typ)
denom = np.outer(self.getVectorLength(vec1, typ=typ), self.getVectorLength(vec2, typ=typ))
angls= np.arccos(np.divide(num, denom))
if units =="radians":
return angls
elif units =="degrees":
return angls/(2*np.pi)*360
else:
print("Those units aren't valid.")
return None
def getZoneRefPairs(self, l1, a1, l2, a2, maxind = 5, err = 0.5, err2 = 2, verbose = False):
"""This function helps transform lengths and directions measured in the diffraction pattern to reflections and zones.
This is essentially a help for indexing.
As input you provide the length and angle measured from 2 reflections in a diffraction pattern. Make sure angles are measured properly!
Enter lengths in nm^-1 because that is usually the case in DM. Angles in degrees.
Optional arguments are the largest valu of h, k or l to search, the +/- error on the length where it's a match (in nm^-1), and the +/- error on the angle between reflections, in degrees
Returned is (matching reflections to l1, the calculated lengths vector, matching reflections to l2, the calculated lengths vector, the angle between them, the calculated zone axis)"""
#l1 and l2 are often times in nm^-1. Convert to angstrom^-1.
l1 = l1/10
l2 = l2/10
err = err/10
#Crucial thing is angle between the vectors to differentiate them
a = abs(a2-a1)
#first get a length match, only positive vectors need to be considered
#construct a list of possible combined zones in which to search.
tryvecs = getHKLlist(maxind = maxind).T
#Make all the possible permutations of each unique type of indices
longlist = np.array([[0,0,0]]).T
for i in tryvecs:
cps = np.array(self.getSym(i))
longlist = np.c_[longlist, cps]
#find the length of the vectors assuming they are recyprocal lattice points
lv = self.getVectorLength(longlist, typ = "recyp")[0]
#find where l1 and l2 match the length array. Then find the vectors matching the indices.
indxes1 = np.where(np.logical_and(lv<l1+err, lv>l1-err))[0]
vecs1 = longlist[:, indxes1]
indxes2 = np.where(np.logical_and(lv<l2+err, lv>l2-err))[0]
vecs2 = longlist[:, indxes2]
#find angles between all the vectors that are ok in length
angls = self.getVectorAngle(vecs1, vecs2, typ = "recyp", units = "degrees")
#find indexes of those vectors where the angle between them are ok
anglindx = np.where(np.logical_and(angls<a+err2, angls>a-err2))
#find the vectors that match the good fit for the angle
#rows or anglindx[0] matches vec1, columns or anglindx[1] matches vec2
match1 = vecs1[:, anglindx[0]]
match2 = vecs2[:, anglindx[1]]
matchangls = angls[anglindx[0], anglindx[1]]
matchl1 = self.getVectorLength(match1, typ = "recyp")
matchl2 = self.getVectorLength(match2, typ = "recyp")
zones = calcCross(match1, match2)
if verbose:
print("All testing vectors:")
print(longlist)
print("Lengths of the vectors:")
print(lv)
print("Matches to l1")
print(indxes1)
print(vecs1)
print("Matches to l2")
print(indxes2)
print(vecs2)
print("Angles between l1 and l2:")
print(angls)
#put into right format to output
return match1.T.tolist(), matchl1[0].tolist(), match2.T.tolist(), matchl2[0].tolist(), matchangls.tolist(), zones.T.tolist()
class Crystal(object):
def __init__(self, name, stage, structurename, comment = ""):