Source code for geophires_x.SurfacePlantAGS

import os
import sys
from geophires_x.WellBores import *
from geophires_x.Parameter import floatParameter, OutputParameter
from geophires_x.Units import *
from geophires_x.OptionList import WorkingFluid, EndUseOptions
from geophires_x.SurfacePlant import SurfacePlant as SurfacePlant
from scipy.interpolate import interpn, interp1d


[docs] class SurfacePlantAGS(SurfacePlant): """ SurfacePlantAGS Child class of SurfacePlant; it is the same, but has advanced AGS closed-loop functionality """ def __init__(self, model: Model): """ The __init__ function is the constructor for a class. It is called whenever an instance of the class is created. The __init__ function can take arguments, but self is always the first one. Self refers to the instance of the object that has already been created, and it's used to access variables that belong to that object; :param model: The container class of the application, giving access to everything else, including the logger :type model: :class:`~geophires_x.Model.Model` :return: Nothing, and is used to initialize the class """ model.logger.info("Init " + str(__class__) + ": " + self.__init__.__name__) # Initialize the superclass first to gain access to those variables super().__init__(model) self.MyClass = self.__class__.__name__ self.MyPath = os.path.abspath(__file__) self.Discount_rate = 0 self.T0 = 0 self.Lifetime = 0 self.Pump_efficiency = 0 self.End_use = 0 self.error = 0 self.PumpingPower = 0 self.Annual_pumping_power = None self.Linear_production_pressure = None self.Linear_production_temperature = None self.Average_fluid_density = None self.Annual_electricity_production = None self.h_0 = None self.s_0 = None self.cp_air_array = None self.Instantaneous_turbine_power = None self.Instantaneous_production_enthalpy = None self.Tair_for_cp_array = None self.Utilization_efficiency_correlation_temperatures = None self.Utilization_efficiency_correlation_conversion = None self.Heat_to_power_efficiency_correlation_temperatures = None self.Heat_to_power_efficiency_correlation_conversion = None self.Instantaneous_exergy_production = None self.Instantaneous_exergy_extraction = None self.Instantaneous_electricity_production_method_1 = None self.Instantaneous_electricity_production_method_2 = None self.Instantaneous_electricity_production_method_3 = None self.Instantaneous_electricity_production_method_4 = None self.Instantaneous_utilization_efficiency_method_1 = None self.Instantaneous_utilization_efficiency_method_2 = None self.Instantaneous_thermal_efficiency = None self.Time_array = None self.Linear_time_distribution = None self.TNOP = None self.indexclosestlifetime = 0 self.Instantaneous_production_enthalpy = None self.Instantaneous_temperature_after_isenthalpic_throttling = None self.Instantaneous_heat_production = None self.Annual_heat_production = None self.Inst_electricity_production = None self.AveInstElectricityProduction = 0 self.Average_electricity_production = None self.AveAnnualElectricityProduction = 0.0 self.AveInstNetElectricityProduction = None self.AveAnnualNetElectricityProduction = 0.0 self.FirstYearElectricityProduction = None self.Inst_Net_Electricity_production = None self.Pre_cooling_temperature = 0.0 self.Post_compressor_T_actual = None self.T_turbine_out_actual = None self.AverageInstNetExergyProduction = None self.AverageInstNetExergyExtraction = None self.AveAnnualHeatProduction = None self.AveInstHeatProduction = None self.Average_heat_production = 0.0 self.h_prod = 0.0 self.h_inj = 0.0 self.s_prod = None self.s_inj = None self.hprod = None self.hinj = None # 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. # NB: inputs we already have (marked as "already have it") need to be set at ReadParameter time so values are # set at the last possible time self.enduseefficiencyfactor = self.ParameterDict[self.enduseefficiencyfactor.Name] = floatParameter( "End-Use Efficiency Factor", value=0.9, DefaultValue=0.9, Min=0.1, Max=1.0, UnitType=Units.PERCENT, PreferredUnits=PercentUnit.TENTH, CurrentUnits=PercentUnit.TENTH, ErrMessage="assume default end-use efficiency factor (0.9)", ToolTipText="Constant thermal efficiency of the direct-use application" ) self.P0 = self.ParameterDict[self.P0.Name] = floatParameter( "Dead-state Pressure", value=1e5, DefaultValue=1e5, Min=0.8e5, Max=1.1e5, UnitType=Units.PRESSURE, PreferredUnits=PressureUnit.PASCAL, CurrentUnits=PressureUnit.PASCAL, Required=True, ErrMessage="assume default Dead-state pressure (1e5 Pa)" ) # Input data for electricity generation with CO2 self.Turbine_isentropic_efficiency = self.ParameterDict[ self.Turbine_isentropic_efficiency.Name] = floatParameter( "Isentropic Efficiency for CO2 Turbine", value=0.9, DefaultValue=0.9, Min=0.8, Max=1.0, UnitType=Units.PERCENT, PreferredUnits=PercentUnit.TENTH, CurrentUnits=PercentUnit.TENTH, Required=False, ErrMessage="assume default Isentropic efficiency for turbine when CO2 is working fluid (0.9)" ) self.Generator_efficiency = self.ParameterDict[self.Generator_efficiency.Name] = floatParameter( "Generator Conversion Efficiency", value=0.98, DefaultValue=0.98, Min=0.8, Max=1.0, UnitType=Units.PERCENT, PreferredUnits=PercentUnit.TENTH, CurrentUnits=PercentUnit.TENTH, Required=False, ErrMessage="assume default Conversion efficiency from mechanical turbine work to electricity (0.98)" ) self.Compressor_isentropic_efficiency = self.ParameterDict[ self.Compressor_isentropic_efficiency.Name] = floatParameter( "Isentropic Efficiency for CO2 Compressor", value=0.9, DefaultValue=0.9, Min=0.8, Max=1.0, UnitType=Units.PERCENT, PreferredUnits=PercentUnit.TENTH, CurrentUnits=PercentUnit.TENTH, Required=False, ErrMessage="assume default Isentropic efficiency for compressor when CO2 is working fluid (0.9)" ) self.Pre_Cooling_Delta_T = self.ParameterDict[self.Pre_Cooling_Delta_T.Name] = floatParameter( "CO2 Temperature Decline with Cooling", value=12.0, DefaultValue=12.0, Min=0.0, Max=15.0, UnitType=Units.TEMPERATURE, PreferredUnits=TemperatureUnit.CELSIUS, CurrentUnits=TemperatureUnit.CELSIUS, Required=False, ErrMessage="assume default CO2 temperature decline with cooling after turbine and before compressor (12 degC)") self.Turbine_outlet_pressure = self.ParameterDict[self.Turbine_outlet_pressure.Name] = floatParameter( "CO2 Turbine Outlet Pressure", value=81.0, DefaultValue=81.0, Min=75.0, Max=200.0, UnitType=Units.PRESSURE, PreferredUnits=PressureUnit.BAR, CurrentUnits=PressureUnit.BAR, Required=False, ErrMessage="assume default CO2 turbine outlet pressure (81 bar)" ) # local variable initiation self.P_in = 2e7 # Constant Injection pressure [Pa] self.Number_of_points_per_year = 4 # Number of time steps per year in database [-] (must be 4) # Initialize error code self.error_codes = np.zeros(0) # if error occurs, code will be assigned to this tag # outputs self.HeatExtracted = self.OutputParameterDict[self.HeatExtracted.Name] = OutputParameter( Name="Heat Extracted", UnitType=Units.POWER, PreferredUnits=PowerUnit.MW, CurrentUnits=PowerUnit.MW ) self.HeatProduced = self.OutputParameterDict[self.HeatProduced.Name] = OutputParameter( Name="Heat Produced in MW", UnitType=Units.POWER, PreferredUnits=PowerUnit.MW, CurrentUnits=PowerUnit.MW ) self.HeatkWhProduced = self.OutputParameterDict[self.HeatkWhProduced.Name] = OutputParameter( Name="Heat Produced", UnitType=Units.ENERGY, PreferredUnits=EnergyUnit.KWH, CurrentUnits=EnergyUnit.KWH ) self.PumpingkWh = self.OutputParameterDict[self.PumpingkWh.Name] = OutputParameter( Name="pumping power needed", UnitType=Units.ENERGY, PreferredUnits=EnergyUnit.KWH, CurrentUnits=EnergyUnit.KWH ) self.HeatkWhExtracted = self.OutputParameterDict[self.HeatkWhExtracted.Name] = OutputParameter( Name="Heat Extracted", UnitType=Units.ENERGY, PreferredUnits=EnergyUnit.KWH, CurrentUnits=EnergyUnit.KWH ) self.FirstYearElectricityProduction = self.OutputParameterDict[ self.FirstYearElectricityProduction.Name] = OutputParameter( Name="Electricity Produced in the First Year", UnitType=Units.ENERGY, PreferredUnits=EnergyUnit.KWH, CurrentUnits=EnergyUnit.KWH ) self.AveInstNetElectricityProduction = self.OutputParameterDict[ self.AveInstNetElectricityProduction.Name] = OutputParameter( Name="Average Net Daily Electricity Production", UnitType=Units.POWER, PreferredUnits=PowerUnit.KW, CurrentUnits=PowerUnit.KW ) self.FirstYearHeatProduction = self.OutputParameterDict[self.FirstYearHeatProduction.Name] = OutputParameter( Name="Heat Produced in the First Year", UnitType=Units.ENERGY, PreferredUnits=EnergyUnit.KWH, CurrentUnits=EnergyUnit.KWH ) self.AveInstHeatProduction = self.OutputParameterDict[self.AveInstHeatProduction.Name] = OutputParameter( Name="Average Net Daily Heat Production", UnitType=Units.POWER, PreferredUnits=PowerUnit.KW, CurrentUnits=PowerUnit.KW ) self.AveProductionPressure = self.OutputParameterDict[self.AveProductionPressure.Name] = OutputParameter( Name="Average Production Pressure", UnitType=Units.PRESSURE, PreferredUnits=PressureUnit.BAR, CurrentUnits=PressureUnit.BAR ) self.AveProductionTemperature = self.OutputParameterDict[self.AveProductionTemperature.Name] = OutputParameter( Name="Average Production Temperature", UnitType=Units.TEMPERATURE, PreferredUnits=TemperatureUnit.CELSIUS, CurrentUnits=TemperatureUnit.CELSIUS ) model.logger.info("complete " + str(__class__) + ": " + self.__init__.__name__) def __str__(self): return "SurfacePlantAGS"
[docs] def read_parameters(self, model: Model) -> None: """ The read_parameters function reads in the parameters from a dictionary and stores them in the parameters. It also handles special cases that need to be handled after a value has been read in and checked. If you choose to subclass this master class, you can also choose to override this method (or not), and if you do :param model: The container class of the application, giving access to everything else, including the logger :type model: :class:`~geophires_x.Model.Model` :return: None """ model.logger.info("Init " + str(__class__) + ": " + self.read_parameters.__name__) super().read_parameters(model) # 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 len(model.InputParameters) > 0: # loop through all the parameters that the user wishes to set, looking for parameters that match this object for item in self.ParameterDict.items(): ParameterToModify = item[1] key = ParameterToModify.Name.strip() if key in model.InputParameters: ParameterReadIn = model.InputParameters[key] # handle special cases if ParameterToModify.Name == "End-Use Option": ParameterToModify.value = EndUseOptions.from_input_string(ParameterReadIn.sValue) else: model.logger.info("No parameters read because no content provided") # inputs we already have - needs to be set at ReadParameter time so values set at the latest possible time self.End_use = model.surfaceplant.enduse_option.value # same units are GEOPHIRES self.Pump_efficiency = model.surfaceplant.pump_efficiency.value # same units are GEOPHIRES self.Lifetime = int(model.surfaceplant.plant_lifetime.value) # same units are GEOPHIRES self.T0 = model.surfaceplant.ambient_temperature.value + 273.15 # convert Celsius to Kelvin self.Discount_rate = model.economics.discountrate.value # same units are GEOPHIRES # initialize some arrays self.HeatkWhProduced.value = np.zeros(model.surfaceplant.plant_lifetime.value) # initialize the array self.HeatkWhExtracted.value = np.zeros(model.surfaceplant.plant_lifetime.value) # initialize the array self.PumpingkWh.value = np.zeros(model.surfaceplant.plant_lifetime.value) # initialize the array model.logger.info("complete " + str(__class__) + ": " + self.read_parameters.__name__)
[docs] 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 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}') self.error = 0 errors = [] def on_invalid_parameter_value(err_msg): errors.append(err_msg) print(err_msg) model.logger.fatal(err_msg) self.error = 1 if self.T0 < 278.15 or self.T0 > 303.15: on_invalid_parameter_value("Error: CLGS model database imposes additional range restrictions: Dead-state temperature must be \ between 278.15 and 303.15 K. Simulation terminated.") if self.P0.value < 0.8e5 or self.P0.value > 1.1e5: on_invalid_parameter_value("Error: CLGS model database imposes additional range restrictions: Dead state pressure must be \ between 0.8e5 and 1.1e5 Pa. Simulation terminated.") if self.Pump_efficiency < 0.5 or self.Pump_efficiency > 1: on_invalid_parameter_value("Error: CLGS model database imposes additional range restrictions: Pump efficiency must be \ between 0.5 and 1. Simulation terminated.") if self.Lifetime < 5 or self.Lifetime > 40: on_invalid_parameter_value("Error: CLGS model database imposes additional range restrictions: System lifetime must be \ between 5 and 40 years. Simulation terminated.") if not isinstance(self.Lifetime, int): on_invalid_parameter_value("Error: CLGS model database imposes additional range restrictions: System lifetime must be \ an integer. Simulation terminated.") if self.Turbine_isentropic_efficiency.value < 0.8 or self.Turbine_isentropic_efficiency.value > 1: on_invalid_parameter_value("Error: CLGS model database imposes additional range restrictions: Turbine isentropic efficiency must \ be between 0.8 and 1. Simulation terminated.") if self.Generator_efficiency.value < 0.8 or self.Generator_efficiency.value > 1: on_invalid_parameter_value("Error: CLGS model database imposes additional range restrictions: Generator efficiency must be \ between 0.8 and 1. Simulation terminated.") if self.Compressor_isentropic_efficiency.value < 0.8 or self.Compressor_isentropic_efficiency.value > 1: on_invalid_parameter_value("Error: CLGS model database imposes additional range restrictions: Compressor isentropic efficiency \ must be between 0.8 and 1. Simulation terminated.") if self.Pre_Cooling_Delta_T.value < 0 or self.Pre_Cooling_Delta_T.value > 15: on_invalid_parameter_value("Error: CLGS model database imposes additional range restrictions: CO2 temperature decline after turbine \ and before compressor must be between 0 and 15 degrees C. Simulation terminated.") if self.Turbine_outlet_pressure.value < 75 or self.Turbine_outlet_pressure.value > 200: on_invalid_parameter_value("Error: CLGS model database imposes additional range restrictions: Turbine outlet pressure must be \ between 75 and 200 bar. Simulation terminated.") if self.error > 0: subErrors = '\n'.join(errors) msg = f'Error: GEOPHIRES failed to validate CLGS surfaceplant input value(s):\n{subErrors}' print(msg) raise RuntimeError(msg) model.logger.info(f'complete {str(__class__)}: {sys._getframe().f_code.co_name}') return self.error
[docs] def calculatepumpingpower(self, model): """ 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
[docs] 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, np.dstack((0.5 * self.P_in + 0.5 * self.Linear_production_pressure, 0.5 * model.wellbores.Tinj.value + 0.5 * self.Linear_production_temperature + 273.15))[0]) self.hprod = interpn((model.wellbores.Pvector, model.wellbores.Tvector), model.wellbores.enthalpy, np.dstack((self.Linear_production_pressure, self.Linear_production_temperature + 273.15))[ 0]) self.hinj = interpn((model.wellbores.Pvector, model.wellbores.Tvector), model.wellbores.enthalpy, np.array([self.P_in, model.wellbores.Tinj.value + 273.15])) self.Instantaneous_heat_production = model.wellbores.prodwellflowrate.value * ( self.hprod - self.hinj) / 1000 # Heat production based on produced minus injected enthalpy [kW] # Calculate annual heat production (kWh) self.Annual_heat_production = 8760 / 5 * (self.Instantaneous_heat_production[0::4][0:-1] + self.Instantaneous_heat_production[1::4] + self.Instantaneous_heat_production[2::4] + self.Instantaneous_heat_production[3::4] + self.Instantaneous_heat_production[4::4]) # Calculate average heat production self.AveAnnualHeatProduction = np.average(self.Annual_heat_production) # kWh self.AveInstHeatProduction.value = np.average(self.Instantaneous_heat_production) # kWth # Calculate average heat production and first year heat production self.Average_heat_production = np.average(self.Instantaneous_heat_production) # [kW] # Average_production_temperature = np.average(Linear_production_temperature) #[deg.C] self.FirstYearHeatProduction.value = self.Annual_heat_production[0] # kWh self.calculatepumpingpower(model)
[docs] def calculateelectricityproduction(self, model): """ 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] # produced entropy [J/kg/K] self.s_prod = interpn((model.wellbores.Pvector, model.wellbores.Tvector), model.wellbores.entropy, np.dstack((self.Linear_production_pressure, self.Linear_production_temperature + 273.15))[ 0]) self.s_inj = interpn((model.wellbores.Pvector, model.wellbores.Tvector), model.wellbores.entropy, np.array([self.P_in, model.wellbores.Tinj.value + 273.15])) # injected entropy [J/kg/K] self.Instantaneous_exergy_production = (model.wellbores.prodwellflowrate.value * ( self.h_prod - self.h_0 - model.surfaceplant.T0 * (self.s_prod - self.s_0))) / 1000 # [kW] self.Instantaneous_exergy_extraction = (model.wellbores.prodwellflowrate.value * ( self.h_prod - self.h_inj - model.surfaceplant.T0 * (self.s_prod - self.s_inj))) / 1000 # [kW] self.AverageInstNetExergyProduction = np.average(self.Instantaneous_exergy_production) # [kW] 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: # 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) self.Instantaneous_electricity_production_method_3 = self.Instantaneous_heat_production * self.Instantaneous_thermal_efficiency # [kW] else: # Water injection temperature and/or production temperature fall outside the range used in the correlations self.error_codes = np.append(self.error_codes, 2000) self.Instantaneous_utilization_efficiency_method_1 = np.zeros(len(self.Time_array)) self.Instantaneous_electricity_production_method_1 = np.zeros(len(self.Time_array)) self.Instantaneous_thermal_efficiency = np.zeros(len(self.Time_array)) self.Instantaneous_electricity_production_method_3 = np.zeros(len(self.Time_array)) # 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.Inst_electricity_production = self.Instantaneous_electricity_production_method_1 # kW self.AveInstElectricityProduction = np.average(self.Instantaneous_electricity_production_method_1) # kW else: T_prod = self.Linear_production_temperature # Production temperature [deg.C] 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]) 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] self.T_turbine_out_actual = interpn((model.wellbores.Pvector_ap, model.wellbores.hvector_ap), model.wellbores.TPh, np.dstack( (np.ones(self.TNOP) * self.Turbine_outlet_pressure.value * 1e5, h_turbine_out_actual))[0]) - 273.15 if min(self.T_turbine_out_actual) > 37 and model.wellbores.Tinj.value > 32: self.Pre_cooling_temperature = min(self.T_turbine_out_actual) - self.Pre_Cooling_Delta_T.value Post_cooling = 2000 valuefound = 0 lastrun = 0 while valuefound == 0: Pre_compressor_h = interpn((model.wellbores.Pvector, model.wellbores.Tvector), model.wellbores.enthalpy, np.array( [self.Turbine_outlet_pressure.value * 1e5, self.Pre_cooling_temperature + 273.15])) # Pre-compressor cooling [kWth] Pre_cooling = model.wellbores.prodwellflowrate.value * ( 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])) Post_compressor_h_ideal = interpn((model.wellbores.Pvector_ap, model.wellbores.svector_ap), 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 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 # Fluid cooling after compression [kWth] Post_cooling = model.wellbores.prodwellflowrate.value * ( Post_compressor_h_actual - self.h_inj) / 1e3 if lastrun == 0: if self.Pre_cooling_temperature < 32: lastrun = 1 self.Pre_cooling_temperature = self.Pre_cooling_temperature + 0.5 elif Post_cooling < 0: self.Pre_cooling_temperature = self.Pre_cooling_temperature + 0.5 lastrun = 1 elif Post_cooling > 0: self.Pre_cooling_temperature = self.Pre_cooling_temperature - 0.5 elif lastrun == 1: valuefound = 1 print(self.Pre_cooling_temperature - min(self.T_turbine_out_actual)) if Post_cooling < 0: ResistiveHeating = -Post_cooling Post_cooling = 0 else: ResistiveHeating = 0 Total_cooling = Pre_cooling + Post_cooling # Total CO2 cooling requirements [kWth] T_air_in_pre_cooler = self.T0 - 273.15 # Air outlet temperature in pre-cooler [deg.C] T_air_out_pre_cooler = (self.T_turbine_out_actual + self.Pre_cooling_temperature) / 2 # Air specific heat capacity in pre-cooler [J/kg/K] cp_air = np.interp(0.5 * T_air_in_pre_cooler + 0.5 * T_air_out_pre_cooler, self.Tair_for_cp_array, self.cp_air_array) # Air flow rate in pre-cooler [kg/s] m_air_pre_cooler = Pre_cooling * 1000 / (cp_air * (T_air_out_pre_cooler - T_air_in_pre_cooler)) T_air_in_post_cooler = self.T0 - 273.15 # Air outlet temperature in post-cooler [deg.C] T_air_out_post_cooler = (self.Post_compressor_T_actual + model.wellbores.Tinj.value) / 2 cp_air = np.interp(0.5 * T_air_in_post_cooler + 0.5 * T_air_out_post_cooler, self.Tair_for_cp_array, self.cp_air_array) # Air specific heat capacity in post-cooler [J/kg/K] # Air flow rate in post-cooler [kg/s] m_air_post_cooler = Post_cooling * 1000 / (cp_air * (T_air_out_post_cooler - T_air_in_post_cooler)) # Electricity for air-cooling, assuming 0.25 kWe per kg/s [kWe] Air_cooling_power = (m_air_pre_cooler + m_air_post_cooler) * 0.25 self.Instantaneous_electricity_production_method_4 = self.Instantaneous_turbine_power * \ self.Generator_efficiency.value - \ Compressor_Work - \ Air_cooling_power - ResistiveHeating self.Inst_electricity_production = self.Instantaneous_electricity_production_method_4 # [kW] self.Annual_electricity_production = 8760 / 5 * ( self.Instantaneous_electricity_production_method_4[0::4][0:-1] + self.Instantaneous_electricity_production_method_4[1::4] + self.Instantaneous_electricity_production_method_4[2::4] + self.Instantaneous_electricity_production_method_4[3::4] + self.Instantaneous_electricity_production_method_4[4::4]) self.AveInstElectricityProduction = np.average(self.Instantaneous_electricity_production_method_4) # kW # check if negative if min(self.Instantaneous_electricity_production_method_4) < 0: self.error_codes = np.append(self.error_codes, 5500) # Calculated electricity generation is negative self.Annual_electricity_production = np.zeros(self.Lifetime) self.Inst_electricity_production = np.zeros(self.TNOP) self.AveInstElectricityProduction = 0 else: # turbine outlet or reinjection temperature too low if model.wellbores.Tinj.value <= 32: self.error_codes = np.append(self.error_codes, 3000) if min(self.T_turbine_out_actual) <= 37: self.error_codes = np.append(self.error_codes, 4000) self.Annual_electricity_production = np.zeros(self.Lifetime) self.Inst_electricity_production = np.zeros(self.TNOP) self.AveInstElectricityProduction = 0 self.calculatepumpingpower(model) self.Average_electricity_production = np.average(self.Annual_electricity_production) / 8760 # [kW] self.AveAnnualElectricityProduction = np.average(self.Annual_electricity_production) # [kWh] self.AveInstNetElectricityProduction.value = self.AveInstElectricityProduction - np.average( self.PumpingPower) # [kW] if self.AveInstNetElectricityProduction.value < 0: self.AveInstNetElectricityProduction.value = 0 self.AveAnnualNetElectricityProduction = self.AveAnnualElectricityProduction - np.average( self.Annual_pumping_power) # kWh self.FirstYearElectricityProduction.value = self.Annual_electricity_production[0] # kWh self.Inst_Net_Electricity_production = self.Inst_electricity_production - self.PumpingPower # [kW]
[docs] def initialize(self, model: Model) -> None: """ The initialize function reads values and arrays to be in the format that AGS 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) self.Time_array = np.linspace(0, self.Lifetime * 365 * 24 * 3600, 1 + self.Lifetime * self.Number_of_points_per_year) # [s] self.Linear_time_distribution = self.Time_array / 365 / 24 / 3600 self.TNOP = (self.Lifetime * self.Number_of_points_per_year + 1) # Total number of points for selected lifetime # Find the closest lifetime closestlifetime = model.wellbores.timearray.flat[np.abs(model.wellbores.timearray - self.Lifetime).argmin()] self.indexclosestlifetime = np.where(model.wellbores.timearray == closestlifetime)[0][0] # Initialize heat/electricity arrays self.Instantaneous_production_enthalpy = np.zeros(len(self.Time_array)) self.Instantaneous_temperature_after_isenthalpic_throttling = np.zeros(len(self.Time_array)) self.Instantaneous_heat_production = np.zeros(len(self.Time_array)) self.Annual_heat_production = np.zeros(self.Lifetime) self.Annual_pumping_power = np.zeros(self.Lifetime) self.Average_fluid_density = np.zeros(len(self.Time_array)) if self.End_use == EndUseOptions.ELECTRICITY: # Define ORC power plant conversion efficiencies # Linear correlation assumed here based on GEOPHIRES ORC correlation between 100 and 200 deg C [deg.C] plus plateaued above 200 deg. C self.Utilization_efficiency_correlation_temperatures = np.array([100, 200, 385]) # Efficiency of ORC conversion from production exergy to electricity based on GEOPHIRES correlation [-] self.Utilization_efficiency_correlation_conversion = np.array([0.2, 0.45, 0.45]) # Linear correlation based on Chad Augustine's thesis [deg.C] plus plateaued above 200 deg. C self.Heat_to_power_efficiency_correlation_temperatures = np.array([100, 200, 385]) # Conversion from enthalpy to electricity [-] self.Heat_to_power_efficiency_correlation_conversion = np.array([0.05, 0.14, 0.14]) # Produced exergy only (independent of injection conditions) self.Instantaneous_exergy_production = np.zeros(len(self.Time_array)) # Difference between produced exergy and injected exergy self.Instantaneous_exergy_extraction = np.zeros(len(self.Time_array)) # based on exergy produced (only for water) self.Instantaneous_electricity_production_method_1 = np.zeros(len(self.Time_array)) # based on exergy extracted self.Instantaneous_electricity_production_method_2 = np.zeros(len(self.Time_array)) # based on thermal efficiency self.Instantaneous_electricity_production_method_3 = np.zeros(len(self.Time_array)) # based on direct turbine expansion (for CO2) self.Instantaneous_electricity_production_method_4 = np.zeros(len(self.Time_array)) # conversion from produced exergy to electricity self.Instantaneous_utilization_efficiency_method_1 = np.zeros(len(self.Time_array)) # conversion from extracted exergy to electricity self.Instantaneous_utilization_efficiency_method_2 = np.zeros(len(self.Time_array)) # conversion from enthalpy to electricity self.Instantaneous_thermal_efficiency = np.zeros(len(self.Time_array)) self.Annual_electricity_production = np.zeros(self.Lifetime) if model.wellbores.Fluid.value == WorkingFluid.SCO2.value: # Direct turbine expansion considered for systems using sCO2 self.Instantaneous_turbine_power = np.zeros(len(self.Time_array)) # Calculate dead-state enthalpy and entropy in case of electricity production if self.End_use == EndUseOptions.ELECTRICITY: self.h_0 = interpn((model.wellbores.Pvector, model.wellbores.Tvector), model.wellbores.enthalpy, np.array([model.surfaceplant.P0.value, model.surfaceplant.T0]))[ 0] # dead-state enthalpy [J/kg] self.s_0 = interpn((model.wellbores.Pvector, model.wellbores.Tvector), model.wellbores.entropy, np.array([model.surfaceplant.P0.value, model.surfaceplant.T0]))[ 0] # dead-state entropy [J/kg/K] # Pre-populate specific heat capacity of air in case of electricity production if self.End_use == EndUseOptions.ELECTRICITY: self.Tair_for_cp_array = np.linspace(0, 100, num=10) self.cp_air_array = np.array([1005.65818063, 1005.87727966, 1006.19281999, 1006.60616167, 1007.11890862, 1007.73265999, 1008.44882744, 1009.26850304, 1010.19236691, 1011.2206266]) # Initialize error code self.error_codes = np.zeros(0) # if error occurs, code will be assigned to this tag model.logger.info("complete " + str(__class__) + ": " + sys._getframe().f_code.co_name)
[docs] 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(f'Init {__class__!s}: {sys._getframe().f_code.co_name}') err = self.verify(model) if err > 0: msg = 'Error: GEOPHIRES failed to validate CLGS input value. Exiting...' model.logger.fatal(msg) raise RuntimeError(msg) self.initialize(model) self.Linear_production_temperature = model.wellbores.InterpolatedTemperatureArray self.Linear_production_pressure = model.wellbores.InterpolatedPressureArray self.AveProductionTemperature.value = np.average(self.Linear_production_temperature) self.AveProductionPressure.value = np.average(self.Linear_production_pressure) / 1e5 # [bar] if min(self.Linear_production_temperature) > model.wellbores.Tinj.value: self.calculateheatproduction(model) if self.End_use == EndUseOptions.ELECTRICITY: self.calculateelectricityproduction(model) else: # Production temperature went below injection temperature self.error_codes = np.append(self.error_codes, 1000) # Now transfer the results to the GEOPHIRES-X arrays: Deep Copy the Arrays model.wellbores.ProducedTemperature.value = self.Linear_production_temperature.copy() self.TenteringPP.value = model.wellbores.ProducedTemperature.value model.wellbores.PumpingPower.value = self.Annual_pumping_power.copy() self.HeatExtracted.value = self.Instantaneous_heat_production.copy() # convert to MW because that is what GEOPHIRES expects self.HeatExtracted.value = self.HeatExtracted.value / 1000.0 # useful direct-use heat provided to application [MWth] self.HeatProduced.value = self.HeatExtracted.value * self.enduseefficiencyfactor.value for i in range(0, self.plant_lifetime.value): self.HeatkWhExtracted.value[i] = np.trapz(self.HeatExtracted.value[ (i * model.economics.timestepsperyear.value):(( i + 1) * model.economics.timestepsperyear.value) + 1], dx=1. / model.economics.timestepsperyear.value * 365. * 24.) * 1000. * self.utilization_factor.value self.PumpingkWh.value[i] = np.trapz(model.wellbores.PumpingPower.value[ (i * model.economics.timestepsperyear.value):(( i + 1) * model.economics.timestepsperyear.value) + 1], dx=1. / model.economics.timestepsperyear.value * 365. * 24.) * 1000. * self.utilization_factor.value self.RemainingReservoirHeatContent.value = model.reserv.InitialReservoirHeatContent.value - np.cumsum( self.HeatkWhExtracted.value) * 3600 * 1E3 / 1E15 if self.End_use is not EndUseOptions.ELECTRICITY: self.HeatkWhProduced.value = np.zeros(self.plant_lifetime.value) for i in range(0, self.plant_lifetime.value): self.HeatkWhProduced.value[i] = np.trapz(self.HeatProduced.value[ (0 + i * model.economics.timestepsperyear.value):(( i + 1) * model.economics.timestepsperyear.value) + 1], dx=1. / model.economics.timestepsperyear.value * 365. * 24.) * 1000. * self.utilization_factor.value else: # copy some arrays so we have a GEOPHIRES equivalent self.TotalkWhProduced.value = self.Annual_electricity_production.copy() self.ElectricityProduced.value = self.Annual_electricity_production.copy() / 8760.0 / 1000.0 f = interp1d(np.arange(0, len(self.ElectricityProduced.value)), self.ElectricityProduced.value, fill_value="extrapolate") self.ElectricityProduced.value = f(np.arange(0, 40, 1.0)) self.NetElectricityProduced.value = self.Inst_Net_Electricity_production.copy() # covert to MW, which is what GEOPHIRES expects self.NetElectricityProduced.value = self.NetElectricityProduced.value / 1000.0 f = interp1d(np.arange(0, len(self.NetElectricityProduced.value)), self.NetElectricityProduced.value, fill_value="extrapolate") 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 # handle errors if len(self.error_codes) > 0: base_msg = f'failed with the following error codes: {str(self.error_codes[0:])}' class_file_info_msg = f'{base_msg} in {str(__class__)} {os.path.abspath(__file__)}' model.logger.fatal(class_file_info_msg) print(f'Error: {class_file_info_msg}. Exiting....') raise RuntimeError(base_msg) model.logger.info(f"complete {str(__class__)}: {sys._getframe().f_code.co_name}")