Skip to content

Commit 1026fa4

Browse files
authored
Feature: add postprocess for stm plot (#6137)
1 parent 00dcc80 commit 1026fa4

File tree

2 files changed

+358
-0
lines changed

2 files changed

+358
-0
lines changed

tools/stm/plot.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from stm import STM
2+
import matplotlib.pyplot as plt
3+
import numpy as np
4+
5+
6+
7+
dirname='OUT.autotest'
8+
bias = 2
9+
z = 8.0
10+
11+
stm = STM(dirname)
12+
13+
14+
# 1. Constant current 2-d scan
15+
c = stm.get_averaged_current(bias, z)
16+
x, y, h = stm.scan(bias, c, repeat=(3, 5))
17+
18+
plt.gca().axis('equal')
19+
plt.contourf(x, y, h, 40)
20+
plt.colorbar()
21+
plt.savefig('2d_current.png')
22+
23+
24+
# 2. Constant height 2-d scan
25+
plt.figure()
26+
plt.gca().axis('equal')
27+
x, y, I = stm.scan2(bias, z, repeat=(3, 5))
28+
plt.contourf(x, y, I, 40)
29+
plt.colorbar()
30+
plt.savefig('2d_height.png')
31+
32+
33+
# 3. Constant current line scan
34+
plt.figure()
35+
a = stm.atoms.cell[0, 0]
36+
x, y = stm.linescan(bias, c, [0, 0], [2 * a, 0])
37+
plt.plot(x, y)
38+
plt.savefig('line.png')
39+
40+
41+
# Scanning tunneling spectroscopy
42+
# 4. dI/dV spectrum
43+
plt.figure()
44+
biasstart = 2.0
45+
biasend = 3.0
46+
biasstep = 0.1
47+
bias, I, dIdV = stm.sts(0, 0, z, biasstart, biasend, biasstep)
48+
plt.plot(bias, I, label='I')
49+
plt.plot(bias, dIdV, label='dIdV')
50+
plt.xlim(biasstart, biasend)
51+
plt.legend()
52+
plt.savefig('dIdV.png')
53+
54+
55+
# 5. dI/dV map
56+
biasstart = -2.0
57+
biasend = 2.0
58+
biasstep = 0.1
59+
start_points = [0, 0, 8]
60+
end_points = [2, 0, 8]
61+
distance = np.linalg.norm(np.array(end_points) - np.array(start_points))
62+
biases, points, dIdV_map = stm.line_sts(biasstart, biasend, biasstep, start_points, end_points, 50)
63+
64+
plt.figure(figsize=(10, 6), dpi=150)
65+
66+
# 2D Colormap
67+
im = plt.pcolormesh(biases, points, dIdV_map, cmap='plasma', shading='auto')
68+
# optional cmap: 'RdYlBu', 'plasma', 'inferno', 'viridis'
69+
70+
# color bar
71+
cbar = plt.colorbar(im, label='dI/dV', pad=0.02)
72+
73+
# labels
74+
plt.xlabel('Sample Bias (V)', fontsize=12)
75+
plt.ylabel('distance to the start point (Angstrom)', fontsize=12)
76+
plt.xlim(biasstart, biasend)
77+
plt.ylim(0, distance)
78+
plt.title('dI/dV (~density of states) map', fontsize=14)
79+
80+
plt.tight_layout()
81+
plt.savefig('dIdV_map.png', bbox_inches='tight', transparent=False)
82+
plt.show()

tools/stm/stm.py

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
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

Comments
 (0)