Required Vibration Data¶
Overview¶
The RequiredVibrationData class is a subclass of CalculationRequirement that is used to get the vibration data for a list of objects. This requirement is used to check if the vibration data is present for a list of objects and to get the vibration data for the desired period.
Usage¶
Besides the name of the desired objects the requirements also needs arguments to determine which vibration data is needed. These arguments are in line with the ones of the perfdb.vibration.spectrum.get method and are detailed in the Class Definition section.
Below there is an example of how to instantiate this requirement:
requirement = RequiredVibrationData(
object_names=["LAN-LAN-01", "LAN-LAN-02"],
data_type="spectrum",
sensors=["4 - HSS - Radial"],
acquisition_frequencies=["High"],
spectrum_type="Envelope,
spectrum_unit="Order",
amplitude_type="Peak",
)
Keep in mind that this class supports returning both time series and spectrum data. This is specified in the data_type argument but the actual data availability will depend on the wind turbine model.
After instantiation and checking, the get_data method will return a DataFrame with the vibration data for the desired period. This DataFrame will not have an index and the columns available are object_name, sensor, acquisition_frequency, timestamp and value, where value is a 2D Numpy array with the vibration data (frequency x amplitude).
Database Requirements¶
This requirement expects that the vibration data is saved in table raw_data_values and the perfdb.vibration.spectrum.get method is configured to handle this data.
Class Definition¶
RequiredVibrationData(object_names, analysis_type, data_type='Vibration', sensors=None, acquisition_frequencies=None, variable_names=None, spectrum_type=None, spectrum_unit=None, amplitude_type=None, optional=False)
¶
Subclass of CalculationRequirement that defines the vibration data that is required for the calculation.
This will check the performance database for the existence of the required vibration data for the wanted objects.
Arguments here are aligned with the arguments from perfdb.vibration.spectrum.get method.
Parameters:
-
(object_names¶list[str]) –List of the object names for which the vibration data is required.
-
(analysis_type¶Literal['timeseries', 'spectrum']) –Type of data to get.
-
(data_type¶Literal['Vibration'] = "Vibration",, default:'Vibration') –Type of data to get. Can be either 'Vibration'. By default 'Vibration'.
-
(sensors¶list[VIBRATION_GE_ALLOWED_SENSOR_NAMES | VIBRATION_GAMESA_ALLOWED_SENSOR_NAMES] | None, default:None) –List of the sensors to get the data for. The options are as shown below:
- GE: "Planetary", "LSS", "HSS", "Generator RS", "Generator GS", "Main Bearing", "Tower Sway Axial", "Tower Sway Transverse"
- Gamesa: "1 - Generator GS - Radial", "2 - Planetary - Axial", "3 - Main Bearing GS - Radial", "4 - HSS - Radial", "5 - Main Bearing RS - Axial", "6 - HSS - Axial", "7 - Generator RS - Axial", "8 - Generator RS - Radial"
These must be specified with the matching manufacturer and cannot be mixed. If GE is selected only GE sensors are allowed and vice versa.
-
(acquisition_frequencies¶list[Literal['Low', 'High', 'Filter']] | None, default:None) –Acquisition frequency, only applicable for Gamesa turbines. For GE leave as None.
-
(spectrum_type¶Literal['Normal', 'Envelope'] | None, default:None) –What kind of spectrum should be returned. Only applicable for spectrum data.
-
(spectrum_unit¶Literal['Hz', 'Order'] | None, default:None) –Unit of the spectrum. Only applicable for spectrum data. Can be one of ['Hz', 'Order'].
-
(amplitude_type¶Literal['RMS', 'Peak', 'Peak-to-Peak'] | None, default:None) –Type of amplitude to return. Only applicable for spectrum data. Can be one of ["RMS", "Peak", "Peak-to-Peak"].
-
(optional¶bool, default:False) –Set to True if this is an optional requirement. by default False
Source code in echo_energycalc/calculation_requirement_vibration_data.py
def __init__(
self,
object_names: list[str],
analysis_type: Literal["timeseries", "spectrum"],
data_type: Literal["Vibration"] = "Vibration",
sensors: list[VIBRATION_GE_ALLOWED_SENSOR_NAMES | VIBRATION_GAMESA_ALLOWED_SENSOR_NAMES] | None = None,
acquisition_frequencies: list[Literal["Low", "High", "Filter"]] | None = None,
variable_names: Literal["Acceleration - X", "Acceleration - Y", "Position - X", "Position - Y"] | None = None,
spectrum_type: Literal["Normal", "Envelope"] | None = None,
spectrum_unit: Literal["Hz", "Order"] | None = None,
amplitude_type: Literal["RMS", "Peak", "Peak-to-Peak"] | None = None,
optional: bool = False,
) -> None:
"""
Subclass of CalculationRequirement that defines the vibration data that is required for the calculation.
This will check the performance database for the existence of the required vibration data for the wanted objects.
Arguments here are aligned with the arguments from perfdb.vibration.spectrum.get method.
Parameters
----------
object_names : list[str]
List of the object names for which the vibration data is required.
analysis_type : Literal["timeseries", "spectrum"]
Type of data to get.
data_type : Literal['Vibration'] = "Vibration",
Type of data to get. Can be either 'Vibration'. By default 'Vibration'.
sensors : list[VIBRATION_GE_ALLOWED_SENSOR_NAMES | VIBRATION_GAMESA_ALLOWED_SENSOR_NAMES] | None
List of the sensors to get the data for. The options are as shown below:
- GE: "Planetary", "LSS", "HSS", "Generator RS", "Generator GS", "Main Bearing", "Tower Sway Axial", "Tower Sway Transverse"
- Gamesa: "1 - Generator GS - Radial", "2 - Planetary - Axial", "3 - Main Bearing GS - Radial", "4 - HSS - Radial", "5 - Main Bearing RS - Axial", "6 - HSS - Axial", "7 - Generator RS - Axial", "8 - Generator RS - Radial"
These must be specified with the matching manufacturer and cannot be mixed. If GE is selected only GE sensors are allowed and vice versa.
acquisition_frequencies : list[Literal["Low", "High", "Filter"]] | None
Acquisition frequency, only applicable for Gamesa turbines. For GE leave as None.
spectrum_type : Literal["Normal", "Envelope"] | None
What kind of spectrum should be returned. Only applicable for spectrum data.
spectrum_unit : Literal['Hz', 'Order'] | None
Unit of the spectrum. Only applicable for spectrum data. Can be one of ['Hz', 'Order'].
amplitude_type : Literal["RMS", "Peak", "Peak-to-Peak"] | None
Type of amplitude to return. Only applicable for spectrum data. Can be one of ["RMS", "Peak", "Peak-to-Peak"].
optional : bool, optional
Set to True if this is an optional requirement. by default False
"""
super().__init__(optional)
self._manufacturer = None
# check if object_names is a list of strings
if not isinstance(object_names, list):
raise TypeError(f"object_names must be a list, not {type(object_names)}")
if not all(isinstance(name, str) for name in object_names):
raise TypeError(f"all object_names must be str, not {[type(name) for name in object_names]}")
self._object_names = object_names
# checking if analysis_type is valid
if analysis_type not in ["timeseries", "spectrum"]:
raise ValueError(f"analysis_type must be one of ['timeseries', 'spectrum'], not {analysis_type}")
self._analysis_type = analysis_type
# checking if data_type is valid
if data_type != "Vibration":
raise ValueError(f"data_type must be one of ['Vibration'], not {data_type}")
self._data_type = data_type
# we will only check sensors when actually getting the data, but lets check if not None
if sensors is None:
raise ValueError("sensors must be a list of sensors, not None")
self._sensors = sensors
# _acquisition_frequencies will be validated in check method
self._acquisition_frequencies = acquisition_frequencies
# _variable_names will be validated in check method
self._variable_names = variable_names
# values specific to spectrum
if analysis_type == "spectrum":
# checking if spectrum_type is valid
if spectrum_type not in ["Normal", "Envelope"]:
raise ValueError(f"spectrum_type must be one of ['Normal', 'Envelope'], not {spectrum_type}")
self._spectrum_type = spectrum_type
# checking if amplitude_type is valid
if amplitude_type not in ["RMS", "Peak", "Peak-to-Peak"]:
raise ValueError(f"amplitude_type must be one of ['RMS', 'Peak', 'Peak-to-Peak'], not {amplitude_type}")
self._amplitude_type = amplitude_type
# checking if spectrum_unit is valid
if spectrum_unit not in ["Hz", "Order"]:
raise ValueError(f"spectrum_unit must be one of ['Hz', 'Order'], not {spectrum_unit}")
self._spectrum_unit = spectrum_unit
else:
# checking if any of the spectrum specific parameters are not None
if any([spectrum_type, amplitude_type]):
raise ValueError(
"spectrum_type, amplitude_type must be None when analysis_type is timeseries",
)
self._spectrum_type = None
self._amplitude_type = None
self._spectrum_unit = None
acquisition_frequencies
property
¶
Acquisition frequency, only applicable for Gamesa turbines.
Returns:
-
list[Literal['Low', 'High', 'Filter']] | None–Acquisition frequency, only applicable for Gamesa turbines.
amplitude_type
property
¶
Type of amplitude to return.
Returns:
-
Literal['RMS', 'Peak', 'Peak-to-Peak'] | None–Type of amplitude to return.
analysis_type
property
¶
Type of analysis.
Returns:
-
Literal['timeseries', 'spectrum']–Type of analysis.
checked
property
¶
Attribute that defines if the requirement has been checked. It's value will start as False and will be set to True after the check method is called.
Returns:
-
bool–True if the requirement has been checked.
data
property
¶
Attribute used to store the data required for the calculation.
Initially it is None and will be set with the data acquired by the get_data method. The data type will depend on the subclass implementation, but usually it will be a polars DataFrame or a dictionary.
Returns:
-
Any | None–Returns the data required for the calculation.
data_type
property
¶
Type of data to get.
Returns:
-
Literal['Vibration']–Type of data to get.
fetched
property
¶
Attribute that defines if get_data() has been called on this requirement.
True even when the fetch returned no data (e.g. an optional requirement
that found nothing). Use this to distinguish "never fetched" from "fetched
but empty/None".
Returns:
-
bool–True if get_data() has been called at least once.
manufacturer
property
¶
Manufacturer of the wind turbine.
Returns:
-
str–Manufacturer of the wind turbine.
objects
property
¶
List of the object names for which the vibration data is required.
Returns:
-
list[str]–List of the object names for which the vibration data is required.
optional
property
¶
Attribute that defines if the requirement is optional.
If optional is True, the requirement is only validated to check if it could exist, not if it is actually present. This is useful for requirements that are not necessary for all calculations, but are useful for some of them.
Returns:
-
bool–True if the requirement is optional.
sensors
property
¶
List of the sensors to get the data for.
Returns:
-
list[VIBRATION_GE_ALLOWED_SENSOR_NAMES | VIBRATION_GAMESA_ALLOWED_SENSOR_NAMES] | None–List of the sensors to get the data for.
spectrum_type
property
¶
What kind of spectrum should be returned.
Returns:
-
Literal['Normal', 'Envelope'] | None–What kind of spectrum should be returned.
spectrum_unit
property
¶
Unit of the spectrum.
Returns:
-
Literal['Hz', 'Order'] | None–Unit of the spectrum.
variable_names
property
¶
Variable names to get the data for.
Returns:
-
Literal['Acceleration - X', 'Acceleration - Y', 'Position - X', 'Position - Y'] | None–Variable names to get the data for.
check()
¶
Check that the requirement is met.
This concrete implementation handles two concerns automatically so that
subclasses only need to implement _do_check():
- Already-checked guard — returns
Trueimmediately ifcheck()has already succeeded for this instance, avoiding redundant DB round-trips when_fetch_requirements()iterates requirements on every_compute()call. - Per-thread caching — when
_check_cache_key()returns a non-None key, the result produced by_do_check()is stored in a thread-local cache and reused by subsequent instances in the same thread with the same key. Because the cache is never shared across threads, no locking is needed and concurrent Polars operations inside_do_checkcannot deadlock.
The optional guard is intentionally delegated to _do_check() because
different subclasses have different optional semantics (see _do_check docs).
Returns:
-
bool–True if the requirement is met; raises on unmet non-optional requirements.
Source code in echo_energycalc/calculation_requirements_core.py
def check(self) -> bool:
"""
Check that the requirement is met.
This concrete implementation handles two concerns automatically so that
subclasses only need to implement ``_do_check()``:
1. **Already-checked guard** — returns ``True`` immediately if ``check()`` has
already succeeded for this instance, avoiding redundant DB round-trips when
``_fetch_requirements()`` iterates requirements on every ``_compute()`` call.
2. **Per-thread caching** — when ``_check_cache_key()`` returns a non-None key,
the result produced by ``_do_check()`` is stored in a thread-local cache and
reused by subsequent instances in the same thread with the same key. Because
the cache is never shared across threads, no locking is needed and concurrent
Polars operations inside ``_do_check`` cannot deadlock.
The **optional guard** is intentionally delegated to ``_do_check()`` because
different subclasses have different optional semantics (see ``_do_check`` docs).
Returns
-------
bool
True if the requirement is met; raises on unmet non-optional requirements.
"""
if self._checked:
return True
cache_key = self._check_cache_key()
if cache_key is not None:
_tl = type(self)._cache_local # noqa: SLF001
if not hasattr(_tl, "cache"):
_tl.cache = {}
cached = _tl.cache.get(cache_key)
if cached is None:
self._do_check()
_tl.cache[cache_key] = self._get_cache_value()
cached = _tl.cache[cache_key]
else:
logger.debug("Cache hit for %s (key=%s)", type(self).__name__, cache_key)
self._set_from_cache(cached)
else:
self._do_check()
self._checked = True
return True
get_data(period, **kwargs)
¶
Method used to get the vibration data for the required sensors and period.
Parameters:
-
(period¶DateTimeRange) –Desired period for the vibration data.
Returns:
-
DataFrame–DataFrame with the vibration data for the required sensors and period.
Source code in echo_energycalc/calculation_requirement_vibration_data.py
@validate_call
def get_data(self, period: DateTimeRange, **kwargs) -> pl.DataFrame: # noqa: ARG002
"""
Method used to get the vibration data for the required sensors and period.
Parameters
----------
period : DateTimeRange
Desired period for the vibration data.
Returns
-------
pl.DataFrame
DataFrame with the vibration data for the required sensors and period.
"""
# checking if necessary keyword arguments are present
if not isinstance(period, DateTimeRange):
raise TypeError(f"period must be a DateTimeRange object, not {type(period)}")
# check if requirement has been checked
if not self._checked:
self.check()
logger.debug("Fetching vibration %s data for objects %s over %s", self.analysis_type, self.objects, period)
try:
# getting the data as polars directly from the database
if self.analysis_type == "timeseries":
df_pl = self._perfdb.vibration.timeseries.get(
period=period,
object_names=self.objects,
data_type=self.data_type,
manufacturer=self.manufacturer,
sensors=self.sensors,
acquisition_frequencies=self.acquisition_frequencies,
variable_names=self.variable_names,
output_type="pl.DataFrame",
)
else:
df_pl = self._perfdb.vibration.spectrum.get(
period=period,
object_names=self.objects,
data_type=self.data_type,
manufacturer=self.manufacturer,
sensors=self.sensors,
acquisition_frequencies=self.acquisition_frequencies,
variable_names=self.variable_names,
spectrum_type=self.spectrum_type,
amplitude_type=self.amplitude_type,
unit=self.spectrum_unit,
output_type="pl.DataFrame",
)
df_pl = df_pl.select(["object_name", "sensor", "acquisition_frequency", "timestamp", "value"])
logger.debug("Fetched vibration data: %d rows", len(df_pl))
# add metadata columns
spectrum_type_val = None if self.analysis_type == "timeseries" else self.spectrum_type
self._data = df_pl.with_columns(
pl.lit("timeseries", dtype=pl.Utf8).alias("analysis_type"),
pl.lit(spectrum_type_val, dtype=pl.Utf8).alias("spectrum_type"),
).select(["object_name", "analysis_type", "spectrum_type", "sensor", "acquisition_frequency", "timestamp", "value"])
except Exception as e:
if self.optional:
self._data = None
else:
raise e
finally:
self._fetched = True
return self.data