Skip to content

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:

Python
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
Python
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():

  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.

Source code in echo_energycalc/calculation_requirements_core.py
Python
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
Python
@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