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:
-
Regression Parameter Retrieval:
Obtains the linear regression parameters (constand irradiance coefficient) from the object'sunclipped_pwr_regressionattribute. -
Irradiance Data Collection:
Retrieves the time series of plane-of-array irradiance (IrradiancePOACommOk_5min.AVG) from the associated simple weather station. -
Data Adjustment and Filling:
Adjusts the calculation period, fills missing values in the irradiance series (forward and backward fill), and ensures index alignment. -
Unclipped Power Calculation:
Applies the linear regression equation for each timestamp:
unclipped_power = constant + irradiance_coefficient * IrradiancePOACommOk_5min.AVG -
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_wsfield). -
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,
)