Time Series Amplitude¶
Overview¶
FeatureCalcTimeseriesAmplitude reduces a raw vibration time series (a waveform stored per 10-minute period) to a single scalar amplitude value. This produces a feature that trends the vibration amplitude over time, making it easy to spot deterioration in rotating machinery or blade structural health.
Example use case: Calculate the peak-to-peak blade gap amplitude for Blade C using a blade gap sensor, to detect changes in blade flapping behavior.
Calculation Logic¶
Per-Period Computation¶
Inside _compute():
-
Fetch vibration time series data: Requests all time series records for the configured sensor, data type, and (optionally) acquisition rate and variable name within the period.
-
Timestamp alignment: Time series measurements may not fall exactly on 10-minute boundaries. The calculator uses
join_asof(..., strategy="nearest", tolerance=4m59s)to match each waveform record to the nearest 10-minute result timestamp. Records with no match within the tolerance are discarded. -
Amplitude calculation: For each matched waveform (stored as a numpy array in the
valuecolumn), apply the configuredoperation:Operation Formula peakmax(waveform)peak-to-peakmax(waveform) - min(waveform)rmssqrt(mean(waveform²))meanmean(waveform)medianmedian(waveform)stdstd(waveform) -
Result: Only timestamps with a computed value are kept — null timestamps are dropped from the output rather than stored as null. This means the output DataFrame can have fewer rows than the requested period if vibration data is sparse.
Database Requirements¶
Feature Attribute¶
| Attribute | Value |
|---|---|
server_calc_type |
timeseries_amplitude |
feature_options_json |
JSON object — see below |
feature_options_json Schema¶
| Key | Type | Required | Description |
|---|---|---|---|
sensor_name |
string | Yes | Sensor identifier, as used in perfdb.vibration.timeseries.get. |
data_type |
string | No | Type of data to retrieve (e.g., "Vibration", "Blade Gap"). |
acquisition_rate |
string or null | Yes | Acquisition rate filter. Set to null for non-Gamesa turbines and non-vibration sensors. |
variable_name |
string or null | No | Variable name filter. Only applicable for Gamesa blade gap sensors (deprecated for other types). Set to null if not applicable. |
operation |
string | Yes | Amplitude operation to apply. One of: "peak", "peak-to-peak", "rms", "mean", "median", "std". |
Example:
{
"sensor_name": "Blade C",
"data_type": "Blade Gap",
"acquisition_rate": null,
"variable_name": "Position - Y",
"operation": "peak-to-peak"
}
Vibration Time Series Data¶
Raw waveform measurements must exist in the raw_data_values table for the object, sensor, data type, acquisition rate, variable name, and time range being calculated.
Example SQL Setup¶
-- Create the feature
SELECT * FROM performance.fn_create_or_update_feature(
'G97-2.07',
'server_calc',
'test_timeseries_amplitude',
'Test for FeatureCalcTimeseriesAmplitude',
NULL, NULL, NULL, NULL
);
-- Set server_calc_type
SELECT * FROM performance.fn_set_feature_attribute(
'test_timeseries_amplitude', 'G97-2.07',
'server_calc_type',
'{"attribute_value": "timeseries_amplitude"}'
);
-- Set feature_options_json
SELECT * FROM performance.fn_set_feature_attribute(
'test_timeseries_amplitude', 'G97-2.07',
'feature_options_json',
'{"attribute_value": {"sensor_name": "Blade C", "data_type": "Blade Gap", "acquisition_rate": null, "variable_name": "Position - Y", "operation": "peak-to-peak"}}'
);
Class Definition¶
FeatureCalcTimeseriesAmplitude(object_name, feature)
¶
FeatureCalculator class for features that are based on the amplitude of the vibration time series of a wind turbine.
The calculation is fairly simple:
- Get the vibration timeseries for the specified sensor, data type, acquisition rate, and variable name.
- Apply the desired operation (peak, peak-to-peak, rms, mean, median, std) to the timeseries data. This will reduce the timeseries to a single value for each timestamp.
For this to work the feature must have attribute feature_options_json with the following keys:
sensor_name: Name of the sensor that will be used to get the timeseries. Names must be the ones available forperfdb.vibration.timeseries.getmethod.data_type: Type of the data that will be used to get the timeseries. All values allowed inperfdb.vibration.timeseries.getmethod are valid. For example, "Vibration", etc.acquisition_rate: Acquisition rate of the sensor. Only applicable for Gamesa turbines and vibration sensors. For other manufacturers set this to none.variable_name: Name of the variable that will be used to get the timeseries. This is only applicable for Gamesa turbines and blade gap sensors (deprecated). For other manufacturers set this to none.operation: Which time of amplitude calculation will be used. Valid values are "peak", "peak-to-peak", "rms", "mean", "median", "std".
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_timeseries_amplitude.py
def __init__(
self,
object_name: str,
feature: str,
) -> None:
"""
FeatureCalculator class for features that are based on the amplitude of the vibration time series of a wind turbine.
The calculation is fairly simple:
1. Get the vibration timeseries for the specified sensor, data type, acquisition rate, and variable name.
2. Apply the desired operation (peak, peak-to-peak, rms, mean, median, std) to the timeseries data. This will reduce the timeseries to a single value for each timestamp.
For this to work the feature must have attribute `feature_options_json` with the following keys:
- `sensor_name`: Name of the sensor that will be used to get the timeseries. Names must be the ones available for `perfdb.vibration.timeseries.get` method.
- `data_type`: Type of the data that will be used to get the timeseries. All values allowed in `perfdb.vibration.timeseries.get` method are valid. For example, "Vibration", etc.
- `acquisition_rate`: Acquisition rate of the sensor. Only applicable for Gamesa turbines and vibration sensors. For other manufacturers set this to none.
- `variable_name`: Name of the variable that will be used to get the timeseries. This is only applicable for Gamesa turbines and blade gap sensors (deprecated). For other manufacturers set this to none.
- `operation`: Which time of amplitude calculation will be used. Valid values are "peak", "peak-to-peak", "rms", "mean", "median", "std".
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 untilcalculateis 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
_computeto 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
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
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,
)