This page was generated by nbsphinx from docs/notebooks/analysis/swept_langmuir/find_floating_potential.ipynb.
Interactive online version: Binder badge.

Swept Langmuir Analysis: Floating Potential

This notebook covers the use of the **find_floating_potential()** function and how it is used to determine the floating potential from a swept Langmuir trace.

The floating potential, \(V_f\), is defined as the probe bias voltage at which there is no net collected current, \(I=0\). This occurs because the floating potential slows the collected electrons and accelerates the collected ions to a point where the electron- and ion-currents balance each other out.

[1]:
%matplotlib inline

import matplotlib.pyplot as plt
import numpy as np
import pprint

from pathlib import Path

from plasmapy.analysis import swept_langmuir as sla

plt.rcParams["figure.figsize"] = [10.5, 0.56 * 10.5]

How find_floating_potential() works

  1. The passed current array is scanned for points that equal zero and point-pairs that straddle where the current, \(I\), equals zero. This forms an collection of “crossing-points.”

  2. The crossing-points are then grouped into “crossing-islands” based on the threshold keyword.

    • A new island is formed when a successive crossing-point is more (index) steps away from the previous crossing-point than defined by threshold. For example, if threshold=4 then an new island is formed if a crossing-point candidate is more than 4 steps away from the previous candidate.

    • If multiple crossing-islands are identified, then the function will compare the total span of all crossing-islands to min_points. If the span is greater than min_points, then the function is incapable of identifying \(V_f\) and will return numpy.nan values; otherwise, the span will form one larger crossing-island.

  3. To calculate the floating potential…

    • If the number of points that make up the crossing-island is less than min_points, then each side of the “crossing-island” is equally padded with the nearest neighbor points until min_points is satisfied.

    • If fit_type="linear", then a scipy.stats.linregress fit is applied to the points that make up the crossing-island.

    • If fit_type="exponential", then a scipy.optimize.curve_fit fit is applied to the points that make up the crossing-island.

Notes about usage

  • The function provides no signal processing. If needed, the user must smooth, sort, crop, or process the arrays before passing them to the function.

  • The function requires the voltage array to be monotonically increasing or decreasing.

  • If the total range spanned by all crossing-islands is less than or equal to min_points, then threshold is ignored and all crossing-islands are grouped into one island.

Knobs to turn

  • fit_type

    There are two types of curves that can be fitted to the identified crossing point data: "linear" and "exponential". The former will fit a line to the data, whereas, the latter will fit an exponential curve with an offset. The default curve is "exponential" since swept Langmuir data is not typically linear as it passes through \(I=0\).

  • min_points

    This variable specifies the minimum number of points that will be used in the curve fitting. As mentioned above, the crossing-islands are identified and then padded until min_points is satisfied.

    • min_pints = None (Default) then the larger of 5 and factor * array_size is taken, where factor = 0.1 for "linear" and 0.2 for "exponential".

    • min_points = numpy.inf then the entire passed array is fitted.

    • min_points >= 1 then this is the minimum number of points used.

    • 0 < min_points < 1 then then the minimum number of points is taken as min_points * array_size.

  • threshold

    The max allowed index distance between crossing-points before a new crossing-island is formed.

Calculate the Floating Potential

Below we’ll compute the floating potential using the default fitting behavior (fit_type="exponential") and a linear fit (fit_type="linear").

[2]:
# load data
filename = "Beckers2017_noisy.npy"
filepath = (Path.cwd() / ".." / ".." / "langmuir_samples" / filename).resolve()
voltage, current = np.load(filepath)

# voltage array needs to be monotonically increasing/decreasing
isort = np.argsort(voltage)
voltage = voltage[isort]
current = current[isort]

# get default fit results (exponential fit)
results = sla.find_floating_potential(voltage, current, min_points=0.3)

# get linear fit results
results_lin = sla.find_floating_potential(voltage, current, fit_type="linear")

Interpreting results

The find_floating_potential() function returns a six element named tuple, where…

  • results[0] = results.vf = the determined floating potential (same units as the pass voltage array)

