Skip to content

Solar Active Power Unclipped

Overview

The FeatureCalcSolarUnclippedPower class calculates the theoretical maximum AC power that a solar inverter could deliver at each timestamp, based on a linear regression between irradiance and power. The regression parameters are stored in the object's unclipped_pwr_regression attribute. The calculation uses irradiance data from the weather station associated with the inverter.

Calculation Logic

The calculation follows these steps:

  1. Regression Parameter Retrieval:
    Obtains the linear regression parameters (const and irradiance coefficient) from the object's unclipped_pwr_regression attribute.

  2. Irradiance Data Collection:
    Retrieves the time series of plane-of-array irradiance (IrradiancePOACommOk_5min.AVG) from the associated simple weather station.

  3. Data Adjustment and Filling:
    Adjusts the calculation period, fills missing values in the irradiance series (forward and backward fill), and ensures index alignment.

  4. Unclipped Power Calculation:
    Applies the linear regression equation for each timestamp:
    unclipped_power = constant + irradiance_coefficient * IrradiancePOACommOk_5min.AVG

  5. Result Formatting:
    Ensures the result is aligned with the requested period and returns the calculated series.

Required Features

The following are required for the calculation:

  • Object attributes:
  • unclipped_pwr_regression: Linear regression parameters (constant and irradiance coefficient).
  • reference_weather_stations: Dictionary with the associated weather station (simple_ws field).

  • Weather station features:

  • IrradiancePOACommOk_5min.AVG: Plane-of-array irradiance (W/m²).

Class Definition

FeatureCalcSolarUnclippedPower(object_name, feature)

Base class for solar energy unclipped active power. Basing the result on the linear regression parameters saved on unclipped_pwr_regression object attribute.

This class uses linear regression parameters stored in the object's 'unclipped_pwr_regression' attribute and requires reference weather station data (irradiation). It sets up the necessary requirements for the calculation.

Parameters:

  • object_name

    (str) –

    Name of the object for which the feature is calculated.

  • feature

    (str) –

    Name of the feature to be calculated.

Source code in echo_energycalc/feature_calc_solar_unclipped_pwr.py
def __init__(
    self,
    object_name: str,
    feature: str,
) -> None:
    """
    Initializes the FeatureCalcSolarUnclippedPower class for calculating the solar unclipped active power feature.

    This class uses linear regression parameters stored in the object's 'unclipped_pwr_regression' attribute
    and requires reference weather station data (irradiation). It sets up the necessary requirements for the calculation.

    Parameters
    ----------
    object_name : str
        Name of the object for which the feature is calculated.
    feature : str
        Name of the feature to be calculated.
    """
    # initialize parent class
    super().__init__(object_name, feature)

    # Defining which object attributes are required for the calculation.
    self._add_requirement(
        RequiredObjectAttributes(
            {
                self.object: [
                    "unclipped_pwr_regression",
                    "reference_weather_stations",
                ],
            },
        ),
    )
    self._get_required_data()

    # Getting the complete weather station name for the specif object.
    simple_ws = self._get_requirement_data("RequiredObjectAttributes")[self.object]["reference_weather_stations"]["simple_ws"]

    # Defining the features that will be required for the calculation.
    features = {
        simple_ws: ["IrradiancePOACommOk_5min.AVG_b#"],
    }
    self._add_requirement(RequiredFeatures(features=features))

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:

  • Series | DataFrame | None:

    Result of the calculation if the method "calculate" was called. None otherwise.

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

Calculates the solar unclipped active power feature for the specified period.

The calculation uses linear regression parameters stored in the object's 'unclipped_pwr_regression' attribute. It retrieves the required irradiance feature, fills missing values, and computes the unclipped power as: unclipped_power = constant_parameter + irradiance_parameter * IrradiancePOACommOk_5min.AVG

Parameters:

  • period

    (DateTimeRange) –

    Period for which the feature will be calculated.

  • save_into

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

    Where to save the calculated feature. Options: - "all": Save in all destinations. - "performance_db": Save only in performance_db. - None: Do not save. Default is None.

  • cached_data

    (DataFrame | None, default: None ) –

    DataFrame with already queried/calculated features to speed up calculation. Default is None.

  • **kwargs

    (dict, default: {} ) –

    Additional arguments passed to the "save" method.

Returns:

  • Series

    Pandas Series with the calculated unclipped active power.

