Commit 6180dca9 authored by Kyle Larsen's avatar Kyle Larsen
Browse files

Added calc_modulus function to simplify estimating modulus. Updated docstring...

Added calc_modulus function to simplify estimating modulus. Updated docstring comments, adding return values.
parent 3aab5bd1
......@@ -4,7 +4,8 @@ from pyMFD.summarize import get_comp_mat
class FV:
'''
This class represents a single force-volume scan. It contains the relevant scan parameters and force-volume data.
This class represents a single force-volume scan. It contains the
relevant scan parameters and force-volume data.
'''
def __init__(
......@@ -25,20 +26,25 @@ class FV:
----------
fv_filename : str
fv_params_func : function, optional
Function that takes a string to the FV file and returns the required parameters in a dictionary.
See pyMFD.nanoscope for information on the required parameters.
Function that takes a string to the FV file and returns the
required parameters in a dictionary. See `pyMFD.nanoscope` for
nformation on the required parameters.
fv_data_func : function, optional
Function that takes a string to the FV file and parameters dictionary. Must return a tuple where
the first element is a 1-D np.ndarray containing the z_piezo ramp deflection series. The second
argument is a np.ndarray with shape (X, Y, Z) containing the force-volume tip deflection data.
X should be the size of the 1-D z_piezo ramp, Y should be 1 or 2 (depending on if only trace, or
trace and retrace are included), and Z should be the squared value of the size of the FV scan.
E.g. Z=4096 for a 64x64 "pixel" scan.
Function that takes a string to the FV file and parameters
dictionary. Must return a tuple where the first element is a
1-D np.ndarray containing the z_piezo ramp deflection series.
The second argument is a np.ndarray with shape (X, Y, Z)
containing the force-volume tip deflection data. X should be
the size of the 1-D z_piezo ramp, Y should be 1 or 2
(depending on if only trace, or trace and retrace are
included), and Z should be the squared value of the size of the
FV scan. E.g. Z=4096 for a 64x64 "pixel" scan.
sc_params_filename : str, optional
String containing the path to the scan parameters filename.
sc_params_func : function, optional
Function that takes sc_params_filename and returns a dictionary containing the required scan
parameters. See pyMFD.scan_params for information on the required parameters.
Function that takes sc_params_filename and returns a dictionary
containing the required scan parameters. See pyMFD.scan_params
for information on the required parameters.
'''
self.fv_filename = fv_filename
self.sc_params_filename = self.fv_filename + ".json" if sc_params_filename is None else sc_params_filename
......@@ -52,13 +58,20 @@ class FV:
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.
Calculate the size of a single pixel in the force-volume data.
Should be in units of meters.
Parameters
----------
scan_size: float, optional
scan_size : float, optional
The total size of the force-volume scan (in meters).
scan_points: int, optional
scan_points : int, optional
The number of force-deflection ramps in each line of the scan.
Returns
-------
float
Size of pixel (in meters).
'''
if scan_size is None:
scan_size = self.fv_params["scan_size"]
......@@ -70,13 +83,27 @@ class FV:
def get_extend(self):
'''
Return the force-volume data recorded during the extension of the AFM cantilever.
Return the force-volume data recorded during the extension of the
AFM cantilever.
Returns
-------
ndarray
The extension curves of the tapping mode deflection data.
Shape is (ramp_length, 1, num_curves), e.g. (1024, 1, 4096)
'''
return self.tm_defl[:, 0, :]
def get_retract(self):
'''
Return the force-volume data recorded during the retraction of the AFM cantilever.
Return the force-volume data recorded during the retraction of the
AFM cantilever.
Returns
-------
ndarray
The retraction curves of the tapping mode deflection data.
Shape is (ramp_length, 1, num_curves), e.g. (1024, 1, 4096)
'''
return self.tm_defl[:, 1, :]
......@@ -92,6 +119,20 @@ class FV:
Function that will perform the summary. By default, this is a function that takes `z_piezo`,
`tm_defl`, and `sc_params` and returns the compliance matrix and R^2 matrix (how well each curve
was summarized).
Returns
-------
Default return values if `summary_func`=`get_comp_mat`.
ndarray
Compliance matrix. Shape should be square, with the size of the
sides being the square root of the number of force ramps.
E.g. shape is (64, 64).
ndarray
R^2 matrix. See `comp` for shape.
See Also
--------
See `get_comp_mat()`.
'''
if which_dir == 'trace' or which_dir == 'extend' or which_dir == 0:
which_dir = 0
......
......@@ -4,14 +4,29 @@ 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.
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
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.
Returns
-------
thick, width : float
Thickness and width of the cantilever.
igno : int
Number of pixels to ignore from fixed end.
fixed : int
Pixel number of fixed end.
start, end : int
Start and end coordinates describing cantilever.
col_s, col_e : int
Column start and column end (i.e. the x-coordinate).
'''
thick = params["thickness"]
width = params["cantilevers"][cant_num]["width"]
......@@ -33,25 +48,37 @@ def get_cantilever_pos(pixel_size, size):
Parameters
----------
pixel_size: float
pixel_size : float
Size of pixel in meters.
size: int
size : int
Size of scan (number of pixels in scan).
Returns
-------
ndarray
List of cantilever positions (in meters).
'''
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.
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
comp_mat : ndarray
Compliance matrix
row: int
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
rows_to_avg : int, optional
Total number of rows to average. Will always be symmetric, rounded
up. Passing in 2 or 3 is equivalent.
Returns
-------
comp_row : ndarray
Returns the compliance data (possibly averaged).
'''
one_sided = rows_to_avg // 2
comp_row = comp_mat[(row - one_sided):(row + one_sided + 1), :]
......@@ -69,48 +96,72 @@ def fit_compliance_linear(position, compliance):
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).
The slope is proportional to E. To get the fixed end offset, divide the
intercept by the slope (and take negative).
Parameters
----------
position: ndarray
position : ndarray
Vector of positions (in meters).
compliance: ndarray
compliance : ndarray
Vector of linearized compliance.
Returns
-------
res.slope : float
Slope of the compliance data.
res.intercept : float
Y-intercept of the compliance data.
'''
res = stats.linregress(position, compliance)
return (res.slope, res.intercept)
def fit_fun(L, a, c):
'''
Function to fit with scipy.optimize.curve_fit.
Function to fit with scipy.optimize.curve_fit.
compliance = 1/k = 1/a*(L - c)**3
Parameters
----------
L: float
L : float
Position along cantilever
a: float
a : float
Combination of width, thickness, and modulus.
c: float
c : float
Offset from L.
Returns
-------
float
Compliance (inverse of stiffness)
'''
return 1/a*(L - c)**3
def standardize_and_fit(positions, compliances, width, thickness, func = fit_fun):
def fit_compliance(positions, compliances, width, thickness, func = fit_fun):
'''
Standardized and then fit the non-linearized (i.e. original) compliance data.
Standardized and then fit the non-linearized (i.e. original) compliance
data.
Parameters
----------
position: ndarray
position : ndarray
Vector of positions (in meters).
compliance: ndarray
compliance : ndarray
Vector of linearized compliance.
width: float
width : float
Width of cantilever
thick: float
thick : float
Thickness of cantilever
func: function
func : function
Returns
-------
E : float
Young's modulus
pos_off : float
Offset in initial guess of fixed end
a : float
This is the `a` parameter in `fit_fun()`.
'''
# Remove mean and set standard deviation to 1
......@@ -119,9 +170,10 @@ def standardize_and_fit(positions, compliances, width, thickness, func = fit_fun
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)
E = 4*popt[0]*sigma**3/(width*thickness**3)
pos_off = popt[1]*sigma + mu
return (Y, pos_off, popt[0]*sigma**3)
a = popt[0]*sigma**3
return (E, pos_off, a)
def calc_modulus_offset(slope, intercept, width, thickness):
'''
......@@ -129,14 +181,19 @@ def calc_modulus_offset(slope, intercept, width, thickness):
Parameters
----------
slope: float
slope : float
Slope returned from `fit_compliance_linear()`
intercept: float
intercept : float
Intercept returned from `fit_compliance_linear()`
width: float
width : float
Width of cantilever
thick: float
thick : float
Thickness of cantilever
Returns
-------
E, c : float
Young's modulus and position offset (`c`).
'''
E = 4/(slope**3 * width * thickness**3)
c = -intercept / slope
......@@ -148,14 +205,58 @@ 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.
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
offset : float
Offset calculated by fitting. This is how far the estimated fixed
end is from the origin selected for `position` array.
col_s : int
Column (from compliance map). This is the fixed end location.
pixel_size : float
Size of single pixel in meters.
Returns
-------
int
Offset location in column coordinates.
'''
return round(col_s + offset / pixel_size)
def calc_modulus(fv, cant_num, rows_to_avg = 1):
'''
return round(col_s + offset / pixel_size)
\ No newline at end of file
Calculate the modulus from cantilever compliance using both the cubic
model and linear model.
Parameters
----------
fv : FV
Object of class FV, which represents a single force-volume scan.
cant_num : int
Scans can contain more than one cantilever. This is an index
(starting at 0) to select
which cantilever to use.
rows_to_avg : int, optional
Total number of rows to average. Will always be symmetric, rounded
up. Passing in 2 or 3 is equivalent.
Returns
-------
E, offset : float
Modulus and offset from the cubic method
E_lin, offset_lin : float
Modulus and offset using the linear method
'''
(comp_mat, _) = fv.summarize()
pos = get_cantilever_pos(fv.get_pixel_size(), comp_mat.shape[0])
(thick, width, _, _, _, _, 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, offset_lin) = calc_modulus_offset(slope, intercept, width, thick)
(E, offset, a) = fit_compliance(pos[col_s:col_e], comp_row[col_s:col_e]**3, width, thick)
return (E, offset, E_lin, offset_lin)
\ No newline at end of file
......@@ -11,34 +11,46 @@ CSL = b'*Ciao scan list'
def read_fv_header(filename: str) -> dict:
'''
Read the header information from a Bruker/Veeco Nanoscope v7.2 file. Returns a dictionary containing all of the lines
from the header organized under the sections:
Read the header information from a Bruker/Veeco Nanoscope v7.2 file.
Returns a dictionary containing all of the lines from the header
organized under the sections:
- FFL = b'*Force file list'
- CFIL = b'*Ciao force image list'
- CIL = b'*Ciao image list'
- SL = b'*Scanner list'
- CSL = b'*Ciao scan list'
Nanoscope header files are a mess. There will be different sections depending on the type of data in the file. For more
information see Nanoscope User Guide and this informative forum post:
Nanoscope header files are a mess. There will be different sections
depending on the type of data in the file. For more information see
Nanoscope User Guide and this informative forum post:
- https://physics-astronomy-manuals.wwu.edu/Nanosocpe%207.3%20User%20Guide.pdf (broken link as of 2/23/2022)
- http://nanoqam.ca/wiki/lib/exe/fetch.php?media=nanoscope_software_8.10_user_guide-d_004-1025-000_.pdf
- http://nanoscaleworld.bruker-axs.com/nanoscaleworld/forums/p/538/1065.aspx
In the file header some parameters start with '\@' instead of simply '\'. This is an indication to the software
that the data that follows is intended for a CIAO parameter object. After the '@', you might see a number
followed by a colon before the label. This number is what we call a “group number” and can generally be
ignored.
In the file header some parameters start with '\@' instead of simply
'\'. This is an indication to the software that the data that follows
is intended for a CIAO parameter object. After the '@', you might see a
number followed by a colon before the label. This number is what is
called a “group number” and can generally be ignored.
Further, after the label and its colon, you will see a single definition character of 'V', 'C', or 'S'.
- V means _Value_ -- a parameter that contains a double and a unit of measure, and some scaling definitions.
- 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
Further, after the label and its colon, you will see a single
definition character of 'V', 'C', or 'S'.
- V means _Value_ -- a parameter that contains a double and a unit of
measure, and some scaling definitions.
- 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 : str
Filename of the NanoScope file.
Returns
-------
params : dict
Raw paramaters dictionary. Convert with `convert_params()`
'''
# May be useful:
......@@ -93,9 +105,9 @@ 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.
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:
......@@ -115,12 +127,18 @@ def convert_params(old_params, custom_to_extract = []):
Parameters
----------
old_params: dict
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)
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)
Returns
-------
params : dict
Params dictionary with new parameter names.
'''
......@@ -158,27 +176,37 @@ def convert_params(old_params, custom_to_extract = []):
def read_fv_data(filename: str, params: dict) -> np.ndarray:
'''
Read the force-volume or force-ramp data from a Nanoscope file. The data is converted from binary
representation to a float64 representation of the the SPM data in ADC counts. Convert to volts
Read the force-volume or force-ramp data from a Nanoscope file. The
data is converted from binary representation to a float64
representation of the the SPM data in ADC counts. Convert to volts
using `convert_fv_data`.
A force-volume scan contains three dimensions of data. For every point in a 2D array, two force-ramps are
recorded (one for extension towards the sample and one for retraction -- also called trace and retrace).
A force-volume scan contains three dimensions of data. For every point
in a 2D array, two force-ramps are recorded (one for extension towards
the sample and one for retraction -- also called trace and retrace).
The raw data should have a size equal to the number of points in the 2D array times the number of samples in
the force-ramp all times two (for extend and retract).
The raw data should have a size equal to the number of points in the 2D
array times the number of samples in the force-ramp all times two (for
extend and retract).
For example, a 64x64 with 1024 samples per force-ramp will have a data length of:
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
filename : str
Path to NanoScope scan file.
params: dict
params : dict
Parameters dictionary. From `get_params()`.
Returns
-------
ndarray
NanoScope scan data, unpacked from raw bytes.
'''
offset = params["fv_data_offset"]
data_length = params["fv_data_length"]
......@@ -207,15 +235,25 @@ def read_fv_data(filename: str, params: dict) -> np.ndarray:
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).
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
data : ndarray
Raw data from force-volume file (from `read_fv_data()`).
params: dict
params : dict
Parameters dictionary. From `get_params()`.
Returns
-------
z_piezo : ndarray
Displacement of AFM piezo. Has size `ramp_len` (from parameters
`samples_per_ramp`).
tm_defl : ndarray
Tapping mode deflection. Has shape (`ramp_len`, 2, `num_curves`).
The 2 comes from having both an extension and retraction.
'''
z_sens = params["piezo_nm_per_volt"]
ramp_size = params["ramp_size"]
......@@ -239,19 +277,31 @@ def convert_fv_data(data: np.ndarray, params: dict) -> tuple:
def get_fv_data(filename: str, params: dict) -> tuple:
'''
Get the `z_piezo` deflection ramp. `params` should be the converted, generalized parameter dictionary.
Get `z_piezo` and `tm_defl`. `params` should be the converted,
generalized parameter dictionary.
Parameters
----------
filename: str
filename : str
Path to NanoScope scan file.
params: dict
params : dict
Parameters dictionary. From `get_params()`.
Returns
-------
z_piezo : ndarray
Displacement of AFM piezo. Has size `ramp_len` (from parameters
`samples_per_ramp`).
tm_defl : ndarray
Tapping mode deflection. Has shape (`ramp_len`, 2, `num_curves`).
The 2 comes from having both an extension and retraction.
'''
data = read_fv_data(filename, params)
# Convert to metric units
return convert_fv_data(data, params)
(z_piezo, tm_defl) = convert_fv_data(data, params)
return (z_piezo, tm_defl)
def get_params(filename: str) -> dict:
'''
......@@ -259,8 +309,13 @@ def get_params(filename: str) -> dict:
Parameters
----------
filename: str
filename : str
Path to NanoScope scan file.
Returns
-------
params : dict
Params dictionary with new parameter names.
'''
all_fv_params = read_fv_header(filename )
fv_params = convert_params(all_fv_params)
......@@ -269,13 +324,14 @@ 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.
Save the converted data to an ASCII file using the same format as
exports from Nanoscope Analysis 2.0.
Parameters
----------
data: ndarray
data : ndarray
Converted data to be saved in ASCII format.
filename: str
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"
......
......@@ -4,8 +4,9 @@ def get_scan_params(sp_filename: str) -> dict:
'''
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 (and including) '#' 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
......@@ -37,8 +38,14 @@ def get_scan_params(sp_filename: str) -> dict:
Parameters
----------
sp_filename: str
Filename string (passed to json.load()) pointing to scan parameter JSON file.
sp_filename : str
Filename string (passed to json.load()) pointing to scan parameter
JSON