[3]:
(results[0], results.vf)
[3]:
(-5.665729883017972, -5.665729883017972)
  • results[1] = results.vf_err = the associated uncertainty in the \(V_F\) calculation (same units as results.vf)

[4]:
(results[1], results.vf_err)
[4]:
(0.45174046877528606, 0.45174046877528606)
  • results[2] = results.rsq = the coefficient of determination (r-squared) value of the fit

[5]:
(results[2], results.rsq)
[5]:
(0.9564766142728285, 0.9564766142728285)
  • results[3] = results.func = the resulting fitted function

    • results.func is a callable representation of the fitted function I = results.func(V).

    • resulst.func is an instance of a sub-class of AbstractFitFunction. (FitFuction classes)

    • Since results.func is a class instance, there are many other attributes available. For example,

      • results.func.params is a named tuple of the fitted parameters

      • results.func.param_errors is a named tuple of the fitted parameter errors

      • results.func.root_solve() finds the roots of the fitted function. This is how \(V_f\) is calculated.

[6]:
(
    results[3],
    results.func,
    results.func.params,
    results.func.params.a,
    results.func.param_errors,
    results.func.param_errors.a,
    results.func(results.vf),
)
[6]:
(f(x) = a exp(alpha x) + b <class 'plasmapy.analysis.fit_functions.ExponentialPlusOffset'>,
 f(x) = a exp(alpha x) + b <class 'plasmapy.analysis.fit_functions.ExponentialPlusOffset'>,
 FitParamTuple(a=0.01027537751642042, alpha=0.3382781633131013, b=-0.001511583538487724),
 0.01027537751642042,
 FitParamTuple(a=0.00031440591547328146, alpha=0.021631300563905463, b=0.00012999282477305467),
 0.00031440591547328146,
 -4.336808689942018e-19)
  • results[4] = results.islands = a list of slice objects representing all the indentified crossing-islands

[7]:
(
    results[4],
    results.islands,
    voltage[results.islands[0]],
)
[7]:
([slice(192, 195, None), slice(201, 210, None)],
 [slice(192, 195, None), slice(201, 210, None)],
 array([-7.49600064, -7.22376048, -7.15120308]))
  • results[5] = results.indices = a slice object representing the indices used in the fit

[8]:
(
    results[5],
    results.indices,
    voltage[results.indices],
)
[8]:
(slice(155, 247, None),
 slice(155, 247, None),
 array([-11.65942357, -11.64882654, -11.53579538, -11.38604798,
        -11.11860447, -11.11179009, -11.09142646, -11.00483855,
        -10.99442158, -10.57322789, -10.50059601, -10.37949917,
        -10.315585  , -10.2698363 , -10.14162481,  -9.97124346,
         -9.62883006,  -9.5755686 ,  -9.36686515,  -9.28450711,
         -9.07258268,  -9.06560791,  -8.91301953,  -8.8817602 ,
         -8.76066758,  -8.74794412,  -8.74340073,  -8.72406653,
         -8.55441808,  -8.34046557,  -8.2062713 ,  -8.13718384,
         -7.98570514,  -7.68529681,  -7.65010823,  -7.55378266,
         -7.49960431,  -7.49600064,  -7.22376048,  -7.15120308,
         -7.02994327,  -6.77525529,  -6.51507915,  -6.39924154,
         -6.36984977,  -6.24817453,  -6.20213736,  -6.19568807,
         -6.04787946,  -5.76312984,  -5.46965878,  -5.32623331,
         -5.119681  ,  -5.0823194 ,  -5.0486728 ,  -4.95381421,
         -4.88749753,  -4.78029289,  -4.68512796,  -4.51734484,
         -4.40678539,  -4.19565637,  -4.17704741,  -4.17117347,
         -4.10283824,  -4.00779631,  -3.93855879,  -3.82827448,
         -3.78163806,  -3.27907775,  -3.10329043,  -3.09674414,
         -3.09107256,  -2.96068724,  -2.80465682,  -2.60784067,
         -2.26018439,  -2.17330689,  -2.15893471,  -1.98233593,
         -1.94282875,  -1.91200309,  -1.71890015,  -1.60465752,
         -1.56380995,  -1.38993179,  -1.32157414,  -1.30011675,
         -1.28811388,  -1.25099997,  -0.98118767,  -0.85042501]))

