From e99adeaea890005774088b71969df9f80ceaeffa Mon Sep 17 00:00:00 2001 From: Malcolm Ross Date: Tue, 13 Feb 2024 19:57:27 -0600 Subject: [PATCH 1/7] Fixed typo in Economics.py for Parallel CHP production LCOH calculation. --- src/geophires_x/Economics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/geophires_x/Economics.py b/src/geophires_x/Economics.py index 6d09f936..b45dd9db 100644 --- a/src/geophires_x/Economics.py +++ b/src/geophires_x/Economics.py @@ -286,7 +286,7 @@ def CalculateLCOELCOH(self, model: Model) -> tuple: LCOH = (NPVcap + NPVoandm + NPVfc + NPVit + NPVgrt - NPVitc - np.sum( annualelectricityincome * inflationvector * discountvector)) / np.sum( model.surfaceplant.HeatkWhProduced.value * inflationvector * discountvector) * 1E8 - LCOH = self.LCOELCOHCombined.value * 2.931 # $/MMBTU + LCOH = LCOH * 2.931 # $/MMBTU elif model.surfaceplant.enduse_option.value == EndUseOptions.HEAT and model.surfaceplant.plant_type.value == PlantType.ABSORPTION_CHILLER: PumpingCosts = model.surfaceplant.PumpingkWh.value * model.surfaceplant.electricity_cost_to_buy.value / 1E6 From dd34c853ef7b58385912f4b7a635c68abba22018 Mon Sep 17 00:00:00 2001 From: Malcolm Ross Date: Tue, 20 Feb 2024 17:08:10 -0600 Subject: [PATCH 2/7] Update HIP-RA-X functions to include pressure in all calculations. --- src/geophires_x/GeoPHIRESUtils.py | 42 +++-- src/geophires_x/Reservoir.py | 6 +- src/geophires_x/Units.py | 1 + src/geophires_x/WellBores.py | 2 +- src/hip_ra_x/hip_ra_x.py | 161 +++++++++++------- tests/geophires_x_tests/test_reservoir.py | 4 +- .../examples/HIP-RA-X_example1.out | 32 ++-- 7 files changed, 147 insertions(+), 101 deletions(-) diff --git a/src/geophires_x/GeoPHIRESUtils.py b/src/geophires_x/GeoPHIRESUtils.py index 09b2805e..1c49c2f5 100644 --- a/src/geophires_x/GeoPHIRESUtils.py +++ b/src/geophires_x/GeoPHIRESUtils.py @@ -102,15 +102,13 @@ def quantity(value: float, unit: str) -> PlainQuantity: @lru_cache -def density_water_kg_per_m3( - Twater_degC: float, - pressure: Optional[PlainQuantity] = None) -> float: +def density_water_kg_per_m3(Twater_degC: float, pressure: Optional[PlainQuantity] = None) -> float: """ Calculate the density of water as a function of temperature. Args: Twater_degC: The temperature of water in degrees C. - pressure: Pressure - should be provided + pressure: Pressure - should be provided as a Pint quantity that knows its units Returns: The density of water in kg/m³. Raises: @@ -244,15 +242,13 @@ def RecoverableHeat(Twater_degC: float) -> float: @lru_cache -def vapor_pressure_water_kPa( - Twater_degC: float, - pressure: Optional[PlainQuantity] = None) -> float: +def vapor_pressure_water_kPa(Twater_degC: float, pressure: Optional[PlainQuantity] = None) -> float: """ Calculate the vapor pressure of water as a function of temperature. Args: Twater_degC: the temperature of water in degrees C - pressure: Pressure - should be provided + pressure: Pressure - should be provided as a Pint quantity that knows its units Returns: The vapor pressure of water as a function of temperature in kPa Raises: @@ -275,22 +271,23 @@ def vapor_pressure_water_kPa( return (quantity(CP.PropsSI('P', 'T', celsius_to_kelvin(Twater_degC), 'Q', 0, 'Water'), 'Pa') .to('kPa').magnitude) - except (NotImplementedError, ValueError) as e: raise ValueError(f'Input temperature {Twater_degC} is out of range or otherwise not implemented') from e @lru_cache -def entropy_water_kJ_per_kg_per_K(temperature_degC: float) -> float: +def entropy_water_kJ_per_kg_per_K(temperature_degC: float, pressure: Optional[PlainQuantity] = None) -> float: """ Calculate the entropy of water as a function of temperature - TODO take pressure as a parameter https://github.com/NREL/GEOPHIRES-X/issues/119 Args: temperature_degC: the temperature of water in degrees C + pressure: Pressure - should be provided as a Pint quantity that knows its units Returns: the entropy of water as a function of temperature in kJ/(kg·K) Raises: + TypeError: If temperature is not a float or convertible to float. + ValueError: If temperature is not within the range of 0 to 373.946 degrees C. """ try: @@ -299,19 +296,23 @@ def entropy_water_kJ_per_kg_per_K(temperature_degC: float) -> float: raise TypeError(f'Input temperature ({temperature_degC}) must be a float') try: - return CP.PropsSI('S', 'T', celsius_to_kelvin(temperature_degC), 'Q', 0, 'Water') * 1e-3 + if pressure is not None: + return CP.PropsSI('S', 'T', celsius_to_kelvin(temperature_degC), + 'P', pressure.to('Pa').magnitude, 'Water') * 1e-3 + else: + return CP.PropsSI('S', 'T', celsius_to_kelvin(temperature_degC), 'Q', 0, 'Water') * 1e-3 except (NotImplementedError, ValueError) as e: raise ValueError(f'Input temperature {temperature_degC} is out of range or otherwise not implemented') from e @lru_cache -def enthalpy_water_kJ_per_kg(temperature_degC: float) -> float: +def enthalpy_water_kJ_per_kg(temperature_degC: float, pressure: Optional[PlainQuantity] = None) -> float: """ Calculate the enthalpy of water as a function of temperature - TODO take pressure as a parameter https://github.com/NREL/GEOPHIRES-X/issues/119 Args: temperature_degC: the temperature of water in degrees C (float) + pressure: Pressure - should be provided as a Pint quantity that knows its units Returns: the enthalpy of water as a function of temperature in kJ/kg Raises: @@ -324,7 +325,12 @@ def enthalpy_water_kJ_per_kg(temperature_degC: float) -> float: raise TypeError(f'Input temperature ({temperature_degC}) must be a float') try: - return CP.PropsSI('H', 'T', celsius_to_kelvin(temperature_degC), 'Q', 0, 'Water') * 1e-3 + if pressure is not None: + return CP.PropsSI('H', 'T', celsius_to_kelvin(temperature_degC), + 'P', pressure.to('Pa').magnitude, 'Water') * 1e-3 + else: + return CP.PropsSI('H', 'T', celsius_to_kelvin(temperature_degC), 'Q', 0, 'Water') * 1e-3 + except (NotImplementedError, ValueError) as e: raise ValueError(f'Input temperature {temperature_degC} is out of range or otherwise not implemented') from e @@ -456,15 +462,15 @@ def json_dumpse(obj) -> str: return json.dumps(obj, cls=_EnhancedJSONEncoder) -def lithostatic_pressure_MPa(rho_kg_per_m3: float, depth_m: float) -> float: +def static_pressure_MPa(rho_kg_per_m3: float, depth_m: float) -> float: """ - Calculate lithostatic pressure in a reservoir. + Calculate litho- (or hydro-) static pressure in a reservoir. Args: rho_kg_per_m3 (float): Density of the fluid in kg/m^3. depth_m (float): Depth of the reservoir in meters. Returns: - float: Lithostatic pressure in megapascals (MPa). + pint quantity: Lithostatic pressure in megapascals (MPa). """ g = scipy.constants.g # Acceleration due to gravity (m/s^2) diff --git a/src/geophires_x/Reservoir.py b/src/geophires_x/Reservoir.py index 129a5aaf..f644efd1 100644 --- a/src/geophires_x/Reservoir.py +++ b/src/geophires_x/Reservoir.py @@ -10,7 +10,7 @@ from .Units import * import geophires_x.Model as Model -from geophires_x.GeoPHIRESUtils import heat_capacity_water_J_per_kg_per_K, quantity, lithostatic_pressure_MPa +from geophires_x.GeoPHIRESUtils import heat_capacity_water_J_per_kg_per_K, quantity, static_pressure_MPa from geophires_x.GeoPHIRESUtils import density_water_kg_per_m3 class Reservoir: @@ -795,7 +795,7 @@ def Calculate(self, model: Model) -> None: model.logger.info(f'complete {str(__class__)}: {sys._getframe().f_code.co_name}') def lithostatic_pressure(self) -> PlainQuantity: - return quantity(lithostatic_pressure_MPa(self.rhorock.quantity().to('kg/m**3').magnitude, - self.depth.quantity().to('m').magnitude), 'MPa') + return quantity(static_pressure_MPa(self.rhorock.quantity().to('kg/m**3').magnitude, + self.depth.quantity().to('m').magnitude), 'MPa') diff --git a/src/geophires_x/Units.py b/src/geophires_x/Units.py index 008531eb..fef9c48f 100644 --- a/src/geophires_x/Units.py +++ b/src/geophires_x/Units.py @@ -193,6 +193,7 @@ class CostPerDistanceUnit(str, Enum): class PressureUnit(str, Enum): """Pressure Units""" + MPASCAL = "mPa" KPASCAL = "kPa" PASCAL = "Pa" BAR = "bar" diff --git a/src/geophires_x/WellBores.py b/src/geophires_x/WellBores.py index b3c436a9..1be24f51 100644 --- a/src/geophires_x/WellBores.py +++ b/src/geophires_x/WellBores.py @@ -3,7 +3,7 @@ from pint.facets.plain import PlainQuantity from .Parameter import floatParameter, intParameter, boolParameter, OutputParameter, ReadParameter -from geophires_x.GeoPHIRESUtils import vapor_pressure_water_kPa, quantity, lithostatic_pressure_MPa +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 * diff --git a/src/hip_ra_x/hip_ra_x.py b/src/hip_ra_x/hip_ra_x.py index bbf3c50a..5a8b75a2 100644 --- a/src/hip_ra_x/hip_ra_x.py +++ b/src/hip_ra_x/hip_ra_x.py @@ -17,6 +17,7 @@ from geophires_x.GeoPHIRESUtils import entropy_water_kJ_per_kg_per_K from geophires_x.GeoPHIRESUtils import heat_capacity_water_J_per_kg_per_K from geophires_x.GeoPHIRESUtils import read_input_file +from geophires_x.GeoPHIRESUtils import static_pressure_MPa from geophires_x.Parameter import ConvertOutputUnits from geophires_x.Parameter import ConvertUnitsBack from geophires_x.Parameter import LookupUnits @@ -39,6 +40,7 @@ from geophires_x.Units import PowerPerUnitAreaUnit from geophires_x.Units import PowerPerUnitVolumeUnit from geophires_x.Units import PowerUnit +from geophires_x.Units import PressureUnit from geophires_x.Units import TemperatureUnit from geophires_x.Units import TimeUnit from geophires_x.Units import Units @@ -278,6 +280,38 @@ def parameter_dict_entry(param: Parameter) -> Parameter: ToolTipText='percent of fluid that is recoverable from the reservoir (0.5 = 50%)', ) ) + self.reservoir_depth: Parameter = parameter_dict_entry( + floatParameter( + 'Reservoir Depth', + value=-1.0, + Min=0.001, + Max=15.0, + UnitType=Units.LENGTH, + PreferredUnits=LengthUnit.KILOMETERS, + CurrentUnits=LengthUnit.KILOMETERS, + Required=False, + Provided=False, + ErrMessage='calculate based on an assumed gradient of 30 C/km and the reservoir temperature', + ToolTipText='depth to top of reservoir (km). Calculated based on an assumed gradient \ + and the reservoir temperature if no value given', + ) + ) + self.reservoir_pressure: Parameter = parameter_dict_entry( + floatParameter( + 'Reservoir Pressure', + value=-1.0, + Min=0.00, + Max=10000.000, + UnitType=Units.PRESSURE, + PreferredUnits=PressureUnit.MPASCAL, + CurrentUnits=PressureUnit.MPASCAL, + Required=False, + Provided=False, + ErrMessage='calculate assuming hydrostatic pressure and the reservoir depth & water density', + ToolTipText='pressure of the of reservoir (in mPa). Calculated assuming hydrostatic pressure and \ + reservoir depth & water density if no value given', + ) + ) self.recoverable_rock_heat: Parameter = parameter_dict_entry( floatParameter( 'Recoverable Heat from Rock', @@ -555,9 +589,8 @@ def read_parameters(self) -> None: # Before we change the parameter, let's assume that the unit preferences will match - # if they don't, the later code will fix this. ParameterToModify.CurrentUnits = ParameterToModify.PreferredUnits - ReadParameter( - ParameterReadIn, ParameterToModify, self - ) # this should handle all the non-special cases + # this should handle all the non-special cases + ReadParameter(ParameterReadIn, ParameterToModify, self) else: self.logger.info('No parameters read because no content provided') @@ -578,25 +611,42 @@ def Calculate(self): try: # Calculate the volume of rock and fluid in the reservoir. - self.reservoir_volume.value = self.reservoir_area.value * self.reservoir_thickness.value self.volume_rock.value = self.reservoir_volume.value * (1.0 - (self.reservoir_porosity.value / 100.0)) + + # Note that we can't recover all the fluid from the reservoir, so we multiply by the recoverable fluid factor self.volume_recoverable_fluid.value = ( self.reservoir_volume.value * (self.reservoir_porosity.value / 100.0) * self.recoverable_fluid_factor.value - # Note that we can't recover all the fluid from the reservoir, - # so we multiply by the recoverable fluid factor ) + if not self.reservoir_depth.Provided: + self.logger.info( + f'Deriving value of {self.reservoir_depth.Name} because provided value ' + f'({self.reservoir_depth.value}) was not provided)' + ) + # assume ambient Temperature of 15 C and 30C/km + self.reservoir_depth.value = (self.reservoir_temperature.value - 15.0) / 30.0 + + if not self.reservoir_pressure.Provided: + self.logger.info( + f'Deriving value of {self.reservoir_pressure.Name} because provided value ' + f'({self.reservoir_pressure.value}) was not provided)' + ) + # Assumes a water density of 1.0 g/cm3, which is high, since the water density decreases with depth + self.reservoir_pressure.value = static_pressure_MPa(1000.0, self.reservoir_depth.value * 1000.0) + if self.fluid_density.value < self.fluid_density.Min: self.logger.info( f'Deriving value of {self.fluid_density.Name} because provided value ' f'({self.fluid_density.value}) was less than min ({self.fluid_density.Min})' ) - density_h20_kg_per_m3 = density_water_kg_per_m3(self.reservoir_temperature.value) - + density_h20_kg_per_m3 = density_water_kg_per_m3( + self.reservoir_temperature.value, + pressure=HIP_RA_X._ureg.Quantity(self.reservoir_pressure.value, 'MPa'), + ) self.fluid_density.value = density_h20_kg_per_m3 * 1_000_000_000.0 # converted to kg/km3 self.mass_rock.value = self.volume_rock.value * self.rock_density.value @@ -609,48 +659,50 @@ def Calculate(self): f'({self.fluid_heat_capacity.value}) was less than min ({self.fluid_heat_capacity.Min})' ) + # converted to kJ/(kg·K) self.fluid_heat_capacity.value = ( - heat_capacity_water_J_per_kg_per_K(self.reservoir_temperature.value) + heat_capacity_water_J_per_kg_per_K( + self.reservoir_temperature.value, + pressure=HIP_RA_X._ureg.Quantity(self.reservoir_pressure.value, 'MPa'), + ) / 1000.0 - # converted to kJ/(kg·K) ) rejection_temperature_k = celsius_to_kelvin(self.rejection_temperature.value) reservoir_temperature_k = celsius_to_kelvin(self.reservoir_temperature.value) delta_temperature_k = reservoir_temperature_k - rejection_temperature_k - fluid_net_enthalpy = fluid_net_enthalpy = enthalpy_water_kJ_per_kg( - self.reservoir_temperature.value - ) - enthalpy_water_kJ_per_kg(self.rejection_temperature.value) + fluid_net_enthalpy = enthalpy_water_kJ_per_kg( + self.reservoir_temperature.value, pressure=HIP_RA_X._ureg.Quantity(self.reservoir_pressure.value, 'MPa') + ) - enthalpy_water_kJ_per_kg( + self.rejection_temperature.value, pressure=HIP_RA_X._ureg.Quantity(self.reservoir_pressure.value, 'MPa') + ) fluid_net_entropy = entropy_water_kJ_per_kg_per_K( - self.reservoir_temperature.value - ) - entropy_water_kJ_per_kg_per_K(self.rejection_temperature.value) + self.reservoir_temperature.value, pressure=HIP_RA_X._ureg.Quantity(self.reservoir_pressure.value, 'MPa') + ) - entropy_water_kJ_per_kg_per_K( + self.rejection_temperature.value, pressure=HIP_RA_X._ureg.Quantity(self.reservoir_pressure.value, 'MPa') + ) # fmt: off - self.enthalpy_rock.value = ( - self.rock_heat_capacity.value * delta_temperature_k * self.volume_rock.value - ) / self.mass_rock.value + self.enthalpy_rock.value = ((self.rock_heat_capacity.value * delta_temperature_k * self.volume_rock.value) / + self.mass_rock.value) # fmt: on + # result in kJ self.stored_heat_rock.value = ( - self.recoverable_rock_heat.value - * self.enthalpy_rock.value - * self.mass_rock.value - # result in kJ + self.recoverable_rock_heat.value * self.enthalpy_rock.value * self.mass_rock.value ) - self.stored_heat_fluid.value = fluid_net_enthalpy * self.mass_recoverable_fluid.value # result in kJ + self.stored_heat_fluid.value = fluid_net_enthalpy * self.mass_recoverable_fluid.value self.reservoir_stored_heat.value = self.stored_heat_rock.value + self.stored_heat_fluid.value # equation 4 in Garg and Combs(2011) amount_fluid_produced_kg = self.reservoir_stored_heat.value / fluid_net_enthalpy self.mass_recoverable_fluid.value = amount_fluid_produced_kg + # equation 7 in Garg and Combs(2011) fluid_exergy_kJ_per_kg = ( - # equation 7 in Garg and Combs(2011) - fluid_net_enthalpy - - celsius_to_kelvin(self.rejection_temperature.value) * fluid_net_entropy + fluid_net_enthalpy - celsius_to_kelvin(self.rejection_temperature.value) * fluid_net_entropy ) self.enthalpy_fluid.value = fluid_exergy_kJ_per_kg - self.reservoir_enthalpy.value = self.enthalpy_rock.value + self.enthalpy_fluid.value # (equation 8 in Garg and Combs(2011)) @@ -671,9 +723,7 @@ def Calculate(self): self.reservoir_producible_heat.value / self.reservoir_stored_heat.value ) - # Now assuming a 30-year lifetime: - plant_lifetime_years = 30 - maximum_power_kW = maximum_lifetime_electricity_kJ / (plant_lifetime_years * 365 * 24 * 3600) + maximum_power_kW = maximum_lifetime_electricity_kJ / (self.reservoir_life_cycle.value * 365 * 24 * 3600) electricity_with_actual_power_plant_kW = UtilEff_func(self.reservoir_temperature.value) * maximum_power_kW producible_power_kW = electricity_with_actual_power_plant_kW @@ -744,9 +794,7 @@ def render_scientific(p: floatParameter | OutputParameter) -> str: summary_of_inputs = {} summary_of_results = {} - for param, render in [ - # TODO: Commented parameters are defined in initialization but not calculated - either calculate or - # remove entirely + inputs = [ (self.reservoir_temperature, render_default), (self.rejection_temperature, render_default), (self.reservoir_porosity, render_default), @@ -757,63 +805,52 @@ def render_scientific(p: floatParameter | OutputParameter) -> str: (self.fluid_heat_capacity, render_default), (self.fluid_density, render_scientific), (self.rock_density, render_scientific), - # (self.rock_recoverable_heat, render_default), - # (self.fluid_recoverable_heat, render_default), (self.recoverable_fluid_factor, render_default), (self.recoverable_rock_heat, render_default), - ]: + ] + + # If depth and/or pressure are provided, report them as inputs. If not, as outputs + if self.reservoir_depth.Provided: + inputs.append((self.reservoir_depth, render_default)) + if self.reservoir_pressure.Provided: + inputs.append((self.reservoir_pressure, render_default)) + + for param, render in inputs: summary_of_inputs[param.Name] = render(param) case_data_inputs = {'SUMMARY OF INPUTS': summary_of_inputs} - for param, render in [ - # TODO: Commented parameters are defined in initialization but not calculated - either calculate or - # remove entirely + outputs = [ (self.reservoir_volume, render_default), (self.volume_rock, render_default), (self.volume_recoverable_fluid, render_default), (self.reservoir_stored_heat, render_scientific), (self.stored_heat_rock, render_scientific), (self.stored_heat_fluid, render_scientific), - # (self.reservoir_mass, render_scientific), (self.mass_rock, render_scientific), (self.mass_recoverable_fluid, render_scientific), (self.reservoir_enthalpy, render_default), (self.enthalpy_rock, render_default), (self.enthalpy_fluid, render_default), - # (self.wellhead_heat, render_scientific), - # (self.wellhead_heat_recovery_rock, render_scientific), - # (self.wellhead_heat_recovery_fluid, render_scientific), ( self.reservoir_recovery_factor, lambda rg: f'{(100 * rg.value):10.2f} {self.reservoir_recovery_factor.CurrentUnits.value}', ), - # ( - # self.recovery_factor_rock, - # lambda rg: f'{(100 * rg.value):10.2f} {self.recovery_factor_rock.CurrentUnits.value}', - # ), - # ( - # self.recovery_factor_fluid, - # lambda rg: f'{(100 * rg.value):10.2f} {self.recovery_factor_fluid.CurrentUnits.value}', - # ), (self.reservoir_available_heat, render_scientific), - # (self.available_heat_rock, render_scientific), - # (self.available_heat_fluid, render_scientific), (self.reservoir_producible_heat, render_scientific), - # (self.producible_heat_rock, render_scientific), - # (self.producible_heat_fluid, render_scientific), (self.producible_heat_per_unit_area, render_scientific), (self.heat_per_unit_volume_reservoir, render_scientific), - # (self.heat_per_unit_area_rock, render_scientific), - # (self.heat_per_unit_area_fluid, render_scientific), (self.reservoir_producible_electricity, render_default), - # (self.producible_electricity_rock, render_default), - # (self.producible_electricity_fluid, render_default), (self.producible_electricity_per_unit_area, render_default), (self.electricity_per_unit_volume_reservoir, render_default), - # (self.electricity_per_unit_area_rock, render_default), - # (self.electricity_per_unit_area_fluid, render_default), - ]: + ] + + # If depth and/or pressure are provided, report them as inputs. If not, as outputs + if not self.reservoir_depth.Provided: + outputs.insert(0, (self.reservoir_depth, render_default)) + if not self.reservoir_pressure.Provided: + outputs.insert(0, (self.reservoir_pressure, render_default)) + for param, render in outputs: summary_of_results[param.Name] = render(param) case_data_results = {'SUMMARY OF RESULTS': summary_of_results} diff --git a/tests/geophires_x_tests/test_reservoir.py b/tests/geophires_x_tests/test_reservoir.py index a5a837d4..0cedc225 100644 --- a/tests/geophires_x_tests/test_reservoir.py +++ b/tests/geophires_x_tests/test_reservoir.py @@ -5,14 +5,14 @@ from pint.facets.plain import PlainQuantity from base_test_case import BaseTestCase -from geophires_x.GeoPHIRESUtils import lithostatic_pressure_MPa +from geophires_x.GeoPHIRESUtils import static_pressure_MPa from geophires_x.Model import Model from geophires_x.Reservoir import Reservoir class ReservoirTestCase(BaseTestCase): def test_lithostatic_pressure(self): - p = lithostatic_pressure_MPa(2700, 3000) + p = static_pressure_MPa(2700, 3000) self.assertEqual(79.433865, p) def test_reservoir_lithostatic_pressure(self): diff --git a/tests/hip_ra_x_tests/examples/HIP-RA-X_example1.out b/tests/hip_ra_x_tests/examples/HIP-RA-X_example1.out index 2ee618ee..3856b409 100644 --- a/tests/hip_ra_x_tests/examples/HIP-RA-X_example1.out +++ b/tests/hip_ra_x_tests/examples/HIP-RA-X_example1.out @@ -10,29 +10,31 @@ Reservoir Thickness: 0.25 kilometer Reservoir Life Cycle: 25.00 yr Rock Heat Capacity: 2.84e+12 kJ/km**3C - Fluid Specific Heat Capacity: 4.87 kJ/kgC - Density Of Reservoir Fluid: 7.99e+11 kg/km**3 + Fluid Specific Heat Capacity: 4.34 kJ/kgC + Density Of Reservoir Fluid: 8.62e+11 kg/km**3 Density Of Reservoir Rock: 2.55e+12 kg/km**3 Recoverable Fluid Factor: 0.50 Recoverable Heat from Rock: 0.75 ***SUMMARY OF RESULTS*** + Reservoir Pressure: 76.82 mPa + Reservoir Depth: 7.83 kilometer Reservoir Volume (reservoir): 13.75 km**3 Reservoir Volume (rock): 12.38 km**3 Recoverable Volume (recoverable fluid): 0.69 km**3 - Stored Heat (reservoir): 5.47e+15 kJ + Stored Heat (reservoir): 5.48e+15 kJ Stored Heat (rock): 5.01e+15 kJ - Stored Heat (fluid): 4.58e+14 kJ + Stored Heat (fluid): 4.67e+14 kJ Mass of Reservoir (rock): 3.16e+13 kilogram - Mass of Reservoir (fluid): 6.55e+12 kilogram - Specific Enthalpy (reservoir): 392.48 kJ/kg + Mass of Reservoir (fluid): 6.95e+12 kilogram + Specific Enthalpy (reservoir): 377.60 kJ/kg Specific Enthalpy (rock): 211.61 kJ/kg - Specific Enthalpy (fluid): 180.87 kJ/kg - Recovery Factor (reservoir): 14.30 % - Available Heat (reservoir): 1.18e+15 kJ - Producible Heat (reservoir): 7.82e+14 kJ - Producible Heat/Unit Area (reservoir): 1.42e+13 kJ/km**2 - Producible Heat/Unit Volume (reservoir): 5.69e+13 kJ/km**3 - Producible Electricity (reservoir): 500.89 MW - Producible Electricity/Unit Area (reservoir): 9.11 MW/km**2 - Producible Electricity/Unit Volume (reservoir): 36.43 MW/km**3 + Specific Enthalpy (fluid): 166.00 kJ/kg + Recovery Factor (reservoir): 13.90 % + Available Heat (reservoir): 1.15e+15 kJ + Producible Heat (reservoir): 7.61e+14 kJ + Producible Heat/Unit Area (reservoir): 1.38e+13 kJ/km**2 + Producible Heat/Unit Volume (reservoir): 5.54e+13 kJ/km**3 + Producible Electricity (reservoir): 585.09 MW + Producible Electricity/Unit Area (reservoir): 10.64 MW/km**2 + Producible Electricity/Unit Volume (reservoir): 42.55 MW/km**3 From 81a44d7ebce6b898769e249f118a6638b3640f90 Mon Sep 17 00:00:00 2001 From: Malcolm Ross Date: Tue, 20 Feb 2024 17:23:50 -0600 Subject: [PATCH 3/7] Update HIP-RA-X functions to include pressure in all calculations. --- src/geophires_x/CylindricalReservoir.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/geophires_x/CylindricalReservoir.py b/src/geophires_x/CylindricalReservoir.py index 93b25bbb..9402c791 100644 --- a/src/geophires_x/CylindricalReservoir.py +++ b/src/geophires_x/CylindricalReservoir.py @@ -6,7 +6,7 @@ import numpy as np from pint.facets.plain import PlainQuantity -from geophires_x.GeoPHIRESUtils import density_water_kg_per_m3, lithostatic_pressure_MPa, quantity +from geophires_x.GeoPHIRESUtils import density_water_kg_per_m3, static_pressure_MPa, quantity from geophires_x.GeoPHIRESUtils import heat_capacity_water_J_per_kg_per_K import geophires_x.Model as Model @@ -266,5 +266,5 @@ def lithostatic_pressure(self) -> PlainQuantity: Standard reservoir implementation uses depth but CylindricalReservoir sets depth to total drilled length """ - return quantity(lithostatic_pressure_MPa(self.rhorock.quantity().to('kg/m**3').magnitude, - self.InputDepth.quantity().to('m').magnitude), 'MPa') + return quantity(static_pressure_MPa(self.rhorock.quantity().to('kg/m**3').magnitude, + self.InputDepth.quantity().to('m').magnitude), 'MPa') From c4eae2cba0772addcfc81a41c72658713adf3682 Mon Sep 17 00:00:00 2001 From: Malcolm Ross Date: Tue, 20 Feb 2024 18:06:38 -0600 Subject: [PATCH 4/7] Update HIP-RA-X functions to include pressure in all calculations. Update Util_eff to allow for temperatures above 373 by assuming efficiency os the same, given the plant is the same (usually flash) --- src/geophires_x/GeoPHIRESUtils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/geophires_x/GeoPHIRESUtils.py b/src/geophires_x/GeoPHIRESUtils.py index 1c49c2f5..fb2c9638 100644 --- a/src/geophires_x/GeoPHIRESUtils.py +++ b/src/geophires_x/GeoPHIRESUtils.py @@ -51,6 +51,7 @@ 340.0, 360.0, 373.946, + 600.0, ] ) @@ -84,6 +85,7 @@ 0.4, 0.4, 0.4, + 0.4, ] ) From 9972159b1b036cffb4f2741ca1d8dfa7994179b7 Mon Sep 17 00:00:00 2001 From: Malcolm Ross Date: Thu, 13 Jun 2024 14:48:38 -0500 Subject: [PATCH 5/7] Fixes bug where LCOH was not being calculated correctly due accessing the wrong reference. --- src/geophires_x/Economics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/geophires_x/Economics.py b/src/geophires_x/Economics.py index 6266fa79..17472896 100644 --- a/src/geophires_x/Economics.py +++ b/src/geophires_x/Economics.py @@ -630,7 +630,7 @@ def CalculateLCOELCOHLCOC(self, model: Model) -> tuple: NPVgrt = self.GTR.value / (1 - self.GTR.value) * (NPVcap + NPVoandm + NPVfc + NPVit - NPVitc) LCOH = (NPVcap + NPVoandm + NPVfc + NPVit + NPVgrt - NPVitc) / np.sum( model.surfaceplant.HeatkWhProduced.value * inflationvector * discountvector) * 1E8 - LCOH = self.LCOH.value * 2.931 # $/MMBTU + LCOH = LCOH * 2.931 # $/MMBTU elif model.surfaceplant.enduse_option.value == EndUseOptions.HEAT and model.surfaceplant.plant_type.value == PlantType.DISTRICT_HEATING: PumpingCosts = model.surfaceplant.PumpingkWh.value * model.surfaceplant.electricity_cost_to_buy.value / 1E6 From 551707f0dbace385b1f0d142a8420c0739963405 Mon Sep 17 00:00:00 2001 From: Malcolm Ross Date: Wed, 26 Jun 2024 14:05:04 -0500 Subject: [PATCH 6/7] Added new estimate from GR website --- src/geophires_x/Economics.py | 74 +++++++++++++++++++++++++++++++++++- src/geophires_x/Outputs.py | 6 ++- src/geophires_x/Units.py | 19 +++++++++ 3 files changed, 96 insertions(+), 3 deletions(-) diff --git a/src/geophires_x/Economics.py b/src/geophires_x/Economics.py index 343c4672..221b0aad 100644 --- a/src/geophires_x/Economics.py +++ b/src/geophires_x/Economics.py @@ -1497,11 +1497,46 @@ def __init__(self, model: Model): self.jobs_created_per_MW_electricity.Name] = floatParameter( "Estimated Jobs Created per MW of Electricity Produced", DefaultValue=2.13, - UnitType=Units.NONE, + UnitType=Units.JOBS_PER_ENERGY, + PreferredUnits=JobsPerEnergyUnit.JOBSPERMW, + CurrentUnits=JobsPerEnergyUnit.JOBSPERMW, Required=False, ToolTipText="Estimated jobs created per MW of electricity produced, per https://geothermal.org/resources/geothermal-basics" ) + self.property_tax_per_MW_electricity = self.ParameterDict[ + self.property_tax_per_MW_electricity.Name] = floatParameter( + "Estimated Property Tax per MW of Electricity Produced", + DefaultValue=0.210, + UnitType=Units.ROYALTY_PER_ENERGY, + PreferredUnits=RoyaltyPerEnergyUnit.ROYALTYPERMW, + CurrentUnits=RoyaltyPerEnergyUnit.ROYALTYPERMW, + Required=False, + ToolTipText="Estimated property tax per MW of electricity produced, per https://geothermal.org/resources/geothermal-basics" + ) + + self.gov_royalty_per_MW_electricity = self.ParameterDict[ + self.gov_royalty_per_MW_electricity.Name] = floatParameter( + "Estimated Governmental Royalty per MW of Electricity Produced", + DefaultValue=0.315, + UnitType=Units.ROYALTY_PER_ENERGY, + PreferredUnits=RoyaltyPerEnergyUnit.ROYALTYPERMW, + CurrentUnits=RoyaltyPerEnergyUnit.ROYALTYPERMW, + Required=False, + ToolTipText="Estimated Estimated Governmental Royalty per MW of electricity produced, per https://geothermal.org/resources/geothermal-basics" + ) + + self.total_royalty_per_MW_electricity = self.ParameterDict[ + self.total_royalty_per_MW_electricity.Name] = floatParameter( + "Estimated Jobs Created per MW of Electricity Produced", + DefaultValue=0.420, + UnitType=Units.ROYALTY_PER_ENERGY, + PreferredUnits=RoyaltyPerEnergyUnit.ROYALTYPERMW, + CurrentUnits=RoyaltyPerEnergyUnit.ROYALTYPERMW, + Required=False, + ToolTipText="Estimated total royalty per MW of electricity produced, per https://geothermal.org/resources/geothermal-basics" + ) + # local variable initialization self.CAPEX_cost_electricity_plant = 0.0 self.CAPEX_cost_heat_plant = 0.0 @@ -1813,7 +1848,27 @@ def __init__(self, model: Model): ) self.jobs_created = self.OutputParameterDict[self.jobs_created.Name] = OutputParameter( Name="Estimated Jobs Created", - UnitType=Units.NONE, + UnitType=Units.JOBS, + CurrentUnits=JobsUnit.JOBS, + PreferredUnits=JobsUnit.JOBS + ) + self.property_tax_created = self.OutputParameterDict[self.property_tax_created.Name] = OutputParameter( + Name="Estimated amount of property tax that will be paid", + UnitType=Units.CURRENCY, + CurrentUnits=CurrencyUnit.MDOLLARS, + PreferredUnits=CurrencyUnit.MDOLLARS + ) + self.total_royalties_created = self.OutputParameterDict[self.total_royalties_created.Name] = OutputParameter( + Name="Estimated total royalties that will have to be paid", + UnitType=Units.CURRENCY, + CurrentUnits=CurrencyUnit.MDOLLARS, + PreferredUnits=CurrencyUnit.MDOLLARS + ) + self.gov_royalties_created = self.OutputParameterDict[self.gov_royalties_created.Name] = OutputParameter( + Name="Estimated governmental royalties to be paid", + UnitType=Units.CURRENCY, + CurrentUnits=CurrencyUnit.MDOLLARS, + PreferredUnits=CurrencyUnit.MDOLLARS ) model.logger.info(f'Complete {__class__!s}: {sys._getframe().f_code.co_name}') @@ -2892,6 +2947,21 @@ def Calculate(self, model: Model) -> None: np.average(model.surfaceplant.ElectricityProduced.quantity().to( 'MW').magnitude * self.jobs_created_per_MW_electricity.value)) + # https://github.com/NREL/GEOPHIRES-X/issues/232 + self.property_tax_created.value = ( + np.average(model.surfaceplant.ElectricityProduced.quantity().to( + 'MW').magnitude * self.property_tax_per_MW_electricity.value)) + + # https://github.com/NREL/GEOPHIRES-X/issues/232 + self.total_royalties_created.value = ( + np.average(model.surfaceplant.ElectricityProduced.quantity().to( + 'MW').magnitude * self.total_royalty_per_MW_electricity.value)) + + # https://github.com/NREL/GEOPHIRES-X/issues/232 + self.gov_royalties_created.value = ( + np.average(model.surfaceplant.ElectricityProduced.quantity().to( + 'MW').magnitude * self.gov_royalty_per_MW_electricity.value)) + model.logger.info(f'complete {__class__!s}: {sys._getframe().f_code.co_name}') def __str__(self): diff --git a/src/geophires_x/Outputs.py b/src/geophires_x/Outputs.py index adbeec34..d8751fd2 100644 --- a/src/geophires_x/Outputs.py +++ b/src/geophires_x/Outputs.py @@ -1641,7 +1641,11 @@ def PrintOutputs(self, model: Model): f.write(f' CHP: Percent cost allocation for electrical plant: {model.economics.CAPEX_heat_electricity_plant_ratio.value*100.0:10.2f} %\n') if model.surfaceplant.enduse_option.value in [EndUseOptions.ELECTRICITY]: - f.write(f' Estimated Jobs Created: {model.economics.jobs_created.value}\n') + f.write(f' Estimates from https://geothermal.org/resources/geothermal-basics:' + NL) + f.write(f' Estimated Jobs Created: {model.economics.jobs_created.value}' + NL) + f.write(f' Estimated Proporty tax that will be paid: {model.economics.property_tax_created.value:10.2f }' + model.economics.property_tax_created.PreferredUnits.value + NL) + f.write(f' Estimated total royalties that will be paid: {model.economics.total_royalties_created.value:10.2f }' + model.economics.total_royalties_created.PreferredUnits.value + NL) + f.write(f' Estimated government royalties to be paid: {model.economics.gov_royalties_created.value:10.2f }' + model.economics.gov_royalties_created.PreferredUnits.value+ NL) f.write(NL) diff --git a/src/geophires_x/Units.py b/src/geophires_x/Units.py index b6d60561..8a344e39 100644 --- a/src/geophires_x/Units.py +++ b/src/geophires_x/Units.py @@ -60,6 +60,9 @@ class Units(IntEnum): POWERPERUNITVOLUME = auto() DECAY_RATE=auto() INFLATION_RATE=auto() + JOBS_PER_ENERGY=auto() + ROYALTY_PER_ENERGY=auto() + JOBS = auto() class TemperatureUnit(str, Enum): @@ -350,3 +353,19 @@ class Decay_RateUnit(str,Enum): class Inflation_RateUnit(str,Enum): """Decay rate Units""" KPASCALPERYEAR = "kPa/yr" + + +class JobsPerEnergyUnit(str,Enum): + """Jobs per energy Units""" + JOBSPERKW = "jobs/kW" + JOBSPERMW = "jobs/MW" + JOBSPERGW = "jobs/GW" + +class RoyaltyPerEnergyUnit(str,Enum): + """Royalty per energy Units""" + ROYALTYPERMW = "MUSD/MW" + ROYALTYPERKW = "MUSD/kW" + +class JobsUnit(str,Enum): + """Jobs Units""" + JOBS = "jobs" From 72013e4089089a083458661a5672918953af21ec Mon Sep 17 00:00:00 2001 From: Malcolm Ross Date: Wed, 26 Jun 2024 14:07:23 -0500 Subject: [PATCH 7/7] Added new estimate from GR website fixed typo --- src/geophires_x/Outputs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/geophires_x/Outputs.py b/src/geophires_x/Outputs.py index d8751fd2..d5931d02 100644 --- a/src/geophires_x/Outputs.py +++ b/src/geophires_x/Outputs.py @@ -1643,7 +1643,7 @@ def PrintOutputs(self, model: Model): if model.surfaceplant.enduse_option.value in [EndUseOptions.ELECTRICITY]: f.write(f' Estimates from https://geothermal.org/resources/geothermal-basics:' + NL) f.write(f' Estimated Jobs Created: {model.economics.jobs_created.value}' + NL) - f.write(f' Estimated Proporty tax that will be paid: {model.economics.property_tax_created.value:10.2f }' + model.economics.property_tax_created.PreferredUnits.value + NL) + f.write(f' Estimated Property tax that will be paid: {model.economics.property_tax_created.value:10.2f }' + model.economics.property_tax_created.PreferredUnits.value + NL) f.write(f' Estimated total royalties that will be paid: {model.economics.total_royalties_created.value:10.2f }' + model.economics.total_royalties_created.PreferredUnits.value + NL) f.write(f' Estimated government royalties to be paid: {model.economics.gov_royalties_created.value:10.2f }' + model.economics.gov_royalties_created.PreferredUnits.value+ NL)