1+ from ase .io .cube import read_cube_data
2+ from ase .io .jsonio import read_json , write_json
3+ import matplotlib .pyplot as plt
4+ import numpy as np
5+
6+ class STM :
7+ def __init__ (self , dirname = '.' ):
8+ """Scanning tunneling microscope.
9+
10+ dirname: string
11+ Directory containing the cube files with local density of states.
12+ """
13+
14+ self .dirname = dirname
15+
16+
17+ def read_ldos (self , bias ):
18+ """Read local density of states from cube file.
19+
20+ bias: float
21+ Bias voltage in Volts.
22+ """
23+
24+ if (abs (bias ) < 1e-5 ):
25+ bias = 0.0
26+
27+ filename = f'{ self .dirname } /LDOS_{ bias :.5g} eV.cube'
28+ print ('read in ' + filename )
29+ self .ldos , self .atoms = read_cube_data (filename )
30+ self .cell = self .atoms .cell
31+ self .bias = bias
32+
33+
34+ def write (self , filename ):
35+ """Write local density of states to JSON file."""
36+ write_json (filename , (self .ldos , self .bias , self .cell ))
37+
38+
39+ def get_averaged_current (self , bias , z ):
40+ """Calculate avarage current at height z (in Angstrom).
41+
42+ Use this to get an idea of what current to use when scanning."""
43+
44+ self .read_ldos (bias )
45+
46+ nz = self .ldos .shape [2 ]
47+
48+ # Find grid point:
49+ n = z / self .cell [2 , 2 ] * nz
50+ dn = n - np .floor (n )
51+ n = int (n ) % nz
52+
53+ # Average and do linear interpolation:
54+ return ((1 - dn ) * self .ldos [:, :, n ].mean () +
55+ dn * self .ldos [:, :, (n + 1 ) % nz ].mean ())
56+
57+
58+ def scan (self , bias , current , z0 = None , repeat = (1 , 1 )):
59+ """Constant current 2-d scan.
60+
61+ Returns three 2-d arrays (x, y, z) containing x-coordinates,
62+ y-coordinates and heights. These three arrays can be passed to
63+ matplotlibs contourf() function like this:
64+
65+ >>> import matplotlib.pyplot as plt
66+ >>> plt.contourf(x, y, z)
67+ >>> plt.show()
68+
69+ """
70+
71+ self .read_ldos (bias )
72+
73+ L = self .cell [2 , 2 ]
74+ nz = self .ldos .shape [2 ]
75+ h = L / nz
76+
77+ ldos = self .ldos .reshape ((- 1 , nz ))
78+
79+ heights = np .empty (ldos .shape [0 ])
80+ for i , a in enumerate (ldos ):
81+ heights [i ] = find_height (a , current , h , z0 )
82+
83+ s0 = heights .shape = self .ldos .shape [:2 ]
84+ heights = np .tile (heights , repeat )
85+ s = heights .shape
86+
87+ ij = np .indices (s , dtype = float ).reshape ((2 , - 1 )).T
88+ x , y = np .dot (ij / s0 , self .cell [:2 , :2 ]).T .reshape ((2 ,) + s )
89+
90+ return x , y , heights
91+
92+
93+ def scan2 (self , bias , z , repeat = (1 , 1 )):
94+ """Constant height 2-d scan.
95+
96+ Returns three 2-d arrays (x, y, I) containing x-coordinates,
97+ y-coordinates and currents. These three arrays can be passed to
98+ matplotlibs contourf() function like this:
99+
100+ >>> import matplotlib.pyplot as plt
101+ >>> plt.contourf(x, y, I)
102+ >>> plt.show()
103+
104+ """
105+
106+ self .read_ldos (bias )
107+
108+ nz = self .ldos .shape [2 ]
109+ ldos = self .ldos .reshape ((- 1 , nz ))
110+
111+ current = np .empty (ldos .shape [0 ])
112+
113+ for i , a in enumerate (ldos ):
114+ current [i ] = self .find_current (a , z )
115+
116+ s0 = current .shape = self .ldos .shape [:2 ]
117+ current = np .tile (current , repeat )
118+ s = current .shape
119+
120+ ij = np .indices (s , dtype = float ).reshape ((2 , - 1 )).T
121+ x , y = np .dot (ij / s0 , self .cell [:2 , :2 ]).T .reshape ((2 ,) + s )
122+
123+ # Returing scan with axes in Angstrom.
124+ return x , y , current
125+
126+
127+ def linescan (self , bias , current , p1 , p2 , npoints = 50 , z0 = None ):
128+ """Constant current line scan.
129+
130+ Example::
131+
132+ stm = STM(...)
133+ z = ... # tip position
134+ c = stm.get_averaged_current(-1.0, z)
135+ stm.linescan(-1.0, c, (1.2, 0.0), (1.2, 3.0))
136+ """
137+
138+ heights = self .scan (bias , current , z0 )[2 ]
139+
140+ p1 = np .asarray (p1 , float )
141+ p2 = np .asarray (p2 , float )
142+ d = p2 - p1
143+ s = np .dot (d , d )** 0.5
144+
145+ cell = self .cell [:2 , :2 ]
146+ shape = np .array (heights .shape , float )
147+ M = np .linalg .inv (cell )
148+ line = np .empty (npoints )
149+ for i in range (npoints ):
150+ p = p1 + i * d / (npoints - 1 )
151+ q = np .dot (p , M ) * shape
152+ line [i ] = interpolate (q , heights )
153+ return np .linspace (0 , s , npoints ), line
154+
155+
156+ def pointcurrent (self , bias , x , y , z ):
157+ """Current for a single x, y, z position for a given bias."""
158+
159+ self .read_ldos (bias )
160+
161+ nx = self .ldos .shape [0 ]
162+ ny = self .ldos .shape [1 ]
163+ nz = self .ldos .shape [2 ]
164+
165+ # Find grid point:
166+ xp = x / np .linalg .norm (self .cell [0 ]) * nx
167+ dx = xp - np .floor (xp )
168+ xp = int (xp ) % nx
169+
170+ yp = y / np .linalg .norm (self .cell [1 ]) * ny
171+ dy = yp - np .floor (yp )
172+ yp = int (yp ) % ny
173+
174+ zp = z / np .linalg .norm (self .cell [2 ]) * nz
175+ dz = zp - np .floor (zp )
176+ zp = int (zp ) % nz
177+
178+ # 3D interpolation of the LDOS at point (x,y,z) at given bias.
179+ xyzldos = (((1 - dx ) + (1 - dy ) + (1 - dz )) * self .ldos [xp , yp , zp ] +
180+ dx * self .ldos [(xp + 1 ) % nx , yp , zp ] +
181+ dy * self .ldos [xp , (yp + 1 ) % ny , zp ] +
182+ dz * self .ldos [xp , yp , (zp + 1 ) % nz ])
183+
184+ return dos2current (bias , xyzldos )
185+
186+
187+ def sts (self , x , y , z , bias0 , bias1 , biasstep ):
188+ """Returns the dI/dV curve for position x, y at height z (in Angstrom),
189+ for bias from bias0 to bias1 with step biasstep."""
190+
191+ biases = np .arange (bias0 , bias1 + biasstep , biasstep )
192+ current = np .zeros (biases .shape )
193+
194+ for b in np .arange (len (biases )):
195+ print (b , biases [b ])
196+ current [b ] = self .pointcurrent (biases [b ], x , y , z )
197+
198+ dIdV = np .gradient (current , biasstep )
199+
200+ return biases , current , dIdV
201+
202+
203+ def line_sts (self , bias0 , bias1 , biasstep , p1 , p2 , npoints = 50 ):
204+ """Returns the dI/dV curve for line between p1 and p2,
205+ for bias from bias0 to bias1 with step biasstep."""
206+
207+ p1 = np .asarray (p1 , float )
208+ p2 = np .asarray (p2 , float )
209+ d = p2 - p1
210+ s = np .dot (d , d )** 0.5
211+ biases = np .arange (bias0 , bias1 + biasstep , biasstep )
212+
213+ dIdV = np .zeros ((npoints , len (biases )))
214+
215+ for i in range (npoints ):
216+ x , y , z = p1 + i * d / (npoints - 1 )
217+ biases , current , dIdV [i , :] = self .sts (x , y , z , bias0 , bias1 , biasstep )
218+
219+ return biases , np .linspace (0 , s , npoints ), dIdV
220+
221+
222+ def find_current (self , ldos , z ):
223+ """ Finds current for given LDOS at height z."""
224+ nz = self .ldos .shape [2 ]
225+
226+ zp = z / self .cell [2 , 2 ] * nz
227+ dz = zp - np .floor (zp )
228+ zp = int (zp ) % nz
229+
230+ ldosz = (1 - dz ) * ldos [zp ] + dz * ldos [(zp + 1 ) % nz ]
231+
232+ return dos2current (self .bias , ldosz )
233+
234+
235+ def dos2current (bias , dos ):
236+ # Borrowed from gpaw/analyse/simple_stm.py:
237+ # The connection between density n and current I
238+ # n [e/Angstrom^3] = 0.0002 sqrt(I [nA])
239+ # as given in Hofer et al., RevModPhys 75 (2003) 1287
240+ return 5000. * dos ** 2 * (1 if bias > 0 else - 1 )
241+
242+
243+ def interpolate (q , heights ):
244+ qi = q .astype (int )
245+ f = q - qi
246+ g = 1 - f
247+ qi %= heights .shape
248+ n0 , m0 = qi
249+ n1 , m1 = (qi + 1 ) % heights .shape
250+ z = (g [0 ] * g [1 ] * heights [n0 , m0 ] +
251+ f [0 ] * g [1 ] * heights [n1 , m0 ] +
252+ g [0 ] * f [1 ] * heights [n0 , m1 ] +
253+ f [0 ] * f [1 ] * heights [n1 , m1 ])
254+ return z
255+
256+
257+ def find_height (ldos , current , h , z0 = None ):
258+ if z0 is None :
259+ n = len (ldos ) - 2
260+ else :
261+ n = int (z0 / h )
262+ while n >= 0 :
263+ if ldos [n ] > current :
264+ break
265+ n -= 1
266+ else :
267+ return 0.0
268+
269+ c2 , c1 = ldos [n :n + 2 ]
270+ return (n + 1 - (current - c1 ) / (c2 - c1 )) * h
271+
272+
273+ def delta (biases , bias , width ):
274+ """Return a delta-function centered at 'bias'"""
275+ x = - ((biases - bias ) / width )** 2
276+ return np .exp (x ) / (np .sqrt (np .pi ) * width )
0 commit comments