-
Notifications
You must be signed in to change notification settings - Fork 306
/
langmuir.py
1403 lines (1084 loc) · 44.4 KB
/
langmuir.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""Defines the Langmuir analysis module as part of the diagnostics package."""
__all__ = [
"Characteristic",
"swept_probe_analysis",
"get_plasma_potential",
"get_floating_potential",
"get_electron_saturation_current",
"get_ion_saturation_current",
"get_ion_density_LM",
"get_electron_density_LM",
"extract_exponential_section",
"extract_ion_section",
"get_electron_temperature",
"extrapolate_electron_current",
"reduce_bimaxwellian_temperature",
"get_ion_density_OML",
"extrapolate_ion_current_OML",
"get_EEDF",
]
import astropy.units as u
import copy
import numpy as np
from astropy.constants import si as const
from astropy.visualization import quantity_support
from scipy.optimize import curve_fit
from warnings import warn
from plasmapy.particles import Particle
from plasmapy.utils.decorators import validate_quantities
def _langmuir_futurewarning() -> None:
warn(
"The plasmapy.diagnostics.langmuir module will be deprecated in favor of "
"the plasmapy.analysis.swept_langmuir sub-package and phased out over "
"2021. The plasmapy.analysis package was released in v0.5.0.",
FutureWarning,
)
def _fit_func_lin(x, x0, y0, c0):
r"""Linear fitting function."""
return y0 + c0 * (x - x0)
def _fit_func_lin_inverse(x, x0, y0, T0):
r"""Linear fitting function with inverse slope parameter for use in fitting
of the electron current growth region.
"""
return y0 + (x - x0) / T0
def _fit_func_double_lin_inverse(x, x0, y0, T0, Delta_T):
r"""Piecewise linear fitting function with inverse slope parameters and
an offset for use in fitting a bi-Maxwellian electron current growth
region. (x0, y0) denotes the location of the knee of the transition,
with T0 and T0 + Delta_T being the cold and hot temperatures, respectively.
"""
hot_T_func = lambda x: y0 + (x - x0) / (T0 + Delta_T)
cold_T_func = lambda x: y0 + (x - x0) / T0
return np.piecewise(x, x < x0, [hot_T_func, cold_T_func])
class Characteristic:
r"""Class representing a single I-V probe characteristic for convenient
experimental data access and computation. Supports units.
Attributes
----------
bias : `astropy.units.Quantity`, `~numpy.ndarray`
Array of applied probe biases in units convertible to V.
current : `astropy.units.Quantity`, `~numpy.ndarray`
Array of applied probe currents in units convertible to A.
"""
@validate_quantities(bias={"can_be_inf": False}, current={"can_be_inf": False})
def __init__(self, bias: u.V, current: u.A):
_langmuir_futurewarning()
self.bias = bias
self.current = current
self.get_unique_bias(True)
self._check_validity()
def __getitem__(self, key):
r"""Allow array indexing operations directly on the Characteristic
object.
"""
return Characteristic(self.bias[key], self.current[key])
def __sub__(self, other):
r"""Support current subtraction"""
b = copy.deepcopy(self)
b.current -= other.current
return b
def __add__(self, other):
r"""Support current addition"""
b = copy.deepcopy(self)
b.current += other.current
return b
def sort(self):
r"""Sort the characteristic by ascending bias."""
_sort = self.bias.argsort()
self.current = self.current[_sort]
self.bias = self.bias[_sort]
def get_unique_bias(self, inplace=False):
r"""Remove any duplicate bias values through averaging."""
if len(self.bias) != len(self.current):
raise ValueError(
f"Unequal array lengths of bias "
f"({len(self.bias)}) and current "
f"({len(self.current)})."
)
bias_unique = np.unique(self.bias)
current_unique = []
for bias in bias_unique:
current_unique = np.append(
current_unique, np.mean(self.current[self.bias == bias].to(u.A).value)
)
current_unique *= u.A
if not inplace:
return Characteristic(bias_unique, current_unique)
self.bias = bias_unique
self.current = current_unique
def _check_validity(self):
r"""Check the unit and value validity of the characteristic."""
if len(self.bias.shape) != 1:
raise ValueError("Non-1D array for voltage!")
if len(self.current.shape) != 1:
raise ValueError("Non-1D array for current!")
if len(self.bias) != len(self.current):
raise ValueError(
f"Unequal array lengths of bias "
f"({len(self.bias)}) and current "
f"({len(self.current)})."
)
if len(np.unique(self.bias)) != len(self.bias):
raise ValueError("Bias array contains duplicate values.")
def get_padded_limit(self, padding, log=False): # coverage: ignore
r"""Return the limits of the current range for plotting, taking into
account padding. Matplotlib lacks this functionality.
Parameters
----------
padding : `float`
The padding ratio as a float between 0.0 and 1.0.
log : `bool`, optional
If `True` the calculation will be performed on a logarithmic scale.
Default is `False`.
"""
ymax = np.max(self.current).to(u.A).value
if log:
ymin = np.min(np.abs(self.current[self.current != 0])).to(u.A).value
return [
ymin * 10 ** (-padding * np.log10(ymax / ymin)),
ymax * 10 ** (padding * np.log10(ymax / ymin)),
] * u.A
else:
ymin = np.min(self.current).to(u.A).value
return [
ymin - padding * (ymax - ymin),
ymax + padding * (ymax - ymin),
] * u.A
def plot(self): # coverage: ignore
r"""Plot the characteristic in matplotlib."""
import matplotlib.pyplot as plt
with quantity_support():
plt.figure()
plt.scatter(self.bias.to(u.V), self.current.to(u.mA), marker=".", color="k")
plt.title("Probe characteristic")
@validate_quantities(
probe_area={"can_be_negative": False, "can_be_inf": False, "can_be_nan": False}
)
def swept_probe_analysis(
probe_characteristic,
probe_area: u.m**2,
gas_argument,
bimaxwellian=False,
visualize=False,
plot_electron_fit=False,
plot_EEDF=False,
):
r"""Attempt to perform a basic swept probe analysis based on the provided
characteristic and probe data. Suitable for single cylindrical probes in
low-pressure DC plasmas, since OML is applied.
Parameters
----------
probe_characteristic : `~plasmapy.diagnostics.langmuir.Characteristic`
The swept probe characteristic that is to be analyzed.
probe_area : `~astropy.units.Quantity`
The area of the probe exposed to plasma in units convertible to m^2.
gas_argument : argument to instantiate the |Particle| class.
`str`, `int`, or `~plasmapy.particles.particle_class.Particle`
A string representing a particle, element, isotope, or ion; an
integer representing the atomic number of an element; or a
|Particle| instance.
visualize : `bool`, optional
Can be used to plot the characteristic and the obtained parameters.
Default is `False`.
plot_electron_fit : `bool`, optional
If `True`, the fit of the electron current in the exponential section is
shown. Default is False.
plot_EEDF : `bool`, optional
If `True`, the EEDF is computed and shown. Default is `False`.
Returns
-------
Results are returned as Dictionary
"T_e" : `astropy.units.Quantity`
Best estimate of the electron temperature in units of eV. Contains
two values if bimaxwellian is True.
"n_e" : `astropy.units.Quantity`
Estimate of the electron density in units of m\ :sup:`-3`\ . See the Notes on
plasma densities.
"n_i" : `astropy.units.Quantity`
Estimate of the ion density in units of m\ :sup:`-3`\ . See the Notes on
plasma densities.
"n_i_OML" : `astropy.units.Quantity`
OML-theory estimate of the ion density in units of m\ :sup:`-3`\ . See the Notes
on plasma densities.
"V_F" : `astropy.units.Quantity`
Estimate of the floating potential in units of V.
"V_P" : `astropy.units.Quantity`
Estimate of the plasma potential in units of V.
"I_es" : `astropy.units.Quantity`
Estimate of the electron saturation current in units of Am^-2.
"I_is" : `astropy.units.Quantity`
Estimate of the ion saturation current in units of Am^-2.
"hot_fraction" : float
Estimate of the total hot (energetic) electron fraction.
Notes
-----
This function combines the separate probe analysis functions into a single
analysis. Results are returned as a Dictionary. On plasma densities: in an
ideal quasi-neutral plasma all densities should be equal. However, in
practice this will not be the case. The electron density is the poorest
estimate due to the hard to obtain knee in the electron current. The
density provided by OML theory is likely the best estimate as it is not
dependent on the obtained electron temperature, given that the conditions
for OML theory hold.
"""
_langmuir_futurewarning()
# Instantiate gas using the Particle class
gas = Particle(argument=gas_argument)
if not isinstance(probe_characteristic, Characteristic):
raise TypeError(
f"For 'probe_characteristic' expected type "
f"{Characteristic.__module__ + '.' + Characteristic.__qualname__} "
f"and got {type(probe_characteristic)}"
)
# Obtain the plasma and floating potentials
V_P = get_plasma_potential(probe_characteristic)
V_F = get_floating_potential(probe_characteristic)
# Obtain the electron and ion saturation currents
I_es = get_electron_saturation_current(probe_characteristic)
I_is = get_ion_saturation_current(probe_characteristic)
# The OML method is used to obtain an ion density without knowing the
# electron temperature. This can then be used to obtain the ion current
# and subsequently a better electron current fit.
n_i_OML, fit = get_ion_density_OML(
probe_characteristic, probe_area, gas, return_fit=True
)
ion_current = extrapolate_ion_current_OML(probe_characteristic, fit)
# First electron temperature iteration
exponential_section = extract_exponential_section(
probe_characteristic, ion_current=ion_current
)
T_e, hot_fraction = get_electron_temperature(
exponential_section, bimaxwellian=bimaxwellian, return_hot_fraction=True
)
# Second electron temperature iteration, using an electron temperature-
# adjusted exponential section
exponential_section = extract_exponential_section(
probe_characteristic, T_e=T_e, ion_current=ion_current
)
T_e, hot_fraction, fit = get_electron_temperature(
exponential_section,
bimaxwellian=bimaxwellian,
visualize=plot_electron_fit,
return_fit=True,
return_hot_fraction=True,
)
# Extrapolate the fit of the exponential section to obtain the full
# electron current. This has no use in the analysis except for
# visualization.
electron_current = extrapolate_electron_current(
probe_characteristic, fit, bimaxwellian=bimaxwellian
)
# Using a good estimate of electron temperature, obtain the ion and
# electron densities from the saturation currents.
n_i = get_ion_density_LM(
I_is, reduce_bimaxwellian_temperature(T_e, hot_fraction), probe_area, gas.mass
)
n_e = get_electron_density_LM(
I_es, reduce_bimaxwellian_temperature(T_e, hot_fraction), probe_area
)
if visualize: # coverage: ignore
import matplotlib.pyplot as plt
with quantity_support():
fig, (ax1, ax2) = plt.subplots(2, 1)
ax1.plot(
probe_characteristic.bias,
probe_characteristic.current,
marker=".",
color="k",
linestyle="",
label="Probe current",
)
ax1.set_title("Probe characteristic")
ax2.set_ylim(probe_characteristic.get_padded_limit(0.1))
ax2.plot(
probe_characteristic.bias,
np.abs(probe_characteristic.current),
marker=".",
color="k",
linestyle="",
label="Probe current",
)
ax2.set_title("Logarithmic")
ax2.set_ylim(probe_characteristic.get_padded_limit(0.1, log=True))
ax1.axvline(x=V_P.value, color="gray", linestyle="--")
ax1.axhline(y=I_es.value, color="grey", linestyle="--")
ax1.axvline(x=V_F.value, color="k", linestyle="--")
ax1.axhline(y=I_is.value, color="r", linestyle="--")
ax1.plot(ion_current.bias, ion_current.current, c="y", label="Ion current")
ax1.plot(
electron_current.bias,
electron_current.current,
c="c",
label="Electron current",
)
tot_current = ion_current + electron_current
ax1.plot(tot_current.bias, tot_current.current, c="g")
ax2.axvline(x=V_P.value, color="gray", linestyle="--")
ax2.axhline(y=I_es.value, color="grey", linestyle="--")
ax2.axvline(x=V_F.value, color="k", linestyle="--")
ax2.axhline(y=np.abs(I_is.value), color="r", linestyle="--")
ax2.plot(
ion_current.bias,
np.abs(ion_current.current),
label="Ion current",
c="y",
)
ax2.plot(
electron_current.bias,
np.abs(electron_current.current),
label="Electron current",
c="c",
)
ax2.plot(tot_current.bias, np.abs(tot_current.current), c="g")
ax2.set_yscale("log", nonpositive="clip")
ax1.legend(loc="best")
ax2.legend(loc="best")
fig.tight_layout()
# Obtain and show the EEDF. This is only useful if the characteristic data
# has been preprocessed to be sufficiently smooth and noiseless.
if plot_EEDF: # coverage: ignore
get_EEDF(probe_characteristic, visualize=True)
# Compile the results dictionary
results = {
"V_P": V_P,
"V_F": V_F,
"I_es": I_es,
"I_is": I_is,
"n_e": n_e,
"n_i": n_i,
"T_e": T_e,
"n_i_OML": n_i_OML,
}
if bimaxwellian:
results["hot_fraction"] = hot_fraction
return results
def get_plasma_potential(probe_characteristic, return_arg=False):
r"""Implement the simplest but crudest method for obtaining an estimate of
the plasma potential from the probe characteristic.
Parameters
----------
probe_characteristic : `~plasmapy.diagnostics.langmuir.Characteristic`
The probe characteristic that is being analyzed.
return_arg : `bool`, optional
Controls whether or not the argument of the plasma potential within the
characteristic array should be returned instead of the value of the
voltage. Default is False.
Returns
-------
V_P : `~astropy.units.Quantity`
Estimate of the plasma potential in units convertible to V.
Notes
-----
The method used in the function takes the maximum gradient of the probe
current as the 'knee' of the transition from exponential increase into the
electron the saturation region.
"""
_langmuir_futurewarning()
if not isinstance(probe_characteristic, Characteristic):
raise TypeError(
f"For 'probe_characteristic' expected type "
f"{Characteristic.__module__ + '.' + Characteristic.__qualname__} "
f"and got {type(probe_characteristic)}"
)
# Sort the characteristic prior to differentiation
probe_characteristic.sort()
# Acquiring first derivative
dIdV = np.gradient(
probe_characteristic.current.to(u.A).value,
probe_characteristic.bias.to(u.V).value,
)
arg_V_P = np.argmax(dIdV)
if return_arg:
return probe_characteristic.bias[arg_V_P], arg_V_P
return probe_characteristic.bias[arg_V_P]
def get_floating_potential(probe_characteristic, return_arg=False):
r"""Implement the simplest but crudest method for obtaining an estimate of
the floating potential from the probe characteristic.
Parameters
----------
probe_characteristic : `~plasmapy.diagnostics.langmuir.Characteristic`
The probe characteristic that is being analyzed.
return_arg : `bool`, optional
Controls whether or not the argument of the floating potential within
the characteristic array should be returned instead of the value of the
voltage. Default is False.
Returns
-------
V_F : `~astropy.units.Quantity`
Estimate of the floating potential in units convertible to V.
Notes
-----
The method used in this function takes the probe current closest to zero
Amperes as the floating potential.
"""
_langmuir_futurewarning()
if not isinstance(probe_characteristic, Characteristic):
raise TypeError(
f"For 'probe_characteristic' expected type "
f"{Characteristic.__module__ + '.' + Characteristic.__qualname__} "
f"and got {type(probe_characteristic)}"
)
arg_V_F = np.argmin(np.abs(probe_characteristic.current))
if return_arg:
return probe_characteristic.bias[arg_V_F], arg_V_F
return probe_characteristic.bias[arg_V_F]
def get_electron_saturation_current(probe_characteristic):
r"""Obtain an estimate of the electron saturation current corresponding
to the obtained plasma potential.
Parameters
----------
probe_characteristic : `~plasmapy.diagnostics.langmuir.Characteristic`
The probe characteristic that is being analyzed.
Returns
-------
I_es : `~astropy.units.Quantity`
Estimate of the electron saturation current in units convertible to A.
Notes
-----
The function `~plasmapy.diagnostics.langmuir.get_plasma_potential`
is used to obtain an estimate of the plasma potential. The
corresponding electron saturation current is returned.
"""
_langmuir_futurewarning()
if not isinstance(probe_characteristic, Characteristic):
raise TypeError(
f"For 'probe_characteristic' expected type "
f"{Characteristic.__module__ + '.' + Characteristic.__qualname__} "
f"and got {type(probe_characteristic)}"
)
_, arg_V_P = get_plasma_potential(probe_characteristic, return_arg=True)
return probe_characteristic.current[arg_V_P]
def get_ion_saturation_current(probe_characteristic):
r"""Implement the simplest but crudest method for obtaining an estimate of
the ion saturation current from the probe characteristic.
Parameters
----------
probe_characteristic : `~plasmapy.diagnostics.langmuir.Characteristic`
The probe characteristic that is being analyzed.
Returns
-------
I_is : `~astropy.units.Quantity`
Estimate of the ion saturation current in units convertible to A.
Notes
-----
The method implemented in this function assumes the ion saturation current
to be the smallest probe current in the characteristic. This assumes the
bias range in the ion region is sufficiently negative for the ion current to
saturate.
"""
_langmuir_futurewarning()
if not isinstance(probe_characteristic, Characteristic):
raise TypeError(
f"For 'probe_characteristic' expected type "
f"{Characteristic.__module__ + '.' + Characteristic.__qualname__} "
f"and got {type(probe_characteristic)}"
)
return np.min(probe_characteristic.current)
@validate_quantities(
ion_saturation_current={
"can_be_negative": True,
"can_be_inf": False,
"can_be_nan": False,
},
T_e={
"can_be_negative": False,
"can_be_inf": False,
"can_be_nan": False,
"equivalencies": u.temperature_energy(),
},
probe_area={"can_be_negative": False, "can_be_inf": False, "can_be_nan": False},
validations_on_return={"can_be_negative": False},
)
def get_ion_density_LM(
ion_saturation_current: u.A, T_e: u.eV, probe_area: u.m**2, gas
) -> u.m**-3:
r"""Implement the Langmuir-Mottley (LM) method of obtaining the ion
density.
Parameters
----------
ion_saturation_current : `~astropy.units.Quantity`
The ion saturation current in units convertible to A.
T_e : `~astropy.units.Quantity`
The electron temperature in units convertible to eV.
probe_area : `~astropy.units.Quantity`
The area of the probe exposed to plasma in units convertible to m^2.
gas : `~astropy.units.Quantity`
The (mean) mass of the background gas in atomic mass units.
Returns
-------
n_i : `~astropy.units.Quantity`
Estimate of the ion density in units convertible to m\ :sup:`-3`\ .
Notes
-----
The method implemented in this function obtains the ion density from the
ion saturation current density assuming that the ion current loss to the
probe is equal to the Bohm loss. The acoustic Bohm velocity is obtained
from the electron temperature and the ion mass.
The ion saturation current is given by
.. math::
I_{is} = 0.6 e A_p n_i \sqrt{\frac{T_e}{m_i}}.
"""
_langmuir_futurewarning()
# Calculate the acoustic (Bohm) velocity
c_s = np.sqrt(T_e / gas)
return np.abs(ion_saturation_current) / (0.6 * const.e * probe_area * c_s)
@validate_quantities(
electron_saturation_current={
"can_be_negative": True,
"can_be_inf": False,
"can_be_nan": False,
},
T_e={
"can_be_negative": False,
"can_be_inf": False,
"can_be_nan": False,
"equivalencies": u.temperature_energy(),
},
probe_area={"can_be_negative": False, "can_be_inf": False, "can_be_nan": False},
validations_on_return={"can_be_negative": False},
)
def get_electron_density_LM(
electron_saturation_current: u.A, T_e: u.eV, probe_area: u.m**2
) -> u.m**-3:
r"""Implement the Langmuir-Mottley (LM) method of obtaining the electron
density.
Parameters
----------
electron_saturation_current : `~astropy.units.Quantity`
The electron saturation current in units convertible to A.
T_e : `~astropy.units.Quantity`
The electron temperature in units convertible to eV.
probe_area : `~astropy.units.Quantity`
The area of the probe exposed to plasma in units convertible to m^2.
Returns
-------
n_e : `~astropy.units.Quantity`
Estimate of the electron density in units convertible to m\ :sup:`-3`\ .
Notes
-----
The method implemented in this function obtains the electron density from
the electron saturation current density, assuming a low plasma density. The
electron saturation current is given by
.. math::
I_{es} = \frac{1}{4} e n_e A_p \sqrt{\frac{8 T_e}{\pi m_e}}.
Please note that the electron saturation current density is a hard
parameter to acquire and it is usually better to measure the ion density,
which should be identical to the electron density in quasineutral plasmas.
"""
_langmuir_futurewarning()
# Calculate the thermal electron velocity
v_th = np.sqrt(8 * T_e / (np.pi * const.m_e))
return 4 * electron_saturation_current / (probe_area * const.e * v_th)
def extract_exponential_section(probe_characteristic, T_e=None, ion_current=None):
r"""Extract the section of exponential electron current growth from the
probe characteristic.
Parameters
----------
probe_characteristic : `~plasmapy.diagnostics.langmuir.Characteristic`
The probe characteristic that is being analyzed.
T_e : `~astropy.units.Quantity`, optional
If given, the electron temperature can improve the accuracy of the
bounds of the exponential region.
ion_current : `~plasmapy.diagnostics.langmuir.Characteristic`, optional
If given, the ion current will be subtracted from the probe
characteristic to yield a better estimate of the electron current in
the exponential region.
Returns
-------
exponential_section : `~plasmapy.diagnostics.langmuir.Characteristic`
The exponential electron current growth section.
Notes
-----
This function extracts the region of exponential electron growth from the
probe characteristic under the assumption that this bias region is bounded
by the floating and plasma potentials. Additionally, an improvement in
accuracy can be made when the electron temperature is supplied.
"""
_langmuir_futurewarning()
if not isinstance(probe_characteristic, Characteristic):
raise TypeError(
f"For 'probe_characteristic' expected type "
f"{Characteristic.__module__ + '.' + Characteristic.__qualname__} "
f"and got {type(probe_characteristic)}"
)
V_F = get_floating_potential(probe_characteristic)
V_P = get_plasma_potential(probe_characteristic)
if T_e is not None:
# If a bi-Maxwellian electron temperature is supplied grab the first
# (cold) temperature
if np.array(T_e).size > 1:
T_e = np.min(T_e)
_filter = (probe_characteristic.bias > V_F + 1.5 * T_e / const.e) & (
probe_characteristic.bias < V_P - 0.2 * T_e / const.e
)
else:
_filter = (probe_characteristic.bias > V_F) & (probe_characteristic.bias < V_P)
exponential_section = probe_characteristic[_filter]
if ion_current is not None:
exponential_section = exponential_section - ion_current[_filter]
return exponential_section
def extract_ion_section(probe_characteristic):
r"""Extract the section dominated by ion collection from the probe
characteristic.
Parameters
----------
probe_characteristic : `~plasmapy.diagnostics.langmuir.Characteristic`
The probe characteristic that is being analyzed.
Returns
-------
ion_section : `~plasmapy.diagnostics.langmuir.Characteristic`
The exponential electron current growth section.
Notes
-----
This function extracts the region dominated by ion collection from the
probe characteristic under the assumption that this bias region is only
bounded by the floating potential on the right hand side.
"""
_langmuir_futurewarning()
if not isinstance(probe_characteristic, Characteristic):
raise TypeError(
f"For 'probe_characteristic' expected type "
f"{Characteristic.__module__ + '.' + Characteristic.__qualname__} "
f"and got {type(probe_characteristic)}"
)
V_F = get_floating_potential(probe_characteristic)
return probe_characteristic[probe_characteristic.bias < V_F]
def get_electron_temperature(
exponential_section,
bimaxwellian=False,
visualize=False,
return_fit=False,
return_hot_fraction=False,
):
r"""Obtain the Maxwellian or bi-Maxwellian electron temperature using the
exponential fit method.
Parameters
----------
exponential_section : `~plasmapy.diagnostics.langmuir.Characteristic`
The probe characteristic that is being analyzed.
bimaxwellian : `bool`, optional
If `True` the exponential section will be fit assuming bi-Maxwellian
electron populations, as opposed to Maxwellian. Default is False.
visualize : `bool`, optional
If `True` a plot of the exponential fit is shown. Default is `False`.
return_fit: `bool`, optional
If `True` the parameters of the fit will be returned in addition to the
electron temperature. Default is `False`.
return_hot_fraction: float, optional
If `True` the total fraction of hot electrons will be returned if the
population is bi-Maxwellian. Default is `False`.
Returns
-------
T_e : `~astropy.units.Quantity`, (ndarray)
The estimated electron temperature in eV. In case of a bi-Maxwellian
plasma an array containing two Quantities is returned.
Notes
-----
In the electron growth region of the probe characteristic the electron
current grows exponentially with bias voltage:
.. math::
I_e = I_{es} \textrm{exp} \left(
-\frac{e\left(V_P - V \right)}{T_e} \right).
In log space the current in this region should be a straight line if the
plasma electrons are fully Maxwellian, or exhibit a knee in a
bi-Maxwellian case. The slope is inversely proportional to the
temperature of the respective electron population:
.. math::
\textrm{log} \left(I_e \right ) \propto \frac{1}{T_e}.
"""
_langmuir_futurewarning()
if not isinstance(exponential_section, Characteristic):
raise TypeError(
f"For 'probe_characteristic' expected type "
f"{Characteristic.__module__ + '.' + Characteristic.__qualname__} "
f"and got {type(exponential_section)}"
)
# Remove values in the section with a current equal to or smaller than
# zero.
exponential_section = exponential_section[
exponential_section.current.to(u.A).value > 0
]
initial_guess = None # for fitting
bounds = (-np.inf, np.inf)
# Instantiate the correct fitting equation, initial values and bounds.
if bimaxwellian:
max_exp_bias = np.max(exponential_section.bias)
min_exp_bias = np.min(exponential_section.bias)
x0 = min_exp_bias + 2 / 3 * (max_exp_bias - min_exp_bias)
initial_guess = [x0.to(u.V).value, 0.6, 2, 1]
bounds = ([-np.inf, -np.inf, 0, 0], np.inf)
fit_func = _fit_func_double_lin_inverse
else:
fit_func = _fit_func_lin_inverse
# Perform the actual fit of the data
fit, _ = curve_fit(
fit_func,
exponential_section.bias.to(u.V).value,
np.log(exponential_section.current.to(u.A).value),
p0=initial_guess,
bounds=bounds,
)
hot_fraction = None
# Obtain the plasma parameters from the fit
if not bimaxwellian:
T0 = fit[2]
T_e = T0 * u.eV
else:
x0, y0 = fit[0], fit[1]
T0, Delta_T = [fit[2], fit[3]]
# In order to obtain the energetic electron fraction the fits of the
# cold and hot populations are extrapolated to the plasma potential
# (ie. the maximum bias of the exponential section). The logarithmic
# difference between these currents equates to the density difference.
k1 = _fit_func_lin_inverse(
np.max(exponential_section.bias.to(u.V).value), *[x0, y0, T0]
)
k2 = _fit_func_lin_inverse(
np.max(exponential_section.bias.to(u.V).value), *[x0, y0, T0 + Delta_T]
)
# Compute the total hot (energetic) fraction
hot_fraction = 1 / (1 + np.exp(k1 - k2))
# If bi-Maxwellian, return main temperature first
T_e = np.array([T0, T0 + Delta_T]) * u.eV
if visualize: # coverage: ignore
import matplotlib.pyplot as plt
with quantity_support():
plt.figure()
plt.scatter(
exponential_section.bias.to(u.V),
np.log(exponential_section.current.to(u.A).value),
color="k",
marker=".",
label="Exponential section",
)
if bimaxwellian:
plt.scatter(x0, y0, marker="o", c="g")
plt.plot(
exponential_section.bias.to(u.V),
_fit_func_lin_inverse(
exponential_section.bias.to(u.V).value,
fit[0],
fit[1],
fit[2] + fit[3],
),
c="g",
linestyle="--",
label="Bimaxwellian exponential section fit",
)
plt.plot(
exponential_section.bias.to(u.V),
fit_func(exponential_section.bias.to(u.V).value, *fit),
label="Exponential fit",
c="g",
)
plt.ylabel("Logarithmic current")
plt.title("Exponential fit")
plt.legend(loc="best")
plt.tight_layout()