This page was generated by
nbsphinx from
docs/notebooks/analysis/swept_langmuir/find_floating_potential.ipynb.
Interactive online version:
.
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]
Contents:¶
How find_floating_potential()
works¶
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.”
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, ifthreshold=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 thanmin_points
, then the function is incapable of identifying \(V_f\) and will returnnumpy.nan
values; otherwise, the span will form one larger crossing-island.
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 untilmin_points
is satisfied.If
fit_type="linear"
, then ascipy.stats.linregress
fit is applied to the points that make up the crossing-island.If
fit_type="exponential"
, then ascipy.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
, thenthreshold
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 andfactor * array_size
is taken, wherefactor = 0.1
for"linear"
and0.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 asmin_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 passvoltage
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 asresults.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 functionresults.func
is a callable representation of the fitted functionI = results.func(V)
.resulst.func
is an instance of a sub-class ofAbstractFitFunction
. (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 parametersresults.func.param_errors
is a named tuple of the fitted parameter errorsresults.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",
)
