Skip to content

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_type must be set to alarm_active_time.
  • Feature 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.

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: 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.

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