"""Define power probes to measure forward and reflected power."""
import logging
from functools import partial
import numpy as np
from multipac_testbench.instruments.instrument import Instrument
from multipac_testbench.instruments.virtual_instrument import VirtualInstrument
from multipac_testbench.util.fixers import fix_power_channel_b
from multipac_testbench.util.post_treaters import get_data_above_noise
from multipac_testbench.util.transfer_functions import power
from multipac_testbench.util.types import POST_TREATER_T
from numpy.typing import NDArray
[docs]
class Power(Instrument):
"""An instrument to measure power."""
def __init__(
self,
*args,
position: float = np.nan,
p_low: float | None = None,
p_high: float | None = None,
k_fix: float | None = None,
alpha_fix: float | None = None,
ensure_no_negative: bool = True,
**kwargs,
) -> None:
r"""Instantiate the instrument, declare other specific attributes.
See Also
--------
:func:`.transfer_functions.power`
:func:`.transfer_functions.power_channel_b`
Notes
-----
If ``k_fix`` and ``alpha_fix`` are provided, we add a second transfer
function, :func:`.transfer_functions.power_channel_b`. It was proposed
to fix the power measure on channel B.
Parameters
----------
p_low, p_high :
Lowest and highest measurable powers in :unit:`W/V`. Must
correspond to what is set in the watt meter. Correspond to
``REC_LIM_LOW`` and ``REC_LIM_UPP`` in LabView.
k_fix :
Fix slope constant.
alpha_fix :
Fix exponent constant.
ensure_no_negative :
Set negative powers to :math:`0~\mathrm{V}`. Should be useless.
"""
self._a_calib: float
self._b_calib: float
self._ensure_no_negative = ensure_no_negative
if p_low is not None and p_high is not None:
self._a_calib, self._b_calib = self._get_wattmeter_calibration(
p_low, p_high
)
self._a_fix: float
if k_fix is not None:
self._k_fix = k_fix
self._alpha_fix: float
if alpha_fix is not None:
self._alpha_fix = alpha_fix
super().__init__(*args, position=position, **kwargs)
[docs]
@classmethod
def ylabel(cls) -> str:
"""Label used for plots."""
return r"Power [W]"
[docs]
def where_is_growing(self, *args, **kwargs) -> NDArray[np.bool]:
"""Identify regions where the signal is increasing ("growing").
.. deprecated:: 1.7.0
Alias to :meth:`.Power.growth_mask`, consider calling it directly.
"""
return self.growth_mask(*args, **kwargs)
[docs]
def growth_mask(
self,
minimum_number_of_points: int = 50,
n_trailing_points_to_check: int = 40,
width: int = 10,
**kwargs,
) -> NDArray[np.bool]:
return super().growth_mask(
minimum_number_of_points=minimum_number_of_points,
n_trailing_points_to_check=n_trailing_points_to_check,
width=width,
**kwargs,
)
[docs]
def _get_wattmeter_calibration(
self, p_low: float, p_high: float, v_low: float = 0.0, v_high=1.0
) -> tuple[float, float]:
r"""Compute the wattmetre transfer function parameters.
We just find the linear relation parameters:
.. math:
P_\mathrm{W} = a_\mathrm{calib} \times V_\mathrm{acqui} +
b_\mathrm{calib}
"""
a_calib = (p_high - p_low) / (v_high - v_low)
b_calib = p_low - a_calib * v_low
return a_calib, b_calib
@property
def _transfer_functions(self) -> list[POST_TREATER_T]:
assert hasattr(self, "_a_calib")
assert hasattr(self, "_b_calib")
funcs = [
partial(
power,
a_calib=self._a_calib,
b_calib=self._b_calib,
ensure_no_negative=self._ensure_no_negative,
)
]
if hasattr(self, "_alpha_fix") and hasattr(self, "_k_fix"):
funcs.append(
partial(
fix_power_channel_b,
k_fix=self._k_fix,
alpha_fix=self._alpha_fix,
)
)
return funcs
[docs]
class ForwardPower(Power):
"""Store the forward power."""
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
if (
self._is_raw
and hasattr(self, "_alpha_fix")
or hasattr(self, "_k_fix")
):
logging.warning(
"ForwardPower typically measured on channel A, so you should "
"not provide the arguments for the channel B fix."
)
[docs]
@classmethod
def ylabel(cls) -> str:
"""Label used for plots."""
return r"Forward power $P_f$ [W]"
[docs]
class ReflectedPower(Power):
"""Store the reflected power."""
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
if self._is_raw and not (
hasattr(self, "_alpha_fix") and hasattr(self, "_k_fix")
):
logging.warning(
"ReflectedPower typically measured on channel B, so you should"
" provide the arguments for the channel B fix."
)
[docs]
@classmethod
def ylabel(cls) -> str:
"""Label used for plots."""
return r"Reflected power $P_r$ [W]"
[docs]
class Sync(VirtualInstrument):
"""Store when power is ON or OFF.
Note
----
Do not use it in :class:`.MultipactorTest`, it only makes sense in a
:class:`.PowerStep`.
"""
def __init__(
self, *args, position: NDArray[np.float64] | float = np.nan, **kwargs
) -> None:
super().__init__(*args, position=position, **kwargs)
@property
def trigger(self) -> int:
"""Compute width of the trigger in samples."""
return len(get_data_above_noise(self.data, noise_level=0))
[docs]
@classmethod
def ylabel(cls) -> str:
"""Label used for plots."""
return r"Sync $[\mathrm{V}]$"
@property
def _transfer_functions(self) -> list[POST_TREATER_T]:
return []