-
Notifications
You must be signed in to change notification settings - Fork 353
[SLA] Find dI/dV
peak
#3057
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
[SLA] Find dI/dV
peak
#3057
Changes from 250 commits
d4d0605
aeb5247
2f4f997
f2e4f43
0ac26b9
bae6520
25e14e6
51e2edf
cea466c
d20998a
b5eee88
6fac0d1
dd0cc76
15f4534
1b4c81d
f15b32d
11d2cde
ff1aa24
89bb8eb
8599653
28ca101
1900b93
6498a66
d2b5d29
66789a9
20fde53
32f905b
10abc65
c953d68
97a31f8
61e61d2
9561b67
dcb0950
565c6c5
0c4de1b
279914a
a197e1c
153edc2
f7ffdfd
f5f1453
a42db86
1f51aa2
647fd83
a64406d
e6d5a35
725484e
ee2651e
b44d614
2bce434
c018cf1
bd6027e
0ecbf59
8544d19
cd1721d
7b4db0b
3a0ec9a
1d59ef4
ce91d9d
f8b3895
b107a6d
0675792
9139141
3cb003f
73c045e
6ba601b
a190cb0
6bd03f7
63696e3
c4de72b
06dd3a3
aab29c7
1b2449c
b23843e
6b8c24b
c80e3d4
81dcff6
ef60181
135308b
7d0e275
b5d0c13
04bdefc
9be3f3f
1330aea
3cc421d
e8b0812
e99fba8
729755d
a215314
84015a1
aa50427
a3094ad
4242fe6
8805ca5
289dbcc
95cbbee
421feda
0368af3
4e6ff2c
271d00d
1bf8b15
984d7cd
c3ee9ff
a804016
e6bbf79
fa73371
7ed8c05
79feb55
4c21080
45a61c5
b5b76da
58710bd
ee7c4a2
47fc01a
18bb22b
626dedb
1c5271a
03309ac
71b46b5
3feaa67
ae1e72e
7d1a207
fce4214
0ab23b9
f6e0852
66e1af4
f587843
d9ad848
b05d76a
1be80d8
06e2fc3
295d5b8
ccea90d
5484347
d978467
66518b9
e7c075e
84ab13e
2663ab5
19b83de
45bc39e
09d807b
e9e77b2
0d1a63a
2b997ec
a0b4af7
a2382aa
31d6969
e4a6c78
a0c545d
0d70ea2
5febd10
dab2bfa
96a78d7
440beaa
b1359c3
115835f
a4144d4
1638c5e
6e1838b
50e7364
30daae3
a1df942
51071e8
641126a
786dcfc
5d9fda3
337b7da
ab59694
fd7b674
2e88fca
c995db8
8d12c14
35c5442
ebd1046
cf8e7c6
adaea82
49ffb54
6a4eb39
02e5b79
d14ada6
b2dbec4
15c386b
e7091f9
b4fcb7f
278c273
c2b0d76
8bae949
c36d1cd
8ca2dfd
6c2766e
b314f62
3a935a2
a3eff39
7b94e1d
2e9d321
1a399ae
f598fda
06094a4
4318df5
dcea729
e4cdf87
64766ab
3d3ed5d
446bf9a
80acae6
0d6cb8f
1412afc
5b00125
7b87e1f
2f89e21
c242dd4
dd89acd
d41dfc6
684343e
6f4621d
f725d84
7f4a337
4a2ab10
15653ba
75fab19
82f73aa
15eceef
3a3946d
0abf9ea
6ac2a09
9be5e55
199191c
28385ef
e6b5378
08dd09b
593e76e
7e9c000
9a03f12
a1fffc7
a8d3deb
5473909
339a3ea
5fa7b86
7dedac5
ad41816
f2ca356
5d8bd89
bbb5760
66fb53f
bd25c14
201f597
4efb1b9
2921590
405528a
755cb56
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,252 @@ | ||
""" | ||
Functionality for determining the plasma potential of a Langmuir sweep. | ||
""" | ||
|
||
__all__ = ["find_didv_peak"] | ||
__aliases__ = [] | ||
__all__ += __aliases__ | ||
|
||
import numbers | ||
from collections.abc import Sequence | ||
from typing import NamedTuple | ||
|
||
import numpy as np | ||
from scipy import signal | ||
|
||
from plasmapy.analysis.swept_langmuir.helpers import check_sweep, merge_voltage_clusters | ||
|
||
|
||
class dIdVExtras(NamedTuple): # noqa: N801 | ||
std: float | None | ||
data_slice: slice | None | ||
savgol_windows: list[int] | None | ||
savgol_peaks: list[float] | None | ||
|
||
|
||
def _condition_voltage_window(voltage, voltage_window) -> slice: | ||
""" | ||
Condition ``voltage_window`` and return resulting `slice` object to | ||
index ``voltage``. | ||
""" | ||
if voltage_window is None: | ||
voltage_window = [None, None] | ||
elif not isinstance(voltage_window, Sequence): | ||
raise TypeError( | ||
f"Expected a 2-element list of floats or None for 'voltage_window', " | ||
f"but got type {type(voltage_window)}." | ||
) | ||
elif len(voltage_window) != 2: | ||
raise ValueError( | ||
f"Expected a 2-element list of floats or None for 'voltage_window', " | ||
f"but got type {len(voltage_window)} elements." | ||
) | ||
elif not all( | ||
isinstance(element, numbers.Real) or element is None | ||
for element in voltage_window | ||
): | ||
raise TypeError(f"Not all elements of 'voltage_window' are floats or None.") | ||
elif None not in voltage_window: | ||
voltage_window = np.sort(voltage).tolist() | ||
|
||
# determine data window | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's probably in Data's quarters on the Enterprise, NCC-1701D |
||
if ( | ||
voltage_window[0] is None | ||
or voltage_window[0] <= voltage[0] | ||
): | ||
first_index = None | ||
elif voltage_window[0] >= voltage[-1]: | ||
raise ValueError( | ||
f"The min value for the voltage window ({voltage_window[0]}) " | ||
f"is larger than the max value of the langmuir trace " | ||
f"({voltage[-1]})." | ||
) | ||
else: | ||
first_index = int(np.where(voltage >= voltage_window[0])[0][0]) | ||
|
||
if ( | ||
voltage_window[1] is None | ||
or voltage_window[1] >= voltage[-1] | ||
): | ||
last_index = None | ||
elif voltage_window[1] <= voltage[0]: | ||
raise ValueError( | ||
f"The max value for the voltage window ({voltage_window[1]}) " | ||
f"is smaller than the min value of the langmuir trace " | ||
f"({voltage[0]})." | ||
) | ||
else: | ||
last_index = int(np.where(voltage < voltage_window[1])[0][0]) | ||
|
||
return slice(first_index, last_index, 1) | ||
|
||
|
||
def _condition_smooth_fractions(smooth_fractions, data_size): | ||
""" | ||
Condition ``smooth_fractions`` and return the resulting | ||
Savitzky-Golay filter windows sizes, based on the ``data_size``. | ||
""" | ||
if smooth_fractions is None: | ||
# Note: If this default value is changed, then the docstring entry | ||
# for smooth_fractions in the find_didv_peak() docstring | ||
# needs to be updated accordingly. | ||
smooth_fractions = np.linspace(0.01, 0.25, num=30) | ||
rocco8773 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
elif ( | ||
isinstance(smooth_fractions, Sequence) | ||
and not isinstance(smooth_fractions, np.ndarray) | ||
): | ||
smooth_fractions = np.array(smooth_fractions) | ||
|
||
if not isinstance(smooth_fractions, np.ndarray): | ||
raise TypeError( | ||
"Expected a 1-D list of floats in the interval (0, 1] for argument " | ||
f"'smooth_fractions', but got type {type(smooth_fractions)}." | ||
) | ||
elif smooth_fractions.ndim != 1: | ||
raise ValueError( | ||
"Expected a 1-D list of floats in the interval (0, 1] for argument " | ||
f"'smooth_fractions', but got a {smooth_fractions.ndim}-D list.") | ||
elif not np.issubdtype(smooth_fractions.dtype, np.floating): | ||
raise ValueError( | ||
"Expected a 1-D list of floats in the interval (0, 1] for argument " | ||
f"'smooth_fractions', not all elements are floats." | ||
) | ||
|
||
smooth_fractions = np.unique(np.sort(smooth_fractions)) | ||
mask1 = smooth_fractions > 0 | ||
mask2 = smooth_fractions <= 1 | ||
mask = np.logical_and(mask1, mask2) | ||
if np.count_nonzero(mask) == 0: | ||
raise ValueError( | ||
"Expected a 1-D list of floats in the interval (0, 1] for argument " | ||
f"'smooth_fractions', no elements are within this interval " | ||
f"{smooth_fractions.tolist()}." | ||
) | ||
|
||
# create bin sizes (savgol_windows) for the savgol_filter | ||
savgol_windows = np.unique(np.rint(smooth_fractions * data_size).astype(int)) | ||
|
||
# windows need to have at least 2 points | ||
mask = savgol_windows > 2 | ||
savgol_windows = savgol_windows[mask] | ||
|
||
# force windows sizes to be odd | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps add a comment why? Like..is an odd number of windows a requirement for the algorithm? |
||
mask = savgol_windows % 2 == 0 | ||
if np.count_nonzero(mask) > 0: | ||
savgol_windows[mask] = savgol_windows[mask] + 1 | ||
savgol_windows = np.unique(savgol_windows) | ||
|
||
# do not let windows sizes exceed data_size | ||
mask = savgol_windows <= data_size | ||
savgol_windows = savgol_windows[mask] | ||
|
||
# check savgol_windows is not null | ||
if savgol_windows.size == 0: | ||
raise ValueError( | ||
f"The given smooth_fractions ({smooth_fractions}) and " | ||
f"window size ({data_size}) resulted in no valid Savitzky-Golay " | ||
f"filter windows. Computed windows must be odd, greater than 3, " | ||
f"and less than or equal to the windows size." | ||
) | ||
|
||
return savgol_windows | ||
|
||
|
||
def find_didv_peak( # noqa: C901, PLR0912 | ||
voltage: np.ndarray, | ||
current: np.ndarray, | ||
*, | ||
voltage_window: list[float | None] | None = None, | ||
smooth_fractions: list[float] | None = None, | ||
) -> tuple[float, dIdVExtras]: | ||
""" | ||
Find the peak slope (:math:`dI/dV_{max}`) of the swept Langmuir | ||
trace. | ||
|
||
The peak slope is often used as a rough estimate of the plasma potential. | ||
However, it will always be slightly less than the actual plasma | ||
potential. | ||
|
||
Parameters | ||
---------- | ||
voltage : `numpy.ndarray` | ||
1-D numpy array of monotonically increasing probe biases | ||
(should be in volts). | ||
|
||
current : `numpy.ndarray` | ||
1-D numpy array of probe current (should be in amperes) | ||
corresponding to the ``voltage`` array. | ||
|
||
voltage_window : `list[float | None]` | `None`, default: `None` | ||
A two-element list ``[v_min, v_max]`` that specifies the voltage | ||
range in which the peak slope will be looked for. Specifying | ||
`None` for either the first or second element will result in a | ||
window using the lower or upper bound of ``voltage`` | ||
respectively. If set to `None` (default), then the whole | ||
``voltage`` window will be used. | ||
|
||
smooth_fractions : `list[float]` | `None`, default: `None` | ||
An order list of fractions in the interval :math:`(0, 1]` used | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
to compute the Savitzky-Golay filter window sizes. For example, | ||
if the ``voltage_windows`` had a size of 50, then a | ||
``smooth_fraction`` value of 0.5 would result in a | ||
Savitzky-Golay window size of 25. If `None` (default), then | ||
``smooth_fractions`` will default to | ||
``numpy.linspace(0.01, 0.25, num=30)``. | ||
|
||
Notes | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm wondering if it would suffice to refer the reader to Wikipedia. 🤔 |
||
----- | ||
|
||
Add details about algorithm. | ||
""" | ||
rtn_extras = dIdVExtras( | ||
std=None, | ||
data_slice=None, | ||
savgol_windows=None, | ||
savgol_peaks=None, | ||
)._asdict() | ||
|
||
# check voltage and current arrays | ||
voltage, current = check_sweep(voltage, current, strip_units=True) | ||
voltage, current = merge_voltage_clusters(voltage, current, voltage_step_size=0) | ||
|
||
# condition voltage_window | ||
_slice = _condition_voltage_window(voltage, voltage_window) | ||
rtn_extras["data_slice"] = _slice | ||
data_size = len(_slice.indices(voltage.size)) | ||
if data_size < 3: | ||
raise ValueError( | ||
f"The specified voltage_window ({voltage_window}) would result " | ||
f"in a null window or a window less than 3-elements." | ||
rocco8773 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) | ||
|
||
# define starting savgol windows | ||
savgol_windows = _condition_smooth_fractions(smooth_fractions, data_size) | ||
|
||
voltage_slice = voltage[_slice] | ||
current_slice = current[_slice] | ||
plasma_potentials = [] | ||
rtn_extras["savgol_windows"] = [] | ||
for _window in savgol_windows: | ||
v_smooth = signal.savgol_filter(voltage_slice, _window, 1) | ||
c_smooth = signal.savgol_filter(current_slice, _window, 1) | ||
|
||
didv = np.gradient(c_smooth, v_smooth) | ||
imax = np.argmax(didv) | ||
if imax.size > 1: | ||
if np.all(np.diff(imax) == 1): | ||
vp = np.average(voltage_slice[imax]) | ||
else: | ||
continue | ||
elif np.isscalar(imax): | ||
vp = voltage_slice[imax] | ||
else: | ||
vp = voltage_slice[imax[0]] | ||
|
||
plasma_potentials.append(float(vp)) | ||
rtn_extras["savgol_windows"].append(int(_window)) | ||
|
||
rtn_extras["savgol_peaks"] = plasma_potentials | ||
rtn_extras["std"] = float(np.std(plasma_potentials)) | ||
|
||
vp = float(np.average(plasma_potentials)) | ||
return vp, dIdVExtras(**rtn_extras) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[optional] A possible alternative would be a
dataclass
. I haven't thought about which would be better, though.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see past conversation about dataclasses vs namedtuples...#889 (comment)
At this time I'm not inclined to make the change to dataclasses, since it would need to be changed for all other existing functionality to keep a consistent design pattern
(value, extras)
.