diff --git a/src/energiapy/components/emission.py b/src/energiapy/components/emission.py new file mode 100644 index 00000000..960e078d --- /dev/null +++ b/src/energiapy/components/emission.py @@ -0,0 +1,112 @@ +"""Emission data class +""" + +__author__ = "Rahul Kakodkar" +__copyright__ = "Copyright 2022, Multi-parametric Optimization & Control Lab" +__credits__ = ["Rahul Kakodkar", "Marco De Sousa", "Betsie Montano Flores", "Efstratios N. Pistikopoulos"] +__license__ = "Open" +__version__ = "2.0.0" +__maintainer__ = "Rahul Kakodkar" +__email__ = "cacodcar@tamu.edu" +__status__ = "Production" + +from dataclasses import dataclass +from typing import Dict + +# from ..components.resource import Resource + + +# @dataclass +# class Material: +# """ +# Materials are needed to set up processes. They could result in the emission, toxicty, etc. + +# Args: +# name (str): name of the material, short ones are better to deal with +# resource_cons (Dict[Resource, float], optional): Resources consumed per unit basis of Material. Defaults to 0. +# basis (str, optional): Unit basis for material. Defaults to 'unit. +# gwp (float, optional): global warming potential per unit basis of Material produced. Defaults to 0. +# toxicity (float, optional): toxicity potential per unit basis of Material produced. Defaults to 0. +# citation (str, optional): Add citation. Defaults to 'citation needed'. +# label (str, optional): Longer descriptive label if required. Defaults to '' + +# Examples: +# Materials can be declared using the resources they consume + +# >>> Steel = Material(name='Steel', gwp=0.8, resource_cons = {H2O: 3.94}, toxicity=40, basis= 'kg', label='Steel') + +# """ + +# name: str +# resource_cons: Dict[Resource, float] = None +# gwp: float = None +# toxicity: float = None +# basis: str = 'unit' +# citation: str = 'citation needed' +# label: str = '' + +# def __repr__(self): +# return self.name + +# def __hash__(self): +# return hash(self.name) + +# def __eq__(self, other): +# return self.name == other.name + + +# from dataclasses import dataclass + +@dataclass +class Emission: + """Emisison are released when Resources are procured or Processes are run + + Args: + name (str): name of emission type + gwp20 (float): + """ + + name: str + # Global warming potential [kg CO2 eq.] + gwp20: float = None + gwp100: float = None + gwp500: float = None + # Global temperature potential [kg CO2 eq.] + gtp20: float = None + gtp100: float = None + gtp500: float = None + # Acidification potential [kg SO2eq.] + ap: float = None + # Eutrophication potential [CML assessment method: kg PO4eq. TRACI assessment method: kg N eq.] + ep: float = None + # Photochemical ozone creation potential [CML assessment method: kg C2H4 eq. TRACI assessment method: kg O3 eq.] + pocp: float = None + # Ozone depletion potential [CML assessment method: kg CFC-11 eq. TRACI assessment method: kg CFC-11 eq.] + odp: float = None + # Abiotic depletion potential for minerals and metals (non-fossil resources) [kg Sb eq.] + adpmm: float = None + # Abiotic depletion potential for fossil resources [MJ] + adpff: float = None + + def __repr__(self): + return self.name + + def __hash__(self): + return hash(self.name) + + def __eq__(self, other): + return self.name == other.name + +# # Create instances of the data class +# '''All global warming and global temperature potentials have been updated with the latest IPCC Annual Report 6''' +# CO2 = Emission(name = 'CO2', gwp20 = 1, gwp100 = 1, gwp500 = 1, gtp20 = 1, gtp100 = 1, gtp500 = 1, ap = 0, ep = 0, pocp = 0, odp = 0, adpmm = 0, adpff = 0) +# CH4 = Emission(name = 'CH4', gwp20 = 81.2, gwp100 = 27.9, gwp500 = 7.95) +# N20 = Emission(name = 'N2O', gwp20 = 273, gwp100 = 273, gwp500 = 130) + +# # Access attributes +# print(CO2.gwp100) +# print(CH4.gwp500) + +# # __repr__ method? +# print(N20) + diff --git a/src/energiapy/components/location.py b/src/energiapy/components/location.py index 9742e787..9eab6ce6 100644 --- a/src/energiapy/components/location.py +++ b/src/energiapy/components/location.py @@ -111,6 +111,7 @@ def __post_init__(self): # fetch all processes with failure rates set self.failure_processes = self.get_failure_processes() self.fail_factor = self.make_fail_factor() + self.emission_dict = {i: i.emission_dict for i in self.processes_full} if self.capacity_factor is not None: # fetch all processes with varying capacities self.varying_capacity = set(self.capacity_factor.keys()) diff --git a/src/energiapy/components/process.py b/src/energiapy/components/process.py index f3dd8eb0..0ffd4602 100644 --- a/src/energiapy/components/process.py +++ b/src/energiapy/components/process.py @@ -175,12 +175,13 @@ def __post_init__(self): cost_dynamics (CostDynamics): Determines whether the cost scales linearly with the unit capacity, or is a piecewise-linear function. """ - if self.varying is None: # if nothing is varying, set defaults to CERTAIN_X + if self.varying is None: # if nothing is varying, set defaults to CERTAIN_X self.varying = [] if (self.capex is not None) or (self.fopex is not None) or (self.vopex is not None): self.varying = self.varying + \ [VaryingProcess.CERTAIN_EXPENDITURE] - if isinstance(self.prod_max, dict): # if maximum production is dictionary, that means that the process uses multiple modes + # if maximum production is dictionary, that means that the process uses multiple modes + if isinstance(self.prod_max, dict): self.varying = self.varying + [VaryingProcess.MULTIMODE] else: if self.prod_max > 0: @@ -195,28 +196,30 @@ def __post_init__(self): if self.storage is not None: self.resource_storage = create_storage_resource( - process_name=self.name, resource=self.storage, store_max=self.store_max, store_min=self.store_min) # create a dummy resource if process is storage type. - self.conversion = {self.storage: -1, self.resource_storage: 1} # efficiency of input to storage is 100 percent - self.conversion_discharge = { - self.resource_storage: -1, self.storage: 1*(1 - self.storage_loss)} # the losses are all at the output (retrival) + process_name=self.name, resource=self.storage, store_max=self.store_max, store_min=self.store_min) # create a dummy resource if process is storage type. + # efficiency of input to storage is 100 percent + self.conversion = {self.storage: -1, self.resource_storage: 1} + self.conversion_discharge = { + self.resource_storage: -1, self.storage: 1*(1 - self.storage_loss)} # the losses are all at the output (retrival) self.processmode = ProcessMode.STORAGE else: - self.conversion_discharge = None + self.conversion_discharge = None self.resource_storage = None if isinstance(list(self.conversion.keys())[0], int): - self.processmode = ProcessMode.MULTI + self.processmode = ProcessMode.MULTI else: self.processmode = ProcessMode.SINGLE if isinstance(self.capex, (int, float)): self.cost_dynamics = CostDynamics.CONSTANT elif isinstance(self.capex, dict): - self.cost_dynamics = CostDynamics.PWL # Capex dictionaries are only provided for piece-wise linear cost functions + # Capex dictionaries are only provided for piece-wise linear cost functions + self.cost_dynamics = CostDynamics.PWL if self.processmode is ProcessMode.MULTI: self.resource_req = { - i.name for i in self.conversion[list(self.conversion.keys())[0]].keys()} # the required resources are drawn from the conversion dict, this includes stored resource + i.name for i in self.conversion[list(self.conversion.keys())[0]].keys()} # the required resources are drawn from the conversion dict, this includes stored resource else: self.resource_req = {i.name for i in self.conversion.keys()} @@ -226,9 +229,11 @@ def __post_init__(self): 'The keys for prod_max and conversion need to match if ProcessMode.multi') if self.cost_dynamics == CostDynamics.PWL: - self.capacity_segments = list(self.capex.keys()) + self.capacity_segments = list(self.capex.keys()) self.capex_segements = list(self.capex.values()) + self.emission_dict = {i: i.emissions for i in self.conversion.keys()} + def __repr__(self): return self.name diff --git a/src/energiapy/components/resource.py b/src/energiapy/components/resource.py index 2a01796f..629b512d 100644 --- a/src/energiapy/components/resource.py +++ b/src/energiapy/components/resource.py @@ -13,7 +13,8 @@ from dataclasses import dataclass from warnings import warn from enum import Enum, auto -from typing import Union, List, Tuple +from typing import Union, List, Tuple, Dict +from ..components.emission import Emission class VaryingResource(Enum): @@ -126,6 +127,7 @@ class Resource: label: str = '' gwp: float = 0 varying_bounds: Tuple[float] = (0, 1) + emissions: (Dict[Emission, float]) = None def __post_init__(self): if self.demand is True: diff --git a/src/energiapy/components/scenario.py b/src/energiapy/components/scenario.py index 3c106870..fc848954 100644 --- a/src/energiapy/components/scenario.py +++ b/src/energiapy/components/scenario.py @@ -207,6 +207,8 @@ def __post_init__(self): self.fail_factor = {i.name: i.fail_factor for i in self.location_set} self.credit_dict = {i.name: {j.name: i.credit[j] for j in i.credit.keys( )} for i in self.location_set if i.credit is not None} + self.emission_dict = { + i.name: {j.name: {l.name: m for l, m in k.items()} for j, k in i.emission_dict.items()} for i in self.location_set} self.process_resource_dict = { i.name: i.resource_req for i in self.process_set} # self.process_material_dict = {i.name: {j.name: i.material_cons[j] for j in i.material_cons.keys()} if i.material_cons is not None else None for i in diff --git a/src/energiapy/model/constraints/emission.py b/src/energiapy/model/constraints/emission.py index d83c083e..9f003be9 100644 --- a/src/energiapy/model/constraints/emission.py +++ b/src/energiapy/model/constraints/emission.py @@ -213,3 +213,25 @@ def global_warming_potential_network_bound_rule(instance, *scale_list): constraint_latex_render(global_warming_potential_network_bound_rule) return Constraint( *scales, rule=global_warming_potential_network_bound_rule, doc='global warming potential bound for the whole network') + + + +def constraint_global_warming_potential_20_resource(instance: ConcreteModel, emission_dict: dict, network_scale_level: int = 0) -> Constraint: + """calculates global warming potential for each process + + Args: + instance (ConcreteModel): pyomo model instance + process_gwp_dict (dict): _description_ + network_scale_level (int, optional): scale for network decisions. Defaults to 0. + + Returns: + Constraint: global_warming_potential_process + """ + scales = scale_list(instance=instance, scale_levels=network_scale_level+1) + + def global_warming_potential_process_rule(instance, location, process, resource, *scale_list): + return instance.global_warming_potential_20_resource[location, resource, scale_list] == emission_dict[location][process][resource]*instance.Cap_P[location, process, scale_list] + instance.constraint_global_warming_potential_process = Constraint( + instance.locations, instance.processes, *scales, rule=global_warming_potential_process_rule, doc='global warming potential for the each process') + constraint_latex_render(global_warming_potential_process_rule) + return instance.constraint_global_warming_potential_process