Alarm Active¶
Overview¶
The FeatureCalcAlarmActive class is a subclass of FeatureCalculator that calculates the active time of a specific alarm for a specific object. This is useful when we want to calculate the time that a specific alarm was active for a specific object.
Calculation Logic¶
The calculation logic is described in the constructor of the class, shown below in the Class Definition section.
Database Requirements¶
- Feature attribute
server_calc_typemust be set toalarm_active_time. - Feature attribute
feature_options_jsonwith the following keys:reference_alarm: Themanufacturer_id(see viewv_alarms_def) of the alarm that is used as reference.
Class Definition¶
FeatureCalcAlarmActive(object_name, feature)
¶
FeatureCalculator class for features that represent the number of seconds an alarm is active in a 10 min period.
The method will get the records from alarms history table and calculate the number of seconds that the wanted alarm was active in a 10 min period.
For this to work the feature must have attribute feature_options_json with the following keys:
reference_alarm: Themanufacturer_id(see viewv_alarms_def) of the alarm that is used as reference.
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_alarm_active.py
def __init__(
self,
object_name: str,
feature: str,
) -> None:
"""
FeatureCalculator class for features that represent the number of seconds an alarm is active in a 10 min period.
The method will get the records from alarms history table and calculate the number of seconds that the wanted alarm was active in a 10 min period.
For this to work the feature must have attribute `feature_options_json` with the following keys:
- `reference_alarm`: The `manufacturer_id` (see view `v_alarms_def`) of the alarm that is used as reference.
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)
# requirements for the feature calculator
self._add_requirement(RequiredFeatureAttributes(self.object, self.feature, ["feature_options_json"]))
self._get_required_data()
# validating feature options
self._validate_feature_options()
# defining required alarms
self._add_requirement(
RequiredAlarms(
{
self.object: [
self._get_requirement_data("RequiredFeatureAttributes")[self.feature]["feature_options_json"]["reference_alarm"],
],
},
),
)
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)
¶
Method that will calculate the Alarm Active Time feature.
Parameters:
-
(period¶DateTimeRange) –Period for which the feature will be calculated.
-
(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.
-
(cached_data¶DataFrame | None, default:None) –DataFrame with features already queried/calculated. This is useful to avoid needing to query all the data again from performance_db, making chained calculations a lot more efficient. By default None
-
(**kwargs¶dict, default:{}) –Additional arguments that will be passed to the "_save" method.
Returns:
-
Series–Pandas Series with the calculated feature.
Source code in echo_energycalc/feature_calc_alarm_active.py
def calculate(
self,
period: DateTimeRange,
save_into: Literal["all", "performance_db"] | None = None,
cached_data: DataFrame | None = None,
**kwargs,
) -> Series:
"""
Method that will calculate the Alarm Active Time feature.
Parameters
----------
period : DateTimeRange
Period for which the feature will be calculated.
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.
cached_data : DataFrame | None, optional
DataFrame with features already queried/calculated. This is useful to avoid needing to query all the data again from performance_db, making chained calculations a lot more efficient.
By default None
**kwargs : dict, optional
Additional arguments that will be passed to the "_save" method.
Returns
-------
Series
Pandas Series with the calculated feature.
"""
# adjusting period to include 10 min before the start
# this is done to make sure that alarms that end in the period are included as timestamps represent the end of the period
alarms_period = DateTimeRange(period.start - timedelta(minutes=10), period.end)
# getting required alarms
self._get_required_data(period=alarms_period, cached_data=cached_data, only_missing=True)
# getting alarms history from requirements
alarms_df = self._get_requirement_data("RequiredAlarms").copy()
# creating series for the result
result = self._create_empty_result(period=period, result_type="Series")
# dropping unfinished alarms
alarms_df = alarms_df[alarms_df["end"].notna()].copy()
# dropping where end is before start
alarms_df = alarms_df[alarms_df["end"] >= alarms_df["start"]].copy()
if not alarms_df.empty:
# creating a column to represent the period of the alarm
alarms_df["period"] = alarms_df.apply(lambda x: DateTimeRange(x["start"], x["end"]), axis=1)
# splitting the period in 10 min periods
alarms_df["period"] = alarms_df["period"].apply(
lambda x: x.split_multiple(timedelta(minutes=10), start_end_equal=True, normalize=True),
)
# exploding the DataFrame so that each period in period column is a row
alarms_df = alarms_df.explode("period")
# dropping rows where period is None
alarms_df = alarms_df[alarms_df["period"].notna()].copy()
# adjusting start and end columns based on period
alarms_df["start"] = alarms_df.apply(lambda x: x["period"].start, axis=1)
alarms_df["end"] = alarms_df.apply(lambda x: x["period"].end, axis=1)
# calculating duration based on start and end columns
alarms_df["duration"] = alarms_df["end"] - alarms_df["start"]
# calculating reference timestamp based in ceil of end in 10 min periods
alarms_df["reference_timestamp"] = alarms_df["end"].dt.ceil("10min")
# grouping by reference timestamp and summing duration
alarms_duration = alarms_df.groupby("reference_timestamp")["duration"].sum()
# converting result to seconds
alarms_duration = alarms_duration.dt.total_seconds()
# updating result with calculated values from alarms_duration
result = result.combine_first(alarms_duration)
# filling NaN values with 0
result = result.fillna(0.0)
# clipping values to 600 seconds
result = result.clip(lower=0.0, upper=600.0)
# adding calculated feature to class result attribute
self._result = result.copy()
# saving results
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
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,
)