Commit d482cffe authored by Kyle Larsen's avatar Kyle Larsen
Browse files

Updated function comments. Included parameters information. Removed some uncessary comments.

parent 4a0c42a3
......@@ -51,6 +51,15 @@ class FV:
self.pixel_size = self.get_pixel_size()
def get_pixel_size(self, scan_size=None, scan_points=None):
'''
Calculate the size of a single pixel in the force-volume data. Should be in units of meters.
Parameters
----------
scan_size: float, optional
The total size of the force-volume scan (in meters).
scan_points: int, optional
The number of force-deflection ramps in each line of the scan.
'''
if scan_size is None:
scan_size = self.fv_params["scan_size"]
......@@ -60,9 +69,15 @@ class FV:
return scan_size / scan_points
def get_extend(self):
'''
Return the force-volume data recorded during the extension of the AFM cantilever.
'''
return self.tm_defl[:, 0, :]
def get_retract(self):
'''
Return the force-volume data recorded during the retraction of the AFM cantilever.
'''
return self.tm_defl[:, 1, :]
def summarize(self, which_dir='retrace', summary_func=get_comp_mat, **kwargs):
......
......@@ -35,9 +35,13 @@ def read_fv_header(filename: str) -> dict:
- C means _Scale_ -- a parameter that is simply a scaled version of another.
- S means _Select_ -- a parameter that describes some selection that has been made
Parameters
----------
filename: str
Filename of the NanoScope file.
'''
# May be usefule:
# May be useful:
# \*Ciao scan list\Scan Size: 15000 nm
# \*Ciao scan list\Samps/line: 64 # Force volume must be square.
......@@ -69,7 +73,7 @@ def read_fv_header(filename: str) -> dict:
else:
params[key][index][args[0]] = ""
# TO DO: I should be able to remove this failsafe.
# TODO: I should be able to remove this failsafe.
# Don't read more than 1000 lines.
count += count
if count > 1000:
......@@ -89,6 +93,12 @@ def read_fv_header(filename: str) -> dict:
def convert_params(old_params, custom_to_extract = []):
'''
Convert the parameters from the NanoScope name to a new (universal) name.
If this code is adapted to new file formats, a new `convert_params` function
should return these same new parameters.
These are the parameters we need:
CFIL
- Data offset
- Data length
......@@ -102,6 +112,16 @@ def convert_params(old_params, custom_to_extract = []):
SL
- @Sens. Zsens
Parameters
----------
old_params: dict
Original parameter dictionary loaded with `read_fv_header()`
custom_to_extract: array of tuples, optional
This function will also convert any additional parameters provided here.
Follow tuple format in function:
(Section, Parameter Name, New parameter name, Function to convert from bytestring to desired type)
'''
from_value_f = lambda x: float(x.split()[-2])
......@@ -151,7 +171,14 @@ def read_fv_data(filename: str, params: dict) -> np.ndarray:
For example, a 64x64 with 1024 samples per force-ramp will have a data length of:
- 64^2 * 1024 * 2 = 8388608
This length should be recorded in the header as `\*Ciao force image list\Data length` (keeping in mind the bytes/pixel).
This length should be recorded in the header as `\*Ciao force image list\Data length` (keeping in mind the bytes/pixel).
Parameters
----------
filename: str
Path to NanoScope scan file.
params: dict
Parameters dictionary. From `get_params()`.
'''
offset = params["fv_data_offset"]
data_length = params["fv_data_length"]
......@@ -178,10 +205,17 @@ def read_fv_data(filename: str, params: dict) -> np.ndarray:
return np.array(unpack(unpack_fmt, raw_data), dtype='float64')
def convert_fv_data(data: np.ndarray, params: str) -> tuple:
def convert_fv_data(data: np.ndarray, params: dict) -> tuple:
'''
Convert from ADC counts to volts. Returns the piezo ramp deflection `z_piezo` and the
force-volume TM deflection data in volts in a tuple: (z_piezo, tm_defl).
Parameters
----------
data: ndarray
Raw data from force-volume file (from `read_fv_data()`).
params: dict
Parameters dictionary. From `get_params()`.
'''
z_sens = params["piezo_nm_per_volt"]
ramp_size = params["ramp_size"]
......@@ -204,13 +238,30 @@ def convert_fv_data(data: np.ndarray, params: str) -> tuple:
return (z_piezo, tm_defl)
def get_fv_data(filename: str, params: dict) -> tuple:
'''Get the `z_piezo` deflection ramp. `params` should be the converted, generalized parameter dictionary.'''
'''
Get the `z_piezo` deflection ramp. `params` should be the converted, generalized parameter dictionary.
Parameters
----------
filename: str
Path to NanoScope scan file.
params: dict
Parameters dictionary. From `get_params()`.
'''
data = read_fv_data(filename, params)
# Convert to metric units
return convert_fv_data(data, params)
def get_params(filename: str) -> dict:
'''
Get the parameters from the NanoScope file header.
Parameters
----------
filename: str
Path to NanoScope scan file.
'''
all_fv_params = read_fv_header(filename )
fv_params = convert_params(all_fv_params)
......@@ -219,6 +270,13 @@ def get_params(filename: str) -> dict:
def save_txt_data(data, filename):
'''
Save the converted data to an ASCII file using the same format as exports from Nanoscope Analysis 2.0.
Parameters
----------
data: ndarray
Converted data to be saved in ASCII format.
filename: str
Filename to which the ASIC data should be saved.
'''
header = "Calc_Ramp_Ex_nm\tCalc_Ramp_Rt_nm\tDefl_mV_Ex\tDefl_mV_Rt\tpN Not Available\tpN Not Available\t"
np.savetxt(filename, data, delimiter='\t', fmt='%1.6e', header=header, comments='')
import json
def get_scan_params(sp_filename: str) -> dict:
"""Loads the scan parameters from a JSON file.
'''
Loads the scan parameters from a JSON file.
The following is an example scan parameter file, with annotation. JSON does not support comments, so anything after '#' should be removed.
The following is an example scan parameter file, with annotation. JSON does not support comments,
so anything after (and including) '#' should be removed.
{
"name": "02041411.001", # Required
......@@ -33,7 +35,11 @@ def get_scan_params(sp_filename: str) -> dict:
]
}
"""
Parameters
----------
sp_filename: str
Filename string (passed to json.load()) pointing to scan parameter JSON file.
'''
with open(sp_filename) as file:
# Get the scan parameters.
sc_params = json.load(file)
......
......@@ -8,6 +8,17 @@ import math
def smooth_z_tip(z_tip, method = "movmean"):
'''
The raw force-deflection data from the AFM scan is frequently noisy. This function performs
either a moving average or a butterworth filter. I found that doing two moving averages with
a window size of 5 and then 11 works well. The data is first flipped to avoid a "lip" at the
beginning of the data.
Parameter
---------
z_tip: ndarray
Numpy array containing the z_tip force-deflection data.
method: {"movmean", "butter"}, optional, default: "movmean"
Select smoothing method. Can be 'movmean' or 'butter'.
'''
if method == "butter":
b, a = signal.butter(1, 0.1)
......@@ -20,15 +31,30 @@ def smooth_z_tip(z_tip, method = "movmean"):
return z_tip_smooth
def get_start_end(z_piezo, z_tip):
dz_tip = np.diff(z_tip)
max_indx = np.argmax(dz_tip) # TODO: this isn't great. Should use peak find or get first zero crossing.
'''
Get the start and end indices for the linear portion of z_piezo vs z_tip.
Parameters
----------
z_piezo: ndarray
Z_piezo data as a numpy array.
z_tip: ndarray
Z_tip data as a numpy array.
'''
dz_tip = np.diff(z_tip)
max_indx = np.argmax(dz_tip) # TODO: this isn't great. Should use peak find or get first zero crossing.
if isinstance(max_indx, list):
max_indx = max_indx[0]
start = round(0.008*len(z_piezo))
end = max_indx# - round(start/2) # We want to be a little bit away from the peak
# The starting index should not be the very first, since the first few datapoints are noisy.
# This is a silly way to do this, but we essentially discard the first 0.8% of the data.
# This is 8 for samples with 1024 points and 4 for samples with 512 points.
start = round(0.008*len(z_piezo))
end = max_indx # This 'end' is a starting point for below
# Find the first zero crossing before `end`
while end - start > 10: # There has got to be a better way to do this.
while end - start > 10: # Repeat, but don't get to close to 'start'
end -= 1
if dz_tip[end] <= 0:
break
......@@ -39,9 +65,13 @@ def get_start_end(z_piezo, z_tip):
# if isinstance(end, list):
# end = end[-1]
# If we started too close, then back off on 'start'
# This will take 'start' in samples with 1024 points from 8 to 4.
# This will take 'start' in samples with 512 points from 4 to 2.
if end - start < 10:
start = round(start/2)
# As a last resort, just fix the start and end indices.
if end - start <= 10:
start = 5
end = 20
......@@ -50,8 +80,9 @@ def get_start_end(z_piezo, z_tip):
def line_slope(z_piezo, z_tip, index = None):
'''
Algorithm for getting the slope of a force ramp. (The following
diagram may not display properly in IDE tooltips.)
Algorithm for getting the slope of a force ramp. Applied to all force ramps in the force volume data (4096 for 64x64 scans).
The slope is used to find the compliance at each point in the map.
(The following diagram may not display properly in IDE tooltips.)
\
\ ^
......@@ -68,13 +99,24 @@ def line_slope(z_piezo, z_tip, index = None):
Get an initial (start, end) estimate using `get_start_end()`.
- `get_start_end()` takes the derivative of z_tip and finds the z_piezo location where that derivative is highest. This is the end point.
- The start point is just 0.8% of the length of z_piezo (8 for ramps with 1024 samples; 4 for ramps with 512 samples)
- The end point is reduced untile the first zero crossing (before the maximum) of the derivative of z_tip is found.
- The end point is reduced until the first zero crossing (before the maximum) of the derivative of z_tip is found.
This (start, end) value is used to fit to the linear region of the force ramp. If R^2 is greater than 0.9, then this slope is returned.
Otherwise, decrease the end value, fit again, and check R^2. This is repeated until R^2 is greater than 0.9 or either of the following
condition is met:
- There are less than 15 points between the start and end values.
Otherwise, decrease the end value, fit again, and check R^2. This is repeated until any of these conditions are met:
- R^2 is greater than 0.9, or
- There are less than 15 points between the start and end values, or
- The process has looped through 10 times without meeting either of the above criteria.
Parameters
----------
z_piezo: ndarray
Z_piezo data as a numpy array.
z_tip: ndarray
Z_tip data as a numpy array.
index: int, optional
If `index` is supplied, this function will not loop through all force-deflection ramps in the FV data. It will only look at the ramp
where the index of z_tip is `index`. Useful for code that selects only one force-ramp to plot.
'''
size = z_tip.shape[1] # 4096 for 64x64 scans; 1024 for 32x32 scans.
slopes = np.zeros((size,))
......@@ -89,16 +131,14 @@ def line_slope(z_piezo, z_tip, index = None):
orig_e = e
tries = 10
while r2 < 0.9:
# Fit the region from `s` to `e`
res = stats.linregress(z_piezo[s:e], z_tip[s:e, n])
r2 = res.rvalue**2
## For next time
old_e = e
e -= orig_e // tries
# Make sure there are at least 20 samples
if e - s < 15:
e = old_e
......@@ -118,6 +158,25 @@ def line_slope(z_piezo, z_tip, index = None):
return (slopes, r2s, s, e)
def get_comp_mat(z_piezo, tm_defl, sc_params, linearize = True, savefile = None, smooth_func = smooth_z_tip, **kwargs):
'''
Get the compliance map. In other words, convert each force-deflection ramp to a compliance value.
Parameters
----------
z_piezo: ndarray
Piezo displacement data as a numpy array.
tm_defl: ndarray
Tapping mode deflection data as a numpy array.
sc_params: dict
Dictionary containg parameters loaded from JSON file with `get_scan_params()`.
linearize: boolean, optional, default: True
If true, will take the cube root of the compliance data. This linearizes the data in displacement, since the
compliance equation depends on the position along the cantilever to the third power (see Euler cantilever equation).
savefile: str, optional
If provided, the slopes will be saved to the file `savefile`.
smooth_func: function, optional
This function will be applied to `tm_defl` to smooth the force-deflection data.
'''
# TM Deflection is called z_tip in the paper. Here I am using tm_defl to hold the entire 64x64 array of TM deflections.
#tm_defl = data[:, 1, :]
......@@ -149,38 +208,32 @@ def get_comp_mat(z_piezo, tm_defl, sc_params, linearize = True, savefile = None,
fixed_edge = cant["fixed_edge"]
# For determining the TM deflection sensitivity, ignore points with R^2 lower than 0.9
mod_slope = slope.copy()
mod_slope = slope.copy()
mod_slope[r2s < 0.9] = np.nan
# Find TM deflection sensitivity by looking at the left part of the slope data.
slice_s = 0
slice_e = fixed_edge - 2
left_slice = mod_slope[:, slice_s:slice_e]
#fig, axs = plt.subplots(1, 2, figsize=(8,4))
# Plot the left part of the slopes
# This average (highest bin in histogram) of left_slice is used to determine
# the TM deflection sensitivity.
#axs[0].imshow(left_slice)
left_slice = left_slice.flatten()
left_slice = left_slice[~np.isnan(left_slice)] # Remove the points that had an R^2 < 0.9
# Plot histogram of left_slice
# Find highest bar in histogram of left_slice
# Use that as the TM deflection sensitivity
h = np.histogram(left_slice.flatten(), bins=20)
edges = h[1]
edges = edges[1:]
tm_defl_sens = 1/float(edges[h[0] == h[0].max()][-1])
print(f"Sample = {sc_params['name']}")
print(f"TM Defl. Sens. = {tm_defl_sens:.2f} nm/V")
slope *= tm_defl_sens # [V/nm]*[nm/V]=[1]
# Calculate compliance using:
# comp = 1/k_afm*(1/slope - 1)
comp = 1/sc_params["afm_spring_constant"]*(slope**-1 - 1) # Compliance
comp[comp <= 0] = 0.000001
comp[comp <= 0] = 0.000001 # Replace zeros with sufficiently small value (but not too small)
if linearize:
comp = comp**(1/3.0)
......@@ -192,6 +245,16 @@ def get_comp_mat(z_piezo, tm_defl, sc_params, linearize = True, savefile = None,
# Move to new file?
def get_cantilever_params(params, cant_num):
'''
Get the important parameters from the parameter dictionary (loaded from JSON) for a specific cantilever.
Parameters
----------
params: dict
Dictionary of parameters. Load from JSON using `get_scan_params()`. Pass in only parameter for single sample.
cant_num: int
Cantilever number for which to get params.
'''
thick = params["thickness"]
width = params["cantilevers"][cant_num]["width"]
start = params["cantilevers"][cant_num]["start"]
......@@ -206,19 +269,32 @@ def get_cantilever_params(params, cant_num):
return (thick, width, start, end, igno, fixed, start, end, row, col_s, col_e)
def comp_mat_inspector(comp_mat, z_piezo, tm_defl, params, fig_width = 12, r2s_mat = None):
def comp_mat_inspector(comp_mat, z_piezo, tm_defl, params, fig_width = 10, r2s_mat = None):
'''
Create the interactive compliance map inspector. This tool shows the compliance map on the left,
the selected force-deflection map in the middle, and an R^2 map on the right. Click on any pixel
in the compliance map or the R^2 map to update the middle force-deflection map.
Parameters
----------
comp_mat: ndarray
Compliance matrix from `get_com_mat()`
z_piezo: ndarray
Piezo displacement data. Used for central plot.
tm_defl: ndarray
Tapping mode deflection data. Used for central plot.
params: dict
Dictionary of parameters. Load from JSON using `get_scan_params()`.
fig_width: int, optional
Width of matplotlib figure in inches.
r2s_mat: ndarray, optional
R^2 matrix to plot in third column. If not included, third column is disabled.
'''
# Plot slopes images
if 'fig' in locals():
plt.close(fig)
#fig, axs = plt.subplots(1, 2, figsize=(12,6))
# fig, axs = plt.subplot_mosaic(
# [['left', 'upper right'],
# ['left', 'lower right']],
# figsize = (fig_width, fig_width/2),
# constrained_layout = True
# )
# Use matplotlib's mosaic to give nice names to axes.
mosaic = """
ABD
ACD
......@@ -231,11 +307,13 @@ def comp_mat_inspector(comp_mat, z_piezo, tm_defl, params, fig_width = 12, r2s_m
axs = fig.subplot_mosaic(mosaic)
if r2s_mat is None:
# TODO: This turns the axis off, but the area is still there (just blank).
axs["D"].set_axis_off()
# Get number of pixels per row in map. Maps should always be square.
size = int(math.sqrt(tm_defl.shape[1]))
# Keep this info with the axis so that the interaction works.
axs["A"].custom_info = {
'z_piezo' : z_piezo,
'z_tip' : tm_defl,
......@@ -244,14 +322,16 @@ def comp_mat_inspector(comp_mat, z_piezo, tm_defl, params, fig_width = 12, r2s_m
'ax_dz_tip' : axs["C"]
}
axs["A"].pcolormesh(comp_mat, vmin=0, vmax=1)#, edgecolors='k', linewidth=0.1)
# Plot compliance map
axs["A"].pcolormesh(comp_mat, vmin=0, vmax=1)
axs["A"].invert_yaxis()
axs["A"].set_title("Compliance map")
# Plot R^2 map
if r2s_mat is not None:
axs["D"].custom_info = axs["A"].custom_info
axs["D"].pcolormesh(r2s_mat, vmin=0, vmax=1)#, edgecolors='k', linewidth=0.1)
axs["D"].pcolormesh(r2s_mat, vmin=0, vmax=1)
axs["D"].invert_yaxis()
axs["D"].set_title("$R^2$ map")
......@@ -266,6 +346,7 @@ def comp_mat_inspector(comp_mat, z_piezo, tm_defl, params, fig_width = 12, r2s_m
axs["A"].add_patch(rect)
# Plot the z_tip data. Shows what was fit to get compliance.
plot_z_tip(0, 0, z_piezo, tm_defl, size, axs["B"], axs["C"])
cid = fig.canvas.mpl_connect('button_press_event', onclick_mat)
......@@ -273,6 +354,22 @@ def comp_mat_inspector(comp_mat, z_piezo, tm_defl, params, fig_width = 12, r2s_m
def plot_z_tip(row, col, z_piezo, z_tip, size, ax1, ax2):
'''
Plot the z_tip data. Each pixel in the compliance map comes from fitting to z_tip.
Parameters
----------
row: int
Row from compliance map. Used along with `col` to identify specific pixel.
col: int
Column from compliance map. Used along with `row` to identify specific pixel.
z_piezo: ndarray
Piezo displacement data.
z_tip: ndarray
AFM tip displacement data.
size: int
Number of columns per compliance map.
ax1, ax2: Axes
Two axes on which to plot. `ax1` is used for the z_tip data and `ax2` is used of its derivative.
'''
index = row*size + col
ax1.plot(z_piezo, z_tip[:, index]*1000)
......@@ -303,6 +400,12 @@ def plot_z_tip(row, col, z_piezo, z_tip, size, ax1, ax2):
def onclick_mat(event):
'''
Click event hander. Used to allow for inspection of the compliance map.
Parameters
----------
event: matplotlib.backend_bases.Event
Event fired when mouse clicked on compliance map.
'''
try:
z_piezo = event.inaxes.custom_info['z_piezo']
......@@ -313,6 +416,7 @@ def onclick_mat(event):
except:
return
# Find the column and row of the pixel that was clicked
col = math.floor(event.xdata)
row = math.floor(event.ydata)
col = 0 if col < 0 else col
......@@ -323,6 +427,7 @@ def onclick_mat(event):
# We use flipud on the matrix, so the row is wrong
row = (size - 1) - row
# Replot
ax1.clear()
ax2.clear()
plot_z_tip(row, col, z_piezo, z_tip, size, ax1, ax2)
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment