Skip to content

Solar Theoretical Power

Overview

FeatureCalcPowerTheoreticalSolar calculates the theoretical active power of a solar inverter at 5-minute resolution using a pre-trained Random Forest model (SolarPowerRFPredictiveModel). The model represents normal inverter operation and is trained per SPE using irradiance, temperatures, humidity, and inverter reactive power.

The model is trained using the script at manual_routines\postgres_fit_solar_power_predict on the Performance Server and stored in performance_db as a serialized file.


Calculation Logic

Initialization

At instantiation:

  1. Reads feature_options_json to find the calc model type and model name.
  2. Queries performance_db for a matching SolarPowerRFPredictiveModel.
  3. Loads and deserializes the model.
  4. Reads model_arguments.reference_features, model_arguments.simple_ws_features, and model_arguments.complete_ws_features from the model to determine all required input features.
  5. If bazefield_features = true, appends _b# suffix to all feature names so they are fetched from Bazefield instead of performance_db.

Per-Period Computation

Inside _compute():

  1. Fetch features: Retrieves all required features from both weather stations and the inverter, rounded to 5-minute timestamps within ±2 minutes tolerance.

  2. Data pre-processing (before model inference):

    • Set IrradiancePOACommOk_5min.AVG to null when it equals 0 and all other columns are also null (avoids spurious night predictions).
    • Forward-fill ModuleTempCommOk_5min.AVG, AmbTemp_5min.AVG, Humidity_5min.AVG, and IrradiancePOACommOk_5min.AVG to reduce gaps.
    • Fill missing ReactivePower_5min.AVG with random values in [-1, 1] (the model is not sensitive to small reactive power values; this prevents dropping valid rows).
    • Cast all inputs to float32 for TensorFlow compatibility.
  3. Predict: Calls self._model.predict(df) on the processed data.

  4. Night masking: Uses pvlib to compute solar elevation from the inverter's latitude and longitude. All timestamps where the sun is below the horizon are set to 0.0 kW.

  5. Clip to nominal power: Values above the inverter's nominal_power are clipped.


Database Requirements

Feature Attribute

Attribute Value
server_calc_type theoretical_active_power_solar
feature_options_json JSON object — see below

feature_options_json Schema