Source code in echo_energycalc/feature_calc_solar_unclipped_pwr.py
def calculate(
    self,
    period: DateTimeRange,
    save_into: Literal["all", "performance_db"] | None = None,
    cached_data: DataFrame | None = None,
    **kwargs,
) -> Series:
    """
    Calculates the solar unclipped active power feature for the specified period.

    The calculation uses linear regression parameters stored in the object's 'unclipped_pwr_regression' attribute.
    It retrieves the required irradiance feature, fills missing values, and computes the unclipped power as:
        unclipped_power = constant_parameter + irradiance_parameter * IrradiancePOACommOk_5min.AVG

    Parameters
    ----------
    period : DateTimeRange
        Period for which the feature will be calculated.
    save_into : Literal["all", "performance_db"] | None, optional
        Where to save the calculated feature. Options:
        - "all": Save in all destinations.
        - "performance_db": Save only in performance_db.
        - None: Do not save.
        Default is None.
    cached_data : DataFrame | None, optional
        DataFrame with already queried/calculated features to speed up calculation.
        Default is None.
    **kwargs : dict, optional
        Additional arguments passed to the "save" method.

    Returns
    -------
    Series
        Pandas Series with the calculated unclipped active power.
    """
    t0 = perf_counter()

    constant_parameter = self._get_requirement_data("RequiredObjectAttributes")[self.object]["unclipped_pwr_regression"][
        "attribute_value"
    ]["params"]["const"]
    irradiance_parameter = self._get_requirement_data("RequiredObjectAttributes")[self.object]["unclipped_pwr_regression"][
        "attribute_value"
    ]["params"]["IrradiancePOACommOk_5min.AVG"]

    # adjusting period to account for lagged timestamps
    adjusted_period = period.copy()

    # creating a series to store the result
    result = self._create_empty_result(period=adjusted_period, freq="5min", result_type="Series")

    # getting feature values
    self._get_required_data(
        period=adjusted_period,
        reindex=None,
        round_timestamps={"freq": timedelta(minutes=5), "tolerance": timedelta(minutes=2)},
        cached_data=cached_data,
    )

    # getting DataFrame with feature values
    df = self._get_requirement_data("RequiredFeatures")
    t1 = perf_counter()

    # Dataframe structure adjustment
    df.columns = df.columns.get_level_values("feature")
    # Remove the suffix _b# from the columns
    df.columns = df.columns.str.replace("_b#$", "", regex=True)

    # Filling missing values, first forward and then backward
    df[df.columns] = df[df.columns].ffill().bfill()

    t2 = perf_counter()

    # Defining crucial columns for calculation
    # Average Grid Voltage to define the pu voltage and apply the corresponding maximum P-Q curve, following inverter datasheet.
    df["ActivePowerUnclipped_5min.AVG"] = constant_parameter + irradiance_parameter * df["IrradiancePOACommOk_5min.AVG"]

    df["ActivePowerUnclipped_5min.AVG"] = df["ActivePowerUnclipped_5min.AVG"].clip(lower=0)

    t3 = perf_counter()

    # Adjusting the Series index with the results.
    # This is done to prevent missing indexes on the requested period of calculation. That is, if the calculated df has less points than the expected for the period.
    wanted_idx = result.index.intersection(df.index)
    result.loc[wanted_idx] = df.loc[wanted_idx, "ActivePowerUnclipped_5min.AVG"].values
    # Trimming result to the original period, just to be sure
    result = result[(result.index >= period.start) & (result.index <= period.end)].copy()
    # Adding calculated feature to class result attribute
    self._result = result.copy()

    # Saving results
    self.save(save_into=save_into, **kwargs)

    logger.debug(
        f"{self.object} - {self.feature} - {period}: Requirements during calc {t1 - t0:.2f}s - Data adjustments {t2 - t1:.2f}s - Calculation core {t3 - t2:.2f}s - Final adjustments {perf_counter() - t3:.2f}s",
    )

    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. Cannot save the feature calculation results. Please make sure to do something like 'self._result = df[self.feature].copy()' in the method 'calculate' before calling 'self.save()'.",
        )

    if save_into is None:
        return

    if isinstance(save_into, str):
        if save_into not in ["performance_db", "all"]:
            raise ValueError(f"save_into must be 'performance_db' or 'all', not {save_into}.")
        upload_to_bazefield = save_into == "all"
    elif save_into is None:
        upload_to_bazefield = False
    else:
        raise TypeError(f"save_into must be a string or None, not {type(save_into)}.")

    # converting result series to DataFrame if needed
    if isinstance(self.result, Series):
        result_df = self.result.to_frame()
    elif isinstance(self.result, DataFrame):
        result_df = self.result.droplevel(0, axis=1)
    else:
        raise TypeError(f"result must be a pandas Series or DataFrame, not {type(self.result)}.")

    # adjusting DataFrame to be inserted in the database
    # making the columns a Multindex with levels object_name and feature_name
    result_df.columns = MultiIndex.from_product([[self.object], result_df.columns], names=["object_name", "feature_name"])

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