multipac-testbench 1.6.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- multipac_testbench/__init__.py +5 -0
- multipac_testbench/instruments/__init__.py +44 -0
- multipac_testbench/instruments/current_probe.py +16 -0
- multipac_testbench/instruments/electric_field/__init__.py +8 -0
- multipac_testbench/instruments/electric_field/field_probe.py +106 -0
- multipac_testbench/instruments/electric_field/i_electric_field.py +22 -0
- multipac_testbench/instruments/electric_field/reconstructed.py +253 -0
- multipac_testbench/instruments/factory.py +171 -0
- multipac_testbench/instruments/frequency.py +51 -0
- multipac_testbench/instruments/instrument.py +504 -0
- multipac_testbench/instruments/optical_fibre.py +21 -0
- multipac_testbench/instruments/penning.py +16 -0
- multipac_testbench/instruments/power.py +83 -0
- multipac_testbench/instruments/reflection_coefficient.py +83 -0
- multipac_testbench/instruments/swr.py +73 -0
- multipac_testbench/instruments/virtual_instrument.py +18 -0
- multipac_testbench/measurement_point/__init__.py +9 -0
- multipac_testbench/measurement_point/factory.py +145 -0
- multipac_testbench/measurement_point/global_diagnostics.py +45 -0
- multipac_testbench/measurement_point/i_measurement_point.py +180 -0
- multipac_testbench/measurement_point/pick_up.py +80 -0
- multipac_testbench/multipactor_band/__init__.py +1 -0
- multipac_testbench/multipactor_band/campaign_multipactor_bands.py +15 -0
- multipac_testbench/multipactor_band/creator.py +247 -0
- multipac_testbench/multipactor_band/instrument_multipactor_bands.py +116 -0
- multipac_testbench/multipactor_band/multipactor_band.py +80 -0
- multipac_testbench/multipactor_band/polisher.py +176 -0
- multipac_testbench/multipactor_band/test_multipactor_bands.py +154 -0
- multipac_testbench/multipactor_band/util.py +122 -0
- multipac_testbench/multipactor_test.py +1214 -0
- multipac_testbench/test_campaign.py +752 -0
- multipac_testbench/theoretical/__init__.py +0 -0
- multipac_testbench/theoretical/somersalo.py +324 -0
- multipac_testbench/theoretical/somersalo_data.txt +25 -0
- multipac_testbench/util/__init__.py +1 -0
- multipac_testbench/util/animate.py +50 -0
- multipac_testbench/util/filtering.py +195 -0
- multipac_testbench/util/helper.py +71 -0
- multipac_testbench/util/log_manager.py +163 -0
- multipac_testbench/util/multipactor_detectors.py +97 -0
- multipac_testbench/util/plot.py +508 -0
- multipac_testbench/util/post_treaters.py +130 -0
- multipac_testbench-1.6.3.dist-info/METADATA +65 -0
- multipac_testbench-1.6.3.dist-info/RECORD +47 -0
- multipac_testbench-1.6.3.dist-info/WHEEL +5 -0
- multipac_testbench-1.6.3.dist-info/licenses/LICENSE +21 -0
- multipac_testbench-1.6.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""This subpackage holds instrument (current, voltage, etc)."""
|
|
2
|
+
|
|
3
|
+
from multipac_testbench.instruments.current_probe import CurrentProbe
|
|
4
|
+
from multipac_testbench.instruments.electric_field.field_probe import (
|
|
5
|
+
FieldProbe,
|
|
6
|
+
)
|
|
7
|
+
from multipac_testbench.instruments.electric_field.i_electric_field import (
|
|
8
|
+
IElectricField,
|
|
9
|
+
)
|
|
10
|
+
from multipac_testbench.instruments.electric_field.reconstructed import (
|
|
11
|
+
Reconstructed,
|
|
12
|
+
)
|
|
13
|
+
from multipac_testbench.instruments.frequency import Frequency
|
|
14
|
+
from multipac_testbench.instruments.instrument import Instrument
|
|
15
|
+
from multipac_testbench.instruments.optical_fibre import OpticalFibre
|
|
16
|
+
from multipac_testbench.instruments.penning import Penning
|
|
17
|
+
from multipac_testbench.instruments.power import (
|
|
18
|
+
ForwardPower,
|
|
19
|
+
Power,
|
|
20
|
+
ReflectedPower,
|
|
21
|
+
)
|
|
22
|
+
from multipac_testbench.instruments.reflection_coefficient import (
|
|
23
|
+
ReflectionCoefficient,
|
|
24
|
+
)
|
|
25
|
+
from multipac_testbench.instruments.swr import SWR
|
|
26
|
+
from multipac_testbench.instruments.virtual_instrument import VirtualInstrument
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"CurrentProbe",
|
|
30
|
+
"IElectricField",
|
|
31
|
+
"FieldProbe",
|
|
32
|
+
"ForwardPower",
|
|
33
|
+
"Frequency",
|
|
34
|
+
"Instrument",
|
|
35
|
+
"OpticalFibre",
|
|
36
|
+
"OpticalFibre",
|
|
37
|
+
"Penning",
|
|
38
|
+
"Power",
|
|
39
|
+
"Reconstructed",
|
|
40
|
+
"ReflectedPower",
|
|
41
|
+
"ReflectionCoefficient",
|
|
42
|
+
"SWR",
|
|
43
|
+
"VirtualInstrument",
|
|
44
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Define current probe to measure multipactor cloud current."""
|
|
2
|
+
|
|
3
|
+
from multipac_testbench.instruments.instrument import Instrument
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CurrentProbe(Instrument):
|
|
7
|
+
"""A probe to measure multipacting current."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
10
|
+
"""Just instantiate."""
|
|
11
|
+
return super().__init__(*args, **kwargs)
|
|
12
|
+
|
|
13
|
+
@classmethod
|
|
14
|
+
def ylabel(cls) -> str:
|
|
15
|
+
"""Label used for plots."""
|
|
16
|
+
return r"Multipactor current [$\mu$A]"
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
r"""Define all :class:`.Instrument`\s measuring voltages, e fields.
|
|
2
|
+
|
|
3
|
+
:class:`.FieldProbe` represents an electric field probe.
|
|
4
|
+
:class:`.Reconstructed` is the electric field at every time and position of the
|
|
5
|
+
coaxial, it is rebuilt from analytical laws fitted on the :class:`.FieldProbe`.
|
|
6
|
+
Both these classes inherit from :class:`.IElectricField`.
|
|
7
|
+
|
|
8
|
+
"""
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Define field probe to measure electric field."""
|
|
2
|
+
|
|
3
|
+
from functools import partial
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from multipac_testbench.instruments.electric_field.i_electric_field import (
|
|
9
|
+
IElectricField,
|
|
10
|
+
)
|
|
11
|
+
from multipac_testbench.util.post_treaters import (
|
|
12
|
+
v_acquisition_to_v_coax,
|
|
13
|
+
v_coax_to_v_acquisition,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FieldProbe(IElectricField):
|
|
18
|
+
"""A probe to measure electric field."""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
*args,
|
|
23
|
+
g_probe: float | None = None,
|
|
24
|
+
calibration_file: str | None = None,
|
|
25
|
+
patch: bool = False,
|
|
26
|
+
**kwargs,
|
|
27
|
+
) -> None:
|
|
28
|
+
r"""Instantiate with some specific arguments.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
g_probe :
|
|
33
|
+
Total attenuation. Probe specific, also depends on frequency.
|
|
34
|
+
a_rack :
|
|
35
|
+
Rack calibration slope in :unit:`V/dBm`.
|
|
36
|
+
b_rack :
|
|
37
|
+
Rack calibration constant in :unit:`dBm`.
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
super().__init__(*args, **kwargs)
|
|
41
|
+
self._g_probe = g_probe
|
|
42
|
+
|
|
43
|
+
self._a_rack: float
|
|
44
|
+
self._b_rack: float
|
|
45
|
+
if calibration_file is not None:
|
|
46
|
+
self._a_rack, self._b_rack = self._load_calibration_file(
|
|
47
|
+
Path(calibration_file)
|
|
48
|
+
)
|
|
49
|
+
if patch:
|
|
50
|
+
self._patch_data()
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def ylabel(cls) -> str:
|
|
54
|
+
"""Label used for plots."""
|
|
55
|
+
return r"Measured voltage [V]"
|
|
56
|
+
|
|
57
|
+
def _patch_data(self, g_probe_in_labview: float = -1.0) -> None:
|
|
58
|
+
"""Correct ``raw_data`` when ``g_probe`` in LabVIEWER is wrong.
|
|
59
|
+
|
|
60
|
+
The default value for ``g_probe_in_labview`` is only a guess.
|
|
61
|
+
|
|
62
|
+
"""
|
|
63
|
+
assert hasattr(self, "_a_rack")
|
|
64
|
+
assert hasattr(self, "_b_rack")
|
|
65
|
+
assert self._g_probe is not None
|
|
66
|
+
fun1 = partial(
|
|
67
|
+
v_coax_to_v_acquisition,
|
|
68
|
+
g_probe=g_probe_in_labview,
|
|
69
|
+
a_rack=self._a_rack,
|
|
70
|
+
b_rack=self._b_rack,
|
|
71
|
+
z_0=50.0,
|
|
72
|
+
)
|
|
73
|
+
fun2 = partial(
|
|
74
|
+
v_acquisition_to_v_coax,
|
|
75
|
+
g_probe=self._g_probe,
|
|
76
|
+
a_rack=self._a_rack,
|
|
77
|
+
b_rack=self._b_rack,
|
|
78
|
+
z_0=50.0,
|
|
79
|
+
)
|
|
80
|
+
self._raw_data = fun1(self._raw_data)
|
|
81
|
+
self._raw_data = fun2(self._raw_data)
|
|
82
|
+
|
|
83
|
+
def _load_calibration_file(
|
|
84
|
+
self,
|
|
85
|
+
calibration_file: Path,
|
|
86
|
+
freq_mhz: float = 120.0,
|
|
87
|
+
freq_col: str = "Frequency [MHz]",
|
|
88
|
+
a_col: str = "a [dBm / V]",
|
|
89
|
+
b_col: str = "b [dBm]",
|
|
90
|
+
) -> tuple[float, float]:
|
|
91
|
+
"""Load calibration file, interpolate proper calibration data."""
|
|
92
|
+
data = pd.read_csv(
|
|
93
|
+
calibration_file,
|
|
94
|
+
sep="\t",
|
|
95
|
+
comment="#",
|
|
96
|
+
index_col=freq_col,
|
|
97
|
+
usecols=[a_col, b_col, freq_col],
|
|
98
|
+
)
|
|
99
|
+
if freq_mhz not in data.index:
|
|
100
|
+
data.loc[freq_mhz] = [np.nan, np.nan]
|
|
101
|
+
data.sort_index(inplace=True)
|
|
102
|
+
data.interpolate(inplace=True)
|
|
103
|
+
ser = data.loc[freq_mhz]
|
|
104
|
+
a_rack = ser[a_col]
|
|
105
|
+
b_rack = ser[b_col]
|
|
106
|
+
return a_rack, b_rack
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Define mother class for all instruments measuring electric fields."""
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from multipac_testbench.instruments.instrument import Instrument
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class IElectricField(Instrument):
|
|
8
|
+
"""A generic instrument for electric fields."""
|
|
9
|
+
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
name: str,
|
|
13
|
+
raw_data: pd.Series,
|
|
14
|
+
**kwargs,
|
|
15
|
+
) -> None:
|
|
16
|
+
"""Instantiate the class."""
|
|
17
|
+
super().__init__(name, raw_data, **kwargs)
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def ylabel(cls) -> str:
|
|
21
|
+
"""Label used for plots."""
|
|
22
|
+
return r"Voltage [V]"
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""Define voltage along line.
|
|
2
|
+
|
|
3
|
+
.. todo::
|
|
4
|
+
voltage fitting, overload: they work but this not clean, not clean at all
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
from functools import partial
|
|
11
|
+
from typing import overload
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
import pandas as pd
|
|
15
|
+
from multipac_testbench.instruments.electric_field.field_probe import (
|
|
16
|
+
FieldProbe,
|
|
17
|
+
)
|
|
18
|
+
from multipac_testbench.instruments.electric_field.i_electric_field import (
|
|
19
|
+
IElectricField,
|
|
20
|
+
)
|
|
21
|
+
from multipac_testbench.instruments.power import ForwardPower
|
|
22
|
+
from multipac_testbench.instruments.reflection_coefficient import (
|
|
23
|
+
ReflectionCoefficient,
|
|
24
|
+
)
|
|
25
|
+
from multipac_testbench.util.helper import r_squared
|
|
26
|
+
from numpy.typing import NDArray
|
|
27
|
+
from scipy import optimize
|
|
28
|
+
from scipy.constants import c
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Reconstructed(IElectricField):
|
|
32
|
+
"""Voltage in the coaxial waveguide, fitted with e field probes."""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
name: str,
|
|
37
|
+
raw_data: pd.Series | None,
|
|
38
|
+
e_field_probes: Sequence[FieldProbe],
|
|
39
|
+
forward_power: ForwardPower,
|
|
40
|
+
reflection: ReflectionCoefficient,
|
|
41
|
+
freq_mhz: float,
|
|
42
|
+
position: NDArray[np.float64] | None = None,
|
|
43
|
+
z_ohm: float = 50.0,
|
|
44
|
+
**kwargs,
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Just instantiate."""
|
|
47
|
+
if position is None:
|
|
48
|
+
position = np.linspace(0.0, 1.3, 201, dtype=np.float64)
|
|
49
|
+
# from_array maybe
|
|
50
|
+
super().__init__(
|
|
51
|
+
name, raw_data, position=position, is_2d=True, **kwargs
|
|
52
|
+
)
|
|
53
|
+
self._e_field_probes = e_field_probes
|
|
54
|
+
self._forward_power = forward_power
|
|
55
|
+
self._reflection = reflection
|
|
56
|
+
self._sample_indexes = self._e_field_probes[0]._raw_data.index
|
|
57
|
+
self._beta = c / freq_mhz * 1e-6
|
|
58
|
+
|
|
59
|
+
self._psi_0: float
|
|
60
|
+
self._data: NDArray[np.float64] | None = None
|
|
61
|
+
self._z_ohm = z_ohm
|
|
62
|
+
self._r_squared: float
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def ylabel(cls) -> str:
|
|
66
|
+
"""Label used for plots."""
|
|
67
|
+
return r"Reconstructed voltage [V]"
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def data(self) -> NDArray[np.float64]:
|
|
71
|
+
"""Give the calculated voltage at every pos and sample index.
|
|
72
|
+
|
|
73
|
+
.. note::
|
|
74
|
+
In contrary to most :class:`.Instrument` objects, here ``data`` is
|
|
75
|
+
2D. Axis are the following: ``data[sample_index, position_index]``
|
|
76
|
+
|
|
77
|
+
"""
|
|
78
|
+
if self._data is not None:
|
|
79
|
+
return self._data
|
|
80
|
+
|
|
81
|
+
assert hasattr(self, "_psi_0")
|
|
82
|
+
|
|
83
|
+
data = []
|
|
84
|
+
for power, reflection in zip(
|
|
85
|
+
self._forward_power.data, self._reflection.data
|
|
86
|
+
):
|
|
87
|
+
v_f = _power_to_volt(power, z_ohm=self._z_ohm)
|
|
88
|
+
data.append(
|
|
89
|
+
voltage_vs_position(
|
|
90
|
+
self.position, v_f, reflection, self._beta, self._psi_0
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
self._data = np.array(data)
|
|
94
|
+
return self._data
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def fit_info(self) -> str:
|
|
98
|
+
"""Print compact info on fit."""
|
|
99
|
+
out = rf"$\psi_0 = ${self._psi_0:2.3f}"
|
|
100
|
+
if not hasattr(self, "_r_squared"):
|
|
101
|
+
return out
|
|
102
|
+
|
|
103
|
+
return "\n".join([out, rf"$r^2 = ${self._r_squared:2.3f}"])
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def label(self) -> str:
|
|
107
|
+
"""Label used for legends in plots vs position."""
|
|
108
|
+
return self.fit_info
|
|
109
|
+
|
|
110
|
+
def fit_voltage(self, full_output: bool = True) -> None:
|
|
111
|
+
r"""Find out the proper voltage parameters.
|
|
112
|
+
|
|
113
|
+
Idea is the following: for every sample index we know the forward
|
|
114
|
+
(injected) power :math:`P_f`, :math:`\Gamma`, and
|
|
115
|
+
:math:`V_\mathrm{coax}` at several pick-ups. We try to find
|
|
116
|
+
:math:`\psi_0` to verify:
|
|
117
|
+
|
|
118
|
+
.. math::
|
|
119
|
+
|V_\mathrm{coax}(z)| = 2\sqrt{P_f Z} \sqrt{1 + |\Gamma|^2
|
|
120
|
+
+ 2|\Gamma| \cos{(2\beta z + \psi_0)}}
|
|
121
|
+
|
|
122
|
+
"""
|
|
123
|
+
x_0 = np.array([np.pi])
|
|
124
|
+
bounds = ([-2.0 * np.pi], [2.0 * np.pi])
|
|
125
|
+
xdata = []
|
|
126
|
+
data = []
|
|
127
|
+
for e_probe in self._e_field_probes:
|
|
128
|
+
for p_f, reflection, e_field in zip(
|
|
129
|
+
self._forward_power.data, self._reflection.data, e_probe.data
|
|
130
|
+
):
|
|
131
|
+
xdata.append([p_f, reflection, e_probe.position])
|
|
132
|
+
data.append(e_field)
|
|
133
|
+
|
|
134
|
+
to_fit = partial(_model, beta=self._beta, z_ohm=self._z_ohm)
|
|
135
|
+
result = optimize.curve_fit(
|
|
136
|
+
to_fit,
|
|
137
|
+
xdata=xdata, # [power, pos] combinations
|
|
138
|
+
ydata=data, # resulting voltages
|
|
139
|
+
p0=x_0,
|
|
140
|
+
bounds=bounds,
|
|
141
|
+
full_output=full_output,
|
|
142
|
+
)
|
|
143
|
+
self._psi_0 = result[0][0]
|
|
144
|
+
if full_output:
|
|
145
|
+
self._r_squared = r_squared(result[2]["fvec"], np.array(data))
|
|
146
|
+
# res_squared = result[2]['fvec']**2
|
|
147
|
+
# expected = np.array(data)
|
|
148
|
+
|
|
149
|
+
# ss_err = np.sum(res_squared)
|
|
150
|
+
# ss_tot = np.sum((expected - expected.mean())**2)
|
|
151
|
+
# r_squared = 1. - ss_err / ss_tot
|
|
152
|
+
# self._r_squared = r_squared
|
|
153
|
+
logging.debug(self.fit_info)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _model(
|
|
157
|
+
var: NDArray[np.float64],
|
|
158
|
+
psi_0: float,
|
|
159
|
+
beta: float,
|
|
160
|
+
z_ohm: float = 50.0,
|
|
161
|
+
) -> float:
|
|
162
|
+
r"""Give voltage for given set of parameters, at proper power and position.
|
|
163
|
+
|
|
164
|
+
Parameters
|
|
165
|
+
----------
|
|
166
|
+
var :
|
|
167
|
+
Variables, namely :math:`[P_f, R, z]`.
|
|
168
|
+
|
|
169
|
+
Returns
|
|
170
|
+
-------
|
|
171
|
+
v :
|
|
172
|
+
Voltage at position :math:`z` for forward power :math:`P_f`.
|
|
173
|
+
|
|
174
|
+
"""
|
|
175
|
+
power, reflection, pos = var[:, 0], var[:, 1], var[:, 2]
|
|
176
|
+
v_f = _power_to_volt(power, z_ohm=z_ohm)
|
|
177
|
+
return voltage_vs_position(pos, v_f, reflection, beta, psi_0)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _power_to_volt(
|
|
181
|
+
power: NDArray[np.float64], z_ohm: float = 50.0
|
|
182
|
+
) -> NDArray[np.float64]:
|
|
183
|
+
return 2.0 * np.sqrt(power * z_ohm)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@overload
|
|
187
|
+
def voltage_vs_position(
|
|
188
|
+
pos: float,
|
|
189
|
+
v_f: float,
|
|
190
|
+
reflection: float,
|
|
191
|
+
beta: float,
|
|
192
|
+
psi_0: float,
|
|
193
|
+
) -> float: ...
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@overload
|
|
197
|
+
def voltage_vs_position(
|
|
198
|
+
pos: NDArray[np.float64],
|
|
199
|
+
v_f: float,
|
|
200
|
+
reflection: float,
|
|
201
|
+
beta: float,
|
|
202
|
+
psi_0: float,
|
|
203
|
+
) -> NDArray[np.float64]: ...
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def voltage_vs_position(
|
|
207
|
+
pos: float | NDArray[np.float64],
|
|
208
|
+
v_f: float,
|
|
209
|
+
reflection: float,
|
|
210
|
+
beta: float,
|
|
211
|
+
psi_0: float,
|
|
212
|
+
) -> float | NDArray[np.float64]:
|
|
213
|
+
r"""Compute voltage in coaxial line at given position.
|
|
214
|
+
|
|
215
|
+
The equation is:
|
|
216
|
+
|
|
217
|
+
.. math::
|
|
218
|
+
|V(z)| = |V_f| \sqrt{1 + |\Gamma|^2 + 2|\Gamma|\cos{(2\beta z +
|
|
219
|
+
\psi_0)}}
|
|
220
|
+
|
|
221
|
+
which comes from:
|
|
222
|
+
|
|
223
|
+
.. math::
|
|
224
|
+
V(z) = V_f \mathrm{e}^{-j\beta z} + \Gamma V_f \mathrm{e}^{j\beta z}
|
|
225
|
+
|
|
226
|
+
Parameters
|
|
227
|
+
----------
|
|
228
|
+
pos :
|
|
229
|
+
:math:`z` position in :unit:`m`.
|
|
230
|
+
v_f :
|
|
231
|
+
Forward voltage :math:`V_f` in :unit:`V`.
|
|
232
|
+
gamma :
|
|
233
|
+
Voltage reflexion coefficient :math:`\Gamma`.
|
|
234
|
+
beta :
|
|
235
|
+
Propagation constant :math:`\beta` in :unit:`m^{-1}`.
|
|
236
|
+
psi_0 :
|
|
237
|
+
Dephasing constant :math:`\psi_0`.
|
|
238
|
+
|
|
239
|
+
Returns
|
|
240
|
+
-------
|
|
241
|
+
voltage :
|
|
242
|
+
:math:`V(z)` at proper position in :unit:`V`.
|
|
243
|
+
|
|
244
|
+
"""
|
|
245
|
+
assert not isinstance(v_f, complex), "not implemented"
|
|
246
|
+
assert not isinstance(reflection, complex), "not implemented"
|
|
247
|
+
|
|
248
|
+
voltage = v_f * np.sqrt(
|
|
249
|
+
1.0
|
|
250
|
+
+ reflection**2
|
|
251
|
+
+ 2.0 * reflection * np.cos(2.0 * beta * pos + psi_0)
|
|
252
|
+
)
|
|
253
|
+
return voltage
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Define a class to create the proper :class:`.Instrument`."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
import multipac_testbench.instruments as ins
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
STRING_TO_INSTRUMENT_CLASS = {
|
|
11
|
+
"CurrentProbe": ins.CurrentProbe,
|
|
12
|
+
"ElectricFieldProbe": ins.FieldProbe,
|
|
13
|
+
"FieldProbe": ins.FieldProbe,
|
|
14
|
+
"ForwardPower": ins.ForwardPower,
|
|
15
|
+
"OpticalFibre": ins.OpticalFibre,
|
|
16
|
+
"Penning": ins.Penning,
|
|
17
|
+
"ReflectedPower": ins.ReflectedPower,
|
|
18
|
+
} #:
|
|
19
|
+
INSTRUMENT_NAME_T = Literal[
|
|
20
|
+
"CurrentProbe",
|
|
21
|
+
"ElectricFieldProbe",
|
|
22
|
+
"FieldProbe",
|
|
23
|
+
"ForwardPower",
|
|
24
|
+
"OpticalFibre",
|
|
25
|
+
"Penning",
|
|
26
|
+
"ReflectedPower",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class InstrumentFactory:
|
|
31
|
+
"""Class to create instruments."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, freq_mhz: float | None = None) -> None:
|
|
34
|
+
"""Set user-defined constants to create correspondig instrument."""
|
|
35
|
+
self.freq_mhz = freq_mhz
|
|
36
|
+
|
|
37
|
+
def run(
|
|
38
|
+
self,
|
|
39
|
+
name: str,
|
|
40
|
+
df_data: pd.DataFrame,
|
|
41
|
+
class_name: INSTRUMENT_NAME_T,
|
|
42
|
+
column_header: str | list[str] | None = None,
|
|
43
|
+
**instruments_kw: Any,
|
|
44
|
+
) -> ins.Instrument:
|
|
45
|
+
"""Take the proper subclass, instantiate it and return it.
|
|
46
|
+
|
|
47
|
+
Parameters
|
|
48
|
+
----------
|
|
49
|
+
name :
|
|
50
|
+
Name of the instrument. For clarity, it should match the name of a
|
|
51
|
+
column in ``df_data`` when it is possible.
|
|
52
|
+
df_data :
|
|
53
|
+
Content of the multipactor tests results ``CSV`` file.
|
|
54
|
+
class_name :
|
|
55
|
+
Name of the instrument class, as given in the ``TOML`` file.
|
|
56
|
+
column_header :
|
|
57
|
+
Name of the column(s) from which the data of the instrument will
|
|
58
|
+
be taken. The default is None, in which case ``column_header`` is
|
|
59
|
+
set to ``name``. In general it is not necessary to provide it. An
|
|
60
|
+
exception is when several ``CSV`` columns should be loaded in the
|
|
61
|
+
instrument.
|
|
62
|
+
instruments_kw :
|
|
63
|
+
Other keyword arguments in the ``TOML`` file.
|
|
64
|
+
|
|
65
|
+
Returns
|
|
66
|
+
-------
|
|
67
|
+
instrument :
|
|
68
|
+
Instrument properly subclassed.
|
|
69
|
+
|
|
70
|
+
"""
|
|
71
|
+
assert class_name in STRING_TO_INSTRUMENT_CLASS, (
|
|
72
|
+
f"{class_name = } not recognized, check STRING_TO_INSTRUMENT_CLASS"
|
|
73
|
+
"in instrument/factory.py"
|
|
74
|
+
)
|
|
75
|
+
instrument_class = STRING_TO_INSTRUMENT_CLASS[class_name]
|
|
76
|
+
|
|
77
|
+
if column_header is None:
|
|
78
|
+
column_header = name
|
|
79
|
+
|
|
80
|
+
raw_data = df_data[column_header]
|
|
81
|
+
|
|
82
|
+
if isinstance(raw_data, pd.DataFrame):
|
|
83
|
+
return instrument_class.from_pd_dataframe(
|
|
84
|
+
name, raw_data, **instruments_kw
|
|
85
|
+
)
|
|
86
|
+
return instrument_class(name, raw_data, **instruments_kw)
|
|
87
|
+
|
|
88
|
+
def run_virtual(
|
|
89
|
+
self,
|
|
90
|
+
instruments: Sequence[ins.Instrument],
|
|
91
|
+
is_global: bool = False,
|
|
92
|
+
**kwargs,
|
|
93
|
+
) -> list[ins.VirtualInstrument]:
|
|
94
|
+
"""Add the implemented :class:`.VirtualInstrument`.
|
|
95
|
+
|
|
96
|
+
Parameters
|
|
97
|
+
----------
|
|
98
|
+
instruments :
|
|
99
|
+
The :class:`.Instrument` that were already created. They are used
|
|
100
|
+
to compute derived quantities, in particular :math:`SWR` and
|
|
101
|
+
:math:`R`.
|
|
102
|
+
is_global :
|
|
103
|
+
Tells if the :class:`.IMeasurementPoint` from which this method is
|
|
104
|
+
called is global. It allows to forbid creation of one
|
|
105
|
+
:class:`.Frequency` or one :class:`.SWR` instrument per
|
|
106
|
+
:class:`.IMeasurementPoint`.
|
|
107
|
+
kwargs :
|
|
108
|
+
Other keyword arguments passed to :meth:`._power_related`.
|
|
109
|
+
|
|
110
|
+
Returns
|
|
111
|
+
-------
|
|
112
|
+
virtuals :
|
|
113
|
+
The created virtual instruments.
|
|
114
|
+
|
|
115
|
+
"""
|
|
116
|
+
virtuals = []
|
|
117
|
+
|
|
118
|
+
power_related = []
|
|
119
|
+
if is_global:
|
|
120
|
+
power_related = self._power_related(instruments, **kwargs)
|
|
121
|
+
if len(power_related) > 0:
|
|
122
|
+
virtuals += power_related
|
|
123
|
+
|
|
124
|
+
n_points = len(instruments[0].data_as_pd)
|
|
125
|
+
constants = []
|
|
126
|
+
if is_global:
|
|
127
|
+
constants = self._constant_values_defined_by_user(n_points)
|
|
128
|
+
if len(constants) > 0:
|
|
129
|
+
virtuals += constants
|
|
130
|
+
|
|
131
|
+
return virtuals
|
|
132
|
+
|
|
133
|
+
def _power_related(
|
|
134
|
+
self, instruments: Sequence[ins.Instrument], **kwargs
|
|
135
|
+
) -> Sequence[ins.VirtualInstrument]:
|
|
136
|
+
"""Create :class:`.ReflectionCoefficient` and :class:`.SWR`."""
|
|
137
|
+
forwards = [x for x in instruments if isinstance(x, ins.ForwardPower)]
|
|
138
|
+
reflecteds = [
|
|
139
|
+
x for x in instruments if isinstance(x, ins.ReflectedPower)
|
|
140
|
+
]
|
|
141
|
+
if len(forwards) != 1 or len(reflecteds) != 1:
|
|
142
|
+
logging.error(
|
|
143
|
+
"Should have exactly one ForwardPower and one ReflectedPower "
|
|
144
|
+
"instruments. Skipping SWR and R, this may create problems in "
|
|
145
|
+
"the future."
|
|
146
|
+
)
|
|
147
|
+
return ()
|
|
148
|
+
|
|
149
|
+
forward = forwards[0]
|
|
150
|
+
reflected = reflecteds[0]
|
|
151
|
+
reflection_coefficient = ins.ReflectionCoefficient.from_powers(
|
|
152
|
+
forward, reflected, **kwargs
|
|
153
|
+
)
|
|
154
|
+
swr = ins.SWR.from_reflection_coefficient(
|
|
155
|
+
reflection_coefficient, **kwargs
|
|
156
|
+
)
|
|
157
|
+
return reflection_coefficient, swr
|
|
158
|
+
|
|
159
|
+
def _constant_values_defined_by_user(
|
|
160
|
+
self,
|
|
161
|
+
n_points: int,
|
|
162
|
+
) -> Sequence[ins.VirtualInstrument]:
|
|
163
|
+
"""Define a fake frequency probe. Maybe a fake SWR, fake R later."""
|
|
164
|
+
constants = []
|
|
165
|
+
if self.freq_mhz is not None:
|
|
166
|
+
constants.append(
|
|
167
|
+
ins.Frequency.from_user_defined_frequency(
|
|
168
|
+
self.freq_mhz, n_points
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
return constants
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Define a fake frequency probe."""
|
|
2
|
+
|
|
3
|
+
from typing import Self
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from multipac_testbench.instruments.virtual_instrument import VirtualInstrument
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Frequency(VirtualInstrument):
|
|
11
|
+
r"""Store a frequency.
|
|
12
|
+
|
|
13
|
+
By default, the frequency is in :unit:`MHz`.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def from_user_defined_frequency(
|
|
19
|
+
cls,
|
|
20
|
+
freq_mhz: float,
|
|
21
|
+
n_points: int,
|
|
22
|
+
name: str = "Reference frequency",
|
|
23
|
+
**kwargs,
|
|
24
|
+
) -> Self:
|
|
25
|
+
r"""Instantiate the object with a constant frequency.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
freq_mhz :
|
|
30
|
+
Frequency in :unit:`MHz`.
|
|
31
|
+
n_points :
|
|
32
|
+
Number of points to fill.
|
|
33
|
+
name :
|
|
34
|
+
Name of the series and of the instrument.
|
|
35
|
+
kwargs :
|
|
36
|
+
Other keyword arguments passed to ``pd.Series`` and constructor.
|
|
37
|
+
|
|
38
|
+
Returns
|
|
39
|
+
-------
|
|
40
|
+
frequency :
|
|
41
|
+
Instantiated object.
|
|
42
|
+
|
|
43
|
+
"""
|
|
44
|
+
raw_data = np.full(n_points, freq_mhz)
|
|
45
|
+
df_data = pd.Series(raw_data, name=name, **kwargs)
|
|
46
|
+
return cls(name, df_data, position=np.nan, **kwargs)
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def ylabel(cls) -> str:
|
|
50
|
+
"""Label used for plots."""
|
|
51
|
+
return r"RF frequency $f~[\mathrm{MHz}]$"
|