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.
Files changed (47) hide show
  1. multipac_testbench/__init__.py +5 -0
  2. multipac_testbench/instruments/__init__.py +44 -0
  3. multipac_testbench/instruments/current_probe.py +16 -0
  4. multipac_testbench/instruments/electric_field/__init__.py +8 -0
  5. multipac_testbench/instruments/electric_field/field_probe.py +106 -0
  6. multipac_testbench/instruments/electric_field/i_electric_field.py +22 -0
  7. multipac_testbench/instruments/electric_field/reconstructed.py +253 -0
  8. multipac_testbench/instruments/factory.py +171 -0
  9. multipac_testbench/instruments/frequency.py +51 -0
  10. multipac_testbench/instruments/instrument.py +504 -0
  11. multipac_testbench/instruments/optical_fibre.py +21 -0
  12. multipac_testbench/instruments/penning.py +16 -0
  13. multipac_testbench/instruments/power.py +83 -0
  14. multipac_testbench/instruments/reflection_coefficient.py +83 -0
  15. multipac_testbench/instruments/swr.py +73 -0
  16. multipac_testbench/instruments/virtual_instrument.py +18 -0
  17. multipac_testbench/measurement_point/__init__.py +9 -0
  18. multipac_testbench/measurement_point/factory.py +145 -0
  19. multipac_testbench/measurement_point/global_diagnostics.py +45 -0
  20. multipac_testbench/measurement_point/i_measurement_point.py +180 -0
  21. multipac_testbench/measurement_point/pick_up.py +80 -0
  22. multipac_testbench/multipactor_band/__init__.py +1 -0
  23. multipac_testbench/multipactor_band/campaign_multipactor_bands.py +15 -0
  24. multipac_testbench/multipactor_band/creator.py +247 -0
  25. multipac_testbench/multipactor_band/instrument_multipactor_bands.py +116 -0
  26. multipac_testbench/multipactor_band/multipactor_band.py +80 -0
  27. multipac_testbench/multipactor_band/polisher.py +176 -0
  28. multipac_testbench/multipactor_band/test_multipactor_bands.py +154 -0
  29. multipac_testbench/multipactor_band/util.py +122 -0
  30. multipac_testbench/multipactor_test.py +1214 -0
  31. multipac_testbench/test_campaign.py +752 -0
  32. multipac_testbench/theoretical/__init__.py +0 -0
  33. multipac_testbench/theoretical/somersalo.py +324 -0
  34. multipac_testbench/theoretical/somersalo_data.txt +25 -0
  35. multipac_testbench/util/__init__.py +1 -0
  36. multipac_testbench/util/animate.py +50 -0
  37. multipac_testbench/util/filtering.py +195 -0
  38. multipac_testbench/util/helper.py +71 -0
  39. multipac_testbench/util/log_manager.py +163 -0
  40. multipac_testbench/util/multipactor_detectors.py +97 -0
  41. multipac_testbench/util/plot.py +508 -0
  42. multipac_testbench/util/post_treaters.py +130 -0
  43. multipac_testbench-1.6.3.dist-info/METADATA +65 -0
  44. multipac_testbench-1.6.3.dist-info/RECORD +47 -0
  45. multipac_testbench-1.6.3.dist-info/WHEEL +5 -0
  46. multipac_testbench-1.6.3.dist-info/licenses/LICENSE +21 -0
  47. multipac_testbench-1.6.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,5 @@
1
+ """This package stores all the function to study MULTIPAC testbench."""
2
+
3
+ import importlib.metadata
4
+
5
+ __version__ = importlib.metadata.version("multipac_testbench")
@@ -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}]$"