Skip to content

Vestas Theoretical Power

Overview

FeatureCalcPowerTheoreticalVestas calculates the contractual theoretical active power for a Vestas wind turbine following the methodology defined in Vestas AOM5000 contracts. When the underlying data is correct, the result should match exactly the values calculated by the Vestas SCADA system, and it is the basis for energy-based availability calculations in those contracts.

The calculator produces two output columns simultaneously:

  • The contractual theoretical power itself.
  • ActivePowerTheoreticalContractualStep_10min.REP: an indicator of which fallback step produced each value (1.xx = Step 1 neighbor, 2 = Step 2 turbine wind speed, 3 = Step 3 met mast, 4 = Step 4 farm average).

Calculation Logic — 4-Step Fallback

All null timestamps are filled before moving to the next step. _max_nan = 0, meaning the calculator attempts to fill every single timestamp.

Text Only
Step 1: Active power from neighbor turbines
   ↓ (if nulls remain)
Step 2: Contractual power curve applied to this turbine's own wind speed
   ↓ (if nulls remain)
Step 3: Contractual power curve applied to reference met mast wind speed (shifted 1 period)
   ↓ (if nulls remain)
Step 4: Average active power of all turbines in the same wind farm

Step 1 — Neighbor Turbine Active Power

Uses contractual_neighbor_wind_turbines if defined, otherwise falls back to neighbor_wind_turbines. Iterates through neighbors in order. For each:

  1. Fetches ActivePower_10min.AVG, CurtailmentState_10min.REP, and IEC-OperationState_10min.REP.
  2. Applies _adjust_features() — nulls out power when curtailment state is not in [0, 3] (state 3 = ambient temp derating is allowed) or when IEC-OperationState indicates stopped.
  3. Uses the filtered neighbor power directly as the theoretical value.

The step indicator is 1.0N where N is the 1-based neighbor index (e.g., 1.01 for the first neighbor, 1.02 for the second).

Step 2 — Contractual Power Curve + Turbine Wind Speed

Applies _adjust_features() to this turbine's data, then:

Text Only
theoretical_power = contractual_power_curve(WindSpeed_10min.AVG)

The power curve is an interpolating function built from contractual_power_curve at initialization using cf.convert_curve_df_to_func(..., extrapolate=True).

Step indicator: 2.0.

Step 3 — Contractual Power Curve + Met Mast Wind Speed

For each met mast in reference_met_masts (in order):

  1. Fetches WindSpeed1_10min.AVG from the met mast.
  2. Shifts by 1 period — uses the previous timestamp's met mast reading. This matches the Vestas contractual convention.
  3. Applies the contractual power curve to the shifted wind speed.

Step indicator: 3.0.

Step 4 — Wind Farm Average Active Power

Computes the average ActivePower_10min.AVG across all turbines in the same wind farm (identified via spe_name), filtering by curtailment and operation state.

Step indicator: 4.0.


Database Requirements

Feature Attribute

Attribute Value
server_calc_type theoretical_active_power_vestas

Object Attributes

Attribute Required Description
contractual_power_curve Yes Dict with wind_speed and active_power arrays defining the contractual power curve at site air density.
neighbor_wind_turbines Yes Ordered list of neighboring turbine object names (closest first).
reference_met_masts Yes Ordered list of reference met mast object names (closest first).
spe_name Yes Name of the SPE this turbine belongs to (used in Step 4 to identify farm siblings).
contractual_neighbor_wind_turbines No Alternative neighbor list per contract definition. Overrides neighbor_wind_turbines in Step 1 if present.

Contractual power curve format:

JSON
{
    "wind_speed": [0, 1, 2, 3, 4, 5, 6],
    "active_power": [0.0, 0.0, 0.0, 0.0, 64.0, 165.0, 310.0]
}

Features (this turbine)

