Skip to content

Spectrum Amplitude

Overview

FeatureCalcSpectrumAmplitude tracks the vibration amplitude at a configurable frequency band in a wind turbine's frequency spectrum over time. This creates a trend feature that can signal component degradation — for example, a rising amplitude at the high-speed shaft rotation frequency indicates increasing wear on that bearing.

Example use case: Track the peak amplitude in a ±0.05-order band around the high-speed shaft rotation frequency (FREQ-HSS-RF) using the radial accelerometer spectrum.


Calculation Logic

Initialization

At instantiation the calculator:

  1. Validates feature_options_json against its schema (raises ValueError on invalid input).
  2. Verifies that the attribute FREQ-<frequency_name> exists in performance_db.
  3. Loads the frequency history from RequiredVibrationFrequencies — a table of dated frequency values (in orders) for the component. This allows the center frequency to change over time (e.g., after a component replacement).

Per-Period Computation

Inside _compute():

  1. Fetch vibration spectrum data: Requests all spectrum records for the configured sensor, spectrum type, and (optionally) acquisition rate within the period.

  2. Timestamp alignment: Spectrum measurements are not guaranteed to fall exactly on 10-minute boundaries. The calculator uses join_asof(..., strategy="nearest", tolerance=4m59s) to match each spectrum record to the nearest 10-minute result timestamp. Records with no match within the tolerance are discarded.

  3. Center frequency resolution: For each matched timestamp, look up the center frequency from the frequency history table using a join_asof(..., strategy="backward"). This means the most-recently valid frequency is used for each timestamp (forward-fill semantics).

  4. Band filtering and aggregation: For each matched spectrum:

    • Compute band limits:

      Text Only
      band_left  = center_frequency - band_width / 2
      band_right = center_frequency + band_width / 2
      
    • Select all frequency bins f where band_left ≤ f ≤ band_right.

    • If no bins fall within the band, the timestamp is skipped (returns null).
    • Apply the configured operation (e.g., max, mean) to the amplitudes of the selected bins using a Polars Series method.
  5. Result: Only timestamps with a computed value are kept — null timestamps are dropped from the output rather than being stored as null.

Note

Spectrum data is stored as numpy arrays of (frequencies, amplitudes) pairs in the value column of the vibration data table. The frequency axis is in orders (multiples of shaft rotation speed), not Hz.


Database Requirements

Feature Attribute

Attribute Value
server_calc_type spectrum_amplitude
feature_options_json JSON object — see below

feature_options_json Schema

Key Type Required Description
frequency_name string Yes Subcomponent attribute name (without the FREQ- prefix) that holds the center frequency in orders. Must exist in performance_db as FREQ-<frequency_name>.
sensor_name string Yes Sensor identifier, as used in perfdb.vibration.spectrum.get.
acquisition_rate string or null Yes Acquisition rate filter (e.g., "High"). Set to null for non-Gamesa turbines.
spectrum_type string Yes "Normal" or "Envelope".
band_width number Yes Full width of the frequency band in orders. Band extends ± band_width/2 around the center.
operation string Yes Aggregation applied to amplitudes within the band. Any polars.Series method name — typically "max" or "mean".

Example:

JSON
{
    "frequency_name": "HSS-RF",
    "sensor_name": "4 - HSS - Radial",
    "acquisition_rate": "High",
    "spectrum_type": "Normal",
    "band_width": 0.1,
    "operation": "max"
}

Vibration Frequency History

The attribute FREQ-<frequency_name> must be defined for the object's subcomponent in performance_db. It must contain the center frequency in orders with valid start_date entries so the correct frequency is used at each timestamp.

Vibration Spectrum Data

Raw spectrum measurements must exist in the raw_data_values table for the object, sensor, acquisition rate, spectrum type, and time range being calculated.

Example SQL Setup

SQL
-- Create the feature
SELECT * FROM performance.fn_create_or_update_feature(
    'G97-2.07',
    'server_calc',
    'test_spectrum_amplitude',
    'Test feature for FeatureCalcSpectrumAmplitude',
    NULL, NULL, NULL, NULL
);

-- Set server_calc_type
SELECT * FROM performance.fn_set_feature_attribute(
    'test_spectrum_amplitude', 'G97-2.07',
    'server_calc_type',
    '{"attribute_value": "spectrum_amplitude"}'
);

-- Set feature_options_json
SELECT * FROM performance.fn_set_feature_attribute(
    'test_spectrum_amplitude', 'G97-2.07',
    'feature_options_json',
    '{"attribute_value": {"frequency_name": "HSS-RF", "sensor_name": "4 - HSS - Radial", "acquisition_rate": "High", "spectrum_type": "Normal", "band_width": 0.1, "operation": "max"}}'
);

