Source code for electric_emission_cost.metrics

"""Functions to estimate flexibility metrics from power consumption trajectories."""

import warnings
import numpy as np


[docs]def roundtrip_efficiency(baseline_kW, flexible_kW): """Calculate the round-trip efficiency of a flexibly operating power trajectory relative to a baseline. Parameters ---------- baseline_kW : list or np.ndarray power consumption data of the baseline system in units of kW flexible_kW : list or np.ndarray power consumption data of the flexibly operating or cost-optimized system in units of kW. Raises ------ TypeError When `baseline_kW` and `flexible_kW` are not an acceptable type (e.g., list vs. np.ndarray). ValueError When `baseline_kW` and `flexible_kW` are not of the same length Warnings When `baseline_kW` and `flexible_kW` contain negative values, which may indicate an error in the data. ValueError When `baseline_kW` and `flexible_kW` contain missing values. Warnings When rte is calculated to be greater than 1. This may indicate an error in the assumptions behind the data. Returns ------- float The round-trip efficiency [0,1] of the flexible power trajectory relative to the baseline. """ # Check if inputs are lists or numpy arrays if not isinstance(baseline_kW, (list, np.ndarray)): raise TypeError("baseline_kW must be a list or numpy array.") if not isinstance(flexible_kW, (list, np.ndarray)): raise TypeError("flexible_kW must be a list or numpy array.") # Check if inputs are of the same length if len(baseline_kW) != len(flexible_kW): raise ValueError("baseline_kW and flexible_kW must have the same length.") # Convert inputs to numpy arrays for easier calculations baseline_kW = np.array(baseline_kW) flexible_kW = np.asarray(flexible_kW) # Check for negative or missing values if np.any(baseline_kW < 0) or np.any(flexible_kW < 0): warnings.warn( "Negative values detected in baseline_kW or flexible_kW. " "This may indicate an error in the data." ) if np.any(np.isnan(baseline_kW)) or np.any(np.isnan(flexible_kW)): raise ValueError( "Missing values detected in baseline_kW or flexible_kW. " "This may indicate an error in the data." ) # Calculate the round-trip efficiency baseline_energy = np.sum(baseline_kW) flexible_energy = np.sum(flexible_kW) if flexible_energy == 0: raise ValueError("The sum of flexible_kW is zero, cannot compute rte.") rte_value = baseline_energy / flexible_energy if rte_value > 1: warnings.warn( "RTE calculated to be greater than 1. " "This may indicate an error in the assumptions behind the data." ) return rte_value
[docs]def power_capacity( baseline_kW, flexible_kW, timestep=0.25, pc_type="average", relative=True ): """Calculate the power capacity of a virtual battery system. This approach implicitly assumes the system has completed a round-trip. Parameters ---------- baseline_kW : array-like The baseline power consumption of the facility in kW. flexible_kW : array-like The flexible power consumption of the facility in kW. timestep : float The time step of the data in hours. Default is 0.25 hours (15 minutes). pc_type : str The type of power capacity to calculate. Options are 'average', 'charging', 'discharging', 'maximum' relative : bool If True, return the fractional power capacity. If False, return the absolute power capacity. Raises ------ ValueError If `pc_type` is not one of the expected values ('average', 'charging', 'discharging', 'maximum'). Returns ------- float The power capacity of the virtual battery system in either relative or absolute terms. """ # calculate the effective battery power (diff) diff_kW = flexible_kW - baseline_kW # charging is positive, discharging is negative charging = np.where(diff_kW > 0, diff_kW, 0) discharging = np.where(diff_kW < 0, -diff_kW, 0) # calculate the power capacity if pc_type == "average": power_capacity = (np.sum(charging) + np.sum(discharging)) / (len(diff_kW)) elif pc_type == "charging": power_capacity = np.sum(charging) / (len(charging)) elif pc_type == "discharging": power_capacity = np.sum(discharging) / (len(discharging)) elif pc_type == "maximum": power_capacity = np.max(np.abs(diff_kW)) else: raise ValueError( "Invalid power capacity type. Must be 'average', " "'charging', 'discharging', or 'maximum'." ) if relative: # normalize by the max baseline power return power_capacity / np.max(baseline_kW) else: return power_capacity
[docs]def energy_capacity( baseline_kW, flexible_kW, timestep=0.25, ec_type="discharging", relative=True ): """Calculate the energy capacity of a virtual battery system. This approach implicitly assumes the system has completed a round-trip. Parameters ---------- baseline_kW : array-like The baseline power consumption of the facility in kW. flexible_kW : array-like The flexible power consumption of the facility in kW. timestep : float The time step of the data in hours. Default is 0.25 hours (15 minutes). ec_type : str The type of energy capacity to calculate. Options are 'average', 'charging', 'discharging' relative : bool If True, return the fractional energy capacity. If False, return the absolute energy capacity. Raises ------ ValueError If `ec_type` is not one of the expected values ('average', 'charging', 'discharging'). Returns ------- float The energy capacity of the virtual battery system in either relative or absolute terms. """ # calculate the effective battery power (diff) diff_kW = flexible_kW - baseline_kW # charging is positive, discharging is negative charging = np.where(diff_kW > 0, diff_kW, 0) discharging = np.where(diff_kW < 0, -diff_kW, 0) # calculate the energy capacity if ec_type == "average": energy_capacity = (np.sum(charging) + np.sum(discharging)) * timestep elif ec_type == "charging": energy_capacity = np.sum(charging) * timestep elif ec_type == "discharging": energy_capacity = np.sum(discharging) * timestep else: raise ValueError( "Invalid energy capacity type. " "Must be 'average', 'charging', or 'discharging'." ) if relative: # normalize by the total baseline power return energy_capacity / ( np.sum(baseline_kW) * timestep + 1e-12 ) # add small value to avoid division by zero else: return energy_capacity
[docs]def net_present_value( capital_cost=0, electricity_savings=0, maintenance_diff=0, ancillary_service_benefit=0, service_curtailment=0, service_price=1.0, timestep=0.25, simulation_years=1, upgrade_lifetime=30, interest_rate=0.03, ): """ Calculate the net present value of flexibility of a virtual battery system. Parameters ---------- capital_cost : float The capital cost of the virtual battery system in $. electricity_savings : float The electricity savings from the flexible operation in $. maintenance_diff : float The difference in maintenance costs between the baseline and flexible operation in $. ancillary_service_benefit : float The benefit from providing ancillary services in $. service_curtailment : float The amount of service curtailment. If the virtual battery system produces a product, this may be in units of volume or mass (e.g., m^3 or kg). service_price : float The marginal price of curtailed service $/amount. Amount here may refer to units of volume or mass (e.g., $/m^3 or $/kg). timestep : float The time step of the data in hours. Default is 0.25 hours (15 minutes). simulation_years : int The number of years in which the electricity savings or ancillary service benefits are calculated for. Default is 1 year. upgrade_lifetime : int The number of years of operation left for the upgrade. Default is 30 years. interest_rate : float The interest rate used to discount future cash flows. Default is 0.03. Raises ------ Warning If the capital cost is less than 0 ValueError If the upgrade lifetime is less than or equal to 0 ValueError If the interest rate is less than 0. ValueError if the timestep is less than or equal to 0. Returns ------- float The net present value benefit of the virtual battery system in $. """ # check if capital cost is negative if capital_cost < 0: warnings.warn( "Capital cost is negative. This may indicate an error in the data." ) # check if upgrade lifetime is valid if upgrade_lifetime <= 0: raise ValueError("Upgrade lifetime must be greater than 0 years.") # check if interest rate is valid if interest_rate < 0: raise ValueError("Interest rate must be greater than or equal to 0.") # check if timestep is valid if timestep <= 0: raise ValueError("Timestep must be greater than 0 hours.") # calculate the total cash flow benefit = electricity_savings + ancillary_service_benefit cost = maintenance_diff + service_curtailment * service_price cash_flow = benefit - cost # calculate the net discount factor discount = sum([1 / ((1 + interest_rate) ** n) for n in range(1, upgrade_lifetime)]) # calculate the net present value npv = discount * (cash_flow / simulation_years) - capital_cost return npv