Feature Used in Step Description
WindSpeed_10min.AVG 2 10-min average wind speed (m/s)
ActivePower_10min.AVG 1, 4 (quality filter) Measured active power (kW)
CurtailmentState_10min.REP 1, 2, 4 Curtailment state filter — only states 0 (none) and 3 (temp derating) are valid
IEC-OperationState_10min.REP 1, 2, 4 Operation state filter — must indicate producing

Features (neighbor turbines — Step 1)

Feature Description
ActivePower_10min.AVG Used as the theoretical value directly
CurtailmentState_10min.REP Must be 0 or 3; otherwise neighbor is excluded
IEC-OperationState_10min.REP Must indicate producing; otherwise neighbor is excluded

Features (reference met masts — Step 3)

Feature Description
WindSpeed1_10min.AVG Met mast wind speed (m/s), shifted 1 period before applying power curve

Class Definition

FeatureCalcPowerTheoreticalVestas(object_name, feature)

Class used to calculate contractual theoretical active power for a Vestas wind turbine.

This follows the calculation logic for Vestas turbines as described in the Vestas contract.

It will follow the following steps trying to fill all timestamps. If one step is not enough to fill all timestamps, the next step will be used until there are no more timestamps with NaN values.

  • Step 1: Use active power from the neighbor turbines.

    Neighbor will be selected based on contractual_neighbor_wind_turbines object attribute if available, otherwise neighbor_wind_turbines will be used.

    Neighbor data will only be used if it meets IEC-OperationState_10min.REP and CurtailmentState_10min.REP requirements.

  • Step 2: Use contractual power curve in wind speed measured by this turbine.

  • Step 3: Use contractual power curve in wind speed measured by reference met masts.
  • Step 4: Consider the average active power of all turbines of this turbine wind farm, as long as it meets IEC-OperationState_10min.REP and CurtailmentState_10min.REP requirements.

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_power_theoretical_vestas.py
Python
def __init__(
    self,
    object_name: str,
    feature: str,
) -> None:
    """Class used to calculate contractual theoretical active power for a Vestas wind turbine.

    This follows the calculation logic for Vestas turbines as described in the Vestas contract.

    It will follow the following steps trying to fill all timestamps. If one step is not enough to fill all timestamps, the next step will be used until there are no more timestamps with NaN values.

    - **Step 1**: Use active power from the neighbor turbines.

        Neighbor will be selected based on `contractual_neighbor_wind_turbines` object attribute if available, otherwise `neighbor_wind_turbines` will be used.

        Neighbor data will only be used if it meets `IEC-OperationState_10min.REP` and `CurtailmentState_10min.REP` requirements.

    - **Step 2**: Use contractual power curve in wind speed measured by this turbine.
    - **Step 3**: Use contractual power curve in wind speed measured by reference met masts.
    - **Step 4**: Consider the average active power of all turbines of this turbine wind farm, as long as it meets `IEC-OperationState_10min.REP` and `CurtailmentState_10min.REP` requirements.

    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)

    # base requirements for the feature calculator
    self._add_requirement(
        RequiredObjectAttributes(
            {
                self.object: [
                    "neighbor_wind_turbines",
                    "contractual_power_curve",
                    "reference_met_masts",
                    "spe_name",
                ],
            },
        ),
    )
    # optional requirements for the feature calculator
    self._add_requirement(
        RequiredObjectAttributes(
            {
                self.object: [
                    "contractual_neighbor_wind_turbines",
                ],
            },
            optional=True,
        ),
    )
    self._fetch_requirements()

    # converting power curve to a function
    try:
        pc_data = self._requirement_data("RequiredObjectAttributes")[self.object]["contractual_power_curve"]
        self._power_curve = cf.convert_curve_df_to_func(pl.DataFrame(pc_data), "wind_speed", "active_power", extrapolate=True)
    except Exception as e:
        raise ValueError(f"{self.object} - Could not convert contractual power curve to a function") from e

    # defining required features
    needed_features = ["WindSpeed_10min.AVG", "ActivePower_10min.AVG", "CurtailmentState_10min.REP", "IEC-OperationState_10min.REP"]
    self._add_requirement(RequiredFeatures({self.object: needed_features}))

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

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