Source code for geophires_x.WellBores

import math
import numpy as np
from pint.facets.plain import PlainQuantity

from .Parameter import floatParameter, intParameter, boolParameter, OutputParameter, ReadParameter, \
    coerce_int_params_to_enum_values
from geophires_x.GeoPHIRESUtils import vapor_pressure_water_kPa, quantity, static_pressure_MPa
from geophires_x.GeoPHIRESUtils import density_water_kg_per_m3
from geophires_x.GeoPHIRESUtils import viscosity_water_Pa_sec
from .Units import *
import geophires_x.Model as Model
from .OptionList import ReservoirModel, Configuration, WorkingFluid


# code from Koenraad


[docs] def calculate_total_drilling_lengths_m(Configuration, numnonverticalsections: int, nonvertical_length_km: float, InputDepth_km: float, OutputDepth_km: float, nprod:int, ninj:int, junction_depth_km: float = 0.0, angle_rad: float = 0.0) -> tuple: """ returns the total length, vertical length, and non-vertical lengths, depending on the configuration :param Configuration: configuration of the well :type Configuration: :class:`~geophires :param numnonverticalsections: number of non-vertical sections :type numnonverticalsections: int :param nonvertical_length_km: length of non-vertical sections in km :type nonvertical_length_km: float :param InputDepth_km: depth of the well in km :type InputDepth_km: float :param OutputDepth_km: depth of the output end of the well in km, if U shaped, and not horizontal :type OutputDepth_km: float :param nprod: number of production wells :type nprod: int :param ninj: number of injection wells :param junction_depth_km: depth of the junction in km :type junction_depth_km: float :param angle_rad: angle of the well in radians, from horizontal :type angle_rad: float :return: total length, vertical length, lateral, and junction lengths in meters :rtype: tuple """ tot_pipe_length_m = vertical_pipe_length_m = lateral_pipe_length_m = tot_to_junction_m = 0.0 if Configuration is Configuration.ULOOP: # Total drilling depth of both wells and laterals in U-loop [m] vertical_pipe_length_m = (nprod * InputDepth_km * 1000.0) + (ninj * OutputDepth_km * 1000.0) lateral_pipe_length_m = numnonverticalsections * nonvertical_length_km * 1000.0 elif Configuration is Configuration.COAXIAL: # Total drilling depth of well and lateral in co-axial case [m] - is not necessarily only vertical vertical_pipe_length_m = (nprod + ninj) * InputDepth_km * 1000.0 lateral_pipe_length_m = numnonverticalsections * nonvertical_length_km * 1000.0 elif Configuration is Configuration.VERTICAL: # Total drilling depth of well in vertical case [m] vertical_pipe_length_m = (nprod + ninj) * InputDepth_km * 1000.0 lateral_pipe_length_m = 0.0 elif Configuration is Configuration.L: # Total drilling depth of well in L case [m] vertical_pipe_length_m = (nprod + ninj) * InputDepth_km * 1000.0 lateral_pipe_length_m = numnonverticalsections * nonvertical_length_km * 1000.0 elif Configuration is Configuration.EAVORLOOP: # Total drilling length of well in EavorLoop [m] vertical_pipe_length_m = (nprod + ninj) * InputDepth_km * 1000.0 # now calculate the distance from the bottom of the vertical to the junction of the laterals [m] O1 = (junction_depth_km - InputDepth_km) * 1000.0 # in meters tot_to_junction_m = (O1 / math.sin(angle_rad)) * 2 # there are two of these of each EavorLoop # now calculate the distance from the junction of the laterals to the end of the laterals [m] O2 = (OutputDepth_km - junction_depth_km) * 1000.0 # in meters lateral_pipe_length_m = (O2 / math.sin(angle_rad)) * 2 # there are two of these of each lateral of an EavorLoop lateral_pipe_length_m = lateral_pipe_length_m * numnonverticalsections # there are numnonverticalsections of these else: raise ValueError(f'Invalid Configuration: {Configuration}') tot_pipe_length_m = vertical_pipe_length_m + lateral_pipe_length_m + tot_to_junction_m return tot_pipe_length_m, vertical_pipe_length_m, lateral_pipe_length_m, tot_to_junction_m
[docs] def InjectionReservoirPressurePredictor(project_lifetime_yr: int, timesteps_per_year: int, initial_pressure_kPa: float, inflation_rate: float) -> list: """ InjectionReservoirPressurePredictor builds the Injection Reservoir Pressure Array for the project lifetime based on the initial (perhaps hydrostatic) pressure and the inflation rate. There is no limit to how high the pressure can go. :param project_lifetime_yr: The lifetime of the project in years :type project_lifetime_yr: int :param timesteps_per_year: The number of timesteps per year :type timesteps_per_year: int :param initial_pressure_kPa: The initial pressure in kPa :type initial_pressure_kPa: float :param inflation_rate: The inflation rate in %/yr :type inflation_rate: float :return: pressure: The pressure array as a function of time. :rtype: list """ # Initialize the pressure array with the initial hydrostatic pressure pressure = [initial_pressure_kPa] * project_lifetime_yr * timesteps_per_year # If the overpressure percentage is 0, # return the hydrostatic pressure array as a constant value equal to the initial hydrostatic pressure if inflation_rate == 0: return pressure # Calculate the initial pressure pressure[0] = initial_pressure_kPa pressure_change_per_timestep = inflation_rate / timesteps_per_year for current_timestep in range(1, project_lifetime_yr * timesteps_per_year): pressure[current_timestep] = initial_pressure_kPa + (pressure_change_per_timestep * current_timestep) return pressure
[docs] def ReservoirPressurePredictor(project_lifetime_yr: int, timesteps_per_year: int, initial_pressure_kPa: float, overpressure_percentage: float, depletion_rate: float) -> list: """ ReservoirPressurePredictor builds the Reservoir Pressure Array for the project lifetime based on the initial (likely hydrostatic) pressure. the overpressure percentage and the depletion rate. Don't let the pressure drop below the initial pressure. :param project_lifetime_yr: The lifetime of the project in years :type project_lifetime_yr: int :param timesteps_per_year: The number of timesteps per year :type timesteps_per_year: int :param initial_pressure_kPa: The hydrostatic pressure in kPa :type initial_pressure_kPa: float :param overpressure_percentage: The overpressure percentage in % :type overpressure_percentage: float :param depletion_rate: The depletion rate in %/yr :type depletion_rate: float :return: hydrostatic_pressure: The hydrostatic pressure array as a function of time. :rtype: list """ # Initialize the hydrostatic pressure array with the initial hydrostatic pressure pressure = [initial_pressure_kPa] * project_lifetime_yr * timesteps_per_year # If the overpressure percentage is 100, # return the hydrostatic pressure array as a constant value equal to the initial pressure if overpressure_percentage == 100.0: return pressure # Calculate the initial overpressure pressure[0] = initial_pressure_kPa * (overpressure_percentage / 100) delta_pressure = (pressure[0] - initial_pressure_kPa) depletion_timesteps = int((100.0 / depletion_rate) * timesteps_per_year) pressure_change_per_timestep = delta_pressure / depletion_timesteps for timestep in range(1, project_lifetime_yr * timesteps_per_year): pressure[timestep] = pressure[0] - (pressure_change_per_timestep * timestep) if pressure[timestep] < initial_pressure_kPa: # If the pressure drops below the hydrostatic pressure, set it to the hydrostatic pressure and break out pressure[timestep] = initial_pressure_kPa break return pressure
[docs] def RameyCalc(krock: float, rhorock: float, cprock: float, welldiam: float, tv, utilfactor: float, flowrate: float, cpwater: float, Trock: float, Tresoutput: float, averagegradient: float, depth: float) -> float: """ Calculate the temperature drop along the length of a well this code is only valid so far for 1 gradient and deviation = 0 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: 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) framey = np.zeros(alen) framey[1:] = -np.log( 1.1 * (welldiam / 2.0) / np.sqrt(4. * alpharock * tv[1:] * 365.0 * 24.0 * 3600.0 * utilfactor)) - 0.29 framey[0] = framey[1] # fource the first value to be the same as the second to get away from near surface effects rameyA = flowrate * cpwater * framey / 2 / math.pi / krock TempDrop = -((Trock - Tresoutput) - averagegradient * (depth - rameyA) + ( Tresoutput - averagegradient * rameyA - Trock) * np.exp(-depth / rameyA)) return TempDrop
[docs] 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 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 :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) rhowater = np.array([ density_water_kg_per_m3( t, pressure=model.reserv.hydrostatic_pressure(), ) for t in Taverage ]) # replace with correlation based on Tprodaverage muwater = np.array([ viscosity_water_Pa_sec( t, pressure=model.reserv.hydrostatic_pressure(), ) for t in Taverage ]) # replace with correlation based on Tprodaverage v = wellflowrate / rhowater / (math.pi / 4. * welldiam ** 2) Rewater = 4.0 * wellflowrate / (muwater * math.pi * welldiam) # laminar or turbulent flow? Rewateraverage = np.average(Rewater) if Rewateraverage < 2300.0: f3 = 64. / Rewater else: relroughness = 1E-4 / welldiam # 6 iterations to converge f3 = 1. / np.power(-2 * np.log10(relroughness / 3.7 + 5.74 / np.power(Rewater, 0.9)), 2.) f3 = 1. / np.power((-2 * np.log10(relroughness / 3.7 + 2.51 / Rewater / np.sqrt(f3))), 2.) f3 = 1. / np.power((-2 * np.log10(relroughness / 3.7 + 2.51 / Rewater / np.sqrt(f3))), 2.) f3 = 1. / np.power((-2 * np.log10(relroughness / 3.7 + 2.51 / Rewater / np.sqrt(f3))), 2.) f3 = 1. / np.power((-2 * np.log10(relroughness / 3.7 + 2.51 / Rewater / np.sqrt(f3))), 2.) f3 = 1. / np.power((-2 * np.log10(relroughness / 3.7 + 2.51 / Rewater / np.sqrt(f3))), 2.) if impedancemodelused: # assumed everything stays liquid throughout; /1E3 to convert from Pa to kPa DPWell = f3 * (rhowater * v ** 2 / 2.) * (depth / welldiam) / 1E3 else: DPWell = [] # it will be calculated elsewhere return DPWell, f3, v, rhowater
[docs] def InjectionWellPressureDrop(model: Model, Taverage: float, wellflowrate: float, welldiam: 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 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 :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) rhowater = (density_water_kg_per_m3(Taverage, pressure=model.reserv.hydrostatic_pressure()) * np.linspace(1, 1, len(model.wellbores.ProducedTemperature.value))) # replace with correlation based on Tinjaverage muwater = viscosity_water_Pa_sec(Taverage, pressure=model.reserv.hydrostatic_pressure()) * np.linspace(1, 1, len(model.wellbores.ProducedTemperature.value)) v = nprod / ninj * wellflowrate * (1.0 + waterloss) / rhowater / (math.pi / 4. * welldiam ** 2) Rewater = 4. * nprod / ninj * wellflowrate * (1.0 + waterloss) / ( muwater * math.pi * welldiam) # laminar or turbulent flow? Rewateraverage = np.average(Rewater) if Rewateraverage < 2300.: # laminar flow f1 = 64. / Rewater else: # turbulent flow relroughness = 1E-4 / welldiam # 6 iterations to converge f1 = 1. / np.power(-2 * np.log10(relroughness / 3.7 + 5.74 / np.power(Rewater, 0.9)), 2.) f1 = 1. / np.power((-2 * np.log10(relroughness / 3.7 + 2.51 / Rewater / np.sqrt(f1))), 2.) f1 = 1. / np.power((-2 * np.log10(relroughness / 3.7 + 2.51 / Rewater / np.sqrt(f1))), 2.) f1 = 1. / np.power((-2 * np.log10(relroughness / 3.7 + 2.51 / Rewater / np.sqrt(f1))), 2.) f1 = 1. / np.power((-2 * np.log10(relroughness / 3.7 + 2.51 / Rewater / np.sqrt(f1))), 2.) f1 = 1. / np.power((-2 * np.log10(relroughness / 3.7 + 2.51 / Rewater / np.sqrt(f1))), 2.) if impedancemodelused: # assumed everything stays liquid throughout, calculate well pressure drop [kPa]; /1E3 to convert from Pa to kPa DPWell = f1 * (rhowater * v ** 2 / 2) * (depth / welldiam) / 1E3 else: DPWell = [] # it will be calculated elsewhere return DPWell, f1, v, rhowater
[docs] def ProdPressureDropsAndPumpingPowerUsingImpedenceModel(f3: float, vprod: float, rhowaterinj: float, rhowaterprod: float, rhowaterreservoir: float, depth: float, wellflowrate: float, prodwelldiam: float, impedance: float, nprod: int, waterloss: float, pumpeff: float, tilt: float = 90.0, trim_neg_to_zero: bool = True) -> tuple: """ Calculate Pressure Drops and Pumping Power needed for the production well using the Impedance Model :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 :param tilt: tilt of the well from the junction box to the bottom of the laterals [degrees] :type tilt: float :param trim_neg_to_zero: whether to trim negative values of pumping power to zero :type trim_neg_to_zero: bool :return: tuple of DPOverall, PumpingPower, DPProdWell, DPReserv, DPBouyancy :rtype: tuple """ # production well pressure drops [kPa] DPProdWell = f3 * (rhowaterprod * vprod ** 2 / 2.) * (depth / prodwelldiam) / 1E3 # /1E3 to convert from Pa to kPa # reservoir pressure drop [kPa] DPReserv = impedance * nprod * wellflowrate * 1000. / rhowaterreservoir # buoyancy pressure drop [kPa] gravity_tilt = 9.81 * np.sin(np.radians(tilt)) DPBouyancy = (rhowaterprod - rhowaterinj) * depth * gravity_tilt / 1E3 # /1E3 to convert from Pa to kPa # overall pressure drop DPOverall = DPReserv + DPProdWell + DPBouyancy # calculate pumping power [MWe] (approximate) PumpingPower = ([0.0] * len(vprod)) PumpingPower = DPOverall * nprod * wellflowrate * (1 + waterloss) / rhowaterinj / pumpeff / 1E3 # in GEOPHIRES v1.2, negative pumping power values become zero (b/c we are not generating electricity) if trim_neg_to_zero: PumpingPower = [0. if x < 0. else x for x in PumpingPower] return DPOverall, PumpingPower, DPProdWell, DPReserv, DPBouyancy
[docs] def InjPressureDropsAndPumpingPowerUsingImpedenceModel(f1: float, vinj: float, rhowaterinj: float, depth: float, wellflowrate: float, injwelldiam: float, ninj: int, waterloss: float, pumpeff: float, DPOverall) -> tuple: """ Calculate Injection well Pressure Drops and Pumping Power needed for the injection well using the Impedance Model :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] DPInjWell = f1 * (rhowaterinj * vinj ** 2 / 2) * (depth / injwelldiam) / 1E3 # /1E3 to convert from Pa to kPa # overall pressure drop newDPOverall = DPOverall + DPInjWell # calculate pumping power [MWe] (approximate) PumpingPower = newDPOverall * ninj * wellflowrate * (1 + waterloss) / rhowaterinj / pumpeff / 1E3 # in GEOPHIRES v1.2, negative pumping power values become zero (b/c we are not generating electricity) PumpingPower = [0. if x < 0. else x for x in PumpingPower] return newDPOverall, PumpingPower, DPInjWell
[docs] def get_hydrostatic_pressure_kPa( Trock_degC: float, Tsurf_degC: float, depth_m: float, gradient_C_per_km: float, lithostatic_pressure: PlainQuantity) -> float: """ Correlation cited as being from Xie, Bloomfield, and Shook in https://workingincaes.inl.gov/SiteAssets/CAES%20Files/FORGE/inl_ext-16-38751%20GETEM%20User%20Manual%20Final.pdf """ CP = 4.64E-7 CT = 9E-4 / (30.796 * Trock_degC ** (-0.552)) return 0 + 1. / CP * (math.exp( density_water_kg_per_m3(Tsurf_degC, pressure=lithostatic_pressure) * 9.81 * CP / 1000 * ( depth_m - CT / 2 * gradient_C_per_km * depth_m ** 2)) - 1)
[docs] def ProdPressureDropAndPumpingPowerUsingIndexes( model: Model, productionwellpumping: bool, usebuiltinppwellheadcorrelation: bool, Trock_degC: float, depth_m: float, ppwellhead_kPa: float, PI_kg_per_sec_per_bar: float, wellflowrate_kg_per_sec: float, f3: float, vprod_m: float, prodwelldiam_m: float, nprod: int, pumpeff: float, rhowaterprod_kg_per_m3: float) -> tuple: """ Calculate Pressure Drops and Pumping Power needed for the production well using indexes :param model: The container class of the application, giving access to everything else, including the logger :type model: :class:`~geophires_x.Model.Model` :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_degC: rock temperature [C] :type Trock_degC: float :param depth_m: depth of the well [m] :type depth_m: float :param ppwellhead_kPa: production wellhead pressure [kPa] :type ppwellhead_kPa: float :param PI_kg_per_sec_per_bar: productivity index [kg/s/bar] :type PI_kg_per_sec_per_bar: float :param wellflowrate_kg_per_sec: flow rate of the fluid in the well [kg/s] :type wellflowrate_kg_per_sec: float :param f3: friction factor [-] :type f3: float :param vprod_m: velocity of the fluid in the production well [m/s] :type vprod_m: float :param prodwelldiam_m: diameter of the well [m] :type prodwelldiam_m: float :param nprod: number of production wells [-] :type nprod: int :param pumpeff: pump efficiency [-] :type pumpeff: float :param rhowaterprod_kg_per_m3: density of the water in the production well [kg/m3] :type rhowaterprod_kg_per_m3: 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_m)) # reservoir hydrostatic pressure Phydrostaticcalc_kPa = model.wellbores.production_reservoir_pressure.quantity().to('kPa').magnitude if productionwellpumping: # Excess pressure covers non-condensable gas pressure and net positive suction head for the pump Pexcess_kPa = 344.7 # = 50 psi # Minimum production pump inlet pressure and minimum wellhead pressure if Trock_degC < 373.9: Pminimum_kPa = vapor_pressure_water_kPa(Trock_degC) + Pexcess_kPa else: #above the critical water temperature, vapor no longer occurs and vapor pressure can no longer be calculated. A "dummy" vapor pressure can be assumed as the fluid phase no longer impacts the pump depth. Pminimum_kPa = 100 #setting artificially to 1 bar = 100 kPa if usebuiltinppwellheadcorrelation: Pprodwellhead = Pminimum_kPa # production wellhead pressure [kPa] else: Pprodwellhead = ppwellhead_kPa if Pprodwellhead < Pminimum_kPa: Pprodwellhead = Pminimum_kPa msg = (f'Provided production wellhead pressure ({Pprodwellhead}kPa) ' f'under minimum pressure ({Pminimum_kPa}kPa). ' f'GEOPHIRES will assume minimum wellhead pressure') print(f'Warning: {msg}') model.logger.warning(msg) PI_kPa = PI_kg_per_sec_per_bar / 100.0 # convert PI from kg/s/bar to kg/s/kPa # calculate pumping depth pumpdepth_m = depth_m + (Pminimum_kPa - Phydrostaticcalc_kPa + wellflowrate_kg_per_sec / PI_kPa) / ( f3 * (rhowaterprod_kg_per_m3 * vprod_m ** 2 / 2.) * ( 1 / prodwelldiam_m) / 1E3 + rhowaterprod_kg_per_m3 * 9.81 / 1E3) pumpdepthfinal_m = np.max(pumpdepth_m) if pumpdepthfinal_m < 0.0: pumpdepthfinal_m = 0.0 msg = (f'GEOPHIRES calculates negative production well pumping depth. ({pumpdepthfinal_m:.2f}m)' f'No production well pumps will be assumed') print(f'Warning: {msg}') model.logger.warning(msg) elif pumpdepthfinal_m > 600.0: msg = (f'GEOPHIRES calculates production pump depth to be deeper than 600m ({pumpdepthfinal_m:.2f}m). ' f'Verify reservoir pressure, production well flow rate and production well dimensions') model.logger.warning(msg) # calculate production well pumping pressure [kPa] DPProdWell = Pprodwellhead - ( Phydrostaticcalc_kPa - wellflowrate_kg_per_sec / PI_kPa - rhowaterprod_kg_per_m3 * 9.81 * depth_m / 1E3 - f3 * (rhowaterprod_kg_per_m3 * vprod_m ** 2 / 2.) * (depth_m / prodwelldiam_m) / 1E3) # [MWe] total pumping power for production wells PumpingPowerProd = DPProdWell * nprod * wellflowrate_kg_per_sec / rhowaterprod_kg_per_m3 / pumpeff / 1E3 PumpingPowerProd = np.array([0. if x < 0. else x for x in PumpingPowerProd]) # total pumping power if productionwellpumping: PumpingPower = PumpingPowerProd # negative pumping power values become zero (b/c we are not generating electricity) PumpingPower = [max(x, 0.) for x in PumpingPower] return PumpingPower, PumpingPowerProd, DPProdWell, Pprodwellhead
[docs] def InjPressureDropAndPumpingPowerUsingIndexes( model: Model, productionwellpumping: bool, usebuiltinppwellheadcorrelation: bool, usebuiltinoutletplantcorrelation: bool, Trock_degC: float, depth_m: float, ppwellhead: float, II: float, wellflowrate: float, f1: float, vinj: float, injwelldiam: float, nprod: int, ninj: int, waterloss: float, pumpeff: float, rhowaterinj: float, Pplantoutlet: float) -> tuple: """ Calculate PressureDrops and Pumping Power needed for the injection well using indexes :param depth_m: depth of the well [m] :type depth_m: 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 Trock_degC: rock temperature [C] :type Trock_degC: float :param usebuiltinppwellheadcorrelation: whether to use the built-in wellhead pressure correlation (True or False) [-] :type usebuiltinppwellheadcorrelation: bool :param productionwellpumping: whether the production well is pumping (True or False) [-] :type productionwellpumping: 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 II: injectivity index [kg/s/bar] :type II: float :return: tuple of PumpingPowerInj, DPInjWell, plant_outlet_pressure, Pprodwellhead [kPa] :rtype: tuple """ PumpingPowerInj = DPInjWell = Pprodwellhead = [0.0] # initialize value in case it doesn't get set. # reservoir hydrostatic pressure Phydrostaticcalc_kPa = model.wellbores.injection_reservoir_pressure.value if productionwellpumping: # Excess pressure covers non-condensable gas pressure and net positive suction head for the pump Pexcess_kPa = 344.7 # = 50 psi # Minimum production pump inlet pressure and minimum wellhead pressure if Trock_degC < 373.9: Pminimum_kPa = vapor_pressure_water_kPa(Trock_degC) + Pexcess_kPa else: #above the critical water temperature, vapor no longer occurs and vapor pressure can no longer be calculated. A "dummy" vapor pressure can be assumed as the fluid phase no longer impacts the pump depth. Pminimum_kPa = 100 #setting artificially to 1 bar = 100 kPa if usebuiltinppwellheadcorrelation: Pprodwellhead = Pminimum_kPa # production wellhead pressure [kPa] else: Pprodwellhead = ppwellhead if Pprodwellhead < Pminimum_kPa: Pprodwellhead = Pminimum_kPa msg = (f'Provided production wellhead pressure ({Pprodwellhead}) under minimum pressure ({Pminimum_kPa}). ' f'GEOPHIRES will assume minimum wellhead pressure') print(f'Warning: {msg}') model.logger.warning(msg) IIkPa = II / 100.0 # convert II from kg/s/bar to kg/s/kPa # necessary injection wellhead pressure [kPa] Pinjwellhead = [0] * len(Phydrostaticcalc_kPa) for i in range(len(Phydrostaticcalc_kPa)): Pinjwellhead[i] = (Phydrostaticcalc_kPa[i] + wellflowrate * (1 + waterloss) * nprod / ninj / IIkPa - rhowaterinj[i] * 9.81 * depth_m / 1E3 + f1[i] * (rhowaterinj[i] * vinj[i] ** 2 / 2) * (depth_m / injwelldiam) / 1E3) # plant outlet pressure [kPa] if usebuiltinoutletplantcorrelation: DPSurfaceplant = 68.95 # [kPa] assumes 10 psi pressure drop in surface equipment Pplantoutlet = Pprodwellhead - DPSurfaceplant # injection pump pressure [kPa] DPInjWell = [0.0] * len(Phydrostaticcalc_kPa) for i in range(len(DPInjWell)): DPInjWell[i] = Pinjwellhead[i] - Pplantoutlet # [MWe] total pumping power for injection wells PumpingPowerInj = [0.0] * len(DPInjWell) for i in range(len(DPInjWell)): PumpingPowerInj[i] = DPInjWell[i] * nprod * wellflowrate * (1 + waterloss) / rhowaterinj[i] / pumpeff / 1E3 PumpingPowerInj = np.array([0. if x < 0. else x for x in PumpingPowerInj]) return PumpingPowerInj, DPInjWell, Pplantoutlet, Pprodwellhead
class WellBores: 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(f'Init {self.__class__.__name__}: {__name__}') self.rhowaterprod = self.rhowaterinj = 0.0 # Set up all the Parameters that will be predefined by this class using the different types of parameter classes. # Setting up includes giving it a name, a default value, The Unit Type (length, volume, temperature, etc.) # and Unit Name of that value, sets it as required (or not), sets allowable range, the error message # if that range is exceeded, the ToolTip Text, and the name of teh class that created it. # This includes setting up temporary variables that will be available to all the class but noy read in by user, # or used for Output # This also includes all Parameters that are calculated and then published using the Printouts function. # If you choose to subclass this master class, you can do so before or after you create your own parameters. # If you do, you can also choose to call this method from you class, which will effectively add and set all # these parameters to your class. # These dictionaries contain a list of all the parameters set in this object, stored as "Parameter" and # OutputParameter Objects. This will allow us later to access them in a user interface and get that list, # along with unit type, preferred units, etc. self.ParameterDict = {} self.OutputParameterDict = {} self.nprod = self.ParameterDict[self.nprod.Name] = intParameter( "Number of Production Wells", DefaultValue=2, AllowableRange=list(range(1, 201, 1)), UnitType=Units.NONE, Required=True, ErrMessage="assume default number of production wells (2)", ToolTipText="Number of (identical) production wells" ) self.ninj = self.ParameterDict[self.ninj.Name] = intParameter( "Number of Injection Wells", DefaultValue=2, AllowableRange=list(range(0, 201, 1)), UnitType=Units.NONE, Required=True, ErrMessage="assume default number of injection wells (2)", ToolTipText="Number of (identical) injection wells" ) self.prodwelldiam = self.ParameterDict[self.prodwelldiam.Name] = floatParameter( "Production Well Diameter", DefaultValue=8.0, Min=1., Max=30., UnitType=Units.LENGTH, PreferredUnits=LengthUnit.INCHES, CurrentUnits=LengthUnit.INCHES, Required=True, ErrMessage="assume default production well diameter (8 inch)", ToolTipText="Inner diameter of production wellbore (assumed constant along the wellbore) to calculate \ frictional pressure drop and wellbore heat transmission with Rameys model" ) self.injwelldiam = self.ParameterDict[self.injwelldiam.Name] = floatParameter( "Injection Well Diameter", DefaultValue=8.0, Min=1., Max=30., UnitType=Units.LENGTH, PreferredUnits=LengthUnit.INCHES, CurrentUnits=LengthUnit.INCHES, Required=True, ErrMessage="assume default injection well diameter (8 inch)", ToolTipText="Inner diameter of production wellbore (assumed constant along the wellbore) to calculate \ frictional pressure drop and wellbore heat transmission with Rameys model" ) self.rameyoptionprod = self.ParameterDict[self.rameyoptionprod.Name] = boolParameter( "Ramey Production Wellbore Model", DefaultValue=True, UnitType=Units.NONE, Required=True, ErrMessage="assume default production wellbore model (Ramey model active)", ToolTipText="Select whether to use Rameys model to estimate the geofluid temperature drop in the \ production wells" ) self.tempdropprod = self.ParameterDict[self.tempdropprod.Name] = floatParameter( "Production Wellbore Temperature Drop", DefaultValue=5.0, Min=-5., Max=50., UnitType=Units.TEMPERATURE, PreferredUnits=TemperatureUnit.CELSIUS, CurrentUnits=TemperatureUnit.CELSIUS, ErrMessage="assume default production wellbore temperature drop (5 deg.C)", ToolTipText="Specify constant production well geofluid temperature drop in case Rameys model is disabled." ) self.tempgaininj = self.ParameterDict[self.tempgaininj.Name] = floatParameter( "Injection Wellbore Temperature Gain", DefaultValue=0.0, Min=-5., Max=50., UnitType=Units.TEMPERATURE, PreferredUnits=TemperatureUnit.CELSIUS, CurrentUnits=TemperatureUnit.CELSIUS, ErrMessage="assume default injection wellbore temperature gain (0 deg.C)", ToolTipText="Specify constant injection well geofluid temperature gain." ) self.prodwellflowrate = self.ParameterDict[self.prodwellflowrate.Name] = floatParameter( "Production Flow Rate per Well", DefaultValue=50.0, Min=1., Max=500., UnitType=Units.FLOWRATE, PreferredUnits=FlowRateUnit.KGPERSEC, CurrentUnits=FlowRateUnit.KGPERSEC, ErrMessage="assume default flow rate per production well (50 kg/s)", ToolTipText="Geofluid flow rate per production well." ) self.impedance = self.ParameterDict[self.impedance.Name] = floatParameter( "Reservoir Impedance", DefaultValue=1000.0, Min=1E-4, Max=1E4, UnitType=Units.IMPEDANCE, PreferredUnits=ImpedanceUnit.GPASPERM3, CurrentUnits=ImpedanceUnit.GPASPERM3, ErrMessage="assume default reservoir impedance (0.1 GPa*s/m^3)", ToolTipText="Reservoir resistance to flow per well-pair. For EGS-type reservoirs when the injection well \ is in hydraulic communication with the production well, this parameter specifies the overall pressure drop \ in the reservoir between injection well and production well (see docs)" ) self.wellsep = self.ParameterDict[self.wellsep.Name] = floatParameter( "Well Separation", DefaultValue=1000.0, Min=10., Max=10000., UnitType=Units.LENGTH, PreferredUnits=LengthUnit.METERS, CurrentUnits=LengthUnit.INCHES, ErrMessage="assume default well separation (1000 m)", ToolTipText="Well separation for built-in TOUGH2 doublet reservoir model" ) self.Tinj = self.ParameterDict[self.Tinj.Name] = floatParameter( "Injection Temperature", DefaultValue=70.0, Min=0., Max=200., UnitType=Units.TEMPERATURE, PreferredUnits=TemperatureUnit.CELSIUS, CurrentUnits=TemperatureUnit.CELSIUS, Required=True, ErrMessage="assume default injection temperature (70 deg.C)", ToolTipText="Constant geofluid injection temperature at injection wellhead." ) self.Phydrostatic = self.ParameterDict[self.Phydrostatic.Name] = floatParameter( "Reservoir Hydrostatic Pressure", DefaultValue=29430, # Calculated from example1 Min=1E2, Max=1E5, UnitType=Units.PRESSURE, PreferredUnits=PressureUnit.KPASCAL, CurrentUnits=PressureUnit.KPASCAL, ErrMessage="calculate reservoir hydrostatic pressure using built-in correlation", ToolTipText="Reservoir hydrostatic far-field pressure. Default value is calculated with built-in modified \ Xie-Bloomfield-Shook equation (DOE, 2016)." ) self.ppwellhead = self.ParameterDict[self.ppwellhead.Name] = floatParameter( "Production Wellhead Pressure", DefaultValue=101.3200 + 344.7, Min=0.0, Max=1E4, UnitType=Units.PRESSURE, PreferredUnits=PressureUnit.KPASCAL, CurrentUnits=PressureUnit.KPASCAL, ErrMessage="using Water vapor pressure (101.3200; at 100 C) + 344.7 kPa (50 psi).", ToolTipText="Constant production wellhead pressure; Required if specifying productivity index" ) self.II = self.ParameterDict[self.II.Name] = floatParameter( "Injectivity Index", DefaultValue=10.0, Min=1E-2, Max=1E4, UnitType=Units.INJECTIVITY_INDEX, PreferredUnits=InjectivityIndexUnit.KGPERSECPERBAR, CurrentUnits=InjectivityIndexUnit.KGPERSECPERBAR, ErrMessage="assume default injectivity index (10 kg/s/bar)", ToolTipText="Injectivity index defined as ratio of injection well flow rate over injection well outflow \ pressure drop (flowing bottom hole pressure - hydrostatic reservoir pressure)." ) self.PI = self.ParameterDict[self.PI.Name] = floatParameter( "Productivity Index", DefaultValue=10.0, Min=1E-2, Max=1E4, UnitType=Units.PRODUCTIVITY_INDEX, PreferredUnits=ProductivityIndexUnit.KGPERSECPERBAR, CurrentUnits=ProductivityIndexUnit.KGPERSECPERBAR, ErrMessage="assume default productivity index (10 kg/s/bar)", ToolTipText="Productivity index defined as ratio of production well flow rate over production well inflow \ pressure drop (see docs)" ) self.maxdrawdown = self.ParameterDict[self.maxdrawdown.Name] = floatParameter( "Maximum Drawdown", DefaultValue=1.0, Min=0.0, Max=1.000001, UnitType=Units.PERCENT, PreferredUnits=PercentUnit.TENTH, CurrentUnits=PercentUnit.TENTH, ErrMessage="assume default maximum drawdown (1)", ToolTipText="Maximum allowable thermal drawdown before redrilling of all wells into new reservoir \ (most applicable to EGS-type reservoirs with heat farming strategies). E.g. a value of 0.2 means that \ all wells are redrilled after the production temperature (at the wellhead) has dropped by 20% of \ its initial temperature" ) self.IsAGS = self.ParameterDict[self.IsAGS.Name] = boolParameter( "Is AGS", DefaultValue=False, UnitType=Units.NONE, Required=False, ErrMessage="assume default is not AGS", ToolTipText="Set to true if the model is for an Advanced Geothermal System (AGS)" ) self.overpressure_percentage = self.ParameterDict[self.overpressure_percentage.Name] = floatParameter( "Overpressure Percentage", DefaultValue=100.0, UnitType=Units.PERCENT, PreferredUnits=PercentUnit.PERCENT, CurrentUnits=PercentUnit.PERCENT, Required=False, ErrMessage="assume there is no overpressure", ToolTipText="enter the amount of pressure over the hydrostatic pressure in the reservoir (100%=hydrostatic)" ) self.overpressure_depletion_rate = self.ParameterDict[self.overpressure_depletion_rate.Name] = floatParameter( "Overpressure Depletion Rate", DefaultValue=0.0, UnitType=Units.DECAY_RATE, PreferredUnits=Decay_RateUnit.PERCENTPERYEAR, CurrentUnits=Decay_RateUnit.PERCENTPERYEAR, Required=False, ErrMessage="assume there is no overpressure", ToolTipText="enter the amount of pressure over the hydrostatic pressure in the reservoir (100%=hydrostatic)" ) self.injection_reservoir_temperature = self.ParameterDict[self.injection_reservoir_temperature.Name] = floatParameter( "Injection Reservoir Temperature", DefaultValue=100.0, UnitType=Units.TEMPERATURE, PreferredUnits=TemperatureUnit.CELSIUS, CurrentUnits=TemperatureUnit.CELSIUS, Required=False, ErrMessage="assume there is not an injection reservoir, so there is no injection reservoir temperature", ToolTipText="enter the temperature of the injection reservoir (100 C)" ) self.injection_reservoir_depth = self.ParameterDict[self.injection_reservoir_depth.Name] = floatParameter( "Injection Reservoir Depth", DefaultValue=1000.0, UnitType=Units.LENGTH, PreferredUnits=LengthUnit.METERS, CurrentUnits=LengthUnit.METERS, Required=False, ErrMessage="assume there is not an injection reservoir, so there is no injection reservoir depth", ToolTipText="enter the depth of the injection reservoir (1000 m)" ) self.injection_reservoir_initial_pressure = self.ParameterDict[self.injection_reservoir_initial_pressure.Name] = floatParameter( "Injection Reservoir Initial Pressure", DefaultValue=0.0, UnitType=Units.PRESSURE, PreferredUnits=PressureUnit.KPASCAL, CurrentUnits=PressureUnit.KPASCAL, Required=False, ErrMessage="assume there is not an injection reservoir, so there is no injection reservoir initial pressure", ToolTipText="enter the depth of the injection reservoir initial pressure (use lithostatic pressure)" ) self.injection_reservoir_inflation_rate = self.ParameterDict[self.injection_reservoir_inflation_rate.Name] = floatParameter( "Injection Reservoir Inflation Rate", DefaultValue=1000.0, UnitType=Units.INFLATION_RATE, CurrentUnits=Inflation_RateUnit.KPASCALPERYEAR, Required=False, ErrMessage="assume there is not an injection reservoir, so there is no injection reservoir inflation rate", ToolTipText="enter the rate at which the pressure increases per year in the injection reservoir (1000 kPa/yr)" ) # This is a alias for "Well Geometry Configuration" - putting it here for backwards compatibility self.Configuration = self.ParameterDict[self.Configuration.Name] = intParameter( "Closed-loop Configuration", DefaultValue=Configuration.VERTICAL.int_value, AllowableRange=[1, 2, 3, 4, 5], ValuesEnum=Configuration, UnitType=Units.NONE, Required=True, ErrMessage="assume simple vertical well (3)", ToolTipText = '; '.join([f'{it.int_value}: {it.value}' for it in Configuration]) ) # This is a alias for "Closed-loop Configuration" - putting it here for backwards compatibility self.Configuration = self.ParameterDict[self.Configuration.Name] = intParameter( "Well Geometry Configuration", DefaultValue=Configuration.VERTICAL.int_value, AllowableRange=[1, 2, 3, 4, 5], ValuesEnum=Configuration, UnitType=Units.NONE, Required=True, ErrMessage="assume simple vertical well (3)", ToolTipText='; '.join([f'{it.int_value}: {it.value}' for it in Configuration]) ) self.WaterThermalConductivity = self.ParameterDict[self.WaterThermalConductivity.Name] = floatParameter( "Water Thermal Conductivity", DefaultValue=0.6, Min=0.0, Max=100.0, UnitType=Units.THERMAL_CONDUCTIVITY, PreferredUnits=ThermalConductivityUnit.WPERMPERK, CurrentUnits=ThermalConductivityUnit.WPERMPERK, ErrMessage="assume default for water thermal conductivity (0.6 W/m/K)", ToolTipText="Water Thermal Conductivity" ) self.Fluid = self.ParameterDict[self.Fluid.Name] = intParameter( "Heat Transfer Fluid", DefaultValue=WorkingFluid.WATER.int_value, AllowableRange=[1, 2], ValuesEnum=WorkingFluid, UnitType=Units.NONE, Required=True, ErrMessage="assume default Heat transfer fluid is water (1)", ToolTipText='; '.join([f'{it.int_value}: {it.value}' for it in WorkingFluid]) ) # Input data for subsurface condition self.Nonvertical_length = self.ParameterDict[self.Nonvertical_length.Name] = floatParameter( "Nonvertical Length per Multilateral Section", DefaultValue=1000.0, Min=50.0, Max=20000.0, UnitType=Units.LENGTH, PreferredUnits=LengthUnit.METERS, CurrentUnits=LengthUnit.METERS, Required=True, ErrMessage="assume default Nonvertical Length per Multilateral Section (1000 m)" ) self.nonverticalwellborediameter = self.ParameterDict[self.nonverticalwellborediameter.Name] = floatParameter( "Nonvertical Wellbore Diameter", DefaultValue=0.156, Min=0.01, Max=100.0, UnitType=Units.LENGTH, PreferredUnits=LengthUnit.METERS, CurrentUnits=LengthUnit.METERS, ErrMessage="assume default for Non-vertical Wellbore Diameter (0.156 m)", ToolTipText="Non-vertical Wellbore Diameter" ) self.numnonverticalsections = self.ParameterDict[self.numnonverticalsections.Name] = intParameter( "Number of Multilateral Sections", DefaultValue=1, AllowableRange=list(range(0, 101, 1)), UnitType=Units.NONE, ErrMessage="assume default for Number of Nonvertical Wellbore Sections (1)", ToolTipText="Number of Nonvertical Wellbore Sections" ) self.NonverticalsCased = self.ParameterDict[self.NonverticalsCased.Name] = boolParameter( "Multilaterals Cased", DefaultValue=False, Required=False, Provided=False, Valid=True, ErrMessage="assume default value (False)" ) # local variable initiation self.Pinjwellhead = 0.0 self.usebuiltinhydrostaticpressurecorrelation = True self.usebuiltinppwellheadcorrelation = True self.Pminimum = 0.0 sclass = self.__class__.__name__ self.MyClass = sclass self.MyPath = __file__ # Results - used by other objects or printed in output downstream self.production_reservoir_pressure = self.OutputParameterDict[self.production_reservoir_pressure.Name] = OutputParameter( Name="Calculated Reservoir Pressure", value=self.Phydrostatic.value, UnitType=Units.PRESSURE, PreferredUnits=PressureUnit.KPASCAL, CurrentUnits=PressureUnit.KPASCAL ) self.average_production_reservoir_pressure = self.OutputParameterDict[self.average_production_reservoir_pressure.Name] = OutputParameter( Name="Average Reservoir Pressure", UnitType=Units.PRESSURE, PreferredUnits=PressureUnit.KPASCAL, CurrentUnits=PressureUnit.KPASCAL ) self.injection_reservoir_pressure = self.OutputParameterDict[self.injection_reservoir_pressure.Name] = OutputParameter( Name="Calculated Injection Reservoir Pressure", value=-1, UnitType=Units.PRESSURE, PreferredUnits=PressureUnit.KPASCAL, CurrentUnits=PressureUnit.KPASCAL ) self.redrill = self.OutputParameterDict[self.redrill.Name] = OutputParameter( Name="redrill", UnitType=Units.NONE ) self.PumpingPowerProd = self.OutputParameterDict[self.PumpingPowerProd.Name] = OutputParameter( Name="PumpingPowerProd", UnitType=Units.POWER, PreferredUnits=PowerUnit.MW, CurrentUnits=PowerUnit.MW ) self.PumpingPowerInj = self.OutputParameterDict[self.PumpingPowerInj.Name] = OutputParameter( Name="PumpingPowerInj", UnitType=Units.POWER, PreferredUnits=PowerUnit.MW, CurrentUnits=PowerUnit.MW ) self.pumpdepth = self.OutputParameterDict[self.pumpdepth.Name] = OutputParameter( Name="pumpdepth", UnitType=Units.LENGTH, PreferredUnits=LengthUnit.METERS, CurrentUnits=LengthUnit.METERS ) self.impedancemodelallowed = self.OutputParameterDict[self.impedancemodelallowed.Name] = OutputParameter( Name="impedancemodelallowed", UnitType=Units.NONE ) self.productionwellpumping = self.OutputParameterDict[self.productionwellpumping.Name] = OutputParameter( Name="productionwellpumping", value=True, UnitType=Units.NONE ) self.impedancemodelused = self.OutputParameterDict[self.impedancemodelused.Name] = OutputParameter( Name="impedancemodelused", UnitType=Units.NONE ) self.ProdTempDrop = self.OutputParameterDict[self.ProdTempDrop.Name] = OutputParameter( Name="Production Well Temperature Drop", UnitType=Units.TEMPERATURE, PreferredUnits=TemperatureUnit.CELSIUS, CurrentUnits=TemperatureUnit.CELSIUS ) self.DPOverall = self.OutputParameterDict[self.DPOverall.Name] = OutputParameter( Name="Total Pressure Drop", UnitType=Units.PRESSURE, PreferredUnits=PressureUnit.KPASCAL, CurrentUnits=PressureUnit.KPASCAL ) self.DPInjWell = self.OutputParameterDict[self.DPInjWell.Name] = OutputParameter( Name="Injection Well Pressure Drop", UnitType=Units.PRESSURE, PreferredUnits=PressureUnit.KPASCAL, CurrentUnits=PressureUnit.KPASCAL ) self.DPReserv = self.OutputParameterDict[self.DPReserv.Name] = OutputParameter( Name="Reservoir Pressure Drop", UnitType=Units.PRESSURE, PreferredUnits=PressureUnit.KPASCAL, CurrentUnits=PressureUnit.KPASCAL ) self.DPProdWell = self.OutputParameterDict[self.DPProdWell.Name] = OutputParameter( Name="Production Well Pump Pressure Drop", UnitType=Units.PRESSURE, PreferredUnits=PressureUnit.KPASCAL, CurrentUnits=PressureUnit.KPASCAL ) self.DPBouyancy = self.OutputParameterDict[self.DPBouyancy.Name] = OutputParameter( Name="Bouyancy Pressure Drop", UnitType=Units.PRESSURE, PreferredUnits=PressureUnit.KPASCAL, CurrentUnits=PressureUnit.KPASCAL ) self.ProducedTemperature = self.OutputParameterDict[self.ProducedTemperature.Name] = OutputParameter( Name="Produced Temperature", UnitType=Units.TEMPERATURE, PreferredUnits=TemperatureUnit.CELSIUS, CurrentUnits=TemperatureUnit.CELSIUS ) self.PumpingPower = self.OutputParameterDict[self.PumpingPower.Name] = OutputParameter( Name="Pumping Power", UnitType=Units.POWER, PreferredUnits=PowerUnit.MW, CurrentUnits=PowerUnit.MW ) self.Pprodwellhead = self.OutputParameterDict[self.Pprodwellhead.Name] = OutputParameter( Name="Production wellhead pressure", UnitType=Units.PRESSURE, PreferredUnits=PressureUnit.KPASCAL, CurrentUnits=PressureUnit.KPASCAL ) self.NonverticalProducedTemperature = self.OutputParameterDict[self.ProducedTemperature.Name] = OutputParameter( Name="Nonvertical Produced Temperature", value=[0.0], UnitType=Units.TEMPERATURE, PreferredUnits=TemperatureUnit.CELSIUS, CurrentUnits=TemperatureUnit.CELSIUS ) self.NonverticalPressureDrop = self.OutputParameterDict[self.NonverticalPressureDrop.Name] = OutputParameter( Name="Nonvertical Pressure Drop", value=[0.0], UnitType=Units.PRESSURE, PreferredUnits=PressureUnit.KPASCAL, CurrentUnits=PressureUnit.KPASCAL ) self.total_drilled_length = self.OutputParameterDict[self.total_drilled_length.Name] = OutputParameter( Name="Total length of all drilling", value=-1.0, UnitType=Units.LENGTH, PreferredUnits=LengthUnit.KILOMETERS, CurrentUnits=LengthUnit.KILOMETERS ) self.tot_vert_m = self.OutputParameterDict[self.tot_vert_m.Name] = OutputParameter( Name="Total length of vertical drilling", value=-1.0, UnitType=Units.LENGTH, PreferredUnits=LengthUnit.METERS, CurrentUnits=LengthUnit.METERS ) self.tot_to_junction_m = self.OutputParameterDict[self.tot_to_junction_m.Name] = OutputParameter( Name="Total length from lateral junction to base of vertical drilling", value=-1.0, UnitType=Units.LENGTH, PreferredUnits=LengthUnit.METERS, CurrentUnits=LengthUnit.METERS ) self.tot_lateral_m = self.OutputParameterDict[self.tot_lateral_m.Name] = OutputParameter( Name="Total length of lateral drilling", value=-1.0, UnitType=Units.LENGTH, PreferredUnits=LengthUnit.METERS, CurrentUnits=LengthUnit.METERS ) def __str__(self): return "WellBores" 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 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 {self.__class__.__name__}: {__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 modify all these superclass parameters in your class. if len(model.InputParameters) > 0: # See https://github.com/NREL/GEOPHIRES-X/issues/278?title=Total+Nonvertical+Length+is+actually+per+section deprecated_nonvertical_length_param_name = 'Total Nonvertical Length' if deprecated_nonvertical_length_param_name in model.InputParameters: model.logger.warning( f'"{deprecated_nonvertical_length_param_name}" parameter name is deprecated, ' f'use "{self.Nonvertical_length.Name}" instead') if self.Nonvertical_length.Name not in model.InputParameters: model.InputParameters[self.Nonvertical_length.Name] = model.InputParameters[ deprecated_nonvertical_length_param_name] else: model.logger.warning( f'Ignoring value of "{deprecated_nonvertical_length_param_name}" ' f'because it is set by "{self.Nonvertical_length.Name}" instead') del model.InputParameters[deprecated_nonvertical_length_param_name] # 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] ReadParameter(ParameterReadIn, ParameterToModify, model) # this should handle all non-special cases # handle special cases if ParameterToModify.Name == "Heat Transfer Fluid": self.Fluid.value = WorkingFluid.from_input_string(ParameterReadIn.sValue) # IsAGS is false by default - if it equals 1, then it is true if ParameterToModify.Name == "Ramey Production Wellbore Model": if ParameterReadIn.sValue == '0': ParameterToModify.value = False # Ramey Production Wellbore Model is true by default - if it equals 0, then it is false elif ParameterToModify.Name == "Is AGS": if ParameterReadIn.sValue == '1': ParameterToModify.value = True # impedance: impedance per well pair (input as GPa*s/m^3 and converted to KPa/kg/s # (assuming 1000 for density; density will be corrected for later)) elif ParameterToModify.Name == "Reservoir Impedance": # shift it by a constant to make the units right, per line 619 of GEOPHIRES 2 self.impedance.value = self.impedance.value * (1E6 / 1E3) self.impedancemodelused.value = True if self.impedance.Provided is False: self.impedancemodelused.value = False elif ParameterToModify.Name == "Reservoir Hydrostatic Pressure": if ParameterToModify.value == -1: self.usebuiltinhydrostaticpressurecorrelation = True else: self.usebuiltinhydrostaticpressurecorrelation = False elif ParameterToModify.Name == "Production Wellhead Pressure": if ParameterToModify.value == -1.0: self.usebuiltinppwellheadcorrelation = True else: self.usebuiltinppwellheadcorrelation = False elif (ParameterToModify.Name == "Closed-loop Configuration" or ParameterToModify.Name == "Well Geometry Configuration"): # These two are alias of each other self.Configuration.value = Configuration.from_input_string(ParameterReadIn.sValue) else: model.logger.info("No parameters read because no content provided") coerce_int_params_to_enum_values(self.ParameterDict) model.logger.info(f"read parameters complete {self.__class__.__name__}: {__name__}") def Calculate(self, model: Model) -> None: """ The Calculate function is where all the calculations are done. This function can be called multiple times, and will only recalculate what has changed each time it is called. :param model: The container class of the application, giving access to everything else, including the logger :type model: :class:`~geophires_x.Model.Model` :return: Nothing, but it does make calculations and set values in the model """ model.logger.info(f'Init {self.__class__.__name__}: {__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! # calculate the reservoir pressure as a function of time if self.usebuiltinhydrostaticpressurecorrelation: self.production_reservoir_pressure.value = get_hydrostatic_pressure_kPa(model.reserv.Trock.value, model.reserv.Tsurf.value, model.reserv.depth.quantity().to('m').magnitude, model.reserv.averagegradient.value, model.reserv.hydrostatic_pressure()) else: self.production_reservoir_pressure.value = self.Phydrostatic.quantity().to(self.production_reservoir_pressure.CurrentUnits).magnitude self.production_reservoir_pressure.value = ReservoirPressurePredictor(model.surfaceplant.plant_lifetime.value, model.economics.timestepsperyear.value, self.production_reservoir_pressure.value, self.overpressure_percentage.value, self.overpressure_depletion_rate.value) self.average_production_reservoir_pressure.value = np.average(self.production_reservoir_pressure.value) if self.overpressure_percentage.Provided: # if we are doing an overpressure calculation, it is possible that the user has chosen to # split the reservoir into two parts - the deeper, overpressured Production Reservoir, # and a shallower, lower pressure Injection Reservoir. # If so, calculate the injection reservoir pressure as a function of time if overpressure is provided. # If the injection reservoir temperature or pressure are not provided, calculate a default for them. if self.injection_reservoir_depth.Provided or self.injection_reservoir_inflation_rate.Provided: # this means they must be doing a split reservoir - # now deal give default values to whatever values they didn't set if not self.injection_reservoir_temperature.Provided: self.injection_reservoir_temperature.value = model.reserv.Trock.value if not self.injection_reservoir_depth.Provided: self.injection_reservoir_depth.value = model.reserv.depth.value if not self.injection_reservoir_temperature.Provided: self.injection_reservoir_temperature.value = (model.reserv.averagegradient.value * self.injection_reservoir_depth.value) + model.reserv.Tsurf.value injection_reservoir_static_pressure = quantity(static_pressure_MPa( model.reserv.rhorock.value, self.injection_reservoir_depth.quantity().to('m').magnitude), 'MPa') if self.injection_reservoir_pressure.value < 0: # they didn't provide a pressure so assume hydrostatic. self.injection_reservoir_pressure.value = get_hydrostatic_pressure_kPa(self.injection_reservoir_temperature.value, model.reserv.Tsurf.value, self.injection_reservoir_depth.value, model.reserv.averagegradient.value * 1000.0, injection_reservoir_static_pressure) self.injection_reservoir_initial_pressure.value = self.injection_reservoir_pressure.value = get_hydrostatic_pressure_kPa(self.injection_reservoir_temperature.value, model.reserv.Tsurf.value, self.injection_reservoir_depth.value, model.reserv.averagegradient.value, injection_reservoir_static_pressure) # if not self.injection_reservoir_initial_pressure.Provided: self.injection_reservoir_pressure.value = InjectionReservoirPressurePredictor(model.surfaceplant.plant_lifetime.value, model.economics.timestepsperyear.value, self.injection_reservoir_initial_pressure.value, self.injection_reservoir_inflation_rate.value) else: # assume it is the same as the production reservoir pressure if not self.injection_reservoir_pressure.value = self.production_reservoir_pressure.value self.injection_reservoir_temperature.value = model.reserv.Trock.value self.injection_reservoir_depth.value = model.reserv.depth.value # special case: production and injection well diameters are input as inches and call calculations # assume meters! Check and change if needed, assuming anything > 2 must be talking about inches # TODO: heuristic calculation if self.injwelldiam.value > 2.0: self.injwelldiam.value = self.injwelldiam.value * 0.0254 self.injwelldiam.CurrentUnits = LengthUnit.METERS if self.prodwelldiam.value > 2.0: self.prodwelldiam.value = self.prodwelldiam.value * 0.0254 self.prodwelldiam.CurrentUnits = LengthUnit.METERS # calculate wellbore temperature drop self.ProdTempDrop.value = self.tempdropprod.value # if not Ramey, hard code a user-supplied temperature drop. if self.rameyoptionprod.value: if hasattr(model.reserv, 'InputDepth'): d = model.reserv.InputDepth.value else: d = model.reserv.depth.value self.ProdTempDrop.value = RameyCalc(model.reserv.krock.value, model.reserv.rhorock.value, model.reserv.cprock.value, self.prodwelldiam.value, model.reserv.timevector.value, model.surfaceplant.utilization_factor.value, self.prodwellflowrate.value, model.reserv.cpwater.value, model.reserv.Trock.value, model.reserv.Tresoutput.value, model.reserv.averagegradient.value, d) self.ProducedTemperature.value = model.reserv.Tresoutput.value - self.ProdTempDrop.value # redrilling # only applies to the built-in analytical reservoir models if model.reserv.resoption.value in \ [ReservoirModel.MULTIPLE_PARALLEL_FRACTURES, ReservoirModel.LINEAR_HEAT_SWEEP, ReservoirModel.SINGLE_FRACTURE, ReservoirModel.ANNUAL_PERCENTAGE]: indexfirstmaxdrawdown = np.argmax(self.ProducedTemperature.value < (1 - model.wellbores.maxdrawdown.value) * self.ProducedTemperature.value[0]) if indexfirstmaxdrawdown > 0: # redrilling necessary self.redrill.value = int(np.floor(len(self.ProducedTemperature.value) / indexfirstmaxdrawdown)) ProducedTemperatureRepeatead = np.tile(self.ProducedTemperature.value[0:indexfirstmaxdrawdown], self.redrill.value + 1) self.ProducedTemperature.value = ProducedTemperatureRepeatead[0:len(self.ProducedTemperature.value)] TResOutputRepeated = np.tile(model.reserv.Tresoutput.value[0:indexfirstmaxdrawdown], self.redrill.value + 1) model.reserv.Tresoutput.value = TResOutputRepeated[0:len(self.ProducedTemperature.value)] # calculate pressure drops and pumping power self.DPProdWell.value, f3, vprod, self.rhowaterprod = WellPressureDrop( model, model.reserv.Tresoutput.value - self.ProdTempDrop.value / 4.0, self.prodwellflowrate.value, self.prodwelldiam.value, self.impedancemodelused.value, model.reserv.depth.value) self.DPInjWell.value, f1, vinj, self.rhowaterinj = InjectionWellPressureDrop( model, self.Tinj.value, self.prodwellflowrate.value, self.injwelldiam.value, self.impedancemodelused.value, model.reserv.depth.value, self.nprod.value, self.ninj.value, model.reserv.waterloss.value) if self.impedancemodelused.value: # assumed everything stays liquid throughout, based on TARB in Geophires v1.2 self.DPOverall.value, self.PumpingPower.value, self.DPProdWell.value, self.DPReserv.value, self.DPBouyancy.value = \ ProdPressureDropsAndPumpingPowerUsingImpedenceModel(f3, vprod, self.rhowaterinj, self.rhowaterprod, model.reserv.rhowater.value, model.reserv.depth.value, self.prodwellflowrate.value, self.prodwelldiam.value, self.impedance.value, self.nprod.value, model.reserv.waterloss.value, model.surfaceplant.pump_efficiency.value) self.DPOverall.value, self.PumpingPower.value, self.DPInjWell.value = \ InjPressureDropsAndPumpingPowerUsingImpedenceModel(f1, vinj, self.rhowaterinj, model.reserv.depth.value, self.prodwellflowrate.value, self.injwelldiam.value, self.ninj.value, model.reserv.waterloss.value, model.surfaceplant.pump_efficiency.value, self.DPOverall.value) else: # PI and II are used self.PumpingPower.value, self.PumpingPowerProd.value, self.DPProdWell.value, self.Pprodwellhead.value = \ ProdPressureDropAndPumpingPowerUsingIndexes(model, self.productionwellpumping.value, self.usebuiltinppwellheadcorrelation, model.reserv.Trock.value, model.reserv.depth.value, self.ppwellhead.value, self.PI.value, self.prodwellflowrate.value, f3, vprod, self.prodwelldiam.value, self.nprod.value, model.surfaceplant.pump_efficiency.value, self.rhowaterprod) self.PumpingPowerInj.value, self.DPInjWell.value, model.surfaceplant.plant_outlet_pressure.value, self.Pprodwellhead.value = \ InjPressureDropAndPumpingPowerUsingIndexes(model, self.productionwellpumping.value, self.usebuiltinppwellheadcorrelation, model.surfaceplant.usebuiltinoutletplantcorrelation.value, self.injection_reservoir_temperature.value, self.injection_reservoir_depth.value, self.ppwellhead.value, self.II.value, self.prodwellflowrate.value, f1, vinj, self.injwelldiam.value, self.nprod.value, self.ninj.value, model.reserv.waterloss.value, model.surfaceplant.pump_efficiency.value, self.rhowaterinj, model.surfaceplant.plant_outlet_pressure.value) # total pumping power if self.productionwellpumping.value: self.PumpingPower.value = self.PumpingPowerInj.value + self.PumpingPowerProd.value else: self.PumpingPower.value = self.PumpingPowerInj.value # negative pumping power values become zero (b/c we are not generating electricity) self.PumpingPower.value = [0. if x < 0. else x for x in self.PumpingPower.value] model.logger.info(f'complete {self.__class__.__name__}: {__name__}')