Source code for geophires_x.SBTEconomics

import sys, math
import numpy as np
import geophires_x.Model as Model
from .Economics import Economics, calculate_cost_of_one_vertical_well, BuildPTCModel, BuildPricingModel, \
    CalculateRevenue, CalculateFinancialPerformance, CalculateLCOELCOHLCOC
from .OptionList import Configuration, WellDrillingCostCorrelation, PlantType
from geophires_x.Parameter import floatParameter
from geophires_x.Units import *
from geophires_x.OptionList import WorkingFluid, EndUseOptions, EconomicModel


[docs] def calculate_cost_of_lateral_section(model: Model, length_m: float, well_correlation: int, lateral_drilling_cost_per_m: float, num_lateral_sections: int, fixed_well_cost_name: str, NonverticalsCased: bool, well_cost_adjustment_factor: float) -> float: """ calculate_cost_of_lateral_section calculates the cost of the lateral section of the well. Assume that the cost per meter for drilling of the lateral section is the same as the vertical section, except the casing cost is half, if it is uncased. :param model: The model object :type model: :class:`~geophires :param length_m: The depth of the well in meters :type length_m: float :param well_correlation: The well cost correlation :type well_correlation: int :param lateral_drilling_cost_per_m: The lateral drilling cost per meter in $/m :type lateral_drilling_cost_per_m: float :param num_lateral_sections: The number of lateral sections :type num_lateral_sections: int :param fixed_well_cost_name: The fixed well cost name :type fixed_well_cost_name: str :param NonverticalsCased: Are the laterals cased? :type NonverticalsCased: bool :param well_cost_adjustment_factor: The well cost adjustment factor :type well_cost_adjustment_factor: float :return: cost_of_one_well: The cost of the lateral section in MUSD :rtype: float """ # if we are drilling a vertical well, the lateral cost is 0 if model.wellbores.Configuration.value == Configuration.VERTICAL: return 0.0 # Check if well length is out of standard bounds for cost correlation length_per_section_m = length_m / num_lateral_sections correlations_min_valid_length_m = 500. correlations_max_valid_length_m = 8000. cost_of_lateral_section = 0.0 cost_per_section = 0.0 if length_per_section_m < correlations_min_valid_length_m and not well_correlation is WellDrillingCostCorrelation.SIMPLE: well_correlation = WellDrillingCostCorrelation.SIMPLE model.logger.warning( f'Invalid cost correlation specified ({well_correlation}) for per lateral section drilling length ' f'<{correlations_min_valid_length_m}m ({length_per_section_m}m). ' f'Falling back to simple user-specified cost ' f'({lateral_drilling_cost_per_m} per meter)' ) if length_per_section_m > correlations_max_valid_length_m and not well_correlation is WellDrillingCostCorrelation.SIMPLE: model.logger.warning( f'{well_correlation} may be invalid for per lateral section rilling length ' f'>{correlations_max_valid_length_m}m ({length_per_section_m}m). ' f'Consider using {WellDrillingCostCorrelation.SIMPLE} (per-meter cost) or ' f'{fixed_well_cost_name} (fixed cost per well) instead.' ) casing_factor = 1.0 if not NonverticalsCased: # assume that casing & cementing costs 50% of drilling costs casing_factor = 0.5 if model.economics.Nonvertical_drilling_cost_per_m.Provided or well_correlation is WellDrillingCostCorrelation.SIMPLE: cost_of_lateral_section = casing_factor * ((num_lateral_sections * lateral_drilling_cost_per_m * length_per_section_m)) * 1E-6 else: cost_per_section = well_correlation.calculate_cost_MUSD(length_per_section_m) cost_of_lateral_section = casing_factor * num_lateral_sections * cost_per_section # account for adjustment factor cost_of_lateral_section = well_cost_adjustment_factor * cost_of_lateral_section return cost_of_lateral_section
[docs] class SBTEconomics(Economics): """ SBTEconomics Child class of Economics; it is the same, but has advanced SBT closed-loop functionality """ def __init__(self, model: Model): """ The __init__ function is the constructor for a class. It is called whenever an instance of the class is created. The __init__ function can take arguments, but self is always the first one. Self refers to the instance of the object that has already been created, and it's used to access variables that belong to that object :param model: The container class of the application, giving access to everything else, including the logger :type model: Model :return: Nothing, and is used to initialize the class """ model.logger.info(f'Init {__class__!s}: {sys._getframe().f_code.co_name}') # Initialize the superclass first to gain access to those variables super().__init__(model) sclass = str(__class__).replace("<class \'", "") self.MyClass = sclass.replace("\'>", "") self.MyPath = os.path.abspath(__file__) #self.Electricity_rate = None #self.Discount_rate = None #self.error = 0 #self.AverageOPEX_Plant = 0 #self.OPEX_Plant = 0 #self.TotalCAPEX = 0 #self.CAPEX_Surface_Plant = 0 #self.CAPEX_Drilling = 0 # Set up all the Parameters that will be predefined by this class using the different types of parameter classes. # Setting up includes giving it a name, a default value, The Unit Type (length, volume, temperature, etc.) # and Unit Name of that value, sets it as required (or not), sets allowable range, # the error message if that range is exceeded, the ToolTip Text, and the name of the class that created it. # This includes setting up temporary variables that will be available to all the class but noy read in by user, # or used for Output # This also includes all Parameters that are calculated and then published using the Printouts function. # If you choose to subclass this master class, you can do so before or after you create your own parameters. # If you do, you can also choose to call this method from you class, which will effectively add # and set all these parameters to your class. # NB: inputs we already have ("already have it") need to be set at ReadParameter time so values # are set at the last possible time """ self.O_and_M_cost_plant = self.ParameterDict[self.O_and_M_cost_plant.Name] = floatParameter( "Operation & Maintenance Cost of Surface Plant", DefaultValue=0.015, Min=0.0, Max=0.2, UnitType=Units.PERCENT, PreferredUnits=PercentUnit.TENTH, CurrentUnits=PercentUnit.TENTH, Required=True, ErrMessage="assume default Operation & Maintenance cost of surface plant expressed as fraction of total surface plant capital cost (0.015)" ) self.Direct_use_heat_cost_per_kWth = self.ParameterDict[ self.Direct_use_heat_cost_per_kWth.Name] = floatParameter( "Capital Cost for Surface Plant for Direct-use System", DefaultValue=100.0, Min=0.0, Max=10000.0, UnitType=Units.ENERGYCOST, PreferredUnits=EnergyCostUnit.DOLLARSPERKW, CurrentUnits=EnergyCostUnit.DOLLARSPERKW, Required=False, ErrMessage="assume default Capital cost for surface plant for direct-use system (100 $/kWth)" ) self.Power_plant_cost_per_kWe = self.ParameterDict[self.Power_plant_cost_per_kWe.Name] = floatParameter( "Capital Cost for Power Plant for Electricity Generation", DefaultValue=3000.0, Min=0.0, Max=10000.0, UnitType=Units.ENERGYCOST, PreferredUnits=EnergyCostUnit.DOLLARSPERKW, CurrentUnits=EnergyCostUnit.DOLLARSPERKW, Required=True, ErrMessage="assume default Power plant capital cost per kWe (3000 USD/kWe)" ) # results are stored here and in the parent ProducedTemperature array """ model.logger.info(f'complete {__class__!s}: {sys._getframe().f_code.co_name}') def __str__(self): return 'SBTEconomics'
[docs] def read_parameters(self, model: Model) -> None: """ The read_parameters function reads in the parameters from a dictionary and stores them in the parameters. It also handles special cases that need to be handled after a value has been read in and checked. If you choose to subclass this master class, you can also choose to override this method (or not), and if you do :param model: The container class of the application, giving access to everything else, including the logger :type model: :class:`~geophires_x.Model.Model` :return: None """ model.logger.info(f'Init {__class__!s}: {sys._getframe().f_code.co_name}') super().read_parameters(model) # read the default parameters # if we call super, we don't need to deal with setting the parameters here, # just deal with the special cases for the variables in this class # because the call to the super.readparameters will set all the variables, # including the ones that are specific to this class # inputs we already have - needs to be set at ReadParameter time so values set at the latest possible time # self.Discount_rate = model.economics.discountrate.value # same units are GEOPHIRES # self.Electricity_rate = model.surfaceplant.electricity_cost_to_buy.value # same units are GEOPHIRES model.logger.info(f'complete {__class__!s}: {sys._getframe().f_code.co_name}')
[docs] def Calculate(self, model: Model) -> None: """ The Calculate function is where all the calculations are done. This function can be called multiple times, and will only recalculate what has changed each time it is called. This is where all the calculations are made using all the values that have been set. If you subclass this class, you can choose to run these calculations before (or after) your calculations, but that assumes you have set all the values that are required for these calculations If you choose to subclass this master class, you can also choose to override this method (or not), and if you do, do it before or after you call you own version of this method. If you do, you can also choose to call this method from you class, which can effectively run the calculations of the superclass, making all thr values available to your methods. but you had better have set all the parameters! :param model: The container class of the application, giving access to everything else, including the logger :type model: :class:`~geophires_x.Model.Model` :return: Nothing, but it does make calculations and set values in the model """ model.logger.info(f'Init {__class__!s}: {sys._getframe().f_code.co_name}') #if hasattr(model.wellbores, 'numnonverticalsections') and model.wellbores.numnonverticalsections.Provided: #self.cost_lateral_section.value = 0.0 # capital costs # well costs (using GeoVision drilling correlations). These are calculated whether totalcapcostvalid = 1 # start with the cost of one well # C1well is well drilling and completion cost in M$/well if self.per_production_well_cost.Valid: self.cost_one_production_well.value = self.per_production_well_cost.value if not self.per_injection_well_cost.Provided: self.cost_one_injection_well.value = self.per_production_well_cost.value else: self.cost_one_injection_well.value = self.per_injection_well_cost.value self.Cwell.value = ((self.cost_one_production_well.value * model.wellbores.nprod.value) + (self.cost_one_injection_well.value * model.wellbores.ninj.value)) else: # calculate the cost of one vertical production well # 1.05 for 5% indirect costs self.cost_one_production_well.value = 1.05 * calculate_cost_of_one_vertical_well(model, model.wellbores.vertical_section_length.value, self.wellcorrelation.value, self.Vertical_drilling_cost_per_m.value, self.per_production_well_cost.Name, self.production_well_cost_adjustment_factor.value) # If there is no injector well, then we assume we are doing a coaxial closed-loop. if model.wellbores.ninj.value == 0: self.cost_one_injection_well.value = 0.0 else: # Now calculate the cost of one vertical injection well # assume the cost of the injector and producer is the same self.cost_one_injection_well.value = self.cost_one_production_well.value if hasattr(model.wellbores, 'numnonverticalsections') and model.wellbores.numnonverticalsections.Provided: # now calculate the costs if we have a lateral section # 1.05 for 5% indirect costs self.cost_lateral_section.value = 1.05 * calculate_cost_of_lateral_section(model, model.wellbores.tot_lateral_m.value, self.wellcorrelation.value, self.Nonvertical_drilling_cost_per_m.value, model.wellbores.numnonverticalsections.value, self.per_injection_well_cost.Name, model.wellbores.NonverticalsCased.value, self.production_well_cost_adjustment_factor.value) # If it is an EavorLoop, we need to calculate the cost of the section of the well from # the bottom of the vertical to the junction with the laterals. # This section is not vertical, but it is cased, so we will estimate the cost # of this section as if it were a vertical section. if model.wellbores.Configuration.value == Configuration.EAVORLOOP: self.cost_to_junction_section.value = 1.05 * calculate_cost_of_one_vertical_well(model, model.wellbores.tot_to_junction_m.value, self.wellcorrelation.value, self.Vertical_drilling_cost_per_m.value, self.per_injection_well_cost.Name, self.injection_well_cost_adjustment_factor.value) else: self.cost_lateral_section.value = 0.0 self.cost_to_junction_section.value = 0.0 # cost of the well field self.Cwell.value = ((self.cost_one_production_well.value * model.wellbores.nprod.value) + (self.cost_one_injection_well.value * model.wellbores.ninj.value) + self.cost_lateral_section.value + self.cost_to_junction_section.value) # reservoir stimulation costs (M$/injection well). These are calculated whether totalcapcost.Valid = 1 if self.ccstimfixed.Valid: self.Cstim.value = self.ccstimfixed.value else: self.Cstim.value = 1.05 * 1.15 * self.ccstimadjfactor.value * model.wellbores.ninj.value * 1.25 # 1.15 for 15% contingency and 1.05 for 5% indirect costs # field gathering system costs (M$) if self.ccgathfixed.Valid: self.Cgath.value = self.ccgathfixed.value else: self.Cgath.value = self.ccgathadjfactor.value * 50 - 6 * np.max( model.surfaceplant.HeatExtracted.value) * 1000. # (GEOPHIRES v1 correlation) if model.wellbores.impedancemodelused.value: pumphp = np.max(model.wellbores.PumpingPower.value) * 1341 numberofpumps = np.ceil(pumphp / 2000) # pump can be maximum 2,000 hp if numberofpumps == 0: self.Cpumps = 0.0 else: pumphpcorrected = pumphp / numberofpumps self.Cpumps = numberofpumps * 1.5 * ( (1750 * pumphpcorrected ** 0.7) * 3 * pumphpcorrected ** (-0.11)) else: if model.wellbores.productionwellpumping.value: prodpumphp = np.max(model.wellbores.PumpingPowerProd.value) / model.wellbores.nprod.value * 1341 Cpumpsprod = model.wellbores.nprod.value * 1.5 * (1750 * prodpumphp ** 0.7 + 5750 * prodpumphp ** 0.2 + 10000 + np.max( model.wellbores.pumpdepth.value) * 50 * 3.281) # see page 46 in user's manual assuming rental of rig for 1 day. else: Cpumpsprod = 0 injpumphp = np.max(model.wellbores.PumpingPowerInj.value) * 1341 numberofinjpumps = np.ceil(injpumphp / 2000) # pump can be maximum 2,000 hp if numberofinjpumps == 0: Cpumpsinj = 0 else: injpumphpcorrected = injpumphp / numberofinjpumps Cpumpsinj = numberofinjpumps * 1.5 * ( 1750 * injpumphpcorrected ** 0.7) * 3 * injpumphpcorrected ** (-0.11) self.Cpumps = Cpumpsinj + Cpumpsprod # Based on GETEM 2016 #1.15 for 15% contingency and 1.12 for 12% indirect costs self.Cgath.value = 1.15 * self.ccgathadjfactor.value * 1.12 * ( (model.wellbores.nprod.value + model.wellbores.ninj.value) * 750 * 500. + self.Cpumps) / 1E6 # plant costs if (model.surfaceplant.enduse_option.value == EndUseOptions.HEAT and model.surfaceplant.plant_type.value not in [PlantType.ABSORPTION_CHILLER, PlantType.HEAT_PUMP, PlantType.DISTRICT_HEATING]): # direct-use if self.ccplantfixed.Valid: self.Cplant.value = self.ccplantfixed.value else: self.Cplant.value = 1.12 * 1.15 * self.ccplantadjfactor.value * 250E-6 * np.max( model.surfaceplant.HeatExtracted.value) * 1000. # 1.15 for 15% contingency and 1.12 for 12% indirect costs # absorption chiller elif model.surfaceplant.enduse_option.value == EndUseOptions.HEAT and model.surfaceplant.plant_type.value == PlantType.ABSORPTION_CHILLER: # absorption chiller if self.ccplantfixed.Valid: self.Cplant.value = self.ccplantfixed.value else: # this is for the direct-use part all the way up to the absorption chiller self.Cplant.value = 1.12 * 1.15 * self.ccplantadjfactor.value * 250E-6 * np.max( model.surfaceplant.HeatExtracted.value) * 1000. # 1.15 for 15% contingency and 1.12 for 12% indirect costs if self.chillercapex.value == -1: # no value provided by user, use built-in correlation ($2500/ton) self.chillercapex.value = 1.12 * 1.15 * np.max( model.surfaceplant.cooling_produced.value) * 1000 / 3.517 * 2500 / 1e6 # $2,500/ton of cooling. 1.15 for 15% contingency and 1.12 for 12% indirect costs # now add chiller cost to surface plant cost self.Cplant.value += self.chillercapex.value # heat pump elif model.surfaceplant.enduse_option.value == EndUseOptions.HEAT and model.surfaceplant.plant_type.value == PlantType.HEAT_PUMP: if self.ccplantfixed.Valid: self.Cplant.value = self.ccplantfixed.value else: # this is for the direct-use part all the way up to the heat pump self.Cplant.value = 1.12 * 1.15 * self.ccplantadjfactor.value * 250E-6 * np.max( model.surfaceplant.HeatExtracted.value) * 1000. # 1.15 for 15% contingency and 1.12 for 12% indirect costs if self.heatpumpcapex.value == -1: # no value provided by user, use built-in correlation ($150/kWth) self.heatpumpcapex.value = 1.12 * 1.15 * np.max( model.surfaceplant.HeatProduced.value) * 1000 * 150 / 1e6 # $150/kW. 1.15 for 15% contingency and 1.12 for 12% indirect costs # now add heat pump cost to surface plant cost self.Cplant.value += self.heatpumpcapex.value # district heating elif model.surfaceplant.enduse_option.value == EndUseOptions.HEAT and model.surfaceplant.plant_type.value == PlantType.DISTRICT_HEATING: if self.ccplantfixed.Valid: self.Cplant.value = self.ccplantfixed.value else: self.Cplant.value = 1.12 * 1.15 * self.ccplantadjfactor.value * 250E-6 * np.max( model.surfaceplant.HeatExtracted.value) * 1000. # 1.15 for 15% contingency and 1.12 for 12% indirect costs self.peakingboilercost.value = 65 * model.surfaceplant.max_peaking_boiler_demand.value / 1000 # add 65$/KW for peaking boiler self.Cplant.value += self.peakingboilercost.value # add peaking boiler cost to surface plant cost else: # all other options have power plant if model.surfaceplant.plant_type.value == PlantType.SUB_CRITICAL_ORC: MaxProducedTemperature = np.max(model.surfaceplant.TenteringPP.value) if MaxProducedTemperature < 150.: C3 = -1.458333E-3 C2 = 7.6875E-1 C1 = -1.347917E2 C0 = 1.0075E4 CCAPP1 = C3 * MaxProducedTemperature ** 3 + C2 * MaxProducedTemperature ** 2 + C1 * MaxProducedTemperature + C0 else: CCAPP1 = 2231 - 2 * (MaxProducedTemperature - 150.) x = np.max(model.surfaceplant.ElectricityProduced.value) y = np.max(model.surfaceplant.ElectricityProduced.value) if y == 0.0: y = 15.0 z = math.pow(y / 15., -0.06) self.Cplantcorrelation = CCAPP1 * z * x * 1000. / 1E6 elif model.surfaceplant.plant_type.value == PlantType.SUPER_CRITICAL_ORC: MaxProducedTemperature = np.max(model.surfaceplant.TenteringPP.value) if MaxProducedTemperature < 150.: C3 = -1.458333E-3 C2 = 7.6875E-1 C1 = -1.347917E2 C0 = 1.0075E4 CCAPP1 = C3 * MaxProducedTemperature ** 3 + C2 * MaxProducedTemperature ** 2 + C1 * MaxProducedTemperature + C0 else: CCAPP1 = 2231 - 2 * (MaxProducedTemperature - 150.) # factor 1.1 to make supercritical 10% more expansive than subcritical self.Cplantcorrelation = 1.1 * CCAPP1 * math.pow( np.max(model.surfaceplant.ElectricityProduced.value) / 15., -0.06) * np.max( model.surfaceplant.ElectricityProduced.value) * 1000. / 1E6 elif model.surfaceplant.plant_type.value == PlantType.SINGLE_FLASH: if np.max(model.surfaceplant.ElectricityProduced.value) < 10.: C2 = 4.8472E-2 C1 = -35.2186 C0 = 8.4474E3 D2 = 4.0604E-2 D1 = -29.3817 D0 = 6.9911E3 PLL = 5. PRL = 10. elif np.max(model.surfaceplant.ElectricityProduced.value) < 25.: C2 = 4.0604E-2 C1 = -29.3817 C0 = 6.9911E3 D2 = 3.2773E-2 D1 = -23.5519 D0 = 5.5263E3 PLL = 10. PRL = 25. elif np.max(model.surfaceplant.ElectricityProduced.value) < 50.: C2 = 3.2773E-2 C1 = -23.5519 C0 = 5.5263E3 D2 = 3.4716E-2 D1 = -23.8139 D0 = 5.1787E3 PLL = 25. PRL = 50. elif np.max(model.surfaceplant.ElectricityProduced.value) < 75.: C2 = 3.4716E-2 C1 = -23.8139 C0 = 5.1787E3 D2 = 3.5271E-2 D1 = -24.3962 D0 = 5.1972E3 PLL = 50. PRL = 75. else: C2 = 3.5271E-2 C1 = -24.3962 C0 = 5.1972E3 D2 = 3.3908E-2 D1 = -23.4890 D0 = 5.0238E3 PLL = 75. PRL = 100. maxProdTemp = np.max(model.surfaceplant.TenteringPP.value) CCAPPLL = C2 * maxProdTemp ** 2 + C1 * maxProdTemp + C0 CCAPPRL = D2 * maxProdTemp ** 2 + D1 * maxProdTemp + D0 b = math.log(CCAPPRL / CCAPPLL) / math.log(PRL / PLL) a = CCAPPRL / PRL ** b # factor 0.75 to make double flash 25% more expansive than single flash self.Cplantcorrelation = (0.8 * a * math.pow(np.max(model.surfaceplant.ElectricityProduced.value), b) * np.max(model.surfaceplant.ElectricityProduced.value) * 1000. / 1E6) elif model.surfaceplant.plant_type.value == PlantType.DOUBLE_FLASH: if np.max(model.surfaceplant.ElectricityProduced.value) < 10.: C2 = 4.8472E-2 C1 = -35.2186 C0 = 8.4474E3 D2 = 4.0604E-2 D1 = -29.3817 D0 = 6.9911E3 PLL = 5. PRL = 10. elif np.max(model.surfaceplant.ElectricityProduced.value) < 25.: C2 = 4.0604E-2 C1 = -29.3817 C0 = 6.9911E3 D2 = 3.2773E-2 D1 = -23.5519 D0 = 5.5263E3 PLL = 10. PRL = 25. elif np.max(model.surfaceplant.ElectricityProduced.value) < 50.: C2 = 3.2773E-2 C1 = -23.5519 C0 = 5.5263E3 D2 = 3.4716E-2 D1 = -23.8139 D0 = 5.1787E3 PLL = 25. PRL = 50. elif np.max(model.surfaceplant.ElectricityProduced.value) < 75.: C2 = 3.4716E-2 C1 = -23.8139 C0 = 5.1787E3 D2 = 3.5271E-2 D1 = -24.3962 D0 = 5.1972E3 PLL = 50. PRL = 75. else: C2 = 3.5271E-2 C1 = -24.3962 C0 = 5.1972E3 D2 = 3.3908E-2 D1 = -23.4890 D0 = 5.0238E3 PLL = 75. PRL = 100. maxProdTemp = np.max(model.surfaceplant.TenteringPP.value) CCAPPLL = C2 * maxProdTemp ** 2 + C1 * maxProdTemp + C0 CCAPPRL = D2 * maxProdTemp ** 2 + D1 * maxProdTemp + D0 b = math.log(CCAPPRL / CCAPPLL) / math.log(PRL / PLL) a = CCAPPRL / PRL ** b self.Cplantcorrelation = (a * math.pow(np.max(model.surfaceplant.ElectricityProduced.value), b) * np.max(model.surfaceplant.ElectricityProduced.value) * 1000. / 1E6) if self.ccplantfixed.Valid: self.Cplant.value = self.ccplantfixed.value self.CAPEX_cost_electricity_plant = self.Cplant.value * self.CAPEX_heat_electricity_plant_ratio.value self.CAPEX_cost_heat_plant = self.Cplant.value * (1.0 - self.CAPEX_heat_electricity_plant_ratio.value) else: # 1.02 to convert cost from 2012 to 2016 #factor 1.15 for 15% contingency and 1.12 for 12% indirect costs. factor 1.10 to convert from 2016 to 2022 self.Cplant.value = 1.12 * 1.15 * self.ccplantadjfactor.value * self.Cplantcorrelation * 1.02 * 1.10 self.CAPEX_cost_electricity_plant = self.Cplant.value # add direct-use plant cost of co-gen system to Cplant (only of no total Cplant was provided) if not self.ccplantfixed.Valid: # 1.15 below for contingency and 1.12 for indirect costs if model.surfaceplant.enduse_option.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICITY, EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT]: # enduse_option = 3: cogen topping cycle self.CAPEX_cost_heat_plant = 1.12 * 1.15 * self.ccplantadjfactor.value * 250E-6 * np.max( model.surfaceplant.HeatProduced.value / model.surfaceplant.enduse_efficiency_factor.value) * 1000. elif model.surfaceplant.enduse_option.value in [EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICITY]: # enduse_option = 4: cogen bottoming cycle self.CAPEX_cost_heat_plant = 1.12 * 1.15 * self.ccplantadjfactor.value * 250E-6 * np.max( model.surfaceplant.HeatProduced.value / model.surfaceplant.enduse_efficiency_factor.value) * 1000. elif model.surfaceplant.enduse_option.value in [EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICITY, EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT]: # cogen parallel cycle self.CAPEX_cost_heat_plant = 1.12 * 1.15 * self.ccplantadjfactor.value * 250E-6 * np.max( model.surfaceplant.HeatProduced.value / model.surfaceplant.enduse_efficiency_factor.value) * 1000. self.Cplant.value = self.Cplant.value + self.CAPEX_cost_heat_plant if not self.CAPEX_heat_electricity_plant_ratio.Provided: self.CAPEX_heat_electricity_plant_ratio.value = self.CAPEX_cost_electricity_plant/self.Cplant.value if not self.totalcapcost.Valid: # exploration costs (same as in Geophires v1.2) (M$) if self.ccexplfixed.Valid: self.Cexpl.value = self.ccexplfixed.value else: self.Cexpl.value = 1.15 * self.ccexpladjfactor.value * 1.12 * ( 1. + self.cost_one_production_well.value * 0.6) # 1.15 for 15% contingency and 1.12 for 12% indirect costs # Surface Piping Length Costs (M$) #assumed $750k/km self.Cpiping.value = 750 / 1000 * model.surfaceplant.piping_length.value # district heating network costs if model.surfaceplant.plant_type.value == PlantType.DISTRICT_HEATING: # district heat if self.dhtotaldistrictnetworkcost.Provided: self.dhdistrictcost.value = self.dhtotaldistrictnetworkcost.value elif self.dhpipinglength.Provided: self.dhdistrictcost.value = self.dhpipinglength.value * self.dhpipingcostrate.value / 1000 # M$ elif self.dhroadlength.Provided: # check if road length is provided to calculate cost self.dhdistrictcost.value = self.dhroadlength.value * 0.75 * self.dhpipingcostrate.value / 1000 # M$ (assuming 75% of road length is used for district network piping) else: # calculate district network cost based on population density if self.dhlandarea.Provided == False: model.logger.warning("District heating network cost calculated based on default district area") if self.dhpopulation.Provided: self.populationdensity.value = self.dhpopulation.value / self.dhlandarea.value elif model.surfaceplant.dh_number_of_housing_units.Provided: self.populationdensity.value = model.surfaceplant.dh_number_of_housing_units.value * 2.6 / self.dhlandarea.value # estimate population based on 2.6 number of people per household else: model.logger.warning( "District heating network cost calculated based on default number of people in district") self.populationdensity.value = self.dhpopulation.value / self.dhlandarea.value if self.populationdensity.value > 1000: self.dhpipinglength.value = 7.5 * self.dhlandarea.value # using constant 7.5km of pipe per km^2 when population density is >1500 else: self.dhpipinglength.value = max( self.populationdensity.value / 1000 * 7.5 * self.dhlandarea.value, self.dhlandarea.value) # scale the piping length based on population density, but with a minimum of 1 km of piping per km^2 of area self.dhdistrictcost.value = self.dhpipingcostrate.value * self.dhpipinglength.value / 1000 else: self.dhdistrictcost.value = 0 self.CCap.value = self.Cexpl.value + self.Cwell.value + self.Cstim.value + self.Cgath.value + self.Cplant.value + self.Cpiping.value + self.dhdistrictcost.value else: self.CCap.value = self.totalcapcost.value # update the capitol costs, assuming the entire ITC is used to reduce the capitol costs if self.RITC.Provided: self.RITCValue.value = self.RITC.value * self.CCap.value self.CCap.value = self.CCap.value - self.RITCValue.value # Add in the FlatLicenseEtc, OtherIncentives, & TotalGrant self.CCap.value = self.CCap.value + self.FlatLicenseEtc.value - self.OtherIncentives.value - self.TotalGrant.value # O&M costs # calculate first O&M costs independent of whether oamtotalfixed is provided or not # additional electricity cost for heat pump as end-use if model.surfaceplant.plant_type.value == PlantType.HEAT_PUMP: # heat pump: self.averageannualheatpumpelectricitycost.value = np.average( model.surfaceplant.heat_pump_electricity_kwh_used.value) * model.surfaceplant.electricity_cost_to_buy.value / 1E6 # M$/year # district heating peaking fuel annual cost if model.surfaceplant.plant_type.value == PlantType.DISTRICT_HEATING: # district heating self.annualngcost.value = model.surfaceplant.annual_ng_demand.value * self.ngprice.value / 1000 / self.peakingboilerefficiency.value # array with annual O&M cost for peaking fuel self.averageannualngcost.value = np.average(self.annualngcost.value) # calculate average annual pumping costs in case no electricity is provided if model.surfaceplant.plant_type.value in [PlantType.INDUSTRIAL, PlantType.ABSORPTION_CHILLER, PlantType.HEAT_PUMP, PlantType.DISTRICT_HEATING]: self.averageannualpumpingcosts.value = np.average(model.surfaceplant.PumpingkWh.value) * model.surfaceplant.electricity_cost_to_buy.value / 1E6 # M$/year if not self.oamtotalfixed.Valid: # labor cost if model.surfaceplant.enduse_option.value == EndUseOptions.ELECTRICITY: # electricity if np.max(model.surfaceplant.ElectricityProduced.value) < 2.5: self.Claborcorrelation = 236. / 1E3 # M$/year else: self.Claborcorrelation = (589. * math.log( np.max(model.surfaceplant.ElectricityProduced.value)) - 304.) / 1E3 # M$/year else: if np.max(model.surfaceplant.HeatExtracted.value) < 2.5 * 5.: self.Claborcorrelation = 236. / 1E3 # M$/year else: self.Claborcorrelation = (589. * math.log( np.max(model.surfaceplant.HeatExtracted.value) / 5.) - 304.) / 1E3 # M$/year # * 1.1 to convert from 2012 to 2016$ with BLS employment cost index (for utilities in March) self.Claborcorrelation = self.Claborcorrelation * 1.1 # plant O&M cost if self.oamplantfixed.Valid: self.Coamplant.value = self.oamplantfixed.value else: self.Coamplant.value = self.oamplantadjfactor.value * ( 1.5 / 100. * self.Cplant.value + 0.75 * self.Claborcorrelation) # wellfield O&M cost if self.oamwellfixed.Valid: self.Coamwell.value = self.oamwellfixed.value else: self.Coamwell.value = self.oamwelladjfactor.value * ( 1. / 100. * (self.Cwell.value + self.Cgath.value) + 0.25 * self.Claborcorrelation) # water O&M cost if self.oamwaterfixed.Valid: self.Coamwater.value = self.oamwaterfixed.value else: # here is assumed 1 l per kg maybe correct with real temp. (M$/year) 925$/ML = 3.5$/1,000 gallon self.Coamwater.value = self.oamwateradjfactor.value * (model.wellbores.nprod.value * model.wellbores.prodwellflowrate.value * model.reserv.waterloss.value * model.surfaceplant.utilization_factor.value * 365. * 24. * 3600. / 1E6 * 925. / 1E6) # additional O&M cost for absorption chiller if used if model.surfaceplant.plant_type.value == PlantType.ABSORPTION_CHILLER: # absorption chiller: if self.chilleropex.value == -1: self.chilleropex.value = self.chillercapex.value * 2 / 100 # assumed annual O&M for chiller is 2% of investment cost # correct plant O&M cost as otherwise chiller opex would be counted double (subtract chiller capex from plant cost when calculating Coandmplant) if self.oamplantfixed.Valid == False: self.Coamplant.value = self.oamplantadjfactor.value * ( 1.5 / 100. * (self.Cplant.value - self.chillercapex.value) + 0.75 * self.Claborcorrelation) else: self.chilleropex.value = 0 # district heating O&M cost if model.surfaceplant.plant_type.value == PlantType.DISTRICT_HEATING: # district heating self.annualngcost.value = model.surfaceplant.annual_ng_demand.value * self.ngprice.value / 1000 # array with annual O&M cost for peaking fuel if self.dhoandmcost.Provided: self.dhdistrictoandmcost.value = self.dhoandmcost.value # M$/yr else: self.dhdistrictoandmcost.value = 0.01 * self.dhdistrictcost.value + 0.02 * sum( model.surfaceplant.daily_heating_demand.value) * model.surfaceplant.electricity_cost_to_buy.value / 1000 # [M$/year] we assume annual district OPEX equals 1% of district CAPEX and 2% of total heat demand for pumping costs else: self.dhdistrictoandmcost.value = 0 self.Coam.value = self.Coamwell.value + self.Coamplant.value + self.Coamwater.value + self.chilleropex.value + self.dhdistrictoandmcost.value # total O&M cost (M$/year) else: self.Coam.value = self.oamtotalfixed.value # total O&M cost (M$/year) if model.wellbores.redrill.value > 0: # account for well redrilling self.Coam.value = self.Coam.value + \ (self.Cwell.value + self.Cstim.value) * model.wellbores.redrill.value / model.surfaceplant.plant_lifetime.value # Add in the AnnualLicenseEtc and TaxRelief self.Coam.value = self.Coam.value + self.AnnualLicenseEtc.value - self.TaxRelief.value # partition the OPEX for CHP plants based on the CAPEX ratio self.OPEX_cost_electricity_plant = self.Coam.value * self.CAPEX_heat_electricity_plant_ratio.value self.OPEX_cost_heat_plant = self.Coam.value * (1.0 - self.CAPEX_heat_electricity_plant_ratio.value) # The Reservoir depth measure was arbitrarily changed to meters despite being defined in the docs as kilometers. # For display consistency sake, we need to convert it back if model.reserv.depth.value > 500: model.reserv.depth.value = model.reserv.depth.value / 1000.0 model.reserv.depth.CurrentUnits = LengthUnit.KILOMETERS # build the PTC price models self.PTCElecPrice = [0.0] * model.surfaceplant.plant_lifetime.value self.PTCHeatPrice = [0.0] * model.surfaceplant.plant_lifetime.value self.PTCCoolingPrice = [0.0] * model.surfaceplant.plant_lifetime.value self.PTCCarbonPrice = [0.0] * model.surfaceplant.plant_lifetime.value if self.PTCElec.Provided: self.PTCElecPrice = BuildPTCModel(model.surfaceplant.plant_lifetime.value, self.PTCDuration.value, self.PTCElec.value, self.PTCInflationAdjusted.value, self.RINFL.value) if self.PTCHeat.Provided: self.PTCHeatPrice = BuildPTCModel(model.surfaceplant.plant_lifetime.value, self.PTCDuration.value, self.PTCHeat.value, self.PTCInflationAdjusted.value, self.RINFL.value) if self.PTCCooling.Provided: self.PTCCoolingPrice = BuildPTCModel(model.surfaceplant.plant_lifetime.value, self.PTCDuration.value, self.PTCCooling.value, self.PTCInflationAdjusted.value, self.RINFL.value) # build the price models self.ElecPrice.value = BuildPricingModel(model.surfaceplant.plant_lifetime.value, self.ElecStartPrice.value, self.ElecEndPrice.value, self.ElecEscalationStart.value, self.ElecEscalationRate.value, self.PTCElecPrice) self.HeatPrice.value = BuildPricingModel(model.surfaceplant.plant_lifetime.value, self.HeatStartPrice.value, self.HeatEndPrice.value, self.HeatEscalationStart.value, self.HeatEscalationRate.value, self.PTCHeatPrice) self.CoolingPrice.value = BuildPricingModel(model.surfaceplant.plant_lifetime.value, self.CoolingStartPrice.value, self.CoolingEndPrice.value, self.CoolingEscalationStart.value, self.CoolingEscalationRate.value, self.PTCCoolingPrice) self.CarbonPrice.value = BuildPricingModel(model.surfaceplant.plant_lifetime.value, self.CarbonStartPrice.value, self.CarbonEndPrice.value, self.CarbonEscalationStart.value, self.CarbonEscalationRate.value, self.PTCCarbonPrice) # do the additional economic calculations first, if needed, so the summaries below work. if self.DoAddOnCalculations.value: model.addeconomics.Calculate(model) if self.DoSDACGTCalculations.value: model.sdacgteconomics.Calculate(model) # Calculate cashflow and cumulative cash flow total_duration = model.surfaceplant.plant_lifetime.value + model.surfaceplant.construction_years.value self.ElecRevenue.value = [0.0] * total_duration self.ElecCummRevenue.value = [0.0] * total_duration self.HeatRevenue.value = [0.0] * total_duration self.HeatCummRevenue.value = [0.0] * total_duration self.CoolingRevenue.value = [0.0] * total_duration self.CoolingCummRevenue.value = [0.0] * total_duration self.CarbonRevenue.value = [0.0] * total_duration self.CarbonCummCashFlow.value = [0.0] * total_duration self.TotalRevenue.value = [0.0] * total_duration self.TotalCummRevenue.value = [0.0] * total_duration self.CarbonThatWouldHaveBeenProducedTotal.value = 0.0 # Based on the style of the project, calculate the revenue & cumulative revenue if model.surfaceplant.enduse_option.value == EndUseOptions.ELECTRICITY: self.ElecRevenue.value, self.ElecCummRevenue.value = CalculateRevenue( model.surfaceplant.plant_lifetime.value, model.surfaceplant.construction_years.value, model.surfaceplant.NetkWhProduced.value, self.ElecPrice.value) self.TotalRevenue.value = self.ElecRevenue.value #self.TotalCummRevenue.value = self.ElecCummRevenue.value elif model.surfaceplant.enduse_option.value == EndUseOptions.HEAT and model.surfaceplant.plant_type.value not in [PlantType.ABSORPTION_CHILLER]: self.HeatRevenue.value, self.HeatCummRevenue.value = CalculateRevenue( model.surfaceplant.plant_lifetime.value, model.surfaceplant.construction_years.value, model.surfaceplant.HeatkWhProduced.value, self.HeatPrice.value) self.TotalRevenue.value = self.HeatRevenue.value #self.TotalCummRevenue.value = self.HeatCummRevenue.value elif model.surfaceplant.enduse_option.value == EndUseOptions.HEAT and model.surfaceplant.plant_type.value in [PlantType.ABSORPTION_CHILLER]: self.CoolingRevenue.value, self.CoolingCummRevenue.value = CalculateRevenue( model.surfaceplant.plant_lifetime.value, model.surfaceplant.construction_years.value, model.surfaceplant.cooling_kWh_Produced.value, self.CoolingPrice.value) self.TotalRevenue.value = self.CoolingRevenue.value #self.TotalCummRevenue.value = self.CoolingCummRevenue.value elif model.surfaceplant.enduse_option.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICITY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICITY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICITY]: # co-gen # else: self.ElecRevenue.value, self.ElecCummRevenue.value = CalculateRevenue( model.surfaceplant.plant_lifetime.value, model.surfaceplant.construction_years.value, model.surfaceplant.NetkWhProduced.value, self.ElecPrice.value) self.HeatRevenue.value, self.HeatCummRevenue.value = CalculateRevenue( model.surfaceplant.plant_lifetime.value, model.surfaceplant.construction_years.value, model.surfaceplant.HeatkWhProduced.value, self.HeatPrice.value) for i in range(0, model.surfaceplant.plant_lifetime.value + model.surfaceplant.construction_years.value, 1): self.TotalRevenue.value[i] = self.ElecRevenue.value[i] + self.HeatRevenue.value[i] #if i > 0: # self.TotalCummRevenue.value[i] = self.TotalCummRevenue.value[i - 1] + self.TotalRevenue.value[i] if self.DoCarbonCalculations.value: self.CarbonRevenue.value, self.CarbonCummCashFlow.value, self.CarbonThatWouldHaveBeenProducedAnnually.value, \ self.CarbonThatWouldHaveBeenProducedTotal.value = CalculateCarbonRevenue(model, model.surfaceplant.plant_lifetime.value, model.surfaceplant.construction_years.value, self.CarbonPrice.value, self.GridCO2Intensity.value, self.NaturalGasCO2Intensity.value, model.surfaceplant.NetkWhProduced.value, model.surfaceplant.HeatkWhProduced.value) for i in range(model.surfaceplant.construction_years.value, model.surfaceplant.plant_lifetime.value + model.surfaceplant.construction_years.value, 1): self.TotalRevenue.value[i] = self.TotalRevenue.value[i] + self.CarbonRevenue.value[i] #self.TotalCummRevenue.value[i] = self.TotalCummRevenue.value[i] + self.CarbonCummCashFlow.value[i] # for the sake of display, insert zeros at the beginning of the pricing arrays for i in range(0, model.surfaceplant.construction_years.value, 1): self.ElecPrice.value.insert(0, 0.0) self.HeatPrice.value.insert(0, 0.0) self.CoolingPrice.value.insert(0, 0.0) self.CarbonPrice.value.insert(0, 0.0) # Insert the cost of construction into the front of the array that will be used to calculate NPV # the convention is that the upfront CAPEX is negative # This is the same for all projects ProjectCAPEXPerConstructionYear = self.CCap.value / model.surfaceplant.construction_years.value for i in range(0, model.surfaceplant.construction_years.value, 1): self.TotalRevenue.value[i] = -1.0 * ProjectCAPEXPerConstructionYear self.TotalCummRevenue.value[i] = -1.0 * ProjectCAPEXPerConstructionYear # self.TotalRevenue.value, self.TotalCummRevenue.value = CalculateTotalRevenue( # model.surfaceplant.plant_lifetime.value, model.surfaceplant.construction_years.value, self.CCap.value, # self.Coam.value, self.TotalRevenue.value, self.TotalCummRevenue.value) # Do a one-time calculation that accounts for OPEX - no OPEX in the first year. for i in range(model.surfaceplant.construction_years.value, model.surfaceplant.plant_lifetime.value + model.surfaceplant.construction_years.value, 1): self.TotalRevenue.value[i] = self.TotalRevenue.value[i] - self.Coam.value # Now do a one-time calculation that calculates the cumulative cash flow after everything else has been accounted for for i in range(1, model.surfaceplant.plant_lifetime.value + model.surfaceplant.construction_years.value, 1): self.TotalCummRevenue.value[i] = self.TotalCummRevenue.value[i-1] + self.TotalRevenue.value[i] # Calculate more financial values using numpy financials self.ProjectNPV.value, self.ProjectIRR.value, self.ProjectVIR.value, self.ProjectMOIC.value = \ CalculateFinancialPerformance(model.surfaceplant.plant_lifetime.value, self.FixedInternalRate.value, self.TotalRevenue.value, self.TotalCummRevenue.value, self.CCap.value, self.Coam.value) # Calculate the project payback period self.ProjectPaybackPeriod.value = 0.0 # start by assuming the project never pays back for i in range(0, len(self.TotalCummRevenue.value), 1): # find out when the cumm cashflow goes from negative to positive if self.TotalCummRevenue.value[i] > 0 >= self.TotalCummRevenue.value[i - 1]: # we just crossed the threshold into positive project cummcashflow, so we can calculate payback period dFullDiff = self.TotalCummRevenue.value[i] + math.fabs(self.TotalCummRevenue.value[(i - 1)]) dPerc = math.fabs(self.TotalCummRevenue.value[(i - 1)]) / dFullDiff self.ProjectPaybackPeriod.value = i + dPerc # Calculate LCOE/LCOH self.LCOE.value, self.LCOH.value, self.LCOC.value = CalculateLCOELCOHLCOC(self, model) model.logger.info(f'complete {__class__!s}: {sys._getframe().f_code.co_name}')