Source code for onstove.technology

"""This module contains the technology classes used in OnStove."""

import os
import numpy as np
import pandas as pd
import geopandas as gpd
from typing import Optional, Callable
from math import exp

from onstove._layer_utils import raster_setter, vector_setter
from onstove._utils import Processes
from onstove.layer import VectorLayer, RasterLayer


[docs]class Technology: """ Standard technology class used in order to model the different stoves used in the analysis. Parameters ---------- name: str, optional. Name of the technology to model. carbon_intensity: float, optional The CO2 equivalent emissions in kg/GJ of burned fuel. If this attribute is used, then none of the gas-specific intensities will be used (e.g. ch4_intensity). co2_intensity: float, default 0 The CO2 emissions in kg/GJ of burned fuel. ch4_intensity: float, default 0 The CH4 emissions in kg/GJ of burned fuel. n2o_intensity: float, default 0 The N2O emissions in kg/GJ of burned fuel. co_intensity: float, default 0 The CO emissions in kg/GJ of burned fuel. bc_intensity: float, default 0 The black carbon emissions in kg/GJ of burned fuel. oc_intensity: float, default 0 The organic carbon emissions in kg/GJ of burned fuel. energy_content: float, default 0 Energy content of the fuel in MJ/kg. tech_life: int, default 0 Stove life in year. inv_cost: float, default 0 Investment cost of the stove in USD. fuel_cost: float, default 0 Fuel cost in USD/kg if any. time_of_cooking: float, default 0 Daily average time spent for cooking with this stove in hours. om_cost: float, default 0 Operation and maintenance cost in USD/year. efficiency: float, default 0 Efficiency of the stove. pm25: float, default 0 Particulate Matter emissions (PM25) in mg/kg of fuel. is_base: boolean, default False Boolean determining if a specific stove is the base stove for everyone in the area of interest. transport_cost: float, default 0 Cost of transportation is_clean: boolean, default False Boolean indicating whether the stove is clean or not. current_share_urban: float, default 0 Current share of the stove assessed in the urban areas of the area of interest. current_share_rural: float, default 0 Current share of the stove assessed in the rural areas of the area of interest. epsilon: float, default 0.71 Emissions adjustment factor multiplied with the PM25 emissions. """ normalize = Processes.normalize def __init__(self, name: Optional[str] = None, carbon_intensity: Optional[str] = None, co2_intensity: float = 0, ch4_intensity: float = 0, n2o_intensity: float = 0, co_intensity: float = 0, bc_intensity: float = 0, oc_intensity: float = 0, energy_content: float = 0, tech_life: int = 0, inv_cost: float = 0, fuel_cost: float = 0, time_of_cooking: float = 0, om_cost: float = 0, efficiency: float = 0, pm25: float = 0, is_base: bool = False, transport_cost: float = 0, is_clean: bool = False, current_share_urban: float = 0, current_share_rural: float = 0, epsilon: float = 0.71): self.name = name self.carbon_intensity = carbon_intensity self.co2_intensity = co2_intensity self.ch4_intensity = ch4_intensity self.n2o_intensity = n2o_intensity self.co_intensity = co_intensity self.bc_intensity = bc_intensity self.oc_intensity = oc_intensity self.energy_content = energy_content self.tech_life = tech_life self.fuel_cost = fuel_cost self.inv_cost = inv_cost self.om_cost = om_cost self.time_of_cooking = time_of_cooking self.efficiency = efficiency self.pm25 = pm25 self.time_of_collection = 0 self.fuel_use = None self.is_base = is_base self.transport_cost = transport_cost self.carbon = None self.total_time_yr = None self.is_clean = is_clean self.current_share_urban = current_share_urban self.current_share_rural = current_share_rural self.energy = 0 self.epsilon = epsilon for paf in ['paf_alri_', 'paf_copd_', 'paf_ihd_', 'paf_lc_', 'paf_stroke_']: for s in ['u', 'r']: self[paf + s] = 0 self.discounted_fuel_cost = 0 self.discounted_investments = 0 self.benefits = None self.net_benefits = None self.gdf = gpd.GeoDataFrame() def __setitem__(self, idx, value): self.__dict__[idx] = value def __getitem__(self, idx): return self.__dict__[idx]
[docs] def adjusted_pm25(self): """Adjusts the PM25 value of each stove based on the adjusment factor. This is to take into account the potential behaviour change resulting from stove change [1]_. See also -------- relative_risk paf health_parameters mort_morb mortality morbidity References ---------- .. [1] Das, I. et al. The benefits of action to reduce household air pollution (BAR-HAP) model: A new decision support tool. PLOS ONE 16, e0245729 (2021). """ self.pm25 *= self.epsilon
[docs] def relative_risk(self) -> float: """Calculates the relative risk of contracting ALRI, COPD, IHD, lung cancer or stroke based on the adjusted PM25 emissions. The equations and parameters used are based on the work done by Burnett et al.[1]_ References ---------- .. [1] Burnett, R. T. et al. An Integrated Risk Function for Estimating the Global Burden of Disease Attributable to Ambient Fine Particulate Matter Exposure. Environmental Health Perspectives 122, 397–403 (2014). Returns ------- rr_alri: float Relative Risk of ALRI rr_copd: float Relative Risk of COPD rr_ihd: float Relative Risk of IHD rr_lc: float Relative Risk of lung cancer rr_stroke: float Relative Risk of stroke See also -------- adjusted_pm25 paf health_parameters mort_morb mortality morbidity """ if self.pm25 < 7.298: rr_alri = 1 else: rr_alri = 1 + 2.383 * (1 - exp(-0.004 * (self.pm25 - 7.298) ** 1.193)) if self.pm25 < 7.337: rr_copd = 1 else: rr_copd = 1 + 22.485 * (1 - exp(-0.001 * (self.pm25 - 7.337) ** 0.694)) if self.pm25 < 7.449: rr_ihd = 1 else: rr_ihd = 1 + 1.647 * (1 - exp(-0.048 * (self.pm25 - 7.449) ** 0.467)) if self.pm25 < 7.345: rr_lc = 1 else: rr_lc = 1 + 152.496 * (1 - exp(-0.000167 * (self.pm25 - 7.345) ** 0.76)) if self.pm25 < 7.358: rr_stroke = 1 else: rr_stroke = 1 + 1.314 * (1 - exp(-0.012 * (self.pm25 - 7.358) ** 1.275)) return rr_alri, rr_copd, rr_ihd, rr_lc, rr_stroke
[docs] def paf(self, rr: float, sfu: float) -> float: """Calculates the Population Attributable Fraction for (PAF) ALRI, COPD, IHD, lung cancer or stroke based on the percentage of population using non-clean stoves and the relative risk. Given the total mortality and incidence (or prevalence) rates of each disease the PAF estimates the share of these factors attributed to the share of solid fuel users [1]_. References ---------- .. [1] Jeuland, M., Tan Soo, J.-S. & Shindell, D. The need for policies to reduce the costs of cleaner cooking in low income settings: Implications from systematic analysis of costs and benefits. Energy Policy 121, 275–285 (2018). Parameters ---------- rr: float The relative risk of contracting ALRI, COPD, IHD, lung cancer and stroke. sfu: float Solid Fuel Users. This is the percentage of people using traditional cooking fuels. This is read from the techno-economic specification file as fuels not being clean Returns ------- paf: float The Population Attributable Fraction for each disease. See also -------- adjusted_pm25 relative_risk health_parameters mort_morb mortality morbidity """ paf = (sfu * (rr - 1)) / (sfu * (rr - 1) + 1) return paf
[docs] @staticmethod def discount_factor(specs: dict) -> tuple[list[float], list[float]]: """Calculates and returns the discount factor used for benefits and costs in the net-benefit equation. Parameters ---------- specs: dict The socio-economic specification file containing socio-economic data applying to your study area Returns ------- Discount factor and the project life """ if specs["start_year"] == specs["end_year"]: proj_life = 1 else: proj_life = specs["end_year"] - specs["start_year"] year = np.arange(proj_life) + 1 discount_factor = (1 + specs["discount_rate"]) ** year return discount_factor, proj_life
[docs] def required_energy(self, model: 'onstove.OnStove'): """ Calculates the annual energy needed for cooking in MJ/yr. This is dependent on the number of meals cooked and the efficiency of the stove. Function does not return anything but saves the energy in the `energy` attribute of the OnStove model object. Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. """ self.energy = model.specs["meals_per_day"] * 365 * model.energy_per_meal / self.efficiency
[docs] def get_carbon_intensity(self, model: 'onstove.OnStove'): """Calculates the carbon intensity of the associated stove. Function does not return anything but saves the carbon_intensity in the `carbon_intensity` attribute of the OnStove model object. Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. See also -------- carb carbon_emissions """ pollutants = ['co2', 'ch4', 'n2o', 'co', 'bc', 'oc'] self.carbon_intensity = sum([self[f'{pollutant}_intensity'] * model.gwp[pollutant] for pollutant in pollutants])
[docs] def carb(self, model: 'onstove.OnStove'): """Checks if carbon_emission is given in the socio-economic specification file. If it is given this is read directly, otherwise the get_carbon_intensity function is called. Function does not return anything but saves the carbon_emissions in the `carbon` attribute of the OnStove model object. Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. See also -------- required_energy get_carbon_intensity carbon_emissions """ self.required_energy(model) if self.carbon_intensity is None: self.get_carbon_intensity(model) self.carbon = pd.Series([(self.energy * self.carbon_intensity) / 1000] * model.gdf.shape[0], index=model.gdf.index)
[docs] def carbon_emissions(self, model: 'onstove.OnStove'): """Calculates the reduced emissions and the costs avoided by reducing these emissions. Function does not return anything but the reduced emissions and avoided costs are saved in the `decreased_carbon_emissions` and `decreased_carbon_costs` attributes of the OnStove model object respectively Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. See also -------- carb required_energy get_carbon_intensity """ self.carb(model) proj_life = model.specs['end_year'] - model.specs['start_year'] carbon = model.specs["cost_of_carbon_emissions"] * (model.base_fuel.carbon - self.carbon) / 1000 / ( 1 + model.specs["discount_rate"]) ** (proj_life) self.decreased_carbon_emissions = model.base_fuel.carbon - self.carbon self.decreased_carbon_costs = carbon
[docs] def health_parameters(self, model: 'onstove.OnStove'): """Calculates the population attributable fraction for ALRI, COPD, IHD, lung cancer or stroke for urban and rural settlements of the area of interest. Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. See also -------- adjusted_pm25 relative_risk paf mort_morb mortality morbidity """ rr_alri, rr_copd, rr_ihd, rr_lc, rr_stroke = self.relative_risk() self.paf_alri_r = self.paf(rr_alri, 1 - model.clean_cooking_access_r) self.paf_copd_r = self.paf(rr_copd, 1 - model.clean_cooking_access_r) self.paf_ihd_r = self.paf(rr_ihd, 1 - model.clean_cooking_access_r) self.paf_lc_r = self.paf(rr_lc, 1 - model.clean_cooking_access_r) self.paf_stroke_r = self.paf(rr_stroke, 1 - model.clean_cooking_access_r) self.paf_alri_u = self.paf(rr_alri, 1 - model.clean_cooking_access_u) self.paf_copd_u = self.paf(rr_copd, 1 - model.clean_cooking_access_u) self.paf_ihd_u = self.paf(rr_ihd, 1 - model.clean_cooking_access_u) self.paf_lc_u = self.paf(rr_lc, 1 - model.clean_cooking_access_u) self.paf_stroke_u = self.paf(rr_stroke, 1 - model.clean_cooking_access_u)
[docs] def mort_morb(self, model: 'onstove.OnStove', parameter: str = 'mort', dr: str = 'discount_rate') -> tuple[ float, float]: """Calculates mortality or morbidity rate per fuel. These two calculations are very similar in nature and are therefore combined in one function. In order to indicate if morbidity or mortality should be calculated, the `parameter` parameter can be changed (to either `Morb` or `Mort`). Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. parameter: str, default 'Mort' Parameter to calculate. For mortality enter 'Mort' and for morbidity enter 'Morb' dr: str, default 'Discount_rate' Discount rate used in the analysis read from the socio-economic file Returns ---------- Monetary value of mortality or morbidity for each stove in every cell of the analysis See also -------- adjusted_pm25 relative_risk paf health_parameters mortality morbidity """ self.health_parameters(model) mor_u = {} mor_r = {} diseases = ['alri', 'copd', 'ihd', 'lc', 'stroke'] is_urban = model.gdf["IsUrban"] > 20 is_rural = model.gdf["IsUrban"] < 20 for disease in diseases: rate = model.specs[f'{parameter}_{disease}'] paf = f'paf_{disease.lower()}_u' mor_u[disease] = model.gdf.loc[is_urban, "Calibrated_pop"].sum() * (model.base_fuel[paf] - self[paf]) * ( rate / 100000) paf = f'paf_{disease.lower()}_r' mor_r[disease] = model.gdf.loc[is_rural, "Calibrated_pop"].sum() * (model.base_fuel[paf] - self[paf]) * ( rate / 100000) cl_diseases = {'alri': {1: 0.7, 2: 0.1, 3: 0.07, 4: 0.07, 5: 0.06}, 'copd': {1: 0.3, 2: 0.2, 3: 0.17, 4: 0.17, 5: 0.16}, 'lc': {1: 0.2, 2: 0.1, 3: 0.24, 4: 0.23, 5: 0.23}, 'ihd': {1: 0.2, 2: 0.1, 3: 0.24, 4: 0.23, 5: 0.23}, 'stroke': {1: 0.2, 2: 0.1, 3: 0.24, 4: 0.23, 5: 0.23}} i = 1 total_mor_u = 0 total_mor_r = 0 while i < 6: for disease in diseases: if parameter == 'morb': cost = model.specs[f'coi_{disease}'] elif parameter == 'mort': cost = model.specs['vsl'] total_mor_u += cl_diseases[disease][i] * cost * mor_u[disease] / (1 + model.specs[dr]) ** (i - 1) total_mor_r += cl_diseases[disease][i] * cost * mor_r[disease] / (1 + model.specs[dr]) ** (i - 1) i += 1 is_urban = model.gdf["IsUrban"] > 20 is_rural = model.gdf["IsUrban"] < 20 urban_denominator = model.gdf.loc[is_urban, "Calibrated_pop"].sum() * model.gdf.loc[is_urban, 'Households'] rural_denominator = model.gdf.loc[is_rural, "Calibrated_pop"].sum() * model.gdf.loc[is_rural, 'Households'] distributed_cost = pd.Series(index=model.gdf.index, dtype='float64') distributed_cost[is_urban] = model.gdf.loc[is_urban, "Calibrated_pop"] * total_mor_u / urban_denominator distributed_cost[is_rural] = model.gdf.loc[is_rural, "Calibrated_pop"] * total_mor_r / rural_denominator cases_avoided = pd.Series(index=model.gdf.index, dtype='float64') cases_avoided[is_urban] = sum(mor_u.values()) * model.gdf.loc[is_urban, "Calibrated_pop"] / urban_denominator cases_avoided[is_rural] = sum(mor_r.values()) * model.gdf.loc[is_rural, "Calibrated_pop"] / rural_denominator return distributed_cost, cases_avoided
[docs] def mortality(self, model: 'onstove.OnStove'): """Calculates the mortality across the study area per stove by calling the `mort_morb` function. Function does not return anything but saves the avoided costs in `distributed_mortality` of the Onstove model object, the avoided deaths in the `deaths_avoided` attribute of the OnStove model object. The function takes into account the `health_spillovers_parameter` Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. See also -------- djusted_pm25 relative_risk paf health_parameters mort_morb """ distributed_mortality, deaths_avoided = self.mort_morb(model, parameter='mort', dr='discount_rate') self.distributed_mortality = distributed_mortality self.deaths_avoided = deaths_avoided if model.specs['health_spillovers_parameter'] > 0: self.distributed_spillovers_mort = distributed_mortality * model.specs['health_spillovers_parameter'] self.deaths_avoided += deaths_avoided * model.specs['health_spillovers_parameter'] else: self.distributed_spillovers_mort = pd.Series(0, index=model.gdf.index, dtype='float64')
[docs] def morbidity(self, model: 'onstove.OnStove'): """Calculates the morbidity across the study area per stove by calling the `mort_morb` function. Function does not return anything but saves the avoided costs in the `distributed_morbidity` attribute of the Onstove model object, the avoided cases in the `cases_avoided` attribute of the OnStove model object. The function takes into account the `health_spillovers_parameter` Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. See also -------- djusted_pm25 relative_risk paf health_parameters mort_morb """ distributed_morbidity, cases_avoided = self.mort_morb(model, parameter='morb', dr='discount_rate') self.distributed_morbidity = distributed_morbidity self.cases_avoided = cases_avoided if model.specs['health_spillovers_parameter'] > 0: self.distributed_spillovers_morb = distributed_morbidity * model.specs['health_spillovers_parameter'] self.cases_avoided += cases_avoided * model.specs['health_spillovers_parameter'] else: self.distributed_spillovers_morb = pd.Series(0, index=model.gdf.index, dtype='float64')
[docs] def salvage(self, model: 'onstove.OnStove'): """Calls discount_factor function and calculates discounted salvage cost for each stove assuming a straight-line depreciation of the stove value. Function does not return anything but saves the discounted salvage cost in the `discounted_salvage_cost` attribute of the Onstove model object Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. See also -------- discount_factor """ discount_rate, proj_life = self.discount_factor(model.specs) used_life = proj_life % self.tech_life used_life_base = proj_life % model.base_fuel.tech_life base_salvage = model.base_fuel.inv_cost * (1 - used_life_base / model.base_fuel.tech_life) salvage = self.inv_cost * (1 - used_life / self.tech_life) salvage = salvage - base_salvage # TODO: this needs to be changed to use a series for each salvage value discounted_salvage = salvage / discount_rate self.discounted_salvage_cost = discounted_salvage
[docs] def discounted_om(self, model: 'onstove.OnStove'): """Calls discount_factor function and calculates discounted operation and maintenance cost for each stove. Function does not return anything but saves the discounted operation and maintenance cost in the `discounted_om_costs` attribute of the Onstove model object. Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. See also -------- discount_factor """ discount_rate, proj_life = self.discount_factor(model.specs) operation_and_maintenance = self.om_cost * np.ones(proj_life) discounted_om = np.array([sum((operation_and_maintenance - x) / discount_rate) for x in model.base_fuel.om_cost]) self.discounted_om_costs = pd.Series(discounted_om, index=model.gdf.index)
[docs] def discounted_inv(self, model: 'onstove.OnStove', relative: bool = True): """ Calls discount_factor function and calculates discounted investment cost. Uses proj_life and tech_life to determine number of necessary re-investments. Function does not return anything but saves the discounted investment cost in the `discounted_investments` attribute of the Onstove model object. Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. relative: bool, default True Boolean parameter to indicate if the discounted investments will be calculated relative to the `base_fuel` or not. See also -------- discount_factor """ discount_rate, proj_life = self.discount_factor(model.specs) inv = self.inv_cost * np.ones(model.gdf.shape[0]) tech_life = self.tech_life * np.ones(model.gdf.shape[0]) proj_years = np.matmul(np.expand_dims(np.ones(model.gdf.shape[0]), axis=1), np.expand_dims(np.zeros(proj_life), axis=0)) for i in np.unique(tech_life): where = np.where(tech_life == i) for j in range(int(i) - 1, proj_life, int(i)): proj_years[where, j] = 1 investments = proj_years * np.array(inv)[:, None] if relative: discounted_base_investments = model.base_fuel.discounted_investments else: discounted_base_investments = 0 investments_discounted = np.array([sum(x / discount_rate) for x in investments]) self.discounted_investments = pd.Series(investments_discounted, index=model.gdf.index) + self.inv_cost - \ discounted_base_investments
[docs] def discount_fuel_cost(self, model: 'onstove.OnStove', relative: bool = True): """Calls discount_factor function and calculates discounted fuel costs. Function does not return anything but saves the discounted fuel cost in the `discounted_fuel_cost` attribute of the Onstove model object. Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. relative: bool, default True Boolean parameter to indicate if the discounted fuel cost will be calculated relative to the `base_fuel` or not. See also -------- discount_factor """ self.required_energy(model) discount_rate, proj_life = self.discount_factor(model.specs) cost = (self.energy * self.fuel_cost / self.energy_content + self.transport_cost) * np.ones(model.gdf.shape[0]) fuel_cost = [np.ones(proj_life) * x for x in cost] fuel_cost_discounted = np.array([sum(x / discount_rate) for x in fuel_cost]) if relative: discounted_base_fuel_cost = model.base_fuel.discounted_fuel_cost else: discounted_base_fuel_cost = 0 self.discounted_fuel_cost = pd.Series(fuel_cost_discounted, index=model.gdf.index) - discounted_base_fuel_cost
[docs] def total_time(self, model: 'onstove.OnStove'): """Calculates total time used per year by taking into account time of cooking and time of fuel collection (if relevant). Function does not return anything but saves the total time used in the `total_time_yr` attribute of the Onstove model object. Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. """ self.total_time_yr = (self.time_of_cooking + self.time_of_collection) * 365
[docs] def time_saved(self, model: 'onstove.OnStove'): """Calculates time saved per year by adopting a new stove. Function does not return anything but saves the total time saved in the `total_time_saved` attribute of the OnStove model object and the total monetary value of the time saved in `time_value` attribute of the OnStove model object. Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. """ proj_life = model.specs['end_year'] - model.specs['start_year'] self.total_time(model) self.total_time_saved = model.base_fuel.total_time_yr - self.total_time_yr # time value of time saved per sq km self.time_value = self.total_time_saved * model.gdf["value_of_time"] / ( 1 + model.specs["discount_rate"]) ** (proj_life)
[docs] def total_costs(self): """ Calculates total costs (fuel, investment, operation and maintenance as well as salvage costs). Function does not return anything but saves the total costs in the `costs` attribute of the OnStove model object. Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. See also -------- discount_fuel_cost, discounted_om, salvage, discounted_inv """ self.costs = (self.discounted_fuel_cost + self.discounted_investments + self.discounted_om_costs - self.discounted_salvage_cost)
[docs] def net_benefit(self, model: 'onstove.OnStove', w_health: int = 1, w_spillovers: int = 1, w_environment: int = 1, w_time: int = 1, w_costs: int = 1): """This method combines all costs and benefits as specified by the user using the weights parameters. Function does not return anything but saves the total benefits in the `benefits` attribute of the OnStove object and the net-benefits in the `net-benefits` attribute of the OnStove net-benefits object. Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. w_health: int, default 1 Determines the weight of the health parameters (reduced morbidity and mortality) in the net-benefit equation. w_spillovers: int, default 1 Determines the weight of the spillover effects from cooking with traditional fuels in the net-benefit equation. w_environment: int, default 1 Determines the weight of the environmental effects (reduced emissions) in the net-benefit equation. w_time: int, default 1 Determines the weight of the opportunity cost (reduced time spent) in the net-benefit equation. w_costs: int, default 1 Determines the weight of the costs in the net-benefit equation. See also -------- total_costs morbidity mortality time_saved carbon_emissions """ self.total_costs() self.benefits = w_health * (self.distributed_morbidity + self.distributed_mortality) + \ w_spillovers * (self.distributed_spillovers_morb + self.distributed_spillovers_mort) + \ w_environment * self.decreased_carbon_costs + w_time * self.time_value self.net_benefits = self.benefits - w_costs * self.costs model.gdf["costs_{}".format(self.name)] = self.costs model.gdf["benefits_{}".format(self.name)] = self.benefits model.gdf["net_benefit_{}".format(self.name)] = self.benefits - w_costs * self.costs self.factor = pd.Series(np.ones(model.gdf.shape[0]), index=model.gdf.index) self.households = model.gdf['Households']
[docs]class LPG(Technology): """LPG technology class used to model LPG stoves. This class inherits the standard :class:`Technology` class and is used to model stoves using LPG as fuel. The LPG is assumed to be bought either by the closest vendor or in the closest urban settlement depending on data availability. In the first case a point layer indicating vendors is assumed to be passed to the OnStove after which a least-cost path is determined using a friction map. In the other case it is assumed that the travel time map is passed to OnStove directly. Parameters ---------- name: str, optional. Name of the technology to model. carbon_intensity: float, optional The CO2 equivalent emissions in kg/GJ of burned fuel. If this attribute is used, then none of the gas-specific intensities will be used (e.g. ch4_intensity). co2_intensity: float, default 63 The CO2 emissions in kg/GJ of burned fuel. ch4_intensity: float, default 0.003 The CH4 emissions in kg/GJ of burned fuel. n2o_intensity: float, default 0.0001 The N2O emissions in kg/GJ of burned fuel. co_intensity: float, default 0 The CO emissions in kg/GJ of burned fuel. bc_intensity: float, default 0.0044 The black carbon emissions in kg/GJ of burned fuel. oc_intensity: float, default 0.0091 The organic carbon emissions in kg/GJ of burned fuel. energy_content: float, default 45.5 Energy content of the fuel in MJ/kg. tech_life: int, default 7 Stove life in year. inv_cost: float, default 44 Investment cost of the stove in USD. fuel_cost: float, default 0.73 Fuel cost in USD/kg. time_of_cooking: float, default 2 Daily average time spent for cooking with this stove in hours. om_cost: float, default 3.7 Operation and maintenance cost in USD/year. efficiency: float, default 0.5 Efficiency of the stove. pm25: float, default 43 Particulate Matter emissions (PM25) in mg/kg of fuel. travel_time: Pandas Series, optional Pandas Series describing the time needed (in hours) to reach either the closest LPG supply point or urban settlement from each population point. It is either calculated using the LPG supply points, friction layer and population density layer or taken directly from a travel time map. truck_capacity: float, default 2000 Capacity of the truck carrying the fuel in kg. diesel_cost: float, 1.04 Cost of diesel used in order to estimate the cost of transportation. Given in USD/liter diesel_per_hour: float, default 14 Average diesel consumption of the truck carrying the fuel. Measured in liter/h lpg_path: str, optional Path to the lpg supply points (point vector file). friction_path: str, optional Path to the friction raster file describing the time needed (in minutes) to travel one meter within each cell using motorized transport. cylinder_cost: float, default 2.78 Cost of LPG cylinder. This is relevant for first time buyers currenty not having access to an LPG cylinder. Given in USD/kg cylinder_life: float, 15 Lifetime of LPG cylinder, measured in years. """ def __init__(self, name: Optional[str] = None, carbon_intensity: Optional[float] = None, co2_intensity: float = 63, ch4_intensity: float = 0.003, n2o_intensity: float = 0.0001, co_intensity: float = 0, bc_intensity: float = 0.0044, oc_intensity: float = 0.0091, energy_content: float = 45.5, tech_life: int = 7, # in years inv_cost: float = 44, # in USD fuel_cost: float = 0.73, time_of_cooking: float = 2, om_cost: float = 3.7, efficiency: float = 0.5, # ratio pm25: float = 43, travel_time: Optional[pd.Series] = None, truck_capacity: float = 2000, diesel_cost: float = 1.04, diesel_per_hour: float = 14, lpg_path: Optional[str] = None, friction_path: Optional[str] = None, cylinder_cost: float = 2.78, # USD/kg, cylinder_life: float = 15): super().__init__(name, carbon_intensity, co2_intensity, ch4_intensity, n2o_intensity, co_intensity, bc_intensity, oc_intensity, energy_content, tech_life, inv_cost, fuel_cost, time_of_cooking, om_cost, efficiency, pm25, is_clean=True) self.travel_time = travel_time self.truck_capacity = truck_capacity self.diesel_cost = diesel_cost self.diesel_per_hour = diesel_per_hour self.transport_cost = None self.lpg_path = lpg_path self.friction_path = friction_path self.cylinder_cost = cylinder_cost self.cylinder_life = cylinder_life
[docs] def add_travel_time(self, model: 'onstove.OnStove', lpg_path: Optional[str] = None, friction_path: Optional[str] = None, align: bool = False): """This method calculates the travel time needed to transport LPG. The travel time is calculated as the time needed (in hours) to reach the closest LPG supplier from each population point. It uses a point layer for LPG suppliers, a friction layer and a population density layer. The function does not return anything but saves the travel time in the `travel_time` attribute of the LPG technology class. Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. lpg_path: str Path to the LPG supply points. friction_path: str Path to the friction raster file describing the time needed (in minutes) to travel one meter within each cell using motorized transport. align: bool, default False Boolean parameter to indicate if the friction layer need to be align with the population data in the `model`. """ if lpg_path is None: if self.lpg_path is not None: lpg_path = self.lpg_path else: raise ValueError('A path to a LPG point layer must be passed or stored in the `lpg_path` attribute.') lpg = VectorLayer(self.name, 'Suppliers', path=lpg_path) if friction_path is None: if self.friction_path is not None: friction_path = self.friction_path else: raise ValueError('A path to a friction raster layer must be passed or stored in the `friction_path`' ' attribute.') friction = RasterLayer(self.name, 'Friction', path=friction_path, resample='average') if align: os.makedirs(os.path.join(model.output_directory, self.name, 'Suppliers'), exist_ok=True) lpg.reproject(model.base_layer.meta['crs'], os.path.join(model.output_directory, self.name, 'Suppliers')) friction.align(model.base_layer.path, os.path.join(model.output_directory, self.name, 'Friction')) lpg.friction = friction lpg.travel_time(create_raster=True) self.travel_time = 2 * model.raster_to_dataframe(lpg.distance_raster, fill_nodata_method='interpolate', method='read')
[docs] def transportation_cost(self, model: 'onstove.OnStove'): """The cost of transporting LPG. Transportation cost = (2 * diesel consumption per h * national diesel price * travel time)/transported LPG Total cost = (LPG cost + Transportation cost)/efficiency of LPG stoves For more information about cost formula see [1]_. The function uses the following attributes of model: ``diesel_per_hour``, ``diesel_cost``, ``travel_time``, ``truck_capacity``, ``efficiency`` and ``energy_content``. The function does not return anything but saves the transport cost in the `transport_cost` attribute of the LPG technology class. References ---------- .. [1] Szabó, S., Bódis, K., Huld, T. & Moner-Girona, M. Energy solutions in rural Africa: mapping electrification costs of distributed solar and diesel generation versus grid extension. Environ. Res. Lett. 6, 034002 (2011). Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. """ transport_cost = (self.diesel_per_hour * self.diesel_cost * self.travel_time) / self.truck_capacity kg_yr = (model.specs["meals_per_day"] * 365 * model.energy_per_meal) / ( self.efficiency * self.energy_content) # energy content in MJ/kg transport_cost = transport_cost * kg_yr transport_cost[transport_cost < 0] = np.nan self.transport_cost = transport_cost
[docs] def discount_fuel_cost(self, model: 'onstove.OnStove', relative: bool = True): """This method expands :meth:`discount_fuel_cost` when LPG is the stove assessed in order to ensure that the transportation costs are included Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. relative: bool, default True Boolean parameter to indicate if the discounted investments will be calculated relative to the `base_fuel` or not. See also -------- discount_fuel_cost """ self.transportation_cost(model) super().discount_fuel_cost(model, relative)
[docs] def transport_emissions(self, model: 'onstove.OnStove'): """Calculates the emissions caused by the transportation of LPG. This is dependent on the diesel consumption of the truck. Diesel consumption is assumed to be 14 l/h (14 l/100km). Each truck is assumed to transport 2,000 kg LPG Emissions intensities and diesel density are taken from [1]_. The function uses the following attributes of model: ``energy``, ``energy_content``, ``travel_time`` and ``truck_capacity``. References ---------- .. [1] Ntziachristos, L. and Z. Samaras (2018), “1.A.3.b.i, 1.A.3.b.ii, 1.A.3.b.iii, 1.A.3.b.iv Passenger cars, light commercial trucks, heavy-duty vehicles including buses and motor cycles”, in EMEP/EEA air pollutant emission inventory guidebook 2016 – Update Jul. 2018 Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. Returns ------- The total transport emissions that can be associated with each household in kg of CO2-eq per year. """ diesel_density = 840 # kg/m3 bc_fraction = 0.55 # BC fraction of pm2.5 oc_fraction = 0.70 # OC fraction of BC pm_diesel = 1.52 # g/kg_diesel diesel_ef = {'co2': 3.169, 'co': 7.40, 'n2o': 0.056, 'bc': bc_fraction * pm_diesel, 'oc': oc_fraction * bc_fraction * pm_diesel} # g/kg_Diesel kg_yr = self.energy / self.energy_content # LPG use (kg/yr). Energy required (MJ/yr)/LPG energy content (MJ/kg) diesel_consumption = self.travel_time * (14 / 1000) * diesel_density # kg of diesel per trip hh_emissions = sum([ef * model.gwp[pollutant] * diesel_consumption / self.truck_capacity * kg_yr for pollutant, ef in diesel_ef.items()]) # in gCO2eq per yr return hh_emissions / 1000 # in kgCO2eq per yr
[docs] def carb(self, model: 'onstove.OnStove'): """This method expands :meth:`Technology.carbon` when LPG is the stove assessed in order to ensure that the emissions caused by the transportation is included. Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. See also -------- transport_emissions """ super().carb(model) self.carbon += self.transport_emissions(model)
[docs] def infrastructure_cost(self, model: 'onstove.OnStove'): """Calculates cost of cylinders for first-time LPG users. It is assumed that the cylinder contains 12.5 kg of LPG. The function calls ``infrastructure_salvage``. The function does not return anything but saves the infrastructure cost (cylinder cost) in the `discounted_infra_cost` attribute of the LPG technology class. The function uses the ``cylinder_cost`` attribute of the model. Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. See also -------- infrastructure_salvage """ cost = self.cylinder_cost * 12.5 salvage = self.infrastructure_salvage(model, cost, self.cylinder_life) self.discounted_infra_cost = (cost - salvage)
[docs] def infrastructure_salvage(self, model: 'onstove.OnStove', cost: float, life: int): """Calculates the salvaged cylinder cost. The function calls ``discount_factor``. Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. cost: float The cost of buying an LPG-cylinder. life: int The lifetime of a cylinder. Returns ------- The discounted salvage cost of an LPG-cylinder. See also -------- discount_factor """ discount_rate, proj_life = self.discount_factor(model.specs) used_life = proj_life % life salvage = cost * (1 - used_life / life) return salvage / discount_rate[0]
[docs] def discounted_inv(self, model: 'onstove.OnStove', relative: bool = True): """This method expands :meth:`Technology.discounted_inv` by adding the cylinder cost for households currently not using LPG. Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. relative: bool, default True Boolean parameter to indicate if the discounted investments will be calculated relative to the `base_fuel` or not. See also -------- infrastructure_cost """ # TODO: this method needs update on the current shares based on the new calibration methods super().discounted_inv(model, relative=relative) self.infrastructure_cost(model) if relative: share = (model.gdf['IsUrban'] > 20) * self.current_share_urban share[model.gdf['IsUrban'] < 20] *= self.current_share_rural self.discounted_investments += (self.discounted_infra_cost * (1 - share))
[docs]class Biomass(Technology): """Biomass technology class used to model traditional and improved stoves. This class inherits the standard :class:`Technology` class and is used to model traditional and Improved Cook Stoves (ICS) using biomass as fuel. The biomass can be either collected or purchased, which is indicated with the attribute ``collected_fuel``. Depending on the biomass type (e.g. fuelwood or pellets), the parameters passed to the class such as efficiency, energy content, pm25 and emissions need to be representative of the fuel-stove. Moreover, the ICS can be modelled as natural draft or forced draft options by specifying it with the ``draft_type`` attribute. If forced draft is used, then the class will consider and extra capital cost for a standard 6 watt solar panel in order to run the fan in unelectrified areas. Attributes ---------- forest: object of type RasterLayer, optional This is the forest cover raster dataset read from the ``forest_path`` parameter. See the :class:`onstove.RasterLayer` class for more information. friction: str, optional This is the forest cover raster dataset read from the ``friction_path``. trips_per_yr: float The trips that a person needs to do to the nearest forest point, in order to collect the amount of biomass required for cooking in one year (per households). Parameters ---------- name: str, optional. Name of the technology to model. carbon_intensity: float, optional The CO2 equivalent emissions in kg/GJ of burned fuel. If this attribute is used, then none of the gas-specific intensities will be used (e.g. ch4_intensity). co2_intensity: float, default 112 The CO2 emissions in kg/GJ of burned fuel. ch4_intensity: float, default 0.864 The CH4 emissions in kg/GJ of burned fuel. n2o_intensity: float, default 0.0039 The N2O emissions in kg/GJ of burned fuel. co_intensity: float, default 0 The CO emissions in kg/GJ of burned fuel. bc_intensity: float, default 0.1075 The black carbon emissions in kg/GJ of burned fuel. oc_intensity: float, default 0.308 The organic carbon emissions in kg/GJ of burned fuel. energy_content: float, default 16 Energy content of the fuel in MJ/kg. tech_life: int, default 2 Stove life in years. inv_cost: float, default 0 Investment cost of the stove in USD. fuel_cost: float, default 0 Fuel cost in USD/kg if any. time_of_cooking: float, default 2.9 Daily average time spent for cooking with this stove in hours. om_cost: float, default 0 Operation and maintenance cost in USD/year. efficiency: float, default 0.12 Efficiency of the stove. pm25: float, default 844 Particulate Matter emissions (PM25) in mg/kg of fuel. forest_path: str, optional Path to the forest cover raster file. friction_path: str, optional Path to the friction raster file describing the time needed (in minutes) to travel one meter within each cell. travel_time: Pandas Series, optional Pandas Series describing the time needed (in hours) to reach the closest forest cover point from each population point. It is calculated using the forest cover, friction layer and population density layer. .. seealso:: :meth:`transportation_time<onstove.Biomass.transportation_time>` and :meth:`total_time<onstove.Biomass.total_time>` collection_capacity: float, default 25 Average wood collection capacity per person in kg/trip. collected_fuel: bool, default True Boolean indicating if the fuel is collected or purchased. If True, then the ``travel_time`` will be calculated. If False, the ``fuel_cost`` will be used and a travel and collection time disregarded. time_of_collection: float, default 2 Time spent collecting biomass on a single trip (excluding travel time) in hours. draft_type: str, default 'natural' Whether the ICS uses a natural draft or a forced draft. forest_condition: Callable object (function or lambda function) with a numpy array as input, optional Function or lambda function describing which forest canopy cover to consider when assessing the potential points for biomass collection. .. code-block:: python :caption: **Example**: lambda function for canopy cover equal or over 30% >>> forest_condition = lambda x: x >= 0.3 Examples -------- An OnStove Biomass class can be created by providing the technology input data on an `csv` file and calling the :meth:`read_tech_data<onstove.read_tech_data>` method of the :class:`onstove.OnStove>` class, or by passing all technology information in the script. Creating the technologies from a `csv` configuration file (see *link to examples or mendeley* for a example of the configuration file): >>> from onstove import OnStove ... model = OnStove(output_directory='output_directory') ... mode.read_tech_data(path_to_config='path_to_csv_file', delimiter=',') ... model.techs {'Biomass': {'Biomass': <onstove.Biomass at 0x2478e85ee80>}} Creating a Biomass technology in the script: >>> from onstove import OnStove ... from onstove import Biomass ... model = OnStove(output_directory='output_directory') ... biomass = Biomass(name='Biomass') # we define the name and leave all other parameters with the default values ... model.techs['Biomass'] = biomass ... model.techs {'Biomass': {'Biomass': <onstove.Biomass at 0x2478e85ee80>}} """ forest: Optional[RasterLayer] = None friction: Optional[RasterLayer] = None trips_per_yr: float = 0.0 def __init__(self, name: Optional[str] = None, carbon_intensity: Optional[float] = None, co2_intensity: float = 112, ch4_intensity: float = 0.864, n2o_intensity: float = 0.0039, co_intensity: float = 0, bc_intensity: float = 0.1075, oc_intensity: float = 0.308, energy_content: float = 16, tech_life: int = 2, inv_cost: float = 0, fuel_cost: float = 0, time_of_cooking: float = 2.9, om_cost: float = 0, efficiency: float = 0.12, pm25: float = 844, forest_path: Optional[str] = None, friction_path: Optional[str] = None, travel_time: Optional[pd.Series] = None, collection_capacity: float = 25, collected_fuel: bool = True, time_of_collection: float = 2, draft_type: str = 'natural', forest_condition: Optional[Callable[[np.ndarray], np.ndarray]] = None): """Instantiates the class either with default or user defined values for each class attribute. """ super().__init__(name, carbon_intensity, co2_intensity, ch4_intensity, n2o_intensity, co_intensity, bc_intensity, oc_intensity, energy_content, tech_life, inv_cost, fuel_cost, time_of_cooking, om_cost, efficiency, pm25, is_clean=False) self.forest_condition = forest_condition self.travel_time = travel_time self.forest_path = forest_path self.friction_path = friction_path self.collection_capacity = collection_capacity self.draft_type = draft_type self.collected_fuel = collected_fuel self.time_of_collection = time_of_collection self.solar_panel_adjusted: bool = False #: boolean check to avoid adding the solar panel cost twice def __setitem__(self, idx, value): self.__dict__[idx] = value
[docs] def transportation_time(self, friction_path: str, forest_path: str, model: 'onstove.OnStove'): """This method calculates the travel time needed to gather biomass. The travel time is calculated as the time needed (in hours) to reach the closest forest cover point from each population point. It uses a forest cover layer, a friction layer and population density layer. The function does not return anything but saves the travel time in the `travel_time` attribute of the Biomass technology class. Parameters ---------- friction_path: str Path to the friction raster file describing the time needed (in minutes) to travel one meter within each cell. forest_path: str Path to the forest cover raster file. model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. """ self.forest = RasterLayer(self.name, 'Forest', path=forest_path, resample='mode') self.friction = RasterLayer(self.name, 'Friction', path=friction_path, resample='average') self.forest.friction = self.friction rows, cols = self.forest.start_points(condition=self.forest_condition) self.friction.travel_time(rows=rows, cols=cols, include_starting_cells=True, create_raster=True) travel_time = 2 * model.raster_to_dataframe(self.friction.distance_raster, fill_nodata_method='interpolate', method='read') travel_time[travel_time > 7] = 7 # cap to max travel time based on literature self.travel_time = travel_time
[docs] def total_time(self, model: 'onstove.OnStove'): """This method expands :meth:`Technology.total_time` when biomass is collected. It calculates the time needed for collecting biomass, based on the ``collection_capacity`` the ``energy_content`` of the fuel, the ``energy`` required for cooking a standard meal and the travel time to the nearest forest area. Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. """ if self.collected_fuel: self.transportation_time(self.friction_path, self.forest_path, model) self.trips_per_yr = self.energy / (self.collection_capacity * self.energy_content) self.total_time_yr = self.time_of_cooking * 365 + \ (self.travel_time + self.time_of_collection) * self.trips_per_yr else: self.time_of_collection = 0 super().total_time(model)
[docs] def get_carbon_intensity(self, model: 'onstove.OnStove'): """This method expands :meth:`Technology.get_carbon_intensity`. It excludes the CO2 emissions from the share of firewood that is sustainably harvested (i.e. it does not affect other emissions such as CH4) by using the fraction of Non-Renewable Biomass (fNRB). Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. Notes ----- For more information about fNRB see [1]_. References ---------- .. [1] R. Bailis, R. Drigo, A. Ghilardi, O. Masera, The carbon footprint of traditional woodfuels, Nature Clim Change. 5 (2015) 266–272. https://doi.org/10.1038/nclimate2491. """ intensity = self['co2_intensity'] self['co2_intensity'] *= model.specs['fnrb'] super().get_carbon_intensity(model) self['co2_intensity'] = intensity
[docs] def solar_panel_investment(self, model: 'onstove.OnStove'): """This method adds the cost of a solar panel to unelectrified areas. The stove can be modelled as ICS with natural draft or forced draft. This is achieved by specifying the ``draft_type`` attribute of the class. If forced draft is used, then the class will consider an extra capital cost for a standard 6 watt solar panel to run the fan in currently unelectrified areas. The cost used for the panel is 1.25 USD per watt. Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. """ if not self.solar_panel_adjusted: solar_panel_cost = 7.5 # Based on a cost of 1.25 USD per watt is_electrified = model.gdf['Elec_pop_calib'] > 0 inv_cost = pd.Series(np.ones(model.gdf.shape[0]) * self.inv_cost, index=model.gdf.index) inv_cost[~is_electrified] += solar_panel_cost self.inv_cost = inv_cost self.solar_panel_adjusted = True # This is to prevent to adjust the capital cost more than once
[docs] def discounted_inv(self, model: 'onstove.OnStove', relative: bool = True): """This method expands :meth:`Technology.discounted_inv` by adding the solar panel cost in unlectrified areas. Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. relative: bool, default True Boolean parameter to indicate if the discounted investments will be calculated relative to the `base_fuel` or not. See also -------- solar_panel_investment """ if self.draft_type.lower().replace('_', ' ') in ['forced', 'forced draft']: self.solar_panel_investment(model) super().discounted_inv(model, relative=relative)
[docs]class Charcoal(Technology): """Charcoal technology class used to model traditional and improved stoves. This class inherits the standard :class:`Technology` class and is used to model traditional and Improved Cook Stoves (ICS) using charcoal as fuel. Parameters ---------- name: str, optional. Name of the technology to model. carbon_intensity: float, optional The CO2 equivalent emissions in kg/GJ of burned fuel. If this attribute is used, then none of the gas-specific intensities will be used (e.g. ch4_intensity). co2_intensity: float, default 121 The CO2 emissions in kg/GJ of burned fuel. ch4_intensity: float, default 0.576 The CH4 emissions in kg/GJ of burned fuel. n2o_intensity: float, default 0.001 The N2O emissions in kg/GJ of burned fuel. co_intensity: float, default 0 The CO emissions in kg/GJ of burned fuel. bc_intensity: float, default 0.1075 The black carbon emissions in kg/GJ of burned fuel. oc_intensity: float, default 0.308 The organic carbon emissions in kg/GJ of burned fuel. energy_content: float, default 30 Energy content of the fuel in MJ/kg. tech_life: int, default 2 Stove life in years. inv_cost: float, default 4 Investment cost of the stove in USD. fuel_cost: float, default 0.09 Fuel cost in USD/kg if any. time_of_cooking: float, default 2.6 Daily average time spent for cooking with this stove in hours. om_cost: float, default 3.7 Operation and maintenance cost in USD/year. efficiency: float, default 0.2 Efficiency of the stove. pm25: float, default 256 Particulate Matter emissions (PM25) in mg/kg of fuel. """ def __init__(self, name: Optional[str] = None, carbon_intensity: Optional[float] = None, co2_intensity: float = 121, ch4_intensity: float = 0.576, n2o_intensity: float = 0.001, co_intensity: float = 0, bc_intensity: float = 0.1075, oc_intensity: float = 0.308, energy_content: float = 30, tech_life: float = 2, # in years inv_cost: int = 4, # in USD fuel_cost: float = 0.09, time_of_cooking: float = 2.6, om_cost: float = 3.7, efficiency: float = 0.2, # ratio pm25: float = 256): super().__init__(name, carbon_intensity, co2_intensity, ch4_intensity, n2o_intensity, co_intensity, bc_intensity, oc_intensity, energy_content, tech_life, inv_cost, fuel_cost, time_of_cooking, om_cost, efficiency, pm25, is_clean=False)
[docs] def get_carbon_intensity(self, model: 'onstove.OnStove'): """This method expands :meth:`Technology.get_carbon_intensity`. It excludes the CO2 emissions from the share of firewood that is sustainably harvested (i.e. it does not affect other emissions such as CH4) by using the fraction of Non-Renewable Biomass (fNRB). Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. Notes ----- For more information about fNRB see [1]_. References ---------- .. [1] R. Bailis, R. Drigo, A. Ghilardi, O. Masera, The carbon footprint of traditional woodfuels, Nature Clim Change. 5 (2015) 266–272. https://doi.org/10.1038/nclimate2491. """ intensity = self['co2_intensity'] self['co2_intensity'] *= model.specs['fnrb'] super().get_carbon_intensity(model) self['co2_intensity'] = intensity
[docs] def production_emissions(self, model: 'onstove.OnStove'): """Calculates the emissions caused by the production of Charcoal. The function uses emission factors in regards to CO2, CO, CH4, BC and OC as well as the ``energy`` and ``energy_content`` attributes of te model. Emissions factors for the production of charcoal are taken from [1]_. References ---------- .. [1] Akagi, S. K., Yokelson, R. J., Wiedinmyer, C., Alvarado, M. J., Reid, J. S., Karl, T., Crounse, J. D., & Wennberg, P. O. (2010). Emission factors for open and domestic biomass burning for use in atmospheric models. Atmospheric Chemistry and Physics Discussions. 10: 27523–27602., 27523–27602. https://www.fs.usda.gov/treesearch/pubs/39297 Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. Returns ------- The total charcoal production emissions that can be associated with each household measured in kg of CO2-eq per year. """ emission_factors = {'co2': 1626, 'co': 255, 'ch4': 39.6, 'bc': 0.02, 'oc': 0.74} # g/kg_Charcoal # Charcoal produced (kg/yr). Energy required (MJ/yr)/Charcoal energy content (MJ/kg) kg_yr = self.energy / self.energy_content hh_emissions = sum([ef * model.gwp[pollutant] * kg_yr for pollutant, ef in emission_factors.items()]) # gCO2eq/yr return hh_emissions / 1000 # kgCO2/yr
[docs] def carb(self, model: 'onstove.OnStove'): """This method expands :meth:`Technology.carbon` when Charcoal is the fuel used (both traditional stoves and ICS) to ensure that the emissions caused by the production and transportation is included in the total emissions. Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. See also -------- carbon """ super().carb(model) self.carbon += self.production_emissions(model)
[docs]class Electricity(Technology): """Electricity technology class used to model electrical stoves. This class inherits the standard :class:`Technology` class and is used to model electrical stoves. Attributes ---------- carbon_intensities: dict Carbon intensities of the different power plants used in the electricity generation mix. grid_capacity_costs: dict Costs of adding capacity of the different power plants used in the generation mix. grid_techs_life: dict Technology life of the different power plants used in the generation mix. Parameters ---------- name: str, optional Name of the technology to model. carbon_intensity: float, optional The CO2 equivalent emissions in kg/GJ of burned fuel. If this attribute is used, then none of the gas-specific intensities will be used (e.g. ch4_intensity). energy_content: float, default 3.6 Energy content in MJ/kWh. tech_life: int, default 10 Stove life in years. connection_cost: float, defualt 0 Cost of strengthening a household connection to enable electrical cooking. grid_capacity_cost: float, optional Cost of added capacity to the grid (USD/kW) inv_cost: float, default 36.3 Investment cost of the stove in USD. fuel_cost: float, default 0.1 Fuel cost in USD/kWh if any. time_of_cooking: float, default 1.8 Daily average time spent for cooking with this stove in hours. om_cost: float, default 3.7 Operation and maintenance cost in USD/year. efficiency: float, default 0.85 Efficiency of the stove. pm25: float, default 32 Particulate Matter emissions (PM25) in mg/kg of fuel. """ def __init__(self, name: Optional[str] = None, carbon_intensity: Optional[str] = None, energy_content: float = 3.6, tech_life: int = 10, # in years inv_cost: float = 36.3, # in USD connection_cost: float = 0, # cost of additional infrastructure grid_capacity_cost: float = None, fuel_cost: float = 0.1, time_of_cooking: float = 1.8, om_cost: float = 3.7, # percentage of investement cost efficiency: float = 0.85, # ratio pm25: float = 32): super().__init__(name, carbon_intensity, None, None, None, None, None, None, energy_content, tech_life, inv_cost, fuel_cost, time_of_cooking, om_cost, efficiency, pm25, is_clean=True) # Carbon intensity of fossil fuel plants in kg/GWh self.generation = {} self.capacities = {} self.grid_capacity_cost = grid_capacity_cost self.tiers_path = None self.connection_cost = connection_cost self.carbon_intensities = {'coal': 0.090374363, 'natural_gas': 0.050300655, 'crude_oil': 0.070650288, 'heavy_fuel_oil': 0.074687989, 'oil': 0.072669139, 'diesel': 0.069332823, 'still_gas': 0.060849859, 'flared_natural_gas': 0.051855075, 'waste': 0.010736111, 'biofuels_and_waste': 0.010736111, 'nuclear': 0, 'hydro': 0, 'wind': 0, 'solar': 0, 'other': 0, 'geothermal': 0} # TODO: make this general, with other fuel mix this crash self.grid_capacity_costs = {'oil': 1467, 'natural_gas': 550, 'biofuels_and_waste': 2117, 'nuclear': 4000, 'hydro': 2100, 'coal': 1600, 'wind': 1925, 'solar': 1400, 'geothermal': 2917} self.grid_techs_life = {'oil': 40, 'natural_gas': 30, 'biofuels_and_waste': 25, 'nuclear': 50, 'hydro': 60, 'coal': 40, 'wind': 22, 'solar': 25, 'geothermal': 30} def __setitem__(self, idx, value): if 'generation' in idx: self.generation[idx.lower().replace('generation_', '')] = value elif 'grid_capacity_cost' in idx: self.grid_capacity_cost = value elif 'capacity' in idx: self.capacities[idx.lower().replace('capacity_', '')] = value elif 'carbon_intensity' == idx: self.carbon_intensity = value elif 'carbon_intensity' in idx: self.carbon_intensities[idx.lower().replace('carbon_intensity_', '')] = value elif 'connection_cost' in idx: self.connection_cost = value elif 'grid_cap_life' in idx: self.grid_cap_life = value else: super().__setitem__(idx, value)
[docs] def get_capacity_cost(self, model: 'onstove.OnStove'): """This method determines the cost of electricity for each added unit of capacity (kW). The added capacity is assumed to be the same shares as the current installed capacity (i.e. if a country uses 10% coal powered power plants and 90% natural gas, the added capacity will consist of 10% coal and 90% natural gas). The function does not return anything but assigns capacity to the `capacity` of the Electricity class and the capacity cost to the `capacity_cost` of the Electricity class. Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. """ self.required_energy(model) if self.tiers_path is None: add_capacity = 1 else: model.raster_to_dataframe(self.tiers_path, name='Electricity_tiers', method='sample') self.tiers = model.gdf['Electricity_tiers'].copy() add_capacity = (self.tiers < 3) if self.grid_capacity_cost is None: self.get_grid_capacity_cost() salvage = self.grid_salvage(model) else: salvage = self.grid_salvage(model, True) self.capacity = self.energy * add_capacity / (3.6 * self.time_of_cooking * 365) self.capacity_cost = self.capacity * (self.grid_capacity_cost - salvage)
[docs] def get_carbon_intensity(self, model: 'onstove.OnStove'): """This function determines the carbon intensity of generated electricity based on the power plant mix in the area of interest. The function does not return anything but assigns carbon intensity to the `carbon_intensity` of the Electricity class. Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. """ grid_emissions = sum([gen * self.carbon_intensities[fuel] for fuel, gen in self.generation.items()]) grid_generation = sum(self.generation.values()) self.carbon_intensity = grid_emissions / grid_generation * 1000 # to convert from Mton/PJ to kg/GJ
[docs] def get_grid_capacity_cost(self): """This function determines the grid capacity cost in the area of interest and assigns it to the `grid_capacity_cost` attribute of the Electricity class.""" self.grid_capacity_cost = sum( [self.grid_capacity_costs[fuel] * (cap / sum(self.capacities.values())) for fuel, cap in self.capacities.items()])
[docs] def grid_salvage(self, model: 'onstove.OnStove', single: bool = False): """This method determines the salvage cost of the grid connected power plants. Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. single: bool, default True Boolean parameter to indicate if there is only one grid_capacity_cost or several. Returns ------- The discounted salvage cost of the grid connected powerplants. """ discount_rate, proj_life = self.discount_factor(model.specs) if single: used_life = proj_life % self.grid_cap_life salvage = self.grid_capacity_cost * (1 - used_life / self.grid_cap_life) else: salvage_values = [] for tech, cap in self.capacities.items(): used_life = proj_life % self.grid_techs_life[tech] salvage = self.grid_capacity_costs[tech] * (1 - used_life / self.grid_techs_life[tech]) salvage_values.append(salvage * cap / sum(self.capacities.values())) salvage = sum(salvage_values) # TODO: vectorize this return salvage / discount_rate[0]
[docs] def carb(self, model: 'onstove.OnStove'): """This method expands :meth:`Technology.carbon` when electricity is the fuel used Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. See also -------- carbon """ if self.carbon_intensity is None: self.get_carbon_intensity(model) super().carb(model)
[docs] def discounted_inv(self, model: 'onstove.OnStove', relative: bool = True): """This method expands :meth:`Technology.discounted_inv` by adding connection and added capacity costs. Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. relative: bool, default True Boolean parameter to indicate if the discounted investments will be calculated relative to the `base_fuel` or not. See also -------- get_capacity_cost """ # TODO: this method needs update on the current shares based on the new calibration methods super().discounted_inv(model, relative=relative) if relative: share = (model.gdf['IsUrban'] > 20) * self.current_share_urban share[model.gdf['IsUrban'] < 20] *= self.current_share_rural self.discounted_investments += (self.connection_cost + self.capacity_cost * (1 - share))
[docs] def net_benefit(self, model: 'onstove.OnStove', w_health: int = 1, w_spillovers: int = 1, w_environment: int = 1, w_time: int = 1, w_costs: int = 1): """This method expands :meth:`Technology.net_benefit` by taking into account electricity availability in the calculations. Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. w_health: int, default 1 Determines the weight of the health parameters (reduced morbidity and mortality) in the net-benefit equation. w_spillovers: int, default 1 Determines the weight of the spillover effects from cooking with traditional fuels in the net-benefit equation. w_environment: int, default 1 Determines the weight of the environmental effects (reduced emissions) in the net-benefit equation. w_time: int, default 1 Determines the weight of the opportunity cost (reduced time spent) in the net-benefit equation. w_costs: int, default 1 Determines the weight of the costs in the net-benefit equation. See also -------- net_benefit """ super().net_benefit(model, w_health, w_spillovers, w_environment, w_time, w_costs) model.gdf.loc[model.gdf['Current_elec'] == 0, "net_benefit_{}".format(self.name)] = np.nan self.net_benefits.loc[model.gdf['Current_elec'] == 0] = np.nan factor = model.gdf['Elec_pop_calib'] / model.gdf['Calibrated_pop'] factor[factor > 1] = 1 self.factor = factor self.households = model.gdf['Households'] * factor
class MiniGrids(Electricity): """Mini-grids technology class used to model electrical stoves powered by mini-grids. This class inherits and modifies the :class:`Electricity` class. Attributes ---------- coverage distance Parameters ---------- name: str, optional Name of the technology to model. carbon_intensity: float, optional The CO2 equivalent emissions in kg/GJ of burned fuel. If this attribute is used, then none of the gas-specific intensities will be used (e.g. ch4_intensity). energy_content: float, default 3.6 Energy content in MJ/kWh. tech_life: int, default 10 Stove life in years. connection_cost: float, defualt 0 Cost of strengthening a household connection to enable electrical cooking. grid_capacity_cost: float, optional Cost of added capacity to the grid (USD/kW) inv_cost: float, default 36.3 Investment cost of the stove in USD. fuel_cost: float, default 0.1 Fuel cost in USD/kWh if any. time_of_cooking: float, default 1.8 Daily average time spent for cooking with this stove in hours. om_cost: float, default 3.7 Operation and maintenance cost in USD/year. efficiency: float, default 0.85 Efficiency of the stove. pm25: float, default 32 Particulate Matter emissions (PM25) in mg/kg of fuel. stove_power: float, default 0.7 The power of the stove in kW. capacity_factor: float, default 0.8 Capacity factor of the mini-grid. base_load: float, default 0.1 Base load of the mini-grid. w_pop: float, default 0 Weight of population count when calibrating access to mini-grids. w_ntl: float, default 0 Weight of nighttime lights intensity when calibrating access to mini-grids. w_mg_dist: float, default 1 Weight of the distance to the closest mini-grid when calibrating access to mini-grids """ def __init__(self, name: Optional[str] = None, carbon_intensity: Optional[str] = None, energy_content: float = 3.6, tech_life: int = 10, # in years inv_cost: float = 36.3, # in USD connection_cost: float = 0, # cost of additional infrastructure grid_capacity_cost: float = None, fuel_cost: float = 0.1, time_of_cooking: float = 2.5, om_cost: float = 3.7, efficiency: float = 0.85, # ratio pm25: float = 32, stove_power: float = 0.7, capacity_factor: float = 0.8, base_load: float = 0.1, # in kW w_pop: float = 0, # weight of population count to calibrate access to minigrids w_ntl: float = 0, # weight of nighttime lights to calibrate access to minigrids w_mg_dist: float = 1, # weight of distance to calibrate access to minigrids ): super().__init__(name=name, carbon_intensity=carbon_intensity, energy_content=energy_content, tech_life=tech_life, inv_cost=inv_cost, connection_cost=connection_cost, grid_capacity_cost=grid_capacity_cost, fuel_cost=fuel_cost, time_of_cooking=time_of_cooking, om_cost=om_cost, efficiency=efficiency, pm25=pm25) self.coverage = None self.distance = None self.capacity_factor = capacity_factor self.stove_power = stove_power self.base_load = base_load self.w_pop = w_pop self.w_ntl = w_ntl self.w_mg_dist = w_mg_dist @property def coverage(self) -> RasterLayer: """:class:`VectorLayer` object containing a vector dataset showing the areas of coverage of the mini-grids. This layer must contain the following columns: * `capacity`: installed capacity of the mini-grids * `households`: amount of households served by the mini-grids * `geometry`: polygons showing areas of coverage .. seealso:: :meth:`calculate_potential` """ return self._coverage @coverage.setter def coverage(self, layer): self._coverage = vector_setter(layer) @property def distance(self) -> RasterLayer: """:class:`RasterLayer` object containing a raster dataset with the distance to mini-grids in km. .. seealso:: :meth:`calculate_potential` """ return self._distance @distance.setter def distance(self, layer): self._distance = raster_setter(layer) @property def ntl(self) -> RasterLayer: """:class:`RasterLayer` object containing raster dataset showing the nighttime lights data. .. seealso:: :meth:`calculate_potential` """ return self._ntl @ntl.setter def ntl(self, layer): self._ntl = raster_setter(layer) def calculate_potential(self, model): # TODO: Expand here. """Calculates the potential of each mini-grid for supporting eCooking in each area. """ leftover = np.maximum(self.capacity_factor * self.coverage.data['capacity'] - self.coverage.data['households'] * self.base_load, 0) self.coverage.data['potential_hh'] = leftover / self.stove_power # in number of households self.gdf = model.gdf[['geometry']].sjoin(self.coverage.data, how='left') self.gdf['mg_dist'] = model.raster_to_dataframe(self.distance, method='read') self.gdf['ntl'] = model.raster_to_dataframe(self.ntl, method='read') pop_norm = model.normalize('Calibrated_pop') ntl_norm = self.normalize('ntl') mg_dist_norm = self.normalize('mg_dist', inverse=True) weights = self.w_pop + self.w_ntl + self.w_mg_dist weight_sum = (self.w_pop * pop_norm + self.w_ntl * ntl_norm + self.w_mg_dist * mg_dist_norm) / weights self.gdf['mg_acces_weight'] = weight_sum self.gdf["current_mg_elec"] = 0 self.gdf['supported_hh'] = 0 self.gdf['supported_pop'] = 0 for municipality, group in self.gdf.groupby('municipality'): hh_access = group['potential_hh'].mean() weights = sorted(group['mg_acces_weight'], reverse=True) elec_hh = 0 i = 0 while elec_hh < hh_access: muni_vec = (self.gdf['municipality'] == municipality) not_grid = (model.gdf['Current_elec'] == 0) bool_vec = muni_vec & not_grid & (self.gdf['mg_acces_weight'] >= weights[i]) elec_hh = model.gdf.loc[bool_vec, "Households"].sum() self.gdf.loc[bool_vec, 'supported_hh'] = model.gdf.loc[bool_vec, "Households"] self.gdf.loc[bool_vec, 'current_mg_elec'] = 1 self.gdf.loc[bool_vec, 'supported_pop'] = model.gdf.loc[bool_vec, "Calibrated_pop"] i += 1 if i == len(weights): break def discounted_inv(self, model: 'onstove.OnStove', relative: bool = True): """This method expands :meth:`Electricity.discounted_inv` by adding connection costs. Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. relative: bool, default True Boolean parameter to indicate if the discounted investments will be calculated relative to the `base_fuel` or not. See also -------- get_capacity_cost """ super(Electricity, self).discounted_inv(model, relative=relative) self.discounted_investments += self.connection_cost def net_benefit(self, model: 'onstove.OnStove', w_health: int = 1, w_spillovers: int = 1, w_environment: int = 1, w_time: int = 1, w_costs: int = 1): """This method modifies :meth:`Electricity.net_benefit` for the mini-grid class Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. w_health: int, default 1 Determines the weight of the health parameters (reduced morbidity and mortality) in the net-benefit equation. w_spillovers: int, default 1 Determines the weight of the spillover effects from cooking with traditional fuels in the net-benefit equation. w_environment: int, default 1 Determines the weight of the environmental effects (reduced emissions) in the net-benefit equation. w_time: int, default 1 Determines the weight of the opportunity cost (reduced time spent) in the net-benefit equation. w_costs: int, default 1 Determines the weight of the costs in the net-benefit equation. See also -------- net_benefit """ super(Electricity, self).net_benefit(model, w_health, w_spillovers, w_environment, w_time, w_costs) self.calculate_potential(model) self.households = self.gdf['supported_hh'] model.gdf.loc[self.households == 0, "net_benefit_{}".format(self.name)] = np.nan self.net_benefits.loc[self.households == 0] = np.nan # factor = self.gdf['Elec_pop_calib'] / self.gdf['Calibrated_pop'] factor = np.ones(self.households.shape[0]) factor[self.households == 0] = 0 self.factor = factor
[docs]class Biogas(Technology): """Biogas technology class used to model biogas fueled stoves. This class inherits the standard :class:`Technology` class and is used to model stoves using biogas as fuel. Biogas stoves are assumed to not be available in urban settlements as the collection of manure is assumed to be limited. If the fuel is assumed to be purchased changes can be made to the function called ``available_biogas``. Biogas is also assumed to be restricted based on temperature (an average yearly temperature below 10 degrees Celsius is assumed to lead to heavy drops of efficiency [1]_). Biogas production is also assumed to be a very water intensive process [2]_, hence areas experiencing water stress are assumed restricted as well. References ---------- .. [1] Lohani, S. P., Dhungana, B., Horn, H. & Khatiwada, D. Small-scale biogas technology and clean cooking fuel: Assessing the potential and links with SDGs in low-income countries – A case study of Nepal. Sustainable Energy Technologies and Assessments 46, 101301 (2021). .. [2] Bansal, V., Tumwesige, V. & Smith, J. U. Water for small-scale biogas digesters in sub-Saharan Africa. GCB Bioenergy 9, 339–357 (2017). Parameters ---------- name: str, optional. Name of the technology to model. carbon_intensity: float, optional The CO2 equivalent emissions in kg/GJ of burned fuel. If this attribute is used, then none of the gas-specific intensities will be used (e.g. ch4_intensity). co2_intensity: float, default 0 The CO2 emissions in kg/GJ of burned fuel. ch4_intensity: float, default 0.029 The CH4 emissions in kg/GJ of burned fuel. n2o_intensity: float, default 0.0006 The N2O emissions in kg/GJ of burned fuel. co_intensity: float, default 0 The CO emissions in kg/GJ of burned fuel. bc_intensity: float, default 0.0043 The black carbon emissions in kg/GJ of burned fuel. oc_intensity: float, default 0.0091 The organic carbon emissions in kg/GJ of burned fuel. energy_content: float, default 22.8 Energy content of the fuel in MJ/m3. tech_life: int, default 20 Stove life in year. inv_cost: float, default 550 Investment cost of the stove in USD. fuel_cost: float, default 0 Fuel cost in USD/kg if any. time_of_cooking: float, default 2 Daily average time spent for cooking with this stove in hours. manure_feed_time: float, default 3 Time spent collecting and feeding manure to the digester (excluding travel time) in hours. om_cost: float, default 3.7 Operation and maintenance cost in USD/year. efficiency: float, default 0.4 Efficiency of the stove. pm25: float, default 43 Particulate Matter emissions (PM25) in mg/kg of fuel. digester_eff: float, default 0.4 Efficiency of the digestor. friction_path: str, optional Path to the friction raster file describing the time needed (in minutes) to travel one meter within each cell. """ def __init__(self, name: Optional[str] = None, carbon_intensity: Optional[float] = None, co2_intensity: float = 0, ch4_intensity: float = 0.029, n2o_intensity: float = 0.0006, co_intensity: float = 0, bc_intensity: float = 0.0043, oc_intensity: float = 0.0091, energy_content: float = 22.8, tech_life: int = 20, # in years inv_cost: float = 550, # in USD fuel_cost: float = 0, time_of_cooking: float = 2, manure_feed_time: float = 0.5, om_cost: float = 3.7, efficiency: float = 0.4, # ratio pm25: float = 43, digester_eff: float = 0.4, friction_path: Optional[str] = None): super().__init__(name, carbon_intensity, co2_intensity, ch4_intensity, n2o_intensity, co_intensity, bc_intensity, oc_intensity, energy_content, tech_life, inv_cost, fuel_cost, time_of_cooking, om_cost, efficiency, pm25, is_clean=True) self.digester_eff = digester_eff self.friction_path = friction_path self.manure_feed_time = manure_feed_time self.time_of_collection = None self.water = None self.temperature = None
[docs] def read_friction(self, model: 'onstove.OnStove', friction_path: str): """Reads a friction layer in min per meter (walking time per meter) and returns a pandas series with the values for each populated grid cell in hours per meter Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. friction_path: str Path to where the friction layer is stored. Returns ------- A pandas series with the values for each populated grid cell in hours per meter """ friction = RasterLayer(self.name, 'Friction', path=friction_path, resample='average') data = model.raster_to_dataframe(friction, fill_nodata_method='interpolate', method='read') return data / 60
[docs] def required_energy_hh(self, model: 'onstove.OnStove'): """Determines the required annual energy needed for cooking taking into account the stove efficiency. Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. Returns ------- Required annual energy needed for cooking """ self.required_energy(model) return self.energy / self.digester_eff
[docs] def get_collection_time(self, model: 'onstove.OnStove'): """Calculates the daily time of collection based on friction (hour/meter), the available biogas energy from each cell (MJ/yr/meter, 1000000 represents meters per km2) and the required energy per household (MJ/yr). The function does not return anything but saves the time of collection in the `time_of_collection` of the Biogas class. Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. """ self.available_biogas(model) required_energy_hh = self.required_energy_hh(model) friction = self.read_friction(model, self.friction_path) time_of_collection = required_energy_hh * friction / (model.gdf["biogas_energy"] / 1000000) / 365 time_of_collection[time_of_collection == float('inf')] = np.nan self.time_of_collection = time_of_collection
[docs] def available_biogas(self, model: 'onstove.OnStove'): """Calculates the biogas production potential in liters per day. It currently takes into account 6 categories of livestock (cattle, buffalo, sheep, goat, pig and poultry). The biogas potential for each category is determined following the methodology outlined by Lohani et al.[1]_ This function also applies a restriction to biogas production in urban areas, areas with temperature lower than 10 degrees[1]_ celsius and areas experiencing water stress[2]_. The function does not return anything but creates a column in the main dataframe for total biogas energy available in every settlement. References ---------- .. [1] Lohani, S. P., Dhungana, B., Horn, H. & Khatiwada, D. Small-scale biogas technology and clean cooking fuel: Assessing the potential and links with SDGs in low-income countries – A case study of Nepal. Sustainable Energy Technologies and Assessments 46, 101301 (2021). .. [2] Bansal, V., Tumwesige, V. & Smith, J. U. Water for small-scale biogas digesters in sub-Saharan Africa. GCB Bioenergy 9, 339–357 (2017). Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. """ from_cattle = model.gdf["Cattles"] * 12 * 0.15 * 0.8 * 305 from_buffalo = model.gdf["Buffaloes"] * 14 * 0.2 * 0.75 * 305 from_sheep = model.gdf["Sheeps"] * 0.7 * 0.25 * 0.8 * 452 from_goat = model.gdf["Goats"] * 0.6 * 0.3 * 0.85 * 450 from_pig = model.gdf["Pigs"] * 5 * 0.75 * 0.14 * 470 from_poultry = model.gdf["Poultry"] * 0.12 * 0.25 * 0.75 * 450 model.gdf["available_biogas"] = ((from_cattle + from_buffalo + from_goat + from_pig + from_poultry + from_sheep) * self.digester_eff / 1000) * 365 # Temperature restriction if self.temperature is not None: if isinstance(self.temperature, str): self.temperature = RasterLayer('Biogas', 'Temperature', self.temperature) model.raster_to_dataframe(self.temperature, name="Temperature", method='read', fill_nodata_method='interpolate') model.gdf.loc[model.gdf["Temperature"] < 10, "available_biogas"] = 0 model.gdf.loc[(model.gdf["IsUrban"] > 20), "available_biogas"] = 0 # Water availability restriction if self.water is not None: if isinstance(self.water, str): self.water = VectorLayer('Biogas', 'Water scarcity', self.water, bbox=model.mask_layer.data) model.raster_to_dataframe(self.water, name="Water", fill_nodata_method='interpolate', method='read') model.gdf.loc[model.gdf["Water"] == 0, "available_biogas"] = 0 # Available biogas energy per year in MJ (energy content in MJ/m3) model.gdf["biogas_energy"] = model.gdf["available_biogas"] * self.energy_content
[docs] def recalibrate_livestock(self, model: 'onstove.OnStove', buffaloes: str, cattles: str, poultry: str, goats: str, pigs: str, sheeps: str): """Recalibrates the livestock maps and adds them to the main dataframe. It currently takes into account 6 categories of livestock (cattle, buffalo, sheep, goat, pig and poultry). Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. buffaloes: str Path to the buffalo dataset. cattles: str Path to the cattle dataset. poultry: str Path to the poultry dataset. goats: str Path to the goat dataset. pigs: str Path to the pig dataset. sheeps: str Path to the sheep dataset. """ paths = { 'Buffaloes': buffaloes, 'Cattles': cattles, 'Poultry': poultry, 'Goats': goats, 'Pigs': pigs, 'Sheeps': sheeps} for name, path in paths.items(): layer = RasterLayer('Livestock', name, path=path) model.raster_to_dataframe(layer, name=name, method='read', fill_nodata_method='interpolate')
[docs] def total_time(self, model: 'onstove.OnStove'): """This method expands :meth:`Technology.total_time` by adding the biogas collection time Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. See also -------- total_time """ self.get_collection_time(model) super().total_time(model) self.total_time_yr += (self.manure_feed_time * 365)
[docs] def net_benefit(self, model: 'onstove.OnStove', w_health: int = 1, w_spillovers: int = 1, w_environment: int = 1, w_time: int = 1, w_costs: int = 1): """This method expands :meth:`Technology.net_benefit` by taking into account biogas availability in the calculations. Parameters ---------- model: OnStove model Instance of the OnStove model containing the main data of the study case. See :class:`onstove.OnStove`. w_health: int, default 1 Determines the weight of the health parameters (reduced morbidity and mortality) in the net-benefit equation. w_spillovers: int, default 1 Determines the weight of the spillover effects from cooking with traditional fuels in the net-benefit equation. w_environment: int, default 1 Determines the weight of the environmental effects (reduced emissions) in the net-benefit equation. w_time: int, default 1 Determines the weight of the opportunity cost (reduced time spent) in the net-benefit equation. w_costs: int, default 1 Determines the weight of the costs in the net-benefit equation. See also -------- net_benefit """ super().net_benefit(model, w_health, w_spillovers, w_environment, w_time, w_costs) required_energy_hh = self.required_energy_hh(model) model.gdf.loc[(model.gdf['biogas_energy'] < required_energy_hh), "benefits_{}".format(self.name)] = np.nan model.gdf.loc[(model.gdf['biogas_energy'] < required_energy_hh), "net_benefit_{}".format(self.name)] = np.nan self.net_benefits = model.gdf["benefits_{}".format(self.name)].copy() factor = model.gdf['biogas_energy'] / (required_energy_hh * model.gdf['Households']) factor[factor > 1] = 1 self.factor = factor self.households = model.gdf['Households'] * factor del model.gdf["Cattles"] del model.gdf["Buffaloes"] del model.gdf["Sheeps"] del model.gdf["Goats"] del model.gdf["Pigs"] del model.gdf["Poultry"]