diff --git a/.gitignore b/.gitignore index 679d733a..b56aa567 100644 --- a/.gitignore +++ b/.gitignore @@ -76,6 +76,19 @@ docs/reference/parameters.rst docs/geophires-request.json docs/parameters.rst _site/ +/docs/geophires_x.rstx +/docs/temperature.txt +/geophires_x.rst +/modules.rst +/Useful sites for Sphinx docstrings.txt +/.github/workflows/workflows.7z # Mypy Cache .mypy_cache/ + +#CLG Simulator +/src/geophires_x/CLG Simulator/clg_tea_module_laptop_auto.py +/src/geophires_x/CLG Simulator/clg_tea_v3.py +/src/geophires_x/CLG Simulator/clg_tea_v3_coaxial_water_elec.py +/src/geophires_x/CLG Simulator/clg_tea_v3_SCO2_elec.py +/src/geophires_x/CLG Simulator/clgs_v2.py diff --git a/src/geophires_x/AGSEconomics.py b/src/geophires_x/AGSEconomics.py index 9e615bff..bd8f03bd 100644 --- a/src/geophires_x/AGSEconomics.py +++ b/src/geophires_x/AGSEconomics.py @@ -12,44 +12,16 @@ class AGSEconomics(Economics.Economics): """ AGSEconomics Child class of Economics; it is the same, but has advanced AGS closed-loop functionality - .. list-table:: **Parameters** - :widths: 100 25 25 50 25 25 25 - :header-rows: 1 - * - Name - - Type - - Default Value - - Units - - Min - - Max - - Required - * - Operation & Maintenance Cost of Surface Plant - - float - - 0.015 - - PercentUnit.TENTH - - 0.0 - - 0.2 - - True - * - Capital Cost for Surface Plant for Direct-use System - - float - - 100.0 - - EnergyCostUnit.DOLLARSPERKW - - 0.0, - - 10000.0 - - False - """ 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 self: Reference the class object itself + 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 - :doc-author: Malcolm Ross """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) @@ -92,7 +64,8 @@ def __init__(self, model: Model): 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( + 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", value=100.0, DefaultValue=100.0, @@ -128,12 +101,9 @@ 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 self: Access variables that belong to a class :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 - :doc-author: Malcolm Ross """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) super().read_parameters(model) # read the default parameters @@ -152,12 +122,9 @@ def verify(self, model: Model) -> int: """ The validate function checks that all values provided are within the range expected by AGS modeling system. These values in within a smaller range than the value ranges available to GEOPHIRES-X - - :param self: Access variables that belong to a class :param model: The container class of the application, giving access to everything else, including the logger - + :type model: :class:`~geophires_x.Model.Model` :return: 0 if all OK, 1 if error. - :doc-author: Koenraad Beckers """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) @@ -220,12 +187,9 @@ def verify(self, model: Model) -> int: def Calculate(self, model: Model) -> None: """ The calculate function verifies, initializes, and calculate the values for the AGS model - - :param self: Access variables that belong to a class :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 - :doc-author: Koenraad Beckers """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) @@ -279,7 +243,7 @@ def Calculate(self, model: Model) -> None: model.surfaceplant.error_codes = np.append(model.surfaceplant.error_codes, 6000) else: self.LCOE.value = (self.TotalCAPEX + np.sum(self.OPEX_Plant * Discount_vector)) * 1e6 / np.sum(( - model.surfaceplant.Annual_electricity_production - model.surfaceplant.Annual_pumping_power) / 1e3 * Discount_vector) # $/MWh + model.surfaceplant.Annual_electricity_production - model.surfaceplant.Annual_pumping_power) / 1e3 * Discount_vector) # $/MWh if self.LCOE.value < 0: self.LCOE.value = 9999 model.surfaceplant.error_codes = np.append(model.surfaceplant.error_codes, 7000) diff --git a/src/geophires_x/AGSOutputs.py b/src/geophires_x/AGSOutputs.py index 4adb2e94..79714d38 100644 --- a/src/geophires_x/AGSOutputs.py +++ b/src/geophires_x/AGSOutputs.py @@ -14,13 +14,23 @@ class AGSOutputs(Outputs.Outputs): - """description of class""" + """ + Handles the display of AGS data + """ def PrintOutputs(self, model: Model): + """ + Print the outputs to the screen and to the output file + :param model: the model object + :type model: :class:`~geophires_x.Model.Model` + :return: None + """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) # Deal with converting Units back to PreferredUnits, if required. - # before we write the outputs, we go thru all the parameters for all of the objects and set the values back to the units that the user entered the data in - # We do this because the value may be displayed in the output, and we want the user to recognize their value, not some converted value + # before we write the outputs, we go thru all the parameters for all of the objects and set the values + # back to the units that the user entered the data in + # We do this because the value may be displayed in the output, and we want the user to recognize their value, + # not some converted value for obj in [model.reserv, model.wellbores, model.surfaceplant, model.economics]: for key in obj.ParameterDict: param = obj.ParameterDict[key] @@ -28,7 +38,8 @@ def PrintOutputs(self, model: Model): ConvertUnitsBack(param, model) # now we need to loop thru all thw output parameters to update their units to whatever units the user has specified. - # i.e., they may have specified that all LENGTH results must be in feet, so we need to convert those from whatever LENGTH unit they are to feet. + # i.e., they may have specified that all LENGTH results must be in feet, so we need to convert + # those from whatever LENGTH unit they are to feet. # same for all the other classes of units (TEMPERATURE, DENSITY, etc). for obj in [model.reserv, model.wellbores, model.surfaceplant, model.economics]: diff --git a/src/geophires_x/AGSSurfacePlant.py b/src/geophires_x/AGSSurfacePlant.py index 723dc9bb..ffc4b424 100644 --- a/src/geophires_x/AGSSurfacePlant.py +++ b/src/geophires_x/AGSSurfacePlant.py @@ -24,10 +24,9 @@ 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 self: Reference the class object itself :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, and is used to initialize the class - :doc-author: Malcolm Ross """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) @@ -295,10 +294,9 @@ 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 self: Access variables that belong to a class :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 - :doc-author: Malcolm Ross """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) super().read_parameters(model) @@ -342,10 +340,10 @@ def verify(self, model: Model) -> int: """ The validate function checks that all values provided are within the range expected by AGS modeling system. These values in within a smaller range than the value ranges available to GEOPHIRES-X - :param self: Access variables that belong to a class :param model: The container class of the application, giving access to everything else, including the logger + :type model: :class:`~geophires_x.Model.Model` :return: 0 if all OK, 1 if error. - :doc-author: Koenraad Beckers + :rtype: int """ model.logger.info(f'Init {str(__class__)}: {sys._getframe().f_code.co_name}') self.error = 0 @@ -398,18 +396,30 @@ def on_invalid_parameter_value(err_msg): return self.error def calculatepumpingpower(self, model): - # Calculate pumping power - self.PumpingPower = ( - self.P_in - self.Linear_production_pressure) * model.wellbores.prodwellflowrate.value / self.Average_fluid_density / self.Pump_efficiency / 1e3 # Pumping power [kW] + """ + The calculatepumpingpower function calculates the pumping power needed to pump the fluid from the injection well + to the production well. + :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 + """ + self.PumpingPower = ((self.P_in - self.Linear_production_pressure) * model.wellbores.prodwellflowrate.value / + self.Average_fluid_density / self.Pump_efficiency / 1e3) # Pumping power [kW] # Set negative values to zero (if the production pressure is above the injection pressure, we throttle the fluid) self.PumpingPower[self.PumpingPower < 0] = 0 self.Annual_pumping_power = 8760 / 5 * ( - self.PumpingPower[0::4][0:-1] + self.PumpingPower[1::4] + self.PumpingPower[2::4] + self.PumpingPower[ - 3::4] + self.PumpingPower[ - 4::4]) # kWh + self.PumpingPower[0::4][0:-1] + self.PumpingPower[1::4] + self.PumpingPower[2::4] + self.PumpingPower[ + 3::4] + self.PumpingPower[ + 4::4]) # kWh def calculateheatproduction(self, model): + """ + The calculateheatproduction function calculates the heat production of the AGS system. + :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 + """ # Calculate instantaneous heat production self.Average_fluid_density = interpn((model.wellbores.Pvector, model.wellbores.Tvector), model.wellbores.density, @@ -443,7 +453,13 @@ def calculateheatproduction(self, model): self.calculatepumpingpower(model) def calculateelectricityproduction(self, model): - # Calculate instantaneous exergy production, exergy extraction, and electricity generation (MW) and annual electricity generation [kWh] + """ + The calculateelectricityproduction function calculates instantaneous exergy production, exergy extraction, + and electricity generation (MW) and annual electricity generation [kWh]the electricity production of the AGS system. + :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 + """ Pre_cooling = Compressor_Work = 0.0 self.h_prod = self.hprod # produced enthalpy [J/kg] self.h_inj = self.hinj # injected enthalpy [J/kg] @@ -463,18 +479,18 @@ def calculateelectricityproduction(self, model): self.AverageInstNetExergyExtraction = np.average(self.Instantaneous_exergy_extraction) # [kW] if model.wellbores.Fluid.value == WorkingFluid.WATER: - if model.wellbores.Tinj.value >= 50 and min(self.Linear_production_temperature) >= 100 and max( - self.Linear_production_temperature) <= 385: + if model.wellbores.Tinj.value >= 50 and min(self.Linear_production_temperature) >= 100 and max(self.Linear_production_temperature) <= 385: # Utilization efficiency based on conversion of produced exergy to electricity self.Instantaneous_utilization_efficiency_method_1 = np.interp(self.Linear_production_temperature, self.Utilization_efficiency_correlation_temperatures, self.Utilization_efficiency_correlation_conversion, left=0) self.Instantaneous_electricity_production_method_1 = self.Instantaneous_exergy_production * self.Instantaneous_utilization_efficiency_method_1 # [kW] + # Utilization efficiency based on conversion of produced exergy to electricity self.Instantaneous_thermal_efficiency = np.interp(self.Linear_production_temperature, self.Heat_to_power_efficiency_correlation_temperatures, self.Heat_to_power_efficiency_correlation_conversion, - left=0) # Utilization efficiency based on conversion of produced exergy to electricity + left=0) self.Instantaneous_electricity_production_method_3 = self.Instantaneous_heat_production * self.Instantaneous_thermal_efficiency # [kW] else: @@ -487,11 +503,11 @@ def calculateelectricityproduction(self, model): # based on method 1 for now (could be 50-50) self.Annual_electricity_production = 8760 / 5 * ( - self.Instantaneous_electricity_production_method_1[0::4][0:-1] + - self.Instantaneous_electricity_production_method_1[1::4] + - self.Instantaneous_electricity_production_method_1[2::4] + - self.Instantaneous_electricity_production_method_1[3::4] + - self.Instantaneous_electricity_production_method_1[4::4]) + self.Instantaneous_electricity_production_method_1[0::4][0:-1] + + self.Instantaneous_electricity_production_method_1[1::4] + + self.Instantaneous_electricity_production_method_1[2::4] + + self.Instantaneous_electricity_production_method_1[3::4] + + self.Instantaneous_electricity_production_method_1[4::4]) self.Inst_electricity_production = self.Instantaneous_electricity_production_method_1 # kW self.AveInstElectricityProduction = np.average(self.Instantaneous_electricity_production_method_1) # kW @@ -500,8 +516,7 @@ def calculateelectricityproduction(self, model): P_prod = self.Linear_production_pressure # Production pressure [Pa] h_turbine_out_ideal = interpn((model.wellbores.Pvector_ap, model.wellbores.svector_ap), model.wellbores.hPs, - np.dstack((np.ones(self.TNOP) * self.Turbine_outlet_pressure.value * 1e5, - self.s_prod))[0]) + np.dstack((np.ones(self.TNOP) * self.Turbine_outlet_pressure.value * 1e5, self.s_prod))[0]) self.Instantaneous_turbine_power = model.wellbores.prodwellflowrate.value * ( self.h_prod - h_turbine_out_ideal) * self.Turbine_isentropic_efficiency.value / 1000 # Turbine output [kW] h_turbine_out_actual = self.h_prod - self.Instantaneous_turbine_power / model.wellbores.prodwellflowrate.value * 1000 # Actual fluid enthalpy at turbine outlet [J/kg] @@ -521,7 +536,7 @@ def calculateelectricityproduction(self, model): # Pre-compressor cooling [kWth] Pre_cooling = model.wellbores.prodwellflowrate.value * ( - h_turbine_out_actual - Pre_compressor_h) / 1e3 + h_turbine_out_actual - Pre_compressor_h) / 1e3 Pre_compressor_s = interpn((model.wellbores.Pvector, model.wellbores.Tvector), model.wellbores.entropy, np.array( [self.Turbine_outlet_pressure.value * 1e5, self.Pre_cooling_temperature + 273.15])) @@ -530,15 +545,15 @@ def calculateelectricityproduction(self, model): model.wellbores.hPs, np.array([self.P_in, Pre_compressor_s[0]])) # Actual fluid enthalpy at compressor outlet [J/kg] Post_compressor_h_actual = Pre_compressor_h + ( - Post_compressor_h_ideal - Pre_compressor_h) / self.Compressor_isentropic_efficiency.value + Post_compressor_h_ideal - Pre_compressor_h) / self.Compressor_isentropic_efficiency.value self.Post_compressor_T_actual = interpn((model.wellbores.Pvector_ap, model.wellbores.hvector_ap), model.wellbores.TPh, np.array([self.P_in, Post_compressor_h_actual[0]])) - 273.15 Compressor_Work = model.wellbores.prodwellflowrate.value * ( - Post_compressor_h_actual - Pre_compressor_h) / 1e3 # kWe + Post_compressor_h_actual - Pre_compressor_h) / 1e3 # kWe # Fluid cooling after compression [kWth] Post_cooling = model.wellbores.prodwellflowrate.value * ( - Post_compressor_h_actual - self.h_inj) / 1e3 + Post_compressor_h_actual - self.h_inj) / 1e3 if lastrun == 0: if self.Pre_cooling_temperature < 32: @@ -628,12 +643,9 @@ def calculateelectricityproduction(self, model): def initialize(self, model: Model) -> None: """ The initialize function reads values and arrays to be in the format that AGS model systems expects - - :param self: Access variables that belong to a class :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 - :doc-author: Koenraad Beckers """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) @@ -710,12 +722,9 @@ def initialize(self, model: Model) -> None: def Calculate(self, model: Model) -> None: """ The calculate function verifies, initializes, and extracts the values from the AGS model - - :param self: Access variables that belong to a class :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 - :doc-author: Koenraad Beckers """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) if (model.wellbores.Tini > 375.0) or (model.wellbores.numnonverticalsections.value > 1): @@ -753,11 +762,11 @@ def Calculate(self, model: Model) -> None: for i in range(0, self.plantlifetime.value): self.HeatkWhExtracted.value[i] = np.trapz(self.HeatExtracted.value[ (i * model.economics.timestepsperyear.value):(( - i + 1) * model.economics.timestepsperyear.value) + 1], + i + 1) * model.economics.timestepsperyear.value) + 1], dx=1. / model.economics.timestepsperyear.value * 365. * 24.) * 1000. * self.utilfactor.value self.PumpingkWh.value[i] = np.trapz(model.wellbores.PumpingPower.value[ (i * model.economics.timestepsperyear.value):(( - i + 1) * model.economics.timestepsperyear.value) + 1], + i + 1) * model.economics.timestepsperyear.value) + 1], dx=1. / model.economics.timestepsperyear.value * 365. * 24.) * 1000. * self.utilfactor.value self.RemainingReservoirHeatContent.value = model.reserv.InitialReservoirHeatContent.value - np.cumsum( @@ -768,7 +777,7 @@ def Calculate(self, model: Model) -> None: for i in range(0, self.plantlifetime.value): self.HeatkWhProduced.value[i] = np.trapz(self.HeatProduced.value[ (0 + i * model.economics.timestepsperyear.value):(( - i + 1) * model.economics.timestepsperyear.value) + 1], + i + 1) * model.economics.timestepsperyear.value) + 1], dx=1. / model.economics.timestepsperyear.value * 365. * 24.) * 1000. * self.utilfactor.value else: # copy some arrays so we have a GEOPHIRES equivalent @@ -785,8 +794,7 @@ def Calculate(self, model: Model) -> None: self.NetElectricityProduced.value = f(np.arange(0, 40, 1.0)) self.NetkWhProduced.value = (self.NetElectricityProduced.value * 1000.0) * 8760.0 - self.FirstLawEfficiency.value = ( - self.NetElectricityProduced.value * 1000.0) / self.AveInstHeatProduction.value + self.FirstLawEfficiency.value = (self.NetElectricityProduced.value * 1000.0) / self.AveInstHeatProduction.value # handle errors if len(self.error_codes) > 0: @@ -795,12 +803,6 @@ def Calculate(self, model: Model) -> None: model.logger.fatal(class_file_info_msg) print(f'Error: {class_file_info_msg}. Exiting....') raise RuntimeError(base_msg) - # store the calculation result and associated object parameters in the database - resultkey = AdvGeoPHIRESUtils.store_result(model, self) - if resultkey.startswith("ERROR"): - model.logger.warn(f"Failed To Store {str(__class__)} {os.path.abspath(__file__)}") - elif len(resultkey) == 0: - pass model.logger.info(f"complete {str(__class__)}: {sys._getframe().f_code.co_name}") diff --git a/src/geophires_x/AGSWellBores.py b/src/geophires_x/AGSWellBores.py index e99be58f..bd3f95d5 100644 --- a/src/geophires_x/AGSWellBores.py +++ b/src/geophires_x/AGSWellBores.py @@ -46,6 +46,13 @@ def get(fname, case, fluid): return d def __init__(self, fname, case, fluid): + """ + Initialize the data structures for a given case and fluid + :param fname: h5 file name + :param case: case name + :param fluid: fluid name + :return: None + """ self.fluid = fluid self.case = case @@ -158,15 +165,25 @@ def pointsource(self, yy, zz, yt, zt, ye, ze, alpha, sp, t): """ point source/sink solution functions :param yy: y coordinate of the point source/sink + :type yy: float :param zz: z coordinate of the point source/sink + :type zz: float :param yt: y coordinate of the point source/sink + :type yt: float :param zt: z coordinate of the point source/sink + :type zt: float :param ye: y coordinate of the point source/sink + :type ye: float :param ze: z coordinate of the point source/sink + :type ze: float :param alpha: thermal diffusivity + :type alpha: float :param sp: Laplace variable + :type sp: float :param t: time + :type t: float :return: z + :rtype: float """ rhorock_cprock_4 = self.rhorock * self.cprock * 4.0 theta_yt_minus_yy = thetaY(yt - yy, ye, alpha, t) @@ -186,14 +203,23 @@ def chebeve_pointsource(self, yy, zz, yt, zt, ye, ze, alpha, sp) -> float: """ Chebyshev approximation for numerical Laplace transformation integration from 1e-8 to 1e30 :param yy: y coordinate of the point source/sink + :type yy: float :param zz: z coordinate of the point source/sink + :type zz: float :param yt: y coordinate of the point source/sink + :type yt: float :param zt: z coordinate of the point source/sink + :type zt: float :param ye: y coordinate of the point source/sink + :type ye: float :param ze: z coordinate of the point source/sink + :type ze: float :param alpha: thermal diffusivity + :type alpha: float :param sp: Laplace variable + :type sp: float :return: ???? + :rtype: float """ m = 32 t_1 = 1.0e-8 @@ -213,7 +239,9 @@ def laplace_solution(self, sp) -> float: """ Duhamel convolution method for closed-loop system :param sp: Laplace variable + :type sp: float :return: Toutletl + :rtype: float """ Toutletl = 0.0 @@ -232,8 +260,11 @@ def inverselaplace(self, NL, MM): """ Numerical Laplace transformation algorithm :param NL: NL + :type NL: int :param MM: MM + :type MM: int :return: Toutlet + :rtype: float """ V = np.zeros(50) Gi = np.zeros(50) @@ -337,18 +368,31 @@ def Chebyshev(self, a, b, n, yy, zz, yt, zt, ye, ze, alpha, sp, func): """ Chebyshev approximation for numerical Laplace transformation integration from 1e-8 to 1e30 :param a: a + :type a: float :param b: b + :type b: float :param n: n + :type n: int :param yy: y coordinate of the point source/sink + :type yy: float :param zz: z coordinate of the point source/sink + :type zz: float :param yt: y coordinate of the point source/sink + :type yt: float :param zt: z coordinate of the point source/sink + :type zt: float :param ye: y coordinate of the point source/sink + :type ye: float :param ze: z coordinate of the point source/sink + :type ze: float :param alpha: thermal diffusivity + :type alpha: float :param sp: Laplace variable + :type sp: float :param func: function + :type func: function :return: y * d - dd + 0.5 * cint[0] + :rtype: float """ bma = 0.5 * (b - a) bpa = 0.5 * (b + a) @@ -399,6 +443,7 @@ def __init__(self, model: Model): 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: :class:`~geophires_x.Model.Model` :return: Nothing, and is used to initialize the class """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) @@ -597,6 +642,7 @@ def read_parameters(self, model: Model) -> None: 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("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) @@ -657,7 +703,9 @@ def calculatedrillinglengths(self, model) -> tuple: """ returns the total length, vertical length, and horizontal lengths, depending on the configuration :param model: The container class of the application, giving access to everything else, including the logger + :type model: :class:`~geophires_x.Model.Model` :return: total length, vertical length, and horizontal lengths + :rtype: tuple """ if self.Configuration.value == Configuration.ULOOP: # Total drilling depth of both wells and laterals in U-loop [m] @@ -674,6 +722,7 @@ def initialize(self, model: Model) -> None: """ The initialize function reads values and arrays to be in the format that CLGS model systems expects :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("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) @@ -734,6 +783,7 @@ def getTandP(self, model: Model) -> None: """ The getTandP function reads and prepares Temperature and Pressure values from the CLGS database :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("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) @@ -758,7 +808,9 @@ def verify(self, model: Model) -> int: The verify function checks that all values provided are within the range expected by CLGS modeling system. These values in within a smaller range than the value ranges available to GEOPHIRES-X :param model: The container class of the application, giving access to everything else, including the logger + :type model: :class:`~geophires_x.Model.Model` :return: 0 if all OK, 1 if error. + :rtype: int """ model.logger.info(f"Init {str(__class__)}: {sys._getframe().f_code.co_name}") @@ -791,10 +843,15 @@ def CalculateNonverticalPressureDrop(self, model, time_operation: float, time_ma """ Calculate nonvertical pressure drops - it will vary as the temperature varies :param model: The container class of the application, giving access to everything else, including the logger + :type model: :class:`~geophires_x.Model.Model` :param time_operation: time of operation + :type time_operation: float :param time_max: maximum time of operation + :type time_max: float :param al: time step + :type al: float :return: NonverticalPressureDrop, friction + :rtype: tuple """ friction = 0.0 NonverticalPressureDrop = [0.0] * model.surfaceplant.plantlifetime.value # initialize the array @@ -841,6 +898,7 @@ def Calculate(self, model: Model) -> None: """ The calculate function verifies, initializes, and extracts the values from the AGS model :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("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) diff --git a/src/geophires_x/CLEconomics.py b/src/geophires_x/CLEconomics.py index c634a5fb..7e1cbfb4 100644 --- a/src/geophires_x/CLEconomics.py +++ b/src/geophires_x/CLEconomics.py @@ -16,24 +16,22 @@ 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 closed loop Economics. - :param self: Store data that will be used by the class + Sets 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. :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 - :doc-author: Malcolm Ross """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) - # 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 contains a list of all the parameters set in this object, stored as "Parameter" # and "OutputParameter" Objects. This will alow us later to access them in a user interface and get that list, # along with unit type, preferred units, etc. @@ -90,7 +88,8 @@ def __init__(self, model: Model): ToolTipText="Set user specified all-in cost per meter of vertical drilling, including \ drilling, casing, cement, insulated insert" ) - self.Nonvertical_drilling_cost_per_m = self.ParameterDict[self.Nonvertical_drilling_cost_per_m.Name] = floatParameter( + self.Nonvertical_drilling_cost_per_m = self.ParameterDict[ + self.Nonvertical_drilling_cost_per_m.Name] = floatParameter( "All-in Nonvertical Drilling Costs", value=1300.0, DefaultValue=1300.0, @@ -132,21 +131,21 @@ def __init__(self, model: Model): def read_parameters(self, model: Model) -> None: """ read_parameters read and update the Economics parameters and handle the special cases - Args: - model (Model): The container class of the application, giving access to everything else, including the logger + Deals 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 talen care of after a vlaue 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. + :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("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 talen care of after a vlaue 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(): @@ -209,22 +208,19 @@ 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 self: Access variables that belongs to the class + 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 the 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 - :doc-author: Malcolm Ross """ model.logger.info("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 the values available to your methods. but you had better have set all the parameters! - # ------------- # capital costs # ------------- @@ -236,13 +232,17 @@ def Calculate(self, model: Model) -> None: else: # well drilling and completion cost in M$/well if self.horizontalwellcorrelation.value == WellDrillingCostCorrelation.VERTICAL_SMALL: - self.C1well.value = (0.3021 * model.clwellbores.l_pipe.value ** 2 + 584.9112 * model.clwellbores.l_pipe.value + 751368.) * 1E-6 + self.C1well.value = ( + 0.3021 * model.clwellbores.l_pipe.value ** 2 + 584.9112 * model.clwellbores.l_pipe.value + 751368.) * 1E-6 elif self.horizontalwellcorrelation.value == WellDrillingCostCorrelation.DEVIATED_SMALL: - self.C1well.value = (0.2898 * model.clwellbores.l_pipe.value ** 2 + 822.1507 * model.clwellbores.l_pipe.value + 680563.) * 1E-6 + self.C1well.value = ( + 0.2898 * model.clwellbores.l_pipe.value ** 2 + 822.1507 * model.clwellbores.l_pipe.value + 680563.) * 1E-6 elif self.horizontalwellcorrelation.value == WellDrillingCostCorrelation.VERTICAL_LARGE: - self.C1well.value = (0.2818 * model.clwellbores.l_pipe.value ** 2 + 1275.5213 * model.clwellbores.l_pipe.value + 632315.) * 1E-6 + self.C1well.value = ( + 0.2818 * model.clwellbores.l_pipe.value ** 2 + 1275.5213 * model.clwellbores.l_pipe.value + 632315.) * 1E-6 elif self.horizontalwellcorrelation.value == WellDrillingCostCorrelation.DEVIATED_LARGE: - self.C1well.value = (0.2553 * model.clwellbores.l_pipe.value ** 2 + 1716.7157 * model.clwellbores.l_pipe.value + 500867.) * 1E-6 + self.C1well.value = ( + 0.2553 * model.clwellbores.l_pipe.value ** 2 + 1716.7157 * model.clwellbores.l_pipe.value + 500867.) * 1E-6 else: # MIR MIR MIR pass diff --git a/src/geophires_x/CLOutputs.py b/src/geophires_x/CLOutputs.py index fa9dae3e..b05e838b 100644 --- a/src/geophires_x/CLOutputs.py +++ b/src/geophires_x/CLOutputs.py @@ -6,8 +6,15 @@ class CLOutputs(Outputs): - """description of class""" + """Handles thr closed-loop specific outputs""" def PrintOutputs(self, model: Model): + """ + Prints the closed-loop specific outputs + :param model: The model object + :type model: :class:`~geophires_x.Model.Model` + :return: None + """ + model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) # now do CL output, which will append to the original output diff --git a/src/geophires_x/CLWellBores.py b/src/geophires_x/CLWellBores.py index aab55ae7..ff2b5c39 100644 --- a/src/geophires_x/CLWellBores.py +++ b/src/geophires_x/CLWellBores.py @@ -1,7 +1,3 @@ -import math -import sys -import os -import numpy as np from scipy.interpolate import interpn, interp1d import geophires_x.Model as Model from geophires_x.WellBores import * diff --git a/src/geophires_x/CylindricalReservoir.py b/src/geophires_x/CylindricalReservoir.py index 7ffd38c5..e1f3a82f 100644 --- a/src/geophires_x/CylindricalReservoir.py +++ b/src/geophires_x/CylindricalReservoir.py @@ -1,116 +1,31 @@ -import sys -import os -import math -from functools import lru_cache -import numpy as np -import geophires_x.Model as Model -from .Parameter import floatParameter, OutputParameter -from .Units import * from .Reservoir import * class CylindricalReservoir(Reservoir): """ The CylindricalReservoir class is a subclass of the Reservoir class in a straightforward conduction-only model. - It inherits from the primary Reservoir model but offers new parameters and calculations. - - .. list-table:: Input Parameters - :widths: 25 50 10 10 10 10 25 - :header-rows: 1 - - * - Name - - Description - - Default Value Type - - Default Value - - Min - - Max - - Preferred Units - * - Cylindrical Reservoir Input Depth - - Depth of the inflow end of a cylindrical reservoir. - - float - - 3.0 - - 0.1 - - 15 - - LengthUnit.KILOMETERS - * - Cylindrical Reservoir Output Depth - - Depth of the outflow end of a cylindrical reservoir - - float - - Input Depth - - 0.1 - - 15 - - LengthUnit.KILOMETERS - * - Reservoir Length - - Length of cylindrical reservoir - - float - - 4.0 - - 0.1 - - 10.0 - - LengthUnit.KILOMETERS - * - Cylindrical Reservoir Radius of Effect - - The radius of effect - the distance into the rock from the center of the cylinder that will be perturbed by at least 1 C - - float - - 30.0 - - 0 - - 1000.0 - - LengthUnit.METERS - * - Cylindrical Reservoir Radius of Effect Factor - - The radius of effect reduction factor accounts for the fact that we cannot extract 100% of the heat in the cylinder. - - float - - 1.0 - - 0.0 - - 10.0 - - PercentUnit.TENTH - - .. list-table:: Output Parameters - :widths: 25 20 20 - :header-rows: 1 - - * - Name - - Default Value Type - - Preferred Units - * - Cylindrical Reservoir Surface Area - - float - - METERS2 - * - Average Gradient - - float - - dEGC/KM - * - Time Vector - - float array - - YEAR - * - Reservoir Temperature History - - float array - - CELSIUS - - - :doc-author: Malcolm Ross """ - def __init__(self, model:Model): + 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 the Reservoir. - - :param self: Store data that will be used by the class + 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. :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 - :doc-author: Malcolm Ross """ model.logger.info(f'Init {str(__class__)}: {sys._getframe().f_code.co_name}') super().__init__(model) # initialize the parent parameters and variables - # 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. - self.InputDepth = self.ParameterDict[self.InputDepth.Name] = floatParameter( "Cylindrical Reservoir Input Depth", value=3.0, @@ -238,15 +153,12 @@ def read_parameters(self, model: Model) -> None: """ The read_parameters function reads in the parameters from a dictionary created by reading the user-provided file and updates the parameter values for this object. - The function reads in all the parameters that relate to this object, including those that are inherited from other objects. It then updates any of these parameter values that have been changed by the user. It also handles any special cases. - - :param self: Reference the class instance (such as it is) from within the class :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 - :doc-author: Malcolm Ross """ model.logger.info(f"Init {str(__class__)}: {sys._getframe().f_code.co_name}") super().read_parameters(model) @@ -285,25 +197,21 @@ def read_parameters(self, model: Model) -> None: 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 self: Access variables that belongs to the class + 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 the 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 - :doc-author: Malcolm Ross """ 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 the values available to your methods. but you had - # better have set all the parameters! - # specify time-stepping vectors self.timevector.value = np.linspace(0, model.surfaceplant.plantlifetime.value, model.economics.timestepsperyear.value*model.surfaceplant.plantlifetime.value) diff --git a/src/geophires_x/Economics.py b/src/geophires_x/Economics.py index 55f72b55..45a2ba42 100644 --- a/src/geophires_x/Economics.py +++ b/src/geophires_x/Economics.py @@ -11,7 +11,24 @@ def BuildPricingModel(plantlifetime: int, StartYear: int, StartPrice: float, EndPrice: float, EscalationStart: int, EscalationRate: float): - # build the price model array + """ + BuildPricingModel builds the price model array for the project lifetime. It is used to calculate the revenue + stream for the project. + :param plantlifetime: The lifetime of the project in years + :type plantlifetime: int + :param StartYear: The year the project starts in years (not including construction years) + :type StartYear: int + :param StartPrice: The price in the first year of the project in $/kWh + :type StartPrice: float + :param EndPrice: The price in the last year of the project in $/kWh + :type EndPrice: float + :param EscalationStart: The year the price escalation starts in years (not including construction years) in years + :type EscalationStart: int + :param EscalationRate: The rate of price escalation in $/kWh/year + :type EscalationRate: float + :return: Price: The price model array for the project in $/kWh + :rtype: list + """ Price = [StartPrice] * plantlifetime if StartPrice == EndPrice: return Price @@ -24,6 +41,25 @@ def BuildPricingModel(plantlifetime: int, StartYear: int, StartPrice: float, End def CalculateRevenue(plantlifetime: int, ConstructionYears: int, CAPEX: float, OPEX: float, Energy, Price): + """ + CalculateRevenue calculates the revenue stream for the project. It is used to calculate the revenue + stream for the project. + :param plantlifetime: The lifetime of the project in years in years (not including construction years) in years + :type plantlifetime: int + :param ConstructionYears: The number of years of construction for the project in years + :type ConstructionYears: int + :param CAPEX: The total capital cost of the project in MUSD + :type CAPEX: float + :param OPEX: The total annual operating cost of the project in MUSD + :type OPEX: float + :param Energy: The energy production array for the project in kWh + :type Energy: list + :param Price: The price model array for the project in $/kWh + :type Price: list + :return: CashFlow: The annual cash flow for the project in MUSD and CummCashFlow: The cumulative cash flow for the + project in MUSD + :rtype: list + """ # Calculate the revenue ProjectCAPEXPerConstructionYear = CAPEX / ConstructionYears CashFlow = [0.0] * (plantlifetime + ConstructionYears) @@ -47,6 +83,31 @@ def CalculateRevenue(plantlifetime: int, ConstructionYears: int, CAPEX: float, O def CalculateFinancialPerformance(plantlifetime: int, FixedInternalRate: float, TotalRevenue, TotalCummRevenue, CAPEX: float, OPEX: float): + """ + CalculateFinancialPerformance calculates the financial performance of the project. It is used to calculate the + financial performance of the project. It is used to calculate the revenue stream for the project. + :param plantlifetime: The lifetime of the project in years + :type plantlifetime: int + :param FixedInternalRate: The fixed internal rate of return for the project in % + :type FixedInternalRate: float + :param TotalRevenue: The total revenue stream for the project in MUSD + :type TotalRevenue: list + :param TotalCummRevenue: The total cumulative revenue stream for the project in MUSD + :type TotalCummRevenue: list + :param CAPEX: The total capital cost of the project in MUSD + :type CAPEX: float + :param OPEX: The total annual operating cost of the project in MUSD + :type OPEX: float + :return: NPV: The net present value of the project in MUSD + :rtype: float + :return: IRR: The internal rate of return of the project in % + :rtype: float + :return: VIR: The value to investment ratio of the project in % + :rtype: float + :return: MOIC: The money on investment capital of the project in % + :rtype: float + :rtype: tuple + """ # Calculate financial performance values using numpy financials NPV = npf.npv(FixedInternalRate / 100, TotalRevenue) IRR = npf.irr(TotalRevenue) @@ -61,129 +122,194 @@ def CalculateFinancialPerformance(plantlifetime: int, FixedInternalRate: float, def CalculateLCOELCOH(self, model: Model) -> tuple: - LCOE = LCOH = LCOC = 0.0 + """ + CalculateLCOELCOH calculates the levelized cost of electricity and heat for the project. + :param model: The model object + :type model: :class:`~geophires_x.Model.Model` + :return: LCOE: The levelized cost of electricity and LCOH: The levelized cost of heat and LCOC: The levelized cost of cooling + :rtype: tuple + """ + LCOE = LCOH = LCOC = 0.0 # Calculate LCOE/LCOH/LCOC if self.econmodel.value == EconomicModel.FCR: if model.surfaceplant.enduseoption.value == EndUseOptions.ELECTRICITY: - LCOE = (self.FCR.value*(1+self.inflrateconstruction.value)*self.CCap.value + self.Coam.value) / \ - np.average(model.surfaceplant.NetkWhProduced.value)*1E8 # cents/kWh + LCOE = (self.FCR.value * (1 + self.inflrateconstruction.value) * self.CCap.value + self.Coam.value) / \ + np.average(model.surfaceplant.NetkWhProduced.value) * 1E8 # cents/kWh elif model.surfaceplant.enduseoption.value == EndUseOptions.HEAT: - LCOH = (self.FCR.value*(1+self.inflrateconstruction.value)*self.CCap.value + self.Coam.value + - self.averageannualpumpingcosts.value)/np.average(model.surfaceplant.HeatkWhProduced.value)*1E8 # cents/kWh + LCOH = (self.FCR.value * (1 + self.inflrateconstruction.value) * self.CCap.value + self.Coam.value + + self.averageannualpumpingcosts.value) / np.average( + model.surfaceplant.HeatkWhProduced.value) * 1E8 # cents/kWh LCOH = LCOH * 2.931 # $/Million Btu - elif model.surfaceplant.enduseoption.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICTY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICTY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICTY]: #co-gen + elif model.surfaceplant.enduseoption.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, + EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICTY, + EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICTY, + EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, + EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, + EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICTY]: # co-gen # heat sales is additional income revenue stream if model.surfaceplant.enduseoption.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT]: - averageannualheatincome = np.average(self.HeatkWhProduced.value)*self.heatprice.value/1E6 # M$/year ASSUMING heatprice IS IN $/KWH FOR HEAT SALES - LCOE = (self.FCR.value*(1+self.inflrateconstruction.value)*self.CCap.value + self.Coam.value - averageannualheatincome)/np.average(model.surfaceplant.NetkWhProduced.value)*1E8 # cents/kWh + averageannualheatincome = np.average( + self.HeatkWhProduced.value) * self.heatprice.value / 1E6 # M$/year ASSUMING heatprice IS IN $/KWH FOR HEAT SALES + LCOE = (self.FCR.value * ( + 1 + self.inflrateconstruction.value) * self.CCap.value + self.Coam.value - averageannualheatincome) / np.average( + model.surfaceplant.NetkWhProduced.value) * 1E8 # cents/kWh elif model.surfaceplant.enduseoption.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICTY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICTY, EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICTY]: # electricity sales is additional income revenue stream - averageannualelectricityincome = np.average(model.surfaceplant.NetkWhProduced.value)*model.surfaceplant.elecprice.value/1E6 # M$/year - LCOH = (self.CCap.value + self.Coam.value - averageannualelectricityincome)/np.average(model.surfaceplant.HeatkWhProduced.value)*1E8 # cents/kWh + averageannualelectricityincome = np.average( + model.surfaceplant.NetkWhProduced.value) * model.surfaceplant.elecprice.value / 1E6 # M$/year + LCOH = (self.CCap.value + self.Coam.value - averageannualelectricityincome) / np.average( + model.surfaceplant.HeatkWhProduced.value) * 1E8 # cents/kWh LCOH = LCOH * 2.931 # $/MMBTU elif model.surfaceplant.enduseoption.value == EndUseOptions.ABSORPTION_CHILLER: - LCOC = (self.FCR.value*(1+self.inflrateconstruction.value)*self.CCap.value + self.Coam.value + self.averageannualpumpingcosts.value)/np.average(model.surfaceplant.CoolingkWhProduced.value)*1E8 #cents/kWh - LCOC = LCOC*2.931 #$/Million Btu + LCOC = (self.FCR.value * ( + 1 + self.inflrateconstruction.value) * self.CCap.value + self.Coam.value + self.averageannualpumpingcosts.value) / np.average( + model.surfaceplant.CoolingkWhProduced.value) * 1E8 # cents/kWh + LCOC = LCOC * 2.931 # $/Million Btu elif model.surfaceplant.enduseoption.value == EndUseOptions.HEAT_PUMP: - LCOH = (self.FCR.value*(1+self.inflrateconstruction.value)*self.CCap.value + self.Coam.value + self.averageannualpumpingcosts.value + self.averageannualheatpumpelectricitycost.value)/np.average(model.surfaceplant.HeatkWhProduced.value)*1E8 #cents/kWh - LCOH = LCOH*2.931 #$/Million Btu + LCOH = (self.FCR.value * ( + 1 + self.inflrateconstruction.value) * self.CCap.value + self.Coam.value + self.averageannualpumpingcosts.value + self.averageannualheatpumpelectricitycost.value) / np.average( + model.surfaceplant.HeatkWhProduced.value) * 1E8 # cents/kWh + LCOH = LCOH * 2.931 # $/Million Btu elif model.surfaceplant.enduseoption.value == EndUseOptions.DISTRICT_HEATING: - LCOH = (self.FCR.value*(1+self.inflrateconstruction.value)*self.CCap.value + self.Coam.value + self.averageannualpumpingcosts.value + self.averageannualngcost.value)/model.surfaceplant.annualheatingdemand.value*1E2 #cents/kWh - LCOH = LCOH*2.931 #$/Million Btu + LCOH = (self.FCR.value * ( + 1 + self.inflrateconstruction.value) * self.CCap.value + self.Coam.value + self.averageannualpumpingcosts.value + self.averageannualngcost.value) / model.surfaceplant.annualheatingdemand.value * 1E2 # cents/kWh + LCOH = LCOH * 2.931 # $/Million Btu elif self.econmodel.value == EconomicModel.STANDARDIZED_LEVELIZED_COST: - discountvector = 1./np.power(1+self.discountrate.value, np.linspace(0, model.surfaceplant.plantlifetime.value-1, model.surfaceplant.plantlifetime.value)) + discountvector = 1. / np.power(1 + self.discountrate.value, + np.linspace(0, model.surfaceplant.plantlifetime.value - 1, + model.surfaceplant.plantlifetime.value)) if model.surfaceplant.enduseoption.value == EndUseOptions.ELECTRICITY: - LCOE = ((1+self.inflrateconstruction.value)*self.CCap.value + np.sum(self.Coam.value*discountvector))/np.sum(model.surfaceplant.NetkWhProduced.value*discountvector)*1E8 # cents/kWh + LCOE = ((1 + self.inflrateconstruction.value) * self.CCap.value + np.sum( + self.Coam.value * discountvector)) / np.sum( + model.surfaceplant.NetkWhProduced.value * discountvector) * 1E8 # cents/kWh elif model.surfaceplant.enduseoption.value == EndUseOptions.HEAT: - self.averageannualpumpingcosts.value = np.average(model.surfaceplant.PumpingkWh.value)*model.surfaceplant.elecprice.value/1E6 # M$/year - LCOH = ((1+self.inflrateconstruction.value)*self.CCap.value + np.sum((self.Coam.value+model.surfaceplant.PumpingkWh.value*model.surfaceplant.elecprice.value/1E6)*discountvector))/np.sum(model.surfaceplant.HeatkWhProduced.value*discountvector)*1E8 # cents/kWh + self.averageannualpumpingcosts.value = np.average( + model.surfaceplant.PumpingkWh.value) * model.surfaceplant.elecprice.value / 1E6 # M$/year + LCOH = ((1 + self.inflrateconstruction.value) * self.CCap.value + np.sum(( + self.Coam.value + model.surfaceplant.PumpingkWh.value * model.surfaceplant.elecprice.value / 1E6) * discountvector)) / np.sum( + model.surfaceplant.HeatkWhProduced.value * discountvector) * 1E8 # cents/kWh LCOH = LCOH * 2.931 # $/MMBTU - elif model.surfaceplant.enduseoption.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICTY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICTY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICTY]: #co-gen + elif model.surfaceplant.enduseoption.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, + EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICTY, + EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICTY, + EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, + EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, + EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICTY]: # co-gen if model.surfaceplant.enduseoption.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT]: # heat sales is additional income revenue stream - annualheatincome = model.surfaceplant.HeatkWhProduced.value*model.surfaceplant.heatprice.value/1E6 # M$/year ASSUMING heatprice IS IN $/KWH FOR HEAT SALES - LCOE = ((1+self.inflrateconstruction.value)*self.CCap.value + np.sum((self.Coam.value-annualheatincome)*discountvector))/np.sum(model.surfaceplant.NetkWhProduced.value*discountvector)*1E8 # cents/kWh + annualheatincome = model.surfaceplant.HeatkWhProduced.value * model.surfaceplant.heatprice.value / 1E6 # M$/year ASSUMING heatprice IS IN $/KWH FOR HEAT SALES + LCOE = ((1 + self.inflrateconstruction.value) * self.CCap.value + np.sum( + (self.Coam.value - annualheatincome) * discountvector)) / np.sum( + model.surfaceplant.NetkWhProduced.value * discountvector) * 1E8 # cents/kWh elif model.surfaceplant.enduseoption.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICTY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICTY, EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICTY]: # electricity sales is additional income revenue stream - annualelectricityincome = model.surfaceplant.NetkWhProduced.value*self.elecprice.value/1E6 # M$/year - LCOH = ((1+self.inflrateconstruction.value)*self.CCap.value + np.sum((self.Coam.value-annualelectricityincome)*discountvector))/np.sum(model.surfaceplant.HeatkWhProduced.value*discountvector)*1E8 # cents/kWh - LCOH = LCOH*2.931 # $/MMBTU + annualelectricityincome = model.surfaceplant.NetkWhProduced.value * self.elecprice.value / 1E6 # M$/year + LCOH = ((1 + self.inflrateconstruction.value) * self.CCap.value + np.sum( + (self.Coam.value - annualelectricityincome) * discountvector)) / np.sum( + model.surfaceplant.HeatkWhProduced.value * discountvector) * 1E8 # cents/kWh + LCOH = LCOH * 2.931 # $/MMBTU elif model.surfaceplant.enduseoption.value == EndUseOptions.ABSORPTION_CHILLER: - LCOC = ((1+self.inflrateconstruction.value)*self.CCap.value + np.sum((self.Coam.value+model.surfaceplant.PumpingkWh.value*model.surfaceplant.elecprice.value/1E6)*discountvector))/np.sum(model.surfaceplant.CoolingkWhProduced.value*discountvector)*1E8 #cents/kWh - LCOC = LCOC*2.931 #$/Million Btu + LCOC = ((1 + self.inflrateconstruction.value) * self.CCap.value + np.sum(( + self.Coam.value + model.surfaceplant.PumpingkWh.value * model.surfaceplant.elecprice.value / 1E6) * discountvector)) / np.sum( + model.surfaceplant.CoolingkWhProduced.value * discountvector) * 1E8 # cents/kWh + LCOC = LCOC * 2.931 # $/Million Btu elif model.surfaceplant.enduseoption.value == EndUseOptions.HEAT_PUMP: - LCOH = ((1+self.inflrateconstruction.value)*self.CCap.value + np.sum((self.Coam.value+model.surfaceplant.PumpingkWh.value*model.surfaceplant.elecprice.value/1E6+\ - model.surfaceplant.HeatPumpElectricitykWhUsed.value*model.surfaceplant.elecprice.value/1E6)*discountvector))/np.sum(model.surfaceplant.HeatkWhProduced.value*discountvector)*1E8 #cents/kWh - LCOH = LCOH*2.931 #$/Million Btu + LCOH = ((1 + self.inflrateconstruction.value) * self.CCap.value + np.sum( + (self.Coam.value + model.surfaceplant.PumpingkWh.value * model.surfaceplant.elecprice.value / 1E6 + \ + model.surfaceplant.HeatPumpElectricitykWhUsed.value * model.surfaceplant.elecprice.value / 1E6) * discountvector)) / np.sum( + model.surfaceplant.HeatkWhProduced.value * discountvector) * 1E8 # cents/kWh + LCOH = LCOH * 2.931 # $/Million Btu elif model.surfaceplant.enduseoption.value == EndUseOptions.DISTRICT_HEATING: - LCOH = ((1+self.inflrateconstruction.value)*self.CCap.value + np.sum((self.Coam.value+model.surfaceplant.PumpingkWh.value*model.surfaceplant.elecprice.value/1E6+\ - self.annualngcost.value)*discountvector))/np.sum(model.surfaceplant.annualheatingdemand.value*discountvector)*1E2 #cents/kWh - LCOH = self.LCOH.value*2.931 #$/Million Btu + LCOH = ((1 + self.inflrateconstruction.value) * self.CCap.value + np.sum( + (self.Coam.value + model.surfaceplant.PumpingkWh.value * model.surfaceplant.elecprice.value / 1E6 + \ + self.annualngcost.value) * discountvector)) / np.sum( + model.surfaceplant.annualheatingdemand.value * discountvector) * 1E2 # cents/kWh + LCOH = self.LCOH.value * 2.931 # $/Million Btu elif self.econmodel.value == EconomicModel.BICYCLE: - iave = self.FIB.value*self.BIR.value*(1-self.CTR.value) + (1-self.FIB.value)*self.EIR.value # average return on investment (tax and inflation adjusted) - CRF = iave/(1-np.power(1+iave, -model.surfaceplant.plantlifetime.value)) # capital recovery factor - inflationvector = np.power(1+self.RINFL.value, np.linspace(1, model.surfaceplant.plantlifetime.value, model.surfaceplant.plantlifetime.value)) - discountvector = 1./np.power(1+iave, np.linspace(1, model.surfaceplant.plantlifetime.value, model.surfaceplant.plantlifetime.value)) - NPVcap = np.sum((1+self.inflrateconstruction.value)*self.CCap.value*CRF*discountvector) - NPVfc = np.sum((1+self.inflrateconstruction.value)*self.CCap.value*self.PTR.value*inflationvector*discountvector) - NPVit = np.sum(self.CTR.value/(1-self.CTR.value)*((1+self.inflrateconstruction.value)*self.CCap.value*CRF-self.CCap.value/model.surfaceplant.plantlifetime.value)*discountvector) - NPVitc = (1+self.inflrateconstruction.value)*self.CCap.value*self.RITC.value/(1-self.CTR.value) + iave = self.FIB.value * self.BIR.value * (1 - self.CTR.value) + ( + 1 - self.FIB.value) * self.EIR.value # average return on investment (tax and inflation adjusted) + CRF = iave / (1 - np.power(1 + iave, -model.surfaceplant.plantlifetime.value)) # capital recovery factor + inflationvector = np.power(1 + self.RINFL.value, np.linspace(1, model.surfaceplant.plantlifetime.value, + model.surfaceplant.plantlifetime.value)) + discountvector = 1. / np.power(1 + iave, np.linspace(1, model.surfaceplant.plantlifetime.value, + model.surfaceplant.plantlifetime.value)) + NPVcap = np.sum((1 + self.inflrateconstruction.value) * self.CCap.value * CRF * discountvector) + NPVfc = np.sum( + (1 + self.inflrateconstruction.value) * self.CCap.value * self.PTR.value * inflationvector * discountvector) + NPVit = np.sum(self.CTR.value / (1 - self.CTR.value) * (( + 1 + self.inflrateconstruction.value) * self.CCap.value * CRF - self.CCap.value / model.surfaceplant.plantlifetime.value) * discountvector) + NPVitc = (1 + self.inflrateconstruction.value) * self.CCap.value * self.RITC.value / (1 - self.CTR.value) if model.surfaceplant.enduseoption.value == EndUseOptions.ELECTRICITY: - NPVoandm = np.sum(self.Coam.value*inflationvector*discountvector) - NPVgrt = self.GTR.value/(1-self.GTR.value)*(NPVcap + NPVoandm + NPVfc + NPVit - NPVitc) - LCOE = (NPVcap + NPVoandm + NPVfc + NPVit + NPVgrt - NPVitc)/np.sum(model.surfaceplant.NetkWhProduced.value*inflationvector*discountvector)*1E8 + NPVoandm = np.sum(self.Coam.value * inflationvector * discountvector) + NPVgrt = self.GTR.value / (1 - self.GTR.value) * (NPVcap + NPVoandm + NPVfc + NPVit - NPVitc) + LCOE = (NPVcap + NPVoandm + NPVfc + NPVit + NPVgrt - NPVitc) / np.sum( + model.surfaceplant.NetkWhProduced.value * inflationvector * discountvector) * 1E8 elif model.surfaceplant.enduseoption.value == EndUseOptions.HEAT: - PumpingCosts = model.surfaceplant.PumpingkWh.value*model.surfaceplant.elecprice.value/1E6 - NPVoandm = np.sum((self.Coam.value+PumpingCosts)*inflationvector*discountvector) - NPVgrt = self.GTR.value/(1-self.GTR.value)*(NPVcap + NPVoandm + NPVfc + NPVit - NPVitc) - LCOH = (NPVcap + NPVoandm + NPVfc + NPVit + NPVgrt - NPVitc)/np.sum(model.surfaceplant.HeatkWhProduced.value*inflationvector*discountvector)*1E8 - LCOH = LCOH*2.931 # $/MMBTU - elif model.surfaceplant.enduseoption.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICTY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICTY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICTY]: #co-gen + PumpingCosts = model.surfaceplant.PumpingkWh.value * model.surfaceplant.elecprice.value / 1E6 + NPVoandm = np.sum((self.Coam.value + PumpingCosts) * inflationvector * discountvector) + NPVgrt = self.GTR.value / (1 - self.GTR.value) * (NPVcap + NPVoandm + NPVfc + NPVit - NPVitc) + LCOH = (NPVcap + NPVoandm + NPVfc + NPVit + NPVgrt - NPVitc) / np.sum( + model.surfaceplant.HeatkWhProduced.value * inflationvector * discountvector) * 1E8 + LCOH = LCOH * 2.931 # $/MMBTU + elif model.surfaceplant.enduseoption.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, + EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICTY, + EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICTY, + EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, + EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, + EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICTY]: # co-gen if model.surfaceplant.enduseoption.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT]: # heat sales is additional income revenue stream - annualheatincome = model.surfaceplant.HeatkWhProduced.value*model.surfaceplant.heatprice.value/1E6 # M$/year ASSUMING ELECPRICE IS IN $/KWH FOR HEAT SALES - NPVoandm = np.sum(self.Coam.value*inflationvector*discountvector) - NPVgrt = self.GTR.value/(1-self.GTR.value)*(NPVcap + NPVoandm + NPVfc + NPVit - NPVitc) - LCOE = (NPVcap + NPVoandm + NPVfc + NPVit + NPVgrt - NPVitc - np.sum(annualheatincome*inflationvector*discountvector))/np.sum(model.surfaceplant.NetkWhProduced.value*inflationvector*discountvector)*1E8 + annualheatincome = model.surfaceplant.HeatkWhProduced.value * model.surfaceplant.heatprice.value / 1E6 # M$/year ASSUMING ELECPRICE IS IN $/KWH FOR HEAT SALES + NPVoandm = np.sum(self.Coam.value * inflationvector * discountvector) + NPVgrt = self.GTR.value / (1 - self.GTR.value) * (NPVcap + NPVoandm + NPVfc + NPVit - NPVitc) + LCOE = (NPVcap + NPVoandm + NPVfc + NPVit + NPVgrt - NPVitc - np.sum( + annualheatincome * inflationvector * discountvector)) / np.sum( + model.surfaceplant.NetkWhProduced.value * inflationvector * discountvector) * 1E8 elif model.surfaceplant.enduseoption.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICTY, - EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICTY, - EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICTY]: # electricity sales is additional income revenue stream - annualelectricityincome = model.surfaceplant.NetkWhProduced.value*model.surfaceplant.elecprice.value/1E6 # M$/year - NPVoandm = np.sum(self.Coam.value*inflationvector*discountvector) - NPVgrt = self.GTR.value/(1-self.GTR.value)*(NPVcap + NPVoandm + NPVfc + NPVit - NPVitc) - LCOH = (NPVcap + NPVoandm + NPVfc + NPVit + NPVgrt - NPVitc - np.sum(annualelectricityincome*inflationvector*discountvector))/np.sum(model.surfaceplant.HeatkWhProduced.value*inflationvector*discountvector)*1E8 - LCOH = self.LCOELCOHCombined.value*2.931 # $/MMBTU + EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICTY, + EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICTY]: # electricity sales is additional income revenue stream + annualelectricityincome = model.surfaceplant.NetkWhProduced.value * model.surfaceplant.elecprice.value / 1E6 # M$/year + NPVoandm = np.sum(self.Coam.value * inflationvector * discountvector) + NPVgrt = self.GTR.value / (1 - self.GTR.value) * (NPVcap + NPVoandm + NPVfc + NPVit - NPVitc) + LCOH = (NPVcap + NPVoandm + NPVfc + NPVit + NPVgrt - NPVitc - np.sum( + annualelectricityincome * inflationvector * discountvector)) / np.sum( + model.surfaceplant.HeatkWhProduced.value * inflationvector * discountvector) * 1E8 + LCOH = self.LCOELCOHCombined.value * 2.931 # $/MMBTU elif model.surfaceplant.enduseoption.value == EndUseOptions.ABSORPTION_CHILLER: - PumpingCosts = model.surfaceplant.PumpingkWh.value*model.surfaceplant.elecprice.value/1E6 - NPVoandm = np.sum((self.Coam.value+PumpingCosts)*inflationvector*discountvector) - NPVgrt = self.GTR.value/(1-self.GTR.value)*(NPVcap + NPVoandm + NPVfc + NPVit - NPVitc) - LCOC = (NPVcap + NPVoandm + NPVfc + NPVit + NPVgrt - NPVitc)/np.sum(model.surfaceplant.CoolingkWhProduced.value*inflationvector*discountvector)*1E8 - LCOC = LCOC*2.931 #$/MMBTU + PumpingCosts = model.surfaceplant.PumpingkWh.value * model.surfaceplant.elecprice.value / 1E6 + NPVoandm = np.sum((self.Coam.value + PumpingCosts) * inflationvector * discountvector) + NPVgrt = self.GTR.value / (1 - self.GTR.value) * (NPVcap + NPVoandm + NPVfc + NPVit - NPVitc) + LCOC = (NPVcap + NPVoandm + NPVfc + NPVit + NPVgrt - NPVitc) / np.sum( + model.surfaceplant.CoolingkWhProduced.value * inflationvector * discountvector) * 1E8 + LCOC = LCOC * 2.931 # $/MMBTU elif model.surfaceplant.enduseoption.value == EndUseOptions.HEAT_PUMP: - PumpingCosts = model.surfaceplant.PumpingkWh.value*model.surfaceplant.elecprice.value/1E6 - HeatPumpElecCosts = model.surfaceplant.HeatPumpElectricitykWhUsed.value*model.surfaceplant.elecprice.value/1E6 - NPVoandm = np.sum((self.Coam.value+PumpingCosts+HeatPumpElecCosts)*inflationvector*discountvector) - NPVgrt = self.GTR.value/(1-self.GTR.value)*(NPVcap + NPVoandm + NPVfc + NPVit - NPVitc) - LCOH = (NPVcap + NPVoandm + NPVfc + NPVit + NPVgrt - NPVitc)/np.sum(model.surfaceplant.HeatkWhProduced.value*inflationvector*discountvector)*1E8 - LCOH = self.LCOH.value*2.931 #$/MMBTU + PumpingCosts = model.surfaceplant.PumpingkWh.value * model.surfaceplant.elecprice.value / 1E6 + HeatPumpElecCosts = model.surfaceplant.HeatPumpElectricitykWhUsed.value * model.surfaceplant.elecprice.value / 1E6 + NPVoandm = np.sum((self.Coam.value + PumpingCosts + HeatPumpElecCosts) * inflationvector * discountvector) + NPVgrt = self.GTR.value / (1 - self.GTR.value) * (NPVcap + NPVoandm + NPVfc + NPVit - NPVitc) + LCOH = (NPVcap + NPVoandm + NPVfc + NPVit + NPVgrt - NPVitc) / np.sum( + model.surfaceplant.HeatkWhProduced.value * inflationvector * discountvector) * 1E8 + LCOH = self.LCOH.value * 2.931 # $/MMBTU elif model.surfaceplant.enduseoption.value == EndUseOptions.DISTRICT_HEATING: - PumpingCosts = model.surfaceplant.PumpingkWh.value*model.surfaceplant.elecprice.value/1E6 - NPVoandm = np.sum((self.Coam.value+PumpingCosts+self.annualngcost.value)*inflationvector*discountvector) - NPVgrt = self.GTR.value/(1-self.GTR.value)*(NPVcap + NPVoandm + NPVfc + NPVit - NPVitc) - LCOH = (NPVcap + NPVoandm + NPVfc + NPVit + NPVgrt - NPVitc)/np.sum(model.surfaceplant.annualheatingdemand.value*inflationvector*discountvector)*1E2 - LCOH = LCOH*2.931 #$/MMBTU + PumpingCosts = model.surfaceplant.PumpingkWh.value * model.surfaceplant.elecprice.value / 1E6 + NPVoandm = np.sum( + (self.Coam.value + PumpingCosts + self.annualngcost.value) * inflationvector * discountvector) + NPVgrt = self.GTR.value / (1 - self.GTR.value) * (NPVcap + NPVoandm + NPVfc + NPVit - NPVitc) + LCOH = (NPVcap + NPVoandm + NPVfc + NPVit + NPVgrt - NPVitc) / np.sum( + model.surfaceplant.annualheatingdemand.value * inflationvector * discountvector) * 1E2 + LCOH = LCOH * 2.931 # $/MMBTU return LCOE, LCOH, LCOC @@ -192,32 +318,30 @@ class 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 self: Store data that will be used by the class + 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. :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 - :doc-author: Malcolm Ross """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) - # 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. @@ -234,7 +358,7 @@ def __init__(self, model: Model): Required=True, ErrMessage="assume default economic model (2)", ToolTipText="Specify the economic model to calculate the levelized cost of energy." + - " 1: Fixed Charge Rate Model, 2: Standard Levelized Cost Model, 3: BICYCLE Levelized Cost Model, 4: CLGS" + " 1: Fixed Charge Rate Model, 2: Standard Levelized Cost Model, 3: BICYCLE Levelized Cost Model, 4: CLGS" ) self.ccstimfixed = self.ParameterDict[self.ccstimfixed.Name] = floatParameter( "Reservoir Stimulation Capital Cost", @@ -456,7 +580,7 @@ def __init__(self, model: Model): Provided=False, Valid=False, ErrMessage="calculate total capital cost using user-provided costs or" + - " built-in correlations for each category.", + " built-in correlations for each category.", ToolTipText="Total initial capital cost." ) self.oamtotalfixed = self.ParameterDict[self.oamtotalfixed.Name] = floatParameter( @@ -622,9 +746,9 @@ def __init__(self, model: Model): UnitType=Units.NONE, ErrMessage="assume default well drilling cost correlation (1)", ToolTipText="Select the built-in horizontal well drilling and completion cost correlation." + - " 1: vertical open-hole, small diameter; 2: deviated liner, small diameter;" + - " 3: vertical open-hole, large diameter; 4: deviated liner, large diameter;" + - " 5: Simple - user specified cost per meter" + " 1: vertical open-hole, small diameter; 2: deviated liner, small diameter;" + + " 3: vertical open-hole, large diameter; 4: deviated liner, large diameter;" + + " 5: Simple - user specified cost per meter" ) self.DoAddOnCalculations = self.ParameterDict[self.DoAddOnCalculations.Name] = boolParameter( "Do AddOn Calculations", @@ -664,9 +788,10 @@ def __init__(self, model: Model): CurrentUnits=CostPerDistanceUnit.DOLLARSPERM, ErrMessage="assume default all-in cost for drill vertical well segment(s) (1000 $/m)", ToolTipText="Set user specified all-in cost per meter of vertical drilling," + - " including drilling, casing, cement, insulated insert" + " including drilling, casing, cement, insulated insert" ) - self.Nonvertical_drilling_cost_per_m = self.ParameterDict[self.Nonvertical_drilling_cost_per_m.Name] = floatParameter( + self.Nonvertical_drilling_cost_per_m = self.ParameterDict[ + self.Nonvertical_drilling_cost_per_m.Name] = floatParameter( "All-in Nonvertical Drilling Costs", value=1300.0, DefaultValue=1300.0, @@ -677,7 +802,7 @@ def __init__(self, model: Model): CurrentUnits=CostPerDistanceUnit.DOLLARSPERM, ErrMessage="assume default all-in cost for drill non-vertical well segment(s) (1300 $/m)", ToolTipText="Set user specified all-in cost per meter of non-vertical drilling, including" + - " drilling, casing, cement, insulated insert" + " drilling, casing, cement, insulated insert" ) # absorption chiller @@ -706,18 +831,18 @@ def __init__(self, model: Model): ToolTipText="Absorption chiller O&M cost" ) - #heat pump + # heat pump self.heatpumpcapex = self.ParameterDict[self.heatpumpcapex.Name] = floatParameter( "Heat Pump Capital Cost", - value = -1.0, + 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" + UnitType=Units.CURRENCY, + PreferredUnits=CurrencyUnit.MDOLLARS, + CurrentUnits=CurrencyUnit.MDOLLARS, + Provided=False, + Valid=False, + ToolTipText="Heat pump capital cost" ) # district heating @@ -994,7 +1119,7 @@ def __init__(self, model: Model): ToolTipText="Fixed Internal Rate (used in NPV calculation)" ) - # local variable initialization + # local variable initialization self.Claborcorrelation = 0.0 self.Cpumps = 0.0 self.annualelectricityincome = 0.0 @@ -1020,7 +1145,7 @@ def __init__(self, model: Model): UnitType=Units.ENERGYCOST, PreferredUnits=EnergyCostUnit.DOLLARSPERMMBTU, CurrentUnits=EnergyCostUnit.DOLLARSPERMMBTU - ) # $/MMBTU + ) # $/MMBTU self.Cstim = self.OutputParameterDict[self.Cstim.Name] = OutputParameter( Name="O&M Surface Plant costs", value=-999.9, @@ -1098,7 +1223,8 @@ def __init__(self, model: Model): PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR ) - self.averageannualpumpingcosts = self.OutputParameterDict[self.averageannualpumpingcosts.Name] = OutputParameter( + self.averageannualpumpingcosts = self.OutputParameterDict[ + self.averageannualpumpingcosts.Name] = OutputParameter( Name="Average Annual Pumping Costs", value=-0.0, UnitType=Units.CURRENCYFREQUENCY, @@ -1106,60 +1232,59 @@ def __init__(self, model: Model): CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR ) - #heat pump - self.averageannualheatpumpelectricitycost = self.OutputParameterDict[self.averageannualheatpumpelectricitycost.Name] = OutputParameter( - Name = "Average Annual Heat Pump Electricity Cost", + # 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 + UnitType=Units.CURRENCYFREQUENCY, + PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, + CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR ) - #district heating + # district heating self.peakingboilercost = self.OutputParameterDict[self.peakingboilercost.Name] = OutputParameter( - Name = "Peaking boiler cost", + Name="Peaking boiler cost", value=0, - UnitType = Units.CURRENCY, - PreferredUnits = CurrencyUnit.MDOLLARS, - CurrentUnits = CurrencyUnit.MDOLLARS + UnitType=Units.CURRENCY, + PreferredUnits=CurrencyUnit.MDOLLARS, + CurrentUnits=CurrencyUnit.MDOLLARS ) self.dhdistrictcost = self.OutputParameterDict[self.dhdistrictcost.Name] = OutputParameter( - Name = "District Heating System Cost", + Name="District Heating System Cost", value=0, - UnitType = Units.CURRENCY, - PreferredUnits = CurrencyUnit.MDOLLARS, - CurrentUnits = CurrencyUnit.MDOLLARS + UnitType=Units.CURRENCY, + PreferredUnits=CurrencyUnit.MDOLLARS, + CurrentUnits=CurrencyUnit.MDOLLARS ) self.populationdensity = self.OutputParameterDict[self.populationdensity.Name] = OutputParameter( - Name = "District Heating System Population Density", + Name="District Heating System Population Density", value=0, - UnitType = Units.POPDENSITY, - PreferredUnits = PopDensityUnit.perkm2, - CurrentUnits = PopDensityUnit.perkm2 + UnitType=Units.POPDENSITY, + PreferredUnits=PopDensityUnit.perkm2, + CurrentUnits=PopDensityUnit.perkm2 ) self.annualngcost = self.OutputParameterDict[self.annualngcost.Name] = OutputParameter( - Name = "Annual Peaking Fuel Cost", - value=0, UnitType = Units.CURRENCYFREQUENCY, - PreferredUnits = CurrencyFrequencyUnit.MDOLLARSPERYEAR, - CurrentUnits = CurrencyFrequencyUnit.MDOLLARSPERYEAR + Name="Annual Peaking Fuel Cost", + value=0, UnitType=Units.CURRENCYFREQUENCY, + PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, + CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR ) self.dhdistrictoandmcost = self.OutputParameterDict[self.dhdistrictoandmcost.Name] = OutputParameter( - Name = "Annual District Heating O&M Cost", + Name="Annual District Heating O&M Cost", value=0, - UnitType = Units.CURRENCYFREQUENCY, - PreferredUnits = CurrencyFrequencyUnit.MDOLLARSPERYEAR, - CurrentUnits = CurrencyFrequencyUnit.MDOLLARSPERYEAR + UnitType=Units.CURRENCYFREQUENCY, + PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, + CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR ) self.averageannualngcost = self.OutputParameterDict[self.averageannualngcost.Name] = OutputParameter( - Name = "Average Annual Peaking Fuel Cost", + Name="Average Annual Peaking Fuel Cost", value=0, - UnitType = Units.CURRENCYFREQUENCY, - PreferredUnits = CurrencyFrequencyUnit.MDOLLARSPERYEAR, - CurrentUnits = CurrencyFrequencyUnit.MDOLLARSPERYEAR + UnitType=Units.CURRENCYFREQUENCY, + PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, + CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR ) - - self.ElecRevenue = self.OutputParameterDict[self.ElecRevenue.Name] = OutputParameter( Name="Annual Revenue from Electricity Production", value=[0.0], @@ -1236,22 +1361,21 @@ def __init__(self, model: Model): def read_parameters(self, model: Model) -> None: """ read_parameters read and update the Economics parameters and handle the special cases - - Args: - model (Model): The container class of the application, giving access to everything else, including the logger + 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. + :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("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(): @@ -1291,306 +1415,313 @@ def read_parameters(self, model: Model) -> None: elif ParameterToModify.Name == "Reservoir Stimulation Capital Cost Adjustment Factor": if self.ccstimfixed.Valid and ParameterToModify.Valid: print("Warning: Provided reservoir stimulation cost adjustment factor not considered" + - " because valid total reservoir stimulation cost provided.") - model.logger.warning("Provided reservoir stimulation cost adjustment factor not considered" + - " because valid total reservoir stimulation cost provided.") + " because valid total reservoir stimulation cost provided.") + model.logger.warning( + "Provided reservoir stimulation cost adjustment factor not considered" + + " because valid total reservoir stimulation cost provided.") elif not self.ccstimfixed.Provided and not ParameterToModify.Provided: ParameterToModify.value = 1.0 print("Warning: No valid reservoir stimulation total cost or adjustment factor provided." + - " GEOPHIRES will assume default built-in reservoir stimulation cost correlation with" + - " adjustment factor = 1.") + " GEOPHIRES will assume default built-in reservoir stimulation cost correlation with" + + " adjustment factor = 1.") model.logger.warning("No valid reservoir stimulation total cost or adjustment factor" + - " provided. GEOPHIRES will assume default built-in reservoir stimulation cost correlation" + - " with adjustment factor = 1.") + " provided. GEOPHIRES will assume default built-in reservoir stimulation cost correlation" + + " with adjustment factor = 1.") elif self.ccstimfixed.Provided and not self.ccstimfixed.Valid: print("Warning: Provided reservoir stimulation cost outside of range 0-100. GEOPHIRES" + - " will assume default built-in reservoir stimulation cost correlation with" + - " adjustment factor = 1.") - model.logger.warning("Provided reservoir stimulation cost outside of range 0-100. GEOPHIRES" + - " will assume default built-in reservoir stimulation cost correlation with" + - " adjustment factor = 1.") + " will assume default built-in reservoir stimulation cost correlation with" + + " adjustment factor = 1.") + model.logger.warning( + "Provided reservoir stimulation cost outside of range 0-100. GEOPHIRES" + + " will assume default built-in reservoir stimulation cost correlation with" + + " adjustment factor = 1.") ParameterToModify.value = 1.0 elif not self.ccstimfixed.Provided and ParameterToModify.Provided and not ParameterToModify.Valid: print("Warning: Provided reservoir stimulation cost adjustment factor outside of" + - " range 0-10. GEOPHIRES will assume default reservoir stimulation cost correlation with" + - " adjustment factor = 1.") + " range 0-10. GEOPHIRES will assume default reservoir stimulation cost correlation with" + + " adjustment factor = 1.") model.logger.warning("Provided reservoir stimulation cost adjustment factor outside of" + - " range 0-10. GEOPHIRES will assume default reservoir stimulation cost correlation with" + - " adjustment factor = 1.") + " range 0-10. GEOPHIRES will assume default reservoir stimulation cost correlation with" + + " adjustment factor = 1.") ParameterToModify.value = 1.0 elif ParameterToModify.Name == "Exploration Capital Cost Adjustment Factor": if self.totalcapcost.Valid: if self.ccexplfixed.Provided: print("Warning: Provided exploration cost not considered because valid" + - " total capital cost provided.") + " total capital cost provided.") model.logger.warning("Warning: Provided exploration cost not considered" + - " because valid total capital cost provided.") + " because valid total capital cost provided.") if ParameterToModify.Provided: print("Warning: Provided exploration cost adjustment factor not considered because" + - " valid total capital cost provided.") + " valid total capital cost provided.") model.logger.warning("Warning: Provided exploration cost not considered because valid" + - " total capital cost provided.") + " total capital cost provided.") else: if self.ccexplfixed.Valid and ParameterToModify.Valid: print("Warning: Provided exploration cost adjustment factor not considered" + - " because valid total exploration cost provided.") + " because valid total exploration cost provided.") model.logger.warning("Provided exploration cost adjustment factor not" + - " considered because valid total exploration cost provided.") + " considered because valid total exploration cost provided.") elif not self.ccexplfixed.Provided and not ParameterToModify.Provided: ParameterToModify.value = 1.0 print("Warning: No valid exploration total cost or adjustment factor provided." + - " GEOPHIRES will assume default built-in exploration cost correlation with" + - " adjustment factor = 1.") + " GEOPHIRES will assume default built-in exploration cost correlation with" + + " adjustment factor = 1.") model.logger.warning("No valid exploration total cost or adjustment factor provided." + - " GEOPHIRES will assume default built-in exploration cost correlation with" + - " adjustment factor = 1.") + " GEOPHIRES will assume default built-in exploration cost correlation with" + + " adjustment factor = 1.") elif self.ccexplfixed.Provided and not self.ccexplfixed.Valid: print("Warning: Provided exploration cost outside of range 0-100. GEOPHIRES" + - " will assume default built-in exploration cost correlation with adjustment factor = 1.") + " will assume default built-in exploration cost correlation with adjustment factor = 1.") model.logger.warning("Provided exploration cost outside of range 0-100. GEOPHIRES" + - " will assume default built-in exploration cost correlation with adjustment factor = 1.") + " will assume default built-in exploration cost correlation with adjustment factor = 1.") ParameterToModify.value = 1.0 elif not self.ccexplfixed.Provided and ParameterToModify.Provided and not ParameterToModify.Valid: print("Warning: Provided exploration cost adjustment factor outside of range 0-10." + - " GEOPHIRES will assume default exploration cost correlation with adjustment factor = 1.") + " GEOPHIRES will assume default exploration cost correlation with adjustment factor = 1.") model.logger.warning("Provided exploration cost adjustment factor outside of" + - " range 0-10. GEOPHIRES will assume default exploration cost correlation with" + - " adjustment factor = 1.") + " range 0-10. GEOPHIRES will assume default exploration cost correlation with" + + " adjustment factor = 1.") ParameterToModify.value = 1.0 elif ParameterToModify.Name == "Well Drilling and Completion Capital Cost Adjustment Factor": if self.ccwellfixed.Valid and ParameterToModify.Valid: print("Warning: Provided well drilling and completion cost adjustment factor not" + - " considered because valid total well drilling and completion cost provided.") + " considered because valid total well drilling and completion cost provided.") model.logger.warning("Provided well drilling and completion cost adjustment factor not" + - " considered because valid total well drilling and completion cost provided.") + " considered because valid total well drilling and completion cost provided.") elif not self.ccwellfixed.Provided and not self.ccwelladjfactor.Provided: ParameterToModify.value = 1.0 print("Warning: No valid well drilling and completion total cost or adjustment" + - " factor provided. GEOPHIRES will assume default built-in well drilling and" + - " completion cost correlation with adjustment factor = 1.") - model.logger.warning("No valid well drilling and completion total cost or adjustment factor" + - " provided. GEOPHIRES will assume default built-in well drilling and completion cost" + - " correlation with adjustment factor = 1.") + " factor provided. GEOPHIRES will assume default built-in well drilling and" + + " completion cost correlation with adjustment factor = 1.") + model.logger.warning( + "No valid well drilling and completion total cost or adjustment factor" + + " provided. GEOPHIRES will assume default built-in well drilling and completion cost" + + " correlation with adjustment factor = 1.") elif self.ccwellfixed.Provided and not self.ccwellfixed.Valid: print("Warning: Provided well drilling and completion cost outside of range 0-1000." + - " GEOPHIRES will assume default built-in well drilling and completion cost correlation" + - " with adjustment factor = 1.") + " GEOPHIRES will assume default built-in well drilling and completion cost correlation" + + " with adjustment factor = 1.") model.logger.warning("Provided well drilling and completion cost outside of range 0-1000." + - " GEOPHIRES will assume default built-in well drilling and completion cost correlation with" + - " adjustment factor = 1.") + " GEOPHIRES will assume default built-in well drilling and completion cost correlation with" + + " adjustment factor = 1.") self.ccwelladjfactor.value = 1.0 elif not self.ccwellfixed.Provided and self.ccwelladjfactor.Provided and not self.ccwelladjfactor.Valid: print("Warning: Provided well drilling and completion cost adjustment factor outside" + - " of range 0-10. GEOPHIRES will assume default built-in well drilling and completion" + - " cost correlation with adjustment factor = 1.") - model.logger.warning("Provided well drilling and completion cost adjustment factor outside" + - " of range 0-10. GEOPHIRES will assume default built-in well drilling and completion" + - " cost correlation with adjustment factor = 1.") + " of range 0-10. GEOPHIRES will assume default built-in well drilling and completion" + + " cost correlation with adjustment factor = 1.") + model.logger.warning( + "Provided well drilling and completion cost adjustment factor outside" + + " of range 0-10. GEOPHIRES will assume default built-in well drilling and completion" + + " cost correlation with adjustment factor = 1.") self.ccwelladjfactor.value = 1.0 elif ParameterToModify.Name == "Wellfield O&M Cost Adjustment Factor": if self.oamtotalfixed.Valid: if self.oamwellfixed.Provided: print("Warning: Provided total wellfield O&M cost not considered because" + - " valid total annual O&M cost provided.") + " valid total annual O&M cost provided.") model.logger.warning("Provided total wellfield O&M cost not considered because" + - " valid total annual O&M cost provided.") + " valid total annual O&M cost provided.") if ParameterToModify.Provided: print("Warning: Provided wellfield O&M cost adjustment factor not considered because" + - " valid total annual O&M cost provided.") + " valid total annual O&M cost provided.") model.logger.warning("Provided wellfield O&M cost adjustment factor not considered" + - " because valid total annual O&M cost provided.") + " because valid total annual O&M cost provided.") else: if self.oamwellfixed.Valid and ParameterToModify.Valid: print("Warning: Provided wellfield O&M cost adjustment factor not considered" + - " because valid total wellfield O&M cost provided.") + " because valid total wellfield O&M cost provided.") model.logger.warning("Provided wellfield O&M cost adjustment factor not considered" + - " because valid total wellfield O&M cost provided.") + " because valid total wellfield O&M cost provided.") elif not self.oamwellfixed.Provided and not ParameterToModify.Provided: ParameterToModify.value = 1.0 print("Warning: No valid total wellfield O&M cost or adjustment factor provided." + - " GEOPHIRES will assume default built-in wellfield O&M cost correlation with" + - " adjustment factor = 1.") + " GEOPHIRES will assume default built-in wellfield O&M cost correlation with" + + " adjustment factor = 1.") model.logger.warning("No valid total wellfield O&M cost or adjustment factor" + - " provided. GEOPHIRES will assume default built-in wellfield O&M cost correlation" + - " with adjustment factor = 1.") + " provided. GEOPHIRES will assume default built-in wellfield O&M cost correlation" + + " with adjustment factor = 1.") elif self.oamwellfixed.Provided and not self.oamwellfixed.Valid: print("Warning: Provided total wellfield O&M cost outside of range 0-100." + - " GEOPHIRES will assume default built-in wellfield O&M cost correlation with" + - " adjustment factor = 1.") + " GEOPHIRES will assume default built-in wellfield O&M cost correlation with" + + " adjustment factor = 1.") model.logger.warning("Provided total wellfield O&M cost outside of range 0-100." + - " GEOPHIRES will assume default built-in wellfield O&M cost correlation with" + - " adjustment factor = 1.") + " GEOPHIRES will assume default built-in wellfield O&M cost correlation with" + + " adjustment factor = 1.") ParameterToModify.value = 1.0 elif not self.oamwellfixed.Provided and self.oamwelladjfactor.Provided and not self.oamwelladjfactor.Valid: print("Warning: Provided wellfield O&M cost adjustment factor outside of range 0-10." + - " GEOPHIRES will assume default wellfield O&M cost correlation with adjustment factor = 1.") + " GEOPHIRES will assume default wellfield O&M cost correlation with adjustment factor = 1.") model.logger.warning("Provided wellfield O&M cost adjustment factor outside of" + - " range 0-10. GEOPHIRES will assume default wellfield O&M cost correlation with" + - " adjustment factor = 1.") + " range 0-10. GEOPHIRES will assume default wellfield O&M cost correlation with" + + " adjustment factor = 1.") ParameterToModify.value = 1.0 elif ParameterToModify.Name == "Surface Plant Capital Cost Adjustment Factor": if self.totalcapcost.Valid: if self.ccplantfixed.Provided: print("Warning: Provided surface plant cost not considered because valid" + - " total capital cost provided.") + " total capital cost provided.") model.logger.warning("Provided surface plant cost not considered because valid" + - " total capital cost provided.") + " total capital cost provided.") if ParameterToModify.Provided: print("Warning: Provided surface plant cost adjustment factor not considered" + - " because valid total capital cost provided.") + " because valid total capital cost provided.") model.logger.warning("Provided surface plant cost adjustment factor not considered" + - " because valid total capital cost provided.") + " because valid total capital cost provided.") else: if self.ccplantfixed.Valid and ParameterToModify.Valid: print("Warning: Provided surface plant cost adjustment factor not considered because" + - " valid total surface plant cost provided.") + " valid total surface plant cost provided.") model.logger.warning("Provided surface plant cost adjustment factor not considered" + - " because valid total surface plant cost provided.") + " because valid total surface plant cost provided.") elif not self.ccplantfixed.Provided and not ParameterToModify.Provided: ParameterToModify.value = 1.0 print("Warning: No valid surface plant total cost or adjustment factor provided." + - " GEOPHIRES will assume default built-in surface plant cost correlation with" + - " adjustment factor = 1.") + " GEOPHIRES will assume default built-in surface plant cost correlation with" + + " adjustment factor = 1.") model.logger.warning("No valid surface plant total cost or adjustment factor" + - " provided. GEOPHIRES will assume default built-in surface plant cost correlation" + - " with adjustment factor = 1.") + " provided. GEOPHIRES will assume default built-in surface plant cost correlation" + + " with adjustment factor = 1.") elif self.ccplantfixed.Provided and not self.ccplantfixed.Valid: print("Warning: Provided surface plant cost outside of range 0-1000." + - " GEOPHIRES will assume default built-in surface plant cost correlation with" + - " adjustment factor = 1.") + " GEOPHIRES will assume default built-in surface plant cost correlation with" + + " adjustment factor = 1.") model.logger.warning("Provided surface plant cost outside of range 0-1000." + - " GEOPHIRES will assume default built-in surface plant cost correlation with" + - " adjustment factor = 1.") + " GEOPHIRES will assume default built-in surface plant cost correlation with" + + " adjustment factor = 1.") ParameterToModify.value = 1.0 elif not self.ccplantfixed.Provided and self.ccplantadjfactor.Provided and not self.ccplantadjfactor.Valid: print("Warning: Provided surface plant cost adjustment factor outside of range 0-10." + - " GEOPHIRES will assume default surface plant cost correlation with" + - " adjustment factor = 1.") + " GEOPHIRES will assume default surface plant cost correlation with" + + " adjustment factor = 1.") model.logger.warning("Provided surface plant cost adjustment factor outside of" + - " range 0-10. GEOPHIRES will assume default surface plant cost correlation with" + - " adjustment factor = 1.") + " range 0-10. GEOPHIRES will assume default surface plant cost correlation with" + + " adjustment factor = 1.") ParameterToModify.value = 1.0 elif ParameterToModify.Name == "Field Gathering System Capital Cost Adjustment Factor": if self.totalcapcost.Valid: if self.ccgathfixed.Provided: print("Warning: Provided field gathering system cost not considered because valid" + - " total capital cost provided.") - model.logger.warning("Provided field gathering system cost not considered because valid" + - " total capital cost provided.") + " total capital cost provided.") + model.logger.warning( + "Provided field gathering system cost not considered because valid" + + " total capital cost provided.") if ParameterToModify.Provided: print("Warning: Provided field gathering system cost adjustment factor not" + - " considered because valid total capital cost provided.") + " considered because valid total capital cost provided.") model.logger.warning("Provided field gathering system cost adjustment factor not" + - " considered because valid total capital cost provided.") + " considered because valid total capital cost provided.") else: if self.ccgathfixed.Valid and ParameterToModify.Valid: print("Warning: Provided field gathering system cost adjustment factor not" + - " considered because valid total field gathering system cost provided.") + " considered because valid total field gathering system cost provided.") model.logger.warning("Provided field gathering system cost adjustment factor not" + - " considered because valid total field gathering system cost provided.") + " considered because valid total field gathering system cost provided.") elif not self.ccgathfixed.Provided and not ParameterToModify.Provided: ParameterToModify.value = 1.0 print("Warning: No valid field gathering system total cost or adjustment factor" + - " provided. GEOPHIRES will assume default built-in field gathering system cost" + - " correlation with adjustment factor = 1.") + " provided. GEOPHIRES will assume default built-in field gathering system cost" + + " correlation with adjustment factor = 1.") model.logger.warning("No valid field gathering system total cost or adjustment factor" + - " provided. GEOPHIRES will assume default built-in field gathering system cost" + - " correlation with adjustment factor = 1.") + " provided. GEOPHIRES will assume default built-in field gathering system cost" + + " correlation with adjustment factor = 1.") elif self.ccgathfixed.Provided and not self.ccgathfixed.Valid: print("Warning: Provided field gathering system cost outside of range 0-100." + - " GEOPHIRES will assume default built-in field gathering system cost correlation" + - " with adjustment factor = 1.") + " GEOPHIRES will assume default built-in field gathering system cost correlation" + + " with adjustment factor = 1.") model.logger.warning("Provided field gathering system cost outside of range 0-100." + - " GEOPHIRES will assume default built-in field gathering system cost correlation with" + - " adjustment factor = 1.") + " GEOPHIRES will assume default built-in field gathering system cost correlation with" + + " adjustment factor = 1.") ParameterToModify.value = 1.0 elif not self.ccgathfixed.Provided and ParameterToModify.Provided and not ParameterToModify.Valid: print("Warning: Provided field gathering system cost adjustment factor" + - " outside of range 0-10. GEOPHIRES will assume default field gathering system" + - " cost correlation with adjustment factor = 1.") + " outside of range 0-10. GEOPHIRES will assume default field gathering system" + + " cost correlation with adjustment factor = 1.") model.logger.warning("Provided field gathering system cost adjustment factor" + - " outside of range 0-10. GEOPHIRES will assume default field gathering system cost" + - " correlation with adjustment factor = 1.") + " outside of range 0-10. GEOPHIRES will assume default field gathering system cost" + + " correlation with adjustment factor = 1.") ParameterToModify.value = 1.0 elif ParameterToModify.Name == "Water Cost Adjustment Factor": if self.oamtotalfixed.Valid: if self.oamwaterfixed.Provided: print("Warning: Provided total water cost not considered because valid" + - " total annual O&M cost provided.") + " total annual O&M cost provided.") model.logger.warning("Provided total water cost not considered because valid" + - " total annual O&M cost provided.") + " total annual O&M cost provided.") if ParameterToModify.Provided: print("Warning: Provided water cost adjustment factor not considered because" + - " valid total annual O&M cost provided.") + " valid total annual O&M cost provided.") model.logger.warning("Provided water cost adjustment factor not considered because" + - " valid total annual O&M cost provided.") + " valid total annual O&M cost provided.") else: if self.oamwaterfixed.Valid and ParameterToModify.Valid: print("Warning: Provided water cost adjustment factor not considered because" + - " valid total water cost provided.") + " valid total water cost provided.") model.logger.warning("Provided water cost adjustment factor not considered because" + - " valid total water cost provided.") + " valid total water cost provided.") elif not self.oamwaterfixed.Provided and not ParameterToModify.Provided: ParameterToModify.value = 1.0 print("Warning: No valid total water cost or adjustment factor provided." + - " GEOPHIRES will assume default built-in water cost correlation with" + - " adjustment factor = 1.") + " GEOPHIRES will assume default built-in water cost correlation with" + + " adjustment factor = 1.") model.logger.warning("No valid total water cost or adjustment factor provided." + - " GEOPHIRES will assume default built-in water cost correlation with" + - " adjustment factor = 1.") + " GEOPHIRES will assume default built-in water cost correlation with" + + " adjustment factor = 1.") elif self.oamwaterfixed.Provided and not self.oamwaterfixed.Valid: print("Warning: Provided total water cost outside of range 0-100. GEOPHIRES" + - " will assume default built-in water cost correlation with adjustment factor = 1.") + " will assume default built-in water cost correlation with adjustment factor = 1.") model.logger.warning("Provided total water cost outside of range 0-100. GEOPHIRES" + - " will assume default built-in water cost correlation with adjustment factor = 1.") + " will assume default built-in water cost correlation with adjustment factor = 1.") ParameterToModify.value = 1.0 elif not self.oamwaterfixed.Provided and ParameterToModify.Provided and not ParameterToModify.Valid: print("Warning: Provided water cost adjustment factor outside of range 0-10." + - " GEOPHIRES will assume default water cost correlation with adjustment factor = 1.") + " GEOPHIRES will assume default water cost correlation with adjustment factor = 1.") model.logger.warning("Provided water cost adjustment factor outside of range 0-10." + - " GEOPHIRES will assume default water cost correlation with adjustment factor = 1.") + " GEOPHIRES will assume default water cost correlation with adjustment factor = 1.") ParameterToModify.value = 1.0 elif ParameterToModify.Name == "Surface Plant O&M Cost Adjustment Factor": if self.oamtotalfixed.Valid: if self.oamplantfixed.Provided: print("Warning: Provided total surface plant O&M cost not considered because" + - " valid total annual O&M cost provided.") + " valid total annual O&M cost provided.") model.logger.warning("Provided total surface plant O&M cost not considered because" + - " valid total annual O&M cost provided.") + " valid total annual O&M cost provided.") if ParameterToModify.Provided: print("Warning: Provided surface plant O&M cost adjustment factor not considered" + - " because valid total annual O&M cost provided.") - model.logger.warning("Provided surface plant O&M cost adjustment factor not considered" + - " because valid total annual O&M cost provided.") + " because valid total annual O&M cost provided.") + model.logger.warning( + "Provided surface plant O&M cost adjustment factor not considered" + + " because valid total annual O&M cost provided.") else: if self.oamplantfixed.Valid and ParameterToModify.Valid: print("Warning: Provided surface plant O&M cost adjustment factor not considered" + - " because valid total surface plant O&M cost provided.") - model.logger.warning("Provided surface plant O&M cost adjustment factor not considered" + - " because valid total surface plant O&M cost provided.") + " because valid total surface plant O&M cost provided.") + model.logger.warning( + "Provided surface plant O&M cost adjustment factor not considered" + + " because valid total surface plant O&M cost provided.") elif not self.oamplantfixed.Provided and not ParameterToModify.Provided: ParameterToModify.value = 1.0 print("Warning: No valid surface plant O&M cost or adjustment factor provided." + - " GEOPHIRES will assume default built-in surface plant O&M cost correlation with" + - " adjustment factor = 1.") + " GEOPHIRES will assume default built-in surface plant O&M cost correlation with" + + " adjustment factor = 1.") model.logger.warning("No valid surface plant O&M cost or adjustment factor provided." + - " GEOPHIRES will assume default built-in surface plant O&M cost correlation with" + - " adjustment factor = 1.") + " GEOPHIRES will assume default built-in surface plant O&M cost correlation with" + + " adjustment factor = 1.") elif self.oamplantfixed.Provided and not self.oamplantfixed.Valid: print("Warning: Provided surface plant O&M cost outside of range 0-100." + - " GEOPHIRES will assume default built-in surface plant O&M cost correlation with" + - " adjustment factor = 1.") + " GEOPHIRES will assume default built-in surface plant O&M cost correlation with" + + " adjustment factor = 1.") model.logger.warning("Provided surface plant O&M cost outside of range 0-100." + - " GEOPHIRES will assume default built-in surface plant O&M cost correlation with" + - " adjustment factor = 1.") + " GEOPHIRES will assume default built-in surface plant O&M cost correlation with" + + " adjustment factor = 1.") ParameterToModify.value = 1.0 elif not self.oamplantfixed.Provided and ParameterToModify.Provided and not ParameterToModify.Valid: print("Warning: Provided surface plant O&M cost adjustment factor outside of" + - " range 0-10. GEOPHIRES will assume default surface plant O&M cost correlation with" + - " adjustment factor = 1.") + " range 0-10. GEOPHIRES will assume default surface plant O&M cost correlation with" + + " adjustment factor = 1.") model.logger.warning("Provided surface plant O&M cost adjustment factor outside of" + - " range 0-10. GEOPHIRES will assume default surface plant O&M cost correlation with" + - " adjustment factor = 1.") + " range 0-10. GEOPHIRES will assume default surface plant O&M cost correlation with" + + " adjustment factor = 1.") ParameterToModify.value = 1.0 else: model.logger.info("No parameters read because no content provided") @@ -1615,116 +1746,130 @@ 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 self: Access variables that belongs to the class + 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 - :doc-author: Malcolm Ross """ model.logger.info("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! - # capital costs # well costs (using GeoVision drilling correlations). These are calculated whether totalcapcostvalid = 1 # start with the cost of one well if self.ccwellfixed.Valid: self.C1well = self.ccwellfixed.value - self.Cwell.value = self.C1well*(model.wellbores.nprod.value+model.wellbores.ninj.value) + self.Cwell.value = self.C1well * (model.wellbores.nprod.value + model.wellbores.ninj.value) else: # if depth is > 7000 m, we don't have a correlation for it, so we must use the SIMPLE logic checkdepth = model.reserv.depth.value if model.reserv.depth.CurrentUnits != LengthUnit.METERS: - checkdepth = checkdepth*1000.0 - if (checkdepth > 7000.0 or checkdepth < 500) and not self.wellcorrelation.value == WellDrillingCostCorrelation.SIMPLE: + checkdepth = checkdepth * 1000.0 + if ( + checkdepth > 7000.0 or checkdepth < 500) and not self.wellcorrelation.value == WellDrillingCostCorrelation.SIMPLE: 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") + model.logger.warning( + "Warning: simple user-specified cost per meter used for drilling depth < 500 or > 7000 m") self.wellcorrelation.value = WellDrillingCostCorrelation.SIMPLE if self.wellcorrelation.value == WellDrillingCostCorrelation.SIMPLE: # use SIMPLE approach if hasattr(model.wellbores, 'Configuration'): if model.wellbores.Configuration.value == Configuration.ULOOP: - if hasattr(model.reserv, 'InputDepth'): # must be using simple cylindrical model, which has an Input and Output Depth + if hasattr(model.reserv, + 'InputDepth'): # must be using simple cylindrical model, which has an Input and Output Depth self.C1well = ((self.Vertical_drilling_cost_per_m.value * - (model.reserv.InputDepth.value*1000.0)) + - (self.Vertical_drilling_cost_per_m.value * (model.reserv.OutputDepth.value*1000.0)) + - (self.Nonvertical_drilling_cost_per_m.value * model.wellbores.Nonvertical_length.value))*1E-6 + (model.reserv.InputDepth.value * 1000.0)) + + (self.Vertical_drilling_cost_per_m.value * ( + model.reserv.OutputDepth.value * 1000.0)) + + ( + self.Nonvertical_drilling_cost_per_m.value * model.wellbores.Nonvertical_length.value)) * 1E-6 else: if hasattr(model.wellbores, 'Nonvertical_length'): self.C1well = ((2 * self.Vertical_drilling_cost_per_m.value * - (model.reserv.depth.value*1000.0)) + - (self.Nonvertical_drilling_cost_per_m.value * model.wellbores.Nonvertical_length.value))*1E-6 + (model.reserv.depth.value * 1000.0)) + + ( + self.Nonvertical_drilling_cost_per_m.value * model.wellbores.Nonvertical_length.value)) * 1E-6 else: - self.C1well = (2 * self.Vertical_drilling_cost_per_m.value * (model.reserv.depth.value * 1000.0)) * 1E-6 + self.C1well = (2 * self.Vertical_drilling_cost_per_m.value * ( + model.reserv.depth.value * 1000.0)) * 1E-6 else: # Coaxial - self.C1well = ((self.Vertical_drilling_cost_per_m.value * (model.reserv.depth.value*1000.0)) + - (self.Nonvertical_drilling_cost_per_m.value * model.wellbores.Nonvertical_length.value))*1E-6 + self.C1well = ((self.Vertical_drilling_cost_per_m.value * (model.reserv.depth.value * 1000.0)) + + ( + self.Nonvertical_drilling_cost_per_m.value * model.wellbores.Nonvertical_length.value)) * 1E-6 elif self.wellcorrelation.value == WellDrillingCostCorrelation.VERTICAL_SMALL: - self.C1well = (0.3021*checkdepth**2 + 584.9112*checkdepth + 751368.)*1E-6 # well drilling and completion cost in M$/well + self.C1well = ( + 0.3021 * checkdepth ** 2 + 584.9112 * checkdepth + 751368.) * 1E-6 # well drilling and completion cost in M$/well elif self.wellcorrelation.value == WellDrillingCostCorrelation.DEVIATED_SMALL: - self.C1well = (0.2898*checkdepth**2 + 822.1507*checkdepth + 680563.)*1E-6 + self.C1well = (0.2898 * checkdepth ** 2 + 822.1507 * checkdepth + 680563.) * 1E-6 elif self.wellcorrelation.value == WellDrillingCostCorrelation.VERTICAL_LARGE: - self.C1well = (0.2818*checkdepth**2 + 1275.5213*checkdepth + 632315.)*1E-6 + self.C1well = (0.2818 * checkdepth ** 2 + 1275.5213 * checkdepth + 632315.) * 1E-6 elif self.wellcorrelation.value == WellDrillingCostCorrelation.DEVIATED_LARGE: - self.C1well = (0.2553*checkdepth**2 + 1716.7157*checkdepth + 500867.)*1E-6 + self.C1well = (0.2553 * checkdepth ** 2 + 1716.7157 * checkdepth + 500867.) * 1E-6 # account for adjustment factor - self.C1well = self.ccwelladjfactor.value*self.C1well + self.C1well = self.ccwelladjfactor.value * self.C1well # cost of the well field - self.Cwell.value = 1.05*self.C1well*(model.wellbores.nprod.value+model.wellbores.ninj.value) # 1.05 for 5% indirect costs + self.Cwell.value = 1.05 * self.C1well * ( + model.wellbores.nprod.value + model.wellbores.ninj.value) # 1.05 for 5% indirect costs # 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 + 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) + 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 + 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)) + 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. + 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 + 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) + 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 + 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.enduseoption.value == EndUseOptions.HEAT: # 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 + 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.enduseoption.value == EndUseOptions.ABSORPTION_CHILLER: # absorption chiller @@ -1756,14 +1901,15 @@ def Calculate(self, model: Model) -> None: # now add heat pump cost to surface plant cost self.Cplant.value += self.heatpumpcapex.value - #district heating + # district heating elif model.surfaceplant.enduseoption.value == EndUseOptions.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.maxpeakingboilerdemand.value/1000 #add 65$/KW for peaking boiler - self.Cplant.value += self.peakingboilercost.value #add peaking boiler cost to surface plant cost + 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.maxpeakingboilerdemand.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 @@ -1774,15 +1920,15 @@ def Calculate(self, model: Model) -> None: C2 = 7.6875E-1 C1 = -1.347917E2 C0 = 1.0075E4 - CCAPP1 = C3*MaxProducedTemperature**3 + C2*MaxProducedTemperature**2 + C1*MaxProducedTemperature + C0 + CCAPP1 = C3 * MaxProducedTemperature ** 3 + C2 * MaxProducedTemperature ** 2 + C1 * MaxProducedTemperature + C0 else: - CCAPP1 = 2231 - 2*(MaxProducedTemperature-150.) + 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 + z = math.pow(y / 15., -0.06) + self.Cplantcorrelation = CCAPP1 * z * x * 1000. / 1E6 elif model.surfaceplant.pptype.value == PowerPlantType.SUPER_CRITICAL_ORC: MaxProducedTemperature = np.max(model.surfaceplant.TenteringPP.value) @@ -1791,11 +1937,13 @@ def Calculate(self, model: Model) -> None: C2 = 7.6875E-1 C1 = -1.347917E2 C0 = 1.0075E4 - CCAPP1 = C3*MaxProducedTemperature**3 + C2*MaxProducedTemperature**2 + C1*MaxProducedTemperature + C0 + CCAPP1 = C3 * MaxProducedTemperature ** 3 + C2 * MaxProducedTemperature ** 2 + C1 * MaxProducedTemperature + C0 else: - CCAPP1 = 2231 - 2*(MaxProducedTemperature-150.) + 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 + 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.pptype.value == PowerPlantType.SINGLE_FLASH: if np.max(model.surfaceplant.ElectricityProduced.value) < 10.: @@ -1844,13 +1992,13 @@ def Calculate(self, model: Model) -> None: 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 + 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) + 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.pptype.value == PowerPlantType.DOUBLE_FLASH: if np.max(model.surfaceplant.ElectricityProduced.value) < 10.: @@ -1899,125 +2047,138 @@ def Calculate(self, model: Model) -> None: 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) + 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 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.Cplant.value = 1.12 * 1.15 * self.ccplantadjfactor.value * self.Cplantcorrelation * 1.02 * 1.10 # 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.enduseoption.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICTY, EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT]: # enduseoption = 3: cogen topping cycle - self.Cplant.value = self.Cplant.value + 1.12*1.15*self.ccplantadjfactor.value*250E-6*np.max(model.surfaceplant.HeatProduced.value/model.surfaceplant.enduseefficiencyfactor.value)*1000. + self.Cplant.value = self.Cplant.value + 1.12 * 1.15 * self.ccplantadjfactor.value * 250E-6 * np.max( + model.surfaceplant.HeatProduced.value / model.surfaceplant.enduseefficiencyfactor.value) * 1000. elif model.surfaceplant.enduseoption.value in [EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICTY]: # enduseoption = 4: cogen bottoming cycle - self.Cplant = self.Cplant.value + 1.12*1.15*self.ccplantadjfactor.value*250E-6*np.max(model.surfaceplant.HeatProduced.value/model.surfaceplant.enduseefficiencyfactor.value)*1000. + self.Cplant = self.Cplant.value + 1.12 * 1.15 * self.ccplantadjfactor.value * 250E-6 * np.max( + model.surfaceplant.HeatProduced.value / model.surfaceplant.enduseefficiencyfactor.value) * 1000. elif model.surfaceplant.enduseoption.value in [EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICTY, EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT]: # cogen parallel cycle - self.Cplant.value = self.Cplant.value + 1.12*1.15*self.ccplantadjfactor.value*250E-6*np.max(model.surfaceplant.HeatProduced.value/model.surfaceplant.enduseefficiencyfactor.value)*1000. + self.Cplant.value = self.Cplant.value + 1.12 * 1.15 * self.ccplantadjfactor.value * 250E-6 * np.max( + model.surfaceplant.HeatProduced.value / model.surfaceplant.enduseefficiencyfactor.value) * 1000. 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.C1well*0.6) # 1.15 for 15% contingency and 1.12 for 12% indirect costs + self.Cexpl.value = 1.15 * self.ccexpladjfactor.value * 1.12 * ( + 1. + self.C1well * 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.pipinglength.value + self.Cpiping.value = 750 / 1000 * model.surfaceplant.pipinglength.value - #district heating network costs - if model.surfaceplant.enduseoption.value == EndUseOptions.DISTRICT_HEATING: #district heat + # district heating network costs + if model.surfaceplant.enduseoption.value == EndUseOptions.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 + 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 + self.populationdensity.value = self.dhpopulation.value / self.dhlandarea.value elif model.surfaceplant.dhnumberofhousingunits.Provided: - self.populationdensity.value = model.surfaceplant.dhnumberofhousingunits.value*2.6/self.dhlandarea.value #estimate population based on 2.6 number of people per household + self.populationdensity.value = model.surfaceplant.dhnumberofhousingunits.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 + 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 + 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 + 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 # 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.enduseoption.value == EndUseOptions.HEAT_PUMP: #heat pump: - self.averageannualheatpumpelectricitycost.value = np.average(model.surfaceplant.HeatPumpElectricitykWhUsed.value) * model.surfaceplant.elecprice.value / 1E6 # M$/year - - #district heating peaking fuel annual cost - if model.surfaceplant.enduseoption.value == EndUseOptions.DISTRICT_HEATING: #district heating - self.annualngcost.value = model.surfaceplant.annualngdemand.value*self.ngprice.value/1000/self.peakingboilerefficiency.value #array with annual O&M cost for peaking fuel + # 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.enduseoption.value == EndUseOptions.HEAT_PUMP: # heat pump: + self.averageannualheatpumpelectricitycost.value = np.average( + model.surfaceplant.HeatPumpElectricitykWhUsed.value) * model.surfaceplant.elecprice.value / 1E6 # M$/year + + # district heating peaking fuel annual cost + if model.surfaceplant.enduseoption.value == EndUseOptions.DISTRICT_HEATING: # district heating + self.annualngcost.value = model.surfaceplant.annualngdemand.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.enduseoption.value in [EndUseOptions.HEAT, EndUseOptions.ABSORPTION_CHILLER, EndUseOptions.HEAT_PUMP, EndUseOptions.DISTRICT_HEATING]: - self.averageannualpumpingcosts.value = np.average(model.surfaceplant.PumpingkWh.value)*model.surfaceplant.elecprice.value/1E6 #M$/year + # calculate average annual pumping costs in case no electricity is provided + if model.surfaceplant.enduseoption.value in [EndUseOptions.HEAT, EndUseOptions.ABSORPTION_CHILLER, + EndUseOptions.HEAT_PUMP, EndUseOptions.DISTRICT_HEATING]: + self.averageannualpumpingcosts.value = np.average( + model.surfaceplant.PumpingkWh.value) * model.surfaceplant.elecprice.value / 1E6 # M$/year if not self.oamtotalfixed.Valid: # labor cost if model.surfaceplant.enduseoption.value == EndUseOptions.ELECTRICITY: # electricity if np.max(model.surfaceplant.ElectricityProduced.value) < 2.5: - self.Claborcorrelation = 236./1E3 # M$/year + self.Claborcorrelation = 236. / 1E3 # M$/year else: - self.Claborcorrelation = (589.*math.log(np.max(model.surfaceplant.ElectricityProduced.value))-304.)/1E3 # M$/year + 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 + 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 + 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 + 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) + 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) + 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.utilfactor.value * - 365.*24.*3600./1E6*925./1E6) + self.Coamwater.value = self.oamwateradjfactor.value * (model.wellbores.nprod.value * + model.wellbores.prodwellflowrate.value * + model.reserv.waterloss.value * model.surfaceplant.utilfactor.value * + 365. * 24. * 3600. / 1E6 * 925. / 1E6) # additional O&M cost for absorption chiller if used if model.surfaceplant.enduseoption.value == EndUseOptions.ABSORPTION_CHILLER: # absorption chiller: @@ -2027,7 +2188,7 @@ def Calculate(self, model: Model) -> None: # 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) + 1.5 / 100. * (self.Cplant.value - self.chillercapex.value) + 0.75 * self.Claborcorrelation) else: self.chilleropex.value = 0 @@ -2045,8 +2206,7 @@ def Calculate(self, model: Model) -> None: 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) + 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) @@ -2054,21 +2214,22 @@ def Calculate(self, model: Model) -> None: 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.plantlifetime.value + ( + self.Cwell.value + self.Cstim.value) * model.wellbores.redrill.value / model.surfaceplant.plantlifetime.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.value = model.reserv.depth.value / 1000.0 model.reserv.depth.CurrentUnits = LengthUnit.KILOMETERS # build the price models self.ElecPrice.value = BuildPricingModel(model.surfaceplant.plantlifetime.value, 0, - self.ElecStartPrice.value, self.ElecEndPrice.value, - self.ElecEscalationStart.value, self.ElecEscalationRate.value) + self.ElecStartPrice.value, self.ElecEndPrice.value, + self.ElecEscalationStart.value, self.ElecEscalationRate.value) self.HeatPrice.value = BuildPricingModel(model.surfaceplant.plantlifetime.value, 0, - self.HeatStartPrice.value, self.HeatEndPrice.value, - self.HeatEscalationStart.value, self.HeatEscalationRate.value) + self.HeatStartPrice.value, self.HeatEndPrice.value, + self.HeatEscalationStart.value, self.HeatEscalationRate.value) # Add in the FlatLicenseEtc, OtherIncentives, TotalGrant, AnnualLicenseEtc, and TaxRelief self.CCap.value = self.CCap.value + self.FlatLicenseEtc.value - self.OtherIncentives.value - self.TotalGrant.value @@ -2083,38 +2244,45 @@ def Calculate(self, model: Model) -> None: self.TotalCummRevenue.value = self.ElecCummRevenue.value elif model.surfaceplant.enduseoption.value == EndUseOptions.HEAT: self.HeatRevenue.value, self.HeatCummRevenue.value = CalculateRevenue( - model.surfaceplant.plantlifetime.value, model.surfaceplant.ConstructionYears.value, self.CCap.value, - self.Coam.value, model.surfaceplant.HeatkWhProduced.value, self.HeatPrice.value) + model.surfaceplant.plantlifetime.value, model.surfaceplant.ConstructionYears.value, self.CCap.value, + self.Coam.value, model.surfaceplant.HeatkWhProduced.value, self.HeatPrice.value) self.TotalRevenue.value = self.HeatRevenue.value self.TotalCummRevenue.value = self.HeatCummRevenue.value - elif model.surfaceplant.enduseoption.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICTY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICTY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICTY]: #co-gen - #else: + elif model.surfaceplant.enduseoption.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, + EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICTY, + EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICTY, + EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, + EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, + EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICTY]: # co-gen + # else: self.ElecRevenue.value, self.ElecCummRevenue.self = CalculateRevenue( - model.surfaceplant.plantlifetime.value, model.surfaceplant.ConstructionYears.value, self.CCap.value, - self.Coam.value, model.surfaceplant.NetkWhProduced.value, self.ElecPrice.value) - # note that CAPEX & OPEX are 0.0 because we only want them counted once, and it will be accounted - # for in the previous line + model.surfaceplant.plantlifetime.value, model.surfaceplant.ConstructionYears.value, self.CCap.value, + self.Coam.value, model.surfaceplant.NetkWhProduced.value, self.ElecPrice.value) + # note that CAPEX & OPEX are 0.0 because we only want them counted once, and it will be accounted + # for in the previous line self.HeatRevenue.value, self.HeatCummRevenue.self = CalculateRevenue( - model.surfaceplant.plantlifetime.value, model.surfaceplant.ConstructionYears.value, 0.0, 0.0, - model.surfaceplant.HeatkWhProduced.value, self.HeatPrice.value) - self.TotalRevenue.value = [0.0] * (model.surfaceplant.plantlifetime.value+model.surfaceplant.ConstructionYears.value) - self.TotalCummRevenue.value = [0.0] * (model.surfaceplant.plantlifetime.value+model.surfaceplant.ConstructionYears.value) - for i in range(0, model.surfaceplant.plantlifetime.value+model.surfaceplant.ConstructionYears.value, 1): + model.surfaceplant.plantlifetime.value, model.surfaceplant.ConstructionYears.value, 0.0, 0.0, + model.surfaceplant.HeatkWhProduced.value, self.HeatPrice.value) + self.TotalRevenue.value = [0.0] * ( + model.surfaceplant.plantlifetime.value + model.surfaceplant.ConstructionYears.value) + self.TotalCummRevenue.value = [0.0] * ( + model.surfaceplant.plantlifetime.value + model.surfaceplant.ConstructionYears.value) + for i in range(0, model.surfaceplant.plantlifetime.value + model.surfaceplant.ConstructionYears.value, 1): self.TotalRevenue.value[i] = self.ElecRevenue.value[i] + self.HeatRevenue.value[i] self.TotalCummRevenue.value[i] = self.TotalRevenue.value[i] if i > 0: - self.TotalCummRevenue.value[i] = self.TotalCummRevenue.value[i-1] + self.TotalRevenue.value[i] + 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.plantlifetime.value, self.FixedInternalRate.value, - self.TotalRevenue.value, self.TotalCummRevenue.value, self.CCap.value, - self.Coam.value) + self.TotalRevenue.value, self.TotalCummRevenue.value, self.CCap.value, + self.Coam.value) # Calculate LCOE/LCOH self.LCOE.value, self.LCOH.value, self.LCOC.value = CalculateLCOELCOH(self, model) - model.logger.info("complete "+ str(__class__) + ": " + sys._getframe().f_code.co_name) + model.logger.info("complete " + str(__class__) + ": " + sys._getframe().f_code.co_name) def __str__(self): return "Economics" diff --git a/src/geophires_x/EconomicsAddOns.py b/src/geophires_x/EconomicsAddOns.py index 9defea6e..9f047793 100644 --- a/src/geophires_x/EconomicsAddOns.py +++ b/src/geophires_x/EconomicsAddOns.py @@ -17,39 +17,35 @@ def __init__(self, model: Model): 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 AddOns. - - :param self: Store data that will be used by the class + 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. + set up the parameters using the Parameter Constructors (intParameter, floatParameter, strParameter, etc.); + initialize with their name, default value, and valid range (if int or float). Optionally, you can specify: + Required (is it required to run? default value = False), ErrMessage (what GEOPHIRES will report if the value + provided is invalid, "assume default value (see manual)"), ToolTipText (when there is a GIU, this is the + text that the user will see, "This is ToolTip Text"), UnitType (the type of units associated with this + parameter (length, temperature, density, etc), Units.NONE), CurrentUnits (what the units are for this + parameter (meters, Celsius, gm/cc, etc., Units:NONE), and PreferredUnits (usually equal to CurrentUnits, + but these are the units that the calculations assume when running, Units.NONE :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 - :doc-author: Malcolm Ross """ model.logger.info(f'Init {str(__class__)}: {sys._getframe().f_code.co_name}') - super().__init__(model) # initialize the parent parameters and variables + super().__init__(model) # initialize the parent parameters and variables sclass = str(__class__).replace("", "") self.MyPath = os.path.abspath(__file__) - # 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. - - # set up the parameters using the Parameter Constructors (intParameter, floatParameter, strParameter, etc.); - # initialize with their name, default value, and valid range (if int or float). Optionally, you can specify: - # Required (is it required to run? default value = False), ErrMessage (what GEOPHIRES will report if the value - # provided is invalid, "assume default value (see manual)"), ToolTipText (when there is a GIU, this is the - # text that the user will see, "This is ToolTip Text"), UnitType (the type of units associated with this - # parameter (length, temperature, density, etc), Units.NONE), CurrentUnits (what the units are for this - # parameter (meters, Celcius, gm/cc, etc., Units:NONE), and PreferredUnits (usually equal to CurrentUnits, - # but these are the units that the calculations assume when running, Units.NONE - self.AddOnNickname = self.ParameterDict[self.AddOnNickname.Name] = listParameter( "AddOn Nickname", UnitType=Units.NONE, @@ -111,19 +107,22 @@ def __init__(self, model: Model): PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR ) - self.AddOnElecGainedTotalPerYear = self.OutputParameterDict[self.AddOnElecGainedTotalPerYear.Name] = OutputParameter( + self.AddOnElecGainedTotalPerYear = self.OutputParameterDict[ + self.AddOnElecGainedTotalPerYear.Name] = OutputParameter( "AddOn Electricity Gained Total Per Year", UnitType=Units.ENERGYFREQUENCY, PreferredUnits=EnergyFrequencyUnit.KWPERYEAR, CurrentUnits=EnergyFrequencyUnit.KWPERYEAR ) - self.AddOnHeatGainedTotalPerYear = self.OutputParameterDict[self.AddOnHeatGainedTotalPerYear.Name] = OutputParameter( + self.AddOnHeatGainedTotalPerYear = self.OutputParameterDict[ + self.AddOnHeatGainedTotalPerYear.Name] = OutputParameter( "AddOn Heat Gained Total Per Year", UnitType=Units.ENERGYFREQUENCY, PreferredUnits=EnergyFrequencyUnit.KWPERYEAR, CurrentUnits=EnergyFrequencyUnit.KWPERYEAR ) - self.AddOnProfitGainedTotalPerYear = self.OutputParameterDict[self.AddOnProfitGainedTotalPerYear.Name] = OutputParameter( + self.AddOnProfitGainedTotalPerYear = self.OutputParameterDict[ + self.AddOnProfitGainedTotalPerYear.Name] = OutputParameter( "AddOn Profit Gained Total Per Year", UnitType=Units.CURRENCYFREQUENCY, PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, @@ -211,11 +210,9 @@ def read_parameters(self, model: Model) -> None: extension. The user can create as many or as few parameters as needed. Each parameter is created by a call to the InputParameter class, which is defined below, and then stored in a dictionary with a name assigned to - - :param self: Access the class variables :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 - :doc-author: Malcolm Ross """ model.logger.info(f'Init {str(__class__)}: {sys._getframe().f_code.co_name}') super().read_parameters(model) # read the parameters for the parent. @@ -257,23 +254,20 @@ 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 self: Access variables that belongs to the class + 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 - :doc-author: Malcolm Ross """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) - # his 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! - # sum all the AddOn values together, so we can treat all AddOns together. If an AddOn slot is not used, # it has zeros for the values, so this won't create problems if len(self.AddOnCAPEX.value) > 0: @@ -297,13 +291,14 @@ def Calculate(self, model: Model) -> None: if model.surfaceplant.enduseoption.value != EndUseOptions.ELECTRICITY: model.surfaceplant.HeatkWhProduced.value[i] = model.surfaceplant.HeatkWhProduced.value[i] + self.AddOnHeatGainedTotalPerYear.value else: - model.surfaceplant.HeatkWhProduced.value[i] = model.surfaceplant.HeatkWhProduced.value[i] + self.AddOnHeatGainedTotalPerYear.value # all the end-use option of direct-use only component + # all the end-use option of direct-use only components have a heat generation component + model.surfaceplant.HeatkWhProduced.value[i] = model.surfaceplant.HeatkWhProduced.value[i] + self.AddOnHeatGainedTotalPerYear.value # Calculate the adjusted OPEX and CAPEX self.AdjustedProjectCAPEX.value = model.economics.CCap.value + self.AddOnCAPEXTotal.value self.AdjustedProjectOPEX.value = model.economics.Coam.value + self.AddOnOPEXTotalPerYear.value - AddOnCapCostPerYear = self.AddOnCAPEXTotal.value/model.surfaceplant.ConstructionYears.value - ProjectCapCostPerYear = self.AdjustedProjectCAPEX.value/model.surfaceplant.ConstructionYears.value + AddOnCapCostPerYear = self.AddOnCAPEXTotal.value / model.surfaceplant.ConstructionYears.value + ProjectCapCostPerYear = self.AdjustedProjectCAPEX.value / model.surfaceplant.ConstructionYears.value # (re)Calculate the revenues self.AddOnElecRevenue.value = [0.0] * model.surfaceplant.plantlifetime.value @@ -328,11 +323,16 @@ def Calculate(self, model: Model) -> None: AddOnElectricalEnergy = self.AddOnElecGainedTotalPerYear.value AddOnHeatEnergy = self.AddOnHeatGainedTotalPerYear.value - self.AddOnElecRevenue.value[i] = (AddOnElectricalEnergy * model.economics.ElecPrice.value[i]) / 1_000_000.0 # Electricity revenue in MUSD - self.AddOnHeatRevenue.value[i] = (AddOnHeatEnergy * model.economics.HeatPrice.value[i]) / 1_000_000.0 # Heat revenue in MUSD - self.AddOnRevenue.value[i] = self.AddOnElecRevenue.value[i] + self.AddOnHeatRevenue.value[i] + self.AddOnProfitGainedTotalPerYear.value - self.AddOnOPEXTotalPerYear.value + self.AddOnElecRevenue.value[i] = (AddOnElectricalEnergy * model.economics.ElecPrice.value[ + i]) / 1_000_000.0 # Electricity revenue in MUSD + self.AddOnHeatRevenue.value[i] = (AddOnHeatEnergy * model.economics.HeatPrice.value[ + i]) / 1_000_000.0 # Heat revenue in MUSD + self.AddOnRevenue.value[i] = self.AddOnElecRevenue.value[i] + self.AddOnHeatRevenue.value[ + i] + self.AddOnProfitGainedTotalPerYear.value - self.AddOnOPEXTotalPerYear.value self.AddOnCashFlow.value[i] = self.AddOnRevenue.value[i] - self.ProjectCashFlow.value[i] = self.AddOnRevenue.value[i] + (((ProjectElectricalEnergy * model.economics.ElecPrice.value[i]) + (ProjectHeatEnergy * model.economics.HeatPrice.value[i])) / 1_000_000.0) - model.economics.Coam.value # MUSD + self.ProjectCashFlow.value[i] = self.AddOnRevenue.value[i] + (((ProjectElectricalEnergy * + model.economics.ElecPrice.value[i]) + (ProjectHeatEnergy * + model.economics.HeatPrice.value[i])) / 1_000_000.0) - model.economics.Coam.value # MUSD # now 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 @@ -342,11 +342,11 @@ def Calculate(self, model: Model) -> None: # Now calculate a new "NPV", "IRR", "VIR", "Payback Period", and "MOIC" # Calculate more financial values using numpy financials - self.ProjectNPV.value = npf.npv(self.FixedInternalRate.value/100, self.ProjectCashFlow.value) + self.ProjectNPV.value = npf.npv(self.FixedInternalRate.value / 100, self.ProjectCashFlow.value) self.ProjectIRR.value = npf.irr(self.ProjectCashFlow.value) if math.isnan(self.ProjectIRR.value): self.ProjectIRR.value = 0.0 - self.ProjectVIR.value = 1.0 + (self.ProjectNPV.value/self.AdjustedProjectCAPEX.value) + self.ProjectVIR.value = 1.0 + (self.ProjectNPV.value / self.AdjustedProjectCAPEX.value) # calculate Cummcashflows and paybacks self.ProjectCummCashFlow.value = [0.0] * len(self.ProjectCashFlow.value) @@ -356,7 +356,8 @@ def Calculate(self, model: Model) -> None: self.ProjectCummCashFlow.value[i] = val else: self.ProjectCummCashFlow.value[i] = self.ProjectCummCashFlow.value[i - 1] + val - if self.ProjectCummCashFlow.value[i] > 0 >= self.ProjectCummCashFlow.value[i - 1]: # we just crossed the threshold into positive project cummcashflow, so we can calculate payback period + if self.ProjectCummCashFlow.value[i] > 0 >= self.ProjectCummCashFlow.value[ + i - 1]: # we just crossed the threshold into positive project cummcashflow, so we can calculate payback period dFullDiff = self.ProjectCummCashFlow.value[i] + math.fabs(self.ProjectCummCashFlow.value[(i - 1)]) dPerc = math.fabs(self.ProjectCummCashFlow.value[(i - 1)]) / dFullDiff self.ProjectPaybackPeriod.value = i + dPerc @@ -368,14 +369,17 @@ def Calculate(self, model: Model) -> None: self.AddOnCummCashFlow.value[0] = val else: self.AddOnCummCashFlow.value[i] = self.AddOnCummCashFlow.value[i - 1] + val - if self.AddOnCummCashFlow.value[i] > 0 >= self.AddOnCummCashFlow.value[i - 1]: # we just crossed the threshold into positive project cummcashflow, so we can calculate payback period + if self.AddOnCummCashFlow.value[i] > 0 >= self.AddOnCummCashFlow.value[ + i - 1]: # we just crossed the threshold into positive project cummcashflow, so we can calculate payback period dFullDiff = self.AddOnCummCashFlow.value[i] + math.fabs(self.AddOnCummCashFlow.value[(i - 1)]) dPerc = math.fabs(self.AddOnCummCashFlow.value[(i - 1)]) / dFullDiff self.AddOnPaybackPeriod.value = i + dPerc i = i + 1 # Calculate MOIC which depends on CumCashFlow - self.ProjectMOIC.value = self.ProjectCummCashFlow.value[len(self.ProjectCummCashFlow.value)-1] / (self.AdjustedProjectCAPEX.value + (self.AdjustedProjectOPEX.value * model.surfaceplant.plantlifetime.value)) + self.ProjectMOIC.value = self.ProjectCummCashFlow.value[len(self.ProjectCummCashFlow.value) - 1] / ( + self.AdjustedProjectCAPEX.value + ( + self.AdjustedProjectOPEX.value * model.surfaceplant.plantlifetime.value)) # recalculate LCOE/LCOH self.LCOE.value, self.LCOH.value, LCOC = Economics.CalculateLCOELCOH(self, model) diff --git a/src/geophires_x/EconomicsCCUS.py b/src/geophires_x/EconomicsCCUS.py index c76ed5ed..ece28324 100644 --- a/src/geophires_x/EconomicsCCUS.py +++ b/src/geophires_x/EconomicsCCUS.py @@ -16,38 +16,35 @@ def __init__(self, model: Model): 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 the CCUS Economics. - - :param self: Store data that will be used by the class + 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. + set up the parameters using the Parameter Constructors (intParameter, floatParameter, strParameter, etc.); + initialize with their name, default value, and valid range (if int or float). Optionally, you can specify: + Required (is it required to run? default value = False), ErrMessage (what GEOPHIRES will report if the + value provided is invalid, "assume default value (see manual)"), ToolTipText (when there is a GUI, + this is the text that the user will see, "This is ToolTip Text"), + UnitType (the type of units associated with this parameter (length, temperature, density, etc), Units.NONE), + CurrentUnits (what the units are for this parameter (meters, celcius, gm/cc, etc., Units:NONE), + and PreferredUnits (usually equal to CurrentUnits, but these are the units that the calculations assume + when running, Units.NONE :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 - :doc-author: Malcolm Ross """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) - super().__init__(model) # initialize the parent parameters and variables + super().__init__(model) # initialize the parent parameters and variables sclass = str(__class__).replace("", "") self.MyPath = os.path.abspath(__file__) - # 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. - - # set up the parameters using the Parameter Constructors (intParameter, floatParameter, strParameter, etc.); - # initialize with their name, default value, and valid range (if int or float). Optionally, you can specify: - # Required (is it required to run? default value = False), ErrMessage (what GEOPHIRES will report if the - # value provided is invalid, "assume default value (see manual)"), ToolTipText (when there is a GUI, - # this is the text that the user will see, "This is ToolTip Text"), - # UnitType (the type of units associated with this parameter (length, temperature, density, etc), Units.NONE), - # CurrentUnits (what the units are for this parameter (meters, celcius, gm/cc, etc., Units:NONE), - # and PreferredUnits (usually equal to CurrentUnits, but these are the units that the calculations assume - # when running, Units.NONE self.FixedInternalRate = self.ParameterDict[self.FixedInternalRate.Name] = floatParameter( "Fixed Internal Rate", value=6.25, @@ -79,7 +76,8 @@ def __init__(self, model: Model): PreferredUnits=CostPerMassUnit.DOLLARSPERLB, CurrentUnits=CostPerMassUnit.DOLLARSPERLB ) - self.CCUSEscalationStart = self.ParameterDict[self.CCUSEscalationStart.Name] = intParameter("CCUS Escalation Start Year", + self.CCUSEscalationStart = self.ParameterDict[self.CCUSEscalationStart.Name] = intParameter( + "CCUS Escalation Start Year", value=0, DefaultValue=0, AllowableRange=list(range(0, 101, 1)), @@ -88,7 +86,7 @@ def __init__(self, model: Model): CurrentUnits=TimeUnit.YEAR, ErrMessage="assume default CCUS escalation delay time (5 years)", ToolTipText="Number of years after start of project before start of CCUS incentives" - ) + ) self.CCUSEscalationRate = self.ParameterDict[self.CCUSEscalationRate.Name] = floatParameter( "CCUS Escalation Rate Per Year", value=0.0, @@ -141,7 +139,8 @@ def __init__(self, model: Model): PreferredUnits=EnergyCostUnit.DOLLARSPERKWH, CurrentUnits=EnergyCostUnit.DOLLARSPERKWH ) - self.HeatEscalationStart = self.ParameterDict[self.HeatEscalationStart.Name] = intParameter("Heat Escalation Start Year", + self.HeatEscalationStart = self.ParameterDict[self.HeatEscalationStart.Name] = intParameter( + "Heat Escalation Start Year", value=5, DefaultValue=5, AllowableRange=list(range(0, 101, 1)), @@ -150,7 +149,7 @@ def __init__(self, model: Model): CurrentUnits=TimeUnit.YEAR, ErrMessage="assume default heat escalation delay time (5 years)", ToolTipText="Number of years after start of project before start of escalation" - ) + ) self.HeatEscalationRate = self.ParameterDict[self.HeatEscalationRate.Name] = floatParameter( "Heat Escalation Rate Per Year", value=0.0, @@ -282,14 +281,16 @@ def __init__(self, model: Model): PreferredUnits=CurrencyUnit.MDOLLARS, CurrentUnits=CurrencyUnit.MDOLLARS ) - self.CarbonThatWouldHaveBeenProducedAnnually = self.OutputParameterDict[self.CarbonThatWouldHaveBeenProducedAnnually.Name] = OutputParameter( + self.CarbonThatWouldHaveBeenProducedAnnually = self.OutputParameterDict[ + self.CarbonThatWouldHaveBeenProducedAnnually.Name] = OutputParameter( "Annual Saved Carbon Production", value=[0.0], UnitType=Units.MASS, PreferredUnits=MassUnit.LB, CurrentUnits=MassUnit.LB ) - self.CarbonThatWouldHaveBeenProducedTotal = self.OutputParameterDict[self.CarbonThatWouldHaveBeenProducedTotal.Name] = OutputParameter( + self.CarbonThatWouldHaveBeenProducedTotal = self.OutputParameterDict[ + self.CarbonThatWouldHaveBeenProducedTotal.Name] = OutputParameter( "Annual Saved Carbon Production", UnitType=Units.MASS, PreferredUnits=MassUnit.LB, @@ -317,15 +318,12 @@ 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 self: Access variables that belong to a class :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 - :doc-author: Malcolm Ross """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) - super().read_parameters(model) # read the parameters for the parent. + super().read_parameters(model) # read the parameters for the parent. # 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 @@ -343,33 +341,36 @@ 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 self: Access variables that belongs to the class + 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 - :doc-author: Malcolm Ross """ model.logger.info("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! - self.CCUSRevenue.value = [0.0] * model.surfaceplant.plantlifetime.value self.CCUSCashFlow.value = [0.0] * model.surfaceplant.plantlifetime.value self.CCUSCummCashFlow.value = [0.0] * model.surfaceplant.plantlifetime.value self.CarbonThatWouldHaveBeenProducedAnnually.value = [0.0] * model.surfaceplant.plantlifetime.value self.CarbonThatWouldHaveBeenProducedTotal.value = 0.0 - ProjectCapCostPerYear = model.economics.CCap.value/self.ConstructionYears.value + ProjectCapCostPerYear = model.economics.CCap.value / self.ConstructionYears.value # Calculate carbon price models - self.CCUSPrice.value = BuildPricingModel(model.surfaceplant.plantlifetime.value, self.CCUSEscalationStart.value, self.CCUSStartPrice.value, self.CCUSEndPrice.value, self.CCUSEscalationStart.value, self.CCUSEscalationRate.value) - self.CCUSOnElecPrice.value = BuildPricingModel(model.surfaceplant.plantlifetime.value, 0, self.ElecStartPrice.value, self.ElecEndPrice.value, self.ElecEscalationStart.value, self.ElecEscalationRate.value) - self.CCUSOnHeatPrice.value = BuildPricingModel(model.surfaceplant.plantlifetime.value, 0, self.HeatStartPrice.value, self.HeatEndPrice.value, self.HeatEscalationStart.value, self.HeatEscalationRate.value) + self.CCUSPrice.value = BuildPricingModel(model.surfaceplant.plantlifetime.value, self.CCUSEscalationStart.value, + self.CCUSStartPrice.value, self.CCUSEndPrice.value, + self.CCUSEscalationStart.value, self.CCUSEscalationRate.value) + self.CCUSOnElecPrice.value = BuildPricingModel(model.surfaceplant.plantlifetime.value, 0, + self.ElecStartPrice.value, self.ElecEndPrice.value, + self.ElecEscalationStart.value, self.ElecEscalationRate.value) + self.CCUSOnHeatPrice.value = BuildPricingModel(model.surfaceplant.plantlifetime.value, 0, + self.HeatStartPrice.value, self.HeatEndPrice.value, + self.HeatEscalationStart.value, self.HeatEscalationRate.value) # Figure out how much energy is being produced each year, and the amount of carbon that would have been # produced if that energy had been made using the grid average carbon production. @@ -397,10 +398,15 @@ def Calculate(self, model: Model) -> None: dBothEnergy = dElectricalEnergy + dHeatEnergy self.CarbonThatWouldHaveBeenProducedAnnually.value[i] = dBothEnergy * self.CCUSGridCO2.value - self.CarbonThatWouldHaveBeenProducedTotal.value = self.CarbonThatWouldHaveBeenProducedTotal.value + self.CarbonThatWouldHaveBeenProducedAnnually.value[i] - self.CCUSRevenue.value[i] = (self.CarbonThatWouldHaveBeenProducedAnnually.value[i] * self.CCUSPrice.value[i]) / 1_000_000.0 # CCUS (from both heat and elec) based on total, not net energy; in $M + self.CarbonThatWouldHaveBeenProducedTotal.value = self.CarbonThatWouldHaveBeenProducedTotal.value + \ + self.CarbonThatWouldHaveBeenProducedAnnually.value[i] + self.CCUSRevenue.value[i] = (self.CarbonThatWouldHaveBeenProducedAnnually.value[i] * self.CCUSPrice.value[ + i]) / 1_000_000.0 # CCUS (from both heat and elec) based on total, not net energy; in $M self.CCUSCashFlow.value[i] = self.CCUSRevenue.value[i] - self.ProjectCashFlow.value[i] = self.CCUSRevenue.value[i] + (((ProjectElectricalEnergy * self.CCUSOnElecPrice.value[i]) + (ProjectHeatEnergy * self.CCUSOnHeatPrice.value[i])) / 1_000_000.0) - model.economics.Coam.value # MUSD + self.ProjectCashFlow.value[i] = (self.CCUSRevenue.value[i] + (((ProjectElectricalEnergy * + self.CCUSOnElecPrice.value[i]) + + (ProjectHeatEnergy * self.CCUSOnHeatPrice.value[i])) / 1_000_000.0) - + model.economics.Coam.value) # MUSD # Calculate the Carbon credit cumulative cash flows i = 0 @@ -424,22 +430,25 @@ def Calculate(self, model: Model) -> None: self.ProjectCummCashFlow.value[0] = val else: self.ProjectCummCashFlow.value[i] = self.ProjectCummCashFlow.value[i - 1] + val - if self.ProjectCummCashFlow.value[i] > 0 >= self.ProjectCummCashFlow.value[i - 1]: # we just crossed the threshold into positive project cummcashflow, so we can calculate payback period + if self.ProjectCummCashFlow.value[i] > 0 >= self.ProjectCummCashFlow.value[ + i - 1]: # we just crossed the threshold into positive project cummcashflow, so we can calculate payback period dFullDiff = self.ProjectCummCashFlow.value[i] + math.fabs(self.ProjectCummCashFlow.value[(i - 1)]) dPerc = math.fabs(self.ProjectCummCashFlow.value[(i - 1)]) / dFullDiff self.ProjectPaybackPeriod.value = i + dPerc i = i + 1 # Calculate more financial values using numpy financials - self.ProjectNPV.value = npf.npv(self.FixedInternalRate.value/100, self.ProjectCashFlow.value) + self.ProjectNPV.value = npf.npv(self.FixedInternalRate.value / 100, self.ProjectCashFlow.value) self.ProjectIRR.value = npf.irr(self.ProjectCashFlow.value) if math.isnan(self.ProjectIRR.value): self.ProjectIRR.value = 0.0 - self.ProjectVIR.value = 1.0 + (self.ProjectNPV.value/model.economics.CCap.value) + self.ProjectVIR.value = 1.0 + (self.ProjectNPV.value / model.economics.CCap.value) # Calculate MOIC which depends on CumCashFlow - self.ProjectMOIC.value = self.ProjectCummCashFlow.value[len(self.ProjectCummCashFlow.value)-1] / (model.economics.CCap.value + (model.economics.Coam.value * model.surfaceplant.plantlifetime.value)) + self.ProjectMOIC.value = self.ProjectCummCashFlow.value[len(self.ProjectCummCashFlow.value) - 1] / ( + model.economics.CCap.value + (model.economics.Coam.value * model.surfaceplant.plantlifetime.value)) model.logger.info("complete " + str(__class__) + ": " + sys._getframe().f_code.co_name) - def __str__(self): return "EconomicsCCUS" + def __str__(self): + return "EconomicsCCUS" diff --git a/src/geophires_x/EconomicsS_DAC_GT.py b/src/geophires_x/EconomicsS_DAC_GT.py index bc1038df..aa699324 100644 --- a/src/geophires_x/EconomicsS_DAC_GT.py +++ b/src/geophires_x/EconomicsS_DAC_GT.py @@ -1,7 +1,5 @@ import sys import os -import math -import numpy as np from .Parameter import floatParameter, OutputParameter, ReadParameter from .Units import * from .OptionList import EndUseOptions @@ -9,6 +7,7 @@ import numpy as np import geophires_x.Economics as Economics + class EconomicsS_DAC_GT(Economics.Economics): """ Solid Sorbent Direct Air Capture Using Geothermal Energy Resources (S-DAC-GT) @@ -22,116 +21,397 @@ class EconomicsS_DAC_GT(Economics.Economics): Integration with GEOPHIRES: Malcolm I Ross Prepared 6/13/2023 """ - 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 self: Reference the class object itself + 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. + 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. :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, and is used to initialize the class - :doc-author: Malcolm Ross """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) - #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 sublass 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 disctionaries contains a list of all the parameters set in this object, stored as "Parameter" and OutputParameter Objects. This will alow us later to access them in a user interface and get that list, along with unit type, preferred units, etc. + # These disctionaries contains a list of all the parameters set in this object, stored as "Parameter" and + # OutputParameter Objects. This will alow us later to access them in a user interface and get that list, + # along with unit type, preferred units, etc. self.ParameterDict = {} self.OutputParameterDict = {} - self.wacc = self.ParameterDict[self.wacc.Name] = floatParameter("WACC", value = 10.0, DefaultValue = 10.0, Min=0.1, Max=30.0, UnitType = Units.PERCENT, PreferredUnits = PercentUnit.PERCENT, CurrentUnits = PercentUnit.PERCENT, ErrMessage = "assume default Weighted Average Cost of Capital (10%)", ToolTipText="Weighted Average Cost of Capital (percent)") - self.CAPEX = self.ParameterDict[self.CAPEX.Name] = floatParameter("S-DAC-GT CAPEX", value = 1379.0, DefaultValue=1379.0, Min=100.0, Max=5000.0, UnitType = Units.COSTPERMASS, PreferredUnits = CostPerMassUnit.DOLLARSPERTONNE, CurrentUnits = CostPerMassUnit.DOLLARSPERTONNE, ErrMessage = "assume default CAPEX (1379 USD per tonne CO2 capacity)", ToolTipText="CAPEX (USD per tonne CO2 capacity)") - self.OPEX = self.ParameterDict[self.OPEX.Name] = floatParameter("S-DAC-GT OPEX", value = 56.0, DefaultValue=56.0, Min=10.0, Max=500.0, UnitType = Units.COSTPERMASS, PreferredUnits = CostPerMassUnit.DOLLARSPERTONNE, CurrentUnits = CostPerMassUnit.DOLLARSPERTONNE, ErrMessage = "assume default OPEX (56 USD per tonne CO2)", ToolTipText="OPEX (USD per tonne CO2)") - self.elec = self.ParameterDict[self.elec.Name] = floatParameter("S-DAC-GT Electrical Energy", value = 916.0, DefaultValue=916.0, Min=100.0, Max=5000.0, UnitType = Units.ENERGYPERCO2, PreferredUnits = EnergyPerCO2Unit.KWHEPERTONNE, CurrentUnits = EnergyPerCO2Unit.KWHEPERTONNE, ErrMessage = "assume default Electrical Energy (916 kWh_e per tonne CO2)", ToolTipText="Electrical Energy (kWh_e per tonne CO2)") - self.therm = self.ParameterDict[self.therm.Name] = floatParameter("S-DAC-GT Thermal Energy", value = 1447.0, DefaultValue=1447.0, Min=100.0, Max=5000.0, UnitType = Units.ENERGYPERCO2, PreferredUnits = EnergyPerCO2Unit.KWTHPERTONNE, CurrentUnits = EnergyPerCO2Unit.KWTHPERTONNE, ErrMessage = "assume default Thermal Energy (1447 kW_th per tonne CO2)", ToolTipText="Thermal Energy (kW_th per tonne CO2)") - self.NG_price = self.ParameterDict[self.NG_price.Name] = floatParameter("S-DAC-GT Natural Gas Price", value = 5.0, DefaultValue=5.0, Min=0.5, Max=500.0, UnitType = Units.ENERGYCOST, PreferredUnits = EnergyCostUnit.DOLLARSPERMCF, CurrentUnits = EnergyCostUnit.DOLLARSPERMCF, ErrMessage = "assume default Natural Gas Price (5 USD per MCF)", ToolTipText="Natural Gas Price (USD per MCF)") - self.power_co2intensity = self.ParameterDict[self.power_co2intensity.Name] = floatParameter("S-DAC-GT CO2 Intensity of Electricity", value = 0.4, DefaultValue=0.4, Min=0.0, Max=1.0, UnitType = Units.CO2PRODUCTION, PreferredUnits = CO2ProductionUnit.TONNEPERMWH, CurrentUnits = CO2ProductionUnit.TONNEPERMWH, ErrMessage = "assume default CO2 Intensity of Electricity (0.4 tonne CO2 emitted per MWh)", ToolTipText="CO2 Intensity of Electricity (tonne CO2 emitted per MWh)") - self.NG_co2intensity = self.ParameterDict[self.NG_co2intensity.Name] = floatParameter("S-DAC-GT CO2 Intensity of Natural Gas", value = 0.194965384, DefaultValue=0.194965384, Min=0.0, Max=1.0, UnitType = Units.CO2PRODUCTION, PreferredUnits = CO2ProductionUnit.TONNEPERMWH, CurrentUnits = CO2ProductionUnit.TONNEPERMWH, ErrMessage = "assume default Natural Gas Intensity of Electricity (0.194965384 tonne CO2 emitted per MWh)", ToolTipText="CO2 Intensity of Natural Gas (tonne CO2 emitted per MWh)") - self.NG_EnergyDensity = self.ParameterDict[self.NG_EnergyDensity.Name] = floatParameter("S-DAC-GT Natural Gas Energy Density", value = 282.6142719, DefaultValue=282.6142719, Min=0.0, Max=1000.0, UnitType = Units.ENERGYDENSITY, PreferredUnits = EnergyDensityUnit.KWHPERMCF, CurrentUnits = EnergyDensityUnit.KWHPERMCF, ErrMessage = "assume default Natural Gas Energy Density (282.6142719 kWh per MCF)", ToolTipText="Natural Gas Energy Density (kWh per MCF)") - self.CAPEX_mult = self.ParameterDict[self.CAPEX_mult.Name] = floatParameter("S-DAC-GT CAPEX Multiplier", value = 1.0, DefaultValue=1.0, Min=0.5, Max=3.0, UnitType = Units.PERCENT, PreferredUnits = PercentUnit.TENTH, CurrentUnits = PercentUnit.TENTH, ErrMessage = "assume default CAPEX Multiplier (1.0)", ToolTipText="CAPEX Multiplier") - self.OPEX_mult = self.ParameterDict[self.OPEX_mult.Name] = floatParameter("S-DAC-GT OPEX Multiplier", value = 1.0, DefaultValue=1.0, Min=0.5, Max=3.0, UnitType = Units.PERCENT, PreferredUnits = PercentUnit.TENTH, CurrentUnits = PercentUnit.TENTH, ErrMessage = "assume default OPEX Multiplier (1.0)", ToolTipText="OPEX Multiplier") - self.therm_index = self.ParameterDict[self.therm_index.Name] = floatParameter("S-DAC-GT Thermal Energy Multiplier", value = 1.0, DefaultValue=1.0, Min=0.5, Max=1.8, UnitType = Units.PERCENT, PreferredUnits = PercentUnit.TENTH, CurrentUnits = PercentUnit.TENTH, ErrMessage = "assume default S-DAC Thermal Energy Multiplier (1.0)", ToolTipText="S-DAC Thermal Energy Multiplier [usually due to avg humidity/temperature]") - self.transport = self.ParameterDict[self.transport.Name] = floatParameter("S-DAC-GT CO2 Transportation Cost", value = 10.0, DefaultValue=10.0, Min=1.0, Max=50.0, UnitType = Units.COSTPERMASS, PreferredUnits = CostPerMassUnit.DOLLARSPERTONNE, CurrentUnits = CostPerMassUnit.DOLLARSPERTONNE, ErrMessage = "assume default CO2 Transportation Cost (10 USD per tonne CO2)", ToolTipText="CO2 Transportation Cost (USD per tonne CO2)") - self.storage = self.ParameterDict[self.storage.Name] = floatParameter("S-DAC-GT CO2 Storage Cost", value = 10.0, DefaultValue=10.0, Min=5.0, Max=50.0, UnitType = Units.COSTPERMASS, PreferredUnits = CostPerMassUnit.DOLLARSPERTONNE, CurrentUnits = CostPerMassUnit.DOLLARSPERTONNE, ErrMessage = "assume default CO2 Storage Cost (10 USD per tonne CO2)", ToolTipText="CO2 Storage Cost (USD per tonne CO2)") - self.EnergySplit = self.ParameterDict[self.EnergySplit.Name] = floatParameter("S-DAC-GT CO2 Percent Energy Devoted To Process", value = 0.5, DefaultValue=0.5, Min=0.0, Max=1.0, UnitType = Units.PERCENT, PreferredUnits = PercentUnit.TENTH, CurrentUnits = PercentUnit.TENTH, ErrMessage = "assume default Percent Energy Devoted To Process (50%)", ToolTipText="Percent Energy Devoted To Process (%)") - - #local variable initiation + self.wacc = self.ParameterDict[self.wacc.Name] = floatParameter( + "WACC", + value=10.0, + DefaultValue=10.0, + Min=0.1, + Max=30.0, + UnitType=Units.PERCENT, + PreferredUnits=PercentUnit.PERCENT, + CurrentUnits=PercentUnit.PERCENT, + ErrMessage="assume default Weighted Average Cost of Capital (10%)", + ToolTipText="Weighted Average Cost of Capital (percent)" + ) + self.CAPEX = self.ParameterDict[self.CAPEX.Name] = floatParameter( + "S-DAC-GT CAPEX", + value=1379.0, + DefaultValue=1379.0, + Min=100.0, + Max=5000.0, + UnitType=Units.COSTPERMASS, + PreferredUnits=CostPerMassUnit.DOLLARSPERTONNE, + CurrentUnits=CostPerMassUnit.DOLLARSPERTONNE, + ErrMessage="assume default CAPEX (1379 USD per tonne CO2 capacity)", + ToolTipText="CAPEX (USD per tonne CO2 capacity)" + ) + self.OPEX = self.ParameterDict[self.OPEX.Name] = floatParameter( + "S-DAC-GT OPEX", + value=56.0, + DefaultValue=56.0, + Min=10.0, + Max=500.0, + UnitType=Units.COSTPERMASS, + PreferredUnits=CostPerMassUnit.DOLLARSPERTONNE, + CurrentUnits=CostPerMassUnit.DOLLARSPERTONNE, + ErrMessage="assume default OPEX (56 USD per tonne CO2)", + ToolTipText="OPEX (USD per tonne CO2)" + ) + self.elec = self.ParameterDict[self.elec.Name] = floatParameter( + "S-DAC-GT Electrical Energy", + value=916.0, + DefaultValue=916.0, + Min=100.0, Max=5000.0, + UnitType=Units.ENERGYPERCO2, + PreferredUnits=EnergyPerCO2Unit.KWHEPERTONNE, + CurrentUnits=EnergyPerCO2Unit.KWHEPERTONNE, + ErrMessage="assume default Electrical Energy (916 kWh_e per tonne CO2)", + ToolTipText="Electrical Energy (kWh_e per tonne CO2)" + ) + self.therm = self.ParameterDict[self.therm.Name] = floatParameter( + "S-DAC-GT Thermal Energy", + value=1447.0, + DefaultValue=1447.0, + Min=100.0, + Max=5000.0, + UnitType=Units.ENERGYPERCO2, + PreferredUnits=EnergyPerCO2Unit.KWTHPERTONNE, + CurrentUnits=EnergyPerCO2Unit.KWTHPERTONNE, + ErrMessage="assume default Thermal Energy (1447 kW_th per tonne CO2)", + ToolTipText="Thermal Energy (kW_th per tonne CO2)" + ) + self.NG_price = self.ParameterDict[self.NG_price.Name] = floatParameter( + "S-DAC-GT Natural Gas Price", + value=5.0, + DefaultValue=5.0, + Min=0.5, + Max=500.0, + UnitType=Units.ENERGYCOST, + PreferredUnits=EnergyCostUnit.DOLLARSPERMCF, + CurrentUnits=EnergyCostUnit.DOLLARSPERMCF, + ErrMessage="assume default Natural Gas Price (5 USD per MCF)", + ToolTipText="Natural Gas Price (USD per MCF)" + ) + self.power_co2intensity = self.ParameterDict[self.power_co2intensity.Name] = floatParameter( + "S-DAC-GT CO2 Intensity of Electricity", + value=0.4, + DefaultValue=0.4, + Min=0.0, + Max=1.0, + UnitType=Units.CO2PRODUCTION, + PreferredUnits=CO2ProductionUnit.TONNEPERMWH, + CurrentUnits=CO2ProductionUnit.TONNEPERMWH, + ErrMessage="assume default CO2 Intensity of Electricity (0.4 tonne CO2 emitted per MWh)", + ToolTipText="CO2 Intensity of Electricity (tonne CO2 emitted per MWh)" + ) + self.NG_co2intensity = self.ParameterDict[self.NG_co2intensity.Name] = floatParameter( + "S-DAC-GT CO2 Intensity of Natural Gas", + value=0.194965384, + DefaultValue=0.194965384, + Min=0.0, + Max=1.0, + UnitType=Units.CO2PRODUCTION, + PreferredUnits=CO2ProductionUnit.TONNEPERMWH, + CurrentUnits=CO2ProductionUnit.TONNEPERMWH, + ErrMessage="assume default Natural Gas Intensity of Electricity (0.194965384 tonne CO2 emitted per MWh)", + ToolTipText="CO2 Intensity of Natural Gas (tonne CO2 emitted per MWh)" + ) + self.NG_EnergyDensity = self.ParameterDict[self.NG_EnergyDensity.Name] = floatParameter( + "S-DAC-GT Natural Gas Energy Density", + value=282.6142719, + DefaultValue=282.6142719, + Min=0.0, + Max=1000.0, + UnitType=Units.ENERGYDENSITY, + PreferredUnits=EnergyDensityUnit.KWHPERMCF, + CurrentUnits=EnergyDensityUnit.KWHPERMCF, + ErrMessage="assume default Natural Gas Energy Density (282.6142719 kWh per MCF)", + ToolTipText="Natural Gas Energy Density (kWh per MCF)" + ) + self.CAPEX_mult = self.ParameterDict[self.CAPEX_mult.Name] = floatParameter( + "S-DAC-GT CAPEX Multiplier", + value=1.0, + DefaultValue=1.0, + Min=0.5, + Max=3.0, + UnitType=Units.PERCENT, + PreferredUnits=PercentUnit.TENTH, + CurrentUnits=PercentUnit.TENTH, + ErrMessage="assume default CAPEX Multiplier (1.0)", + ToolTipText="CAPEX Multiplier" + ) + self.OPEX_mult = self.ParameterDict[self.OPEX_mult.Name] = floatParameter( + "S-DAC-GT OPEX Multiplier", + value=1.0, + DefaultValue=1.0, + Min=0.5, + Max=3.0, + UnitType=Units.PERCENT, + PreferredUnits=PercentUnit.TENTH, + CurrentUnits=PercentUnit.TENTH, + ErrMessage="assume default OPEX Multiplier (1.0)", + ToolTipText="OPEX Multiplier" + ) + self.therm_index = self.ParameterDict[self.therm_index.Name] = floatParameter( + "S-DAC-GT Thermal Energy Multiplier", + value=1.0, + DefaultValue=1.0, + Min=0.5, + Max=1.8, + UnitType=Units.PERCENT, + PreferredUnits=PercentUnit.TENTH, + CurrentUnits=PercentUnit.TENTH, + ErrMessage="assume default S-DAC Thermal Energy Multiplier (1.0)", + ToolTipText="S-DAC Thermal Energy Multiplier [usually due to avg humidity/temperature]" + ) + self.transport = self.ParameterDict[self.transport.Name] = floatParameter( + "S-DAC-GT CO2 Transportation Cost", + value=10.0, + DefaultValue=10.0, + Min=1.0, + Max=50.0, + UnitType=Units.COSTPERMASS, + PreferredUnits=CostPerMassUnit.DOLLARSPERTONNE, + CurrentUnits=CostPerMassUnit.DOLLARSPERTONNE, + ErrMessage="assume default CO2 Transportation Cost (10 USD per tonne CO2)", + ToolTipText="CO2 Transportation Cost (USD per tonne CO2)" + ) + self.storage = self.ParameterDict[self.storage.Name] = floatParameter( + "S-DAC-GT CO2 Storage Cost", + value=10.0, + DefaultValue=10.0, + Min=5.0, + Max=50.0, + UnitType=Units.COSTPERMASS, + PreferredUnits=CostPerMassUnit.DOLLARSPERTONNE, + CurrentUnits=CostPerMassUnit.DOLLARSPERTONNE, + ErrMessage="assume default CO2 Storage Cost (10 USD per tonne CO2)", + ToolTipText="CO2 Storage Cost (USD per tonne CO2)" + ) + self.EnergySplit = self.ParameterDict[self.EnergySplit.Name] = floatParameter( + "S-DAC-GT CO2 Percent Energy Devoted To Process", + value=0.5, + DefaultValue=0.5, + Min=0.0, + Max=1.0, + UnitType=Units.PERCENT, + PreferredUnits=PercentUnit.TENTH, + CurrentUnits=PercentUnit.TENTH, + ErrMessage="assume default Percent Energy Devoted To Process (50%)", + ToolTipText="Percent Energy Devoted To Process (%)" + ) + + # local variable initiation # Capital Recovery Rate or Fixed Charge Factor - set initially for definitions self.CRF = 0.1175 sclass = str(__class__).replace("","") + self.MyClass = sclass.replace("\'>", "") self.MyPath = os.path.abspath(__file__) - #Results - used by other objects or printed in output downstream - self.LCOD_elec = self.OutputParameterDict[self.LCOD_elec.Name] = OutputParameter("Total LCOD 100% electric", value = 0.0, UnitType = Units.COSTPERMASS, PreferredUnits = CostPerMassUnit.DOLLARSPERTONNE, CurrentUnits = CostPerMassUnit.DOLLARSPERTONNE) - self.LCOD_ng = self.OutputParameterDict[self.LCOD_ng.Name] = OutputParameter(Name = "Total LCOD natural gas", value = 0.0, UnitType = Units.COSTPERMASS, PreferredUnits = CostPerMassUnit.DOLLARSPERTONNE, CurrentUnits = CostPerMassUnit.DOLLARSPERTONNE) - self.LCOD_geo = self.OutputParameterDict[self.LCOD_geo.Name] = OutputParameter(Name = "Total LCOD S-DAC-GT", value = 0.0, UnitType = Units.COSTPERMASS, PreferredUnits = CostPerMassUnit.DOLLARSPERTONNE, CurrentUnits = CostPerMassUnit.DOLLARSPERTONNE) - self.CO2total_elec = self.OutputParameterDict[self.CO2total_elec.Name] = OutputParameter(Name = "Total CO2 Intensity 100% electric", value = 0.0, UnitType = Units.CO2PRODUCTION, PreferredUnits = CO2ProductionUnit.TONNEPERMWH, CurrentUnits = CO2ProductionUnit.TONNEPERMWH) - self.CO2total_ng = self.OutputParameterDict[self.CO2total_ng.Name] = OutputParameter(Name = "Total CO2 Intensity natural gas", value = 0.0, UnitType = Units.CO2PRODUCTION, PreferredUnits = CO2ProductionUnit.TONNEPERMWH, CurrentUnits = CO2ProductionUnit.TONNEPERMWH) - self.CO2total_geo = self.OutputParameterDict[self.CO2total_geo.Name] = OutputParameter(Name = "Total CO2 Intensity S-DAC-GT", value = 0.0, UnitType = Units.CO2PRODUCTION, PreferredUnits = CO2ProductionUnit.TONNEPERMWH, CurrentUnits = CO2ProductionUnit.TONNEPERMWH) - self.LCOH = self.OutputParameterDict[self.LCOH.Name] = OutputParameter(Name = "LCOH", value = 0.0, UnitType = Units.ENERGYCOST, PreferredUnits = EnergyCostUnit.DOLLARSPERKWH, CurrentUnits = EnergyCostUnit.DOLLARSPERKWH) - self.kWh_e_per_kWh_th = self.OutputParameterDict[self.kWh_e_per_kWh_th.Name] = OutputParameter(Name = "Energy Use Ratio heat vs electrcity", value = 0.0, UnitType = Units.PERCENT, PreferredUnits = PercentUnit.TENTH, CurrentUnits = PercentUnit.TENTH) - - self.tot_heat_energy_consumed_per_tonne = self.OutputParameterDict[self.tot_heat_energy_consumed_per_tonne.Name] = OutputParameter(Name = "Total Heat Energy Used to extract carbon", value = 0.0, UnitType = Units.ENERGYPERCO2, PreferredUnits = EnergyPerCO2Unit.KWTHPERTONNE, CurrentUnits = EnergyPerCO2Unit.KWTHPERTONNE) - self.tot_cost_per_tonne = self.OutputParameterDict[self.tot_cost_per_tonne.Name] = OutputParameter(Name = "Total Cost per Tonne of CO2 Captured", value = 0.0, UnitType = Units.COSTPERMASS, PreferredUnits = CostPerMassUnit.DOLLARSPERTONNE, CurrentUnits = CostPerMassUnit.DOLLARSPERTONNE) - self.percent_thermal_energy_going_to_heat = self.OutputParameterDict[self.percent_thermal_energy_going_to_heat.Name] = OutputParameter(Name = "Percent of Total Energy Used as heat in processs", value = 0.0, UnitType = Units.PERCENT, PreferredUnits = PercentUnit.TENTH, CurrentUnits = PercentUnit.TENTH) - - self.S_DAC_GTAnnualCost = self.OutputParameterDict[self.S_DAC_GTAnnualCost.Name] = OutputParameter(Name = "Total Cost per Year", value = 0.0, UnitType = Units.CURRENCYFREQUENCY, PreferredUnits = CurrencyFrequencyUnit.DOLLARSPERYEAR, CurrentUnits = CurrencyFrequencyUnit.DOLLARSPERYEAR) - self.S_DAC_GTCummCashFlow = self.OutputParameterDict[self.S_DAC_GTCummCashFlow.Name] = OutputParameter(Name = "Running Total Cost", value = [0.0], UnitType = Units.CURRENCY, PreferredUnits = CurrencyUnit.DOLLARS, CurrentUnits = CurrencyUnit.DOLLARS) - self.CarbonExtractedAnnually = self.OutputParameterDict[self.CarbonExtractedAnnually.Name] = OutputParameter(Name = "Tonnes per Year CO2 extracted", value = [0.0], UnitType = Units.MASSPERTIME, PreferredUnits = MassPerTimeUnit.TONNEPERYEAR, CurrentUnits = MassPerTimeUnit.TONNEPERYEAR) - self.S_DAC_GTCummCarbonExtracted = self.OutputParameterDict[self.S_DAC_GTCummCarbonExtracted.Name] = OutputParameter(Name = "Running Carbon Capture", value = [0.0], UnitType = Units.MASS, PreferredUnits = MassUnit.TONNE, CurrentUnits = MassUnit.TONNE) - self.CarbonExtractedTotal = self.OutputParameterDict[self.CarbonExtractedTotal.Name] = OutputParameter(Name = "Total Tonnes of CO2 extracted", value = 0.0, UnitType = Units.MASS, PreferredUnits = MassUnit.TONNE, CurrentUnits = MassUnit.TONNE) - self.CummCostPerTonne = self.OutputParameterDict[self.CummCostPerTonne.Name] = OutputParameter(Name = "Running cost per Tonne of capture", value = [0.0], UnitType = Units.COSTPERMASS, PreferredUnits = CostPerMassUnit.DOLLARSPERTONNE, CurrentUnits = CostPerMassUnit.DOLLARSPERTONNE) - - model.logger.info("Complete "+ str(__class__) + ": " + sys._getframe().f_code.co_name) + # Results - used by other objects or printed in output downstream + self.LCOD_elec = self.OutputParameterDict[self.LCOD_elec.Name] = OutputParameter( + "Total LCOD 100% electric", + UnitType=Units.COSTPERMASS, + PreferredUnits=CostPerMassUnit.DOLLARSPERTONNE, + CurrentUnits=CostPerMassUnit.DOLLARSPERTONNE + ) + self.LCOD_ng = self.OutputParameterDict[self.LCOD_ng.Name] = OutputParameter( + Name="Total LCOD natural gas", + UnitType=Units.COSTPERMASS, + PreferredUnits=CostPerMassUnit.DOLLARSPERTONNE, + CurrentUnits=CostPerMassUnit.DOLLARSPERTONNE + ) + self.LCOD_geo = self.OutputParameterDict[self.LCOD_geo.Name] = OutputParameter( + Name="Total LCOD S-DAC-GT", + UnitType=Units.COSTPERMASS, + PreferredUnits=CostPerMassUnit.DOLLARSPERTONNE, + CurrentUnits=CostPerMassUnit.DOLLARSPERTONNE + ) + self.CO2total_elec = self.OutputParameterDict[self.CO2total_elec.Name] = OutputParameter( + Name="Total CO2 Intensity 100% electric", + UnitType=Units.CO2PRODUCTION, + PreferredUnits=CO2ProductionUnit.TONNEPERMWH, + CurrentUnits=CO2ProductionUnit.TONNEPERMWH + ) + self.CO2total_ng = self.OutputParameterDict[self.CO2total_ng.Name] = OutputParameter( + Name="Total CO2 Intensity natural gas", + UnitType=Units.CO2PRODUCTION, + PreferredUnits=CO2ProductionUnit.TONNEPERMWH, + CurrentUnits=CO2ProductionUnit.TONNEPERMWH + ) + self.CO2total_geo = self.OutputParameterDict[self.CO2total_geo.Name] = OutputParameter( + Name="Total CO2 Intensity S-DAC-GT", + UnitType=Units.CO2PRODUCTION, + PreferredUnits=CO2ProductionUnit.TONNEPERMWH, + CurrentUnits=CO2ProductionUnit.TONNEPERMWH + ) + self.LCOH = self.OutputParameterDict[self.LCOH.Name] = OutputParameter( + Name="LCOH", + UnitType=Units.ENERGYCOST, + PreferredUnits=EnergyCostUnit.DOLLARSPERKWH, + CurrentUnits=EnergyCostUnit.DOLLARSPERKWH + ) + self.kWh_e_per_kWh_th = self.OutputParameterDict[self.kWh_e_per_kWh_th.Name] = OutputParameter( + Name="Energy Use Ratio heat vs electricity", + UnitType=Units.PERCENT, + PreferredUnits=PercentUnit.TENTH, + CurrentUnits=PercentUnit.TENTH + ) + self.tot_heat_energy_consumed_per_tonne = self.OutputParameterDict[ + self.tot_heat_energy_consumed_per_tonne.Name] = OutputParameter( + Name="Total Heat Energy Used to extract carbon", + UnitType=Units.ENERGYPERCO2, + PreferredUnits=EnergyPerCO2Unit.KWTHPERTONNE, + CurrentUnits=EnergyPerCO2Unit.KWTHPERTONNE + ) + self.tot_cost_per_tonne = self.OutputParameterDict[self.tot_cost_per_tonne.Name] = OutputParameter( + Name="Total Cost per Tonne of CO2 Captured", + UnitType=Units.COSTPERMASS, + PreferredUnits=CostPerMassUnit.DOLLARSPERTONNE, + CurrentUnits=CostPerMassUnit.DOLLARSPERTONNE + ) + self.percent_thermal_energy_going_to_heat = self.OutputParameterDict[ + self.percent_thermal_energy_going_to_heat.Name] = OutputParameter( + Name="Percent of Total Energy Used as heat in processs", + UnitType=Units.PERCENT, + PreferredUnits=PercentUnit.TENTH, + CurrentUnits=PercentUnit.TENTH + ) + self.S_DAC_GTAnnualCost = self.OutputParameterDict[self.S_DAC_GTAnnualCost.Name] = OutputParameter( + Name="Total Cost per Year", + UnitType=Units.CURRENCYFREQUENCY, + PreferredUnits=CurrencyFrequencyUnit.DOLLARSPERYEAR, + CurrentUnits=CurrencyFrequencyUnit.DOLLARSPERYEAR + ) + self.S_DAC_GTCummCashFlow = self.OutputParameterDict[self.S_DAC_GTCummCashFlow.Name] = OutputParameter( + Name="Running Total Cost", + UnitType=Units.CURRENCY, + PreferredUnits=CurrencyUnit.DOLLARS, + CurrentUnits=CurrencyUnit.DOLLARS + ) + self.CarbonExtractedAnnually = self.OutputParameterDict[self.CarbonExtractedAnnually.Name] = OutputParameter( + Name="Tonnes per Year CO2 extracted", + UnitType=Units.MASSPERTIME, + PreferredUnits=MassPerTimeUnit.TONNEPERYEAR, + CurrentUnits=MassPerTimeUnit.TONNEPERYEAR + ) + self.S_DAC_GTCummCarbonExtracted = self.OutputParameterDict[ + self.S_DAC_GTCummCarbonExtracted.Name] = OutputParameter( + Name="Running Carbon Capture", + UnitType=Units.MASS, + PreferredUnits=MassUnit.TONNE, + CurrentUnits=MassUnit.TONNE + ) + self.CarbonExtractedTotal = self.OutputParameterDict[self.CarbonExtractedTotal.Name] = OutputParameter( + Name="Total Tonnes of CO2 extracted", + UnitType=Units.MASS, + PreferredUnits=MassUnit.TONNE, + CurrentUnits=MassUnit.TONNE + ) + self.CummCostPerTonne = self.OutputParameterDict[self.CummCostPerTonne.Name] = OutputParameter( + Name="Running cost per Tonne of capture", + UnitType=Units.COSTPERMASS, + PreferredUnits=CostPerMassUnit.DOLLARSPERTONNE, + CurrentUnits=CostPerMassUnit.DOLLARSPERTONNE + ) + + model.logger.info("Complete " + str(__class__) + ": " + sys._getframe().f_code.co_name) def __str__(self): return "EconomicsS_DAC_GT" - def read_parameters(self, model:Model) -> None: + def read_parameters(self, model: Model) -> None: """ - The read_parameters function reads in the parameters from a dictionary and stores them in the aparmeters. It also handles special cases that need to be handled after a value has been read in and checked. If you choose to sublass this master class, you can also choose to override this method (or not), and if you do - - :param self: Access variables that belong to a class + 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 - :doc-author: Malcolm Ross """ model.logger.info("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 defaulr value set in __init__. It will ignore those. - #This also deals with all the special cases that need to be talen care of after a vlaue has been read in and checked. - #If you choose to sublass 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. + # 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 vlaue 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 thru all the parameters that the user wishes to set, looking for parameters that match this object + # loop thru 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] - ParameterToModify.CurrentUnits = ParameterToModify.PreferredUnits #Before we change the paremeter, let's assume that the unit preferences will match - if they don't, the later code will fix this. - ReadParameter(ParameterReadIn, ParameterToModify, model) #this should handle all the non-special cases - - #handle special cases - #none in this case so far + # Before we change the paremater, let's assume that the unit preferences will match - + # if they don't, the later code will fix this. + ParameterToModify.CurrentUnits = ParameterToModify.PreferredUnits + # this should handle all the non-special cases + ReadParameter(ParameterReadIn, ParameterToModify, model) + + # handle special cases + # none in this case so far else: model.logger.info("No parameters read becuase no content provided") - model.logger.info("read parameters complete "+ str(__class__) + ": " + sys._getframe().f_code.co_name) + model.logger.info("read parameters complete " + str(__class__) + ": " + sys._getframe().f_code.co_name) + + def calculate_CRF(self, wacc: float, num_years: float) -> float: + """ + Calculate the Capital Recovery Factor (CRF) or Fixed Charge Factor (FCF) + :param wacc: Weighted Average Cost of Capital (percent) - default 10% or 0.1 in this model + :type wacc: float + :param num_years: Number of years of project duration - default 20 years in this model (2022-2042) + - 20 years is the minimum for this model to work properly - if you change it, you will need to change the + default values for CAPEX and OPEX to match the new duration - see the paper for more details on this model and + the assumptions made in it - the paper is available at https://www.onepetro.org/conference-paper/SPE-215735-MS - + the paper is also available in the docs folder of this project + :type num_years: float + :return: CRF + :rtype: float + """ - def calculate_CRF(self, wacc:float, num_years:float)->float: - # CRF calculator. Also Fixed Charge Factor - FCF - # Default set to 11.75%, or calculated value for project duration of 20 years with WACC of 10% - wacc = wacc/100.0 - CRF = (wacc*(1+wacc)**num_years)/((1+wacc)**num_years-1) + # Default set to 11.75%, or calculated value for project duration of 20 years with WACC of 10% + wacc = wacc / 100.0 + CRF = (wacc * (1 + wacc) ** num_years) / ((1 + wacc) ** num_years - 1) return CRF - def range_check(self)->tuple: + def range_check(self) -> tuple: + """ + Check that all the values are within the allowable range + :return: tuple of (True/False, Error Message) + :rtype: tuple + """ wacc_min = 0.1 wacc_max = 30 CAPEX_min = 100 @@ -159,81 +439,108 @@ def range_check(self)->tuple: if not (wacc_min <= self.wacc.value <= wacc_max): error_message = "S-DAC-GT ERROR: WACC should be between {}% and {}%".format(wacc_min, wacc_max) - return(True, error_message) + return True, error_message if not (CAPEX_min <= self.CAPEX.value <= CAPEX_max): error_message = "S-DAC-GT ERROR: CAPEX should be between {} and {}".format(CAPEX_min, CAPEX_max) - return(True, error_message) + return True, error_message if not (OPEX_min <= self.OPEX.value <= OPEX_max): error_message = "S-DAC-GT ERROR: OPEX should be between {} and {}".format(OPEX_min, OPEX_max) - return(True, error_message) + return True, error_message if not (elec_min <= self.elec.value <= elec_max): error_message = "S-DAC-GT ERROR: Electrical Energy should be between {} and {}".format(elec_min, elec_max) - return(True, error_message) + return True, error_message if not (therm_min <= self.therm.value <= therm_max): error_message = "S-DAC-GT ERROR: Thermal Energy should be between {} and {}".format(therm_min, therm_max) - return(True, error_message) + return True, error_message if not (NG_price_min <= self.NG_price.value <= NG_price_max): - error_message = "S-DAC-GT ERROR: Natural Gas Price should be between {} and {}".format(NG_price_min, NG_price_max) - return(True, error_message) + error_message = "S-DAC-GT ERROR: Natural Gas Price should be between {} and {}".format(NG_price_min, + NG_price_max) + return True, error_message if not (power_co2intensity_min <= self.power_co2intensity.value <= power_co2intensity_max): - error_message = "S-DAC-GT ERROR: CO2 Intensity of Electricity should be between {} and {}".format(power_co2intensity_min, power_co2intensity_max) - return(True, error_message) + error_message = "S-DAC-GT ERROR: CO2 Intensity of Electricity should be between {} and {}".format( + power_co2intensity_min, power_co2intensity_max) + return True, error_message if not (CAPEX_mult_min <= self.CAPEX_mult.value <= CAPEX_mult_max): - error_message = "S-DAC-GT ERROR: CAPEX Multiplier should be between {} and {}".format(CAPEX_mult_min, CAPEX_mult_max) - return(True, error_message) + error_message = "S-DAC-GT ERROR: CAPEX Multiplier should be between {} and {}".format(CAPEX_mult_min, + CAPEX_mult_max) + return True, error_message if not (OPEX_mult_min <= self.OPEX_mult.value <= OPEX_mult_max): - error_message = "S-DAC-GT ERROR: OPEX Multiplier should be between {} and {}".format(OPEX_mult_min, OPEX_mult_max) - return(True, error_message) + error_message = "S-DAC-GT ERROR: OPEX Multiplier should be between {} and {}".format(OPEX_mult_min, + OPEX_mult_max) + return True, error_message if not (therm_index_min <= self.therm_index.value <= therm_index_max): - error_message = "S-DAC-GT ERROR: S-DAC Thermal Energy Multiplier should be between {} and {}".format(therm_index_min, therm_index_max) - return(True, error_message) + error_message = "S-DAC-GT ERROR: S-DAC Thermal Energy Multiplier should be between {} and {}".format( + therm_index_min, therm_index_max) + return True, error_message if not (transport_min <= self.transport.value <= transport_max): - error_message = "S-DAC-GT ERROR: CO2 Transportation Cost should be between {} and {}".format(transport_min, transport_max) - return(True, error_message) + error_message = "S-DAC-GT ERROR: CO2 Transportation Cost should be between {} and {}".format(transport_min, + transport_max) + return True, error_message if not (storage_min <= self.storage.value <= storage_max): - error_message = "S-DAC-GT ERROR: CO2 Storage Cost should be between {} and {}".format(storage_min, storage_max) - return(True, error_message) - - return(False,"") - - def geo_therm_cost(self, power_cost:float, CAPEX_mult:float, OPEX_mult:float, depth:float, Production_temp:float, Injection_temp:float, Flow_rate:float)->tuple: - # Calculate Levelized cost of heat and ratio of electric power to heat power - # LCOH calculated in USD - # Power ratio calculated as kWh_e / kWh_th --> used for calculating CO2 footprint of geothermal energy - # inputs are cost of electricity, regional capex and opex multipliers, - # depth of geothermal reservoir, Average Production Temperature, Injection Temperature, and Flow Rate - #recoded by Malcolm Ross when integrated with GEOPHIRES - GEOPHIRES has more information, so fewer assumptions are made + error_message = "S-DAC-GT ERROR: CO2 Storage Cost should be between {} and {}".format(storage_min, + storage_max) + return True, error_message + + return False, "" + + def geo_therm_cost(self, power_cost: float, CAPEX_mult: float, OPEX_mult: float, depth: float, + Production_temp: float, Injection_temp: float, Flow_rate: float) -> tuple: + """ + Calculate Levelized cost of heat and ratio of electric power to heat power + LCOH calculated in USD + Power ratio calculated as kWh_e / kWh_th --> used for calculating CO2 footprint of geothermal energy + inputs are cost of electricity, regional capex and opex multipliers, + depth of geothermal reservoir, Average Production Temperature, Injection Temperature, and Flow Rate + recoded by Malcolm Ross when integrated with GEOPHIRES - GEOPHIRES has more information, + so fewer assumptions are made + :param power_cost: Cost of electricity in USD per kWh - default 0.05 USD per kWh in this model (5 cents per kWh) - this is the default value for the US in 2022 + :type power_cost: float + :param CAPEX_mult: Regional CAPEX multiplier - default 1.0 in this model - this is the default value for the US in 2022 + :type CAPEX_mult: float + :param OPEX_mult: Regional OPEX multiplier - default 1.0 in this model - this is the default value for the US in 2022 + :type OPEX_mult: float + :param depth: Depth of geothermal reservoir in feet - default 4101 feet in this model - this is the default value for the US in 2022 + :type depth: float + :param Production_temp: Average Production Temperature in Celsius - default 150 degrees C in this model - this is the default value for the US in 2022 + :type Production_temp: float + :param Injection_temp: Injection Temperature in Celsius - default 50 degrees C in this model - this is the default value for the US in 2022 + :type Injection_temp: float + :param Flow_rate: Flow Rate in kg/s - default 50 kg/s in this model - this is the default value for the US in 2022 + :type Flow_rate: float + :return: tuple of (LCOH, kWh_e_per_kWh_th) - LCOH in USD per kWh_therm, kWh_e_per_kWh_th is ratio of kWh_e to kWh_th (kWh_e / kWh_th) + :rtype: tuple + """ # Update NREL 2016 model for 2022 # Inflation 2017 thru 2022 1H Inflation = 1.189 # 2016 - Sep 2022 - EIA drilling prod report Drilling_efficiency_factor = 1.61 # Thermal capacity of water - H2O_thermal_capacity = 0.001163 # kWh/kg C + H2O_thermal_capacity = 0.001163 # kWh/kg C # Plant capacity factor Capacity_factor = 0.9 # NREL 2016 Model for new well adjusted for inflation - NREL_depth = 4101 # feet - NREL_CAPEX = 3712500 * Inflation # USD, excludes drilling - NREL_CAPEX_drill = 2112500 * Inflation # USD - NREL_drill_per_foot = NREL_CAPEX_drill/NREL_depth # USD/foot - NREL_pumping = 1980215 # kWh - NREL_pump_per_foot = NREL_pumping/NREL_depth # kWh/foot - NREL_inhibitor = 50000 * Inflation # USD - NREL_labor = 100000 * Inflation # USD - NREL_reinjection = 127130 * Inflation # USD + NREL_depth = 4101 # feet + NREL_CAPEX = 3712500 * Inflation # USD, excludes drilling + NREL_CAPEX_drill = 2112500 * Inflation # USD + NREL_drill_per_foot = NREL_CAPEX_drill / NREL_depth # USD/foot + NREL_pumping = 1980215 # kWh + NREL_pump_per_foot = NREL_pumping / NREL_depth # kWh/foot + NREL_inhibitor = 50000 * Inflation # USD + NREL_labor = 100000 * Inflation # USD + NREL_reinjection = 127130 * Inflation # USD # Normalize for region CAPEX = NREL_CAPEX * CAPEX_mult @@ -249,69 +556,79 @@ def geo_therm_cost(self, power_cost:float, CAPEX_mult:float, OPEX_mult:float, de OPEX_total = pump_cost + inhibitor + labor + reinjection # total thermal energy generation - Thermal_capacity = (Production_temp-Injection_temp)*Flow_rate*H2O_thermal_capacity*60*60 # kW - Annual_op_hrs = 365*24*Capacity_factor # hours - Therm_total = Thermal_capacity * Annual_op_hrs # kWh + Thermal_capacity = (Production_temp - Injection_temp) * Flow_rate * H2O_thermal_capacity * 60 * 60 # kW + Annual_op_hrs = 365 * 24 * Capacity_factor # hours + Therm_total = Thermal_capacity * Annual_op_hrs # kWh # Levelized cost of heat (LCOH) - LCOH = (CAPEX_total*self.CRF + OPEX_total)/Therm_total # $/kWh_therm + LCOH = (CAPEX_total * self.CRF + OPEX_total) / Therm_total # $/kWh_therm kWh_e_per_kWh_th = pump_kwh / Therm_total - return (LCOH, kWh_e_per_kWh_th) + return LCOH, kWh_e_per_kWh_th - def Calculate(self, model:Model)->None: + 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 self: Access variables that belongs to the class :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 - :doc-author: Malcolm Ross """ model.logger.info("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 sublcass 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 sublass 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 n=betteer have set all the paremeters! + # 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! # Ensure parameters are within range. If not, exit function without completing calculation or generating charts err_state, err_message = self.range_check() - if (err_state): + if err_state: model.logger.fatal(err_message + " Exiting....") print(err_message + " Exiting....") sys.exit() - self.CRF = self.calculate_CRF(self.wacc.value, model.surfaceplant.plantlifetime.value) # Calculate initial CRF value based on default inputs - CAPEX = self.CAPEX.value * self.CRF #don't change a parameters value directly - it throw off the rehydration + self.CRF = self.calculate_CRF(self.wacc.value, + model.surfaceplant.plantlifetime.value) # Calculate initial CRF value based on default inputs + CAPEX = self.CAPEX.value * self.CRF # don't change a parameters value directly - it throw off the rehydration CAPEX = CAPEX * self.CAPEX_mult.value self.OPEX.value = self.OPEX.value * self.OPEX_mult.value self.therm.value = self.therm.value * self.therm_index.value power_totalcost = self.elec.value * model.surfaceplant.elecprice.value elec_heat_totalcost = self.therm.value * model.surfaceplant.elecprice.value - NG_price = self.NG_price.value / self.NG_EnergyDensity.value # Convert from $/McF to $/kWh_th, but don't change a parameters value directly - it throw off the rehydration + # Convert from $/McF to $/kWh_th, but don't change a parameters value directly - it will throw off the rehydration + NG_price = self.NG_price.value / self.NG_EnergyDensity.value NG_totalcost = self.therm.value * NG_price - self.LCOH.value, self.kWh_e_per_kWh_th.value = self.geo_therm_cost(model.surfaceplant.elecprice.value, self.CAPEX_mult.value, self.OPEX_mult.value, (model.reserv.depth.value * 3280.84), (np.average(model.wellbores.ProducedTemperature.value)), model.wellbores.Tinj.value, (model.wellbores.nprod.value*model.wellbores.prodwellflowrate.value)) - geothermal_totalcost = self.LCOH.value*self.therm.value - co2_power = self.elec.value/1000*self.power_co2intensity.value - co2_elec_heat = self.therm.value/1000*self.power_co2intensity.value - co2_ng = self.therm.value/1000*self.NG_co2intensity.value - co2_geothermal = self.therm.value*self.kWh_e_per_kWh_th.value/1000*self.power_co2intensity.value - - self.LCOD_elec.value = CAPEX+self.OPEX.value+power_totalcost+elec_heat_totalcost+self.storage.value+self.transport.value - self.LCOD_ng.value = CAPEX+self.OPEX.value+power_totalcost+NG_totalcost+self.storage.value+self.transport.value - self.LCOD_geo.value = CAPEX+self.OPEX.value+power_totalcost+geothermal_totalcost+self.storage.value+self.transport.value + self.LCOH.value, self.kWh_e_per_kWh_th.value = self.geo_therm_cost(model.surfaceplant.elecprice.value, + self.CAPEX_mult.value, self.OPEX_mult.value, + model.reserv.depth.value * 3280.84, + np.average(model.wellbores.ProducedTemperature.value), + model.wellbores.Tinj.value, + model.wellbores.nprod.value * model.wellbores.prodwellflowrate.value) + geothermal_totalcost = self.LCOH.value * self.therm.value + co2_power = self.elec.value / 1000 * self.power_co2intensity.value + co2_elec_heat = self.therm.value / 1000 * self.power_co2intensity.value + co2_ng = self.therm.value / 1000 * self.NG_co2intensity.value + co2_geothermal = self.therm.value * self.kWh_e_per_kWh_th.value / 1000 * self.power_co2intensity.value + + self.LCOD_elec.value = CAPEX + self.OPEX.value + power_totalcost + elec_heat_totalcost + self.storage.value + self.transport.value + self.LCOD_ng.value = CAPEX + self.OPEX.value + power_totalcost + NG_totalcost + self.storage.value + self.transport.value + self.LCOD_geo.value = CAPEX + self.OPEX.value + power_totalcost + geothermal_totalcost + self.storage.value + self.transport.value self.CO2total_elec.value = co2_power + co2_elec_heat self.CO2total_ng.value = co2_power + co2_ng self.CO2total_geo.value = co2_power + co2_geothermal - #calculate the net impact of S-DAC-GT on the annual production of the model + # calculate the net impact of S-DAC-GT on the annual production of the model avg_first_law_eff = np.average(model.surfaceplant.FirstLawEfficiency.value) - self.tot_heat_energy_consumed_per_tonne.value = (self.elec.value / avg_first_law_eff) + self.therm.value #kWh_th/tonne - self.tot_cost_per_tonne.value = CAPEX + self.OPEX.value + self.storage.value + self.transport.value #USD/tonne - self.percent_thermal_energy_going_to_heat.value = self.therm.value/self.tot_heat_energy_consumed_per_tonne.value + self.tot_heat_energy_consumed_per_tonne.value = (self.elec.value / avg_first_law_eff) + self.therm.value # kWh_th/tonne + self.tot_cost_per_tonne.value = CAPEX + self.OPEX.value + self.storage.value + self.transport.value # USD/tonne + self.percent_thermal_energy_going_to_heat.value = self.therm.value / self.tot_heat_energy_consumed_per_tonne.value self.S_DAC_GTAnnualCost.value = [0.0] * model.surfaceplant.plantlifetime.value self.S_DAC_GTCummCashFlow.value = [0.0] * model.surfaceplant.plantlifetime.value @@ -320,24 +637,39 @@ def Calculate(self, model:Model)->None: self.CummCostPerTonne.value = [0.0] * model.surfaceplant.plantlifetime.value self.CarbonExtractedTotal.value = 0.0 - #Figure out how much energy is being produced each year, and the amount of carbon that would have been produced if that energy had been made using the grdi average carbon production. That then gives us the revenue, since we have a carbon price model - #We can also get annual cash flow from it. - for i in range(0,model.surfaceplant.plantlifetime.value,1): + # Figure out how much energy is being produced each year, and the amount of carbon that + # would have been produced if that energy had been made using the grid average carbon production. + # That then gives us the revenue, since we have a carbon price model + # We can also get annual cash flow from it. + for i in range(0, model.surfaceplant.plantlifetime.value, 1): self.CarbonExtractedAnnually.value[i] = (self.EnergySplit.value * model.surfaceplant.HeatkWhExtracted.value[i]) / self.tot_heat_energy_consumed_per_tonne.value - if i == 0: self.S_DAC_GTCummCarbonExtracted.value[i] = self.CarbonExtractedAnnually.value[i] - else: self.S_DAC_GTCummCarbonExtracted.value[i] = self.S_DAC_GTCummCarbonExtracted.value[i-1] + self.CarbonExtractedAnnually.value[i] + if i == 0: + self.S_DAC_GTCummCarbonExtracted.value[i] = self.CarbonExtractedAnnually.value[i] + else: + self.S_DAC_GTCummCarbonExtracted.value[i] = self.S_DAC_GTCummCarbonExtracted.value[i - 1] + self.CarbonExtractedAnnually.value[i] self.CarbonExtractedTotal.value = self.CarbonExtractedTotal.value + self.CarbonExtractedAnnually.value[i] self.S_DAC_GTAnnualCost.value[i] = self.CarbonExtractedAnnually.value[i] * self.tot_cost_per_tonne.value - if i == 0: self.S_DAC_GTCummCashFlow.value[i] = self.S_DAC_GTAnnualCost.value[i] - else: self.S_DAC_GTCummCashFlow.value[i] = self.S_DAC_GTCummCashFlow.value[i-1] + self.S_DAC_GTAnnualCost.value[i] - self.CummCostPerTonne.value[i] = self.S_DAC_GTCummCashFlow.value[i]/self.S_DAC_GTCummCarbonExtracted.value[i] - - #We need to update the heat and electricity generated because we have consumed some (all) of it to do the capture, so when they get used in the final economic calculation (below), the new values reflect the impact of S-DAC-GT - for i in range(0,model.surfaceplant.plantlifetime.value): - if model.surfaceplant.enduseoption.value != EndUseOptions.HEAT: #all these end-use options have an electricity generation component - model.surfaceplant.TotalkWhProduced.value[i] = model.surfaceplant.TotalkWhProduced.value[i] - (self.CarbonExtractedAnnually.value[i] * self.elec.value) - model.surfaceplant.NetkWhProduced.value[i] = model.surfaceplant.NetkWhProduced.value[i] - (self.CarbonExtractedAnnually.value[i] * self.elec.value) - if model.surfaceplant.enduseoption.value != EndUseOptions.ELECTRICITY: model.surfaceplant.HeatkWhProduced.value[i] = model.surfaceplant.HeatkWhProduced.value[i] - (self.CarbonExtractedAnnually.value[i] * self.therm.value) - else: model.surfaceplant.HeatkWhProduced.value[i] = model.surfaceplant.HeatkWhProduced.value[i] - (self.CarbonExtractedAnnually.value[i] * self.therm.value) #all the end-use option of direct-use only component - - model.logger.info("complete "+ str(__class__) + ": " + sys._getframe().f_code.co_name) + if i == 0: + self.S_DAC_GTCummCashFlow.value[i] = self.S_DAC_GTAnnualCost.value[i] + else: + self.S_DAC_GTCummCashFlow.value[i] = self.S_DAC_GTCummCashFlow.value[i - 1] + self.S_DAC_GTAnnualCost.value[i] + self.CummCostPerTonne.value[i] = self.S_DAC_GTCummCashFlow.value[i] / self.S_DAC_GTCummCarbonExtracted.value[i] + + # We need to update the heat and electricity generated because we have consumed + # some (all) of it to do the capture, so when they get used in the final economic calculation (below), + # the new values reflect the impact of S-DAC-GT + for i in range(0, model.surfaceplant.plantlifetime.value): + if model.surfaceplant.enduseoption.value != EndUseOptions.HEAT: # all these end-use options have an electricity generation component + model.surfaceplant.TotalkWhProduced.value[i] = model.surfaceplant.TotalkWhProduced.value[i] - ( + self.CarbonExtractedAnnually.value[i] * self.elec.value) + model.surfaceplant.NetkWhProduced.value[i] = model.surfaceplant.NetkWhProduced.value[i] - ( + self.CarbonExtractedAnnually.value[i] * self.elec.value) + if model.surfaceplant.enduseoption.value != EndUseOptions.ELECTRICITY: + model.surfaceplant.HeatkWhProduced.value[i] = model.surfaceplant.HeatkWhProduced.value[i] - ( + self.CarbonExtractedAnnually.value[i] * self.therm.value) + else: + model.surfaceplant.HeatkWhProduced.value[i] = model.surfaceplant.HeatkWhProduced.value[i] - ( + self.CarbonExtractedAnnually.value[ + i] * self.therm.value) # all the end-use option of direct-use only component + + model.logger.info("complete " + str(__class__) + ": " + sys._getframe().f_code.co_name) diff --git a/src/geophires_x/GEOPHIRESv3.py b/src/geophires_x/GEOPHIRESv3.py index bed96b58..054ab608 100644 --- a/src/geophires_x/GEOPHIRESv3.py +++ b/src/geophires_x/GEOPHIRESv3.py @@ -10,6 +10,13 @@ def main(enable_geophires_logging_config=True): + """ + This is the main function for the GEOPHIRESv3 model. It is called when the user runs the model from the command + line. It is also called by the GUI when the user clicks the "Run Model" button. + :param enable_geophires_logging_config: If True, the logging.conf file will be used to configure logging. If False, + logging will be configured in the Model class. + :return: None + """ # set the starting directory to be the directory that this file is in os.chdir(os.path.dirname(os.path.abspath(__file__))) diff --git a/src/geophires_x/GeoPHIRESUtils.py b/src/geophires_x/GeoPHIRESUtils.py index 102efc84..fbb9ea51 100644 --- a/src/geophires_x/GeoPHIRESUtils.py +++ b/src/geophires_x/GeoPHIRESUtils.py @@ -6,6 +6,13 @@ def read_input_file(return_dict_1, logger=None): + """ + Read input file and return a dictionary of parameters + :param return_dict_1: dictionary of parameters + :param logger: logger object + :return: dictionary of parameters + :rtype: dict + """ logger.info(f'Init {__name__}') # Specify path of input file - it will always be the first command line argument. @@ -78,6 +85,13 @@ def read_input_file(return_dict_1, logger=None): class _EnhancedJSONEncoder(json.JSONEncoder): + """ + Enhanced JSON encoder that can handle dataclasses + :param json.JSONEncoder: JSON encoder + :return: JSON encoder + :rtype: json.JSONEncoder + """ + def default(self, o): if dataclasses.is_dataclass(o): return dataclasses.asdict(o) diff --git a/src/geophires_x/LHSReservoir.py b/src/geophires_x/LHSReservoir.py index 1538d220..5cdb1601 100644 --- a/src/geophires_x/LHSReservoir.py +++ b/src/geophires_x/LHSReservoir.py @@ -1,108 +1,125 @@ import sys -#import os +# import os import math -from functools import lru_cache import numpy as np from mpmath import * -#from OptionList import ReservoirModel, FractureShape, ReservoirVolume -from .Parameter import intParameter, floatParameter, strParameter, listParameter, OutputParameter, ReadParameter -from .Units import * import geophires_x.Model as Model from .Reservoir import Reservoir class LHSReservoir(Reservoir): """ - This class models the Linear Heat Sweep Reservoir. + This class models the Linear Heat Sweep Reservoir. It is a subclass of the Reservoir class. + It inherits all the methods and attributes of those classes, and can override them as necessary. + It also has its own methods and attributes that are unique to this class. """ - def __init__(self, model:Model): + + 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. - - :param self: Store data that will be used by the class + 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. :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 - :doc-author: Malcolm Ross """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) - super().__init__(model) # initialize the parent parameters and variables + super().__init__(model) # initialize the parent parameters and variables sclass = str(__class__).replace("","") - - #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. - + self.myClass = sclass.replace("\'>", "") model.logger.info("Complete " + str(__class__) + ": " + sys._getframe().f_code.co_name) def __str__(self): return "LHSReservoir" - def read_parameters(self, model:Model) -> None: + def read_parameters(self, model: Model) -> None: """ - The read_parameters function reads in the parameters from a dictionary created by reading the user-provided file and updates the parameter values for this object. - - The function reads in all of the parameters that relate to this object, including those that are inherited from other objects. It then updates any of these parameter values that have been changed by the user. It also handles any special cases. - - :param self: Reference the class instance (such as it is) from within the class + The read_parameters function reads in the parameters from a dictionary created by reading the user-provided file + and updates the parameter values for this object. + The function reads in all the parameters that relate to this object, including those that are inherited + from other objects. It then updates any of these parameter values that have been changed by the user. + It also handles any special cases. :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 - :doc-author: Malcolm Ross """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) - super().read_parameters(model) #read the paremeters for the parent. - #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 + + # 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 + super().read_parameters(model) # read the parameters for the parent. model.logger.info("Complete " + str(__class__) + ": " + sys._getframe().f_code.co_name) - def Calculate(self, model:Model): + def Calculate(self, model: Model): + """ + The Calculate function calculates the values of all the parameters that are calculated by this object. + It calls the Calculate function of the parent object to calculate the values of the parameters that are + calculated by the parent object. + It then calculates the values of the parameters that are calculated by this object. + :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("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) - super().Calculate(model) #run calculate for the parent. + super().Calculate(model) # run calculate for the parent. # specify rock properties - phi = model.reserv.porrock.value # porosity [%] - h = 500. # heat transfer coefficient [W/m^2 K] - shape = 0.2 # ratio of conduction path length - alpha = model.reserv.krock.value/(model.reserv.rhorock.value*model.reserv.cprock.value) + phi = model.reserv.porrock.value # porosity [%] + h = 500. # heat transfer coefficient [W/m^2 K] + shape = 0.2 # ratio of conduction path length + alpha = model.reserv.krock.value / (model.reserv.rhorock.value * model.reserv.cprock.value) # storage ratio - gamma = (model.reserv.rhowater.value*model.reserv.cpwater.value*phi)/(model.reserv.rhorock.value*model.reserv.cprock.value*(1-phi)) + gamma = (model.reserv.rhowater.value * model.reserv.cpwater.value * phi) / ( + model.reserv.rhorock.value * model.reserv.cprock.value * (1 - phi)) # effective rock radius - r_efr = 0.83*(0.75*(model.reserv.fracsepcalc.value*model.reserv.fracheightcalc.value*model.reserv.fracwidthcalc.value)/math.pi)**(1./3.) + r_efr = 0.83 * (0.75 * (model.reserv.fracsepcalc.value * model.reserv.fracheightcalc.value * model.reserv.fracwidthcalc.value) / math.pi) ** (1. / 3.) # Biot number - Bi = h*r_efr/model.reserv.krock.value + Bi = h * r_efr / model.reserv.krock.value # effective rock time constant - tau_efr = r_efr**2.*(shape + 1./Bi)/(3.*alpha) + tau_efr = r_efr ** 2. * (shape + 1. / Bi) / (3. * alpha) # reservoir dimensions and flow properties - hl = (model.reserv.fracnumbcalc.value-1)*model.reserv.fracsepcalc.value + hl = (model.reserv.fracnumbcalc.value - 1) * model.reserv.fracsepcalc.value wl = model.reserv.fracwidthcalc.value - aave = hl*wl - u0 = model.wellbores.nprod.value*model.wellbores.prodwellflowrate.value/(model.reserv.rhowater.value*aave) - tres = (model.reserv.fracheightcalc.value*phi)/u0 + aave = hl * wl + u0 = model.wellbores.nprod.value * model.wellbores.prodwellflowrate.value / (model.reserv.rhowater.value * aave) + tres = (model.reserv.fracheightcalc.value * phi) / u0 # number of heat transfer units - ntu = tres/tau_efr + ntu = tres / tau_efr # specify Laplace-space function - fp = lambda s: (1/s)*(1-exp(-(1+ntu/(gamma*(s+ntu)))*s)) + fp = lambda s: (1 / s) * (1 - exp(-(1 + ntu / (gamma * (s + ntu))) * s)) # calculate non-dimensional temperature array Twnd = [] try: - for t in range(1,len(model.reserv.timevector.value)): - Twnd = Twnd + [float(invertlaplace(fp, model.reserv.timevector.value[t]*365.*24.*3600./tres, method='talbot'))] + for t in range(1, len(model.reserv.timevector.value)): + Twnd = Twnd + [float( + invertlaplace(fp, model.reserv.timevector.value[t] * 365. * 24. * 3600. / tres, method='talbot'))] except: - print("Error: GEOPHIRES could not execute numerical inverse laplace calculation for reservoir model 2. Simulation will abort.") + print( + "Error: GEOPHIRES could not execute numerical inverse laplace calculation for reservoir model 2. Simulation will abort.") sys.exit() Twnd = np.asarray(Twnd) - # calculate dimensional temperature, add error-handling for non-sensical temperatures - model.reserv.Tresoutput.value = Twnd*(model.reserv.Trock.value-model.wellbores.Tinj.value) + model.wellbores.Tinj.value + # calculate dimensional temperature, add error-handling for nonsensical temperatures + model.reserv.Tresoutput.value = Twnd * ( + model.reserv.Trock.value - model.wellbores.Tinj.value) + model.wellbores.Tinj.value model.reserv.Tresoutput.value = np.append([model.reserv.Trock.value], model.reserv.Tresoutput.value) - model.reserv.Tresoutput.value = np.asarray([model.reserv.Trock.value if x>model.reserv.Trock.value or x model.reserv.Trock.value or x < model.wellbores.Tinj.value else x for x in model.reserv.Tresoutput.value]) model.logger.info("Complete " + str(__class__) + ": " + sys._getframe().f_code.co_name) diff --git a/src/geophires_x/MC_GeoPHIRES3.py b/src/geophires_x/MC_GeoPHIRES3.py index 3b22d99d..7f8a4a8c 100644 --- a/src/geophires_x/MC_GeoPHIRES3.py +++ b/src/geophires_x/MC_GeoPHIRES3.py @@ -3,12 +3,9 @@ """ Framework for running Monte Carlo simulations using GEOPHIRES v3.0 & HIP-RA 1.0 build date: September 2023 - Created on Wed November 16 10:43:04 2017 - @author: Malcolm Ross V3 """ -# TODO Use this video to update this function https://www.youtube.com/watch?v=fKl2JW_qrso import os import sys @@ -27,11 +24,12 @@ def CheckAndReplaceMean(input_value, args) -> list: """ CheckAndReplaceMean - check to see if the user has requested that a value be replaced by a mean value by specifying a value as "#" - Args: - input_value: the value to check - args: the list of arguments passed in from the command line - - Returns: a list of values that have been checked and replaced if necessary + :param input_value: the value to check + :type input_value: list + :param args: the list of arguments passed in from the command line + :type args: list + :return: the input_value, with the mean value replaced if necessary + :rtype: list """ i = 0 for inputx in input_value: @@ -52,6 +50,12 @@ def CheckAndReplaceMean(input_value, args) -> list: def WorkPackage(pass_list): + """ + WorkPackage - this is the function that is called by the executor. It does the work of running the simulation + :param pass_list: the list of arguments passed in from the command line + :type pass_list: list + :return: None + """ Inputs = pass_list[0] Outputs = pass_list[1] args = pass_list[2] @@ -60,26 +64,26 @@ def WorkPackage(pass_list): PythonPath = pass_list[5] tmpoutputfile = tmpfilename = "" -# get random values for each of the INPUTS based on the distributions and boundary values + # get random values for each of the INPUTS based on the distributions and boundary values rando = 0.0 s = "" print("#", end="") for input_value in Inputs: if input_value[1].strip().startswith('normal'): rando = np.random.normal(float(input_value[2]), float(input_value[3])) - s = s + input_value[0] + ", " + str(rando) + os.linesep + s = s + input_value[0] + ", " + str(rando) + "\n" elif input_value[1].strip().startswith('uniform'): rando = np.random.uniform(float(input_value[2]), float(input_value[3])) - s = s + input_value[0] + ", " + str(rando) + os.linesep + s = s + input_value[0] + ", " + str(rando) + "\n" elif input_value[1].strip().startswith('triangular'): rando = np.random.triangular(float(input_value[2]), float(input_value[3]), float(input_value[4])) - s = s + input_value[0] + ", " + str(rando) + os.linesep + s = s + input_value[0] + ", " + str(rando) + "\n" if input_value[1].strip().startswith('lognormal'): rando = np.random.lognormal(float(input_value[2]), float(input_value[3])) - s = s + input_value[0] + ", " + str(rando) + os.linesep + s = s + input_value[0] + ", " + str(rando) + "\n" if input_value[1].strip().startswith('binomial'): rando = np.random.binomial(int(input_value[2]), float(input_value[3])) - s = s + input_value[0] + ", " + str(rando) + os.linesep + s = s + input_value[0] + ", " + str(rando) + "\n" # make up a temporary file name that will be shared among files for this iteration tmpfilename = working_dir + str(uuid.uuid4()) + ".txt" @@ -90,6 +94,7 @@ def WorkPackage(pass_list): # append those values to the new input file in the format "variable name, new_random_value". # This will cause GeoPHIRES/HIP-RA to replace the value in the file with this random value in the calculation + # if it exists in that fiole already, or it will set it to the value as if it was a new value set by the user. with open(tmpfilename, "a") as f: f.write(s) @@ -108,12 +113,12 @@ def WorkPackage(pass_list): # make sure a key file exists. If not, exit if not os.path.exists(tmpoutputfile): print("Timed out waiting for: " + tmpoutputfile) -# logger.warning("Timed out waiting for: " + tmpoutputfile) + # logger.warning("Timed out waiting for: " + tmpoutputfile) exit(-33) with open(tmpoutputfile, "r") as f: - s1=f.readline() - i=0 + s1 = f.readline() + i = 0 while s1: # read until the end of the file for out in localOutputs: # check for each requested output if out in s1: # If true, we found the output value that the user requested, so process it @@ -133,21 +138,49 @@ def WorkPackage(pass_list): # append the input values to the output values so the optimal input values are easy to find, # the form "inputVar:Rando;nextInputVar:Rando..." - result_s = result_s + "(" + s.replace(os.linesep, ";", -1).replace(", ", ":", -1) + ")" + result_s = result_s + "(" + s.replace("\n", ";", -1).replace(", ", ":", -1) + ")" - # delete temporary files + # delete temporary files os.remove(tmpfilename) os.remove(tmpoutputfile) # write out the results result_s = result_s.strip(" ") # get rid of last space result_s = result_s.strip(",") # et rid of last comma - result_s = result_s + os.linesep + result_s = result_s + "\n" with open(Outputfile, "a") as f: f.write(result_s) def main(enable_geophires_logging_config=True): + """ + main - this is the main function that is called when the program is run + It gets most of its key values from the command line: + 0) Code_File: Python code to run + 1) Input_file: The base model for the calculations + 2) MC_Settings_file: The settings file for the MC run: + a) the input variables to change (spelling and case are IMPORTANT), their distribution functions + (choices = normal, uniform, triangular, lognormal, binomial - see numpy.random for documentation), + and the inputs for that distribution function (Comma separated; If the mean is set to "#", + then value from the Input_file as the mode/mean). In the form: + INPUT, Maximum Temperature, normal, mean, std_dev + INPUT, Utilization Factor,uniform, min, max + INPUT, Ambient Temperature,triangular, left, mode, right + b) the output variable(s) to track (spelling and case are IMPORTANT), in the form + [NOTE: THIS LIST SHOULD BE IN THE ORDER THEY APPEAR IN THE OUTPUT FILE]: + OUTPUT, Average Net Electricity Production + OUTPUT, Electricity breakeven price + c) the number of iterations, in the form: + ITERATIONS, 1000 + d) the name of the output file (it will contain one column for each of the output variables to track), + in the form: + MC_OUTPUT_FILE, "D:\Work\GEOPHIRES3-master\MC_Result.txt" + d) the path to the python executable, it it is not already linked to "python", in the form: + PYTHON_PATH, /user/local/bin/python3 + :param enable_geophires_logging_config: if True, use the logging.conf file to configure logging + :type enable_geophires_logging_config: bool + :return: None + """ # set the starting directory to be the directory that this file is in os.chdir(os.path.dirname(os.path.abspath(__file__))) # set up logging. @@ -164,29 +197,6 @@ def main(enable_geophires_logging_config=True): os.chdir(working_dir) working_dir = working_dir + os.sep - # from the command line, read what we need to know: - # 0) Code_File: Python code to run - # 1) Input_file: The base model for the calculations - # 2) MC_Settings_file: The settings file for the MC run: - # a) the input variables to change (spelling and case are IMPORTANT), their distribition functions - # (choices = normal, uniform, triangular, lognormal, binomial - see numpy.random for documenation), - # and the inputs for that distribution function (Comma seperated; If the mean is set to "#", - # then value from the Input_file as the mode/mean). In the form: - # INPUT, Maximum Temperature, normal, mean, std_dev - # INPUT, Utilization Factor,uniform, min, max - # INPUT, Ambient Temperature,triangular, left, mode, right - # b) the output variable(s) to track (spelling and case are IMPORTANT), in the form - # [NOTE: THIS LIST SHOULD BE IN THE ORDER THEY APPEAR IN THE OUTPUT FILE]: - # OUTPUT, Average Net Electricity Production - # OUTPUT, Electricity breakeven price - # c) the number of iterations, in the form: - # ITERATIONS, 1000 - # d) the name of the output file (it will contain one column for each of the output variables to track), - # in the form: - # MC_OUTPUT_FILE, "D:\Work\GEOPHIRES3-master\MC_Result.txt" - # d) the path to the python executable, it it is not already linked to "python", in the form: - # PYTHON_PATH, /user/local/bin/python3 - # get the values off the command line parser = argparse.ArgumentParser() parser.add_argument("Code_File", help="Code_File") @@ -237,7 +247,7 @@ def main(enable_geophires_logging_config=True): s = s + output + ", " s = "".join(s.rsplit(" ", 1)) # get rid of last space s = "".join(s.rsplit(",", 1)) # get rid of last comma - s = s + os.linesep + s = s + "\n" # write the header so it is easy to import and analyze in Excel with open(Outputfile, "w") as f: @@ -255,7 +265,7 @@ def main(enable_geophires_logging_config=True): with concurrent.futures.ProcessPoolExecutor() as executor: executor.map(WorkPackage, args) - print(os.linesep + "Done with calculations! Summarizing..." + os.linesep) + print("\n" + "Done with calculations! Summarizing..." + "\n") logger.info("Done with calculations! Summarizing...") # read the results into an array @@ -287,28 +297,26 @@ def main(enable_geophires_logging_config=True): # write them out with open(Outputfile, "a") as f: - i=0 + i = 0 if Iterations != actual_records_count: - f.write(os.linesep + os.linesep + str(actual_records_count) + " iterations finished successfully and were used to calculate the statistics" + os.linesep + os.linesep) + f.write("\n\n" + str(actual_records_count) + " iterations finished successfully and were used to calculate the statistics\n\n") for output in Outputs: - f.write(output + ":" + os.linesep) - f.write(f" minimum: {mins[i]:,.2f}" + os.linesep) - f.write(f" maximum: {maxs[i]:,.2f}" + os.linesep) - f.write(f" median: {medians[i]:,.2f}" + os.linesep) - f.write(f" average: {averages[i]:,.2f}" + os.linesep) - f.write(f" mean: {means[i]:,.2f}" + os.linesep) - f.write(f" standard deviation: {std[i]:,.2f}" + os.linesep) + f.write(output + ":" + "\n") + f.write(f" minimum: {mins[i]:,.2f}\n") + f.write(f" maximum: {maxs[i]:,.2f}\n") + f.write(f" median: {medians[i]:,.2f}\n") + f.write(f" average: {averages[i]:,.2f}\n") + f.write(f" mean: {means[i]:,.2f}\n") + f.write(f" standard deviation: {std[i]:,.2f}\n") i = i + 1 - print (" Calculation Time: "+"{0:10.3f}".format((time.time()-tic)) + " sec" + os.linesep) - logger.info(" Calculation Time: "+"{0:10.3f}".format((time.time()-tic)) + " sec" + os.linesep) - print (" Calculation Time per iteration: "+"{0:10.3f}".format(((time.time()-tic))/actual_records_count) +" sec" + os.linesep) - logger.info(" Calculation Time per iteration: "+"{0:10.3f}".format(((time.time()-tic))/actual_records_count) +" sec" + os.linesep) + print(" Calculation Time: " + "{0:10.3f}".format((time.time() - tic)) + " sec\n") + logger.info(" Calculation Time: " + "{0:10.3f}".format((time.time() - tic)) + " sec\n") + print(" Calculation Time per iteration: " + "{0:10.3f}".format(((time.time() - tic)) / actual_records_count) + " sec\n") + logger.info(" Calculation Time per iteration: " + "{0:10.3f}".format(((time.time() - tic)) / actual_records_count) + " sec\n") if Iterations != actual_records_count: - print(os.linesep + os.linesep + "NOTE:" + str(actual_records_count) + - " iterations finished successfully and were used to calculate the statistics." + os.linesep + os.linesep) - logger.warning(os.linesep + os.linesep + "NOTE:" + str(actual_records_count) + - " iterations finished successfully and were used to calculate the statistics." + os.linesep + os.linesep) + print("\n\nNOTE:" + str(actual_records_count) + " iterations finished successfully and were used to calculate the statistics.\n\n") + logger.warning("\n\nNOTE:" + str(actual_records_count) + " iterations finished successfully and were used to calculate the statistics.\n\n") logger.info("Complete " + str(__name__) + ": " + sys._getframe().f_code.co_name) @@ -318,5 +326,4 @@ def main(enable_geophires_logging_config=True): logger = logging.getLogger('root') logger.info("Init " + str(__name__)) - main() diff --git a/src/geophires_x/MPFReservoir.py b/src/geophires_x/MPFReservoir.py index 1bad2bee..72fdde64 100644 --- a/src/geophires_x/MPFReservoir.py +++ b/src/geophires_x/MPFReservoir.py @@ -7,63 +7,82 @@ class MPFReservoir(Reservoir): """ - This class models the Multiple Parallel Fractures Reservoir. + This class models the Multiple Parallel Fractures Reservoir. It is a subclass of the Reservoir class. + It inherits all the methods and attributes of that class, and can override them as necessary. + It also has its own methods and attributes that are unique to this class. """ - def __init__(self, model:Model): + + 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. - - :param self: Store data that will be used by the class + It initializes the attributes of an object, and sets default values for certain arguments + that can be overridden by user input. + 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. :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 - :doc-author: Malcolm Ross """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) - super().__init__(model) # initialize the parent parameters and variables + super().__init__(model) # initialize the parent parameters and variables sclass = str(__class__).replace("","") - - #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. - + self.MyClass = sclass.replace("\'>", "") model.logger.info("Complete " + str(__class__) + ": " + sys._getframe().f_code.co_name) def __str__(self): return "MPFReservoir" - def read_parameters(self, model:Model) -> None: + def read_parameters(self, model: Model) -> None: """ - The read_parameters function reads in the parameters from a dictionary created by reading the user-provided file and updates the parameter values for this object. - - The function reads in all of the parameters that relate to this object, including those that are inherited from other objects. It then updates any of these parameter values that have been changed by the user. It also handles any special cases. - - :param self: Reference the class instance (such as it is) from within the class + The read_parameters function reads in the parameters from a dictionary created by reading the user-provided file + and updates the parameter values for this object. + The function reads in all parameters that relate to this object, including those that are inherited from other + objects. It then updates any of these parameter values that have been changed by the user. + It also handles any special cases. :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 - :doc-author: Malcolm Ross """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) - super().read_parameters(model) #read the paremeters for the parent. - #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 + # 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 + super().read_parameters(model) # read the parameters for the parent. model.logger.info("Complete " + str(__class__) + ": " + sys._getframe().f_code.co_name) - def Calculate(self, model:Model): + def Calculate(self, model: Model): + """ + The Calculate function calculates the values of all the parameters that are calculated by this object. + :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("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) - super().Calculate(model) #run calculate for the parent. + super().Calculate(model) # run calculate for the parent. # convert flowrate to volumetric rate - q = model.wellbores.nprod.value*model.wellbores.prodwellflowrate.value/model.reserv.rhowater.value # m^3/s + q = model.wellbores.nprod.value * model.wellbores.prodwellflowrate.value / model.reserv.rhowater.value # m^3/s # specify Laplace-space function - fp = lambda s: (1./s)*exp(-sqrt(s)*tanh((model.reserv.rhowater.value*model.reserv.cpwater.value*(q/model.reserv.fracnumbcalc.value/model.reserv.fracwidthcalc.value)*(model.reserv.fracsepcalc.value/2.)/(2.*model.reserv.krock.value*model.reserv.fracheightcalc.value))*sqrt(s))) + fp = lambda s: (1. / s) * exp(-sqrt(s) * tanh((model.reserv.rhowater.value * model.reserv.cpwater.value * ( + q / model.reserv.fracnumbcalc.value / model.reserv.fracwidthcalc.value) * ( + model.reserv.fracsepcalc.value / 2.) / ( + 2. * model.reserv.krock.value * model.reserv.fracheightcalc.value)) * sqrt(s))) - #calculate non-dimensional time - td = (model.reserv.rhowater.value*model.reserv.cpwater.value)**2/(4*model.reserv.krock.value*model.reserv.rhorock.value*model.reserv.cprock.value)*(q/float(model.reserv.fracnumbcalc.value)/model.reserv.fracwidthcalc.value/model.reserv.fracheightcalc.value)**2*model.reserv.timevector.value*365.*24.*3600 + # calculate non-dimensional time + td = ((model.reserv.rhowater.value * model.reserv.cpwater.value) ** 2 / (4 * model.reserv.krock.value * model.reserv.rhorock.value * model.reserv.cprock.value) * + (q / float(model.reserv.fracnumbcalc.value) / model.reserv.fracwidthcalc.value / model.reserv.fracheightcalc.value) ** 2 * + model.reserv.timevector.value * 365. * 24. * 3600) # calculate non-dimensional temperature array Twnd = [] @@ -71,13 +90,14 @@ def Calculate(self, model:Model): for t in range(1, len(model.reserv.timevector.value)): Twnd = Twnd + [float(invertlaplace(fp, td[t], method='talbot'))] except: - print("Error: GEOPHIRES could not execute numerical inverse laplace calculation for reservoir model 1. Simulation will abort.") + print( + "Error: GEOPHIRES could not execute numerical inverse laplace calculation for reservoir model 1. Simulation will abort.") sys.exit() Twnd = np.asarray(Twnd) # calculate dimensional temperature, add initial rock temperature to beginning of array - model.reserv.Tresoutput.value = model.reserv.Trock.value - (Twnd*(model.reserv.Trock.value-model.wellbores.Tinj.value)) + model.reserv.Tresoutput.value = model.reserv.Trock.value - (Twnd * (model.reserv.Trock.value - model.wellbores.Tinj.value)) model.reserv.Tresoutput.value = np.append([model.reserv.Trock.value], model.reserv.Tresoutput.value) model.logger.info("Complete " + str(__class__) + ": " + sys._getframe().f_code.co_name) diff --git a/src/geophires_x/Model.py b/src/geophires_x/Model.py index cb1e54ad..be75196d 100644 --- a/src/geophires_x/Model.py +++ b/src/geophires_x/Model.py @@ -2,10 +2,8 @@ import logging import time import logging.config -from typing import Tuple from geophires_x.OptionList import EndUseOptions -from geophires_x.Parameter import Parameter from geophires_x.GeoPHIRESUtils import read_input_file from geophires_x.WellBores import WellBores from geophires_x.SurfacePlant import SurfacePlant @@ -21,13 +19,7 @@ class Model(object): def __init__(self, enable_geophires_logging_config=True): """ The __init__ function is called automatically every time the class is being used to create a new object. - - The self parameter is a Python convention. It must be included in each function definition - and points to the current instance of the class (the object that is being created). - - :param self: Reference the class instance itself :return: Nothing - :doc-author: Malcolm Ross """ # get logging started @@ -42,12 +34,12 @@ def __init__(self, enable_geophires_logging_config=True): # keep track of execution time self.tic = time.time() - # declare some dictionaries - self.InputParameters = {} # dictionary to hold all the input parameter the user wants to change - + # dictionary to hold all the input parameter the user wants to change # This should give us a dictionary with all the parameters the user wants to set. # Should be only those value that they want to change from the default. # we do this as soon as possible because what we instantiate may depend on settings in this file + self.InputParameters = {} + read_input_file(self.InputParameters, logger=self.logger) self.ccuseconomics = None @@ -57,19 +49,14 @@ def __init__(self, enable_geophires_logging_config=True): self.addoutputs = None self.addeconomics = None - # these are database operation we aren't doing yet - # model_elements = self.RunStoredProcedure("model_elements", [1]) - # model_connections = self.RunStoredProcedure("model_connections", [1]) - # self.RunStoredProcedure("delete_model", [14]) - # self.RunStoredProcedure("add_new_model", ["dummy", "new", 999]) - # Initiate the elements of the Model # this is where you can change what class get initiated - the superclass, or one of the subclasses self.logger.info("Initiate the elements of the Model") # we need to decide which reservoir to instantiate based on the user input (InputParameters), # which we just read above for the first time + # Default is Thermal drawdown percentage model (GETEM) from .TDPReservoir import TDPReservoir as TDPReservoir - self.reserv = TDPReservoir(self) # Default is Thermal drawdown percentage model (GETEM) + self.reserv = TDPReservoir(self) if 'Reservoir Model' in self.InputParameters: if self.InputParameters['Reservoir Model'].sValue == '0': from geophires_x.CylindricalReservoir import CylindricalReservoir as CylindricalReservoir @@ -171,10 +158,7 @@ def __str__(self): def read_parameters(self) -> None: """ The read_parameters function reads the parameters from the input file and stores them in a dictionary. - - :param self: Access the variables and other functions of the class :return: None - :doc-author: Malcolm Ross """ self.logger.info(f'Init {str(__class__)}: {sys._getframe().f_code.co_name}') @@ -204,13 +188,9 @@ def read_parameters(self) -> None: def Calculate(self): """ The Calculate function is where all the calculations are made. This is handled on a class-by-class basis. - The Calculate function does not return anything, but it does store the results in self.reserv, self.wellbores and self.surfaceplant for later use by other functions. - - :param self: Access the class variables :return: None - :doc-author: Malcolm Ross """ self.logger.info(f'Init {str(__class__)}: {sys._getframe().f_code.co_name}') # calculate the results diff --git a/src/geophires_x/Outputs.py b/src/geophires_x/Outputs.py index 652845ea..5dc24817 100644 --- a/src/geophires_x/Outputs.py +++ b/src/geophires_x/Outputs.py @@ -10,17 +10,20 @@ NL="\n" + class Outputs: + """ + This class handles all the outputs for the GEOPHIRESv3 model. + """ 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 the Outputs. - :param self: Store data that will be used by the class :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 - :doc-author: Malcolm Ross """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) @@ -40,18 +43,17 @@ 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 self: Access variables that belong to a class - :param model: The container class of the application, giving access to everything else, including the logger - :return: None - :doc-author: Malcolm Ross - #Deal with all the parameter values that the user has provided. They should really only provide values that + 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 + 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), + 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. + :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("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) @@ -74,11 +76,12 @@ def read_parameters(self, model:Model) -> None: model.logger.info("Complete "+ str(__class__) + ": " + sys._getframe().f_code.co_name) - def PrintOutputs(self, model:Model): + def PrintOutputs(self, model: Model): """ PrintOutputs writes the standard outputs to the output file. - Args: - model (Model): The container class of the application, giving access to everything else, including the logger + :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("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) @@ -555,12 +558,8 @@ def PrintOutputs(self, model:Model): model.surfaceplant.HeatkWhExtracted.value[i]/1E6, model.surfaceplant.RemainingReservoirHeatContent.value[i], (model.reserv.InitialReservoirHeatContent.value-model.surfaceplant.RemainingReservoirHeatContent.value[i])*100/model.reserv.InitialReservoirHeatContent.value)+NL) - - - f.write(NL) - # MIR MIR if model.wellbores.HasHorizontalSection.value: model.cloutputs.PrintOutputs(model) if model.economics.DoAddOnCalculations.value: model.addoutputs.PrintOutputs(model) if model.economics.DoCCUSCalculations.value: model.ccusoutputs.PrintOutputs(model) if model.economics.DoSDACGTCalculations.value: model.sdacgtoutputs.PrintOutputs(model) @@ -575,6 +574,12 @@ def PrintOutputs(self, model:Model): model.logger.info("Complete "+ str(__class__) + ": " + sys._getframe().f_code.co_name) def MakeDistrictHeatingPlot(self, model: Model): + """ + Make a plot of the district heating system + :param model: GEOPHIRES model + :type model: :class:`~geophires_x.Model.Model` + :return: None + """ plt.close('all') year_day = np.arange(1, 366, 1) # make an array of days for plot x-axis plt.plot(year_day, model.surfaceplant.dailyheatingdemand.value, label='District Heating Demand') diff --git a/src/geophires_x/OutputsAddOns.py b/src/geophires_x/OutputsAddOns.py index 3aaacfab..fc40993e 100644 --- a/src/geophires_x/OutputsAddOns.py +++ b/src/geophires_x/OutputsAddOns.py @@ -6,17 +6,15 @@ class OutputsAddOns(Outputs): - """description of class""" + """ + Class to handles output of the AddOns values + """ def PrintOutputs(self, model: Model): """ The PrintOutputs function prints the results of the AddOns to a text file and to the screen. - The function is called from within GEOPHIRES when runph is set equal to 1. The PrintOutputs function calls - on other functions that are used in printing specific outputs. - - :param self: Access variables that belong to a class :param model: Model: The container class of the application, giving access to everything else, including the logger + :type model: :class:`~geophires_x.Model.Model` :return: Nothing - :doc-author: Malcolm Ross """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) diff --git a/src/geophires_x/OutputsCCUS.py b/src/geophires_x/OutputsCCUS.py index 0bf27b7a..1d122274 100644 --- a/src/geophires_x/OutputsCCUS.py +++ b/src/geophires_x/OutputsCCUS.py @@ -7,8 +7,16 @@ class OutputsCCUS(Outputs): - """description of class""" + """ + Class to handles output of the CCUS values + """ def PrintOutputs(self, model: Model): + """ + The PrintOutputs function prints the results of the CCUS to a text file and to the screen. + :param model: Model: The container class of the application, giving access to everything else, including the logger + :type model: :class:`~geophires_x.Model.Model` + :return: Nothing + """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) if np.sum(model.ccuseconomics.CCUSRevenue.value) == 0: diff --git a/src/geophires_x/OutputsS_DAC_GT.py b/src/geophires_x/OutputsS_DAC_GT.py index 5681bbc1..7726b63f 100644 --- a/src/geophires_x/OutputsS_DAC_GT.py +++ b/src/geophires_x/OutputsS_DAC_GT.py @@ -6,8 +6,16 @@ class OutputsS_DAC_GT(Outputs): - """description of class""" + """ + Class to handles output of the SDAC_GT values + """ def PrintOutputs(self, model: Model): + """ + The PrintOutputs function prints the results of the SDAC_GT to a text file and to the screen. + :param model: Model: The container class of the application, giving access to everything else, including the logger + :type model: :class:`~geophires_x.Model.Model` + :return: Nothing + """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) # now do S_DAC_GT output, which will append to the original output diff --git a/src/geophires_x/Parameter.py b/src/geophires_x/Parameter.py index 911abc7c..c78059ca 100644 --- a/src/geophires_x/Parameter.py +++ b/src/geophires_x/Parameter.py @@ -183,17 +183,16 @@ def ReadParameter(ParameterReadIn: ParameterEntry, ParamToModify, model): """ ReadParameter: A method to take a single ParameterEntry object and use it to update the associated Parameter. Does validation as well as Unit and Currency conversion - - Args: - ParameterReadIn (ParameterEntry): The value the user wants to change - ParamToModify (Parameter): The Parameter that will be modified (assuming it passes validation and conversion) - model (Model): The container class of the application, giving access to everything else, including the logger - - Returns: - None - - Yields: - None + :param ParameterEntry: The value the user wants to change and the value they want to change it to (as a string) + and any comment they provided with it (as a string) - all in one object (ParameterEntry) that is passed in + to this method as a parameter itself (ParameterReadIn) - see ParameterEntry class for details on the fields in it + :type ParameterEntry: :class:`~geophires_x.Parameter.ParameterEntry` + :param ParamToModify: The Parameter that will be modified (assuming it passes validation and conversion) - this is + the object that will be modified by this method - see Parameter class for details on the fields in it + :type ParamToModify: :class:`~geophires_x.Parameter.Parameter` + :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("Init " + str(__name__) + ": " + sys._getframe().f_code.co_name + " for " + ParamToModify.Name) # these Parameter Types don't have units so don't do anything fancy, and ignore it if the user has supplied units @@ -335,13 +334,17 @@ def ReadParameter(ParameterReadIn: ParameterEntry, ParamToModify, model): def ConvertUnits(ParamToModify, strUnit: str, model) -> str: """ ConvertUnits gets called if a unit version is needed: either currency or standard units like F to C or m to ft - Args: - ParamToModify (Parameter): The Parameter that will be modified (assuming it passes validation and conversion) - strUnit (str): A string containing the value to be converted along with the units it is current in. + :param ParamToModify: The Parameter that will be modified (assuming it passes validation and conversion) - this is + the object that will be modified by this method - see Parameter class for details on the fields in it - this + is the object that will be modified by this method - see Parameter class for details on the fields in it + :type ParamToModify: :class:`~geophires_x.Parameter.Parameter` + :param strUnit: A string containing the value to be converted along with the units it is current in. The units to convert to are set by the PreferredUnits of ParamToModify - model (Model): The container class of the application, giving access to everything else, including the logger - Returns: - str: The new value as a string (without the units, because they are already held in PreferredUnits of ParamToModify) + :type strUnit: str + :param model: The container class of the application, giving access to everything else, including the logger + :type model: :class:`~geophires_x.Model.Model` + :return: The new value as a string (without the units, because they are already held in PreferredUnits of ParamToModify) + :rtype: str """ model.logger.info("Init " + str(__name__) + ": " + sys._getframe().f_code.co_name + " for " + ParamToModify.Name) @@ -504,9 +507,12 @@ def ConvertUnitsBack(ParamToModify, model): """ CovertUnitsBack: Converts units back to what the user specified they as. It does this so that the user can see them in the report as the units they specified. We know that because CurrentUnits contains the desired units - Args: - ParamToModify (Parameter): The Parameter that will be modified. - model (Model): The container class of the application, giving access to everything else, including the logger + :param ParamToModify: The Parameter that will be modified (assuming it passes validation and conversion) - this is + the object that will be modified by this method - see Parameter class for details on the fields in it + :type ParamToModify: :class:`~geophires_x.Parameter.Parameter` + :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("Init " + str(__name__) + ": " + sys._getframe().f_code.co_name + " for " + ParamToModify.Name) @@ -653,10 +659,12 @@ def LookupUnits(sUnitText: str): """ LookupUnits Given a unit class and a text string, this will return the value from the Enumeration if it is there (or return nothing if it is not) - Args: - sUnitText (str): The units desired to be checked (e.g., "ft", "degF") - Returns: - Enum: The Enumerated value and the Unit class Enumeration + :param sUnitText: The text string to look for in the Enumeration of units (like "m" or "feet") - this is the text + that the user provides in the input file or the GUI to specify the units they want to use for a parameter or + output value (like "m" or "feet") + :type sUnitText: str (text) + :return: The Enumerated value and the Unit class Enumeration + :rtype: tuple """ # look through all unit types and names for a match with my units for uType in Units: @@ -741,14 +749,20 @@ def ConvertOutputUnits(oparam: OutputParameter, newUnit: Units, model): """ ConvertOutputUnits Given an output parameter, convert the value(s) from what they contain (as calculated by GEOPHIRES) to what the user specified as what they want for outputs. Conversion happens inline. - Args: - oparam (OutputParameter): The parameter you want to be converted (value or list of values). - Because Parameters know the PreferredUnits and CurrentUnits, this routine knows what to do. - newUnit (Units): The new units you want to convert value to - model (Model): The container class of the application, giving access to everything else, including the logger - - Returns: - None + :param oparam: The parameter you want to be converted (value or list of values). Because Parameters know the + PreferredUnits and CurrentUnits, this routine knows what to do. It will convert the value(s) in the parameter + to the new units, and then reset the CurrentUnits to the new units. This is done so that the user can see the units + they specified in the output report. The value(s) in the parameter are converted back to the original units after + the report is generated. This is done so that the calculations are done in the units that GEOPHIRES expects. If + the user wants to see the output in different units, they can specify that in the input file or the GUI. + :type oparam: :class:`~geophires_x.Parameter.Parameter` + :param newUnit: The new units you want to convert value to (like "m" or "feet") - this is the text that the user + provides in the input file or the GUI to specify the units they want to use for a parameter or output value + (like "m" or "feet") + :type newUnit: str (text) + :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 """ if isinstance(oparam.value, str): return # strings have no units diff --git a/src/geophires_x/Reservoir.py b/src/geophires_x/Reservoir.py index 5af96845..b0b5e31b 100644 --- a/src/geophires_x/Reservoir.py +++ b/src/geophires_x/Reservoir.py @@ -11,6 +11,13 @@ # user-defined functions def densitywater(Twater) -> float: + """ + This function calculates the density of water as a function of temperature. + :param Twater: The temperature of the water in Celsius degrees (°C) + :type Twater: float + :return: The density of the water in kg/m3 + :rtype: float + """ T = Twater + 273.15 # water density correlation as used in Geophires v1.2 [kg/m3] rhowater = (.7983223 + (1.50896E-3 - 2.9104E-6 * T) * T) * 1E3 @@ -18,12 +25,26 @@ def densitywater(Twater) -> float: def viscositywater(Twater) -> float: + """ + This function calculates the viscosity of water as a function of temperature. + :param Twater: The temperature of the water in Celsius degrees (°C) + :type Twater: float + :return: The viscosity of the water in Ns/m2 + :rtype: float + """ # accurate to within 2.5% from 0 to 370 degrees C [Ns/m2] muwater = 2.414E-5 * np.power(10, 247.8 / (Twater + 273.15 - 140)) return muwater def heatcapacitywater(Twater) -> float: + """ + This function calculates the heat capacity of water as a function of temperature. + :param Twater: The temperature of the water in Celsius degrees (°C) + :type Twater: float + :return: The heat capacity of the water in J/kg/K + :rtype: float + """ # J/kg/K (based on TARB in Geophires v1.2) Twater = (Twater + 273.15) / 1000 A = -203.6060 @@ -46,11 +67,9 @@ 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 the Reservoir. - - :param self: Store data that will be used by the class :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 - :doc-author: Malcolm Ross """ model.logger.info(f'Init {str(__class__)}: {sys._getframe().f_code.co_name}') @@ -574,14 +593,12 @@ def read_parameters(self, model: Model) -> None: """ The read_parameters function reads in the parameters from a dictionary created by reading the user-provided file and updates the parameter values for this object. - The function reads in all the parameters that relate to this object, including those that are inherited from other objects. It then updates any of these parameter values that have been changed by the user. It also handles any special cases. - :param self: Reference the class instance (such as it is) from within the class :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 - :doc-author: Malcolm Ross """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) @@ -718,11 +735,9 @@ 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 self: Access variables that belongs to the class :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 - :doc-author: Malcolm Ross """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) diff --git a/src/geophires_x/SFReservoir.py b/src/geophires_x/SFReservoir.py index 7b59ece1..a864a4d0 100644 --- a/src/geophires_x/SFReservoir.py +++ b/src/geophires_x/SFReservoir.py @@ -15,10 +15,9 @@ 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. - :param self: Store data that will be used by the class :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 - :doc-author: Malcolm Ross """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) super().__init__(model) # initialize the parent parameters and variables @@ -60,10 +59,9 @@ def read_parameters(self, model: Model) -> None: The function reads in all the parameters that relate to this object, including those that are inherited from other objects. It then updates any of these parameter values that have been changed by the user. It also handles any special cases. - :param self: Reference the class instance (such as it is) from within the class :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 - :doc-author: Malcolm Ross """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) super().read_parameters(model) # read the parameters for the parent. @@ -74,6 +72,15 @@ def read_parameters(self, model: Model) -> None: model.logger.info("Complete " + str(__class__) + ": " + sys._getframe().f_code.co_name) def Calculate(self, model:Model): + """ + The Calculate function calculates the values of all the parameters that are calculated by this object. + It calls the Calculate function of the parent object to calculate the values of the parameters that are + calculated by the parent object. + It then calculates the values of the parameters that are calculated by this object. + :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("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) super().Calculate(model) # run calculation for the parent. diff --git a/src/geophires_x/SurfacePlant.py b/src/geophires_x/SurfacePlant.py index 15c812f1..b5337c69 100644 --- a/src/geophires_x/SurfacePlant.py +++ b/src/geophires_x/SurfacePlant.py @@ -14,10 +14,9 @@ def __init__(self, model: Model): 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 the Surfaceplant. - :param self: Store data that will be used by the class :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 - :doc-author: Malcolm Ross """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) @@ -501,10 +500,8 @@ 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 self: Access variables that belong to a class :param model: The container class of the application, giving access to everything else, including the logger :return: None - :doc-author: Malcolm Ross """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) @@ -631,11 +628,9 @@ 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 self: Access variables that belongs to the class :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 - :doc-author: Malcolm Ross """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) @@ -967,6 +962,12 @@ def Calculate(self, model: Model) -> None: # district heating routines below def CalculateDHDemand(self, model: Model) -> None: + """ + Calculate the direct Heat demand of the district heating system based on the number of housing units and the census division + :param model: the model + :type model: :class:`~geophires_x.Model.Model` + :return: None + """ # calculate heating demand for a district heating system model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) @@ -987,21 +988,19 @@ def CalculateDHDemand(self, model: Model) -> None: def read_daily_demand(self, demand_file_name, demand_data_column, time_interval): - ''' - Parameters - ---------- - demand_file_name : string - CSV file name containing demand data, a single header row is allowed - demand_data_column : int - column number of demand data, starting from 1 - time_interval : int + """ + Read the daily demand data column from the csv file and return the daily demand in MWh/day + :param demand_file_name: the name of the csv file + :type demand_file_name: str + :param demand_data_column: the column number of the demand data + :type demand_data_column: int + :param time_interval: the time interval of the demand data; 1: hourly data, units in MW or MWh (both are treated equivalent) 2: daily data, units in MWh - - Returns - ------- - numpy array of daily demand in MWh/day - ''' + :type time_interval: int + :return: numpy array of daily demand in MWh/day + :rtype: numpy array + """ np.demand = [] if time_interval == 1: # hourly data diff --git a/src/geophires_x/TDPReservoir.py b/src/geophires_x/TDPReservoir.py index af336afb..8314d95f 100644 --- a/src/geophires_x/TDPReservoir.py +++ b/src/geophires_x/TDPReservoir.py @@ -15,10 +15,9 @@ 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. - :param self: Store data that will be used by the class :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 - :doc-author: Malcolm Ross """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) super().__init__(model) # initialize the parent parameters and variables @@ -60,10 +59,9 @@ def read_parameters(self, model: Model) -> None: The function reads in all the parameters that relate to this object, including those that are inherited from other objects. It then updates any of these parameter values that have been changed by the user. It also handles any special cases. - :param self: Reference the class instance (such as it is) from within the class :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 - :doc-author: Malcolm Ross """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) super().read_parameters(model) # read the parameters for the parent. @@ -75,6 +73,12 @@ def read_parameters(self, model: Model) -> None: model.logger.info("Complete " + str(__class__) + ": " + sys._getframe().f_code.co_name) def Calculate(self, model: Model): + """ + The Calculate function calculates the values of all the parameters that are calculated by this object. + :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("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) super().Calculate(model) # run calculation for the parent. diff --git a/src/geophires_x/TOUGH2Reservoir.py b/src/geophires_x/TOUGH2Reservoir.py index 2f137755..16240ac9 100644 --- a/src/geophires_x/TOUGH2Reservoir.py +++ b/src/geophires_x/TOUGH2Reservoir.py @@ -16,10 +16,9 @@ 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. - :param self: Store data that will be used by the class :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 - :doc-author: Malcolm Ross """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) super().__init__(model) # initialize the parent parameters and variables @@ -72,14 +71,12 @@ def read_parameters(self, model:Model) -> None: """ The read_parameters function reads in the parameters from a dictionary created by reading the user-provided file and updates the parameter values for this object. - The function reads in all the parameters that relate to this object, including those that are inherited from other objects. It then updates any of these parameter values that have been changed by the user. It also handles any special cases. - :param self: Reference the class instance (such as it is) from within the class :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 - :doc-author: Malcolm Ross """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) super().read_parameters(model) # read the parameters for the parent. @@ -115,10 +112,19 @@ def read_parameters(self, model:Model) -> None: model.logger.info("Complete " + str(__class__) + ": " + sys._getframe().f_code.co_name) def Calculate(self, model:Model): + """ + The Calculate function calculates the values of all the parameters that are calculated by this object. + It calls the Calculate function of the parent object to calculate the values of the parameters that are + calculated by the parent object. + It then calculates the values of the parameters that are calculated by this object. + :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("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) super().Calculate(model) # run calculate for the parent. - # GEOPHIRES assumes TOUGH2 executable and input file are in same directory as GEOPHIRESv2.py + # GEOPHIRES assumes TOUGH2 executable and input file are in same directory as GEOPHIRESv3.py # create tough2 input file path_to_exe = str('xt2_eos1.exe') if not os.path.exists(os.path.join(os.getcwd(), path_to_exe)): diff --git a/src/geophires_x/UPPReservoir.py b/src/geophires_x/UPPReservoir.py index 15e038a0..d34affd1 100644 --- a/src/geophires_x/UPPReservoir.py +++ b/src/geophires_x/UPPReservoir.py @@ -14,10 +14,9 @@ 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. - :param self: Store data that will be used by the class :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 - :doc-author: Malcolm Ross """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) super().__init__(model) # initialize the parent parameters and variables @@ -53,14 +52,11 @@ def read_parameters(self, model:Model) -> None: """ The read_parameters function reads in the parameters from a dictionary created by reading the user-provided file and updates the parameter values for this object. - The function reads in all the parameters that relate to this object, including those that are inherited from other objects. It then updates any of these parameter values that have been changed by the user. It also handles any special cases. - :param self: Reference the class instance (such as it is) from within the class :param model: The container class of the application, giving access to everything else, including the logger :return: None - :doc-author: Malcolm Ross """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) super().read_parameters(model) # read the parameters for the parent. @@ -72,6 +68,13 @@ def read_parameters(self, model:Model) -> None: model.logger.info("Complete " + str(__class__) + ": " + sys._getframe().f_code.co_name) def Calculate(self, model: Model): + """ + The Calculate function calculates the values of the parameters that are calculated from other parameters. + This includes the parameters that are calculated and then published using the Printouts function. + :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("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) super().Calculate(model) # run calculations for the parent. diff --git a/src/geophires_x/WellBores.py b/src/geophires_x/WellBores.py index f90b59b7..5c8b5b5d 100644 --- a/src/geophires_x/WellBores.py +++ b/src/geophires_x/WellBores.py @@ -13,9 +13,9 @@ def vaporpressurewater(Twater: float) -> float: """ calculate the vapor pressure of water based on the temperature of the water - :param Twater: temperature of the water - :return: vapor pressure of the water - :doc-author: Malcolm Ross + :param Twater: temperature of the water [C] (can be a vector) + :type Twater: float + :return: vapor pressure of the water [kPa] (can be a vector) """ if Twater < 100: A = 8.07131 @@ -37,20 +37,32 @@ def RameyCalc(krock: float, rhorock: float, cprock: float, welldiam: float, tv, For multiple gradients, use Ramey's model for every layer assume outside diameter of casing is 10% larger than inside diameter of production pipe (=prodwelldiam) assume borehole thermal resistance is negligible to rock thermal resistance - :param depth: - :param averagegradient: - :param Tresoutput: - :param Trock: - :param flowrate: - :param utilfactor: - :param tv: - :param welldiam: - :param cprock: - :param rhorock: - :param krock: - :param cpwater: - :return: temperature drop - :doc-author: Malcolm Ross + :param depth: depth of the well [m] + :type: float + :param averagegradient: average geothermal gradient [C/km] + :type: float + :param Tresoutput: reservoir output temperature [C] + :type: float + :param Trock: rock temperature [C] + :type: float + :param flowrate: flow rate [kg/s] + :type: float + :param utilfactor: utilization factor (fraction of time the well is producing) [-] + :type: float + :param tv: time vector [years] + :type: float + :param welldiam: well diameter [m] + :type: float + :param cprock: rock heat capacity [J/kg/C] + :type: float + :param rhorock: rock density [kg/m3] + :type: float + :param krock: rock thermal conductivity [W/m/C] + :type: float + :param cpwater: water heat capacity [J/kg/C] + :type: float + :return: temperature drop along the length of the well [C] + :rtype: float """ alen = len(tv) alpharock = krock / (rhorock * cprock) @@ -68,15 +80,25 @@ def RameyCalc(krock: float, rhorock: float, cprock: float, welldiam: float, tv, def WellPressureDrop(model: Model, Taverage: float, wellflowrate: float, welldiam: float, impedancemodelused: bool, depth: float) -> tuple: """ - calculate the pressure drop over the length of the well due to friction or impedance - :param model: - :param depth: - :param impedancemodelused: - :param welldiam: - :param wellflowrate: - :param Taverage: + calculate the pressure drop over the length of the well due to friction or impedance for the production well and + the injection well (if applicable) using the Impedance Model or the friction model (if applicable) and the well + flow rate and diameter and the average temperature of the fluid in the well (which is the average of the reservoir + temperature and the injection temperature) and the depth of the well and the impedance of the reservoir (if + applicable) and the number of production wells and the number of injection wells and the water loss (if applicable) + :param model: The container class of the application, giving access to everything else, including the logger + :type model: :class:`~geophires_x.Model.Model` + :param Taverage: average temperature of the fluid in the well [C] + :type Taverage: float + :param wellflowrate: flow rate of the fluid in the well [kg/s] + :type wellflowrate: float + :param welldiam: diameter of the well [m] + :type welldiam: float + :param impedancemodelused: whether or not the impedance model is used (True or False) [-] + :type impedancemodelused: bool + :param depth: depth of the well [m] + :type depth: float :return: tuple of DPWell, f3, v, rhowater - :doc-author: Malcolm Ross + :rtype: tuple """ # start by calculating wellbore fluid conditions [kPa], noting that most temperature drop happens # in upper section (because surrounding rock temperature is lowest in upper section) @@ -110,18 +132,26 @@ def InjectionWellPressureDrop(model: Model, Taverage: float, wellflowrate: float impedancemodelused: bool, depth: float, nprod: int, ninj: int, waterloss: float) -> tuple: """ calculate the injection well pressure drop over the length of the well due to friction or impedance - :param self: - :param model: - :param depth: - :param impedancemodelused: - :param welldiam: - :param wellflowrate: - :param Taverage: - :param waterloss: - :param ninj: - :param nprod: + :param model: The container class of the application, giving access to everything else, including the logger + :type model: :class:`~geophires_x.Model.Model` + :param Taverage: average temperature of the fluid in the well [C] + :type Taverage: float + :param wellflowrate: flow rate of the fluid in the well [kg/s] + :type wellflowrate: float + :param welldiam: diameter of the well [m] + :type welldiam: float + :param impedancemodelused: whether or not the impedance model is used (True or False) [-] + :type impedancemodelused: bool + :param depth: depth of the well [m] + :type depth: float + :param nprod: number of production wells [-] + :type nprod: int + :param ninj: number of injection wells [-] + :type ninj: int + :param waterloss: water loss [-] + :type waterloss: float :return: tuple of DPWell, f1, v, rhowater - :doc-author: Malcolm Ross + :rtype: tuple """ # start by calculating wellbore fluid conditions [kPa], noting that most temperature drop happens in # upper section (because surrounding rock temperature is lowest in upper section) @@ -160,20 +190,32 @@ def ProdPressureDropsAndPumpingPowerUsingImpedenceModel(f3: float, vprod: float, pumpeff: float) -> tuple: """ Calculate Pressure Drops and Pumping Power needed for the production well using the Impedance Model - :param depth: - :param wellflowrate: - :param waterloss: - :param nprod: - :param pumpeff: - :param impedance: - :param prodwelldiam: - :param rhowaterreservoir: - :param rhowaterprod: - :param rhowaterinj: - :param vprod: - :param f3: + :param f3: friction factor [-] + :type f3: float + :param vprod: velocity of the fluid in the production well [m/s] + :type vprod: float + :param rhowaterinj: density of the water in the injection well [kg/m3] + :type rhowaterinj: float + :param rhowaterreservoir: density of the water in the reservoir [kg/m3] + :type rhowaterreservoir: float + :param rhowaterprod: density of the water in the production well [kg/m3] + :type rhowaterprod: float + :param depth: depth of the well [m] + :type depth: float + :param wellflowrate: flow rate of the fluid in the well [kg/s] + :type wellflowrate: float + :param prodwelldiam: diameter of the well [m] + :type prodwelldiam: float + :param impedance: impedance of the reservoir [kg/s/kPa] + :type impedance: float + :param nprod: number of production wells [-] + :type nprod: int + :param waterloss: water loss [-] + :type waterloss: float + :param pumpeff: pump efficiency [-] + :type pumpeff: float :return: tuple of DPOverall, PumpingPower, DPProdWell, DPReserv, DPBouyancy - :doc-author: Malcolm Ross + :rtype: tuple """ # production well pressure drops [kPa] DPProdWell = f3 * (rhowaterprod * vprod ** 2 / 2.) * (depth / prodwelldiam) / 1E3 # /1E3 to convert from Pa to kPa @@ -202,18 +244,28 @@ def InjPressureDropsAndPumpingPowerUsingImpedenceModel(f1: float, vinj: float, r waterloss: float, pumpeff: float, DPOverall) -> tuple: """ Calculate Injection well Pressure Drops and Pumping Power needed for the injection well using the Impedance Model - :param depth: - :param wellflowrate: - :param waterloss: - :param rhowaterinj: - :param DPOverall: - :param pumpeff: - :param ninj: - :param injwelldiam: - :param vinj: - :param f1: - :return: tuple of newDPOverall, PumpingPower, DPInjWell - :doc-author: Malcolm Ross + :param f1: friction factor [-] + :type f1: float + :param vinj: velocity of the fluid in the injection well [m/s] + :type vinj: float + :param rhowaterinj: density of the water in the injection well [kg/m3] + :type rhowaterinj: float + :param depth: depth of the well [m] + :type depth: float + :param wellflowrate: flow rate of the fluid in the well [kg/s] + :type wellflowrate: float + :param injwelldiam: diameter of the well [m] + :type injwelldiam: float + :param ninj: number of injection wells [-] + :type ninj: int + :param waterloss: water loss [-] + :type waterloss: float + :param pumpeff: pump efficiency [-] + :type pumpeff: float + :param DPOverall: overall pressure drop [kPa] + :type DPOverall: float + :return: tuple of newDPOverall, PumpingPower, DPInjWell [kPa] + :rtype: tuple """ # Calculate Pressure Drops and Pumping Power needed for the injection well using the Impedance Model # injection well pressure drops [kPa] @@ -239,25 +291,41 @@ def ProdPressureDropAndPumpingPowerUsingIndexes(model: Model, usebuiltinhydrosta rhowaterprod: float) -> tuple: """ Calculate Pressure Drops and Pumping Power needed for the production well using indexes - :param depth: - :param wellflowrate: - :param pumpeff: - :param rhowaterprod: - :param nprod: - :param prodwelldiam: - :param vprod: - :param f3: - :param PI: - :param ppwellhead: - :param gradient: - :param Tsurf: - :param Trock: - :param usebuiltinppwellheadcorrelation: - :param productionwellpumping: - :param usebuiltinhydrostaticpressurecorrelation: - :param model: - :return: tuple of PumpingPower, PumpingPowerProd, DPProdWell, Pprodwellhead - :doc-author: Malcolm Ross + :param model: The container class of the application, giving access to everything else, including the logger + :type model: :class:`~geophires_x.Model.Model` + :param usebuiltinhydrostaticpressurecorrelation: whether or not to use the built-in hydrostatic pressure correlation (True or False) [-] + :type usebuiltinhydrostaticpressurecorrelation: bool + :param productionwellpumping: whether or not the production well is pumping (True or False) [-] + :type productionwellpumping: bool + :param usebuiltinppwellheadcorrelation: whether or not to use the built-in wellhead pressure correlation (True or False) [-] + :type usebuiltinppwellheadcorrelation: bool + :param Trock: rock temperature [C] + :type Trock: float + :param Tsurf: surface temperature [C] + :type Tsurf: float + :param depth: depth of the well [m] + :type depth: float + :param gradient: geothermal gradient [C/km] + :param ppwellhead: production wellhead pressure [kPa] + :type ppwellhead: float + :param PI: productivity index [kg/s/bar] + :type PI: float + :param wellflowrate: flow rate of the fluid in the well [kg/s] + :type wellflowrate: float + :param f3: friction factor [-] + :type f3: float + :param vprod: velocity of the fluid in the production well [m/s] + :type vprod: float + :param prodwelldiam: diameter of the well [m] + :type prodwelldiam: float + :param nprod: number of production wells [-] + :type nprod: int + :param pumpeff: pump efficiency [-] + :type pumpeff: float + :param rhowaterprod: density of the water in the production well [kg/m3] + :type rhowaterprod: float + :return: tuple of PumpingPower, PumpingPowerProd, DPProdWell, Pprodwellhead [kPa] + :rtype: tuple """ # initialize PumpingPower value in case it doesn't get set. PumpingPower = PumpingPowerProd = DPProdWell = Pprodwellhead = ([0.0] * len(vprod)) @@ -329,30 +397,52 @@ def InjPressureDropAndPumpingPowerUsingIndexes(model: Model, usebuiltinhydrostat rhowaterinj: float, Pplantoutlet: float, PumpingPowerProd) -> tuple: """ Calculate PressureDrops and Pumping Power needed for the injection well using indexes - :param depth: - :param wellflowrate: - :param pumpeff: - :param nprod: - :param ppwellhead: - :param gradient: - :param Tsurf: - :param Trock: - :param usebuiltinppwellheadcorrelation: - :param productionwellpumping: + :param depth: depth of the well [m] + :type depth: float + :param wellflowrate: flow rate of the fluid in the well [kg/s] + :type wellflowrate: float + :param pumpeff: pump efficiency [-] + :type pumpeff: float + :param nprod: number of production wells [-] + :type nprod: int + :param ppwellhead: production wellhead pressure [kPa] + :type ppwellhead: float + :param gradient: geothermal gradient [C/km] + :type gradient: float + :param Tsurf: surface temperature [C] + :type Tsurf: float + :param Trock: rock temperature [C] + :type Trock: float + :param usebuiltinppwellheadcorrelation: whether or not to use the built-in wellhead pressure correlation (True or False) [-] + :type usebuiltinppwellheadcorrelation: bool + :param productionwellpumping: whether or not the production well is pumping (True or False) [-] + :type productionwellpumping: bool :param usebuiltinhydrostaticpressurecorrelation: - :param model: - :param Pplantoutlet: - :param rhowaterinj: - :param waterloss: - :param ninj: - :param injwelldiam: - :param vinj: - :param f1: - :param usebuiltinoutletplantcorrelation: - :param PumpingPowerProd: - :param II: - :return: tuple of PumpingPower, PumpingPowerInj, DPInjWell, Pplantoutlet, Pprodwellhead - :doc-author: Malcolm Ross + :type usebuiltinhydrostaticpressurecorrelation: bool + :param model: The container class of the application, giving access to everything else, including the logger + :type model: :class:`~geophires_x.Model.Model` + :param Pplantoutlet: plant outlet pressure [kPa] + :type Pplantoutlet: float + :param rhowaterinj: density of the water in the injection well [kg/m3] + :type rhowaterinj: float + :param waterloss: water loss [-] + :type waterloss: float + :param ninj: number of injection wells [-] + :type ninj: int + :param injwelldiam: diameter of the well [m] + :type injwelldiam: float + :param vinj: velocity of the fluid in the injection well [m/s] + :type vinj: float + :param f1: friction factor [-] + :type f1: float + :param usebuiltinoutletplantcorrelation: whether or not to use the built-in outlet plant pressure correlation (True or False) [-] + :type usebuiltinoutletplantcorrelation: bool + :param PumpingPowerProd: pumping power for production wells [MWe] + :type PumpingPowerProd: float + :param II: injectivity index [kg/s/bar] + :type II: float + :return: tuple of PumpingPower, PumpingPowerInj, DPInjWell, Pplantoutlet, Pprodwellhead [kPa] + :rtype: tuple """ PumpingPowerInj = DPInjWell = Pprodwellhead = [0.0] # initialize value in case it doesn't get set. @@ -414,12 +504,9 @@ 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 self: Reference the class object itself :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, and is used to initialize the class - :doc-author: Malcolm Ross """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) self.rhowaterprod = self.rhowaterinj = 0.0 @@ -782,10 +869,9 @@ 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). - :param self: Access variables that belong to a class :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 - :doc-author: Malcolm Ross """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) @@ -837,10 +923,9 @@ 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 self: Access variables that belongs to the class :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 - :doc-author: Malcolm Ross """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name)