Skip to content

Theoretical Power

Overview

FeatureCalcPowerTheoretical calculates the theoretical active power of a wind turbine using Echoenergia's internal neural network (NN) power curve methodology. This is the primary, most accurate method for theoretical power estimation — it accounts for turbulence intensity, ambient temperature, and other turbine-specific inputs beyond just wind speed.

The NN model is trained per turbine using the script manual_routines\postgres_fit_wtg_power_curves on the Performance Server. The trained model is stored in performance_db as a serialized object under the name fitted_power_curve.


Calculation Logic — 5-Step Fallback Strategy

The calculator tries to fill all NaN timestamps (up to a maximum of 5 remaining NaNs) using each step in sequence:

Text Only
Step 1: NN power curve model + this turbine's own measured data
   ↓ (if NaNs remain > 5)
Step 2: Neighbor turbine's active power (corrected by regression)
   ↓ (if NaNs remain > 5)
Step 3: NN power curve model + neighbor turbine's wind speed (corrected by regression)
   ↓ (if NaNs remain > 5)
Step 4: NN power curve model + reference met mast wind speed (corrected by regression, shifted 1 timestamp)
   ↓ (if NaNs remain > 5)
Step 5: Average active power of all turbines in the same wind farm

Each step operates only on timestamps that are still null after the previous step. Neighbor and farm-average data is filtered by IEC-OperationState_10min.REP and CurtailmentState_10min.REP to avoid using data from stopped/curtailed turbines.

Wind direction capping

When the NN model includes a relative wind direction input, values exceeding MAX_RELATIVE_WIND_DIRECTION_DEG (30°) are clamped to 0 before model inference to avoid extrapolation.

Final output

Results are clipped to [0, nominal_power] kW.


Database Requirements

Calculation model

  • A calc model named fitted_power_curve must be associated with the object and stored in performance_db.

Object attributes

Attribute Required Description
nominal_power Yes Turbine nominal power in kW. Used for output clipping.
neighbor_wind_turbines Yes Ordered list of neighbor turbine names (closest first).
reference_met_masts Yes Ordered list of reference met mast names (closest first).
spe_name Yes Name of the SPE this turbine belongs to (used for Step 5).
neighbor_active_power_regressions No Dict of {neighbor: {slope, offset}} for Step 2 correction. Defaults to slope=1, offset=0.
neighbor_wind_speed_regressions No Dict of {neighbor: {slope, offset}} for Step 3 correction. Defaults to slope=1, offset=0.
met_mast_wind_speed_regressions No Dict of {mast: {slope, offset}} for Step 4 correction. Defaults to slope=1, offset=0.

Regression format:

JSON
{
    "WT02": {"slope": 1.02, "offset": -0.5},
    "WT03": {"slope": 0.98, "offset": 0.1}
}

Features (this turbine)

The base required features are determined dynamically from the trained model's model_arguments.reference_features. In addition:

Feature Purpose
WindSpeed_10min.AVG Primary wind speed input
WindSpeedEstimated_10min.AVG Estimated wind speed (if turbine anemometer is unavailable)
ActivePower_10min.AVG Used in quality filtering
CurtailmentState_10min.REP Filters out curtailed periods
IEC-OperationState_10min.REP Filters out non-producing periods

Features (neighbor turbines — Steps 2 & 3)

Same reference_features as this turbine, plus ActivePower_10min.AVG, CurtailmentState_10min.REP, and IEC-OperationState_10min.REP.

Features (reference met masts — Step 4)

The met mast feature names are mapped from turbine feature names via TURBINE_MAST_FEATURE_CONVERSION:

Turbine feature Met mast feature
WindSpeed_10min.AVG WindSpeed1_10min.AVG
WindSpeed_10min.STD WindSpeed1_10min.STD
AmbTemp_10min.AVG AmbTemp1_10min.AVG

Met mast readings are shifted by 1 period (using the previous timestamp's reading).


Feature attribute

Set server_calc_type = "theoretical_active_power".


Class Definition

FeatureCalcPowerTheoretical(object_name, feature)

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

It uses Echoenergia's internal calculation methodology for better accuracy.

This 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 the NN power curve model of this turbine with data from this turbine. The NN model is stored in the database with the name fitted_power_curve.
  • Step 2: Use active power from the neighbor turbines corrected by neighbor_active_power_regressions if available. Neighbor data will only be used if it meets IEC-OperationState_10min.REP and CurtailmentState_10min.REP requirements.
  • Step 3: Use the NN power curve model of this turbine with data from neighbor turbines. Neighbor wind speed will be corrected by neighbor_wind_speed_regressions object attribute if available.
  • Step 4: Use the NN power curve model of this turbine with data from reference met masts. Wind speed will be corrected by met_mast_wind_speed_regressions object attribute if available.
  • Step 5: 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.py
Python
def __init__(
    self,
    object_name: str,
    feature: str,
) -> None:
    """Class used to calculate theoretical active power for a wind turbine.

    It uses Echoenergia's internal calculation methodology for better accuracy.

    This 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 the NN power curve model of this turbine with data from this turbine. The NN model is stored in the database with the name `fitted_power_curve`.
    - **Step 2**: Use active power from the neighbor turbines corrected by `neighbor_active_power_regressions` if available. Neighbor data will only be used if it meets `IEC-OperationState_10min.REP` and `CurtailmentState_10min.REP` requirements.
    - **Step 3**: Use the NN power curve model of this turbine with data from neighbor turbines. Neighbor wind speed will be corrected by `neighbor_wind_speed_regressions` object attribute if available.
    - **Step 4**: Use the NN power curve model of this turbine with data from reference met masts. Wind speed will be corrected by `met_mast_wind_speed_regressions` object attribute if available.
    - **Step 5**: 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(RequiredCalcModels(calc_models={self.object: [{"model_name": "^fitted_power_curve$", "model_type": None}]}))
    self._add_requirement(
        RequiredObjectAttributes(
            {
                self.object: [
                    "neighbor_wind_turbines",
                    "nominal_power",
                    "reference_met_masts",
                    "spe_name",
                ],
            },
        ),
    )
    self._add_requirement(
        RequiredObjectAttributes(
            {
                self.object: [
                    "neighbor_active_power_regressions",
                    "neighbor_wind_speed_regressions",
                    "met_mast_wind_speed_regressions",
                ],
            },
            optional=True,
        ),
    )
    self._fetch_requirements()

    # loading calculation model from file
    try:
        self._pc_model: PowerCurveNNPredictiveModel = self._requirement_data("RequiredCalcModels")[self.object]["fitted_power_curve"][
            "model"
        ]
        self._pc_model._deserialize_model()  # noqa: SLF001
    except Exception as e:
        raise RuntimeError(f"'{self.object}' failed to load power curve model.") from e

    # defining required features
    needed_features = [
        "WindSpeed_10min.AVG",
        "WindSpeedEstimated_10min.AVG",
        "ActivePower_10min.AVG",
        "CurtailmentState_10min.REP",
        "IEC-OperationState_10min.REP",
    ]
    needed_features = list(set(needed_features).union(set(self._pc_model.model_arguments.reference_features)))
    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 = 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,
    )