Skip to content

Solar Active Power Unclipped

Overview

FeatureCalcSolarUnclippedPower estimates the theoretical AC power that a solar inverter would produce without AC-side clipping limitations. It uses a simple linear regression between plane-of-array irradiance and power, whose coefficient is stored as an object attribute. The result is a 5-minute time series.

This feature is used in combination with ClippingState to quantify the energy lost due to inverter clipping.


Calculation Logic

Formula

Text Only
unclipped_power = irradiance_coefficient × IrradiancePOACommOk_5min.AVG

Where irradiance_coefficient is a fitted scalar regression parameter (units: kW / (W/m²)) stored in the object attribute unclipped_pwr_regression.

Steps

  1. Read regression parameter: Retrieves irradiance_coefficient from unclipped_pwr_regression["attribute_value"]["params"]["IrradiancePOACommOk_5min.AVG"].

  2. Fetch irradiance: Retrieves IrradiancePOACommOk_5min.AVG from the associated simple weather station (reference_weather_stations["simple_ws"]), rounded to 5-minute timestamps within ±2 minutes tolerance.

  3. Fill missing irradiance: Applies forward-fill then backward-fill to minimize gaps in the irradiance series.

  4. Compute power: Multiplies irradiance by the coefficient and clips to ≥ 0.0 (negative irradiance readings produce zero power).

  5. Align to period: Trims the result to the originally requested period.


Database Requirements

Feature Attribute

Attribute Value
server_calc_type solar_unclipped_power

Object Attributes

Attribute Required Description
unclipped_pwr_regression Yes Nested dict containing the fitted regression coefficient. Expected structure: {"attribute_value": {"params": {"IrradiancePOACommOk_5min.AVG": <float>}}}
reference_weather_stations Yes Dict with a "simple_ws" key naming the weather station object that provides irradiance.

Example unclipped_pwr_regression value:

JSON
{
    "attribute_value": {
        "params": {
            "IrradiancePOACommOk_5min.AVG": 0.285
        }
    }
}

Features (simple weather station — from Bazefield)

Feature Description
IrradiancePOACommOk_5min.AVG Plane-of-array irradiance (W/m²). Fetched with _b# suffix from Bazefield.

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
Python
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._fetch_requirements()

    # Getting the complete weather station name for the specif object.
    simple_ws = self._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:

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