Plotting results

[9]:
figwidth, figheight = plt.rcParams["figure.figsize"]
figheight = 2.0 * figheight
fig, axs = plt.subplots(3, 1, figsize=[figwidth, figheight])

# plot original data
axs[0].set_xlabel("Bias Voltage (V)", fontsize=12)
axs[0].set_ylabel("Current (A)", fontsize=12)

axs[0].plot(voltage, current, zorder=10, label="Sweep Data")
axs[0].axhline(0.0, color="r", linestyle="--", label="I = 0")
axs[0].legend(fontsize=12)

# zoom on fit
for ii, label, fit in zip([1, 2], ["Exponential", "Linear"], [results, results_lin]):
    # calc island points
    isl_pts = np.array([], dtype=np.int64)
    for isl in fit.islands:
        isl_pts = np.concatenate((isl_pts, np.r_[isl]))

    # calc xrange for plot
    xlim = [voltage[fit.indices].min(), voltage[fit.indices].max()]
    vpad = 0.25 * (xlim[1] - xlim[0])
    xlim = [xlim[0] - vpad, xlim[1] + vpad]

    # calc data points for fit curve
    mask1 = np.where(voltage >= xlim[0], True, False)
    mask2 = np.where(voltage <= xlim[1], True, False)
    mask = np.logical_and(mask1, mask2)
    vfit = np.linspace(xlim[0], xlim[1], 201, endpoint=True)
    ifit, ifit_err = fit.func(vfit, reterr=True)

    axs[ii].set_xlabel("Bias Voltage (V)", fontsize=12)
    axs[ii].set_ylabel("Current (A)", fontsize=12)
    axs[ii].set_xlim(xlim)

    axs[ii].plot(
        voltage[mask], current[mask], marker="o", zorder=10, label="Sweep Data",
    )
    axs[ii].scatter(
        voltage[fit.indices],
        current[fit.indices],
        linewidth=2,
        s=6 ** 2,
        facecolors="deepskyblue",
        edgecolors="deepskyblue",
        zorder=11,
        label="Points for Fit",
    )
    axs[ii].scatter(
        voltage[isl_pts],
        current[isl_pts],
        linewidth=2,
        s=8 ** 2,
        facecolors="deepskyblue",
        edgecolors="black",
        zorder=12,
        label="Island Points",
    )
    axs[ii].autoscale(False)
    axs[ii].plot(vfit, ifit, color="orange", zorder=13, label=label + " Fit")
    axs[ii].fill_between(
        vfit,
        ifit + ifit_err,
        ifit - ifit_err,
        color="orange",
        alpha=0.12,
        zorder=0,
        label="Fit Error",
    )
    axs[ii].axhline(0.0, color="r", linestyle="--")
    axs[ii].fill_between(
        [fit.vf - fit.vf_err, fit.vf + fit.vf_err],
        axs[1].get_ylim()[0],
        axs[1].get_ylim()[1],
        color="grey",
        alpha=0.1,
    )
    axs[ii].axvline(fit.vf, color="grey")
    axs[ii].legend(fontsize=12)

    # add text
    rsq = fit.rsq
    txt = f"$V_f = {fit.vf:.2f} \\pm {fit.vf_err:.2f}$ V\n"
    txt += f"$r^2 = {rsq:.3f}$"
    txt_loc = [fit.vf, axs[ii].get_ylim()[1]]
    txt_loc = axs[ii].transData.transform(txt_loc)
    txt_loc = axs[ii].transAxes.inverted().transform(txt_loc)
    txt_loc[0] -= 0.02
    txt_loc[1] -= 0.26
    axs[ii].text(
        txt_loc[0],
        txt_loc[1],
        txt,
        fontsize="large",
        transform=axs[ii].transAxes,
        ha="right",
    )
../../../_images/notebooks_analysis_swept_langmuir_find_floating_potential_21_0.png