Class Definition

FeatureCalcSpectrumAmplitude(object_name, feature)

FeatureCalculator class for features that are based on the amplitude of the vibration spectrum of a wind turbine.

The method will calculate the amplitude of a certain frequency band in the vibration spectrum of a wind turbine.

For this to work the feature must have attribute feature_options_json with the following keys:

  • frequency_name: Name of the subcomponent attribute that contains the value of the desired frequency, not including the "FREQ-" prefix. This must be in orders and will be considered as the center frequency of the band.
  • sensor_name: Name of the sensor that will be used to get the spectrum. Names must be the ones available for perfdb.vibration.spectrum.get method.
  • acquisition_rate: Acquisition rate of the sensor. Only applicable for Gamesa turbines. For other manufacturers set this to none.
  • spectrum_type: Type of the spectrum that will be used. Options are: 'Normal' or 'Envelope'.
  • band_width: Width of the band that will be used to calculate the amplitude. This to allow for getting values not only for the center frequency but also for the surrounding frequencies. Consider that the band will be centered in the frequency defined in frequency_name and will have a width of band_width/2 in each side.
  • operation: Which operation will be used to aggregate all amplitude values in the band. Options are all available for polars.Series, but in most cases max or mean should be used.

Parameters:

  • object_name

    (str) –

    Name of the object for which the feature is calculated. It must exist in performance_db.

  • feature

    (str) –

    Feature of the object that is calculated. It must exist in performance_db.

Source code in echo_energycalc/feature_calc_spectrum_amplitude.py
Python
def __init__(
    self,
    object_name: str,
    feature: str,
) -> None:
    """
    FeatureCalculator class for features that are based on the amplitude of the vibration spectrum of a wind turbine.

    The method will calculate the amplitude of a certain frequency band in the vibration spectrum of a wind turbine.

    For this to work the feature must have attribute `feature_options_json` with the following keys:

    - `frequency_name`: Name of the subcomponent attribute that contains the value of the desired frequency, not including the "FREQ-" prefix. This must be in orders and will be considered as the center frequency of the band.
    - `sensor_name`: Name of the sensor that will be used to get the spectrum. Names must be the ones available for `perfdb.vibration.spectrum.get` method.
    - `acquisition_rate`: Acquisition rate of the sensor. Only applicable for Gamesa turbines. For other manufacturers set this to none.
    - `spectrum_type`: Type of the spectrum that will be used. Options are: 'Normal' or 'Envelope'.
    - `band_width`: Width of the band that will be used to calculate the amplitude. This to allow for getting values not only for the center frequency but also for the surrounding frequencies. Consider that the band will be centered in the frequency defined in `frequency_name` and will have a width of `band_width`/2 in each side.
    - `operation`: Which operation will be used to aggregate all amplitude values in the band. Options are all available for polars.Series, but in most cases `max` or `mean` should be used.

    Parameters
    ----------
    object_name : str
        Name of the object for which the feature is calculated. It must exist in performance_db.
    feature : str
        Feature of the object that is calculated. It must exist in performance_db.
    """
    # initialize parent class
    super().__init__(object_name, feature)

    # requirements for the feature calculator
    self._add_requirement(RequiredFeatureAttributes(self.object, self.feature, ["feature_options_json"]))
    self._fetch_requirements()

    # validating feature options
    self._validate_feature_options()

feature property

Feature that is calculated. This will be defined in the constructor and cannot be changed.

Returns:

  • str

    Name of the feature that is calculated.

name property

Name of the feature calculator. Is defined in child classes of FeatureCalculator.

This must be equal to the "server_calc_type" attribute of the feature in performance_db.

Returns:

  • str

    Name of the feature calculator.

object property

Object for which the feature is calculated. This will be defined in the constructor and cannot be changed.

Returns:

  • str

    Object name for which the feature is calculated.

requirements property

List of requirements of the feature calculator. Is defined in child classes of FeatureCalculator.

Returns:

  • dict[str, list[CalculationRequirement]]

    Dict of requirements.

    The keys are the names of the classes of the requirements and the values are lists of requirements of that class.

    For example: {"RequiredFeatures": [RequiredFeatures(...), RequiredFeatures(...)], "RequiredObjects": [RequiredObjects(...)]}

result property

Result of the calculation. This is None until the method "calculate" is called.

Returns:

  • DataFrame | None

    Polars DataFrame with a "timestamp" column and one or more feature value columns. None until calculate is called.

