Skip to content

Wind Farm Wind Speed

Overview

FeatureCalcWFReferenceWS calculates the most representative, gap-free reference wind speed for a wind farm by cascading through a series of data sources. The result is used as the wind speed KPI for the farm.


Calculation Logic — Fallback Strategy

The calculator fills null timestamps sequentially, stopping at each step once remaining nulls fall below the tolerance of 5.

Text Only
Step 1: Primary met mast (reference_met_masts[0]) — wind speed corrected by regression
   ↓ (if nulls > 5)
Step 2: Next met mast (reference_met_masts[1], then [2], …)
   ↓ (if nulls > 5 after all met masts)
Step 3: Average wind speed from all wind turbines of this wind farm

Step 1 & 2 — Met Mast Data

For each met mast in reference_met_masts (in order):

  1. Identify the contiguous null range (first null timestamp → last null timestamp).
  2. Fetch WindSpeed1_10min.AVG from the met mast only for that sub-period.
  3. Look up regression coefficients for this met mast from the first met mast's met_mast_wind_speed_regressions attribute (optional — defaults to slope=1, offset=0):

    Text Only
    corrected_wind_speed = slope × WindSpeed1_10min.AVG + offset
    
  4. Fill nulls in the result with corrected_wind_speed where available.

  5. If remaining nulls ≤ 5, stop iterating over met masts.

Step 3 — Wind Turbine Average

If nulls remain after exhausting all met masts:

  1. Fetch WindSpeed_10min.AVG from the SPE object (which aggregates readings from all turbines in the farm).
  2. Fill remaining nulls with this average wind speed.

Final Check

If any nulls remain after all steps, a warning is logged with the percentage of unfilled timestamps.


Database Requirements

Feature Attribute

Attribute Value
server_calc_type wind_farm_reference_wind_speed

Object Attributes (SPE object)

Attribute Required Description
reference_met_masts Yes Ordered list of met mast object names (closest first). All are tried in sequence to fill gaps.
met_mast_wind_speed_regressions No Stored on the first met mast object. Dict of {met_mast_name: {"slope": ..., "offset": ...}} for wind speed correction. Defaults to slope=1, offset=0 if absent.

Regression format:

JSON
{
    "MET1": {"slope": 1.02, "offset": -0.1},
    "MET2": {"slope": 0.98, "offset": 0.05}
}

Note

Regressions are computed with the script at manual_routines\postgres_calc_regressions on the Performance Server.

Features (met masts)

Feature Object Description
WindSpeed1_10min.AVG Each met mast in reference_met_masts 10-min average wind speed (m/s).

Features (SPE object — fallback only)

Feature Object Description
WindSpeed_10min.AVG SPE object Average wind speed aggregated across all turbines (m/s). Only fetched when met mast data cannot cover all gaps.

Class Definition

FeatureCalcWFReferenceWS(object_name, feature)

Class used to calculate reference wind speed for a wind farm.

This reference wind speed is calculated going through each of the met masts defined in reference_met_masts attribute of the wind farm and coping it's values corrected by the regression defined in met_mast_wind_speed_regressions attribute of the SPE if present.

The second, third, etc. met masts are only used if the first one is not available for all the time period.

At the end, if there are missing timestamps, will get the average of wind speed from all wind turbines of this wind farm.

The final goal is to have the most representative value for the wind speed at the wind farm avoiding any NaN values.

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_ws_reference.py
Python
def __init__(
    self,
    object_name: str,
    feature: str,
) -> None:
    """
    Class used to calculate reference wind speed for a wind farm.

    This reference wind speed is calculated going through each of the met masts defined in `reference_met_masts` attribute of the wind farm and coping it's values corrected by the regression defined in `met_mast_wind_speed_regressions` attribute of the SPE if present.

    The second, third, etc. met masts are only used if the first one is not available for all the time period.

    At the end, if there are missing timestamps, will get the average of wind speed from all wind turbines of this wind farm.

    The final goal is to have the most representative value for the wind speed at the wind farm avoiding any NaN values.

    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)

    # required reference met masts
    self._add_requirement(RequiredObjectAttributes({self.object: ["reference_met_masts"]}))
    self._fetch_requirements()

    # required met_mast_wind_speed_regressions (first met mast only)
    self._add_requirement(
        RequiredObjectAttributes(
            {
                self._requirement_data("RequiredObjectAttributes")[self.object]["reference_met_masts"][0]: [
                    "met_mast_wind_speed_regressions",
                ],
            },
            optional=True,
        ),
    )
    self._fetch_requirements()

    # amount of timestamps that is acceptable to have NaN values to avoid long calculations trying to fill all NaNs
    self._max_nan = 5

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,
    )