This script demonstrates how to open a Nanosurf Force Spectroscopy map and analyze the data using the DMT contact mechanics model¶

In [1]:
from NSFopen.read import read

import numpy as np

import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.axes_divider import make_axes_locatable
from scipy.optimize import curve_fit

from glob import glob

filename = 'spectroscopy.nid'
filename_large = 'spectroscopy_large.nid'
In [2]:
# If USE_LARGE_MAP is True, then it will use the larger force-curve map, downloading if not already locally available 
USE_LARGE_MAP = False
In [3]:
if USE_LARGE_MAP and filename_large not in glob('*nid'):
    import urllib.request
    urllib.request.urlretrieve("https://www.dropbox.com/s/knzuh588g0uvjfi/spectroscopy_large.nid?dl=1", filename_large)
    filename = filename_large
elif USE_LARGE_MAP:
    filename = filename_large
In [4]:
# Read Date
afm = read(filename, verbose=False)
data = afm.data
param = afm.param

# probe
k = param.Tip.Prop0['Value']  # spring constant
sens = param.Tip.Sensitivity['Value']  # deflection sensitivity (if needed)

radius = 10e-9  # tip radius
nu = 0.35  # Poisson ratio (between 0.35 and 0.5)

spec_backward = data['Spec']['Backward']  # for DMT model we will use backward modulation
d = spec_backward['Deflection']  # force channel

z_signal = list(spec_backward.index)
z_chans = ['Z-Axis Sensor', 'Z-AxisSensor', 'Z-Axis']  # depending on system, Z-Axis channel may be labeled differently
available_chan = [i for i in z_signal if i in z_chans][0]

z = data['Spec']['Backward'][available_chan]  # Z-axis data

sz = int(param.Spec.maps[4])  # Map size

extent = param.Spec.maps[:4]  # X and Y physical dimensions of map
In [5]:
## Functions

# corrects for any baseline tilt
def baseline_correct(x, y, degree=1, start=0, end=0.5):
    i0 = int(np.floor(len(x) * start))
    i1 = int(np.floor(len(x) * end))

    xx = x[i0:i1]
    yy = y[i0:i1]

    p = np.polyfit(xx, yy, degree)

    y -= np.polyval(p, x)
    return y


# fits DMT model to data
def DMT(x, *param):
    elast, depth, wadh = param

    scale = 4./3. * elast * np.sqrt(radius) / (1 - nu**2)
    x = x - depth
    y = scale * np.sign(x) * np.abs(x)**(3./2.) - wadh
    return y


def DMTfit(*args, maxForce=np.Inf):
    import time
    t = time.time()
    # args are: [separation, load (force)]
    if not isinstance(args[0], list):
        args = [[ar] for ar in args]

    E = []
    for x, y in zip(*args):
        # x = x - y / spring_constant  # separation
        lenY = int(np.floor(len(y) * 0.8))
        a = min(y[lenY:])  # adhesion
    
        i0 = np.where(y == a)[0][0]  # contact point estimator index
        i1 = np.where(y > maxForce)[0]  # max force index

        if i1.size == 0:
            i1 = -1
        else:
            i1 = i1[0]

        try:
            coeff, var_matrix = curve_fit(DMT, x[i0:i1], y[i0:i1], p0=[1e9, x[i0], -a])
        except RuntimeError:
            E.append(np.nan)
        else:
            E.append(max(coeff[0], 0))

    print('Elapsed Time: %3.2f sec\n' % (time.time() - t))
    return np.array(E), (coeff, i0, i1)
In [6]:
# correct for tilt in baseline
dd = [baseline_correct(z_, d_, start=0.5, end=0.8) for z_, d_ in zip(z, d)]

# separation
zz = [zz - dd/k for dd, zz in zip(dd, z)]

# elastic modulus
Eb, _ = DMTfit(zz, dd)
Eb /= 1e9

# adhesion
adh = np.array([np.min(d) for d in dd]) * -1e9


# converts 1-D array to 2-D map and puts rows in correct order
def toMap(arr, sz):
    arr_ = np.reshape(arr, (sz, sz))
    arr_[::2] = np.fliplr(arr_[::2])
    return np.fliplr(arr_)


Ebim = toMap(Eb, sz)
Adh = toMap(adh, sz)
Elapsed Time: 0.24 sec

In [7]:
fig, ax = plt.subplots(2, 2, figsize=(10, 8), gridspec_kw={'height_ratios': [2, 1]}, dpi=300)
fig.tight_layout(w_pad=8.0, h_pad=1.0)

im = ax.copy()

cmap = "afmhot"  # colormap
xlbl = 'X [$\mu$m]'
ylbl = 'Y [$\mu$m]'
zlbl = ["Young's Modulus [GPa]", "Adhesion [nN]"]

im[0, 0] = ax[0, 0].imshow(Ebim)
im[0, 1] = ax[0, 1].imshow(Adh)
im[1, 0] = ax[1, 0].hist(Eb)
im[1, 1] = ax[1, 1].hist(adh)

# sets labels and colorbar, etc.
for i in range(2):
    ax_divider = make_axes_locatable(ax[0, i])
    cax = ax_divider.append_axes("right", size="5%", pad="2%")
    cb = plt.colorbar(im[0, i], cax=cax)
    cb.set_label(zlbl[i])

    ax[0, i].set(xlabel=xlbl, ylabel=ylbl)
    ax[1, i].set(xlabel=zlbl[i], ylabel='Counts')


plt.show()