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.
%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]
find_floating_potential()
works¶threshold
keyword.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.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.min_points
, then each side of the "crossing-island" is equally padded with the nearest neighbor points until min_points
is satisfied.fit_type="linear"
, then a scipy.stats.linregress
fit is applied to the points that make up the crossing-island.fit_type="exponential"
, then a scipy.optimize.curve_fit
fit is applied to the points that make up the crossing-island.min_points
, then threshold
is ignored and all crossing-islands are grouped into one island.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.
Below we'll compute the floating potential using the default fitting behavior (fit_type="exponential"
) and a linear fit (fit_type="linear"
).
# 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")
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)(results[0], results.vf)
results[1]
= results.vf_err
= the associated uncertainty in the $V_F$ calculation (same units as results.vf
)(results[1], results.vf_err)
results[2]
= results.rsq
= the coefficient of determination (r-squared) value of the fit(results[2], results.rsq)
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)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.(
results[3],
results.func,
results.func.params,
results.func.params.a,
results.func.param_errors,
results.func.param_errors.a,
results.func(results.vf),
)
results[4]
= results.islands
= a list of slice objects representing all the indentified crossing-islands(
results[4],
results.islands,
voltage[results.islands[0]],
)
results[5]
= results.indices
= a slice object representing the indices used in the fit(
results[5],
results.indices,
voltage[results.indices],
)
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",
)