calculate(period, save_into=None, cached_data=None, **kwargs)

Run the calculation for the given period and optionally save the result.

Calls :meth:_compute to get the result, stores it in :attr:result, then calls :meth:save. Subclasses should implement :meth:_compute instead of overriding this method.

Parameters:

  • period

    (DateTimeRange) –

    Period for which the feature will be calculated.

  • save_into

    (Literal['all', 'performance_db'] | None, default: None ) –
    • "all": save in performance_db and bazefield.
    • "performance_db": save only in performance_db.
    • None: do not save.

    By default None.

  • cached_data

    (DataFrame | None, default: None ) –

    Polars DataFrame with features already fetched/calculated. Passed to _compute to enable chained calculations without re-querying performance_db. By default None.

  • **kwargs

    Forwarded to :meth:save.

Returns:

  • DataFrame

    Polars DataFrame with a "timestamp" column and one or more feature value columns.

Source code in echo_energycalc/feature_calc_core.py
Python
def calculate(
    self,
    period: DateTimeRange,
    save_into: Literal["all", "performance_db"] | None = None,
    cached_data: pl.DataFrame | None = None,
    **kwargs,
) -> pl.DataFrame:
    """
    Run the calculation for the given period and optionally save the result.

    Calls :meth:`_compute` to get the result, stores it in :attr:`result`,
    then calls :meth:`save`. Subclasses should implement :meth:`_compute` instead
    of overriding this method.

    Parameters
    ----------
    period : DateTimeRange
        Period for which the feature will be calculated.
    save_into : Literal["all", "performance_db"] | None, optional
        - ``"all"``: save in performance_db and bazefield.
        - ``"performance_db"``: save only in performance_db.
        - ``None``: do not save.

        By default None.
    cached_data : pl.DataFrame | None, optional
        Polars DataFrame with features already fetched/calculated. Passed to
        ``_compute`` to enable chained calculations without re-querying
        performance_db. By default None.
    **kwargs
        Forwarded to :meth:`save`.

    Returns
    -------
    pl.DataFrame
        Polars DataFrame with a ``"timestamp"`` column and one or more feature value columns.
    """
    result = self._compute(period, cached_data=cached_data)
    self._result = result
    self.save(save_into=save_into, **kwargs)
    return result

save(save_into=None, **kwargs)

Method to save the calculated feature values in performance_db.

Parameters:

  • save_into

    (Literal['all', 'performance_db'] | None, default: None ) –

    Argument that will be passed to the method "save". The options are: - "all": The feature will be saved in performance_db and bazefield. - "performance_db": the feature will be saved only in performance_db. - None: The feature will not be saved.

    By default None.

  • **kwargs

    (dict, default: {} ) –

    Not being used at the moment. Here only for compatibility.

Source code in echo_energycalc/feature_calc_core.py
Python
def save(
    self,
    save_into: Literal["all", "performance_db"] | None = None,
    **kwargs,  # noqa: ARG002
) -> None:
    """
    Method to save the calculated feature values in performance_db.

    Parameters
    ----------
    save_into : Literal["all", "performance_db"] | None, optional
        Argument that will be passed to the method "save". The options are:
        - "all": The feature will be saved in performance_db and bazefield.
        - "performance_db": the feature will be saved only in performance_db.
        - None: The feature will not be saved.

        By default None.
    **kwargs : dict, optional
        Not being used at the moment. Here only for compatibility.
    """
    # checking arguments
    if not isinstance(save_into, str | type(None)):
        raise TypeError(f"save_into must be a string or None, not {type(save_into)}")
    if isinstance(save_into, str) and save_into not in ["all", "performance_db"]:
        raise ValueError(f"save_into must be 'all', 'performance_db' or None, not {save_into}")

    # checking if calculation was done
    if self.result is None:
        raise ValueError(
            "The calculation was not done. Please call 'calculate' before calling 'save'.",
        )

    if save_into is None:
        return

    upload_to_bazefield = save_into == "all"

    if not isinstance(self.result, pl.DataFrame):
        raise TypeError(f"result must be a polars DataFrame, not {type(self.result)}.")
    if "timestamp" not in self.result.columns:
        raise ValueError("result DataFrame must contain a 'timestamp' column.")

    # rename feature columns to "object@feature" format expected by perfdb polars insert
    feat_cols = [c for c in self.result.columns if c != "timestamp"]
    result_pl = self.result.rename({col: f"{self.object}@{col}" for col in feat_cols})

    self._perfdb.features.values.series.insert(
        df=result_pl,
        on_conflict="update",
        bazefield_upload=upload_to_bazefield,
    )