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

Added 'cantilever.py' which contains code for analyzing cantilever compliance....

Added 'cantilever.py' which contains code for analyzing cantilever compliance. Added cantilever tests. Updated example.py to include cantilever analysis code. Can now calculate Young's modulus.
parent 5081a3bc
#from pyMFD.nanoscope import read_spm_data, read_spm_header, convert_spm_data, get_useful_params
from pyMFD.FV import FV
from pyMFD.summarize import comp_mat_inspector
from pyMFD.cantilever import get_cantilever_pos, get_cantilever_params, get_compliance_row, fit_compliance_linear, calc_modulus_offset, standardize_and_fit
import matplotlib.pyplot as plt
use_inspector = False
spm_file = "data/examples/02041411.001" # Example force-volume scan
fv = FV(spm_file) # Load force-volume scan
......@@ -16,5 +19,34 @@ print(comp_mat.shape)
# Use to mouse to select pixels in the (left) compliance map.
# The raw force-deflection data is shown in the center plot.
# The R^2 map (how well the force-deflection data was fit) is shown in the right map.
comp_mat_inspector(comp_mat, fv.z_piezo, fv.get_retract(), fv.sc_params)#, r2s_mat = r2s)
plt.show()
if use_inspector:
comp_mat_inspector(comp_mat, fv.z_piezo, fv.get_retract(), fv.sc_params)#, r2s_mat = r2s)
plt.show()
###########################
# Find cantilever modulus #
###########################
cant_num = 0 # Cantilever number
rows_to_avg = 3 # Number of rows to average around center line of cantilever
pos = get_cantilever_pos(fv.get_pixel_size(), comp_mat.shape[0])
(thick, width, igno, fixed, start, end, row, col_s, col_e) = get_cantilever_params(fv.sc_params, cant_num)
comp_row = get_compliance_row(comp_mat, row, rows_to_avg = rows_to_avg)
(slope, intercept) = fit_compliance_linear(pos[col_s:col_e], comp_row[col_s:col_e])
(E_lin, off_lin) = calc_modulus_offset(slope, intercept, width, thick)
(E, offset, a) = standardize_and_fit(pos[col_s:col_e], comp_row[col_s:col_e]**3, width, thick)
print(f"Width: {width*1e9:.2f} nm")
print(f"Thick: {thick*1e9:.2f} nm")
print("---- Cubic fit ----")
print(f"a: {a:.2g}")
print(f"Young's modulus: {E/1e9:.2f} GPa")
print(f"Offset: {offset*1e6:.2f} µm")
print("---- Linearized fit ----")
print(f"Slope: {slope:.2f}")
print(f"Y-int: {intercept:.2f}")
print(f"Young's modulus: {E_lin/1e9:.2f} GPa")
print(f"Offset: {off_lin*1e6:.2f} µm")
import numpy as np
from scipy.optimize import curve_fit
import scipy.stats as stats
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"]
end = params["cantilevers"][cant_num]["end"]
igno = params["cantilevers"][cant_num]["lin_ignore"]
fixed = params["cantilevers"][cant_num]["fixed_edge"] - 1 # Parameters file indices start at 1, but in python they start at 0
start = np.array(start) - 1
end = np.array(end) - 1
row = (end[1] + start[1]) // 2 # Find center line of cantilever
col_s = start[0] + igno
col_e = end[0]
return (thick, width, igno, fixed, start, end, row, col_s, col_e)
def get_cantilever_pos(pixel_size, size):
'''
Returns the pixel locations in meters across a row.
Parameters
----------
pixel_size: float
Size of pixel in meters.
size: int
Size of scan (number of pixels in scan).
'''
return pixel_size*np.arange(0, size, 1)
def get_compliance_row(comp_mat, row, rows_to_avg = 1):
'''
Return a full row of the compliance map. If rows_to_avg is greater than 1, then rows above and below `row` will be averaged.
Parameters
----------
comp_mat: ndarray
Compliance matrix
row: int
Row of scan to extract
rows_to_avg: int, optional
Total number of rows to average. Will always be symmetric, rounded up. Passing in 2 or 3 is equivalent
'''
one_sided = rows_to_avg // 2
comp_row = comp_mat[(row - one_sided):(row + one_sided + 1), :]
comp_row = np.nanmean(comp_row, axis = 0)
return comp_row
def fit_compliance_linear(position, compliance):
'''
Fit the linearized position vs compliance graph.
1/k = (4/(E*w*t^3))*(L-c)^3
1/k^(1/3) = (4/(E*w*t^3))^(1/3)*(L-c)
1/k^(1/3) = a*(L-c)
1/k^(1/3) = a*L-a*c
y = m*x+b
The slope is proportional to E. To get the fixed end offset, divide the intercept by the slope (and take negative).
Parameters
----------
position: ndarray
Vector of positions (in meters).
compliance: ndarray
Vector of linearized compliance.
'''
res = stats.linregress(position, compliance)
return (res.slope, res.intercept)
def fit_fun(L, a, c):
'''
Function to fit with scipy.optimize.curve_fit.
Parameters
----------
L: float
Position along cantilever
a: float
Combination of width, thickness, and modulus.
c: float
Offset from L.
'''
return 1/a*(L - c)**3
def standardize_and_fit(positions, compliances, width, thickness, func = fit_fun):
'''
Standardized and then fit the non-linearized (i.e. original) compliance data.
Parameters
----------
position: ndarray
Vector of positions (in meters).
compliance: ndarray
Vector of linearized compliance.
width: float
Width of cantilever
thick: float
Thickness of cantilever
func: function
'''
# Remove mean and set standard deviation to 1
mu = np.mean(positions)
sigma = np.std(positions)
pos_std = (positions - mu) / sigma
popt, _ = curve_fit(fit_fun, pos_std, compliances, [150, -1.5])
Y = 4*popt[0]*sigma**3/(width*thickness**3)
pos_off = popt[1]*sigma + mu
return (Y, pos_off, popt[0]*sigma**3)
def calc_modulus_offset(slope, intercept, width, thickness):
'''
Calculate the modulus and fixed end offset.
Parameters
----------
slope: float
Slope returned from `fit_compliance_linear()`
intercept: float
Intercept returned from `fit_compliance_linear()`
width: float
Width of cantilever
thick: float
Thickness of cantilever
'''
E = 4/(slope**3 * width * thickness**3)
c = -intercept / slope
return (E, c)
def offset_to_col_coord(offset, col_s, pixel_size):
'''
TODO: Think of a better name for this function.
TODO: Remove if unused
Takes the offset calculated from fitting the the compliance row and return where that offset is in compliance map space.
Parameters
----------
offset: float
Offset calculated by fitting. This is how far the estimated fixed end is from the origin selected for `position` array.
col_s:
pixel_size: float
Size of single pixel in meters.
'''
return round(col_s + offset / pixel_size)
\ No newline at end of file
from pyMFD.cantilever import get_cantilever_params
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import numpy as np
......@@ -241,34 +243,6 @@ def get_comp_mat(z_piezo, tm_defl, sc_params, linearize = True, savefile = None,
return (comp, r2s)
# 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"]
end = params["cantilevers"][cant_num]["end"]
igno = params["cantilevers"][cant_num]["lin_ignore"]
fixed = params["cantilevers"][cant_num]["fixed_edge"] - 1 # Parameters file indices start at 1, but in python they start at 0
start = np.array(start) - 1
end = np.array(end) - 1
row = (end[1] + start[1]) // 2 # Find center line of cantilever
col_s = start[0] + igno
col_e = end[0]
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 = 10, r2s_mat = None):
'''
Create the interactive compliance map inspector. This tool shows the compliance map on the left,
......@@ -337,7 +311,7 @@ def comp_mat_inspector(comp_mat, z_piezo, tm_defl, params, fig_width = 10, r2s_m
# Add lines over points to fit
for cant_num in range(len(params["cantilevers"])):
(thick, width, start, end, igno, fixed, start, end, row, col_s, col_e) = get_cantilever_params(params, cant_num)
(thick, width, igno, fixed, start, end, row, col_s, col_e) = get_cantilever_params(params, cant_num)
axs["A"].plot([col_s, col_e], [row+0.5, row+0.5], 'r')
......
import pytest
from pyMFD.FV import FV
from pyMFD.cantilever import get_cantilever_pos, get_cantilever_params, get_compliance_row, fit_compliance_linear, calc_modulus_offset, standardize_and_fit
def test_all():
spm_file = "data/examples/02041411.001" # Example force-volume scan
fv = FV(spm_file) # Load force-volume scan
(comp_mat, r2s) = fv.summarize()
pos = get_cantilever_pos(fv.get_pixel_size(), comp_mat.shape[0])
(thick, width, igno, fixed, start, end, row, col_s, col_e) = get_cantilever_params(fv.sc_params, 0)
comp_row = get_compliance_row(comp_mat, row, rows_to_avg = 3)
(slope, intercept) = fit_compliance_linear(pos[col_s:col_e], comp_row[col_s:col_e])
(E_lin, off_lin) = calc_modulus_offset(slope, intercept, width, thick)
(E, offset, a) = standardize_and_fit(pos[col_s:col_e], comp_row[col_s:col_e]**3, width, thick)
assert pos.shape == (64,)
assert thick == pytest.approx(1.6e-07)
assert comp_row.shape == (64,)
assert slope is not None
assert E_lin is not None
assert E is not None
\ 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