Source code for geophires_x.SUTRAEconomics

import sys
import os
import numpy as np
import geophires_x.Model as Model
import geophires_x.Economics as Economics
from geophires_x.OptionList import WellDrillingCostCorrelation, EconomicModel
from geophires_x.Parameter import intParameter, floatParameter, OutputParameter, ReadParameter, boolParameter
from geophires_x.Units import *


[docs] class SUTRAEconomics(Economics.Economics): """ Class to support the default economic calculations in GEOPHIRES """ def __init__(self, model: Model): """ The __init__ function is called automatically when a class is instantiated. It initializes the attributes of an object, and sets default values for certain arguments that can be overridden by user input. The __init__ function is used to set up all the parameters in Economics. :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 {str(__class__)}: {sys._getframe().f_code.co_name}') super().__init__(model) # 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 teh 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. # These dictionaries contain a list of all the parameters set in this object, stored as "Parameter" and # "OutputParameter" Objects. This will allow us later to access them in a user interface and get that list, # along with unit type, preferred units, etc. self.ParameterDict = {} self.OutputParameterDict = {} # Note: setting Valid to False for any of the cost parameters forces GEOPHIRES to use it's builtin cost engine. # This is the default. self.econmodel = self.ParameterDict[self.econmodel.Name] = intParameter( "Economic Model", value=EconomicModel.STANDARDIZED_LEVELIZED_COST, DefaultValue=EconomicModel.STANDARDIZED_LEVELIZED_COST, ValuesEnum=EconomicModel, AllowableRange=[1, 2, 3], Required=True, ErrMessage="assume default economic model (2)", ToolTipText="Specify the economic model to calculate the levelized cost of energy. " + '; '.join([f'{it.int_value}: {it.value}' for it in EconomicModel]) ) self.ccwellfixed = self.ParameterDict[self.ccwellfixed.Name] = floatParameter( "Well Drilling and Completion Capital Cost", value=-1.0, DefaultValue=-1.0, Min=0, Max=200, UnitType=Units.CURRENCY, PreferredUnits=CurrencyUnit.MDOLLARS, CurrentUnits=CurrencyUnit.MDOLLARS, Provided=False, Valid=False, ToolTipText="Well Drilling and Completion Capital Cost", ) self.ccwelladjfactor = self.ParameterDict[self.ccwelladjfactor.Name] = floatParameter( "Well Drilling and Completion Capital Cost Adjustment Factor", value=1.0, DefaultValue=1.0, Min=0, Max=10, UnitType=Units.PERCENT, PreferredUnits=PercentUnit.TENTH, CurrentUnits=PercentUnit.TENTH, Provided=False, Valid=True, ToolTipText="Well Drilling and Completion Capital Cost Adjustment Factor", ) self.ccplantfixed = self.ParameterDict[self.ccplantfixed.Name] = floatParameter( "Surface Plant Capital Cost", value=-1.0, DefaultValue=-1.0, Min=0, Max=1000, UnitType=Units.CURRENCY, PreferredUnits=CurrencyUnit.MDOLLARS, CurrentUnits=CurrencyUnit.MDOLLARS, Provided=False, Valid=False, ToolTipText="Total surface plant capital cost", ) self.ccplantadjfactor = self.ParameterDict[self.ccplantadjfactor.Name] = floatParameter( "Surface Plant Capital Cost Adjustment Factor", value=1.0, DefaultValue=1.0, Min=0, Max=10, UnitType=Units.PERCENT, PreferredUnits=PercentUnit.TENTH, CurrentUnits=PercentUnit.TENTH, Provided=False, Valid=True, ToolTipText="Multiplier for built-in surface plant capital cost correlation", ) self.inflrateconstruction = self.ParameterDict[self.inflrateconstruction.Name] = floatParameter( "Inflation Rate During Construction", value=0.0, DefaultValue=0.0, Min=0.0, Max=1.0, UnitType=Units.PERCENT, PreferredUnits=PercentUnit.PERCENT, CurrentUnits=PercentUnit.TENTH, ErrMessage="assume default inflation rate during construction (0)", ) self.wellcorrelation = self.ParameterDict[self.wellcorrelation.Name] = intParameter( "Well Drilling Cost Correlation", DefaultValue=WellDrillingCostCorrelation.VERTICAL_LARGE_INT1.int_value, AllowableRange=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17], ValuesEnum=WellDrillingCostCorrelation, UnitType=Units.NONE, ErrMessage="assume default well drilling cost correlation (10)", ToolTipText="Select the built-in well drilling and completion cost correlation: " + '; '.join([f'{it.int_value}: {it.value}' for it in WellDrillingCostCorrelation]) ) self.timestepsperyear = self.ParameterDict[self.timestepsperyear.Name] = intParameter( "Time steps per year", value=4, DefaultValue=4, AllowableRange=list(range(1, 101, 1)), UnitType=Units.NONE, Required=True, ErrMessage="assume default number of time steps per year (4)", ToolTipText="Number of internal simulation time steps per year", ) self.DoAddOnCalculations = self.ParameterDict[self.DoAddOnCalculations.Name] = boolParameter( "Do AddOn Calculations", value=False, DefaultValue=False, UnitType=Units.NONE, Required=False, ErrMessage="assume default: no economics calculations", ToolTipText="Set to true if you want the add-on economics calculations to be made", ) self.DoCCUSCalculations = self.ParameterDict[self.DoCCUSCalculations.Name] = boolParameter( "Do CCUS Calculations", value=False, DefaultValue=False, UnitType=Units.NONE, Required=False, ErrMessage="assume default: no CCUS calculations", ToolTipText="Set to true if you want the CCUS economics calculations to be made", ) self.DoSDACGTCalculations = self.ParameterDict[self.DoSDACGTCalculations.Name] = boolParameter( "Do S-DAC-GT Calculations", value=False, DefaultValue=False, UnitType=Units.NONE, Required=False, ErrMessage="assume default: no S-DAC-GT calculations", ToolTipText="Set to true if you want the S-DAC-GT economics calculations to be made", ) # heat pump self.heatpumpcapex = self.ParameterDict[self.heatpumpcapex.Name] = floatParameter( "Heat Pump Capital Cost", value=-1.0, Min=0, Max=100, UnitType=Units.CURRENCY, PreferredUnits=CurrencyUnit.MDOLLARS, CurrentUnits=CurrencyUnit.MDOLLARS, Provided=False, Valid=False, ToolTipText="Heat pump capital cost", ) self.ngprice = self.ParameterDict[self.ngprice.Name] = floatParameter( "Peaking Fuel Cost Rate", value=0.034, Min=0.0, Max=1.0, UnitType=Units.ENERGYCOST, PreferredUnits=EnergyCostUnit.DOLLARSPERKWH, CurrentUnits=EnergyCostUnit.DOLLARSPERKWH, ErrMessage="assume default peaking fuel rate ($0.034/kWh)", ToolTipText="Price of peaking fuel for peaking boilers", ) self.peakingboilerefficiency = self.ParameterDict[self.peakingboilerefficiency.Name] = floatParameter( "Peaking Boiler Efficiency", value=0.85, Min=0, Max=1, UnitType=Units.PERCENT, PreferredUnits=PercentUnit.TENTH, CurrentUnits=PercentUnit.TENTH, Provided=False, Valid=False, ErrMessage="assume default peaking boiler efficiency (85%)", ToolTipText="Peaking boiler efficiency", ) self.LCOH = self.OutputParameterDict[self.LCOH.Name] = OutputParameter( "Heat Sale Price Model", value=[0.025], UnitType=Units.ENERGYCOST, PreferredUnits=EnergyCostUnit.CENTSSPERKWH, CurrentUnits=EnergyCostUnit.CENTSSPERKWH, ) # local variable initialization self.Cpumps = 0.0 self.InputFile = "" self.C1well = 0.0 sclass = str(__class__).replace("<class \'", "") self.MyClass = sclass.replace("\'>", "") self.MyPath = os.path.abspath(__file__) # results self.Cwell = self.OutputParameterDict[self.Cwell.Name] = OutputParameter( Name="Wellfield cost", value=-999.9, UnitType=Units.CURRENCY, PreferredUnits=CurrencyUnit.MDOLLARS, CurrentUnits=CurrencyUnit.MDOLLARS, ) self.CCap = self.OutputParameterDict[self.CCap.Name] = OutputParameter( Name="Total Capital Cost", value=-999.9, UnitType=Units.CURRENCY, PreferredUnits=CurrencyUnit.MDOLLARS, CurrentUnits=CurrencyUnit.MDOLLARS, ) self.Coam = self.OutputParameterDict[self.Coam.Name] = OutputParameter( Name="Total O&M Cost", value=-999.9, UnitType=Units.CURRENCYFREQUENCY, PreferredUnits=CurrencyFrequencyUnit.KDOLLARSPERYEAR, CurrentUnits=CurrencyFrequencyUnit.KDOLLARSPERYEAR, ) self.annualpumpingcosts = self.OutputParameterDict[self.annualpumpingcosts.Name] = OutputParameter( Name="Annual Pumping Costs", value=-0.0, UnitType=Units.CURRENCYFREQUENCY, PreferredUnits=CurrencyFrequencyUnit.KDOLLARSPERYEAR, CurrentUnits=CurrencyFrequencyUnit.KDOLLARSPERYEAR, ) # heat pump self.averageannualheatpumpelectricitycost = self.OutputParameterDict[ self.averageannualheatpumpelectricitycost.Name ] = OutputParameter( Name="Average Annual Heat Pump Electricity Cost", value=0.0, UnitType=Units.CURRENCYFREQUENCY, PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, ) self.peakingboilercost = self.OutputParameterDict[self.peakingboilercost.Name] = OutputParameter( Name="Peaking boiler cost", value=0, UnitType=Units.CURRENCY, PreferredUnits=CurrencyUnit.MDOLLARS, CurrentUnits=CurrencyUnit.MDOLLARS, ) self.annualngcost = self.OutputParameterDict[self.annualngcost.Name] = OutputParameter( Name="Annual Peaking Fuel Cost", value=0, UnitType=Units.CURRENCYFREQUENCY, PreferredUnits=CurrencyFrequencyUnit.KDOLLARSPERYEAR, CurrentUnits=CurrencyFrequencyUnit.KDOLLARSPERYEAR, ) model.logger.info(f'Complete {__class__!s}: {sys._getframe().f_code.co_name}')
[docs] def read_parameters(self, model: Model) -> None: """ read_parameters read and update the Economics parameters and handle the special cases that need to be taken care of after a value has been read in and checked. This is called from the main Model class. It is not called from the __init__ function because the user may not want to read in the parameters from the input file, but may want to set them in the user interface. :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 {str(__class__)}: {sys._getframe().f_code.co_name}') # Deal with all the parameter values that the user has provided. They should really only provide values # that they want to change from the default values, but they can provide a value that is already set # because it is a default value set in __init__. It will ignore those. # This also deals with all the special cases that need to be taken care of 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, 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 modify all these superclass parameters # in your class. if len(model.InputParameters) > 0: # loop through all the parameters that the user wishes to set, looking for parameters that match this object for item in self.ParameterDict.items(): ParameterToModify = item[1] key = ParameterToModify.Name.strip() if key in model.InputParameters: ParameterReadIn = model.InputParameters[key] # this should handle all the non-special cases ReadParameter(ParameterReadIn, ParameterToModify, model) # handle special cases if ParameterToModify.Name == 'Economic Model': self.econmodel.value = EconomicModel.from_input_string(ParameterReadIn.sValue) elif ParameterToModify.Name == 'Well Drilling Cost Correlation': ParameterToModify.value = WellDrillingCostCorrelation.from_input_string(ParameterReadIn.sValue) else: model.logger.info('No parameters read because no content provided') self.sync_interest_rate(model) 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. :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 {str(__class__)}: {sys._getframe().f_code.co_name}') # 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! # CAPEX # Drilling self.C1well = 0 if self.ccwellfixed.Valid: self.C1well = self.ccwellfixed.value self.Cwell.value = self.C1well * (model.wellbores.nprod.value + model.wellbores.ninj.value) else: if model.reserv.depth.value > 7000.0 or model.reserv.depth.value < 500: print('Warning: simple user-specified cost per meter used for drilling depth < 500 or > 7000 m') model.logger.warning( 'Warning: simple user-specified cost per meter used for drilling depth < 500 or > 7000 m' ) self.C1well = self.wellcorrelation.value.calculate_cost_MUSD(model.reserv.depth.value) self.C1well = self.C1well * self.ccwelladjfactor.value self.Cwell.value = self.C1well * (model.wellbores.nprod.value + model.wellbores.ninj.value) # Boiler self.peakingboilercost.value = ( 65 * model.surfaceplant.max_peaking_boiler_demand.value / self.peakingboilerefficiency.value / 1000 ) # add 65$/KW for peaking boiler # Circulation Pump pumphp = np.max(model.wellbores.PumpingPower.value) * 1.341 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)) / 1e6 # Total CAPEX ($M) self.CCap.value = self.Cwell.value + self.peakingboilercost.value + self.Cpumps # OPEX # Pumping self.annualpumpingcosts.value = ( model.surfaceplant.PumpingkWh.value * model.surfaceplant.electricity_cost_to_buy.value / 1e3 ) # Natural Gas self.annualngcost.value = ( model.surfaceplant.AnnualAuxiliaryHeatProduced.value * self.ngprice.value / self.peakingboilerefficiency.value * 1e3 ) # Price for the heat injected currently not considered # Total O&M cost ($K/year) self.Coam.value = self.annualpumpingcosts.value + self.annualngcost.value # LCOH discountvector = 1.0 / np.power( 1 + self.discountrate.value, np.linspace(0, model.surfaceplant.plant_lifetime.value - 1, model.surfaceplant.plant_lifetime.value), ) self.LCOH.value = ( ((1 + self.inflrateconstruction.value) * self.CCap.value + np.sum(self.Coam.value * discountvector)) / np.sum(model.surfaceplant.AnnualTotalHeatProduced.value * 1e6 * discountvector) * 1e8 ) # cents/kWh model.logger.info("complete " + str(__class__) + ": " + sys._getframe().f_code.co_name)
def __str__(self): return "Economics"