Key Type Required Description
calc_model_type string Yes Type of the calc model (e.g., "solar_power_curve"). Used for exact matching.
model_name string Yes Substring of the model name in performance_db (e.g., "solar_power_curve!ActivePowerSolar").
bazefield_features boolean Yes If true, all input features are fetched from Bazefield (append _b#). Required for most solar sites.

Example:

JSON
{
    "calc_model_type": "solar_power_curve",
    "model_name": "solar_power_curve!ActivePowerSolar",
    "bazefield_features": true
}

Object Attributes

Attribute Required Description
reference_weather_stations Yes Dict with "simple_ws" and "complete_ws" keys naming the associated weather station objects.
latitude Yes Inverter geographic latitude (decimal degrees). Used for night masking.
longitude Yes Inverter geographic longitude (decimal degrees). Used for night masking.
nominal_power Yes Inverter nominal AC power (kW). Used to clip output.

Calculation Model

Requirement Description
Model type Must match calc_model_type exactly
Model name Must contain model_name as a substring
Model class Must be SolarPowerRFPredictiveModel from echo-calcmodels

Features

All features are fetched from Bazefield when bazefield_features = true (suffix _b#). The exact feature names are defined in the trained model's model_arguments.

Feature Object Description
ReactivePower_5min.AVG Inverter Reactive power (kVAR)
IrradiancePOACommOk_5min.AVG Simple weather station Plane-of-array irradiance (W/m²)
ModuleTempCommOk_5min.AVG Simple weather station Module temperature (°C)
AmbTemp_5min.AVG Complete weather station Ambient temperature (°C)
Humidity_5min.AVG Complete weather station Relative humidity (%)

Class Definition

FeatureCalcPowerTheoreticalSolar(object_name, feature)

Class used to calculate the theoretical active power for solar inverters.

For this class to work, the feature must have the attribute feature_options_json with the following keys:

  • calc_model_type: Type of the calculation model that will be used to calculate the feature.
  • model_name: Name of the feature that the model was trained to predict.
  • bazefield_features: bool indicating if the required features needs to be acquired from bazefield.

Keep in mind that calc_model_type and model_name will be used to filter the calculation models in the database looking for just ONE that matches both.

The class will handle getting all the necessary features for the model to work based on what was defined when the model was trained.

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_solar.py
Python
def __init__(
    self,
    object_name: str,
    feature: str,
) -> None:
    """
    Class used to calculate features that depend on a PredictiveModel.

    For this class to work, the feature must have the attribute `feature_options_json` with the following keys:

    - `calc_model_type`: Type of the calculation model that will be used to calculate the feature.
    - `model_name`: Name of the feature that the model was trained to predict.
    - `bazefield_features`: bool indicating if the required features needs to be acquired from bazefield.

    Keep in mind that `calc_model_type` and `model_name` will be used to filter the calculation models in the database looking for just ONE that matches both.

    The class will handle getting all the necessary features for the model to work based on what was defined when the model was trained.

    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)

    self._add_requirement(RequiredFeatureAttributes(self.object, self.feature, ["feature_options_json"]))

    self._fetch_requirements()

    self._feature_attributes = self._requirement_data("RequiredFeatureAttributes")[self.feature]

    self._validate_feature_options()

    self._add_requirement(
        RequiredCalcModels(
            calc_models={
                self.object: [
                    {
                        "model_name": f".*{self._feature_attributes['feature_options_json']['model_name']}.*",
                        "model_type": f"^{self._feature_attributes['feature_options_json']['calc_model_type']}$",
                    },
                ],
            },
        ),
    )
    self._add_requirement(
        RequiredObjectAttributes(
            {
                self.object: [
                    "reference_weather_stations",
                    "latitude",
                    "longitude",
                    "nominal_power",
                ],
            },
        ),
    )
    self._fetch_requirements()

    # getting the model name
    self._model_name = next(iter(self._requirement_data("RequiredCalcModels")[self.object].keys()))

    # loading calculation model from file
    try:
        self._model: SolarPowerRFPredictiveModel = self._requirement_data("RequiredCalcModels")[self.object][self._model_name]["model"]
        if not isinstance(self._model, SolarPowerRFPredictiveModel):
            raise TypeError(f"'{self.object}' is not an instance of a subclass of SolarPowerRFPredictiveModel.")
        self._model._deserialize_model()  # noqa: SLF001

    except Exception as e:
        raise RuntimeError(f"'{self.object}' failed to load SolarPowerRFPredictiveModel.") from e

    # checking if model object is an instance of a subclass of SolarPowerRFPredictiveModel
    if not isinstance(self._model, SolarPowerRFPredictiveModel):
        raise TypeError(f"'{self.object}' is not an instance of a subclass of SolarPowerRFPredictiveModel.")

    # defining required features
    reference_features = [
        feat
        for feat in self._model.model_arguments.reference_features
        if feat not in getattr(self._model.model_arguments, "ignore_baze_object_features", [])
    ]
    simple_ws = self._requirement_data("RequiredObjectAttributes")[self.object]["reference_weather_stations"]["simple_ws"]
    complete_ws = self._requirement_data("RequiredObjectAttributes")[self.object]["reference_weather_stations"]["complete_ws"]

    features = {
        self.object: reference_features,
        simple_ws: self._model.model_arguments.simple_ws_features,
        complete_ws: self._model.model_arguments.complete_ws_features,
    }

    # Adiciona sufixo _b# se bazefield_features for True
    if self._feature_attributes["feature_options_json"].get("bazefield_features", False):
        features = {obj: [f"{feat}_b#" for feat in feats] for obj, feats in features.items()}
    self._add_requirement(RequiredFeatures(features=features))

    # checking if model has more than one target feature
    if len(self._model.model_arguments.target_features) > 1:
        raise NotImplementedError("SolarPowerRFPredictiveModel with more than one target feature is not supported yet.")

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