From 3fb5b63676735f2696ddb5ce419eca8091999f54 Mon Sep 17 00:00:00 2001 From: Malcolm Ross Date: Thu, 4 Jan 2024 17:28:57 -0600 Subject: [PATCH] HIP RA refactor WIP --- src/geophires_x/.gitignore | 4 +- src/geophires_x/AGSEconomics.py | 8 +- src/geophires_x/AGSOutputs.py | 22 +- src/geophires_x/AGSSurfacePlant.py | 36 +- src/geophires_x/AGSWellBores.py | 24 +- src/geophires_x/Available Heat (fluid).png | Bin 0 -> 33305 bytes src/geophires_x/CLWellBores.py | 8 +- src/geophires_x/CylindricalReservoir.py | 4 +- src/geophires_x/Economics.py | 282 ++--- src/geophires_x/EconomicsAddOns.py | 38 +- src/geophires_x/EconomicsCCUS.py | 36 +- src/geophires_x/EconomicsS_DAC_GT.py | 38 +- .../GEOPHIRES-X_Output_Validation_Tool.py | 10 +- src/geophires_x/GEOPHIRESv3.py | 2 +- src/geophires_x/GeoPHIRESUtils.py | 18 +- src/geophires_x/MC_GEOPHIRES_Result.txt | 1 + src/geophires_x/MC_GeoPHIRES3.py | 48 +- src/geophires_x/Model.py | 77 +- src/geophires_x/OptionList.py | 29 +- src/geophires_x/Outputs.py | 217 ++-- src/geophires_x/OutputsAddOns.py | 4 +- src/geophires_x/OutputsCCUS.py | 6 +- src/geophires_x/OutputsS_DAC_GT.py | 2 +- src/geophires_x/Parameter.py | 16 +- .../Producible Electricity (fluid).png | Bin 0 -> 33344 bytes ...oducible Electricity-Unit Area (fluid).png | Bin 0 -> 28354 bytes src/geophires_x/Producible Heat (fluid).png | Bin 0 -> 31049 bytes .../Producible Heat-Unit Area (fluid).png | Bin 0 -> 31275 bytes src/geophires_x/Reservoir.py | 6 +- src/geophires_x/SUTRAEconomics.py | 14 +- src/geophires_x/SUTRAOutputs.py | 10 +- src/geophires_x/SUTRAReservoir.py | 8 +- src/geophires_x/SUTRAWellBores.py | 2 +- src/geophires_x/SurfacePlant.py | 994 +++++------------- .../SurfacePlantAbsorptionChiller.py | 146 +++ src/geophires_x/SurfacePlantDirectUseHeat.py | 101 ++ .../SurfacePlantDistrictHeating.py | 437 ++++++++ src/geophires_x/SurfacePlantDoubleFlash.py | 136 +++ src/geophires_x/SurfacePlantHeatPump.py | 136 +++ ...RASurfacePlant.py => SurfacePlantSUTRA.py} | 171 +-- src/geophires_x/SurfacePlantSingleFlash.py | 136 +++ src/geophires_x/SurfacePlantSubcriticalORC.py | 136 +++ .../SurfacePlantSupercriticalORC.py | 133 +++ src/geophires_x/TOUGH2Reservoir.py | 2 +- src/geophires_x/UPPReservoir.py | 8 +- src/geophires_x/Units.py | 13 + src/geophires_x/WellBores.py | 16 +- .../geophires_input_parameters.py | 2 +- src/geophires_x_schema_generator/__init__.py | 2 +- src/hip_ra/HIP_RA.py | 560 +++++++--- tests/HIP_RA_Unit_tests.py | 128 +-- tests/examples/HIPexample1.txt | 5 +- tests/examples/MC_HIP_Settings_file.txt | 9 +- tests/examples/example10_HP.txt | 3 +- tests/examples/example11_AC.txt | 4 +- tests/examples/example12_DH.txt | 3 +- 56 files changed, 2640 insertions(+), 1611 deletions(-) create mode 100644 src/geophires_x/Available Heat (fluid).png create mode 100644 src/geophires_x/MC_GEOPHIRES_Result.txt create mode 100644 src/geophires_x/Producible Electricity (fluid).png create mode 100644 src/geophires_x/Producible Electricity-Unit Area (fluid).png create mode 100644 src/geophires_x/Producible Heat (fluid).png create mode 100644 src/geophires_x/Producible Heat-Unit Area (fluid).png create mode 100644 src/geophires_x/SurfacePlantAbsorptionChiller.py create mode 100644 src/geophires_x/SurfacePlantDirectUseHeat.py create mode 100644 src/geophires_x/SurfacePlantDistrictHeating.py create mode 100644 src/geophires_x/SurfacePlantDoubleFlash.py create mode 100644 src/geophires_x/SurfacePlantHeatPump.py rename src/geophires_x/{SUTRASurfacePlant.py => SurfacePlantSUTRA.py} (59%) create mode 100644 src/geophires_x/SurfacePlantSingleFlash.py create mode 100644 src/geophires_x/SurfacePlantSubcriticalORC.py create mode 100644 src/geophires_x/SurfacePlantSupercriticalORC.py diff --git a/src/geophires_x/.gitignore b/src/geophires_x/.gitignore index 0cc6df7e..368e0973 100644 --- a/src/geophires_x/.gitignore +++ b/src/geophires_x/.gitignore @@ -1,6 +1,6 @@ -# This file tells git what files to ignore (e.g., you won't see them as +# This file tells git what files to ignore (reservoir_enthalpy.g., you won't see them as # untracked with "git status"). Add anything to it that can be cleared -# without any worry (e.g., by "git clean -Xdf"), because it can be +# without any worry (reservoir_enthalpy.g., by "git clean -Xdf"), because it can be # regenerated. Lines beginning with # are comments. You can also ignore # files on a per-repository basis by modifying the core.excludesfile # configuration option (see "git help config"). If you need to make git diff --git a/src/geophires_x/AGSEconomics.py b/src/geophires_x/AGSEconomics.py index c8a6d0aa..9b7e0a7e 100644 --- a/src/geophires_x/AGSEconomics.py +++ b/src/geophires_x/AGSEconomics.py @@ -3,9 +3,9 @@ import numpy as np import geophires_x.Model as Model import geophires_x.Economics as Economics -from .Parameter import floatParameter -from .Units import * -from .OptionList import WorkingFluid, EndUseOptions, EconomicModel +from geophires_x.Parameter import floatParameter +from geophires_x.Units import * +from geophires_x.OptionList import WorkingFluid, EndUseOptions, EconomicModel class AGSEconomics(Economics.Economics): @@ -113,7 +113,7 @@ def read_parameters(self, model: Model) -> None: # inputs we already have - needs to be set at ReadParameter time so values set at the latest possible time self.Discount_rate = model.economics.discountrate.value # same units are GEOPHIRES - self.Electricity_rate = model.surfaceplant.elecprice.value # same units are GEOPHIRES + self.Electricity_rate = model.surfaceplant.electricity_cost_to_buy.value # same units are GEOPHIRES model.logger.info("complete " + str(__class__) + ": " + sys._getframe().f_code.co_name) diff --git a/src/geophires_x/AGSOutputs.py b/src/geophires_x/AGSOutputs.py index 87f53de3..8ee4bfd5 100644 --- a/src/geophires_x/AGSOutputs.py +++ b/src/geophires_x/AGSOutputs.py @@ -31,7 +31,7 @@ def PrintOutputs(self, model: Model): # Deal with converting Units back to PreferredUnits, if required. # before we write the outputs, we go thru all the parameters for all of the objects and set the values # back to the units that the user entered the data in - # We do this because the value may be displayed in the output, and we want the user to recognize their value, + # reservoir_producible_electricity do this because the value may be displayed in the output, and we want the user to recognize their value, # not some converted value for obj in [model.reserv, model.wellbores, model.surfaceplant, model.economics]: for key in obj.ParameterDict: @@ -40,7 +40,7 @@ def PrintOutputs(self, model: Model): ConvertUnitsBack(param, model) # now we need to loop thru all thw output parameters to update their units to whatever units the user has specified. - # i.e., they may have specified that all LENGTH results must be in feet, so we need to convert + # i.reservoir_enthalpy., they may have specified that all LENGTH results must be in feet, so we need to convert # those from whatever LENGTH unit they are to feet. # same for all the other classes of units (TEMPERATURE, DENSITY, etc). @@ -61,7 +61,7 @@ def PrintOutputs(self, model: Model): f = scipy.interpolate.interp1d(np.arange(0, len(model.wellbores.PumpingPower.value)), model.wellbores.PumpingPower.value, fill_value="extrapolate") model.wellbores.PumpingPower.value = f(np.arange(0, len(model.wellbores.ProducedTemperature.value), 1.0)) - if model.surfaceplant.enduseoption.value != EndUseOptions.HEAT: + if model.surfaceplant.enduse_option.value != EndUseOptions.HEAT: if len(model.wellbores.PumpingPower.value) != len(model.wellbores.ProducedTemperature.value): f = scipy.interpolate.interp1d(np.arange(0, len(model.wellbores.PumpingPower.value)), model.wellbores.PumpingPower.value, fill_value="extrapolate") @@ -155,14 +155,14 @@ def PrintOutputs(self, model: Model): f.write(' ******************************\n') f.write(' * POWER GENERATION PROFILE *\n') f.write(' ******************************\n') - if model.surfaceplant.enduseoption.value == EndUseOptions.ELECTRICITY: # only electricity + if model.surfaceplant.enduse_option.value == EndUseOptions.ELECTRICITY: # only electricity f.write( ' YEAR THERMAL GEOFLUID PUMP NET FIRST LAW\n') f.write( ' DRAWDOWN TEMPERATURE POWER POWER EFFICIENCY\n') f.write( " (" + model.wellbores.ProducedTemperature.CurrentUnits.value + ") (" + model.wellbores.PumpingPower.CurrentUnits.value + ") (" + model.surfaceplant.NetElectricityProduced.CurrentUnits.value + ") (%)\n") - for i in range(0, model.surfaceplant.plantlifetime.value): + for i in range(0, model.surfaceplant.plant_lifetime.value): f.write( ' {0:2.0f} {1:8.4f} {2:8.2f} {3:8.4f} {4:8.4f} {5:8.4f}'.format( i + 1, @@ -172,11 +172,11 @@ def PrintOutputs(self, model: Model): model.wellbores.PumpingPower.value[i], model.surfaceplant.NetElectricityProduced.value[i], model.surfaceplant.FirstLawEfficiency.value[i] * 100) + NL) - elif model.surfaceplant.enduseoption.value == EndUseOptions.HEAT: # only direct-use + elif model.surfaceplant.enduse_option.value == EndUseOptions.HEAT: # only direct-use f.write(' YEAR THERMAL GEOFLUID PUMP NET\n') f.write(' DRAWDOWN TEMPERATURE POWER HEAT\n') f.write(' (deg C) (MW) (MW)\n') - for i in range(0, model.surfaceplant.plantlifetime.value - 1): + for i in range(0, model.surfaceplant.plant_lifetime.value - 1): f.write( ' {0:2.0f} {1:8.4f} {2:8.2f} {3:8.4f} {4:8.4f}'.format( i, @@ -193,14 +193,14 @@ def PrintOutputs(self, model: Model): ' * HEAT AND/OR ELECTRICITY EXTRACTION AND GENERATION PROFILE *\n') f.write( ' ***************************************************************\n') - if model.surfaceplant.enduseoption.value == EndUseOptions.ELECTRICITY: # only electricity + if model.surfaceplant.enduse_option.value == EndUseOptions.ELECTRICITY: # only electricity f.write( ' YEAR ELECTRICITY HEAT RESERVOIR PERCENTAGE OF\n') f.write( ' PROVIDED EXTRACTED HEAT CONTENT TOTAL HEAT MINED\n') f.write( ' (GWh/year) (GWh/year) (10^15 J) (%)\n') - for i in range(0, model.surfaceplant.plantlifetime.value): + for i in range(0, model.surfaceplant.plant_lifetime.value): f.write( ' {0:2.0f} {1:8.1f} {2:8.1f} {3:8.2f} {4:8.2f}'.format( i + 1, @@ -208,14 +208,14 @@ def PrintOutputs(self, model: Model): model.surfaceplant.HeatkWhExtracted.value[i] / 1E6, model.surfaceplant.RemainingReservoirHeatContent.value[i], (model.reserv.InitialReservoirHeatContent.value - model.surfaceplant.RemainingReservoirHeatContent.value[i]) * 100 / model.reserv.InitialReservoirHeatContent.value) + NL) - elif model.surfaceplant.enduseoption.value == EndUseOptions.HEAT: # only direct-use + elif model.surfaceplant.enduse_option.value == EndUseOptions.HEAT: # only direct-use f.write( ' YEAR HEAT HEAT RESERVOIR CUM PERCENTAGE OF\n') f.write( ' PROVIDED EXTRACTED HEAT CONTENT TOTAL HEAT MINED\n') f.write( ' (GWh/year) (GWh/year) (10^15 J) (%)\n') - for i in range(0, model.surfaceplant.plantlifetime.value - 1): + for i in range(0, model.surfaceplant.plant_lifetime.value - 1): f.write( ' {0:2.0f} {1:8.1f} {2:8.1f} {3:8.2f} {4:8.2f}'.format( i + 1, diff --git a/src/geophires_x/AGSSurfacePlant.py b/src/geophires_x/AGSSurfacePlant.py index 6ae0fda7..37cd0d60 100644 --- a/src/geophires_x/AGSSurfacePlant.py +++ b/src/geophires_x/AGSSurfacePlant.py @@ -1,11 +1,7 @@ -import sys -import os -import numpy as np -import geophires_x.Model as Model from geophires_x.WellBores import * -from .Parameter import floatParameter, OutputParameter -from .Units import * -from .OptionList import WorkingFluid, EndUseOptions +from geophires_x.Parameter import floatParameter, OutputParameter +from geophires_x.Units import * +from geophires_x.OptionList import WorkingFluid, EndUseOptions from geophires_x.SurfacePlant import SurfacePlant as SurfacePlant # code from Koenraad @@ -322,16 +318,16 @@ def read_parameters(self, model: Model) -> None: model.logger.info("No parameters read because no content provided") # inputs we already have - needs to be set at ReadParameter time so values set at the latest possible time - self.End_use = model.surfaceplant.enduseoption.value # same units are GEOPHIRES - self.Pump_efficiency = model.surfaceplant.pumpeff.value # same units are GEOPHIRES - self.Lifetime = int(model.surfaceplant.plantlifetime.value) # same units are GEOPHIRES - self.T0 = model.surfaceplant.Tenv.value + 273.15 # convert Celsius to Kelvin + self.End_use = model.surfaceplant.enduse_option.value # same units are GEOPHIRES + self.Pump_efficiency = model.surfaceplant.pump_efficiency.value # same units are GEOPHIRES + self.Lifetime = int(model.surfaceplant.plant_lifetime.value) # same units are GEOPHIRES + self.T0 = model.surfaceplant.ambient_temperature.value + 273.15 # convert Celsius to Kelvin self.Discount_rate = model.economics.discountrate.value # same units are GEOPHIRES # initialize some arrays - self.HeatkWhProduced.value = [0.0] * model.surfaceplant.plantlifetime.value # initialize the array - self.HeatkWhExtracted.value = [0.0] * model.surfaceplant.plantlifetime.value # initialize the array - self.PumpingkWh.value = [0.0] * model.surfaceplant.plantlifetime.value # initialize the array + self.HeatkWhProduced.value = [0.0] * model.surfaceplant.plant_lifetime.value # initialize the array + self.HeatkWhExtracted.value = [0.0] * model.surfaceplant.plant_lifetime.value # initialize the array + self.PumpingkWh.value = [0.0] * model.surfaceplant.plant_lifetime.value # initialize the array model.logger.info("complete " + str(__class__) + ": " + sys._getframe().f_code.co_name) @@ -758,26 +754,26 @@ def Calculate(self, model: Model) -> None: self.HeatExtracted.value = self.HeatExtracted.value / 1000.0 # useful direct-use heat provided to application [MWth] self.HeatProduced.value = self.HeatExtracted.value * self.enduseefficiencyfactor.value - for i in range(0, self.plantlifetime.value): + for i in range(0, self.plant_lifetime.value): self.HeatkWhExtracted.value[i] = np.trapz(self.HeatExtracted.value[ (i * model.economics.timestepsperyear.value):(( i + 1) * model.economics.timestepsperyear.value) + 1], - dx=1. / model.economics.timestepsperyear.value * 365. * 24.) * 1000. * self.utilfactor.value + dx=1. / model.economics.timestepsperyear.value * 365. * 24.) * 1000. * self.utilization_factor.value self.PumpingkWh.value[i] = np.trapz(model.wellbores.PumpingPower.value[ (i * model.economics.timestepsperyear.value):(( i + 1) * model.economics.timestepsperyear.value) + 1], - dx=1. / model.economics.timestepsperyear.value * 365. * 24.) * 1000. * self.utilfactor.value + dx=1. / model.economics.timestepsperyear.value * 365. * 24.) * 1000. * self.utilization_factor.value self.RemainingReservoirHeatContent.value = model.reserv.InitialReservoirHeatContent.value - np.cumsum( self.HeatkWhExtracted.value) * 3600 * 1E3 / 1E15 if self.End_use != EndUseOptions.ELECTRICITY: - self.HeatkWhProduced.value = np.zeros(self.plantlifetime.value) - for i in range(0, self.plantlifetime.value): + self.HeatkWhProduced.value = np.zeros(self.plant_lifetime.value) + for i in range(0, self.plant_lifetime.value): self.HeatkWhProduced.value[i] = np.trapz(self.HeatProduced.value[ (0 + i * model.economics.timestepsperyear.value):(( i + 1) * model.economics.timestepsperyear.value) + 1], - dx=1. / model.economics.timestepsperyear.value * 365. * 24.) * 1000. * self.utilfactor.value + dx=1. / model.economics.timestepsperyear.value * 365. * 24.) * 1000. * self.utilization_factor.value else: # copy some arrays so we have a GEOPHIRES equivalent self.TotalkWhProduced.value = self.Annual_electricity_production.copy() diff --git a/src/geophires_x/AGSWellBores.py b/src/geophires_x/AGSWellBores.py index e014703a..252665fe 100644 --- a/src/geophires_x/AGSWellBores.py +++ b/src/geophires_x/AGSWellBores.py @@ -77,7 +77,7 @@ def __init__(self, fname, case, fluid): # dim = Mdot x L2 x L1 x grad x D x Tinj x k self.Wt = file[output_loc + "Wt"][:] # int mdot * dh dt - self.We = file[output_loc + "We"][:] # int mdot * (dh - Too * ds) dt + self.We = file[output_loc + "reservoir_producible_electricity"][:] # int mdot * (dh - Too * ds) dt self.GWhr = 1e6 * 3_600_000.0 @@ -853,7 +853,7 @@ def CalculateNonverticalPressureDrop(self, model, time_operation: float, time_ma :rtype: tuple """ friction = 0.0 - NonverticalPressureDrop = [0.0] * model.surfaceplant.plantlifetime.value # initialize the array + NonverticalPressureDrop = [0.0] * model.surfaceplant.plant_lifetime.value # initialize the array while time_operation <= time_max: year = math.trunc(time_operation / al) @@ -927,13 +927,13 @@ def Calculate(self, model: Model) -> None: self.y_well = 0.5 * self.y_boundary # Nonvertical wellbore in the center self.z_well = 0.5 * self.z_boundary # Nonvertical wellbore in the center self.al = 365.0 / 4.0 * model.economics.timestepsperyear.value - self.time_max = model.surfaceplant.plantlifetime.value * 365.0 + self.time_max = model.surfaceplant.plant_lifetime.value * 365.0 self.rhorock = model.reserv.rhorock.value self.cprock = model.reserv.cprock.value self.alpha_rock = model.reserv.krock.value / model.reserv.rhorock.value / model.reserv.cprock.value * 24.0 * 3600.0 # initialize the arrays - self.NonverticalProducedTemperature.value = [0.0] * model.surfaceplant.plantlifetime.value - self.NonverticalPressureDrop.value = [0.0] * model.surfaceplant.plantlifetime.value + self.NonverticalProducedTemperature.value = [0.0] * model.surfaceplant.plant_lifetime.value + self.NonverticalPressureDrop.value = [0.0] * model.surfaceplant.plant_lifetime.value t = self.time_operation.value while self.time_operation.value <= self.time_max: @@ -961,7 +961,7 @@ def Calculate(self, model: Model) -> None: model.reserv.cprock.value, self.prodwelldiam.value, model.reserv.timevector.value, - model.surfaceplant.utilfactor.value, + model.surfaceplant.utilization_factor.value, self.prodwellflowrate.value, model.reserv.cpwater.value, model.reserv.Trock.value, @@ -991,13 +991,13 @@ def Calculate(self, model: Model) -> None: self.rhowaterinj, self.rhowaterprod, self.rhowaterprod, model.reserv.depth.value, self.prodwellflowrate.value, self.prodwelldiam.value, self.impedance.value, - self.nprod.value, model.reserv.waterloss.value, model.surfaceplant.pumpeff.value) + self.nprod.value, model.reserv.waterloss.value, model.surfaceplant.pump_efficiency.value) self.DPOverall.value, DowngoingPumpingPower, self.DPProdWell.value, self.DPReserv.value, self.DPBouyancy.value = \ ProdPressureDropsAndPumpingPowerUsingImpedenceModel( f3, vprod, self.rhowaterprod, self.rhowaterinj, model.reserv.rhowater.value, model.reserv.depth.value, self.prodwellflowrate.value, self.injwelldiam.value, self.impedance.value, - self.nprod.value, model.reserv.waterloss.value, model.surfaceplant.pumpeff.value) + self.nprod.value, model.reserv.waterloss.value, model.surfaceplant.pump_efficiency.value) else: # PI is used for both the verticals UpgoingPumpingPower, self.PumpingPowerProd.value, self.DPProdWell.value, self.Pprodwellhead.value = \ @@ -1007,7 +1007,7 @@ def Calculate(self, model: Model) -> None: model.reserv.Trock.value, model.reserv.Tsurf.value, model.reserv.depth.value, model.reserv.averagegradient.value, self.ppwellhead.value, self.PI.value, self.prodwellflowrate.value, f3, vprod, - self.prodwelldiam.value, self.nprod.value, model.surfaceplant.pumpeff.value, + self.prodwelldiam.value, self.nprod.value, model.surfaceplant.pump_efficiency.value, self.rhowaterprod) DowngoingPumpingPower, ppp2, dppw, ppwh = ProdPressureDropAndPumpingPowerUsingIndexes( @@ -1016,7 +1016,7 @@ def Calculate(self, model: Model) -> None: model.reserv.Trock.value, model.reserv.Tsurf.value, model.reserv.depth.value, model.reserv.averagegradient.value, self.ppwellhead.value, self.PI.value, self.prodwellflowrate.value, f3, vprod, - self.injwelldiam.value, self.nprod.value, model.surfaceplant.pumpeff.value, + self.injwelldiam.value, self.nprod.value, model.surfaceplant.pump_efficiency.value, self.rhowaterinj) # Calculate Nonvertical Pressure Drop @@ -1030,7 +1030,7 @@ def Calculate(self, model: Model) -> None: # calculate nonvertical well pumping power needed[MWe] NonverticalPumpingPower = self.NonverticalPressureDrop.value * self.nprod.value * \ self.prodwellflowrate.value / self.rhowaterprod / \ - model.surfaceplant.pumpeff.value / 1E3 # [MWe] total pumping power for nonvertical section + model.surfaceplant.pump_efficiency.value / 1E3 # [MWe] total pumping power for nonvertical section NonverticalPumpingPower = np.array( [0. if x < 0. else x for x in NonverticalPumpingPower]) # cannot be negative so set to 0 @@ -1073,7 +1073,7 @@ def Calculate(self, model: Model) -> None: # set pumping power to zero for all times, assuming that the thermosphere wil always # make pumping of working fluid unnecessary self.PumpingPower.value = [0.0] * (len(self.DPOverall.value)) - self.PumpingPower.value = self.DPOverall.value * self.prodwellflowrate.value / rhowater / model.surfaceplant.pumpeff.value / 1E3 + self.PumpingPower.value = self.DPOverall.value * self.prodwellflowrate.value / rhowater / model.surfaceplant.pump_efficiency.value / 1E3 # in GEOPHIRES v1.2, negative pumping power values become zero (b/c we are not generating electricity) = thermosiphon is happening! self.PumpingPower.value = [0. if x < 0. else x for x in self.PumpingPower.value] diff --git a/src/geophires_x/Available Heat (fluid).png b/src/geophires_x/Available Heat (fluid).png new file mode 100644 index 0000000000000000000000000000000000000000..30f8ce6043f00539146e9095f3f723de7477e4e3 GIT binary patch literal 33305 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

$kKr|y?`}Lu*Mhnm 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 #04-O?gtPTP#J|%+2Ra5e6^*<}43l=RB zF?P>rDuvG64i0_U!}C2b72KdW(oQZ1qx((XGP#)I@n1;uR_PG?yAZJ(&2H?@f5vOo z`E{aF9l_>G-ReGdh{;Ur*51qtKrzZS*vH#X;HLZ?jW+IVu>S*I8?9^#L17NhLS+66 zU{0Dm#X{&r0aLcIu?a9Q2E!!B?ELw|sfFK>l3>Sc3K50{ji;t7)-g$5HxvczuI;2Z zqpp=>=~POh>|Ez^`F;vNO&Swi97Q!_SnD9<>T$KJ-5;Xt#K3nM7#Z1}-yLoUd_*=q zFhL|<+`rj^rX6XDOEL|5tC~!egn$(Et9z)w0o#(-A)+&P!BPY?uEviv1r5ieNf9Q@ zkjWf%MBd_i(wW~J1=7H;IHCFC!bdLH&RauuC zPL^hX{D?Z$6^Kuer(nfiRaF$g=mkG?19E;nrAF>U&)dc%UV!Rwszw{wY3yJ!45>ED zKx{ca`pFoTL--K8YugTY=TLSNe9VX6LAIGFOv9a3gg-&|z18Ya@{>?Riz;>+Zui&Lc#z9Rd86@X>npQhRr~U54tTWZ z)HnakvEqSJm|P6zV`2(7AHDwoEiN*^-yB!7O7i04^tK~Nlve6mZ{D=B5izf6WJ%Ir zBd|^ogb)b~qbNc{G(L`*$eD?znWqrOCCGwV8RGh)}yR@9u(ya`2vo7u|F(VnwV~yA&qJ(4?;3A5X zsTWgo2Pt0;^WsQ<)}RO3S4`eGcce?o<1^u)2-mKdkAW_1m4^Vg-H?rxurN87FZXM6 z&SXQv7j~V#GzA?4j5CymgFolZIIry;aoU4B!sJ~z7BO1?_3FIEBo23BDgZY!4BlA> zf|hrE229l<=ND(-){w!K?s3HESg5q_HuZQfOi2P-vC;o&DX}Q2M*s(668QzFR`)(H z35DlTR9~-6K?zWZk|}hhs!g7frxo-sQ9PXnx-4_X( z_Wc&N9>Cw)gwC;YhCg3lXEgp?o5J~EvLB3RG8!+UMZi9NDUHT4L7O&VxkL9lp6|IX%`rFvMX}BD >? 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 None: CurrentUnits=TemperatureUnit.CELSIUS ) self.HorizontalProducedTemperature.value = [ - 0.0] * model.surfaceplant.plantlifetime.value # initialize the array + 0.0] * model.surfaceplant.plant_lifetime.value # initialize the array self.HorizontalPressureDrop = self.OutputParameterDict[self.HorizontalPressureDrop.Name] = OutputParameter( Name="Horizontal Pressure Drop", value=[0.0], @@ -292,7 +292,7 @@ def __init__(self, model: Model) -> None: PreferredUnits=PressureUnit.KPASCAL, CurrentUnits=PressureUnit.KPASCAL ) - self.HorizontalPressureDrop.value = [0.0] * model.surfaceplant.plantlifetime.value # initialize the array + self.HorizontalPressureDrop.value = [0.0] * model.surfaceplant.plant_lifetime.value # initialize the array model.logger.info("Complete " + str(__class__) + ": " + sys._getframe().f_code.co_name) def read_parameters(self, model: Model) -> None: @@ -324,7 +324,7 @@ def read_parameters(self, model: Model) -> None: self.y_well = 0.5 * self.y_boundary # Horizontal wellbore in the center self.z_well = 0.5 * self.z_boundary # Horizontal wellbore in the center self.al = 365.0 / 4.0 * model.economics.timestepsperyear.value - self.time_max = model.surfaceplant.plantlifetime.value * 365.0 + self.time_max = model.surfaceplant.plant_lifetime.value * 365.0 self.rhorock = model.reserv.rhorock.value self.cprock = model.reserv.cprock.value self.alpha_rock = model.reserv.krock.value / model.reserv.rhorock.value / model.reserv.cprock.value * 24.0 * 3600.0 @@ -396,7 +396,7 @@ def Calculate(self, model: Model) -> None: model.wellbores.DPOverall.value = model.wellbores.DPOverall.value + self.HorizontalPressureDrop.value # recalculate pumping power [MWe] (approximate) - model.wellbores.PumpingPower.value = model.wellbores.DPOverall.value * self.q_circulation / rhowater / model.surfaceplant.pumpeff.value / 1E3 + model.wellbores.PumpingPower.value = model.wellbores.DPOverall.value * self.q_circulation / rhowater / model.surfaceplant.pump_efficiency.value / 1E3 # in GEOPHIRES v1.2, negative pumping power values become zero # (b/c we are not generating electricity) = thermosiphon is happening! diff --git a/src/geophires_x/CylindricalReservoir.py b/src/geophires_x/CylindricalReservoir.py index e1f3a82f..a28506fa 100644 --- a/src/geophires_x/CylindricalReservoir.py +++ b/src/geophires_x/CylindricalReservoir.py @@ -213,8 +213,8 @@ def Calculate(self, model:Model) -> None: model.logger.info(f"Init {str(__class__)}: {sys._getframe().f_code.co_name}") # specify time-stepping vectors - self.timevector.value = np.linspace(0, model.surfaceplant.plantlifetime.value, - model.economics.timestepsperyear.value*model.surfaceplant.plantlifetime.value) + self.timevector.value = np.linspace(0, model.surfaceplant.plant_lifetime.value, + model.economics.timestepsperyear.value * model.surfaceplant.plant_lifetime.value) self.averagegradient.value = self.gradient.value[0] self.Trock.value = self.Tsurf.value + (self.gradient.value[0] * (self.InputDepth.value * 1000.0)) diff --git a/src/geophires_x/Economics.py b/src/geophires_x/Economics.py index 282c5c82..2ef8889d 100644 --- a/src/geophires_x/Economics.py +++ b/src/geophires_x/Economics.py @@ -4,9 +4,9 @@ import numpy as np import numpy_financial as npf import geophires_x.Model as Model -from .OptionList import Configuration, WellDrillingCostCorrelation, EconomicModel, EndUseOptions, PowerPlantType -from .Parameter import intParameter, floatParameter, OutputParameter, ReadParameter, boolParameter -from .Units import * +from geophires_x.OptionList import Configuration, WellDrillingCostCorrelation, EconomicModel, EndUseOptions, PlantType +from geophires_x.Parameter import intParameter, floatParameter, OutputParameter, ReadParameter, boolParameter +from geophires_x.Units import * def BuildPricingModel(plantlifetime: int, StartYear: int, StartPrice: float, EndPrice: float, @@ -133,151 +133,151 @@ def CalculateLCOELCOH(self, model: Model) -> tuple: # Calculate LCOE/LCOH/LCOC if self.econmodel.value == EconomicModel.FCR: - if model.surfaceplant.enduseoption.value == EndUseOptions.ELECTRICITY: + if model.surfaceplant.enduse_option.value == EndUseOptions.ELECTRICITY: LCOE = (self.FCR.value * (1 + self.inflrateconstruction.value) * self.CCap.value + self.Coam.value) / \ np.average(model.surfaceplant.NetkWhProduced.value) * 1E8 # cents/kWh - elif model.surfaceplant.enduseoption.value == EndUseOptions.HEAT: + elif model.surfaceplant.enduse_option.value == EndUseOptions.HEAT: LCOH = (self.FCR.value * (1 + self.inflrateconstruction.value) * self.CCap.value + self.Coam.value + self.averageannualpumpingcosts.value) / np.average( model.surfaceplant.HeatkWhProduced.value) * 1E8 # cents/kWh LCOH = LCOH * 2.931 # $/Million Btu - elif model.surfaceplant.enduseoption.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, - EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICTY, - EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICTY, - EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, - EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, - EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICTY]: # co-gen + elif model.surfaceplant.enduse_option.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, + EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICITY, + EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICITY, + EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, + EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, + EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICITY]: # co-gen # heat sales is additional income revenue stream - if model.surfaceplant.enduseoption.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, - EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, - EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT]: + if model.surfaceplant.enduse_option.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, + EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, + EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT]: averageannualheatincome = np.average( - self.HeatkWhProduced.value) * self.heatprice.value / 1E6 # M$/year ASSUMING heatprice IS IN $/KWH FOR HEAT SALES + self.HeatkWhProduced.value) * self.heat_price.value / 1E6 # M$/year ASSUMING heat_price IS IN $/KWH FOR HEAT SALES LCOE = (self.FCR.value * ( 1 + self.inflrateconstruction.value) * self.CCap.value + self.Coam.value - averageannualheatincome) / np.average( model.surfaceplant.NetkWhProduced.value) * 1E8 # cents/kWh - elif model.surfaceplant.enduseoption.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICTY, - EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICTY, - EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICTY]: # electricity sales is additional income revenue stream + elif model.surfaceplant.enduse_option.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICITY, + EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICITY, + EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICITY]: # electricity sales is additional income revenue stream averageannualelectricityincome = np.average( - model.surfaceplant.NetkWhProduced.value) * model.surfaceplant.elecprice.value / 1E6 # M$/year + model.surfaceplant.NetkWhProduced.value) * model.surfaceplant.electricity_cost_to_buy.value / 1E6 # M$/year LCOH = (self.CCap.value + self.Coam.value - averageannualelectricityincome) / np.average( model.surfaceplant.HeatkWhProduced.value) * 1E8 # cents/kWh LCOH = LCOH * 2.931 # $/MMBTU - elif model.surfaceplant.enduseoption.value == EndUseOptions.ABSORPTION_CHILLER: + elif model.surfaceplant.plant_type.value == PlantType.ABSORPTION_CHILLER: LCOC = (self.FCR.value * ( 1 + self.inflrateconstruction.value) * self.CCap.value + self.Coam.value + self.averageannualpumpingcosts.value) / np.average( - model.surfaceplant.CoolingkWhProduced.value) * 1E8 # cents/kWh + model.surfaceplant.cooling_kWh_Produced.value) * 1E8 # cents/kWh LCOC = LCOC * 2.931 # $/Million Btu - elif model.surfaceplant.enduseoption.value == EndUseOptions.HEAT_PUMP: + elif model.surfaceplant.plant_type.value == PlantType.HEAT_PUMP: LCOH = (self.FCR.value * ( 1 + self.inflrateconstruction.value) * self.CCap.value + self.Coam.value + self.averageannualpumpingcosts.value + self.averageannualheatpumpelectricitycost.value) / np.average( model.surfaceplant.HeatkWhProduced.value) * 1E8 # cents/kWh LCOH = LCOH * 2.931 # $/Million Btu - elif model.surfaceplant.enduseoption.value == EndUseOptions.DISTRICT_HEATING: + elif model.surfaceplant.plant_type.value == PlantType.DISTRICT_HEATING: LCOH = (self.FCR.value * ( - 1 + self.inflrateconstruction.value) * self.CCap.value + self.Coam.value + self.averageannualpumpingcosts.value + self.averageannualngcost.value) / model.surfaceplant.annualheatingdemand.value * 1E2 # cents/kWh + 1 + self.inflrateconstruction.value) * self.CCap.value + self.Coam.value + self.averageannualpumpingcosts.value + self.averageannualngcost.value) / model.surfaceplant.annual_heating_demand.value * 1E2 # cents/kWh LCOH = LCOH * 2.931 # $/Million Btu elif self.econmodel.value == EconomicModel.STANDARDIZED_LEVELIZED_COST: discountvector = 1. / np.power(1 + self.discountrate.value, - np.linspace(0, model.surfaceplant.plantlifetime.value - 1, - model.surfaceplant.plantlifetime.value)) - if model.surfaceplant.enduseoption.value == EndUseOptions.ELECTRICITY: + np.linspace(0, model.surfaceplant.plant_lifetime.value - 1, + model.surfaceplant.plant_lifetime.value)) + if model.surfaceplant.enduse_option.value == EndUseOptions.ELECTRICITY: LCOE = ((1 + self.inflrateconstruction.value) * self.CCap.value + np.sum( self.Coam.value * discountvector)) / np.sum( model.surfaceplant.NetkWhProduced.value * discountvector) * 1E8 # cents/kWh - elif model.surfaceplant.enduseoption.value == EndUseOptions.HEAT: + elif model.surfaceplant.enduse_option.value == EndUseOptions.HEAT: self.averageannualpumpingcosts.value = np.average( - model.surfaceplant.PumpingkWh.value) * model.surfaceplant.elecprice.value / 1E6 # M$/year + model.surfaceplant.PumpingkWh.value) * model.surfaceplant.electricity_cost_to_buy.value / 1E6 # M$/year LCOH = ((1 + self.inflrateconstruction.value) * self.CCap.value + np.sum(( - self.Coam.value + model.surfaceplant.PumpingkWh.value * model.surfaceplant.elecprice.value / 1E6) * discountvector)) / np.sum( + self.Coam.value + model.surfaceplant.PumpingkWh.value * model.surfaceplant.electricity_cost_to_buy.value / 1E6) * discountvector)) / np.sum( model.surfaceplant.HeatkWhProduced.value * discountvector) * 1E8 # cents/kWh LCOH = LCOH * 2.931 # $/MMBTU - elif model.surfaceplant.enduseoption.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, - EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICTY, - EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICTY, - EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, - EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, - EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICTY]: # co-gen - if model.surfaceplant.enduseoption.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, - EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, - EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT]: # heat sales is additional income revenue stream - annualheatincome = model.surfaceplant.HeatkWhProduced.value * model.surfaceplant.heatprice.value / 1E6 # M$/year ASSUMING heatprice IS IN $/KWH FOR HEAT SALES + elif model.surfaceplant.enduse_option.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, + EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICITY, + EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICITY, + EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, + EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, + EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICITY]: # co-gen + if model.surfaceplant.enduse_option.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, + EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, + EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT]: # heat sales is additional income revenue stream + annualheatincome = model.surfaceplant.HeatkWhProduced.value * model.surfaceplant.heat_price.value / 1E6 # M$/year ASSUMING heat_price IS IN $/KWH FOR HEAT SALES LCOE = ((1 + self.inflrateconstruction.value) * self.CCap.value + np.sum( (self.Coam.value - annualheatincome) * discountvector)) / np.sum( model.surfaceplant.NetkWhProduced.value * discountvector) * 1E8 # cents/kWh - elif model.surfaceplant.enduseoption.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICTY, - EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICTY, - EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICTY]: # electricity sales is additional income revenue stream - annualelectricityincome = model.surfaceplant.NetkWhProduced.value * model.surfaceplant.elecprice.value / 1E6 # M$/year + elif model.surfaceplant.enduse_option.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICITY, + EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICITY, + EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICITY]: # electricity sales is additional income revenue stream + annualelectricityincome = model.surfaceplant.NetkWhProduced.value * model.surfaceplant.electricity_cost_to_buy.value / 1E6 # M$/year LCOH = ((1 + self.inflrateconstruction.value) * self.CCap.value + np.sum( (self.Coam.value - annualelectricityincome) * discountvector)) / np.sum( model.surfaceplant.HeatkWhProduced.value * discountvector) * 1E8 # cents/kWh LCOH = LCOH * 2.931 # $/MMBTU - elif model.surfaceplant.enduseoption.value == EndUseOptions.ABSORPTION_CHILLER: + elif model.surfaceplant.plant_type.value == PlantType.ABSORPTION_CHILLER: LCOC = ((1 + self.inflrateconstruction.value) * self.CCap.value + np.sum(( - self.Coam.value + model.surfaceplant.PumpingkWh.value * model.surfaceplant.elecprice.value / 1E6) * discountvector)) / np.sum( - model.surfaceplant.CoolingkWhProduced.value * discountvector) * 1E8 # cents/kWh + self.Coam.value + model.surfaceplant.PumpingkWh.value * model.surfaceplant.electricity_cost_to_buy.value / 1E6) * discountvector)) / np.sum( + model.surfaceplant.cooling_kWh_Produced.value * discountvector) * 1E8 # cents/kWh LCOC = LCOC * 2.931 # $/Million Btu - elif model.surfaceplant.enduseoption.value == EndUseOptions.HEAT_PUMP: + elif model.surfaceplant.plant_type.value == PlantType.HEAT_PUMP: LCOH = ((1 + self.inflrateconstruction.value) * self.CCap.value + np.sum( - (self.Coam.value + model.surfaceplant.PumpingkWh.value * model.surfaceplant.elecprice.value / 1E6 + \ - model.surfaceplant.HeatPumpElectricitykWhUsed.value * model.surfaceplant.elecprice.value / 1E6) * discountvector)) / np.sum( + (self.Coam.value + model.surfaceplant.PumpingkWh.value * model.surfaceplant.electricity_cost_to_buy.value / 1E6 + \ + model.surfaceplant.heat_pump_electricity_kwh_used.value * model.surfaceplant.electricity_cost_to_buy.value / 1E6) * discountvector)) / np.sum( model.surfaceplant.HeatkWhProduced.value * discountvector) * 1E8 # cents/kWh LCOH = LCOH * 2.931 # $/Million Btu - elif model.surfaceplant.enduseoption.value == EndUseOptions.DISTRICT_HEATING: + elif model.surfaceplant.plant_type.value == PlantType.DISTRICT_HEATING: LCOH = ((1 + self.inflrateconstruction.value) * self.CCap.value + np.sum( - (self.Coam.value + model.surfaceplant.PumpingkWh.value * model.surfaceplant.elecprice.value / 1E6 + \ + (self.Coam.value + model.surfaceplant.PumpingkWh.value * model.surfaceplant.electricity_cost_to_buy.value / 1E6 + \ self.annualngcost.value) * discountvector)) / np.sum( - model.surfaceplant.annualheatingdemand.value * discountvector) * 1E2 # cents/kWh + model.surfaceplant.annual_heating_demand.value * discountvector) * 1E2 # cents/kWh LCOH = LCOH * 2.931 # $/Million Btu elif self.econmodel.value == EconomicModel.BICYCLE: iave = self.FIB.value * self.BIR.value * (1 - self.CTR.value) + ( 1 - self.FIB.value) * self.EIR.value # average return on investment (tax and inflation adjusted) - CRF = iave / (1 - np.power(1 + iave, -model.surfaceplant.plantlifetime.value)) # capital recovery factor - inflationvector = np.power(1 + self.RINFL.value, np.linspace(1, model.surfaceplant.plantlifetime.value, - model.surfaceplant.plantlifetime.value)) - discountvector = 1. / np.power(1 + iave, np.linspace(1, model.surfaceplant.plantlifetime.value, - model.surfaceplant.plantlifetime.value)) + CRF = iave / (1 - np.power(1 + iave, -model.surfaceplant.plant_lifetime.value)) # capital recovery factor + inflationvector = np.power(1 + self.RINFL.value, np.linspace(1, model.surfaceplant.plant_lifetime.value, + model.surfaceplant.plant_lifetime.value)) + discountvector = 1. / np.power(1 + iave, np.linspace(1, model.surfaceplant.plant_lifetime.value, + model.surfaceplant.plant_lifetime.value)) NPVcap = np.sum((1 + self.inflrateconstruction.value) * self.CCap.value * CRF * discountvector) NPVfc = np.sum( (1 + self.inflrateconstruction.value) * self.CCap.value * self.PTR.value * inflationvector * discountvector) NPVit = np.sum(self.CTR.value / (1 - self.CTR.value) * (( - 1 + self.inflrateconstruction.value) * self.CCap.value * CRF - self.CCap.value / model.surfaceplant.plantlifetime.value) * discountvector) + 1 + self.inflrateconstruction.value) * self.CCap.value * CRF - self.CCap.value / model.surfaceplant.plant_lifetime.value) * discountvector) NPVitc = (1 + self.inflrateconstruction.value) * self.CCap.value * self.RITC.value / (1 - self.CTR.value) - if model.surfaceplant.enduseoption.value == EndUseOptions.ELECTRICITY: + if model.surfaceplant.enduse_option.value == EndUseOptions.ELECTRICITY: NPVoandm = np.sum(self.Coam.value * inflationvector * discountvector) NPVgrt = self.GTR.value / (1 - self.GTR.value) * (NPVcap + NPVoandm + NPVfc + NPVit - NPVitc) LCOE = (NPVcap + NPVoandm + NPVfc + NPVit + NPVgrt - NPVitc) / np.sum( model.surfaceplant.NetkWhProduced.value * inflationvector * discountvector) * 1E8 - elif model.surfaceplant.enduseoption.value == EndUseOptions.HEAT: - PumpingCosts = model.surfaceplant.PumpingkWh.value * model.surfaceplant.elecprice.value / 1E6 + elif model.surfaceplant.enduse_option.value == EndUseOptions.HEAT: + PumpingCosts = model.surfaceplant.PumpingkWh.value * model.surfaceplant.electricity_cost_to_buy.value / 1E6 NPVoandm = np.sum((self.Coam.value + PumpingCosts) * inflationvector * discountvector) NPVgrt = self.GTR.value / (1 - self.GTR.value) * (NPVcap + NPVoandm + NPVfc + NPVit - NPVitc) LCOH = (NPVcap + NPVoandm + NPVfc + NPVit + NPVgrt - NPVitc) / np.sum( model.surfaceplant.HeatkWhProduced.value * inflationvector * discountvector) * 1E8 LCOH = LCOH * 2.931 # $/MMBTU - elif model.surfaceplant.enduseoption.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, - EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICTY, - EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICTY, - EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, - EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, - EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICTY]: # co-gen - if model.surfaceplant.enduseoption.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, - EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, - EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT]: # heat sales is additional income revenue stream - annualheatincome = model.surfaceplant.HeatkWhProduced.value * model.surfaceplant.heatprice.value / 1E6 # M$/year ASSUMING ELECPRICE IS IN $/KWH FOR HEAT SALES + elif model.surfaceplant.enduse_option.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, + EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICITY, + EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICITY, + EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, + EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, + EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICITY]: # co-gen + if model.surfaceplant.enduse_option.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, + EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, + EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT]: # heat sales is additional income revenue stream + annualheatincome = model.surfaceplant.HeatkWhProduced.value * model.surfaceplant.heat_price.value / 1E6 # M$/year ASSUMING ELECPRICE IS IN $/KWH FOR HEAT SALES NPVoandm = np.sum(self.Coam.value * inflationvector * discountvector) NPVgrt = self.GTR.value / (1 - self.GTR.value) * (NPVcap + NPVoandm + NPVfc + NPVit - NPVitc) LCOE = (NPVcap + NPVoandm + NPVfc + NPVit + NPVgrt - NPVitc - np.sum( annualheatincome * inflationvector * discountvector)) / np.sum( model.surfaceplant.NetkWhProduced.value * inflationvector * discountvector) * 1E8 - elif model.surfaceplant.enduseoption.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICTY, - EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICTY, - EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICTY]: # electricity sales is additional income revenue stream - annualelectricityincome = model.surfaceplant.NetkWhProduced.value * model.surfaceplant.elecprice.value / 1E6 # M$/year + elif model.surfaceplant.enduse_option.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICITY, + EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICITY, + EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICITY]: # electricity sales is additional income revenue stream + annualelectricityincome = model.surfaceplant.NetkWhProduced.value * model.surfaceplant.electricity_cost_to_buy.value / 1E6 # M$/year NPVoandm = np.sum(self.Coam.value * inflationvector * discountvector) NPVgrt = self.GTR.value / (1 - self.GTR.value) * (NPVcap + NPVoandm + NPVfc + NPVit - NPVitc) LCOH = (NPVcap + NPVoandm + NPVfc + NPVit + NPVgrt - NPVitc - np.sum( @@ -285,30 +285,30 @@ def CalculateLCOELCOH(self, model: Model) -> tuple: model.surfaceplant.HeatkWhProduced.value * inflationvector * discountvector) * 1E8 LCOH = self.LCOELCOHCombined.value * 2.931 # $/MMBTU - elif model.surfaceplant.enduseoption.value == EndUseOptions.ABSORPTION_CHILLER: - PumpingCosts = model.surfaceplant.PumpingkWh.value * model.surfaceplant.elecprice.value / 1E6 + elif model.surfaceplant.plant_type.value == PlantType.ABSORPTION_CHILLER: + PumpingCosts = model.surfaceplant.PumpingkWh.value * model.surfaceplant.electricity_cost_to_buy.value / 1E6 NPVoandm = np.sum((self.Coam.value + PumpingCosts) * inflationvector * discountvector) NPVgrt = self.GTR.value / (1 - self.GTR.value) * (NPVcap + NPVoandm + NPVfc + NPVit - NPVitc) LCOC = (NPVcap + NPVoandm + NPVfc + NPVit + NPVgrt - NPVitc) / np.sum( - model.surfaceplant.CoolingkWhProduced.value * inflationvector * discountvector) * 1E8 + model.surfaceplant.cooling_kWh_Produced.value * inflationvector * discountvector) * 1E8 LCOC = LCOC * 2.931 # $/MMBTU - elif model.surfaceplant.enduseoption.value == EndUseOptions.HEAT_PUMP: - PumpingCosts = model.surfaceplant.PumpingkWh.value * model.surfaceplant.elecprice.value / 1E6 - HeatPumpElecCosts = model.surfaceplant.HeatPumpElectricitykWhUsed.value * model.surfaceplant.elecprice.value / 1E6 + elif model.surfaceplant.plant_type.value == PlantType.HEAT_PUMP: + PumpingCosts = model.surfaceplant.PumpingkWh.value * model.surfaceplant.electricity_cost_to_buy.value / 1E6 + HeatPumpElecCosts = model.surfaceplant.heat_pump_electricity_kwh_used.value * model.surfaceplant.electricity_cost_to_buy.value / 1E6 NPVoandm = np.sum((self.Coam.value + PumpingCosts + HeatPumpElecCosts) * inflationvector * discountvector) NPVgrt = self.GTR.value / (1 - self.GTR.value) * (NPVcap + NPVoandm + NPVfc + NPVit - NPVitc) LCOH = (NPVcap + NPVoandm + NPVfc + NPVit + NPVgrt - NPVitc) / np.sum( model.surfaceplant.HeatkWhProduced.value * inflationvector * discountvector) * 1E8 LCOH = self.LCOH.value * 2.931 # $/MMBTU - elif model.surfaceplant.enduseoption.value == EndUseOptions.DISTRICT_HEATING: - PumpingCosts = model.surfaceplant.PumpingkWh.value * model.surfaceplant.elecprice.value / 1E6 + elif model.surfaceplant.plant_type.value == PlantType.DISTRICT_HEATING: + PumpingCosts = model.surfaceplant.PumpingkWh.value * model.surfaceplant.electricity_cost_to_buy.value / 1E6 NPVoandm = np.sum( (self.Coam.value + PumpingCosts + self.annualngcost.value) * inflationvector * discountvector) NPVgrt = self.GTR.value / (1 - self.GTR.value) * (NPVcap + NPVoandm + NPVfc + NPVit - NPVitc) LCOH = (NPVcap + NPVoandm + NPVfc + NPVit + NPVgrt - NPVitc) / np.sum( - model.surfaceplant.annualheatingdemand.value * inflationvector * discountvector) * 1E2 + model.surfaceplant.annual_heating_demand.value * inflationvector * discountvector) * 1E2 LCOH = LCOH * 2.931 # $/MMBTU return LCOE, LCOH, LCOC @@ -1876,7 +1876,7 @@ def Calculate(self, model: Model) -> None: (model.wellbores.nprod.value + model.wellbores.ninj.value) * 750 * 500. + self.Cpumps) / 1E6 # plant costs - if model.surfaceplant.enduseoption.value == EndUseOptions.HEAT: # direct-use + if model.surfaceplant.enduse_option.value == EndUseOptions.HEAT: # direct-use if self.ccplantfixed.Valid: self.Cplant.value = self.ccplantfixed.value else: @@ -1884,22 +1884,22 @@ def Calculate(self, model: Model) -> None: model.surfaceplant.HeatExtracted.value) * 1000. # 1.15 for 15% contingency and 1.12 for 12% indirect costs # absorption chiller - elif model.surfaceplant.enduseoption.value == EndUseOptions.ABSORPTION_CHILLER: # absorption chiller + elif model.surfaceplant.enduse_option.value == PlantType.ABSORPTION_CHILLER: # absorption chiller if self.ccplantfixed.Valid: self.Cplant.value = self.ccplantfixed.value else: - # this is for the direct-use part all the way up to the absorprtion chiller + # this is for the direct-use part all the way up to the absorption chiller self.Cplant.value = 1.12 * 1.15 * self.ccplantadjfactor.value * 250E-6 * np.max( model.surfaceplant.HeatExtracted.value) * 1000. # 1.15 for 15% contingency and 1.12 for 12% indirect costs if self.chillercapex.value == -1: # no value provided by user, use built-in correlation ($2500/ton) self.chillercapex.value = 1.12 * 1.15 * np.max( - model.surfaceplant.CoolingProduced.value) * 1000 / 3.517 * 2500 / 1e6 # $2,500/ton of cooling. 1.15 for 15% contingency and 1.12 for 12% indirect costs + model.surfaceplant.cooling_produced.value) * 1000 / 3.517 * 2500 / 1e6 # $2,500/ton of cooling. 1.15 for 15% contingency and 1.12 for 12% indirect costs # now add chiller cost to surface plant cost self.Cplant.value += self.chillercapex.value # heat pump - elif model.surfaceplant.enduseoption.value == EndUseOptions.HEAT_PUMP: + elif model.surfaceplant.enduse_option.value == PlantType.HEAT_PUMP: if self.ccplantfixed.Valid: self.Cplant.value = self.ccplantfixed.value else: @@ -1914,18 +1914,18 @@ def Calculate(self, model: Model) -> None: self.Cplant.value += self.heatpumpcapex.value # district heating - elif model.surfaceplant.enduseoption.value == EndUseOptions.DISTRICT_HEATING: + elif model.surfaceplant.enduse_option.value == PlantType.DISTRICT_HEATING: if self.ccplantfixed.Valid: self.Cplant.value = self.ccplantfixed.value else: self.Cplant.value = 1.12 * 1.15 * self.ccplantadjfactor.value * 250E-6 * np.max( model.surfaceplant.HeatExtracted.value) * 1000. # 1.15 for 15% contingency and 1.12 for 12% indirect costs - self.peakingboilercost.value = 65 * model.surfaceplant.maxpeakingboilerdemand.value / 1000 # add 65$/KW for peaking boiler + self.peakingboilercost.value = 65 * model.surfaceplant.max_peaking_boiler_demand.value / 1000 # add 65$/KW for peaking boiler self.Cplant.value += self.peakingboilercost.value # add peaking boiler cost to surface plant cost else: # all other options have power plant - if model.surfaceplant.pptype.value == PowerPlantType.SUB_CRITICAL_ORC: + if model.surfaceplant.plant_type.value == PlantType.SUB_CRITICAL_ORC: MaxProducedTemperature = np.max(model.surfaceplant.TenteringPP.value) if MaxProducedTemperature < 150.: C3 = -1.458333E-3 @@ -1942,7 +1942,7 @@ def Calculate(self, model: Model) -> None: z = math.pow(y / 15., -0.06) self.Cplantcorrelation = CCAPP1 * z * x * 1000. / 1E6 - elif model.surfaceplant.pptype.value == PowerPlantType.SUPER_CRITICAL_ORC: + elif model.surfaceplant.plant_type.value == PlantType.SUPER_CRITICAL_ORC: MaxProducedTemperature = np.max(model.surfaceplant.TenteringPP.value) if MaxProducedTemperature < 150.: C3 = -1.458333E-3 @@ -1957,7 +1957,7 @@ def Calculate(self, model: Model) -> None: np.max(model.surfaceplant.ElectricityProduced.value) / 15., -0.06) * np.max( model.surfaceplant.ElectricityProduced.value) * 1000. / 1E6 - elif model.surfaceplant.pptype.value == PowerPlantType.SINGLE_FLASH: + elif model.surfaceplant.plant_type.value == PlantType.SINGLE_FLASH: if np.max(model.surfaceplant.ElectricityProduced.value) < 10.: C2 = 4.8472E-2 C1 = -35.2186 @@ -2012,7 +2012,7 @@ def Calculate(self, model: Model) -> None: self.Cplantcorrelation = (0.8 * a * math.pow(np.max(model.surfaceplant.ElectricityProduced.value), b) * np.max(model.surfaceplant.ElectricityProduced.value) * 1000. / 1E6) - elif model.surfaceplant.pptype.value == PowerPlantType.DOUBLE_FLASH: + elif model.surfaceplant.plant_type.value == PlantType.DOUBLE_FLASH: if np.max(model.surfaceplant.ElectricityProduced.value) < 10.: C2 = 4.8472E-2 C1 = -35.2186 @@ -2074,18 +2074,18 @@ def Calculate(self, model: Model) -> None: # add direct-use plant cost of co-gen system to Cplant (only of no total Cplant was provided) if not self.ccplantfixed.Valid: # 1.15 below for contingency and 1.12 for indirect costs - if model.surfaceplant.enduseoption.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICTY, - EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT]: # enduseoption = 3: cogen topping cycle + if model.surfaceplant.enduse_option.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICITY, + EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT]: # enduse_option = 3: cogen topping cycle self.Cplant.value = self.Cplant.value + 1.12 * 1.15 * self.ccplantadjfactor.value * 250E-6 * np.max( - model.surfaceplant.HeatProduced.value / model.surfaceplant.enduseefficiencyfactor.value) * 1000. - elif model.surfaceplant.enduseoption.value in [EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, - EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICTY]: # enduseoption = 4: cogen bottoming cycle + model.surfaceplant.HeatProduced.value / model.surfaceplant.enduse_efficiency_factor.value) * 1000. + elif model.surfaceplant.enduse_option.value in [EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, + EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICITY]: # enduse_option = 4: cogen bottoming cycle self.Cplant.value = self.Cplant.value + 1.12 * 1.15 * self.ccplantadjfactor.value * 250E-6 * np.max( - model.surfaceplant.HeatProduced.value / model.surfaceplant.enduseefficiencyfactor.value) * 1000. - elif model.surfaceplant.enduseoption.value in [EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICTY, - EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT]: # cogen parallel cycle + model.surfaceplant.HeatProduced.value / model.surfaceplant.enduse_efficiency_factor.value) * 1000. + elif model.surfaceplant.enduse_option.value in [EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICITY, + EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT]: # cogen parallel cycle self.Cplant.value = self.Cplant.value + 1.12 * 1.15 * self.ccplantadjfactor.value * 250E-6 * np.max( - model.surfaceplant.HeatProduced.value / model.surfaceplant.enduseefficiencyfactor.value) * 1000. + model.surfaceplant.HeatProduced.value / model.surfaceplant.enduse_efficiency_factor.value) * 1000. if not self.totalcapcost.Valid: # exploration costs (same as in Geophires v1.2) (M$) @@ -2096,10 +2096,10 @@ def Calculate(self, model: Model) -> None: 1. + self.C1well * 0.6) # 1.15 for 15% contingency and 1.12 for 12% indirect costs # Surface Piping Length Costs (M$) #assumed $750k/km - self.Cpiping.value = 750 / 1000 * model.surfaceplant.pipinglength.value + self.Cpiping.value = 750 / 1000 * model.surfaceplant.piping_length.value # district heating network costs - if model.surfaceplant.enduseoption.value == EndUseOptions.DISTRICT_HEATING: # district heat + if model.surfaceplant.enduse_option.value == PlantType.DISTRICT_HEATING: # district heat if self.dhtotaldistrictnetworkcost.Provided: self.dhdistrictcost.value = self.dhtotaldistrictnetworkcost.value elif self.dhpipinglength.Provided: @@ -2111,8 +2111,8 @@ def Calculate(self, model: Model) -> None: model.logger.warning("District heating network cost calculated based on default district area") if self.dhpopulation.Provided: self.populationdensity.value = self.dhpopulation.value / self.dhlandarea.value - elif model.surfaceplant.dhnumberofhousingunits.Provided: - self.populationdensity.value = model.surfaceplant.dhnumberofhousingunits.value * 2.6 / self.dhlandarea.value # estimate population based on 2.6 number of people per household + elif model.surfaceplant.dh_number_of_housing_units.Provided: + self.populationdensity.value = model.surfaceplant.dh_number_of_housing_units.value * 2.6 / self.dhlandarea.value # estimate population based on 2.6 number of people per household else: model.logger.warning( "District heating network cost calculated based on default number of people in district") @@ -2136,24 +2136,24 @@ def Calculate(self, model: Model) -> None: # O&M costs # calculate first O&M costs independent of whether oamtotalfixed is provided or not # additional electricity cost for heat pump as end-use - if model.surfaceplant.enduseoption.value == EndUseOptions.HEAT_PUMP: # heat pump: + if model.surfaceplant.enduse_option.value == PlantType.HEAT_PUMP: # heat pump: self.averageannualheatpumpelectricitycost.value = np.average( - model.surfaceplant.HeatPumpElectricitykWhUsed.value) * model.surfaceplant.elecprice.value / 1E6 # M$/year + model.surfaceplant.heat_pump_electricity_kwh_used.value) * model.surfaceplant.electricity_cost_to_buy.value / 1E6 # M$/year # district heating peaking fuel annual cost - if model.surfaceplant.enduseoption.value == EndUseOptions.DISTRICT_HEATING: # district heating - self.annualngcost.value = model.surfaceplant.annualngdemand.value * self.ngprice.value / 1000 / self.peakingboilerefficiency.value # array with annual O&M cost for peaking fuel + if model.surfaceplant.enduse_option.value == PlantType.DISTRICT_HEATING: # district heating + self.annualngcost.value = model.surfaceplant.annual_ng_demand.value * self.ngprice.value / 1000 / self.peakingboilerefficiency.value # array with annual O&M cost for peaking fuel self.averageannualngcost.value = np.average(self.annualngcost.value) # calculate average annual pumping costs in case no electricity is provided - if model.surfaceplant.enduseoption.value in [EndUseOptions.HEAT, EndUseOptions.ABSORPTION_CHILLER, - EndUseOptions.HEAT_PUMP, EndUseOptions.DISTRICT_HEATING]: + if model.surfaceplant.enduse_option.value in [EndUseOptions.HEAT, PlantType.ABSORPTION_CHILLER, + PlantType.HEAT_PUMP, PlantType.DISTRICT_HEATING]: self.averageannualpumpingcosts.value = np.average( - model.surfaceplant.PumpingkWh.value) * model.surfaceplant.elecprice.value / 1E6 # M$/year + model.surfaceplant.PumpingkWh.value) * model.surfaceplant.electricity_cost_to_buy.value / 1E6 # M$/year if not self.oamtotalfixed.Valid: # labor cost - if model.surfaceplant.enduseoption.value == EndUseOptions.ELECTRICITY: # electricity + if model.surfaceplant.enduse_option.value == EndUseOptions.ELECTRICITY: # electricity if np.max(model.surfaceplant.ElectricityProduced.value) < 2.5: self.Claborcorrelation = 236. / 1E3 # M$/year else: @@ -2189,11 +2189,11 @@ def Calculate(self, model: Model) -> None: # here is assumed 1 l per kg maybe correct with real temp. (M$/year) 925$/ML = 3.5$/1,000 gallon self.Coamwater.value = self.oamwateradjfactor.value * (model.wellbores.nprod.value * model.wellbores.prodwellflowrate.value * - model.reserv.waterloss.value * model.surfaceplant.utilfactor.value * + model.reserv.waterloss.value * model.surfaceplant.utilization_factor.value * 365. * 24. * 3600. / 1E6 * 925. / 1E6) # additional O&M cost for absorption chiller if used - if model.surfaceplant.enduseoption.value == EndUseOptions.ABSORPTION_CHILLER: # absorption chiller: + if model.surfaceplant.enduse_option.value == PlantType.ABSORPTION_CHILLER: # absorption chiller: if self.chilleropex.value == -1: self.chilleropex.value = self.chillercapex.value * 2 / 100 # assumed annual O&M for chiller is 2% of investment cost @@ -2206,14 +2206,14 @@ def Calculate(self, model: Model) -> None: self.chilleropex.value = 0 # district heating O&M cost - if model.surfaceplant.enduseoption.value == EndUseOptions.DISTRICT_HEATING: # district heating - self.annualngcost.value = model.surfaceplant.annualngdemand.value * self.ngprice.value / 1000 # array with annual O&M cost for peaking fuel + if model.surfaceplant.enduse_option.value == PlantType.DISTRICT_HEATING: # district heating + self.annualngcost.value = model.surfaceplant.annual_ng_demand.value * self.ngprice.value / 1000 # array with annual O&M cost for peaking fuel if self.dhoandmcost.Provided: self.dhdistrictoandmcost.value = self.dhoandmcost.value # M$/yr else: self.dhdistrictoandmcost.value = 0.01 * self.dhdistrictcost.value + 0.02 * sum( - model.surfaceplant.dailyheatingdemand.value) * model.surfaceplant.elecprice.value / 1000 # [M$/year] we assume annual district OPEX equals 1% of district CAPEX and 2% of total heat demand for pumping costs + model.surfaceplant.daily_heating_demand.value) * model.surfaceplant.electricity_cost_to_buy.value / 1000 # [M$/year] we assume annual district OPEX equals 1% of district CAPEX and 2% of total heat demand for pumping costs else: self.dhdistrictoandmcost.value = 0 @@ -2227,7 +2227,7 @@ def Calculate(self, model: Model) -> None: # account for well redrilling self.Coam.value = self.Coam.value + \ ( - self.Cwell.value + self.Cstim.value) * model.wellbores.redrill.value / model.surfaceplant.plantlifetime.value + self.Cwell.value + self.Cstim.value) * model.wellbores.redrill.value / model.surfaceplant.plant_lifetime.value # The Reservoir depth measure was arbitrarily changed to meters despite being defined in the docs as kilometers. # For display consistency sake, we need to convert it back @@ -2236,10 +2236,10 @@ def Calculate(self, model: Model) -> None: model.reserv.depth.CurrentUnits = LengthUnit.KILOMETERS # build the price models - self.ElecPrice.value = BuildPricingModel(model.surfaceplant.plantlifetime.value, 0, + self.ElecPrice.value = BuildPricingModel(model.surfaceplant.plant_lifetime.value, 0, self.ElecStartPrice.value, self.ElecEndPrice.value, self.ElecEscalationStart.value, self.ElecEscalationRate.value) - self.HeatPrice.value = BuildPricingModel(model.surfaceplant.plantlifetime.value, 0, + self.HeatPrice.value = BuildPricingModel(model.surfaceplant.plant_lifetime.value, 0, self.HeatStartPrice.value, self.HeatEndPrice.value, self.HeatEscalationStart.value, self.HeatEscalationRate.value) @@ -2248,38 +2248,38 @@ def Calculate(self, model: Model) -> None: self.Coam.value = self.Coam.value + self.AnnualLicenseEtc.value - self.TaxRelief.value # Calculate cashflow and cumulative cash flow - if model.surfaceplant.enduseoption.value == EndUseOptions.ELECTRICITY: + if model.surfaceplant.enduse_option.value == EndUseOptions.ELECTRICITY: self.ElecRevenue.value, self.ElecCummRevenue.value = CalculateRevenue( - model.surfaceplant.plantlifetime.value, model.surfaceplant.ConstructionYears.value, self.CCap.value, + model.surfaceplant.plant_lifetime.value, model.surfaceplant.construction_years.value, self.CCap.value, self.Coam.value, model.surfaceplant.NetkWhProduced.value, self.ElecPrice.value) self.TotalRevenue.value = self.ElecRevenue.value self.TotalCummRevenue.value = self.ElecCummRevenue.value - elif model.surfaceplant.enduseoption.value == EndUseOptions.HEAT: + elif model.surfaceplant.enduse_option.value == EndUseOptions.HEAT: self.HeatRevenue.value, self.HeatCummRevenue.value = CalculateRevenue( - model.surfaceplant.plantlifetime.value, model.surfaceplant.ConstructionYears.value, self.CCap.value, + model.surfaceplant.plant_lifetime.value, model.surfaceplant.construction_years.value, self.CCap.value, self.Coam.value, model.surfaceplant.HeatkWhProduced.value, self.HeatPrice.value) self.TotalRevenue.value = self.HeatRevenue.value self.TotalCummRevenue.value = self.HeatCummRevenue.value - elif model.surfaceplant.enduseoption.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, - EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICTY, - EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICTY, - EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, - EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, - EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICTY]: # co-gen + elif model.surfaceplant.enduse_option.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, + EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICITY, + EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICITY, + EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, + EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, + EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICITY]: # co-gen # else: self.ElecRevenue.value, self.ElecCummRevenue.self = CalculateRevenue( - model.surfaceplant.plantlifetime.value, model.surfaceplant.ConstructionYears.value, self.CCap.value, + model.surfaceplant.plant_lifetime.value, model.surfaceplant.construction_years.value, self.CCap.value, self.Coam.value, model.surfaceplant.NetkWhProduced.value, self.ElecPrice.value) # note that CAPEX & OPEX are 0.0 because we only want them counted once, and it will be accounted # for in the previous line self.HeatRevenue.value, self.HeatCummRevenue.self = CalculateRevenue( - model.surfaceplant.plantlifetime.value, model.surfaceplant.ConstructionYears.value, 0.0, 0.0, + model.surfaceplant.plant_lifetime.value, model.surfaceplant.construction_years.value, 0.0, 0.0, model.surfaceplant.HeatkWhProduced.value, self.HeatPrice.value) self.TotalRevenue.value = [0.0] * ( - model.surfaceplant.plantlifetime.value + model.surfaceplant.ConstructionYears.value) + model.surfaceplant.plant_lifetime.value + model.surfaceplant.construction_years.value) self.TotalCummRevenue.value = [0.0] * ( - model.surfaceplant.plantlifetime.value + model.surfaceplant.ConstructionYears.value) - for i in range(0, model.surfaceplant.plantlifetime.value + model.surfaceplant.ConstructionYears.value, 1): + model.surfaceplant.plant_lifetime.value + model.surfaceplant.construction_years.value) + for i in range(0, model.surfaceplant.plant_lifetime.value + model.surfaceplant.construction_years.value, 1): self.TotalRevenue.value[i] = self.ElecRevenue.value[i] + self.HeatRevenue.value[i] self.TotalCummRevenue.value[i] = self.TotalRevenue.value[i] if i > 0: @@ -2287,7 +2287,7 @@ def Calculate(self, model: Model) -> None: # Calculate more financial values using numpy financials self.ProjectNPV.value, self.ProjectIRR.value, self.ProjectVIR.value, self.ProjectMOIC.value = \ - CalculateFinancialPerformance(model.surfaceplant.plantlifetime.value, self.FixedInternalRate.value, + CalculateFinancialPerformance(model.surfaceplant.plant_lifetime.value, self.FixedInternalRate.value, self.TotalRevenue.value, self.TotalCummRevenue.value, self.CCap.value, self.Coam.value) diff --git a/src/geophires_x/EconomicsAddOns.py b/src/geophires_x/EconomicsAddOns.py index 9f047793..1ae1ca08 100644 --- a/src/geophires_x/EconomicsAddOns.py +++ b/src/geophires_x/EconomicsAddOns.py @@ -5,9 +5,9 @@ import numpy_financial as npf import geophires_x.Economics as Economics import geophires_x.Model as Model -from .OptionList import EndUseOptions -from .Parameter import listParameter, OutputParameter -from .Units import * +from geophires_x.OptionList import EndUseOptions +from geophires_x.Parameter import listParameter, OutputParameter +from geophires_x.Units import * class EconomicsAddOns(Economics.Economics): @@ -221,7 +221,7 @@ def read_parameters(self, model: Model) -> None: # super.read_parameter will have already dealt with all the regular values, but anything unusual # may not be dealt with, so check. # In this case, all the values are array values, and weren't correctly dealt with, so below is where - # we process them. The problem is that they have a position number i.e., "AddOnCAPEX 1, AddOnCAPEX 2" + # we process them. The problem is that they have a position number i.reservoir_enthalpy., "AddOnCAPEX 1, AddOnCAPEX 2" # appended to them, while the # Parameter name is just "AddOnCAPEX" and the position indicates where in the array the user wants it stored. # So we need to look for the 5 arrays and position values and insert them into the arrays. @@ -284,11 +284,11 @@ def Calculate(self, model: Model) -> None: # The amount of electricity and/or heat have for the project already been calculated in SurfacePlant, # so we need to update them here so when they get used in the final economic calculation (below), # the new values reflect the addition of the AddOns - for i in range(0, model.surfaceplant.plantlifetime.value): - if model.surfaceplant.enduseoption.value != EndUseOptions.HEAT: # all these end-use options have an electricity generation component + for i in range(0, model.surfaceplant.plant_lifetime.value): + if model.surfaceplant.enduse_option.value != EndUseOptions.HEAT: # all these end-use options have an electricity generation component model.surfaceplant.TotalkWhProduced.value[i] = model.surfaceplant.TotalkWhProduced.value[i] + self.AddOnElecGainedTotalPerYear.value model.surfaceplant.NetkWhProduced.value[i] = model.surfaceplant.NetkWhProduced.value[i] + self.AddOnElecGainedTotalPerYear.value - if model.surfaceplant.enduseoption.value != EndUseOptions.ELECTRICITY: + if model.surfaceplant.enduse_option.value != EndUseOptions.ELECTRICITY: model.surfaceplant.HeatkWhProduced.value[i] = model.surfaceplant.HeatkWhProduced.value[i] + self.AddOnHeatGainedTotalPerYear.value else: # all the end-use option of direct-use only components have a heat generation component @@ -297,24 +297,24 @@ def Calculate(self, model: Model) -> None: # Calculate the adjusted OPEX and CAPEX self.AdjustedProjectCAPEX.value = model.economics.CCap.value + self.AddOnCAPEXTotal.value self.AdjustedProjectOPEX.value = model.economics.Coam.value + self.AddOnOPEXTotalPerYear.value - AddOnCapCostPerYear = self.AddOnCAPEXTotal.value / model.surfaceplant.ConstructionYears.value - ProjectCapCostPerYear = self.AdjustedProjectCAPEX.value / model.surfaceplant.ConstructionYears.value + AddOnCapCostPerYear = self.AddOnCAPEXTotal.value / model.surfaceplant.construction_years.value + ProjectCapCostPerYear = self.AdjustedProjectCAPEX.value / model.surfaceplant.construction_years.value # (re)Calculate the revenues - self.AddOnElecRevenue.value = [0.0] * model.surfaceplant.plantlifetime.value - self.AddOnHeatRevenue.value = [0.0] * model.surfaceplant.plantlifetime.value - self.AddOnRevenue.value = [0.0] * model.surfaceplant.plantlifetime.value - self.AddOnCashFlow.value = [0.0] * model.surfaceplant.plantlifetime.value - self.ProjectCashFlow.value = [0.0] * model.surfaceplant.plantlifetime.value - for i in range(0, model.surfaceplant.plantlifetime.value, 1): + self.AddOnElecRevenue.value = [0.0] * model.surfaceplant.plant_lifetime.value + self.AddOnHeatRevenue.value = [0.0] * model.surfaceplant.plant_lifetime.value + self.AddOnRevenue.value = [0.0] * model.surfaceplant.plant_lifetime.value + self.AddOnCashFlow.value = [0.0] * model.surfaceplant.plant_lifetime.value + self.ProjectCashFlow.value = [0.0] * model.surfaceplant.plant_lifetime.value + for i in range(0, model.surfaceplant.plant_lifetime.value, 1): ProjectElectricalEnergy = 0.0 ProjectHeatEnergy = 0.0 AddOnElectricalEnergy = 0.0 AddOnHeatEnergy = 0.0 - if model.surfaceplant.enduseoption.value == EndUseOptions.ELECTRICITY: # This option has no heat component + if model.surfaceplant.enduse_option.value == EndUseOptions.ELECTRICITY: # This option has no heat component ProjectElectricalEnergy = model.surfaceplant.NetkWhProduced.value[i] AddOnElectricalEnergy = self.AddOnElecGainedTotalPerYear.value - elif model.surfaceplant.enduseoption.value == EndUseOptions.HEAT: # has heat component but no electricity + elif model.surfaceplant.enduse_option.value == EndUseOptions.HEAT: # has heat component but no electricity ProjectHeatEnergy = model.surfaceplant.HeatkWhProduced.value[i] AddOnHeatEnergy = self.AddOnHeatGainedTotalPerYear.value else: # everything else has a component of both @@ -336,7 +336,7 @@ def Calculate(self, model: Model) -> None: # now insert the cost of construction into the front of the array that will be used to calculate # NPV = the convention is that the upfront CAPEX is negative - for i in range(0, model.surfaceplant.ConstructionYears.value, 1): + for i in range(0, model.surfaceplant.construction_years.value, 1): self.AddOnCashFlow.value.insert(0, -1.0 * AddOnCapCostPerYear) self.ProjectCashFlow.value.insert(0, -1.0 * ProjectCapCostPerYear) @@ -379,7 +379,7 @@ def Calculate(self, model: Model) -> None: # Calculate MOIC which depends on CumCashFlow self.ProjectMOIC.value = self.ProjectCummCashFlow.value[len(self.ProjectCummCashFlow.value) - 1] / ( self.AdjustedProjectCAPEX.value + ( - self.AdjustedProjectOPEX.value * model.surfaceplant.plantlifetime.value)) + self.AdjustedProjectOPEX.value * model.surfaceplant.plant_lifetime.value)) # recalculate LCOE/LCOH self.LCOE.value, self.LCOH.value, LCOC = Economics.CalculateLCOELCOH(self, model) diff --git a/src/geophires_x/EconomicsCCUS.py b/src/geophires_x/EconomicsCCUS.py index ece28324..cec7a8f6 100644 --- a/src/geophires_x/EconomicsCCUS.py +++ b/src/geophires_x/EconomicsCCUS.py @@ -4,9 +4,9 @@ import numpy_financial as npf from geophires_x.Model import Model from geophires_x.Economics import BuildPricingModel, Economics -from .OptionList import EndUseOptions -from .Parameter import intParameter, floatParameter, OutputParameter -from .Units import * +from geophires_x.OptionList import EndUseOptions +from geophires_x.Parameter import intParameter, floatParameter, OutputParameter +from geophires_x.Units import * class EconomicsCCUS(Economics): @@ -354,40 +354,40 @@ def Calculate(self, model: Model) -> None: """ model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) - self.CCUSRevenue.value = [0.0] * model.surfaceplant.plantlifetime.value - self.CCUSCashFlow.value = [0.0] * model.surfaceplant.plantlifetime.value - self.CCUSCummCashFlow.value = [0.0] * model.surfaceplant.plantlifetime.value - self.CarbonThatWouldHaveBeenProducedAnnually.value = [0.0] * model.surfaceplant.plantlifetime.value + self.CCUSRevenue.value = [0.0] * model.surfaceplant.plant_lifetime.value + self.CCUSCashFlow.value = [0.0] * model.surfaceplant.plant_lifetime.value + self.CCUSCummCashFlow.value = [0.0] * model.surfaceplant.plant_lifetime.value + self.CarbonThatWouldHaveBeenProducedAnnually.value = [0.0] * model.surfaceplant.plant_lifetime.value self.CarbonThatWouldHaveBeenProducedTotal.value = 0.0 ProjectCapCostPerYear = model.economics.CCap.value / self.ConstructionYears.value # Calculate carbon price models - self.CCUSPrice.value = BuildPricingModel(model.surfaceplant.plantlifetime.value, self.CCUSEscalationStart.value, + self.CCUSPrice.value = BuildPricingModel(model.surfaceplant.plant_lifetime.value, self.CCUSEscalationStart.value, self.CCUSStartPrice.value, self.CCUSEndPrice.value, self.CCUSEscalationStart.value, self.CCUSEscalationRate.value) - self.CCUSOnElecPrice.value = BuildPricingModel(model.surfaceplant.plantlifetime.value, 0, + self.CCUSOnElecPrice.value = BuildPricingModel(model.surfaceplant.plant_lifetime.value, 0, self.ElecStartPrice.value, self.ElecEndPrice.value, self.ElecEscalationStart.value, self.ElecEscalationRate.value) - self.CCUSOnHeatPrice.value = BuildPricingModel(model.surfaceplant.plantlifetime.value, 0, + self.CCUSOnHeatPrice.value = BuildPricingModel(model.surfaceplant.plant_lifetime.value, 0, self.HeatStartPrice.value, self.HeatEndPrice.value, self.HeatEscalationStart.value, self.HeatEscalationRate.value) # Figure out how much energy is being produced each year, and the amount of carbon that would have been # produced if that energy had been made using the grid average carbon production. # That then gives us the revenue, since we have a carbon price model - # We can also get annual cash flow from it. - self.ProjectCashFlow.value = [0.0] * model.surfaceplant.plantlifetime.value - self.ProjectCummCashFlow.value = [0.0] * model.surfaceplant.plantlifetime.value - for i in range(0, model.surfaceplant.plantlifetime.value, 1): + # reservoir_producible_electricity can also get annual cash flow from it. + self.ProjectCashFlow.value = [0.0] * model.surfaceplant.plant_lifetime.value + self.ProjectCummCashFlow.value = [0.0] * model.surfaceplant.plant_lifetime.value + for i in range(0, model.surfaceplant.plant_lifetime.value, 1): dElectricalEnergy = 0.0 ProjectElectricalEnergy = 0.0 dHeatEnergy = 0.0 ProjectHeatEnergy = 0.0 dBothEnergy = 0.0 - if model.surfaceplant.enduseoption.value == EndUseOptions.ELECTRICITY: # This option has no heat component + if model.surfaceplant.enduse_option.value == EndUseOptions.ELECTRICITY: # This option has no heat component ProjectElectricalEnergy = model.surfaceplant.NetkWhProduced.value[i] dElectricalEnergy = model.surfaceplant.NetkWhProduced.value[i] - elif model.surfaceplant.enduseoption.value == EndUseOptions.HEAT: # has heat component but no electricity + elif model.surfaceplant.enduse_option.value == EndUseOptions.HEAT: # has heat component but no electricity ProjectHeatEnergy = model.surfaceplant.HeatkWhProduced.value[i] dHeatEnergy = model.surfaceplant.HeatkWhProduced.value[i] else: # everything else has a component of both @@ -418,7 +418,7 @@ def Calculate(self, model: Model) -> None: i = i + 1 # now insert the cost of construction into the front of the array that will be used to calculate NPV = the convention is that the upfront CAPEX is negative - self.ProjectCummCashFlow.value = [0.0] * model.surfaceplant.plantlifetime.value + self.ProjectCummCashFlow.value = [0.0] * model.surfaceplant.plant_lifetime.value for i in range(0, self.ConstructionYears.value, 1): self.ProjectCashFlow.value.insert(0, -1.0 * ProjectCapCostPerYear) self.ProjectCummCashFlow.value.insert(0, -1.0 * ProjectCapCostPerYear) @@ -446,7 +446,7 @@ def Calculate(self, model: Model) -> None: # Calculate MOIC which depends on CumCashFlow self.ProjectMOIC.value = self.ProjectCummCashFlow.value[len(self.ProjectCummCashFlow.value) - 1] / ( - model.economics.CCap.value + (model.economics.Coam.value * model.surfaceplant.plantlifetime.value)) + model.economics.CCap.value + (model.economics.Coam.value * model.surfaceplant.plant_lifetime.value)) model.logger.info("complete " + str(__class__) + ": " + sys._getframe().f_code.co_name) diff --git a/src/geophires_x/EconomicsS_DAC_GT.py b/src/geophires_x/EconomicsS_DAC_GT.py index aa699324..0fefad38 100644 --- a/src/geophires_x/EconomicsS_DAC_GT.py +++ b/src/geophires_x/EconomicsS_DAC_GT.py @@ -1,10 +1,10 @@ import sys import os -from .Parameter import floatParameter, OutputParameter, ReadParameter -from .Units import * -from .OptionList import EndUseOptions -import geophires_x.Model as Model import numpy as np +from geophires_x.Parameter import floatParameter, OutputParameter, ReadParameter +from geophires_x.Units import * +from geophires_x.OptionList import EndUseOptions +import geophires_x.Model as Model import geophires_x.Economics as Economics @@ -594,17 +594,17 @@ def Calculate(self, model: Model) -> None: sys.exit() self.CRF = self.calculate_CRF(self.wacc.value, - model.surfaceplant.plantlifetime.value) # Calculate initial CRF value based on default inputs + model.surfaceplant.plant_lifetime.value) # Calculate initial CRF value based on default inputs CAPEX = self.CAPEX.value * self.CRF # don't change a parameters value directly - it throw off the rehydration CAPEX = CAPEX * self.CAPEX_mult.value self.OPEX.value = self.OPEX.value * self.OPEX_mult.value self.therm.value = self.therm.value * self.therm_index.value - power_totalcost = self.elec.value * model.surfaceplant.elecprice.value - elec_heat_totalcost = self.therm.value * model.surfaceplant.elecprice.value + power_totalcost = self.elec.value * model.surfaceplant.electricity_cost_to_buy.value + elec_heat_totalcost = self.therm.value * model.surfaceplant.electricity_cost_to_buy.value # Convert from $/McF to $/kWh_th, but don't change a parameters value directly - it will throw off the rehydration NG_price = self.NG_price.value / self.NG_EnergyDensity.value NG_totalcost = self.therm.value * NG_price - self.LCOH.value, self.kWh_e_per_kWh_th.value = self.geo_therm_cost(model.surfaceplant.elecprice.value, + self.LCOH.value, self.kWh_e_per_kWh_th.value = self.geo_therm_cost(model.surfaceplant.electricity_cost_to_buy.value, self.CAPEX_mult.value, self.OPEX_mult.value, model.reserv.depth.value * 3280.84, np.average(model.wellbores.ProducedTemperature.value), @@ -630,18 +630,18 @@ def Calculate(self, model: Model) -> None: self.tot_cost_per_tonne.value = CAPEX + self.OPEX.value + self.storage.value + self.transport.value # USD/tonne self.percent_thermal_energy_going_to_heat.value = self.therm.value / self.tot_heat_energy_consumed_per_tonne.value - self.S_DAC_GTAnnualCost.value = [0.0] * model.surfaceplant.plantlifetime.value - self.S_DAC_GTCummCashFlow.value = [0.0] * model.surfaceplant.plantlifetime.value - self.CarbonExtractedAnnually.value = [0.0] * model.surfaceplant.plantlifetime.value - self.S_DAC_GTCummCarbonExtracted.value = [0.0] * model.surfaceplant.plantlifetime.value - self.CummCostPerTonne.value = [0.0] * model.surfaceplant.plantlifetime.value + self.S_DAC_GTAnnualCost.value = [0.0] * model.surfaceplant.plant_lifetime.value + self.S_DAC_GTCummCashFlow.value = [0.0] * model.surfaceplant.plant_lifetime.value + self.CarbonExtractedAnnually.value = [0.0] * model.surfaceplant.plant_lifetime.value + self.S_DAC_GTCummCarbonExtracted.value = [0.0] * model.surfaceplant.plant_lifetime.value + self.CummCostPerTonne.value = [0.0] * model.surfaceplant.plant_lifetime.value self.CarbonExtractedTotal.value = 0.0 # Figure out how much energy is being produced each year, and the amount of carbon that # would have been produced if that energy had been made using the grid average carbon production. # That then gives us the revenue, since we have a carbon price model - # We can also get annual cash flow from it. - for i in range(0, model.surfaceplant.plantlifetime.value, 1): + # reservoir_producible_electricity can also get annual cash flow from it. + for i in range(0, model.surfaceplant.plant_lifetime.value, 1): self.CarbonExtractedAnnually.value[i] = (self.EnergySplit.value * model.surfaceplant.HeatkWhExtracted.value[i]) / self.tot_heat_energy_consumed_per_tonne.value if i == 0: self.S_DAC_GTCummCarbonExtracted.value[i] = self.CarbonExtractedAnnually.value[i] @@ -655,16 +655,16 @@ def Calculate(self, model: Model) -> None: self.S_DAC_GTCummCashFlow.value[i] = self.S_DAC_GTCummCashFlow.value[i - 1] + self.S_DAC_GTAnnualCost.value[i] self.CummCostPerTonne.value[i] = self.S_DAC_GTCummCashFlow.value[i] / self.S_DAC_GTCummCarbonExtracted.value[i] - # We need to update the heat and electricity generated because we have consumed + # reservoir_producible_electricity need to update the heat and electricity generated because we have consumed # some (all) of it to do the capture, so when they get used in the final economic calculation (below), # the new values reflect the impact of S-DAC-GT - for i in range(0, model.surfaceplant.plantlifetime.value): - if model.surfaceplant.enduseoption.value != EndUseOptions.HEAT: # all these end-use options have an electricity generation component + for i in range(0, model.surfaceplant.plant_lifetime.value): + if model.surfaceplant.enduse_option.value != EndUseOptions.HEAT: # all these end-use options have an electricity generation component model.surfaceplant.TotalkWhProduced.value[i] = model.surfaceplant.TotalkWhProduced.value[i] - ( self.CarbonExtractedAnnually.value[i] * self.elec.value) model.surfaceplant.NetkWhProduced.value[i] = model.surfaceplant.NetkWhProduced.value[i] - ( self.CarbonExtractedAnnually.value[i] * self.elec.value) - if model.surfaceplant.enduseoption.value != EndUseOptions.ELECTRICITY: + if model.surfaceplant.enduse_option.value != EndUseOptions.ELECTRICITY: model.surfaceplant.HeatkWhProduced.value[i] = model.surfaceplant.HeatkWhProduced.value[i] - ( self.CarbonExtractedAnnually.value[i] * self.therm.value) else: diff --git a/src/geophires_x/GEOPHIRES-X_Output_Validation_Tool.py b/src/geophires_x/GEOPHIRES-X_Output_Validation_Tool.py index c7f537e1..fc3d732a 100644 --- a/src/geophires_x/GEOPHIRES-X_Output_Validation_Tool.py +++ b/src/geophires_x/GEOPHIRES-X_Output_Validation_Tool.py @@ -43,17 +43,17 @@ def main(): # 0) Code_File: Python code to run (this file) # 1) Input_file: The input parameter file that controls the executon of this program # 2) Input_file contains the following lines (the group of following lines can be repeasted as many times as you want for different input parameter files): - # GEOPHIRES-X_Validation_Tool output file, with the results of the analysis, e.g., "D:\Work\GEOPHIRES3-master\Results\GEOPHIRES-X_Output_Validation_Tool_output.txt" - # GEOPHIRES-X input control file, with the parameters the user wishes to change, e.g., D:\Work\GEOPHIRES3-master\Examples\example1.txt - # GEOPHIRES-X output result file, with results from running the above input control file, e.g., D:\Work\GEOPHIRES3-master\Example1V3_output.txt - # Precomputed results file that we will be comparing against, e.g., D:\Work\GEOPHIRES3-master\Results\Example1V3.txt + # GEOPHIRES-X_Validation_Tool output file, with the results of the analysis, reservoir_enthalpy.g., "D:\Work\GEOPHIRES3-master\Results\GEOPHIRES-X_Output_Validation_Tool_output.txt" + # GEOPHIRES-X input control file, with the parameters the user wishes to change, reservoir_enthalpy.g., D:\Work\GEOPHIRES3-master\Examples\example1.txt + # GEOPHIRES-X output result file, with results from running the above input control file, reservoir_enthalpy.g., D:\Work\GEOPHIRES3-master\Example1V3_output.txt + # Precomputed results file that we will be comparing against, reservoir_enthalpy.g., D:\Work\GEOPHIRES3-master\Results\Example1V3.txt # List of output files that you want used to compare and validate. List can be as long as you want, terminated with a blank line. # This string in the search string that must appear in BOTH OUTPUT FILES IN EXACTLY THE SAME FORMAT, case, spelling, etc. or you must specify the equivilent string in the precomputed file # adding a "|" and then the equivilent string in the precomputed file. The tool will look for this string(s) in both files, then extract the associated value (after the colon or =) and compare. # If it is the same, do nothing. If it is not, report it. # For values in a table, the search string is the name of the table, followed by a comma, then the number of lines to skip to get to the value you want to validate, then a comma, # then the column number that contains the value you want to compare. - # e.g., + # reservoir_enthalpy.g., # Average Net Electricity Production # Electricity breakeven price|LCOE # Average Production Temperature diff --git a/src/geophires_x/GEOPHIRESv3.py b/src/geophires_x/GEOPHIRESv3.py index 054ab608..a8cc224e 100644 --- a/src/geophires_x/GEOPHIRESv3.py +++ b/src/geophires_x/GEOPHIRESv3.py @@ -83,7 +83,7 @@ def main(enable_geophires_logging_config=True): sys.stdout.write(line) # make district heating plot - if model.surfaceplant.enduseoption.value == OptionList.EndUseOptions.DISTRICT_HEATING: + if model.surfaceplant.plant_type.value == OptionList.PlantType.DISTRICT_HEATING: model.outputs.MakeDistrictHeatingPlot(model) logger.info(f'Complete {str(__name__)}: {sys._getframe().f_code.co_name}') diff --git a/src/geophires_x/GeoPHIRESUtils.py b/src/geophires_x/GeoPHIRESUtils.py index 3edad8fc..834ebb25 100644 --- a/src/geophires_x/GeoPHIRESUtils.py +++ b/src/geophires_x/GeoPHIRESUtils.py @@ -120,7 +120,7 @@ def HeatCapacityWater(Twater: float) -> float: @lru_cache(maxsize=None) -def RecoverableHeat(DefaultRecoverableHeat: float, Twater: float) -> float: +def RecoverableHeat(Twater: float) -> float: """ the RecoverableHeat function is used to calculate the recoverable heat fraction as a function of temperature @@ -141,18 +141,12 @@ def RecoverableHeat(DefaultRecoverableHeat: float, Twater: float) -> float: if not isinstance(Twater, (int, float)): raise ValueError("Twater must be a number") - if not isinstance(DefaultRecoverableHeat, (int, float)): - raise ValueError("DefaultRecoverableHeat must be a number") - - if DefaultRecoverableHeat >= 0.0: - recoverable_heat = DefaultRecoverableHeat + if Twater <= LOW_TEMP_THRESHOLD: + recoverable_heat = LOW_TEMP_RECOVERABLE_HEAT + elif Twater >= HIGH_TEMP_THRESHOLD: + recoverable_heat = HIGH_TEMP_RECOVERABLE_HEAT else: - if Twater <= LOW_TEMP_THRESHOLD: - recoverable_heat = LOW_TEMP_RECOVERABLE_HEAT - elif Twater >= HIGH_TEMP_THRESHOLD: - recoverable_heat = HIGH_TEMP_RECOVERABLE_HEAT - else: - recoverable_heat = 0.0038 * Twater + 0.085 + recoverable_heat = 0.0038 * Twater + 0.085 return recoverable_heat diff --git a/src/geophires_x/MC_GEOPHIRES_Result.txt b/src/geophires_x/MC_GEOPHIRES_Result.txt new file mode 100644 index 00000000..6d03f9b2 --- /dev/null +++ b/src/geophires_x/MC_GEOPHIRES_Result.txt @@ -0,0 +1 @@ +Gradient 1, Reservoir Temperature, Utilization Factor, Ambient Temperature, Average Net Electricity Production, Average Production Temperature, Average Annual Total Electricity Generation diff --git a/src/geophires_x/MC_GeoPHIRES3.py b/src/geophires_x/MC_GeoPHIRES3.py index 7f8a4a8c..90fed82d 100644 --- a/src/geophires_x/MC_GeoPHIRES3.py +++ b/src/geophires_x/MC_GeoPHIRES3.py @@ -18,6 +18,8 @@ import shutil import concurrent.futures import subprocess +import matplotlib.pyplot as plt +import pandas as pd def CheckAndReplaceMean(input_value, args) -> list: @@ -241,10 +243,10 @@ def main(enable_geophires_logging_config=True): # combination of variables produced the interesting values (like lowest or highest, or mean) # start by creating the string we will write as header s = "" - for input in Inputs: - s = s + input[0] + ", " for output in Outputs: s = s + output + ", " + for input in Inputs: + s = s + input[0] + ", " s = "".join(s.rsplit(" ", 1)) # get rid of last space s = "".join(s.rsplit(",", 1)) # get rid of last comma s = s + "\n" @@ -253,6 +255,7 @@ def main(enable_geophires_logging_config=True): with open(Outputfile, "w") as f: f.write(s) + # TODO Use a scratch directory to minimize the ness: https://docs.python.org/3/library/tempfile.html#tempfile.TemporaryDirectory # build the args list pass_list = [Inputs, Outputs, args, Outputfile, working_dir, PythonPath] # this list never changes @@ -287,6 +290,10 @@ def main(enable_geophires_logging_config=True): actual_records_count = len(Results) + # Load the results into a pandas dataframe + results_pd = pd.read_csv(Outputfile) + df = pd.DataFrame(results_pd) + # Compute the stats along the specified axes. mins = np.nanmin(Results, 0) maxs = np.nanmax(Results, 0) @@ -295,7 +302,16 @@ def main(enable_geophires_logging_config=True): means = np.nanmean(Results, 0) std = np.nanstd(Results, 0) + print(" Calculation Time: " + "{0:10.3f}".format((time.time() - tic)) + " sec\n") + logger.info(" Calculation Time: " + "{0:10.3f}".format((time.time() - tic)) + " sec\n") + print(" Calculation Time per iteration: " + "{0:10.3f}".format(((time.time() - tic)) / actual_records_count) + " sec\n") + logger.info(" Calculation Time per iteration: " + "{0:10.3f}".format(((time.time() - tic)) / actual_records_count) + " sec\n") + if Iterations != actual_records_count: + print("\n\nNOTE:" + str(actual_records_count) + " iterations finished successfully and were used to calculate the statistics.\n\n") + logger.warning("\n\nNOTE:" + str(actual_records_count) + " iterations finished successfully and were used to calculate the statistics.\n\n") + # write them out + annotations = "" with open(Outputfile, "a") as f: i = 0 if Iterations != actual_records_count: @@ -303,20 +319,32 @@ def main(enable_geophires_logging_config=True): for output in Outputs: f.write(output + ":" + "\n") f.write(f" minimum: {mins[i]:,.2f}\n") + annotations = annotations + f" minimum: {mins[i]:,.2f}\n" f.write(f" maximum: {maxs[i]:,.2f}\n") + annotations = annotations + f" maximum: {maxs[i]:,.2f}\n" f.write(f" median: {medians[i]:,.2f}\n") + annotations = annotations + f" median: {medians[i]:,.2f}\n" f.write(f" average: {averages[i]:,.2f}\n") + annotations = annotations + f" average: {averages[i]:,.2f}\n" f.write(f" mean: {means[i]:,.2f}\n") + annotations = annotations + f" mean: {means[i]:,.2f}\n" f.write(f" standard deviation: {std[i]:,.2f}\n") + annotations = annotations + f" standard deviation: {std[i]:,.2f}\n" + + plt.figure(figsize=(8, 6)) + ax = plt.subplot() + ax.set_title(output) + ax.set_xlabel("Output units") + ax.set_ylabel("Probability") + + plt.figtext(0.11, 0.74, annotations, fontsize=8) + ret = plt.hist(df[df.columns[i]].tolist(), bins=50, density=True) + f.write('bin values (as percentage): ' + str(ret[0]) + '\n') + f.write('bin edges: ' + str(ret[1]) + '\n') + fname = df.columns[i].strip().replace("/", "-") + plt.savefig(working_dir + fname + '.png') i = i + 1 - - print(" Calculation Time: " + "{0:10.3f}".format((time.time() - tic)) + " sec\n") - logger.info(" Calculation Time: " + "{0:10.3f}".format((time.time() - tic)) + " sec\n") - print(" Calculation Time per iteration: " + "{0:10.3f}".format(((time.time() - tic)) / actual_records_count) + " sec\n") - logger.info(" Calculation Time per iteration: " + "{0:10.3f}".format(((time.time() - tic)) / actual_records_count) + " sec\n") - if Iterations != actual_records_count: - print("\n\nNOTE:" + str(actual_records_count) + " iterations finished successfully and were used to calculate the statistics.\n\n") - logger.warning("\n\nNOTE:" + str(actual_records_count) + " iterations finished successfully and were used to calculate the statistics.\n\n") + annotations = "" logger.info("Complete " + str(__name__) + ": " + sys._getframe().f_code.co_name) diff --git a/src/geophires_x/Model.py b/src/geophires_x/Model.py index be75196d..9e085c84 100644 --- a/src/geophires_x/Model.py +++ b/src/geophires_x/Model.py @@ -3,12 +3,21 @@ import time import logging.config -from geophires_x.OptionList import EndUseOptions from geophires_x.GeoPHIRESUtils import read_input_file from geophires_x.WellBores import WellBores from geophires_x.SurfacePlant import SurfacePlant +from geophires_x.SurfacePlantDirectUseHeat import surface_plant_direct_use_heat +from geophires_x.SurfacePlantSubcriticalORC import surface_plant_subcritical_orc +from geophires_x.SurfacePlantSupercriticalORC import surface_plant_supercritical_orc +from geophires_x.SurfacePlantSingleFlash import surface_plant_single_flash +from geophires_x.SurfacePlantDoubleFlash import surface_plant_double_flash +from geophires_x.SurfacePlantAbsorptionChiller import surface_plant_absorption_chiller +from geophires_x.SurfacePlantDistrictHeating import surface_plant_district_heating +from geophires_x.SurfacePlantHeatPump import surface_plant_heat_pump +from geophires_x.SurfacePlantSUTRA import surface_plant_sutra from geophires_x.Economics import Economics from geophires_x.Outputs import Outputs +from geophires_x.OptionList import EndUseOptions, PlantType class Model(object): @@ -88,12 +97,12 @@ def __init__(self, enable_geophires_logging_config=True): if 'Reservoir Model' in self.InputParameters: if self.InputParameters['Reservoir Model'].sValue == '7': - #if we use SUTRA output for simulating reservoir thermal energy storage, we use a special wellbore object that can handle SUTRA data + # if we use SUTRA output for simulating reservoir thermal energy storage, we use a special wellbore object that can handle SUTRA data del self.wellbores from geophires_x.SUTRAWellBores import SUTRAWellBores as SUTRAWellBores self.wellbores = SUTRAWellBores(self) del self.surfaceplant - from geophires_x.SUTRASurfacePlant import SUTRASurfacePlant as SUTRASurfacePlant + from geophires_x.SurfacePlantSUTRA import SUTRASurfacePlant as SUTRASurfacePlant self.surfaceplant = SUTRASurfacePlant(self) del self.economics from geophires_x.SUTRAEconomics import SUTRAEconomics as SUTRAEconomics @@ -183,6 +192,51 @@ def read_parameters(self) -> None: self.sdacgteconomics.read_parameters(self) self.sdacgtoutputs.read_parameters(self) + # Once we are done reading and processing parameters, we reset the objects to more specific objects based on user choices + + if self.surfaceplant.enduse_option.value not in [EndUseOptions.HEAT]: + # if we are doing power generation, we need to instantiate the surface plant object based on the user input + if self.surfaceplant.plant_type.value == PlantType.SUB_CRITICAL_ORC: + self.surfaceplant = surface_plant_subcritical_orc(self) + elif self.surfaceplant.plant_type.value == PlantType.SUPER_CRITICAL_ORC: + self.surfaceplant = surface_plant_supercritical_orc(self) + elif self.surfaceplant.plant_type.value == PlantType.SINGLE_FLASH: + self.surfaceplant = surface_plant_single_flash(self) + else: # default is double flash + self.surfaceplant = surface_plant_double_flash(self) + + # re-read the parameters for the newly instantiated surface plant + self.surfaceplant.read_parameters(self) + + # assume that if they are doing CHP of some kind, we have two surface plants we need to account for, + # and that the second surface plant is industrial heat only + if self.surfaceplant.enduse_option.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, + EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICITY, + EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, + EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICITY, + EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, + EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICITY]: + self.surfaceplant2 = surface_plant_direct_use_heat(self) + self.surfaceplant2.read_parameters(self) #read the parameters for the second surface plant + else: #direct use heat only style physical plant + if self.surfaceplant.plant_type.value == PlantType.ABSORPTION_CHILLER: + self.surfaceplant = surface_plant_absorption_chiller(self) + elif self.surfaceplant.plant_type.value == PlantType.HEAT_PUMP: + self.surfaceplant = surface_plant_heat_pump(self) + elif self.surfaceplant.plant_type.value == PlantType.DISTRICT_HEATING: + self.surfaceplant = surface_plant_district_heating(self) + elif self.surfaceplant.plant_type.value == PlantType.RTES: + self.surfaceplant = surface_plant_sutra(self) + else: + self.surfaceplant = surface_plant_direct_use_heat(self) + + # re-read the parameters for the newly instantiated surface plant + self.surfaceplant.read_parameters(self) + + # if end-use option is 8 (district heating), some calculations are required prior to the reservoir and wellbore simulations + if self.surfaceplant.plant_type.value == PlantType.DISTRICT_HEATING: + self.surfaceplant.CalculateDHDemand(self) # calculate district heating demand + self.logger.info("complete " + str(__class__) + ": " + sys._getframe().f_code.co_name) def Calculate(self): @@ -198,21 +252,24 @@ def Calculate(self): # This is where all the calculations are made using all the values that have been set. # This is handled on a class-by-class basis - # We choose not to call calculate of the parent, but rather let the child handle the - # call to the parent if it is needed - - # if end-use option is 8 (district heating), some calculations are required prior to the reservoir and wellbore simulations - if self.surfaceplant.enduseoption.value == EndUseOptions.DISTRICT_HEATING: - self.surfaceplant.CalculateDHDemand(self) # calculate district heating demand self.reserv.Calculate(self) # model the reservoir self.wellbores.Calculate(self) # model the wellbores self.surfaceplant.Calculate(self) # model the surfaceplant + # if we are doing cogeneration, we need to calculate the values for second surface plant + if self.surfaceplant.enduse_option.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, + EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICITY, + EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, + EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICITY, + EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, + EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICITY]: + self.surfaceplant2.Calculate(self) + # in case of district heating, the surface plant module may have updated the utilization factor, # and therefore we need to recalculate the modules reservoir, wellbore and surface plant. # 1 iteration should be sufficient. - if self.surfaceplant.enduseoption.value == EndUseOptions.DISTRICT_HEATING: + if self.surfaceplant.plant_type.value == PlantType.DISTRICT_HEATING: self.reserv.Calculate(self) # model the reservoir self.wellbores.Calculate(self) # model the wellbores self.surfaceplant.Calculate(self) # model the surfaceplant diff --git a/src/geophires_x/OptionList.py b/src/geophires_x/OptionList.py index e66fbbb2..157224e8 100644 --- a/src/geophires_x/OptionList.py +++ b/src/geophires_x/OptionList.py @@ -6,15 +6,23 @@ class EndUseOptions(str, Enum): ELECTRICITY = "Electricity" # 1 HEAT = "Direct-Use Heat" # 2 COGENERATION_TOPPING_EXTRA_HEAT = "Cogeneration Topping Cycle, Heat sales considered as extra income" # 31 - COGENERATION_TOPPING_EXTRA_ELECTRICTY = "Cogeneration Topping Cycle, Electricity sales considered as extra income" # 32 + COGENERATION_TOPPING_EXTRA_ELECTRICITY = "Cogeneration Topping Cycle, Electricity sales considered as extra income" # 32 COGENERATION_BOTTOMING_EXTRA_HEAT = "Cogeneration Bottoming Cycle, Heat sales considered as extra income" # 41 - COGENERATION_BOTTOMING_EXTRA_ELECTRICTY = "Cogeneration Bottoming Cycle, Electricity sales considered as extra income" # 42 + COGENERATION_BOTTOMING_EXTRA_ELECTRICITY = "Cogeneration Bottoming Cycle, Electricity sales considered as extra income" # 42 COGENERATION_PARALLEL_EXTRA_HEAT = "Cogeneration Parallel Cycle, Heat sales considered as extra income" # 51 - COGENERATION_PARALLEL_EXTRA_ELECTRICTY = "Cogeneration Parallel Cycle, Electricity sales considered as extra income" # 52 - ABSORPTION_CHILLER = "Absorption Chiller" # 6 - HEAT_PUMP = "Heat Pump" # 7 - DISTRICT_HEATING = "District Heating" # 8 - RTES = "Reservoir Thermal Energy Storage" # 9 + COGENERATION_PARALLEL_EXTRA_ELECTRICITY = "Cogeneration Parallel Cycle, Electricity sales considered as extra income" # 52 + + +class PlantType(str, Enum): + SUB_CRITICAL_ORC = "Subcritical ORC" # 1 + SUPER_CRITICAL_ORC = "Supercritical ORC" # 2 + SINGLE_FLASH = "Single-Flash" # 3 + DOUBLE_FLASH = "Double-Flash" # 4 + ABSORPTION_CHILLER = "Absorption Chiller" # 5 + HEAT_PUMP = "Heat Pump" # 6 + DISTRICT_HEATING = "District Heating" # 7 + RTES = "Reservoir Thermal Energy Storage" # 8 + INDUSTRIAL = "Industrial" # 9 class EconomicModel(str, Enum): @@ -24,13 +32,6 @@ class EconomicModel(str, Enum): BICYCLE = "BICYCLE" -class PowerPlantType(str, Enum): - SUB_CRITICAL_ORC = "Subcritical ORC" - SUPER_CRITICAL_ORC = "Supercritical ORC" - SINGLE_FLASH = "Single-Flash" - DOUBLE_FLASH = "Double-Flash" - - class ReservoirModel(str, Enum): CYLINDRICAL = "Simple cylindrical" MULTIPLE_PARALLEL_FRACTURES = "Multiple Parallel Fractures" diff --git a/src/geophires_x/Outputs.py b/src/geophires_x/Outputs.py index f0a0e1a7..25b9ed23 100644 --- a/src/geophires_x/Outputs.py +++ b/src/geophires_x/Outputs.py @@ -1,14 +1,13 @@ import datetime import time import sys - import geophires_x import numpy as np -import geophires_x.Model as Model -from .Parameter import ConvertUnitsBack, ConvertOutputUnits, LookupUnits -from .OptionList import EndUseOptions, EconomicModel, ReservoirModel, FractureShape, ReservoirVolume from matplotlib import pyplot as plt -from .Units import * +import geophires_x.Model as Model +from geophires_x.Parameter import ConvertUnitsBack, ConvertOutputUnits, LookupUnits +from geophires_x.OptionList import EndUseOptions, EconomicModel, ReservoirModel, FractureShape, ReservoirVolume, \ + PlantType NL="\n" @@ -90,7 +89,7 @@ def PrintOutputs(self, model: Model): # Deal with converting Units back to PreferredUnits, if required. # before we write the outputs, we go thru all the parameters for all of the objects and set the values back # to the units that the user entered the data in - # We do this because the value may be displayed in the output, and we want the user to recginze their value, + # reservoir_producible_electricity do this because the value may be displayed in the output, and we want the user to recginze their value, # not some converted value for obj in [model.reserv, model.wellbores, model.surfaceplant, model.economics]: for key in obj.ParameterDict: @@ -99,7 +98,7 @@ def PrintOutputs(self, model: Model): # now we need to loop through all thw output parameters to update their units to # whatever units the user has specified. - # i.e., they may have specified that all LENGTH results must be in feet, so we need to convert those + # i.reservoir_enthalpy., they may have specified that all LENGTH results must be in feet, so we need to convert those # from whatever LENGTH unit they are to feet. # same for all the other classes of units (TEMPERATURE, DENSITY, etc). @@ -130,26 +129,26 @@ def PrintOutputs(self, model: Model): f.write(NL) f.write(' ***SUMMARY OF RESULTS***\n') f.write(NL) - f.write(" End-Use Option: " + str(model.surfaceplant.enduseoption.value.value) + NL) - if model.surfaceplant.enduseoption.value == EndUseOptions.DISTRICT_HEATING: - f.write(f" Annual District Heating Demand: {np.average(model.surfaceplant.annualheatingdemand.value):10.2f} "+ model.surfaceplant.annualheatingdemand.CurrentUnits.value + NL) - f.write(f" Average Annual Geothermal Heat Production: {sum(model.surfaceplant.dhgeothermalheating.value*24)/model.surfaceplant.plantlifetime.value/1e3:10.2f} "+ model.surfaceplant.annualheatingdemand.CurrentUnits.value + NL) - f.write(f" Average Annual Peaking Fuel Heat Production: {sum(model.surfaceplant.dhnaturalgasheating.value*24)/model.surfaceplant.plantlifetime.value/1e3:10.2f} "+ model.surfaceplant.annualheatingdemand.CurrentUnits.value + NL) + f.write(" End-Use Option: " + str(model.surfaceplant.enduse_option.value.value) + NL) + if model.surfaceplant.plant_type.value == PlantType.DISTRICT_HEATING: + f.write(f" Annual District Heating Demand: {np.average(model.surfaceplant.annual_heating_demand.value):10.2f} " + model.surfaceplant.annual_heating_demand.CurrentUnits.value + NL) + f.write(f" Average Annual Geothermal Heat Production: {sum(model.surfaceplant.dh_geothermal_heating.value * 24) / model.surfaceplant.plant_lifetime.value / 1e3:10.2f} " + model.surfaceplant.annual_heating_demand.CurrentUnits.value + NL) + f.write(f" Average Annual Peaking Fuel Heat Production: {sum(model.surfaceplant.dh_natural_gas_heating.value * 24) / model.surfaceplant.plant_lifetime.value / 1e3:10.2f} " + model.surfaceplant.annual_heating_demand.CurrentUnits.value + NL) - if model.surfaceplant.enduseoption.value in [EndUseOptions.ELECTRICITY, EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICTY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICTY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICTY]: #there is an electricity componenent + if model.surfaceplant.enduse_option.value in [EndUseOptions.ELECTRICITY, EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICITY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICITY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICITY]: # there is an electricity componenent f.write(f" Average Net Electricity Production: {np.average(model.surfaceplant.NetElectricityProduced.value):10.2f} " + model.surfaceplant.NetElectricityProduced.CurrentUnits.value + NL) - if model.surfaceplant.enduseoption.value != EndUseOptions.ELECTRICITY: # there is a direct-use component + if model.surfaceplant.enduse_option.value != EndUseOptions.ELECTRICITY: # there is a direct-use component f.write(f" Average Direct-Use Heat Production: {np.average(model.surfaceplant.HeatProduced.value):10.2f} "+ model.surfaceplant.HeatProduced.CurrentUnits.value + NL) - if model.surfaceplant.enduseoption.value == EndUseOptions.ABSORPTION_CHILLER: - f.write(f" Average Cooling Production: {np.average(model.surfaceplant.CoolingProduced.value):10.2f} "+ model.surfaceplant.CoolingProduced.CurrentUnits.value + NL) + if model.surfaceplant.plant_type.value == PlantType.ABSORPTION_CHILLER: + f.write(f" Average Cooling Production: {np.average(model.surfaceplant.cooling_produced.value):10.2f} " + model.surfaceplant.cooling_produced.CurrentUnits.value + NL) - if model.surfaceplant.enduseoption.value in [EndUseOptions.ELECTRICITY, EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT]: #levelized cost expressed as LCOE + if model.surfaceplant.enduse_option.value in [EndUseOptions.ELECTRICITY, EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT]: #levelized cost expressed as LCOE f.write(f" Electricity breakeven price: {model.economics.LCOE.value:10.2f} " + model.economics.LCOE.CurrentUnits.value + NL) - elif model.surfaceplant.enduseoption.value in [EndUseOptions.HEAT, EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICTY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICTY, EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICTY, EndUseOptions.HEAT_PUMP,EndUseOptions.DISTRICT_HEATING]: #levelized cost expressed as LCOH + elif model.surfaceplant.enduse_option.value in [EndUseOptions.HEAT, EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICITY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICITY, EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICITY, PlantType.HEAT_PUMP, PlantType.DISTRICT_HEATING]: #levelized cost expressed as LCOH f.write(f" Direct-Use heat breakeven price: {model.economics.LCOH.value:10.2f} " + model.economics.LCOH.CurrentUnits.value + NL) - elif model.surfaceplant.enduseoption.value == EndUseOptions.ABSORPTION_CHILLER: + elif model.surfaceplant.plant_type.value == PlantType.ABSORPTION_CHILLER: f.write(f" Direct-Use Cooling Breakeven Price: {model.economics.LCOC.value:10.2f} " + model.economics.LCOC.CurrentUnits.value + NL) f.write(f" Number of production wells: {model.wellbores.nprod.value:10.0f}"+NL) @@ -178,8 +177,8 @@ def PrintOutputs(self, model: Model): elif model.economics.econmodel.value == EconomicModel.BICYCLE: f.write(" Economic Model = " + model.economics.econmodel.value.value + NL) f.write(f" Accrued financing during construction: {model.economics.inflrateconstruction.value*100:10.2f} " + model.economics.inflrateconstruction.CurrentUnits.value + NL) - f.write(f" Project lifetime: {model.surfaceplant.plantlifetime.value:10.0f} " + model.surfaceplant.plantlifetime.CurrentUnits.value + NL) - f.write(f" Capacity factor: {model.surfaceplant.utilfactor.value*100:10.1f} %" + NL) + f.write(f" Project lifetime: {model.surfaceplant.plant_lifetime.value:10.0f} " + model.surfaceplant.plant_lifetime.CurrentUnits.value + NL) + f.write(f" Capacity factor: {model.surfaceplant.utilization_factor.value * 100:10.1f} %" + NL) f.write(f" Project NPV: {model.economics.ProjectNPV.value:10.2f} " + model.economics.ProjectNPV.PreferredUnits.value + NL) f.write(f" Project IRR: {model.economics.ProjectIRR.value:10.2f} " + model.economics.ProjectIRR.PreferredUnits.value + NL) f.write(f" Project VIR=PI=PIR: {model.economics.ProjectVIR.value:10.2f}" + NL) @@ -192,7 +191,7 @@ def PrintOutputs(self, model: Model): f.write(f" Number of Injection Wells: {model.wellbores.ninj.value:10.0f}" + NL) f.write(f" Well depth (or total length, if not vertical): {model.reserv.depth.value:10.1f} " + model.reserv.depth.CurrentUnits.value + NL) f.write(f" Water loss rate: {model.reserv.waterloss.value*100:10.1f} " + model.reserv.waterloss.CurrentUnits.value + NL) - f.write(f" Pump efficiency: {model.surfaceplant.pumpeff.value*100:10.1f} " + model.surfaceplant.pumpeff.CurrentUnits.value + NL) + f.write(f" Pump efficiency: {model.surfaceplant.pump_efficiency.value * 100:10.1f} " + model.surfaceplant.pump_efficiency.CurrentUnits.value + NL) f.write(f" Injection temperature: {model.wellbores.Tinj.value:10.1f} " + model.wellbores.Tinj.CurrentUnits.value + NL) if model.wellbores.rameyoptionprod.value: f.write(" Production Wellbore heat transmission calculated with Ramey's model\n") @@ -204,8 +203,8 @@ def PrintOutputs(self, model: Model): f.write(f" Injection well casing ID: {model.wellbores.injwelldiam.value:10.3f} " + model.wellbores.injwelldiam.CurrentUnits.value + NL) f.write(f" Production well casing ID: {model.wellbores.prodwelldiam.value:10.3f} " + model.wellbores.prodwelldiam.CurrentUnits.value + NL) f.write(f" Number of times redrilling: {model.wellbores.redrill.value:10.0f}"+NL) - if model.surfaceplant.enduseoption.value in [EndUseOptions.ELECTRICITY, EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICTY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICTY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICTY]: - f.write(" Power plant type: " + str(model.surfaceplant.pptype.value.value) + NL) + if model.surfaceplant.enduse_option.value in [EndUseOptions.ELECTRICITY, EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICITY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICITY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICITY]: + f.write(" Power plant type: " + str(model.surfaceplant.plant_type.value.value) + NL) f.write(NL) f.write(NL) f.write(' ***RESOURCE CHARACTERISTICS***\n') @@ -269,7 +268,7 @@ def PrintOutputs(self, model: Model): f.write(f" Reservoir impedance: {model.wellbores.impedance.value/1000:10.2f} " + model.wellbores.impedance.CurrentUnits.value + NL) else: f.write(f" Reservoir hydrostatic pressure: {model.wellbores.Phydrostaticcalc.value:10.2f} " + model.wellbores.Phydrostaticcalc.CurrentUnits.value + NL) - f.write(f" Plant outlet pressure: {model.surfaceplant.Pplantoutlet.value:10.2f} " + model.surfaceplant.Pplantoutlet.CurrentUnits.value + NL) + f.write(f" Plant outlet pressure: {model.surfaceplant.plant_outlet_pressure.value:10.2f} " + model.surfaceplant.plant_outlet_pressure.CurrentUnits.value + NL) if model.wellbores.productionwellpumping.value: f.write(f" Production wellhead pressure: {model.wellbores.Pprodwellhead.value:10.2f} " + model.wellbores.Pprodwellhead.CurrentUnits.value + NL) f.write(f" Productivity Index: {model.wellbores.PI.value:10.2f} " + model.wellbores.PI.CurrentUnits.value + NL) @@ -324,15 +323,15 @@ def PrintOutputs(self, model: Model): f.write(f" Drilling and completion costs per well: {model.economics.Cwell.value/(model.wellbores.nprod.value+model.wellbores.ninj.value):10.2f} " + model.economics.Cwell.CurrentUnits.value + NL) f.write(f" Stimulation costs: {model.economics.Cstim.value:10.2f} " + model.economics.Cstim.CurrentUnits.value + NL) f.write(f" Surface power plant costs: {model.economics.Cplant.value:10.2f} " + model.economics.Cplant.CurrentUnits.value + NL) - if model.surfaceplant.enduseoption.value == EndUseOptions.ABSORPTION_CHILLER: + if model.surfaceplant.plant_type.value == PlantType.ABSORPTION_CHILLER: f.write(f" of which Absorption Chiller Cost: {model.economics.chillercapex.value:10.2f} " + model.economics.Cplant.CurrentUnits.value + NL) - if model.surfaceplant.enduseoption.value == EndUseOptions.HEAT_PUMP: + if model.surfaceplant.plant_type.value == PlantType.HEAT_PUMP: f.write(f" of which Heat Pump Cost: {model.economics.heatpumpcapex.value:10.2f} " + model.economics.Cplant.CurrentUnits.value + NL) - if model.surfaceplant.enduseoption.value == EndUseOptions.DISTRICT_HEATING: + if model.surfaceplant.plant_type.value == PlantType.DISTRICT_HEATING: f.write(f" of which Peaking Boiler Cost: {model.economics.peakingboilercost.value:10.2f} " + model.economics.peakingboilercost.CurrentUnits.value + NL) f.write(f" Field gathering system costs: {model.economics.Cgath.value:10.2f} " + model.economics.Cgath.CurrentUnits.value + NL) - if model.surfaceplant.pipinglength.value > 0: f.write(f" Transmission pipeline cost {model.economics.Cpiping.value:10.2f} " + model.economics.Cpiping.CurrentUnits.value + NL) - if model.surfaceplant.enduseoption.value == EndUseOptions.DISTRICT_HEATING: + if model.surfaceplant.piping_length.value > 0: f.write(f" Transmission pipeline cost {model.economics.Cpiping.value:10.2f} " + model.economics.Cpiping.CurrentUnits.value + NL) + if model.surfaceplant.plant_type.value == PlantType.DISTRICT_HEATING: f.write(f" District Heating System Cost: {model.economics.dhdistrictcost.value:10.2f} " + model.economics.dhdistrictcost.CurrentUnits.value + NL) f.write(f" Total surface equipment costs: {(model.economics.Cplant.value+model.economics.Cgath.value):10.2f} " + model.economics.Cplant.CurrentUnits.value + NL) f.write(f" Exploration costs: {model.economics.Cexpl.value:10.2f} " + model.economics.Cexpl.CurrentUnits.value + NL) @@ -352,13 +351,13 @@ def PrintOutputs(self, model: Model): f.write(f" Wellfield maintenance costs: {model.economics.Coamwell.value:10.2f} " + model.economics.Coamwell.CurrentUnits.value + NL) f.write(f" Power plant maintenance costs: {model.economics.Coamplant.value:10.2f} " + model.economics.Coamplant.CurrentUnits.value + NL) f.write(f" Water costs: {model.economics.Coamwater.value:10.2f} " + model.economics.Coamwater.CurrentUnits.value + NL) - if model.surfaceplant.enduseoption.value in [EndUseOptions.HEAT, EndUseOptions.ABSORPTION_CHILLER, EndUseOptions.HEAT_PUMP, EndUseOptions.DISTRICT_HEATING]: + if model.surfaceplant.enduse_option.value in [EndUseOptions.HEAT, PlantType.ABSORPTION_CHILLER, PlantType.HEAT_PUMP, PlantType.DISTRICT_HEATING]: f.write(f" Average Reservoir Pumping Cost: {model.economics.averageannualpumpingcosts.value:10.2f} " + model.economics.averageannualpumpingcosts.CurrentUnits.value + NL) - if model.surfaceplant.enduseoption.value == EndUseOptions.ABSORPTION_CHILLER: + if model.surfaceplant.enduse_option.value == PlantType.ABSORPTION_CHILLER: f.write(f" Absorption Chiller O&M Cost: {model.economics.chilleropex.value:10.2f} " + model.economics.chilleropex.CurrentUnits.value + NL) - if model.surfaceplant.enduseoption.value == EndUseOptions.HEAT_PUMP: + if model.surfaceplant.enduse_option.value == PlantType.HEAT_PUMP: f.write(f" Average Heat Pump Electricity Cost: {model.economics.averageannualheatpumpelectricitycost.value:10.2f} " + model.economics.averageannualheatpumpelectricitycost.CurrentUnits.value + NL) - if model.surfaceplant.enduseoption.value == EndUseOptions.DISTRICT_HEATING: + if model.surfaceplant.enduse_option.value == PlantType.DISTRICT_HEATING: f.write(f" Annual District Heating O&M Cost: {model.economics.dhdistrictoandmcost.value:10.2f} " + model.economics.dhdistrictoandmcost.CurrentUnits.value + NL) f.write(f" Average Annual Peaking Fuel Cost: {model.economics.averageannualngcost.value:10.2f} " + model.economics.averageannualngcost.CurrentUnits.value + NL) @@ -370,7 +369,7 @@ def PrintOutputs(self, model: Model): f.write(NL) f.write(' ***SURFACE EQUIPMENT SIMULATION RESULTS***\n') f.write(NL) - if model.surfaceplant.enduseoption.value in [EndUseOptions.ELECTRICITY, EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICTY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICTY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICTY]: #there is an electricity componenent: + if model.surfaceplant.enduse_option.value in [EndUseOptions.ELECTRICITY, EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICITY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICITY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICITY]: #there is an electricity componenent: f.write(f" Initial geofluid availability: {model.surfaceplant.Availability.value[0]:10.2f} " + model.surfaceplant.Availability.PreferredUnits.value + NL) f.write(f" Maximum Total Electricity Generation: {np.max(model.surfaceplant.ElectricityProduced.value):10.2f} " + model.surfaceplant.ElectricityProduced.PreferredUnits.value + NL) f.write(f" Average Total Electricity Generation: {np.average(model.surfaceplant.ElectricityProduced.value):10.2f} " + model.surfaceplant.ElectricityProduced.PreferredUnits.value + NL) @@ -383,33 +382,33 @@ def PrintOutputs(self, model: Model): f.write(f" Average Annual Total Electricity Generation: {np.average(model.surfaceplant.TotalkWhProduced.value/1E6):10.2f} GWh" + NL) f.write(f" Average Annual Net Electricity Generation: {np.average(model.surfaceplant.NetkWhProduced.value/1E6):10.2f} GWh" + NL) if model.wellbores.PumpingPower.value[0] > 0.0: f.write(f" Initial pumping power/net installed power: {(model.wellbores.PumpingPower.value[0]/model.wellbores.PumpingPower.value[0]*100):10.2f} %" + NL) - if model.surfaceplant.enduseoption.value in [EndUseOptions.HEAT,EndUseOptions.ABSORPTION_CHILLER,EndUseOptions.HEAT_PUMP,EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICTY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICTY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICTY]: #geothermal heating component: + if model.surfaceplant.enduse_option.value in [EndUseOptions.HEAT, PlantType.ABSORPTION_CHILLER, PlantType.HEAT_PUMP, EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICITY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICITY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICITY]: # geothermal heating component: f.write(f" Maximum Net Heat Production: {np.max(model.surfaceplant.HeatProduced.value):10.2f} " + model.surfaceplant.HeatProduced.PreferredUnits.value + NL) f.write(f" Average Net Heat Production: {np.average(model.surfaceplant.HeatProduced.value):10.2f} " + model.surfaceplant.HeatProduced.PreferredUnits.value + NL) f.write(f" Minimum Net Heat Production: {np.min(model.surfaceplant.HeatProduced.value):10.2f} " + model.surfaceplant.HeatProduced.PreferredUnits.value + NL) f.write(f" Initial Net Heat Production: {model.surfaceplant.HeatProduced.value[0]:10.2f} " + model.surfaceplant.HeatProduced.PreferredUnits.value + NL) f.write(f" Average Annual Heat Production: {np.average(model.surfaceplant.HeatkWhProduced.value/1E6):10.2f} GWh" + NL) - if model.surfaceplant.enduseoption.value == EndUseOptions.HEAT_PUMP: - f.write(f" Average Annual Heat Pump Electricity Use: {np.average(model.surfaceplant.HeatPumpElectricitykWhUsed.value/1E6):10.2f} " + "GWh/year" + NL) - if model.surfaceplant.enduseoption.value == EndUseOptions.ABSORPTION_CHILLER: - f.write(f" Maximum Cooling Production: {np.max(model.surfaceplant.CoolingProduced.value):10.2f} " + model.surfaceplant.CoolingProduced.PreferredUnits.value + NL) - f.write(f" Average Cooling Production: {np.average(model.surfaceplant.CoolingProduced.value):10.2f} " + model.surfaceplant.CoolingProduced.PreferredUnits.value + NL) - f.write(f" Minimum Cooling Production: {np.min(model.surfaceplant.CoolingProduced.value):10.2f} " + model.surfaceplant.CoolingProduced.PreferredUnits.value + NL) - f.write(f" Initial Cooling Production: {model.surfaceplant.CoolingProduced.value[0]:10.2f} " + model.surfaceplant.CoolingProduced.PreferredUnits.value + NL) - f.write(f" Average Annual Cooling Production: {np.average(model.surfaceplant.CoolingkWhProduced.value/1E6):10.2f} " + "GWh/year" + NL) - - if model.surfaceplant.enduseoption.value == EndUseOptions.DISTRICT_HEATING: - f.write(f" Annual District Heating Demand: {model.surfaceplant.annualheatingdemand.value:10.2f} " + model.surfaceplant.annualheatingdemand.PreferredUnits.value + NL) - f.write(f" Maximum Daily District Heating Demand: {np.max(model.surfaceplant.dailyheatingdemand.value):10.2f} " + model.surfaceplant.dailyheatingdemand.PreferredUnits.value + NL) - f.write(f" Average Daily District Heating Demand: {np.average(model.surfaceplant.dailyheatingdemand.value):10.2f} " + model.surfaceplant.dailyheatingdemand.PreferredUnits.value + NL) - f.write(f" Minimum Daily District Heating Demand: {np.min(model.surfaceplant.dailyheatingdemand.value):10.2f} " + model.surfaceplant.dailyheatingdemand.PreferredUnits.value + NL) - f.write(f" Maximum Geothermal Heating Production: {np.max(model.surfaceplant.dhgeothermalheating.value):10.2f} " + model.surfaceplant.dhgeothermalheating.PreferredUnits.value + NL) - f.write(f" Average Geothermal Heating Production: {np.average(model.surfaceplant.dhgeothermalheating.value):10.2f} " + model.surfaceplant.dhgeothermalheating.PreferredUnits.value + NL) - f.write(f" Minimum Geothermal Heating Production: {np.min(model.surfaceplant.dhgeothermalheating.value):10.2f} " + model.surfaceplant.dhgeothermalheating.PreferredUnits.value + NL) - f.write(f" Maximum Peaking Boiler Heat Production: {np.max(model.surfaceplant.dhnaturalgasheating.value):10.2f} " + model.surfaceplant.dhnaturalgasheating.PreferredUnits.value + NL) - f.write(f" Average Peaking Boiler Heat Production: {np.average(model.surfaceplant.dhnaturalgasheating.value):10.2f} " + model.surfaceplant.dhnaturalgasheating.PreferredUnits.value + NL) - f.write(f" Minimum Peaking Boiler Heat Production: {np.min(model.surfaceplant.dhnaturalgasheating.value):10.2f} " + model.surfaceplant.dhnaturalgasheating.PreferredUnits.value + NL) + if model.surfaceplant.enduse_option.value == PlantType.HEAT_PUMP: + f.write(f" Average Annual Heat Pump Electricity Use: {np.average(model.surfaceplant.heat_pump_electricity_kwh_used.value / 1E6):10.2f} " + "GWh/year" + NL) + if model.surfaceplant.enduse_option.value == PlantType.ABSORPTION_CHILLER: + f.write(f" Maximum Cooling Production: {np.max(model.surfaceplant.cooling_produced.value):10.2f} " + model.surfaceplant.cooling_produced.PreferredUnits.value + NL) + f.write(f" Average Cooling Production: {np.average(model.surfaceplant.cooling_produced.value):10.2f} " + model.surfaceplant.cooling_produced.PreferredUnits.value + NL) + f.write(f" Minimum Cooling Production: {np.min(model.surfaceplant.cooling_produced.value):10.2f} " + model.surfaceplant.cooling_produced.PreferredUnits.value + NL) + f.write(f" Initial Cooling Production: {model.surfaceplant.cooling_produced.value[0]:10.2f} " + model.surfaceplant.cooling_produced.PreferredUnits.value + NL) + f.write(f" Average Annual Cooling Production: {np.average(model.surfaceplant.cooling_kWh_Produced.value / 1E6):10.2f} " + "GWh/year" + NL) + + if model.surfaceplant.enduse_option.value == PlantType.DISTRICT_HEATING: + f.write(f" Annual District Heating Demand: {model.surfaceplant.annual_heating_demand.value:10.2f} " + model.surfaceplant.annual_heating_demand.PreferredUnits.value + NL) + f.write(f" Maximum Daily District Heating Demand: {np.max(model.surfaceplant.daily_heating_demand.value):10.2f} " + model.surfaceplant.daily_heating_demand.PreferredUnits.value + NL) + f.write(f" Average Daily District Heating Demand: {np.average(model.surfaceplant.daily_heating_demand.value):10.2f} " + model.surfaceplant.daily_heating_demand.PreferredUnits.value + NL) + f.write(f" Minimum Daily District Heating Demand: {np.min(model.surfaceplant.daily_heating_demand.value):10.2f} " + model.surfaceplant.daily_heating_demand.PreferredUnits.value + NL) + f.write(f" Maximum Geothermal Heating Production: {np.max(model.surfaceplant.dh_geothermal_heating.value):10.2f} " + model.surfaceplant.dh_geothermal_heating.PreferredUnits.value + NL) + f.write(f" Average Geothermal Heating Production: {np.average(model.surfaceplant.dh_geothermal_heating.value):10.2f} " + model.surfaceplant.dh_geothermal_heating.PreferredUnits.value + NL) + f.write(f" Minimum Geothermal Heating Production: {np.min(model.surfaceplant.dh_geothermal_heating.value):10.2f} " + model.surfaceplant.dh_geothermal_heating.PreferredUnits.value + NL) + f.write(f" Maximum Peaking Boiler Heat Production: {np.max(model.surfaceplant.dh_natural_gas_heating.value):10.2f} " + model.surfaceplant.dh_natural_gas_heating.PreferredUnits.value + NL) + f.write(f" Average Peaking Boiler Heat Production: {np.average(model.surfaceplant.dh_natural_gas_heating.value):10.2f} " + model.surfaceplant.dh_natural_gas_heating.PreferredUnits.value + NL) + f.write(f" Minimum Peaking Boiler Heat Production: {np.min(model.surfaceplant.dh_natural_gas_heating.value):10.2f} " + model.surfaceplant.dh_natural_gas_heating.PreferredUnits.value + NL) f.write(f" Average Pumping Power: {np.average(model.wellbores.PumpingPower.value):10.2f} " + model.wellbores.PumpingPower.PreferredUnits.value + NL) @@ -418,22 +417,22 @@ def PrintOutputs(self, model: Model): f.write(' ************************************************************\n') f.write(' * HEATING, COOLING AND/OR ELECTRICITY PRODUCTION PROFILE *\n') f.write(' ************************************************************\n') - if model.surfaceplant.enduseoption.value == EndUseOptions.ELECTRICITY: #only electricity + if model.surfaceplant.enduse_option.value == EndUseOptions.ELECTRICITY: #only electricity f.write(' YEAR THERMAL GEOFLUID PUMP NET FIRST LAW\n') f.write(' DRAWDOWN TEMPERATURE POWER POWER EFFICIENCY\n') f.write(" (" + model.wellbores.ProducedTemperature.CurrentUnits.value+") (" + model.wellbores.PumpingPower.CurrentUnits.value + ") (" + model.surfaceplant.NetElectricityProduced.CurrentUnits.value + ") (%)\n") - for i in range(0, model.surfaceplant.plantlifetime.value): + for i in range(0, model.surfaceplant.plant_lifetime.value): f.write(' {0:2.0f} {1:8.4f} {2:8.2f} {3:8.4f} {4:8.4f} {5:8.4f}'.format(i+1, model.wellbores.ProducedTemperature.value[i*model.economics.timestepsperyear.value]/model.wellbores.ProducedTemperature.value[0], model.wellbores.ProducedTemperature.value[i*model.economics.timestepsperyear.value], model.wellbores.PumpingPower.value[i*model.economics.timestepsperyear.value], model.surfaceplant.NetElectricityProduced.value[i*model.economics.timestepsperyear.value], model.surfaceplant.FirstLawEfficiency.value[i*model.economics.timestepsperyear.value]*100)+NL) - elif model.surfaceplant.enduseoption.value == EndUseOptions.HEAT: #only direct-use + elif model.surfaceplant.enduse_option.value == EndUseOptions.HEAT: #only direct-use f.write(' YEAR THERMAL GEOFLUID PUMP NET\n') f.write(' DRAWDOWN TEMPERATURE POWER HEAT\n') f.write(' (deg C) (MW) (MW)\n') - for i in range(0, model.surfaceplant.plantlifetime.value): + for i in range(0, model.surfaceplant.plant_lifetime.value): f.write(' {0:2.0f} {1:8.4f} {2:8.2f} {3:8.4f} {4:8.4f}'.format(i, model.wellbores.ProducedTemperature.value[i*model.economics.timestepsperyear.value]/model.wellbores.ProducedTemperature.value[0], model.wellbores.ProducedTemperature.value[i*model.economics.timestepsperyear.value], @@ -441,22 +440,22 @@ def PrintOutputs(self, model: Model): model.surfaceplant.HeatProduced.value[i*model.economics.timestepsperyear.value])+NL) - elif model.surfaceplant.enduseoption.value in [EndUseOptions.HEAT_PUMP]: #heat pump + elif model.surfaceplant.enduse_option.value in [PlantType.HEAT_PUMP]: #heat pump f.write(' YEAR THERMAL GEOFLUID PUMP NET HEAT PUMP\n') f.write(' DRAWDOWN TEMPERATURE POWER HEAT ELECTRICITY USE\n') f.write(' (deg C) (MWe) (MWt) (MWe)\n') - for i in range(0, model.surfaceplant.plantlifetime.value): + for i in range(0, model.surfaceplant.plant_lifetime.value): f.write(' {0:2.0f} {1:8.4f} {2:8.2f} {3:8.4f} {4:8.4f} {5:8.4f}'.format(i, - model.wellbores.ProducedTemperature.value[i*model.economics.timestepsperyear.value]/model.wellbores.ProducedTemperature.value[0], - model.wellbores.ProducedTemperature.value[i*model.economics.timestepsperyear.value], - model.wellbores.PumpingPower.value[i*model.economics.timestepsperyear.value], - model.surfaceplant.HeatProduced.value[i*model.economics.timestepsperyear.value],model.surfaceplant.HeatPumpElectricityUsed.value[i*model.economics.timestepsperyear.value])+NL) + model.wellbores.ProducedTemperature.value[i*model.economics.timestepsperyear.value] / model.wellbores.ProducedTemperature.value[0], + model.wellbores.ProducedTemperature.value[i*model.economics.timestepsperyear.value], + model.wellbores.PumpingPower.value[i*model.economics.timestepsperyear.value], + model.surfaceplant.HeatProduced.value[i*model.economics.timestepsperyear.value], model.surfaceplant.heat_pump_electricity_used.value[i * model.economics.timestepsperyear.value]) + NL) - elif model.surfaceplant.enduseoption.value in [EndUseOptions.DISTRICT_HEATING]: #district heating + elif model.surfaceplant.enduse_option.value in [PlantType.DISTRICT_HEATING]: #district heating f.write(' YEAR THERMAL GEOFLUID PUMP GEOTHERMAL\n') f.write(' DRAWDOWN TEMPERATURE POWER HEAT OUTPUT\n') f.write(' (deg C) (MWe) (MWt)\n') - for i in range(0, model.surfaceplant.plantlifetime.value): + for i in range(0, model.surfaceplant.plant_lifetime.value): f.write(' {0:2.0f} {1:8.4f} {2:8.2f} {3:8.4f} {4:8.4f}'.format(i, model.wellbores.ProducedTemperature.value[i*model.economics.timestepsperyear.value]/model.wellbores.ProducedTemperature.value[0], model.wellbores.ProducedTemperature.value[i*model.economics.timestepsperyear.value], @@ -464,23 +463,23 @@ def PrintOutputs(self, model: Model): model.surfaceplant.HeatProduced.value[i*model.economics.timestepsperyear.value])+NL) - elif model.surfaceplant.enduseoption.value in [EndUseOptions.ABSORPTION_CHILLER]: #absorption chiller + elif model.surfaceplant.enduse_option.value in [PlantType.ABSORPTION_CHILLER]: #absorption chiller f.write(' YEAR THERMAL GEOFLUID PUMP NET NET\n') f.write(' DRAWDOWN TEMPERATURE POWER HEAT COOLING\n') f.write(' (deg C) (MWe) (MWt) (MWt)\n') - for i in range(0, model.surfaceplant.plantlifetime.value): + for i in range(0, model.surfaceplant.plant_lifetime.value): f.write(' {0:2.0f} {1:8.4f} {2:8.2f} {3:8.4f} {4:8.4f} {5:8.4f}'.format(i, - model.wellbores.ProducedTemperature.value[i*model.economics.timestepsperyear.value]/model.wellbores.ProducedTemperature.value[0], - model.wellbores.ProducedTemperature.value[i*model.economics.timestepsperyear.value], - model.wellbores.PumpingPower.value[i*model.economics.timestepsperyear.value], - model.surfaceplant.HeatProduced.value[i*model.economics.timestepsperyear.value],model.surfaceplant.CoolingProduced.value[i*model.economics.timestepsperyear.value],)+NL) + model.wellbores.ProducedTemperature.value[i*model.economics.timestepsperyear.value] / model.wellbores.ProducedTemperature.value[0], + model.wellbores.ProducedTemperature.value[i*model.economics.timestepsperyear.value], + model.wellbores.PumpingPower.value[i*model.economics.timestepsperyear.value], + model.surfaceplant.HeatProduced.value[i*model.economics.timestepsperyear.value], model.surfaceplant.cooling_produced.value[i * model.economics.timestepsperyear.value], ) + NL) - elif model.surfaceplant.enduseoption.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT,EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICTY,EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICTY,EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT,EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT,EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICTY]: # co-gen + elif model.surfaceplant.enduse_option.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICITY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICITY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICITY]: # co-gen f.write(' YEAR THERMAL GEOFLUID PUMP NET NET FIRST LAW\n') f.write(' DRAWDOWN TEMPERATURE POWER POWER HEAT EFFICIENCY\n') f.write(' (deg C) (MW) (MW) (MW) (%)\n') - for i in range(0, model.surfaceplant.plantlifetime.value): + for i in range(0, model.surfaceplant.plant_lifetime.value): f.write(' {0:2.0f} {1:8.4f} {2:8.2f} {3:8.4f} {4:8.4f} {5:8.4f} {6:8.4f}'.format(i, model.wellbores.ProducedTemperature.value[i*model.economics.timestepsperyear.value]/model.wellbores.ProducedTemperature.value[0], model.wellbores.ProducedTemperature.value[i*model.economics.timestepsperyear.value], @@ -494,21 +493,21 @@ def PrintOutputs(self, model: Model): f.write(' *******************************************************************\n') f.write(' * ANNUAL HEATING, COOLING AND/OR ELECTRICITY PRODUCTION PROFILE *\n') f.write(' *******************************************************************\n') - if model.surfaceplant.enduseoption.value == EndUseOptions.ELECTRICITY: #only electricity + if model.surfaceplant.enduse_option.value == EndUseOptions.ELECTRICITY: #only electricity f.write(' YEAR ELECTRICITY HEAT RESERVOIR PERCENTAGE OF\n') f.write(' PROVIDED EXTRACTED HEAT CONTENT TOTAL HEAT MINED\n') f.write(' (GWh/year) (GWh/year) (10^15 J) (%)\n') - for i in range(0, model.surfaceplant.plantlifetime.value): + for i in range(0, model.surfaceplant.plant_lifetime.value): f.write(' {0:2.0f} {1:8.1f} {2:8.1f} {3:8.2f} {4:8.2f}'.format(i+1, model.surfaceplant.NetkWhProduced.value[i]/1E6, model.surfaceplant.HeatkWhExtracted.value[i]/1E6, model.surfaceplant.RemainingReservoirHeatContent.value[i], (model.reserv.InitialReservoirHeatContent.value-model.surfaceplant.RemainingReservoirHeatContent.value[i])*100/model.reserv.InitialReservoirHeatContent.value)+NL) - elif model.surfaceplant.enduseoption.value == EndUseOptions.HEAT: #only direct-use + elif model.surfaceplant.enduse_option.value == EndUseOptions.HEAT: #only direct-use f.write(' YEAR HEAT HEAT RESERVOIR PERCENTAGE OF\n') f.write(' PROVIDED EXTRACTED HEAT CONTENT TOTAL HEAT MINED\n') f.write(' (GWh/year) (GWh/year) (10^15 J) (%)\n') - for i in range(0, model.surfaceplant.plantlifetime.value): + for i in range(0, model.surfaceplant.plant_lifetime.value): f.write(' {0:2.0f} {1:8.1f} {2:8.1f} {3:8.2f} {4:8.2f}'.format(i+1, model.surfaceplant.HeatkWhProduced.value[i]/1E6, model.surfaceplant.HeatkWhExtracted.value[i]/1E6, @@ -516,34 +515,34 @@ def PrintOutputs(self, model: Model): (model.reserv.InitialReservoirHeatContent.value-model.surfaceplant.RemainingReservoirHeatContent.value[i])*100/model.reserv.InitialReservoirHeatContent.value)+NL) - elif model.surfaceplant.enduseoption.value == EndUseOptions.ABSORPTION_CHILLER: #absorption chiller + elif model.surfaceplant.enduse_option.value == PlantType.ABSORPTION_CHILLER: #absorption chiller f.write(' YEAR COOLING HEAT RESERVOIR PERCENTAGE OF\n') f.write(' PROVIDED EXTRACTED HEAT CONTENT TOTAL HEAT MINED\n') f.write(' (GWh/year) (GWh/year) (10^15 J) (%)\n') - for i in range(0, model.surfaceplant.plantlifetime.value): - f.write(' {0:2.0f} {1:8.1f} {2:8.1f} {3:8.2f} {4:8.2f}'.format(i+1, - model.surfaceplant.CoolingkWhProduced.value[i]/1E6, - model.surfaceplant.HeatkWhExtracted.value[i]/1E6, - model.surfaceplant.RemainingReservoirHeatContent.value[i], - (model.reserv.InitialReservoirHeatContent.value-model.surfaceplant.RemainingReservoirHeatContent.value[i])*100/model.reserv.InitialReservoirHeatContent.value)+NL) + for i in range(0, model.surfaceplant.plant_lifetime.value): + f.write(' {0:2.0f} {1:8.1f} {2:8.1f} {3:8.2f} {4:8.2f}'.format(i + 1, + model.surfaceplant.cooling_kWh_Produced.value[i] / 1E6, + model.surfaceplant.HeatkWhExtracted.value[i] / 1E6, + model.surfaceplant.RemainingReservoirHeatContent.value[i], + (model.reserv.InitialReservoirHeatContent.value-model.surfaceplant.RemainingReservoirHeatContent.value[i]) * 100 / model.reserv.InitialReservoirHeatContent.value)+NL) - elif model.surfaceplant.enduseoption.value == EndUseOptions.HEAT_PUMP: #heat pump + elif model.surfaceplant.enduse_option.value == PlantType.HEAT_PUMP: #heat pump f.write(' YEAR HEATING RESERVOIR HEAT HEAT PUMP RESERVOIR PERCENTAGE OF\n') f.write(' PROVIDED EXTRACTED ELECTRICITY USE HEAT CONTENT TOTAL HEAT MINED\n') f.write(' (GWh/year) (GWh/year) (GWh/year) (10^15 J) (%)\n') - for i in range(0, model.surfaceplant.plantlifetime.value): - f.write(' {0:2.0f} {1:8.1f} {2:8.1f} {3:8.2f} {4:8.2f} {5:8.2f}'.format(i+1, - model.surfaceplant.HeatkWhProduced.value[i]/1E6, - model.surfaceplant.HeatkWhExtracted.value[i]/1E6,model.surfaceplant.HeatPumpElectricitykWhUsed.value[i]/1E6, - model.surfaceplant.RemainingReservoirHeatContent.value[i], - (model.reserv.InitialReservoirHeatContent.value-model.surfaceplant.RemainingReservoirHeatContent.value[i])*100/model.reserv.InitialReservoirHeatContent.value)+NL) - - elif model.surfaceplant.enduseoption.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICTY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICTY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICTY]: #co-gen + for i in range(0, model.surfaceplant.plant_lifetime.value): + f.write(' {0:2.0f} {1:8.1f} {2:8.1f} {3:8.2f} {4:8.2f} {5:8.2f}'.format(i + 1, + model.surfaceplant.HeatkWhProduced.value[i] / 1E6, + model.surfaceplant.HeatkWhExtracted.value[i] / 1E6, model.surfaceplant.heat_pump_electricity_kwh_used.value[i] / 1E6, + model.surfaceplant.RemainingReservoirHeatContent.value[i], + (model.reserv.InitialReservoirHeatContent.value-model.surfaceplant.RemainingReservoirHeatContent.value[i]) * 100 / model.reserv.InitialReservoirHeatContent.value)+NL) + + elif model.surfaceplant.enduse_option.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICITY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICITY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICITY]: #co-gen f.write(' YEAR HEAT ELECTRICITY HEAT RESERVOIR PERCENTAGE OF\n') f.write(' PROVIDED PROVIDED EXTRACTED HEAT CONTENT TOTAL HEAT MINED\n') f.write(' (GWh/year) (GWh/year) (GWh/year) (10^15 J) (%)\n') - for i in range(0, model.surfaceplant.plantlifetime.value): + for i in range(0, model.surfaceplant.plant_lifetime.value): f.write(' {0:2.0f} {1:8.1f} {2:8.1f} {3:8.2f} {4:8.2f} {5:8.2f}'.format(i+1, model.surfaceplant.HeatkWhProduced.value[i]/1E6, model.surfaceplant.NetkWhProduced.value[i]/1E6, @@ -551,17 +550,17 @@ def PrintOutputs(self, model: Model): model.surfaceplant.RemainingReservoirHeatContent.value[i], (model.reserv.InitialReservoirHeatContent.value-model.surfaceplant.RemainingReservoirHeatContent.value[i])*100/model.reserv.InitialReservoirHeatContent.value)+NL) - elif model.surfaceplant.enduseoption.value in [EndUseOptions.DISTRICT_HEATING]: #district-heating + elif model.surfaceplant.enduse_option.value in [PlantType.DISTRICT_HEATING]: #district-heating f.write(' YEAR GEOTHERMAL PEAKING BOILER RESERVOIR HEAT RESERVOIR PERCENTAGE OF\n') f.write(' HEATING PROVIDED HEATING PROVIDED EXTRACTED HEAT CONTENT TOTAL HEAT MINED\n') f.write(' (GWh/year) (GWh/year) (GWh/year) (10^15 J) (%)\n') - for i in range(0, model.surfaceplant.plantlifetime.value): - f.write(' {0:2.0f} {1:8.1f} {2:8.1f} {3:8.2f} {4:8.2f} {5:8.2f}'.format(i+1, - model.surfaceplant.HeatkWhProduced.value[i]/1E6, - model.surfaceplant.annualngdemand.value[i]/1E3, - model.surfaceplant.HeatkWhExtracted.value[i]/1E6, - model.surfaceplant.RemainingReservoirHeatContent.value[i], - (model.reserv.InitialReservoirHeatContent.value-model.surfaceplant.RemainingReservoirHeatContent.value[i])*100/model.reserv.InitialReservoirHeatContent.value)+NL) + for i in range(0, model.surfaceplant.plant_lifetime.value): + f.write(' {0:2.0f} {1:8.1f} {2:8.1f} {3:8.2f} {4:8.2f} {5:8.2f}'.format(i + 1, + model.surfaceplant.HeatkWhProduced.value[i] / 1E6, + model.surfaceplant.annual_ng_demand.value[i] / 1E3, + model.surfaceplant.HeatkWhExtracted.value[i] / 1E6, + model.surfaceplant.RemainingReservoirHeatContent.value[i], + (model.reserv.InitialReservoirHeatContent.value-model.surfaceplant.RemainingReservoirHeatContent.value[i]) * 100 / model.reserv.InitialReservoirHeatContent.value)+NL) f.write(NL) if model.economics.DoAddOnCalculations.value: model.addoutputs.PrintOutputs(model) @@ -586,12 +585,12 @@ def MakeDistrictHeatingPlot(self, model: Model): """ plt.close('all') year_day = np.arange(1, 366, 1) # make an array of days for plot x-axis - plt.plot(year_day, model.surfaceplant.dailyheatingdemand.value, label='District Heating Demand') - plt.fill_between(year_day, 0, model.surfaceplant.dhgeothermalheating.value[0:365] * 24, color="g", alpha=0.5,label='Geothermal Heat Supply') - plt.fill_between(year_day, model.surfaceplant.dhgeothermalheating.value[0:365] * 24,model.surfaceplant.dailyheatingdemand.value, color="r", alpha=0.5,label='Natural Gas Heat Supply') + plt.plot(year_day, model.surfaceplant.daily_heating_demand.value, label='District Heating Demand') + plt.fill_between(year_day, 0, model.surfaceplant.dh_geothermal_heating.value[0:365] * 24, color="g", alpha=0.5, label='Geothermal Heat Supply') + plt.fill_between(year_day, model.surfaceplant.dh_geothermal_heating.value[0:365] * 24, model.surfaceplant.daily_heating_demand.value, color="r", alpha=0.5, label='Natural Gas Heat Supply') plt.xlabel('Ordinal Day') plt.ylabel('Heating Demand/Supply [MWh/day]') - plt.ylim([0, max(model.surfaceplant.dailyheatingdemand.value) * 1.05]) + plt.ylim([0, max(model.surfaceplant.daily_heating_demand.value) * 1.05]) plt.legend() plt.title('Geothermal district heating system with peaking boilers') plt.show(block=False) diff --git a/src/geophires_x/OutputsAddOns.py b/src/geophires_x/OutputsAddOns.py index fc40993e..83d1b1af 100644 --- a/src/geophires_x/OutputsAddOns.py +++ b/src/geophires_x/OutputsAddOns.py @@ -65,12 +65,12 @@ def PrintOutputs(self, model: Model): ") (" + model.addeconomics.ProjectCashFlow.PreferredUnits.value + ") (" + model.addeconomics.ProjectCummCashFlow.PreferredUnits.value+")" + NL) i = 0 - for i in range(0, model.surfaceplant.ConstructionYears.value, 1): + for i in range(0, model.surfaceplant.construction_years.value, 1): # construction years... f.write(f" {i+1:3.0f} {model.addeconomics.AddOnCashFlow.value[i]:5.2f} {model.addeconomics.AddOnCummCashFlow.value[i]:5.2f} {model.addeconomics.ProjectCashFlow.value[i]:5.2f} {model.addeconomics.ProjectCummCashFlow.value[i]:5.2f}" + NL) i = i + 1 ii = 0 - for ii in range(0, (model.surfaceplant.ConstructionYears.value + model.surfaceplant.plantlifetime.value - 1), 1): + for ii in range(0, (model.surfaceplant.construction_years.value + model.surfaceplant.plant_lifetime.value - 1), 1): # running years... f.write(f" {i+1:3.0f} {model.economics.ElecPrice.value[ii]:5.3f} {model.addeconomics.AddOnElecRevenue.value[ii]:5.4f} {model.economics.HeatPrice.value[ii]:5.3f} {model.addeconomics.AddOnHeatRevenue.value[ii]:5.4f} {model.addeconomics.AddOnRevenue.value[ii]:5.2f} {model.addeconomics.AddOnCashFlow.value[ii]:5.2f} {model.addeconomics.AddOnCummCashFlow.value[ii]:5.2f} {model.addeconomics.ProjectCashFlow.value[ii]:5.2f} {model.addeconomics.ProjectCummCashFlow.value[ii]:5.2f}" + NL) ii = ii + 1 diff --git a/src/geophires_x/OutputsCCUS.py b/src/geophires_x/OutputsCCUS.py index 1d122274..ca068040 100644 --- a/src/geophires_x/OutputsCCUS.py +++ b/src/geophires_x/OutputsCCUS.py @@ -55,15 +55,15 @@ def PrintOutputs(self, model: Model): ") ("+model.ccuseconomics.ProjectCashFlow.PreferredUnits.value + ") ("+model.ccuseconomics.ProjectCummCashFlow.PreferredUnits.value + ")" + NL) i = 0 - for i in range(0, model.surfaceplant.ConstructionYears.value, 1): + for i in range(0, model.surfaceplant.construction_years.value, 1): # construction years... f.write(f" {i+1:3.0f} {model.ccuseconomics.ProjectCashFlow.value[i]:5.2f} {model.ccuseconomics.ProjectCummCashFlow.value[i]:5.2f}" + NL) i = i + 1 ii = 0 - for ii in range(0, model.surfaceplant.plantlifetime.value, 1): + for ii in range(0, model.surfaceplant.plant_lifetime.value, 1): # running years... - f.write(f" {ii+1+model.surfaceplant.ConstructionYears.value:3.0f} {model.ccuseconomics.CarbonThatWouldHaveBeenProducedAnnually.value[ii]:5.3f} {model.ccuseconomics.CCUSPrice.value[ii]:5.3f} {model.ccuseconomics.CCUSRevenue.value[ii]:5.2f} {model.ccuseconomics.CCUSCashFlow.value[ii]:5.2f} {model.ccuseconomics.CCUSCummCashFlow.value[ii]:5.2f} {model.ccuseconomics.ProjectCashFlow.value[ii+model.surfaceplant.ConstructionYears.value]:5.2f} {model.ccuseconomics.ProjectCummCashFlow.value[ii+model.surfaceplant.ConstructionYears.value]:5.2f}" + NL) + f.write(f" {ii+1+model.surfaceplant.construction_years.value:3.0f} {model.ccuseconomics.CarbonThatWouldHaveBeenProducedAnnually.value[ii]:5.3f} {model.ccuseconomics.CCUSPrice.value[ii]:5.3f} {model.ccuseconomics.CCUSRevenue.value[ii]:5.2f} {model.ccuseconomics.CCUSCashFlow.value[ii]:5.2f} {model.ccuseconomics.CCUSCummCashFlow.value[ii]:5.2f} {model.ccuseconomics.ProjectCashFlow.value[ii + model.surfaceplant.construction_years.value]:5.2f} {model.ccuseconomics.ProjectCummCashFlow.value[ii + model.surfaceplant.construction_years.value]:5.2f}" + NL) ii = ii + 1 except BaseException as ex: diff --git a/src/geophires_x/OutputsS_DAC_GT.py b/src/geophires_x/OutputsS_DAC_GT.py index 7726b63f..80692ffa 100644 --- a/src/geophires_x/OutputsS_DAC_GT.py +++ b/src/geophires_x/OutputsS_DAC_GT.py @@ -56,7 +56,7 @@ def PrintOutputs(self, model: Model): ") ("+model.sdacgteconomics.S_DAC_GTCummCashFlow.PreferredUnits.value + ") ("+model.sdacgteconomics.CummCostPerTonne.PreferredUnits.value + ")" +NL) i = 0 - for i in range(0, model.surfaceplant.plantlifetime.value, 1): + for i in range(0, model.surfaceplant.plant_lifetime.value, 1): f.write(f" {i+1:3.0f} {model.sdacgteconomics.CarbonExtractedAnnually.value[i]:,.2f} {model.sdacgteconomics.S_DAC_GTCummCarbonExtracted.value[i]:,.2f} {model.sdacgteconomics.S_DAC_GTAnnualCost.value[i]:,.2f} {model.sdacgteconomics.S_DAC_GTCummCashFlow.value[i]:,.2f} {model.sdacgteconomics.CummCostPerTonne.value[i]:.2f}" + NL) i = i + 1 diff --git a/src/geophires_x/Parameter.py b/src/geophires_x/Parameter.py index e1e4e7c4..35c3ae41 100644 --- a/src/geophires_x/Parameter.py +++ b/src/geophires_x/Parameter.py @@ -36,7 +36,7 @@ class OutputParameter: Name (str): The official name of that output value: (any): the value of this parameter - can be int, float, text, bool, list, etc... ToolTipText (str): Text to place in a ToolTip in a UI - UnitType (IntEnum): The class of units that parameter falls in (i.e., "length", "time", "area"...) + UnitType (IntEnum): The class of units that parameter falls in (i.reservoir_enthalpy., "length", "time", "area"...) PreferredUnits (Enum): The units as required by GEOPHIRES (or your algorithms) CurrentUnits (Enum): The units that the parameter is provided in (usually the same PreferredUnits) UnitsMatch (boolean): Internal flag set when units are different @@ -68,7 +68,7 @@ class Parameter: by default, it is: "assuming default value (see manual)" InputComment (str): The optional comment that the user provided with that parameter in the text file ToolTipText (str): Text to place in a ToolTip in a UI - UnitType (IntEnum): The class of units that parameter falls in (i.e., "length", "time", "area"...) + UnitType (IntEnum): The class of units that parameter falls in (i.reservoir_enthalpy., "length", "time", "area"...) PreferredUnits (Enum): The units as required by GEOPHIRES (or your algorithms) CurrentUnits (Enum): The units that the parameter is provided in (usually the same PreferredUnits) UnitsMatch (boolean): Internal flag set when units are different @@ -239,7 +239,7 @@ def ReadParameter(ParameterReadIn: ParameterEntry, ParamToModify, model): model.logger.info(f'Complete {str(__name__)}: {sys._getframe().f_code.co_name}') return - # We have nothing to change - user provide value that was the same as the existing value (likely, the default + # reservoir_producible_electricity have nothing to change - user provide value that was the same as the existing value (likely, the default # value) if New_val == ParamToModify.value: return @@ -270,7 +270,7 @@ def ReadParameter(ParameterReadIn: ParameterEntry, ParamToModify, model): " unless you wish to change it from the default value of (" + str(ParamToModify.DefaultValue) + ")") model.logger.info(f'Complete {str(__name__)}: {sys._getframe().f_code.co_name}') if New_val == ParamToModify.value: - # We have nothing to change - user provide value that was the same as the existing value (likely, the default value) + # reservoir_producible_electricity have nothing to change - user provide value that was the same as the existing value (likely, the default value) model.logger.info("Complete " + str(__name__) + ": " + sys._getframe().f_code.co_name) return # user provided value is out of range, so announce it, leave set to whatever it was set to (default value) @@ -296,7 +296,7 @@ def ReadParameter(ParameterReadIn: ParameterEntry, ParamToModify, model): model.logger.info("Complete " + str(__name__) + ": " + sys._getframe().f_code.co_name) return # All is good. With a list, we have to use the last character of the Description to get the position. - # I.e., "Gradient 1" should yield a position = 0 ("1" - 1) + # I.reservoir_enthalpy., "Gradient 1" should yield a position = 0 ("1" - 1) else: parts = ParameterReadIn.Name.split(' ') position = int(parts[1]) - 1 @@ -314,7 +314,7 @@ def ReadParameter(ParameterReadIn: ParameterEntry, ParamToModify, model): New_val = True if New_val == ParamToModify.value: model.logger.info("Complete " + str(__name__) + ": " + sys._getframe().f_code.co_name) - # We have nothing to change - user provide value that was the same as the existing value (likely, the default value) + # reservoir_producible_electricity have nothing to change - user provide value that was the same as the existing value (likely, the default value) return ParamToModify.value = New_val # set the new value ParamToModify.Provided = True # set provided to true because we are using a user provide value now @@ -322,7 +322,7 @@ def ReadParameter(ParameterReadIn: ParameterEntry, ParamToModify, model): elif isinstance(ParamToModify, strParameter): New_val = str(ParameterReadIn.sValue) if New_val == ParamToModify.value: - # We have nothing to change - user provide value that was the same as the existing value (likely, the default value) + # reservoir_producible_electricity have nothing to change - user provide value that was the same as the existing value (likely, the default value) return ParamToModify.value = New_val # set the new value ParamToModify.Provided = True # set provided to true because we are using a user provide value now @@ -506,7 +506,7 @@ def ConvertUnits(ParamToModify, strUnit: str, model) -> str: def ConvertUnitsBack(ParamToModify, model): """ CovertUnitsBack: Converts units back to what the user specified they as. It does this so that the user can see them - in the report as the units they specified. We know that because CurrentUnits contains the desired units + in the report as the units they specified. reservoir_producible_electricity know that because CurrentUnits contains the desired units :param ParamToModify: The Parameter that will be modified (assuming it passes validation and conversion) - this is the object that will be modified by this method - see Parameter class for details on the fields in it :type ParamToModify: :class:`~geophires_x.Parameter.Parameter` diff --git a/src/geophires_x/Producible Electricity (fluid).png b/src/geophires_x/Producible Electricity (fluid).png new file mode 100644 index 0000000000000000000000000000000000000000..fa0680f5d23939729fd7a4d343ee3854e3e54ed9 GIT binary patch literal 33344 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 literal 0 HcmV?d00001 diff --git a/src/geophires_x/Producible Heat (fluid).png b/src/geophires_x/Producible Heat (fluid).png new file mode 100644 index 0000000000000000000000000000000000000000..467549748e64205767f66e3c7b61628ba092ef70 GIT binary patch literal 31049 zcmeFaXH=Ehwk^8UQmeGoGJ_(ZEX(A+wQ&Z{dv2!YKpAzee)Z0j6QnrWA0nWm5(f0 zv~CfD!C1mlIH<~C%;RM+=AQh10siHI{gn&&W3TO@leTJBCbkZztjrW|b?FAc25fO`jf5CPu>$4&f&r)LXAwOJD(6(VPmYt^m z=0wRwnlc!dQdtN0sb6~7*XW?9IWjXd*fIZmXtru=;=C5N~ zd!%@q@Om9HGd@k72M;z&6bCG^F?w)#`=4_x|0r^vnf}l*?)bx&D6Z>XlYRD$vU(jZ z`8|3!y!2I?I$WBT8hI?oB{1Iq&KJ6v{&Hb}*PZ@+x@69G_`}`edtUnS`~k)s{Bdgi z-0$hf`k&Ciw*y_Ha42+}&MnF{AWg=LsKv zrMR*6yUu&R`1tP6Ym2QiEC!l#+FDwCEK|;`UMpgtUiRdWghRhxWLvCeJUcHh&$|7M z;EflLzjTx~mz^EWUvE|T@j{qc<9au%2$hfou`{o?NJ>tmdM;sb{=%61Az0jL^!#3~ zr9FP)XO%T#)LB;-E~~z^Qa~&-`+P^;+TMyt6@mKxmX^@bzWPY>CC$!X?(!==@QtwG zR|$@D9v|u&EZ|A=587*USU<~NBS9}?r(L(Y%hX8CYafA9{e)d3j${4tzx~Eh@|)=9 z7iJ9iWb5M2xeVS=j}20qV~q|W2?jYa&90M5A+8hAnhAQ*GESpg9QqqL^_w<~k9UNK zXa0JuEzl~rELg^A_|qMc2(zj8?@uZPOKTsvvD7a%Ha5k%r#e3K!pAF3jy=LD=gK}M zRE|#cB$~Gt-b+idc>8c~RjihX%h!)L9H+-S)Ea%}GB|H>|HK%J+htML866p{+MMem zfhEZK`st3O^Vg^T+w~uO+I4@se$ul)>pS}Uw>vnDd2HWYtdr}Up&TR`o}iy4DJ*w+ zD2yrSHvP?^@1&)rE3r?e^Gr$}u-Hp^WVZ

?&sqsT?#gYAuSw|Ir-1OqcvioUiY0}ctwlh;>J@}COqWPvFWo2dS zByFQ^uM&KMg^tDzOF9mpV;e>8yv`dwILD^_&56cLJ3lE0%hg+s%P*7t`baIY;I5H; z!Ywba8uP@Qh)=KXY*h)C?tb_9swdZ8>*7-WjT`qNo>WuJs(kUdS8vrkHZ?g`lVQ_g znbFs$&^L&?kFoEoi#9G|4}Jc8e0->Fq*~WXP*%1{Cz9pEuk-BZYtxehay_Y8x_4_% zCK=yyc6L_Jb-o-qHQZfwl&v1AyaBN^(%&dm+n0DZqxR|Ps})1<-W|t3mITYx>-aZF zDEAHAWmd G$;-O}6QU1@3Qt)qTqNFe7#8^CM>T@yGkClhV`oq*ylU++8P8J~@z= z*pOjU%n`bDgXFoXi4n*4fU~Uw1Br+imPp>WB+m^}5ni$rg>|`B&AAW6&w2(s53wZ9 zy^}{+sAbvr^+&zk-R!Jpl|6WU-Oh85o*5OqZfw*K+-+5zY1dO3qY)cuIXPNyRs8Y7 zxtBMWOX9&Iyu;QA>ulMx=VXR;o8rZ+>8VNc(caqbib${PKXN|7(=vbUD_ECd6X`lL z<4cYu8h4I^0=y`}Xbfm$z2( zu|f~uRW&va^W>H>(+@;IG$mKdO@BQgVP0*kv&*b<}H+rA8Ykd zS;sPY^P6aOl0lARK+>6F{%AY=*goMFGd9p{rKhKNEK)h>n6q3C}=*@V?*>j#(bZRoAS zHma9wYAe3)!^d38WIeryGe4-5Fj|p|gbZ_?t0&(ENyQ$#wL&w#Nc-ujU$V2a zE0fL~4U%=KbEE*j{H0xK zlxJK`{WW>KMnTBNcD?zOeOJ>utA zx4W=&RffWwN=lT%kKVV}8Etl(p^dI=*vc2J=qIE(nKEN{@gmDQ@GYgWdI=usOUJiw z-yWlr(rsa3`0VFHkz-4599S)+e*E0IbGPezoAV6s-Mc5GlYB0zuV#Ts^Xse2WTQ}` z1hidS7`@3{0|R+8)jf%M3D^OGLPD=ia|#O!)vNqg*u1#Joz?Jmpd>}-@}f2FaqbK@ zOFz?=G*v6e$1LYjv8q0A( z-PE{|JcN%Vl3Rsg;(`79*}c#4fF<3go!MlhPm0e{$z&%gYn79$c#}WWT~9p z`JKQ2GB4&t=;7P-z9NPO=GAd0UR+;dF@7bmuEpa0(|u>(J}kpSN?gd}{7%QUPtc_1 z=+RR5z`(%Q_eDZ_T~&MP`gC_2RIE=tYh6C%WK;6-#f^CGJ&eL$j(O}uOSq*^2JW&b zNAel2F?5SQdVjlP=i?h}=Zy3~TuWu>k!3mGhKge}<8?AOEG+D;OSOpo`t_1hfY>Y3 z@gqDg%o{IGUmG`{nMCcEiof6B(SVJtg`74$(IaABn^M0qvF4f4k&N{8bG`4Mv8MkF zd7yheccOc%e)@$!9P=)G+>gL*9$xknK?bA~l?mopC`N$EZiM?R(#!j@Np;e?E$t zqE*g_Klh~%b7UquLs4GeRmbaUBpI{r^2kj`q^w0y;zg<l!-rES-->&!Ti>BDrJVX>E}@|yDTmw=l`o@SHIMeqBRG)bI6~Lb zdSTD-NR6Su`j=)^G2!E1I?UfcJsqPK=}DF829N8WSL?Vhw=O5ZHi3B3h*k~X8FKah z{rj1=T~AbG#}8u1QUZn zL=56izqsDzr)5y)GS(P5*iv9VGd-m|=O9~R;VXd<=O@@Yf+#B3Lf0LKJ1-X389Mbz z;4Tf<$+>24*GqrYUJ_`Ihxs~4YD_6DP}U`r7Kl^aq%)<)zy})%$Y!`wO(hy%6aB4K zRYanIzSv*HFadk12Op&M%iQ_1uRxwRYs^fkm|n1yD+-Ti{`~n}pv?!Yu-ccMoSKJ!kYsqT&H<)dv}jQ|!pG;EZlLzTBS(r03XW_( z;#s*^B-^{JjUgZNZXSF7{O`WoA}ecv-B&qpnY4GWWPg8uj9$i&zLbd`F=)m&V0SLQE{Q4Lq@Zpz7W9PNz;l$D<|XO5&@w{LZVek^iuLv=;8rF}M< zmncGrefl-P_Xl3xS(_KScL{^{`D*~Wn2tR~TjnkJk=L0~X#G8JKxL{$ePyz#TF=ze z(=V7T+7KwPh_o>5CeyEIchHKmOIo?pXHE4Xr3| zfWrR$S3G&-5&*|VYtZb=^Ba4VU0b|X9lJQP>j!T~l#kZ0gDpkMV)M}?p@IIE}w`-%r!*4#A;ma&u;2M1Hn?0(FnzRikf zcLFun|8w75bp5awcRYZ5lXcFSVt>=lcaL|e``S}K2;i%ce&KXmX^7k_>b`{kY6b=B zr;)B>feMF4Ml_IVcAkC96*-Evh{k0B&txCIyUwpWwWkkteaX6=$AKIO&rbmiI5gOY z*2jyB2fbKp==}Kh>a8(&OLt9T$j?SbYE@W>B+jeHHj9asD~JIRaOx{w&vG1z(ay4u zuLVjT92!!z4+0Dd$aR@q$7)BlElPN9QnJow`rBnA57E=7PZM-)N5undbscY8tCnJ> zVKeyYkj+T9hFYwq@N;~fZfawcX+;G8mMw)GMsE-?$NK702GaW+({#_8NZvqq6tIF% z@wd;PFZg(SpQ2qSe&)5WZt5I{ckO4FzPGggfm8dvIK}gAyUJfBTQxVS$DJ%oHm`Zx z+mutEY*zJ}ewLp8+QN^1hr{StqCw6ZR6Apf8tl-DTo)&`aK+UXh$wVkoVf0N`}Vcp zJHjoR&+#0PR4radLj2V4jFz4r^BAo}O90t31i|U=-PQ5t!21u3rHnkrKi=RuhQd{m z9a6c+`Rj$GIm}I){zNBIeDKhr#+~jBQ7BU;=yb3aNGRu!^Q}^pf+VYK2b!k*Eo!7L ze)_ctMgG>^yAt^vbgy_GCZhvDY!l~x3fi_kn%iaPdOHjiu!d|Yxi|2k`-i(Fw3RRS^ z;U=en@Gi~__GYIEn2OPKw_yPXf#lc!Bl9o)zd5+SB<%4O$ur!gEC$=USYKA{~VS#|}dj#GvD>u1G<199ABFG#0fct&yJv#--0#cdT&E_8`@o(^K${(ft|Tob zMECBTXe`X!dGl7UUL8)Ke8h`8t>G+?AvO{H!%;Rh-i3ts2Vi0ON)_pkwo)~Xw%wJ2GUKg2IysJZnZ1UJGhlLg zvp!AM1#NaW-t=uh0@~Qp*)Yqd;fkLJ08A|CB|`7F*9gae)evGbTO;b<1k30yz44;U zZt8%jS^=cObkKAQG$Ie()g9jy+ zkdP38n2$iVjzD%dDGin}l~hKTanE?u=FJ1it>-)L8@h@j&_z)JC8Qksk7?+lKnos! z>J8GV5|Z38{9J-fXo-w+S2X`kkW@`&yl&0c$;m(w@u}Op3k+{3%`$07zZikWIpTb6k_o7S_8^BQQS=XD2=N^~HHo|Pe`D5D zTc#Icu9g*2skq%etkNGvWzm{#=cp8;&f%KG0K#{o9cHDv4CIU*1JsTm+o~D&G-13i zwQ&nLMt*7O21i4r_UEX1Qf9@L9iJN=ngh_`=nl+GPg<#=8KSLpYVF*@pq4dpx*UjL zkZPgGEseH0zyB^j(pyWxW!8}XEO%zwiD(;flQ#+%1FdNLm$IhA!dW z-ap~0_W`z5p@gNNd&V-WfJX~uMuTpcK=YPj)1fS^pQ(-uY zyI4ZeNSr!{Zm(jEUcyyWp&=MSJkOHCw^r~aTh#B$`SMoEalGxm)nHM$iyYEM6>^7a zPP6XCJzNCExm|`&_zCN35>ts-6a-hy?75Cyjn%GpLPLYR-FTvY1T=IgXzSG?2GT{D zY4!Gr4_~CzR$I;H#ieN;+j+&;OJ90Q+XHl(&dU05O@zm)pMH8YZ{gCg^tZ8zxiM%% zv#F4_vtLl%P5YlH;?yG!mo^<%lH90I@R4`Fby+7yM$6)&KC0VBHfkY&~=PjZTZhsRHilTcM zER)*7gTJEc*|xo2Z5{-Qu)9jjFr~>0I)~iMx96a*!_fOubd`hPjRX{~GnfG$ihD;u z9R2%SGhJv5t`l7X1i9<7FF$WD4RJeCAY_o8?jc&2<&eniLL_8{PBkil&|I`^Sq(mf z$pVx6?vJYr?I0Z-RqXxcf!LYb%&unz+!bJOS1n(@(Z~b3kL5^ZgJnI~Z}kl8GqJ&V z-sdKUyVxtvo|i)xv~IVh^AQVEnHyj4bWlh0fyXaP@#g`p{>k;-uYVCPO0B)Pw8jY( zQL<6N(kbjMb{L?zcE^S1l)mjgN4_w22I0Xb?tG8%vPFwjp;7ViHO2OS8gd)&1K_7- z0gFSxji^z#Z-X~LFPUl8`XgV(f*yVyBA53-!rWvkeDAhx$4X_!9@0vz1}U<5&9>tZ zE!3ZzC=u75>e&XYM=d6HJeZYhl~~03ge>gNbyD_mW;F>xVetUJ#24TjD>LoPu|Y~R zbA-cFQFu)8#JcNJV!>wndV4<-REsznQUEskFLVgPf4Du+igp`&MWPGx+&~nVLnCFk zAUCL0Rba+6;!d7GBTQ1mNJg`zu1@Lc4_j{;SxRTS_?70}WeOgd{xoFvHEdHmzs2(w zE^5=*d`HcGcb`MWU3TW1ja~2i6TpnhUdp-Jv$4bl9A9>b_k_B-FF52?oSc`7>z`dX zi@dLq=jvk7lwB*R(4@3dKv^Ami71Eg;%Tc6u7taDSa;w3&{saig@`o!nrq*Iw)gwI zcM4XfOL!r^ayHlWa~a8F8~%eH>+Pfm34kOo)~&0*;ID!G1v~SJVWCG*F$C=|200w8 zZwwmnFvy4kSyNCM+Io7ny1BV&BpT?0?|N2+BJl_$j{#ACK-X~s2ODGh4x8jev{&r#Z!DB-yJ`DwzQQe)XQ??~S~O@2~ef>mkbH{P~wIAUCv@N&Zv7UJ(Fz z{NR|;8tPi*oI&ICTkmK`X-Jx@r-#Q*Y!_k&QQ(1Fj#k8Ji?3X}78L4UBLDy+lp&kq z)Sg!b1%aTs0#QCtSOfN4eA3mB@nu6wW2b#jc7@Frsh=|hzq}S}@z48JaP+HGMDNWu zem9>;C2SIY)2yn!Ktzxyt76)Ls7T9*0Pr$FUPwDXkHNdO_~w9l?GhPY?U(DVvL7Sh zy3q$}qQ|a8V@Vl~m=ydt43cpRWIDDjM1ql?8Wmg)b?d{u&r5~)`46*)sR`S6bpf`| z127m-U=VsZ(7RQKv3!^;^O^*WQ>PvQ?W?07bpb$A?im?qHU#q5qE834t}~hEmp3!5 zd(?kh88AtZ|3F>JQG7#^iM%`RU?CF$-HWQKG*CjR zZ5r-NYi>&FbF5FZx{s#k!7d9S!9meHxo@AZLeX{ns}&s><~pY5@$;so3fmK2Yo}gk z3Wm%a$aFq+G|3`O@&>nzBOwPOLMv3{`qa-VqF{Vz0z0%HRW>3qcS3RF#*HNHPK@@& zf@Lv7QmIR`3L$}pRsQ+oi;>^wEjw52FI)$-2i|(;_y5>!d2BQ0lh9so{WuU4o^ho?D_`0djFP72gDnH!w7*J1J0$;zY)+IkbKgKZ6 z^#M8viLDb708QmcP;snbA|)5E-A-~%QNQ`eKd;fFAi5C1P>`2bUiFin0MJ=V^_RiH z6DLo;7p_lmXmLa&_(VoVq9(c441gu4DXo!Cqr7|2Qpk!G-Iiv)(-={X!kvS{J8o}$8Phirn^0a97(y-PHMGCYwYHT zj{l-u`iHR9zKqLjPUuw%#tLtoNLukkgs#KXjYfG_;bNdpipPUlJ7xErZLi>QVtl>(GWUO%d_wcb+t2C^`2 z^JDw=u}`bRe%+%#H6X$&zxF}nwSKN;lEv=e7b8Ua6DY>lr~)!0gNGL&+569K%zWXK zweP@(g+Wq&-P~*lX^;S>(?Cunu@HbJq+JaEv5<#aLdVa4Ec9tC`1OaCJ1=~YC(0R3 zyB3~{s$DNiH7VkPP1o(jDa6sV{rvOKBs-BZ#a_k3lL%!Uk)ktbOL??tDsU>7BLFv_*_sJ@)FW)A; zQ4$G>`?rbvTuaxkwv^L5>-s=w<=V|P+4D8hr!KIp&0^IB3%j_&Uf!5T?i4&aP2djn zT)TU|;t8msN8{^?m|ctZ9r?d`Y@d7U$&)9oZEfMuHFcXDZAEI)SPgS8AOERfU-^-|IhQYAF2ZV~HL4D~ z%-(6$Bn_RAXk2QoWxsy7s@rdctFyMVOW{}Z4TJ7cR8vzUnGb0oE)^X?EK(8reo(4a zJK7sDC!1fW=_<_UWFYM)}uWVabtQrw78(91}=zKq9G$y<iEX39tyK26iP#=eZI`QTzgefmup~VNJbRebyu?B8=!`b-HT3lz@e^20+|7j z`XOiv%EOVV;1{A)LS(Ba0SjV5a1>RGu=r(_&$><=%n+(lNu7G5qUIaxyn#!_6CuxIE(fYAY|5cV=J z2Ib!n+WB$#O(ejnukzZ+C%Y+4BUY0@B6Xuk5_;sY346);7JN)J`bx?W*u+%;=lz|| z;6b7Qi;l8^Z_$CY1x}|pfBx`?BLq1OEOKNUl*u&L+)IN`JVdJvTOGii&-aI%Uszfy z^2)%w_`ZlaUM~1H(z=8ch_k_$^BreEFqi^Xv%^r1qF`hYd@CEmCMgMffZS(FHT7ts zl!K+CsrgD*ZXIYA8k@T78!BIZS!R3hgtn!@E8mK&2Nl+!!SEGZckR*yF3-eOjOffj*>&Mqyyp4{BJob?4`j~KuA-7Rnnc+ zq^~oVaEcxL<(FTm|0B{K;{P8p7N1_;!X0{1sG{VLPfT<}kOVA%q6$hxX)v404CM&` zMa?~#W#Io(Oe^@|kK0r`3MlpZ)hoVNB25J3`}1a82_1=_eM?qCKEAhpqG(825 zA{vi`q;f%S0X{?L!E2CU@0}mG?aOieiPuc1a|*X|`5GtL{cEqfsZTF-;y3LBtxud6|N%uEfq-S+ZQ`Eq$K z!#(b|efoIb{en-pn$n^gE3U ze}XV^R7GaQm-Z8`sS6ZhB`|Iqyl%h~F#rWcP1jQ$N6bhjK$EX{yTE(JJbAF9Ry~@j zxRY)$K0`x8<$$%IsdPzm#s*ZX8bvjqwR)gi%&w5X;de9HHWJ%9?-p z&ueAB*j$uKTm`T+5eE>B;pEdLi9Y>e3p-BAqHd?m;LDYcBUM^6&Vu2VFHbG^dO^X(}Ev49wFAv5Mii2G1|`~P@RQX)iPtCV@YU| zxSB3{7;mpYOx`ng16+eLpN1}7)y`wR{tRC=M=16DSX0G{y|}mc6B$U?BqopvZ>u|( zoU5L&PVzhQ8-d+mW_jTeYy~U1?jZymQ`#B8j7j;fdMzCtkr10r0UR8ApA|ruF}lXj znl-@4+(5TQn@=+;bInf)B`db`jY@;Bav;VqMIU4X=}b3oejmG*2>^#&$g=k6;c3yz zv^AxbnZPbo+aSMY+sQ{*ifH^-lu9lt=va~tA0$|+`-l08m|HeZJXw_5nfTu4v1H1y zK8t-TM`-2xcrqj)2*~AhO1UOS|gg zPt}ZOST;)b*aty>m9T2kqjCZLlzm;JVc>y#kXNfk(Z0lEhw6Mhzy?DmA}1#o zn_y1RwW}iXs3Mn_P49bA3Co6)?BVX%M4cQVM3|;9c>H(BF!80EBrLg{td+20xEoPQ zogg=arW&^Sxmx;5n>!pgX!aLsu>4I=dZ=*nnpJ%2T%{1rx07?p?!!_gf(ZOqF~?>j z56!rfk5SMCbdTU)p&tveZsmx9Q%|dQ87dNpZ}J6uO56m{*U^!^$WFb#FxZEV{kCHt zDz7h`+JvtG-r zv+?xl4&dur$Cu}iANLM>$za#jKcr0G!vQ#1;VY=74!N6EzD(}hBkVMC57q$9gGX2F zN&JE?RHdSIzusY>#&g2GJ*uxMs>M;e7lUsgqwJ_|0i%c3yV&%RXVXxk5BD$d>E+t-Vc zN9MVKl%IedycM609xE2vkqn-EEHAm~bbP9Xz88>x_qDYK>7jMVLDesgbp|My1Z#hR z{`C6Z<{J{b!a!(%#8YPT8s8w!3+%3Pi&lRsq&KLFtZEA_2VL;VATAR@o=7z=aM&$; z@~J#1iRyWCm?rxdGCnAuX&*4xoM(W z{`>_C$a+%LFMu=>;xZagVf-r_QX-GGN+TJvAMCbDK!qjTjFd@rg%Cf|loo2kaRk}O z!PxaK$XYCu#bYBQcA2lv16L~B1Py~DE0vv$qiF@;qZl*@$~LZgiD)>KKXyHUv*a9t zV%GBw_9-$I5YrTBn;q8KdCqT}cDZ0-;}$V7Rcvf^FcLn=pv}6`sB56+JoE?gRByfY zxauJ(uY0VUHAUB~y36||UTcYWo%9lyQ!4W)<)3x;JB`Lcg^t6dMJ#Tei~~e9p(3jp z7#ILiso#E{2-UdZCb##tPj`1W9S7JL0eKaRat7;04(Vw?@bshOr&|IK+wXOPc8B?d zEL5GioRs$IuXsJKYD28RX_Sg;QN+SdOch{ZyK^V|kJ?A{BvVHLoX%vS$s+M#wzVP0 zH#tRQ$6HrkzO<*b&_&o;;g(@;;$E9}4;qYsD?ouwye~)zf$=MDCD>HtwCsGbOzzmT zc62Ld2&QTyJ8*u{U;yyMsQg02Sq?Jg!+iivIDqE4bW7y80HDL%*k>L*RUz%E$Y=Iv_HW3AOX<3%3K#P z6K>Khy)zywOEi?rfyW1eg5m9sAppWym>sIWts&C~8kM+=BiQqxwnJwz z`3$&P8ktzIWZgNSX~N`!C4$x^`1m$_3z{RQDRoMihzSEmlx5Y=OpnRE3SL%ICMaWT z`&#Ywi9+3st^6|_ZGT!AR?;uYoI`S=)IhdTiFOjoIG(5yK#4jK%m@-!TUrj6?0ed!rX^b8am89ok z!-6;XxiP`kg8wtE25ysZ3hGh#oOuhvPS&`?^Nto9!q5TWqbzI^a%9Ai{whgaA{Z&K z9?oqjn?dgraaKIw;H;>CWB=fvfHLlq=jAi^z;EZKhGhJX)i3XFYQR9dQnWl5@wRgZpx zbjG(gA77brvE{I^VQzztX6WH1q|Sq8V28n{(F4v5meMZbqH+xC5KlQdjiqL{xs!Gb{Kkz=8m=f>-<{u`( zF!({T!q%H@t>7&8Eyzpb+MZy2sZ}!=LZ{h#L)Kh{Z*2KVrjRc&Y>5A5Rq&0_F;5K zu#+xQo|nq11xsI!Gw_6{*@?{N$1M5F2=>f#BcJlqOGwz| zaoW#Op9LaC1@N>qK9|J{;|FMU2 zxuoom0et&2(M>1|C_jAq_A!=}#$yN}{0*ly>xrw43~|)~Z5AH(lK0}sXM6NHUKM^X zq*@Kq<1Aa*;)#0Jt;xas_IO&5IJJRf@B6+#YA^g*Kg0SE4LpW~7{NCnZyiaKs8)4Z7<+PEmZAlJni6a7v&IHeOM4{&@A{Q0}di$HnHwyi9TREe zAxc~#Jm}{euh8Je)~E(WED(C7cyKgAta=Yl@yaX*E3%KkC#DX3O~#R3MCGt)K9T|- z{K9d(36R|E+GB7I)HNZWS7UM2(N5-!)|s!{bKz0gOHnZ}?V4M%+HpTcO{)Hyc0 zCSF}%*Is4%@Q$(l+O#7>nSzqed2eTS9eI_REqLQj>>@caRhp0c^yw2Qnpm)AC%_`H z!^qC=HZx7k9cl!(Q?IC9S9zOf4{*iUf*Ydw#*zf)lFQ z!abJffL+O#5x7(`ZzaG@xsQr$G#*`K@pSmhNM*k*C;oI?R1@@>Gh1?hcDg^;9canEggRe;~AGTwCkSgpm*g0Y-Bg$z;O`gLO4XV~KraecC`y8&RmbSLG z%2BkrfxvUWL12anjAmCj_gpxYG`ZJpI(x(hdt-2PbQi2R7$f}h=VFoHz!$DFT9J4F zAgBZ@9+WCVin+M+vx`LmrK<)cihMUbYv|Hw_v7&}Z`vPmF6|=pc&LCL;K<0v2EQJ- z%;(ro6;Oh{phoTOn}l_wx)fPc0`jRi;!V+dDLL}CXM71{yU0nLIkIi{xLK-rupQ6r z-}xNf&g;qC;TU+l_*m`oVx`BUFJPUv^AOF4-$*y)o@l-^5Et1AO{X8i)Cd!LtjxA; zZ&nGZ7jj{ir`G7}_;>)4;W{G^0=9yfK^e#7CH!|7aNSzeF^^HmZP_uo?qM0`sNf)6 zMwZ?Yt$Mtr8w*y0ZNAgC^Ekr9995Y*EN0Cm{F#c`278Dg@Cpq%!VuV0@UK^l8 zEM!E<+Nn$ECglt<+J~Z5?F5<~vIMysL`6kO^F*f}WXyAa%!(a*_VnddiQZwt9~FH+ zB_0&I@0Az!vJ_)--^h>7N27ssi-_Fmii=Yjpaf3jbM%b0I+EqD;f|l0=l&*kSoWMg zThjrNnHeUN?`82z1Esi*?n6CX2>7_tLJkaDL>UX{Wmk3BK2JrF)$RMo21gX>uu2Wd zkS`-ED{_w;&OAt0J}kidEhLbyn+%uurWYJOU+;e~ceBVH6dA{;E7?2{#9%eff18>r z0hC3gA^GR~3hP!s#2hH52w;|YLonGxl-#Bfc-s7ul69E534~edWX^IV9#IPEeCG6- z9P}CO^;UT>_OD#E${3c#GniWE!Fy3@cR+qho?ao15zoznwcOr;8@{A9yNX-XB|m$6 z29TD#C%70Y!(>x6m!pYrZA{9sOIy&6kmnWd7+6DwaMCaF&!9z! z5{oZQhu&lYRVD4)kFc(pATzNac8tJO_%LjtG2*fvU84Bk7TO=z?|P#k2C9Z-&jXj% z4lqp;utv3`AxZNO*=v)(6Dr{ParV(VjPN0tp z7)ktk4a>t{-ueh7aVur(L_vz;|Ai%4)b<_+z2Nj!%`-E#Bv#+$Jaruhu;LGith)V(8o)XUtv?!z& zQ1L*yq8U`0$3q~UfEplycIVKMBRwdOG|^Xupkz+Up=pA;5lZ?ad<>Lt@JOADOC13L zK@bFvaLPg8QTD1;>Jo>(5mwQ`)*+nJsdt%<$4g|XQOi#R}Al?7MYOH zDn_O!`pQP?rm)JvmXo|w5+Y}K8R~@n%u4=a?=Xx-Zg(_yx)|*&2SLfl=b5n=DizJD z%6xev4#xa0rqO;Hb4V}H0^GWa1VU`<&SG`c;-tcLjV)6WL=O|R1bHw* zcY*en&>jh{;6$rX$6*k46si>|Bqo#eX*v)CM-hPGw2PUv?`Esca7Mhss8k97CE;co zfPu1QHGWPGhGSf?3Z%^YOgua#D9Gg2Az=*-i9kkt->3en*BZ})^=!hWMQ%HlSqoL$ z#gQcnwXPFOMH9ad-WH&91FR^A(#cMyPXFQwf%vhD_XOG=M7pD4%H$fl(QxN$p|Ii4 zzZdt^t737pKzTkK{RlPzff9Zxwr<4o+e6E$R1|Y~f($TDqIl8s0hl=&S9e4&1es9` znZ{)bJDMGas-&G3O;S3PbJqT)z@kEP71j)*Cp_6ALah)rT5P6@ii)}O6(;lpWfWY0 z7|(zoRAu%Vz%@WFTCi^L$mN?x3=qQtweB^hbWlc78SpRe5}1gYqf^UToiAzoE>ZY zuIj2st5R9pib=DsbzrNSCC8GTm3Nz?GYTO&2|{iR7<$2AbJ)$O;pzF|G)6kB5ytVQ zlhA6aGbtT`+Yd!T^URpT_+ao-8fHd1W(3U(eKw|TCol@n$08mTc8&xb%b_rk{lybg zuXaS5Q7=dy*ccH{b|ok*ay_DCXwV!jIr%_gu*23GT-t}{s|oIfrYcFXel>*}au6}3 z3IN!O0%Mz{s^dA1w@?-amJg!QC!8@uEh+xjaZYaNiNt9Q?usMw6o3{7c&L(u zOe3WtU-8V77I=-}wjf{mGhrTu#X`SYzV~l|#t#YJLv(g{Tm(|RN;R0OR^T=|*^2i^EswuCl!1dAR>RfxzI#5g*tCQc_2k(^ zZcXHI{kjj#iDR3emMz@i42bY5QJlv#KO3CJP!RnWkVmKI3r$Xgj{3HDbg@G1}t4XqL@^ z@~G{HV^B~_ON&%e8kHa=7^4n!oX(;jfAsbJ%FzT>uMkoJM6z!=f3s)l4`?elc+C0VKAZ|tPtu-{ zdt1daA=S96hc5g##RdY-f1G}m@6JfyT5i5gQDZ3q=T{F=j z9uE@x5GY+=@nRqyQllb0_$xsXs)LwjP={2Wg59%z>q2~l*zUjhgMh|pwMg~kb0=t0 zZPo{<9|^)73N4#dWZ_tlS%AwD(B;)P2q1?--2ffq8yVT5AHAVAq_l8#S5h;K?P7;E znEF*`&S5Al&P{CG>nN|f5gH3y{pqPcR@KxdoBBZ9`IsCB%^uiBEtAG+7{+{-oci)6 z_d)d$=RuS-siJx!n>OvoK$VRSR9jI@dW5Ab!QuEMG&Ft;K;FS%2j1&_t8AY2Vw&ND z(}eijtlWWrn?9oP`4J<$h!OC^oL2q?%edo^>1n_d1Kz_*HQ1}^Fc5!C?gaq>)#TFc z`s|7BSaPx$fR@kLY6pge5wLm=lQW-#Y>s!>4Fe*|*4 z2dA+d2Ua1+ASE*B%Ws~3p&Jle?$ZVRyRzf1QSZ!SNGAVSc_pzSrU1KY#DX zHQR91S8{_ULi~SiH@+~sG>3WTNF_kwu>8NA1OPqvzdqXT(vKpHhBKxvzh3P8)xJv| zStwwc3@=^x|M`>h@`Z6UU;ljgN{wytMT~&C|F^#B|M+o!cXbc11cv&xdI=n8I-^}97BR8B?avt z**{*5>^}eg5n3dEZS1!MSQI0T!WsR19y!P)cEj#rAK+G?sae$jie!DXI(T!u7BXr@ zG-yi(Z_nd}%=u`+PmmiwTOYshlXIKSGNilkUh;09^?h>TLBu2b1)NG_feGt3Y={S^ z7Kz(ucn6)yVk7}G|G3u@yKtw=ge|f0WP$}N_F$*!6J*)2^b%lmaB^pIpp96>ao~+l z4O18Ml6MDyDiLQmbb%XB-7p3;t%ac_pekWH1C1~f(isWl;prtbMhIHB=ge8$9Yg+b zq5YgzaLT0F(ELBD!tCfI)W%gSR~k79Bd=EdGH;>k`LWNRy)n8_0ZN2X_%=zOA*h%B zl)NP1>!<}GB@i76on=E5H+NEPCt{jBhk10JV}tmmDNgNsOtH|B(e5-iMNAG`OftoH9WZRlpF+ zkjZT_LjVpK-WtYP+&{5y-@Qw-p(NALjHcYwun*>mp8$=}*-AntJmp?3c@#mj$BSt^}yc6d%K-!4GLKBTfk|B_BWb-Xb1lk@iIE=(H zZ)SC&N2viNv(?B0((hvo=g}aQa?ino2g#laV_e}QA)mFYSM!O8h`?GZ#?4%;Q0!4D ze^{Y3;9bDI4Zr8y9x!*jM| z6U6+u8G<0r1oLG=6p6+aa-QJEi!P`X+sp8x3)Fxn1{S0{S(>sveg?gyTg2i_X6 zW&EM=(c-aPIlo+{WO7BYd>H$HrD=2pM^=!Nz)SGV^N(zIa za1B4=G5P(&kL2p_>N1^j7O$zMs(mUCuUWKe%Of;+tA%ycAtkEVOWzbd3@^^$;2`Jm zjr2)^f!}#!%S5BsWQV~ae)fU{{j(}tc&-BIIiD#w4;b?8__6RV>4R{~>_^Mv!z8H! zSm>D}i(YT+aenvmk0{_c<1-Y%_ax|+3N)d^;6ZypzE$8?;+fKT<%eK@JUl#$FbsHi zVTOv%!(`HI2&Bi#NBgcO!)pFx0;QsKhcj8+-uUNpFdA!q^=1OGQ>W6x??~c{u><9BJ70rgdIl*3zDtF9w*r0q9toLxpT=n@A>r&RP4PtgU~7;mAqB#h$96gsAw81T0y}}mYJ`xe9gQS7)U{Mv)FW65R3sZ8AfsA%(;8v^wne8c!??`C*tJL1%&{@MZ%q- zLQ|qJbAnpEU)DDJ%H;EF<*Aj!Q4K8iPW#@IM2?Wh1Oqf-h^n&}?C^oV=Lpx&_Xb~} zD~Ab$eNe=_-UiFWV&waz%Aa&m-lABFj3&(C%zAV<^{DnU1lKR!k1?wX@@LS{2sRrg zPskBWo>&~s@Q=4olbbZvjj;k5Bw$5F=bZyIm`K)XL{iCzMYv#hCoNPNI`WmW9qb_| zi$O?kw6d}y8Ju))2JgHxS-MXl0>DP=V>}KlD*z66axq=zsUrJ1#^mzviBDU#VS|P6 z9Z%0{u-oyNRz7>`3|({5E8&x%38E?{;h>L#{|B|!he-r$lkvsH42}EzN-oHoxM#& zCwWWSDT)csZS^DW-=PFzDhFjQkkKYcCj8x$q4LT6`uQ_dj;7j|lTZm{BOP9pUJ~a8q z&sD&X7+KBG!k`;-<`^uqWXMllP~h(0IhtCe&G3#7w>vya!_mgOrY*l57BPeC=ujwXS5Oxs=(wt z|qg`FRj4@Zcwt? z<`Lv|sm30P)}$446&8_04vIGPQ3;J6hyf?D?bP52T#WT%1G6582U9gYXVUUSC@yse{I*@oytpL13-hcnHLj9z_#!mh;)kdsf z1|?!u+AU8{O^mw#4JW_nbzs3tghQ;_ljCUi1Lx!-{Mn5rctFTrNd`?#P4>R@po}@@ zDlDMs0~!mTJwc_6kM>a4ZBcv(m}NV7%p=c0D{4|sbCa)T=djlVA z5IPyw;T1`U@~yPNgyy>UmY!|BZCh(aeMpiagOPSByc5m9-=nHwFAI7XEL^yiNa9a@ zYGCA(t9f>ua;LE(vW`)PrdWjcM;X#`$;}~(jAOSL>?_CvB&Y&Y&RQ?%@0h3XXTsW} zd-@=omN5S0v;A;K(%3x4`R2Jm@sqwn%|W{7EU^5Zi6vnJWI@+&>*@+9YcpQoWp`(R zdoSE|jIdt;89o2=zUO;he9HfYw)4N$47T`}q3QOn{pz82uiNf`K=RQ7MF#KW`lWyYK18_`UJK|0)H+sAYKd&iB^%7eZvCJB7r*GARFc zgg^I-JI6K(dwC&8$X6fw?|o$}e}r<75Tqx}8LNS6tH6k^s-6RE186Fg6CduisXHl* z@AMuY;F@WZnv8~~T~dOm}55gv*+`L}5kzVTwPJs=HO zH0SE0vwrH2LusR6eIvr~c0Jh$Xg&tLHl2b>=dRfMQkQGYiG#Kv;OhY1vmAFIIbrJO zrg2&ZiWZ&OMg}g@K4IXZQ^m2RBQ8CJ6&%y)d#Sm0@AY6<-$z*|xt9i?(EyB)PgmjI zCd_5yR9_DsInbU4)y4Jo^|P1Cm|QN;J^_sp2itaoLsTwS!FdG4NSN`ES4>{seJR%- z?@yAjn8hF!#Mc1-^+#Fa+jyUEc4xl8vF0Fa%h905pMhDM>N1X0Q%muJ~Qv# z!o>|ZM*zMq$-ghM;B&+&*pGo&?~}b0d?d|25L1hed^4-N_IV_EZ9#$_Ws~t54kSAE z4Ze(Qe&vTzSPd1FiITz!;0n+1wr4rgNo#+P|4Cl;9VR(oyivr;Xd=6V?!~nUnV)jR z!GC&s(~0wNw#I*PD9nQsk+>&@u_XQ?9s5&hR(>AGMu( zT+jC&$8*M-+t}R7TpGWa6(iSjHxgZpQp+W`;{5zXNb%cJ+nF*kluH-28HJ?~DW#+- zI%!hrhng_A`blS|i&}M_@1Jwd9>?MF;5^Rx!$0WP_xJsNzVFZb{kna;YaZ7mq-+>u zB>(+?53W5|R;>qdYvj)VwSAuI`3c5-UZi}bOiGC znFJ#jb(R{N=<|kirx(|+_T%I}y&4yRT}IlsDVMJ0HY+iFj>RBaofe|`@|Zux>p%BT zx|u-lR` z)g(Ohf3>~%f~`%2#Y_e?VPa%rME+9Z+3=tAO!$Y~ZN2z6B_`~+gpM(eL{s%*TvF8l zgh3d-V_{&yC5B|zi4iMu?d_IZs4n;)HKOSfLlZm=aRKT#_wY!@-P<0yweiw`t+lC6 z1KU}(l3y-%UDQcI`{~VLI<$ghfhYzaF6BLzX%$UD;w~Bbagvc+tp4Gf>``)Gl}ncB zE$FcgGS143W?r(P1+6PGL!=9LI{{d9s#mxS+mso*9JyGcI z?UfRW&G#Ify=ujZ%Ho4vWyHBNG`n$;3o##**y^4m-ZS!Z{Z5Q#hcpVPc53oW@p5{M zFgt4Y8L_LRLC%Jx+1q+>>zit~(Q}R;-k{xC<&hg7nX)o>^B~Ef!GFANcT9?R`xh7K zzA3i!p9Lq<6zj+`!{nfJhH74MLX_Zh52l#CR}#;XCO?ZMZ5BlFlj#f8sB#DLHXqMf_5G(2CO z8rQOITbG+ifVOZ2WB^VmoYPo&u3o3alUm-A)O5*&Hmiy@xq$Qst|2$ADAV$IUVC0JY%sb&}nzBl5g#U?}$N;{IGD zJagb&?Zn55P*S~1JtHIvhfKCyr87b=8)nCXV`hrJLp3JmqB10M@7~WJy=C{H+4$s* zgr5UhVw!5L3JnYl6vrP0Cu(U=fm;ANh_VcEal74=^0Rg*n42KD5{lH_?Y2eY>Rj5@ zXxLYwy(ur!!B*i~Bl!GATzsW-;_6gs@VGCt6O}|CTA4V-iNnwRkfAb>XL&$nDh;e% zm2b_qWBBGJwJXj0s$Xc{_5t`1$i!c(x>u``1`<-e@639GK?}d4mW1Bvxw^!MPZwNC zV!9YLPwt&i^1v(Z8baz5*e=#o-k8g8D};iP2pzOdf)RP`@VS&6q173KXrjeSx_Qth zU_Vd^!ks$=Zro^9-F5P$NoC@6WzD}i)4y;n#M2sd@<5bB_gQCGbK9baEXZh%LTMU2 zv?IXB;N)&4H=#?)FYgN&Q~z)<7W)zVa>jpXtWZcuhya=BOQS%V#rG&%faPv~S9x1R z;v>_qq#L45(ePcLki6yQsT_BbHCplpLjpE2gO zwu$1*9LjaYIv=-0ieq)j?AIoiy7<&F!N{uAwYEW$)`*(Qe)XeTALwgCxUKKj8Ffx^hX z9$#yN z-zHc_WQ;bB)o3k8wa8gdK}8PKhcj*#gWvX1D9)MU+3MJTRv~8y4cKRQD>A4^J9P0y zgJ5D%e#7h*2J8{~t#9Ks4lB9gDXWbCBgfo`ok0Hd&K6bWVz`WZfbx>0yXuaW==@a; z@}mp`;lQoe9u_BFqA$9Vl$;#Kd+t`)uf(4Ehz(YS6X&AX3evpyjP)HOpFdu2d~zM+ z0wKX}n<1T~PCW$zj6DfO63#~AT!~Lar7&K%1xa>eM&)$nDVO2B{9KV_92YfrA?A6a zM?*l0Lb;j6=Q1;|&8bQ?>8~LQrsy=FWHc*Gl)+g{X>GcV$RkkUC;1>;{?n?@SZm_! zf&L`TJ$TtKm6a>FskX?RY#_TT)|AF@4=Y7CaT~AMSxb7Miz_g z8}8=A{GNCt_aK=-3HOtj;Dbw^I~eu;$Nn-|51(7ZHYAfHX6*H|&rf(Pzjl(;PKdoC z@Y*UCOgR*+oPY7G*%>GF+&=4z?P=FYq4d>#MRa(U+*#501-Wm9IuI=~8+$)cX3Q~> z!G~Ytl z6L8j*s%3*RIKId>PQMrLisQ7iLSjr2IvCy)FkjJ7T4BS66OA{&7Yfw21_Q@YS&G(+ zby}IPIN(!XU$4cEee}99!Y4amkD~hb1|WyPVfB+pZOJfys{gi;wE@Jv zcy^HnmtnV>@XJ4lSGy|;JJ0BO^pW#tt=;ChycubNsIZx z<5q^ZLl?izU7nrWBWVl5jfuiRvIoVqHY{CmA^E#%mW zECZd|vs&uH3Z`sNgOpV1p5Afja_$RUQ51cF75Wyfjn-ZkdM!TMWVc9k;8O8JBh!MIL2gCFX1fATJ!vS} zi_59dXcLwt_iVC+^*xyppg=}3K^n<|F`|T)fc@*o76q^mMKRj_fM38n#X)aa3b9o< z9OYOr5G0F)yX;>x>q%+;5>Ju@Q!S;G>jpf-MUrAewRT33k)ll~wU#%u1wmZ0Vxgq1 zy^QYhP^9yYsX2SgL($R%M#MX+Gcjx_@s+C7ews>?Qw_u~|5)a;>b{o=0+`DZYD~Ma zlm{WCPUsg2z{d|-gl3s^W)h)Qb6Ve215!E zMHfC!y4q&VFZ>B>z(IKPPb-N7IE02NJl!qmddR`M;qe>Z03gIoP1cm0k!9g=)31?~cbwg$GJhSSXSVw#yvL5(^gtfsQd9^sl8G!; zGR3LK$Eq(w&S5-%K&ELP7{{J!{r3zYkvetlx|Az*qpe)L>du7)NT71r*bE-#-iEXnuS!sCmcGBAm`5Y}=KcXe2nK>>D--!Amt8p3slgJXtLe#6kI zgtBwaDn^nMMt^HajsK;h!W}!WJN74f?nQ^!FeNNz9-L1XVuRhze$=;A!`#}sx-f>; zFd5rSKRudmC6-ElkC?Ye(Aaa*`SjXjry9pD(JW1qE(+Cq>7Bps%( zNz}f;&#ReC9enOVGW;?y%m1CtdVZ#d*vGx#B64>?X!dnE&P90coDN-T(jq literal 0 HcmV?d00001 diff --git a/src/geophires_x/Producible Heat-Unit Area (fluid).png b/src/geophires_x/Producible Heat-Unit Area (fluid).png new file mode 100644 index 0000000000000000000000000000000000000000..92c1b48d2f4f17004af1ed9a65ffade0a417a3f4 GIT binary patch literal 31275 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 Ju6|<|{Rh!eOGp3! literal 0 HcmV?d00001 diff --git a/src/geophires_x/Reservoir.py b/src/geophires_x/Reservoir.py index b0b5e31b..391aef73 100644 --- a/src/geophires_x/Reservoir.py +++ b/src/geophires_x/Reservoir.py @@ -127,7 +127,7 @@ def __init__(self, model: Model): CurrentUnits=TemperatureUnit.CELSIUS, Required=True, ErrMessage="assume default maximum temperature (400 deg.C)", - ToolTipText="Maximum allowable reservoir temperature (e.g. due to drill bit or logging tools constraints). \ + ToolTipText="Maximum allowable reservoir temperature (reservoir_enthalpy.g. due to drill bit or logging tools constraints). \ GEOPHIRES will cap the drilling depth to stay below this maximum temperature." ) @@ -809,8 +809,8 @@ def Calculate(self, model: Model) -> None: self.averagegradient.value = (self.Trock.value - self.Tsurf.value) / self.depth.value # specify time-stepping vectors - self.timevector.value = np.linspace(0, model.surfaceplant.plantlifetime.value, - model.economics.timestepsperyear.value * model.surfaceplant.plantlifetime.value+1) + self.timevector.value = np.linspace(0, model.surfaceplant.plant_lifetime.value, + model.economics.timestepsperyear.value * model.surfaceplant.plant_lifetime.value + 1) self.Tresoutput.value = np.zeros(len(self.timevector.value)) if self.resoption.value != ReservoirModel.SUTRA: diff --git a/src/geophires_x/SUTRAEconomics.py b/src/geophires_x/SUTRAEconomics.py index 31d9193f..876b8b8e 100644 --- a/src/geophires_x/SUTRAEconomics.py +++ b/src/geophires_x/SUTRAEconomics.py @@ -1,12 +1,10 @@ -import math import sys import os import numpy as np -import numpy_financial as npf import geophires_x.Model as Model -from .OptionList import Configuration, WellDrillingCostCorrelation, EconomicModel, EndUseOptions, PowerPlantType -from .Parameter import intParameter, floatParameter, OutputParameter, ReadParameter, boolParameter -from .Units import * +from geophires_x.OptionList import WellDrillingCostCorrelation, EconomicModel +from geophires_x.Parameter import intParameter, floatParameter, OutputParameter, ReadParameter, boolParameter +from geophires_x.Units import * class SUTRAEconomics: @@ -402,7 +400,7 @@ def Calculate(self, model: Model) -> None: self.Cwell.value = self.C1well * (model.wellbores.nprod.value + model.wellbores.ninj.value) # Boiler - self.peakingboilercost.value = 65*model.surfaceplant.maxpeakingboilerdemand.value/self.peakingboilerefficiency.value/1000 #add 65$/KW for peaking boiler + self.peakingboilercost.value = 65 * model.surfaceplant.max_peaking_boiler_demand.value / self.peakingboilerefficiency.value / 1000 #add 65$/KW for peaking boiler # Circulation Pump pumphp = np.max(model.wellbores.PumpingPower.value)*1.341 @@ -418,7 +416,7 @@ def Calculate(self, model: Model) -> None: # OPEX # Pumping - self.annualpumpingcosts.value = model.surfaceplant.PumpingkWh.value*model.surfaceplant.elecprice.value/1e3 + self.annualpumpingcosts.value = model.surfaceplant.PumpingkWh.value * model.surfaceplant.electricity_cost_to_buy.value / 1e3 # Natural Gas self.annualngcost.value = model.surfaceplant.AnnualAuxiliaryHeatProduced.value*self.ngprice.value/self.peakingboilerefficiency.value*1e3 @@ -429,7 +427,7 @@ def Calculate(self, model: Model) -> None: self.Coam.value = self.annualpumpingcosts.value + self.annualngcost.value # LCOH - discountvector = 1. / np.power(1 + self.discountrate.value,np.linspace(0, model.surfaceplant.plantlifetime.value - 1,model.surfaceplant.plantlifetime.value)) + discountvector = 1. / np.power(1 + self.discountrate.value, np.linspace(0, model.surfaceplant.plant_lifetime.value - 1, model.surfaceplant.plant_lifetime.value)) self.LCOH.value = ((1 + self.inflrateconstruction.value) * self.CCap.value + np.sum(self.Coam.value * discountvector)) / np.sum(model.surfaceplant.AnnualTotalHeatProduced.value*1E6 * discountvector) * 1E8 # cents/kWh model.logger.info("complete "+ str(__class__) + ": " + sys._getframe().f_code.co_name) diff --git a/src/geophires_x/SUTRAOutputs.py b/src/geophires_x/SUTRAOutputs.py index 3689a445..e1e05e85 100644 --- a/src/geophires_x/SUTRAOutputs.py +++ b/src/geophires_x/SUTRAOutputs.py @@ -84,7 +84,7 @@ def PrintOutputs(self, model: Model): # Deal with converting Units back to PreferredUnits, if required. # before we write the outputs, we go thru all the parameters for all of the objects and set the values back # to the units that the user entered the data in - # We do this because the value may be displayed in the output, and we want the user to recginze their value, + # reservoir_producible_electricity do this because the value may be displayed in the output, and we want the user to recginze their value, # not some converted value #for obj in [model.reserv, model.wellbores, model.surfaceplant, model.economics]: # for key in obj.ParameterDict: @@ -93,7 +93,7 @@ def PrintOutputs(self, model: Model): # now we need to loop through all thw output parameters to update their units to # whatever units the user has specified. - # i.e., they may have specified that all LENGTH results must be in feet, so we need to convert those + # i.reservoir_enthalpy., they may have specified that all LENGTH results must be in feet, so we need to convert those # from whatever LENGTH unit they are to feet. # same for all the other classes of units (TEMPERATURE, DENSITY, etc). @@ -126,7 +126,7 @@ def PrintOutputs(self, model: Model): f.write(NL) f.write(' ***SUMMARY OF RESULTS***\n') f.write(NL) - f.write(" End-Use Option: " + str(model.surfaceplant.enduseoption.value.value) + NL) + f.write(" End-Use Option: " + str(model.surfaceplant.enduse_option.value.value) + NL) f.write(" Reservoir Model = " + str(model.reserv.resoption.value.value) + " Model\n") f.write(f" Direct-Use heat breakeven price: {model.economics.LCOH.value:10.2f} " + model.economics.LCOH.CurrentUnits.value + NL) @@ -148,7 +148,7 @@ def PrintOutputs(self, model: Model): elif model.economics.econmodel.value == EconomicModel.BICYCLE: f.write(" Economic Model = " + model.economics.econmodel.value.value + NL) f.write(f" Accrued financing during construction: {model.economics.inflrateconstruction.value*100:10.2f} " + model.economics.inflrateconstruction.PreferredUnits.value + NL) - f.write(f" Project lifetime: {model.surfaceplant.plantlifetime.value:10.0f} " + model.surfaceplant.plantlifetime.CurrentUnits.value + NL) + f.write(f" Project lifetime: {model.surfaceplant.plant_lifetime.value:10.0f} " + model.surfaceplant.plant_lifetime.CurrentUnits.value + NL) f.write(NL) f.write(' ***ENGINEERING PARAMETERS***\n') @@ -156,7 +156,7 @@ def PrintOutputs(self, model: Model): f.write(f" Number of Production Wells: {model.wellbores.nprod.value:10.0f}" + NL) f.write(f" Number of Injection Wells: {model.wellbores.ninj.value:10.0f}" + NL) f.write(f" Well Depth: {model.reserv.depth.value:10.1f} " + model.reserv.depth.CurrentUnits.value + NL) - f.write(f" Pump efficiency: {model.surfaceplant.pumpeff.value*100:10.1f} " + model.surfaceplant.pumpeff.PreferredUnits.value + NL) + f.write(f" Pump efficiency: {model.surfaceplant.pump_efficiency.value * 100:10.1f} " + model.surfaceplant.pump_efficiency.PreferredUnits.value + NL) f.write(f" Lifetime Average Well Flow Rate: {np.average(abs(model.wellbores.ProductionWellFlowRates.value)):10.1f} " + model.wellbores.ProductionWellFlowRates.CurrentUnits.value + NL) f.write(f" Injection well casing ID: {model.wellbores.injwelldiam.value:10.3f} " + model.wellbores.injwelldiam.CurrentUnits.value + NL) diff --git a/src/geophires_x/SUTRAReservoir.py b/src/geophires_x/SUTRAReservoir.py index eaf3ca74..253b764d 100644 --- a/src/geophires_x/SUTRAReservoir.py +++ b/src/geophires_x/SUTRAReservoir.py @@ -215,7 +215,7 @@ def Calculate(self, model: Model): plt.plot(year, abs(self.AnnualHeatSupplied.value), label='Annual Heat Supplied') plt.xlabel('Year') plt.ylabel('Annual Heat Balance [GWh/year]') - #plt.ylim([0, max(model.surfaceplant.dailyheatingdemand.value) * 1.05]) + #plt.ylim([0, max(model.surfaceplant.daily_heating_demand.value) * 1.05]) plt.legend() plt.title('SUTRA Heat Balance') plt.show(block=False) @@ -225,7 +225,7 @@ def Calculate(self, model: Model): plt.plot(self.TimeProfile.value[0:-1:2], self.SimulatedHeat.value[0:-1:2], label='Simulated Heat') plt.xlabel('Hour') plt.ylabel('Heat Exchange [kWh]') - #plt.ylim([0, max(model.surfaceplant.dailyheatingdemand.value) * 1.05]) + #plt.ylim([0, max(model.surfaceplant.daily_heating_demand.value) * 1.05]) plt.legend() plt.title('SUTRA Target and Simulated Heat') plt.show(block=False) @@ -235,7 +235,7 @@ def Calculate(self, model: Model): plt.plot(self.TimeProfile.value[0:-1:2], self.BalanceWellFlowRate.value[0:-1:2], label='Balance Well Flow Rate') plt.xlabel('Hour') plt.ylabel('Flow Rate [kg/s]') - #plt.ylim([0, max(model.surfaceplant.dailyheatingdemand.value) * 1.05]) + #plt.ylim([0, max(model.surfaceplant.daily_heating_demand.value) * 1.05]) plt.legend() plt.title('SUTRA Well Flow Rates') plt.show(block=False) @@ -245,7 +245,7 @@ def Calculate(self, model: Model): plt.plot(self.TimeProfile.value[0:-1:2], self.BalanceWellTemperature.value[0:-1:2], label='Balance Well Temperature') plt.xlabel('Hour') plt.ylabel('Temperature [C]') - # plt.ylim([0, max(model.surfaceplant.dailyheatingdemand.value) * 1.05]) + # plt.ylim([0, max(model.surfaceplant.daily_heating_demand.value) * 1.05]) plt.legend() plt.title('SUTRA Well Temperatures') plt.show(block=False) diff --git a/src/geophires_x/SUTRAWellBores.py b/src/geophires_x/SUTRAWellBores.py index ae7436ce..d7663825 100644 --- a/src/geophires_x/SUTRAWellBores.py +++ b/src/geophires_x/SUTRAWellBores.py @@ -332,6 +332,6 @@ def Calculate(self, model: Model) -> None: self.DPOverall.value = self.DPProdWell.value + self.DPInjWell.value + DP_buoyancy + DP_reservoir # calculate pumping power [kWe] (approximate) - self.PumpingPower.value = self.DPOverall.value * abs(prodwellflowrates) / (0.5*rhowaterinj+0.5*rhowaterprod) / model.surfaceplant.pumpeff.value + self.PumpingPower.value = self.DPOverall.value * abs(prodwellflowrates) / (0.5*rhowaterinj+0.5*rhowaterprod) / model.surfaceplant.pump_efficiency.value model.logger.info("complete " + str(__class__) + ": " + sys._getframe().f_code.co_name) diff --git a/src/geophires_x/SurfacePlant.py b/src/geophires_x/SurfacePlant.py index 018fa40a..cdea06d0 100644 --- a/src/geophires_x/SurfacePlant.py +++ b/src/geophires_x/SurfacePlant.py @@ -1,13 +1,197 @@ import sys import os import numpy as np -from .OptionList import EndUseOptions, PowerPlantType +from .OptionList import EndUseOptions, PlantType from .Parameter import floatParameter, intParameter, strParameter, OutputParameter, ReadParameter from .Units import * import geophires_x.Model as Model import pandas as pd class SurfacePlant: + def remaining_reservoir_heat_content(self, InitialReservoirHeatContent: np.ndarray, HeatkWhExtracted: np.ndarray) -> np.ndarray: + """ + Calculate reservoir heat content + :param InitialReservoirHeatContent: Initial reservoir heat content [PJ] + :param HeatkWhExtracted: Heat extracted from reservoir [kWh] + :return: Remaining reservoir heat content [PJ] + + """ + # calculate reservoir heat content + return InitialReservoirHeatContent - np.add.accumulate(HeatkWhExtracted) * 3600 * 1E3 / 1E15 + + def power_plant_entering_temperature(self, enduse_option: EndUseOptions, timevector: np.ndarray, + T_chp_bottom: float, ProducedTemperature: np.ndarray) -> np.ndarray: + """ + Calculate power plant entering temperature based on end-use option and power plant type (see docs for details) + :param enduse_option: end-use option + :param timevector: time vector + :param T_chp_bottom: power plant entering temperature used in CHP bottoming cycle + :param ProducedTemperature: produced temperature + :return: power plant entering temperature + + """ + if enduse_option in [EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICITY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT]: + TenteringPP = np.repeat(T_chp_bottom, len(timevector)) + else: + TenteringPP = ProducedTemperature + return TenteringPP + + def availability_water(self, T0, T1, T2): + """ + Availability water: copied from GEOPHIRES v1.0 Fortran Code + :param T0: T0 + :param T1: T1 + :param T2: T2 + :return: Availability water [MJ/kg] + """ + A = 4.041650 + B = -1.204E-2 + C = 1.60500E-5 + + T0 = T0 + 273.15 + T1 = T1 + 273.15 + T2 = T2 + 273.15 + availability = ((A - B * T0) * (T1 - T2) + (B - C * T0) / 2.0 * (T1 ** 2 - T2 ** 2) + C / 3.0 * ( + T1 ** 3 - T2 ** 3) - A * T0 * np.log(T1 / T2)) * 2.2046 / 947.83 # MJ/kg + + return availability + + def reinjection_temperature(self, model: Model, ambient_temperature: float, TenteringPP: np.ndarray, Tinj: float, + C01: float, C11: float, C21: float, D01: float, D11: float, D21: float, + C02: float, C12: float, C22: float, D02: float, D12: float, D22: float) -> tuple: + """ + Calculate reinjection temperature based on ambient temperature, power plant entering temperature and injection temperature. + (see docs for details) + :param model: The container class of the application, giving access to everything else, including the logger + :param ambient_temperature: ambient temperature + :param TenteringPP: power plant entering temperature + :param Tinj: injection temperature + :param C01: C01 + :param C11: C11 + :param C21: C21 + :param D02: D02 + :param D12: D12 + :param D22: D22 + :return: injection temperature, reinjection temperature, and etau + """ + Tfraction = (ambient_temperature - 5.) / 10. + etaull = C21*TenteringPP**2 + C11*TenteringPP + C01 + etauul = D21*TenteringPP**2 + D11*TenteringPP + D01 + etau = (1.-Tfraction)*etaull + Tfraction*etauul + + reinjtll = C22*TenteringPP**2 + C12*TenteringPP + C02 + reinjtul = D22*TenteringPP**2 + D12*TenteringPP + D02 + ReinjTemp = (1.-Tfraction)*reinjtll + Tfraction*reinjtul + + # check if reinjectemp (model calculated) >= Tinj (user provided) + if np.min(ReinjTemp) < Tinj: + Tinj = np.min(ReinjTemp) + print("Warning: injection temperature lowered") + model.logger.warning("injection temperature lowered") + return Tinj, ReinjTemp, etau + + def electricity_heat_production(self, enduse_option: EndUseOptions, availability: np.ndarray, etau: np.ndarray, nprod: int, + prodwellflowrate: float, cpwater: float, ProducedTemperature: np.ndarray, Tinj: float, + ReinjTemp: float, T_chp_bottom: float, enduse_efficiency_factor: float, chp_fraction: float) -> tuple: + """ + Calculate electricity/heat production based on end-use option (see docs for details) + :param enduse_option: end-use option + :param availability: geofluid availability + :param etau: etau + :param nprod: number of production wells + :param prodwellflowrate: production well flow rate + :param cpwater: specific heat capacity of water + :param ProducedTemperature: produced temperature + :param Tinj: injection temperature + :param ReinjTemp: reinjection temperature + :param T_chp_bottom: power plant entering temperature used in CHP bottoming cycle + :param enduse_efficiency_factor: end-use efficiency factor + :param chp_fraction: fraction of produced geofluid flow rate going to direct-use heat application in CHP parallel cycle + :return: electricity produced, heat extracted, heat produced, and heat extracted towards electricity + """ + HeatProduced = np.empty(0) + HeatExtractedTowardsElectricity = np.empty(0) + # calculate electricity/heat - first, calculate the total amount of heat extracted from geofluid [MWth] (same for all) + HeatExtracted = nprod * prodwellflowrate * cpwater * (ProducedTemperature - Tinj) / 1E6 + + # next do the electricity produced - the same for all, except enduse=5, where it is recalculated + ElectricityProduced = availability * etau * nprod * prodwellflowrate + + if enduse_option == EndUseOptions.ELECTRICITY: # pure electricity + HeatExtractedTowardsElectricity = HeatExtracted + # enduse_option = 3: cogen topping cycle + elif enduse_option in [EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICITY, EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT]: + # Useful heat for direct-use application [MWth] + HeatProduced = enduse_efficiency_factor * nprod * prodwellflowrate * cpwater * (ReinjTemp - Tinj) / 1E6 + HeatExtractedTowardsElectricity = nprod * prodwellflowrate * cpwater * (ProducedTemperature - ReinjTemp) / 1E6 + # enduse_option = 4: cogen bottoming cycle + elif enduse_option in [EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICITY]: + # Useful heat for direct-use application [MWth] + HeatProduced = enduse_efficiency_factor * nprod * prodwellflowrate * cpwater * (ProducedTemperature - T_chp_bottom) / 1E6 + HeatExtractedTowardsElectricity = nprod * prodwellflowrate * cpwater * (T_chp_bottom - Tinj) / 1E6 + # enduse_option = 5: cogen split of mass flow rate + elif enduse_option in [EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICITY, EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT]: + # electricity part [MWe] + ElectricityProduced = availability * etau * nprod * prodwellflowrate * (1. - chp_fraction) + # useful heat part for direct-use application [MWth] + HeatProduced = enduse_efficiency_factor * chp_fraction * nprod * prodwellflowrate * cpwater * (ProducedTemperature - Tinj) / 1E6 + HeatExtractedTowardsElectricity = (1. - chp_fraction) * nprod * prodwellflowrate * cpwater * (ProducedTemperature - Tinj) / 1E6 + + return ElectricityProduced, HeatExtracted, HeatProduced, HeatExtractedTowardsElectricity + + def annual_electricity_pumping_power(self, plant_lifetime: int, enduse_option: EndUseOptions, HeatExtracted: np.ndarray, + timestepsperyear: np.ndarray, utilization_factor: float, PumpingPower: np.ndarray, + ElectricityProduced: np.ndarray, NetElectricityProduced: np.ndarray, HeatProduced: np.ndarray) -> tuple: + """ + Calculate annual electricity/heat production + :param plant_lifetime: plant lifetime + :param enduse_option: end-use option + :param HeatExtracted: heat extracted + :param timestepsperyear: timesteps per year + :param utilization_factor: utilization factor + :param PumpingPower: pumping power + :param ElectricityProduced: electricity produced + :param NetElectricityProduced: net electricity produced + :param HeatProduced: heat produced + + """ + # Calculate annual electricity/heat production + # all end-use options have "heat extracted from reservoir" and pumping kWs + HeatkWhExtracted = np.zeros(plant_lifetime) + PumpingkWh = np.zeros(plant_lifetime) + TotalkWhProduced = np.empty(0) + NetkWhProduced = np.empty(0) + HeatkWhProduced = np.empty(0) + + for i in range(0, plant_lifetime): + HeatkWhExtracted[i] = np.trapz(HeatExtracted[(0 + i * timestepsperyear):((i + 1) * timestepsperyear) + 1], + dx = 1. / timestepsperyear * 365. * 24.) * 1000. * utilization_factor + PumpingkWh[i] = np.trapz(PumpingPower[(0 + i * timestepsperyear):((i + 1) * timestepsperyear) + 1], + dx = 1. / timestepsperyear * 365. * 24.) * 1000. * utilization_factor + + if enduse_option in [EndUseOptions.ELECTRICITY, EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, + EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICITY, + EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICITY, + EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, + EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, + EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICITY]: + # all these end-use options have an electricity generation component + TotalkWhProduced = np.zeros(plant_lifetime) + NetkWhProduced = np.zeros(plant_lifetime) + for i in range(0, plant_lifetime): + TotalkWhProduced[i] = np.trapz(ElectricityProduced[(0 + i * timestepsperyear):((i + 1) * timestepsperyear) + 1], + dx=1. / timestepsperyear * 365. * 24.) * 1000. * utilization_factor + NetkWhProduced[i] = np.trapz(NetElectricityProduced[(0 + i * timestepsperyear):((i + 1) * timestepsperyear) + 1], + dx=1. / timestepsperyear * 365. * 24.) * 1000. * utilization_factor + if enduse_option != EndUseOptions.ELECTRICITY: + # all those end-use options have a direct-use component + HeatkWhProduced = np.zeros(plant_lifetime) + for i in range(0, plant_lifetime): + HeatkWhProduced[i] = np.trapz(HeatProduced[(0 + i * timestepsperyear):((i + 1) * timestepsperyear) + 1], + dx=1. / timestepsperyear * 365. * 24.) * 1000. * utilization_factor + + return HeatkWhExtracted, PumpingkWh, TotalkWhProduced, NetkWhProduced, HeatkWhProduced + def __init__(self, model: Model): """ The __init__ function is called automatically when a class is instantiated. @@ -18,8 +202,7 @@ def __init__(self, model: Model): :type model: :class:`~geophires_x.Model.Model` :return: None """ - - model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) + model.logger.info(f"Init {self.__class__.__name__}: {__name__}") self.Tinj = 0.0 # Set up all the Parameters that will be predefined by this class using the different types of parameter classes. @@ -36,24 +219,25 @@ def __init__(self, model: Model): self.ParameterDict = {} self.OutputParameterDict = {} - self.enduseoption = self.ParameterDict[self.enduseoption.Name] = intParameter( + self.enduse_option = self.ParameterDict[self.enduse_option.Name] = intParameter( "End-Use Option", value=EndUseOptions.ELECTRICITY, - AllowableRange=[1, 2, 31, 32, 41, 42, 51, 52, 6, 7, 8, 9], + AllowableRange=[1, 2, 31, 32, 41, 42, 51, 52], UnitType=Units.NONE, ErrMessage="assume default end-use option (1: electricity only)", ToolTipText="Select the end-use application of the geofluid heat (see docs for details)" ) - self.pptype = self.ParameterDict[self.pptype.Name] = intParameter( + self.plant_type = self.ParameterDict[self.plant_type.Name] = intParameter( "Power Plant Type", - value=PowerPlantType.SUB_CRITICAL_ORC, - AllowableRange=[1, 2, 3, 4], + value=PlantType.SUB_CRITICAL_ORC, + AllowableRange=[1, 2, 3, 4, 5, 6, 7, 8, 9], UnitType=Units.NONE, ErrMessage="assume default power plant type (1: subcritical ORC)", - ToolTipText="Specify the type of power plant in case of electricity generation. 1: Subcritical ORC," + - " 2: Supercritical ORC, 3: Single-flash, 4: Double-flash" + ToolTipText="Specify the type of physical plant. 1: Subcritical ORC," + + " 2: Supercritical ORC, 3: Single-flash, 4: Double-flash, 5: Absorption Chiller, 6: Heat Pump" + # 6 + " 7: District Heating, 8: Reservoir Thermal Energy Storage" ) - self.pumpeff = self.ParameterDict[self.pumpeff.Name] = floatParameter( + self.pump_efficiency = self.ParameterDict[self.pump_efficiency.Name] = floatParameter( "Circulation Pump Efficiency", value=0.75, DefaultValue=0.75, @@ -66,7 +250,7 @@ def __init__(self, model: Model): ErrMessage="assume default circulation pump efficiency (0.75)", ToolTipText="Specify the overall efficiency of the injection and production well pumps" ) - self.utilfactor = self.ParameterDict[self.utilfactor.Name] = floatParameter( + self.utilization_factor = self.ParameterDict[self.utilization_factor.Name] = floatParameter( "Utilization Factor", value=0.9, DefaultValue=0.9, @@ -79,7 +263,7 @@ def __init__(self, model: Model): ErrMessage="assume default utilization factor (0.9)", ToolTipText="Ratio of the time the plant is running in normal production in a 1-year time period." ) - self.enduseefficiencyfactor = self.ParameterDict[self.enduseefficiencyfactor.Name] = floatParameter( + self.enduse_efficiency_factor = self.ParameterDict[self.enduse_efficiency_factor.Name] = floatParameter( "End-Use Efficiency Factor", value=0.9, DefaultValue=0.9, @@ -91,7 +275,7 @@ def __init__(self, model: Model): ErrMessage="assume default end-use efficiency factor (0.9)", ToolTipText="Constant thermal efficiency of the direct-use application" ) - self.chpfraction = self.ParameterDict[self.chpfraction.Name] = floatParameter( + self.chp_fraction = self.ParameterDict[self.chp_fraction.Name] = floatParameter( "CHP Fraction", value=0.5, DefaultValue=0.5, @@ -104,7 +288,7 @@ def __init__(self, model: Model): ToolTipText="Fraction of produced geofluid flow rate going to direct-use heat application in" + " CHP parallel cycle" ) - self.Tchpbottom = self.ParameterDict[self.Tchpbottom.Name] = floatParameter( + self.T_chp_bottom = self.ParameterDict[self.T_chp_bottom.Name] = floatParameter( "CHP Bottoming Entering Temperature", value=150.0, DefaultValue=150.0, @@ -116,7 +300,7 @@ def __init__(self, model: Model): ErrMessage="assume default CHP bottom temperature (150 deg.C)", ToolTipText="Power plant entering geofluid temperature used in CHP bottoming cycle" ) - self.Tenv = self.ParameterDict[self.Tenv.Name] = floatParameter( + self.ambient_temperature = self.ParameterDict[self.ambient_temperature.Name] = floatParameter( "Ambient Temperature", value=15.0, DefaultValue=15.0, @@ -128,7 +312,7 @@ def __init__(self, model: Model): ErrMessage="assume default ambient temperature (15 deg.C)", ToolTipText="Ambient (or dead-state) temperature used for calculating power plant utilization efficiency" ) - self.plantlifetime = self.ParameterDict[self.plantlifetime.Name] = intParameter( + self.plant_lifetime = self.ParameterDict[self.plant_lifetime.Name] = intParameter( "Plant Lifetime", value=30, DefaultValue=30, @@ -140,7 +324,7 @@ def __init__(self, model: Model): ErrMessage="assume default plant lifetime (30 years)", ToolTipText="System lifetime" ) - self.pipinglength = self.ParameterDict[self.pipinglength.Name] = floatParameter( + self.piping_length = self.ParameterDict[self.piping_length.Name] = floatParameter( "Surface Piping Length", value=0.0, DefaultValue=0.0, @@ -151,7 +335,7 @@ def __init__(self, model: Model): CurrentUnits=LengthUnit.KILOMETERS, ErrMessage="assume default piping length (5km)" ) - self.Pplantoutlet = self.ParameterDict[self.Pplantoutlet.Name] = floatParameter( + self.plant_outlet_pressure = self.ParameterDict[self.plant_outlet_pressure.Name] = floatParameter( "Plant Outlet Pressure", value=100.0, DefaultValue=100.0, @@ -163,7 +347,7 @@ def __init__(self, model: Model): ErrMessage="assume default plant outlet pressure (100 kPa)", ToolTipText="Constant plant outlet pressure equal to injection well pump(s) suction pressure" ) - self.elecprice = self.ParameterDict[self.elecprice.Name] = floatParameter( + self.electricity_cost_to_buy = self.ParameterDict[self.electricity_cost_to_buy.Name] = floatParameter( "Electricity Rate", value=0.07, DefaultValue=0.07, @@ -176,7 +360,7 @@ def __init__(self, model: Model): ToolTipText="Price of electricity to calculate pumping costs in direct-use heat only mode or revenue" + " from electricity sales in CHP mode." ) - self.heatprice = self.ParameterDict[self.heatprice.Name] = floatParameter( + self.heat_price = self.ParameterDict[self.heat_price.Name] = floatParameter( "Heat Rate", value=0.02, DefaultValue=0.02, @@ -188,7 +372,7 @@ def __init__(self, model: Model): ErrMessage="assume default heat rate ($0.02/kWh)", ToolTipText="Price of heat to calculate revenue from heat sales in CHP mode." ) - self.ConstructionYears = self.ParameterDict[self.ConstructionYears.Name] = intParameter( + self.construction_years = self.ParameterDict[self.construction_years.Name] = intParameter( "Construction Years", value=1, DefaultValue=1, @@ -204,106 +388,6 @@ def __init__(self, model: Model): self.MyClass = sclass.replace("\'>", "") self.MyPath = os.path.abspath(__file__) - # absorption chiller - self.absorptionchillercop = self.ParameterDict[self.absorptionchillercop.Name] = floatParameter( - "Absorption Chiller COP", - value=0.7, - Min=0.1, - Max=1.5, - UnitType=Units.PERCENT, - PreferredUnits=PercentUnit.TENTH, CurrentUnits=PercentUnit.TENTH, - ErrMessage="assume default absorption chiller COP (0.7)", - ToolTipText="Specify the coefficient of performance (COP) of the absorption chiller" - ) - - #heat pump - self.heatpumpcop = self.ParameterDict[self.heatpumpcop.Name] = floatParameter( - "Heat Pump COP", - value = 5, - Min=1, - Max = 10, - UnitType = Units.PERCENT, - PreferredUnits = PercentUnit.TENTH, - CurrentUnits = PercentUnit.TENTH, - ErrMessage="assume default heat pump COP (5)", - ToolTipText="Specify the coefficient of performance (COP) of the heat pump" - ) - - # district heating - self.dhdemandoption = self.ParameterDict[self.dhdemandoption.Name] = intParameter( - "District Heating Demand Option", - value=1, - AllowableRange=[1, 2], - UnitType=Units.NONE, - ErrMessage="assume default district heating demand option (1: known heat demand profile)", - ToolTipText="Select the method to provide the district heating demand to GEOPHIRES" - ) - self.dhdemandfilename = self.ParameterDict[self.dhdemandfilename.Name] = strParameter( - "District Heating Demand File Name", - value='HeatDemand.csv', - UnitType=Units.NONE, - ErrMessage="assume default district heating demand filename (HeatDemand.csv)", - ToolTipText="Provide district heating demand in csv file in MW or MWh per day (if district heating demand option is set to 1)" - ) - self.dhdemandtimeresolution = self.ParameterDict[self.dhdemandtimeresolution.Name] = intParameter( - "District Heating Demand Data Time Resolution", - value=1, - AllowableRange=[1, 2], - UnitType=Units.NONE, - ErrMessage="assume default district heating data time resolution (1: hourly data)", - ToolTipText="Provide time interval for thermal demand data: 1 = hourly (data provided as MW = MWh' 2 = daily (data provided as MWh/day) (if district heating demand option is set to 1)" - ) - self.dhdemanddatacolumnnumber = self.ParameterDict[self.dhdemanddatacolumnnumber.Name] = intParameter( - "District Heating Demand Data Column Number", - value=2, - AllowableRange=list(range(1, 101, 1)), - UnitType=Units.NONE, - ErrMessage="assume default district heating demand data column number (2)", - ToolTipText="Select the column number of the hourly or daily data in the district heating demand csv file (if district heating demand option is set to 1)" - ) - self.dhtemperaturefilename = self.ParameterDict[self.dhtemperaturefilename.Name] = strParameter( - "Temperature File Name", - value='Temperature.csv', - UnitType=Units.NONE, - ErrMessage="assume default temperature filename (Temperature.csv)", - ToolTipText="Provide filename of tempeature file with hourly temperature to calculate district heating demand (if district heating demand option is set to 2)" - ) - self.dhtemperaturedatacolumnnumber = self.ParameterDict[self.dhtemperaturedatacolumnnumber.Name] = intParameter( - "Temperature Data Column Number", - value=2, - AllowableRange=list(range(1, 101, 1)), - UnitType=Units.NONE, - ErrMessage="assume default temperature data column number (2)", - ToolTipText="Select the column number of the hourly temperature data in the temperature csv file (if district heating demand option is set to 2)" - ) - self.dhnumberofhousingunits = self.ParameterDict[self.dhnumberofhousingunits.Name] = floatParameter( - "Number of Housing Units", - value=100, - Min=0, - Max=1000000, - UnitType=Units.NONE, - ErrMessage="assume default number of housing units (100)", - ToolTipText="Specify the number of housing units to calculate district heating demand (if district heating demand option is set to 2)" - ) - self.dhconstantanchordemand = self.ParameterDict[self.dhconstantanchordemand.Name] = floatParameter( - "Constant Anchor Demand", - value=0, - Min=0, - Max=100, - UnitType=Units.POWER, - PreferredUnits=PowerUnit.MW, - CurrentUnits=PowerUnit.MW, - ErrMessage="assume default constant anchor demand (10 MWth)", - ToolTipText="Specify the constant anchor demand to calculate the district heating demand (if district heating demand option is set to 2)" - ) - self.dhuscensusdivision = self.ParameterDict[self.dhuscensusdivision.Name] = intParameter("US Census Division", - value=1, - AllowableRange=[1, 2, 3, 4, 5, 6, 7, 8, 9], - UnitType=Units.NONE, - ErrMessage="assume default U.S. census division (1)", - ToolTipText="Select the U.S. census division to calculate district heating demand (if district heating demand option is set to 2)" - ) - # Results - used by other objects or printed in output downstream self.usebuiltinoutletplantcorrelation = self.OutputParameterDict[self.usebuiltinoutletplantcorrelation.Name] = OutputParameter( Name="usebuiltinoutletplantcorrelation", @@ -402,95 +486,7 @@ def __init__(self, model: Model): CurrentUnits=PowerUnit.MW ) - #absorption chiller - # absorption chiller - self.CoolingProduced = self.OutputParameterDict[self.CoolingProduced.Name] = OutputParameter( - Name="Cooling Produced", - value=[0.0], - UnitType=Units.POWER, - PreferredUnits=PowerUnit.MW, - CurrentUnits=PowerUnit.MW - ) - self.CoolingkWhProduced = self.OutputParameterDict[self.CoolingkWhProduced.Name] = OutputParameter( - Name="Annual Cooling Produced", - value=[0.0], - UnitType=Units.ENERGYFREQUENCY, - PreferredUnits=EnergyFrequencyUnit.KWhPERYEAR, - CurrentUnits=EnergyFrequencyUnit.KWhPERYEAR - ) - - #heat pump - self.HeatPumpElectricityUsed = self.OutputParameterDict[self.HeatPumpElectricityUsed.Name] = OutputParameter( - Name = "Heat Pump Electricity Consumed", - value=[0.0], - UnitType = Units.POWER, - PreferredUnits = PowerUnit.MW, - CurrentUnits = PowerUnit.MW - ) - self.HeatPumpElectricitykWhUsed = self.OutputParameterDict[self.HeatPumpElectricitykWhUsed.Name] = OutputParameter( - Name = "Annual Heat Pump Electricity Consumption", - value=[0.0], - UnitType = Units.ENERGYFREQUENCY, - PreferredUnits = EnergyFrequencyUnit.KWhPERYEAR, - CurrentUnits = EnergyFrequencyUnit.KWhPERYEAR - ) - - #district heating - self.hourlyheatingdemand = self.OutputParameterDict[self.hourlyheatingdemand.Name] = OutputParameter( - Name = "Hourly Heating Demand", - value=[0.0], - UnitType = Units.ENERGYFREQUENCY, - PreferredUnits = EnergyFrequencyUnit.MWhPERHOUR, - CurrentUnits = EnergyFrequencyUnit.MWhPERHOUR - ) - self.dailyheatingdemand = self.OutputParameterDict[self.dailyheatingdemand.Name] = OutputParameter( - Name = "Daily Heating Demand", - value=[0.0], - UnitType = Units.ENERGYFREQUENCY, - PreferredUnits = EnergyFrequencyUnit.MWhPERDAY, - CurrentUnits = EnergyFrequencyUnit.MWhPERDAY - ) - self.annualheatingdemand = self.OutputParameterDict[self.annualheatingdemand.Name] = OutputParameter( - Name = "Annual Heating Demand", - value=[0.0], - UnitType = Units.ENERGYFREQUENCY, - PreferredUnits = EnergyFrequencyUnit.GWhPERYEAR, - CurrentUnits = EnergyFrequencyUnit.GWhPERYEAR - ) - self.utilfactorarray = self.OutputParameterDict[self.utilfactorarray.Name] = OutputParameter( - Name = "Utiliation Factor Array", - value=[0.0], - UnitType = Units.NONE - ) - self.annualngdemand = self.OutputParameterDict[self.annualngdemand.Name] = OutputParameter( - Name = "Annual Peaking Boiler Natural Gas Demand", - value=[0.0], - UnitType = Units.ENERGYFREQUENCY, - PreferredUnits = EnergyFrequencyUnit.MWhPERYEAR, - CurrentUnits = EnergyFrequencyUnit.MWhPERYEAR - ) - self.maxpeakingboilerdemand = self.OutputParameterDict[self.maxpeakingboilerdemand.Name] = OutputParameter( - Name = "Maximum Peaking Boiler Natural Gas Demand", - value=[0.0], - UnitType = Units.POWER, - PreferredUnits = PowerUnit.MW, - CurrentUnits = PowerUnit.MW - ) - self.dhgeothermalheating = self.OutputParameterDict[self.dhgeothermalheating.Name] = OutputParameter( - Name = "Instantaneous Geothermal Heating Over Lifetime", - value=[0.0], - UnitType = Units.POWER, - PreferredUnits = PowerUnit.MW, - CurrentUnits = PowerUnit.MW - ) - self.dhnaturalgasheating = self.OutputParameterDict[self.dhnaturalgasheating.Name] = OutputParameter( - Name = "Instantaneous Natural Gas Heating Over Lifetime", - value=[0.0], - UnitType = Units.POWER, - PreferredUnits = PowerUnit.MW, - CurrentUnits = PowerUnit.MW - ) - model.logger.info("Complete " + str(__class__) + ": " + sys._getframe().f_code.co_name) + model.logger.info(f"Complete {self.__class__.__name__}: {__name__}") def __str__(self): return "SurfacePlant" @@ -503,7 +499,7 @@ def read_parameters(self, model:Model) -> None: :param model: The container class of the application, giving access to everything else, including the logger :return: None """ - model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) + 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 @@ -534,60 +530,65 @@ def read_parameters(self, model:Model) -> None: ParameterToModify.value = EndUseOptions.ELECTRICITY elif ParameterReadIn.sValue == str(2): ParameterToModify.value = EndUseOptions.HEAT + self.plant_type.value = PlantType.INDUSTRIAL elif ParameterReadIn.sValue == str(31): ParameterToModify.value = EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT elif ParameterReadIn.sValue == str(32): - ParameterToModify.value = EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICTY + ParameterToModify.value = EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICITY elif ParameterReadIn.sValue == str(41): ParameterToModify.value = EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT elif ParameterReadIn.sValue == str(42): - ParameterToModify.value = EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICTY + ParameterToModify.value = EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICITY elif ParameterReadIn.sValue == str(51): ParameterToModify.value = EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT elif ParameterReadIn.sValue == str(52): - ParameterToModify.value = EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICTY - elif ParameterReadIn.sValue == str(6): - ParameterToModify.value = EndUseOptions.ABSORPTION_CHILLER - elif ParameterReadIn.sValue == str(7): - ParameterToModify.value = EndUseOptions.HEAT_PUMP - elif ParameterReadIn.sValue == str(8): - ParameterToModify.value = EndUseOptions.DISTRICT_HEATING + ParameterToModify.value = EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICITY if ParameterToModify.Name == "Power Plant Type": if ParameterReadIn.sValue == str(1): - ParameterToModify.value = PowerPlantType.SUB_CRITICAL_ORC + ParameterToModify.value = PlantType.SUB_CRITICAL_ORC elif ParameterReadIn.sValue == str(2): - ParameterToModify.value = PowerPlantType.SUPER_CRITICAL_ORC + ParameterToModify.value = PlantType.SUPER_CRITICAL_ORC elif ParameterReadIn.sValue == str(3): - ParameterToModify.value = PowerPlantType.SINGLE_FLASH + ParameterToModify.value = PlantType.SINGLE_FLASH + elif ParameterReadIn.sValue == str(4): + ParameterToModify.value = PlantType.DOUBLE_FLASH + elif ParameterReadIn.sValue == str(5): + ParameterToModify.value = PlantType.ABSORPTION_CHILLER + elif ParameterReadIn.sValue == str(6): + ParameterToModify.value = PlantType.HEAT_PUMP + elif ParameterReadIn.sValue == str(7): + ParameterToModify.value = PlantType.DISTRICT_HEATING + elif ParameterReadIn.sValue == str(8): + ParameterToModify.value = PlantType.RTES else: - ParameterToModify.value = PowerPlantType.DOUBLE_FLASH - if self.enduseoption.value == EndUseOptions.ELECTRICITY: + ParameterToModify.value = PlantType.INDUSTRIAL + if self.enduse_option.value == EndUseOptions.ELECTRICITY: # simple single- or double-flash power plant assumes no production well pumping - if ParameterToModify.value in [PowerPlantType.SINGLE_FLASH, PowerPlantType.DOUBLE_FLASH]: + if ParameterToModify.value in [PlantType.SINGLE_FLASH, PlantType.DOUBLE_FLASH]: model.wellbores.impedancemodelallowed.value = False model.wellbores.productionwellpumping.value = False self.setinjectionpressurefixed = True - elif self.enduseoption.value in \ - [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICTY]: + elif self.enduse_option.value in \ + [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICITY]: # co-generation topping cycle with single- or double-flash power plant assumes no production well pumping - if ParameterToModify.value in [PowerPlantType.SINGLE_FLASH, PowerPlantType.DOUBLE_FLASH]: + if ParameterToModify.value in [PlantType.SINGLE_FLASH, PlantType.DOUBLE_FLASH]: model.wellbores.impedancemodelallowed.value = False model.wellbores.productionwellpumping.value = False self.setinjectionpressurefixed = True - elif self.enduseoption.value in \ - [EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICTY]: + elif self.enduse_option.value in \ + [EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICITY]: # co-generation bottoming cycle with single- or double-flash power plant assumes # production well pumping - if ParameterToModify.value in [PowerPlantType.SINGLE_FLASH, PowerPlantType.DOUBLE_FLASH]: + if ParameterToModify.value in [PlantType.SINGLE_FLASH, PlantType.DOUBLE_FLASH]: model.wellbores.impedancemodelallowed.value = False self.setinjectionpressurefixed = True - elif self.enduseoption.value in \ - [EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICTY]: + elif self.enduse_option.value in \ + [EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICITY]: # co-generation parallel cycle with single- or double-flash power plant assumes # production well pumping - if ParameterToModify.value in [PowerPlantType.SINGLE_FLASH, PowerPlantType.DOUBLE_FLASH]: + if ParameterToModify.value in [PlantType.SINGLE_FLASH, PlantType.DOUBLE_FLASH]: model.wellbores.impedancemodelallowed.value = False self.setinjectionpressurefixed = True if ParameterToModify.Name == "Plant Outlet Pressure": @@ -609,7 +610,7 @@ def read_parameters(self, model:Model) -> None: if "Plant Outlet Pressure" not in model.InputParameters: if self.setinjectionpressurefixed: self.usebuiltinoutletplantcorrelation.value = False - self.Pplantoutlet.value = 100 + self.plant_outlet_pressure.value = 100 print("Warning: No valid plant outlet pressure provided." + " GEOPHIRES will assume default plant outlet pressure (100 kPa)") model.logger.warning("No valid plant outlet pressure provided." + @@ -622,7 +623,8 @@ def read_parameters(self, model:Model) -> None: " pressure based on production wellhead pressure and surface equipment pressure drop of 10 psi") else: model.logger.info("No parameters read because no content provided") - model.logger.info("complete "+ str(__class__) + ": " + sys._getframe().f_code.co_name) + + model.logger.info(f"Complete {self.__class__.__name__}: {__name__}") def Calculate(self, model: Model) -> None: """ @@ -632,518 +634,8 @@ def Calculate(self, model: Model) -> None: :type model: :class:`~geophires_x.Model.Model` :return: Nothing, but it does make calculations and set values in the model """ - model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) - - # This is where all the calculations are made using all the values that have been set. - # If you subclass this class, you can choose to run these calculations before (or after) your calculations, - # but that assumes you have set all the values that are required for these calculations - # If you choose to subclass this master class, you can also choose to override this method (or not), - # and if you do, do it before or after you call you own version of this method. If you do, you can also choose - # to call this method from you class, which can effectively run the calculations of the superclass, making all - # the values available to your methods. but you had better have set all the parameters! - - # calculate produced electricity/direct-use heat - if self.enduseoption.value == EndUseOptions.HEAT: # direct-use - self.HeatExtracted.value = model.wellbores.nprod.value * model.wellbores.prodwellflowrate.value * \ - model.reserv.cpwater.value*(model.wellbores.ProducedTemperature.value - - model.wellbores.Tinj.value)/1E6 # heat extracted from geofluid [MWth] - # useful direct-use heat provided to application [MWth] - self.HeatProduced.value = self.HeatExtracted.value*self.enduseefficiencyfactor.value - # absorption chiller - elif self.enduseoption.value == EndUseOptions.ABSORPTION_CHILLER: # absorption chiller cooling - self.HeatExtracted.value = model.wellbores.nprod.value * model.wellbores.prodwellflowrate.value * model.reserv.cpwater.value * ( - model.wellbores.ProducedTemperature.value - model.wellbores.Tinj.value) / 1E6 # heat extracted from geofluid [MWth] - self.HeatProduced.value = self.HeatExtracted.value # we don't consider end-use efficiency factor here. All extracted heat will go to absorption chiller and there is the end-use efficiency factor. [MWth] - - if self.absorptionchillercop.Provided == False: - chiller_cop_correlation_temperatures = np.array([65, 68, 72, 75, 82, 90, 120, - 150]) # Linear correlation assumed here based on GEOPHIRES ORC correlation between 100 and 200 deg C [deg.C] plus plateaued above 200 deg. C - chiller_cop_correlation_values = np.array([0, 0.3, 0.5, 0.59, 0.65, 0.69, 0.74, - 0.78]) # Efficiency of ORC conversion from production exergy to electricity based on GEOPHIRES correlation [-] - chillercops = np.interp(model.wellbores.ProducedTemperature.value, chiller_cop_correlation_temperatures, - chiller_cop_correlation_values) - self.CoolingProduced.value = self.HeatProduced.value * chillercops * self.enduseefficiencyfactor.value # MW - else: - self.CoolingProduced.value = self.HeatProduced.value * self.absorptionchillercop.value * self.enduseefficiencyfactor.value # MW - - - # heat pump - elif self.enduseoption.value == EndUseOptions.HEAT_PUMP: # heat pump heating booster - self.HeatExtracted.value = model.wellbores.nprod.value * model.wellbores.prodwellflowrate.value * model.reserv.cpwater.value * ( - model.wellbores.ProducedTemperature.value - model.wellbores.Tinj.value) / 1E6 # heat extracted from geofluid [MWth] - - self.HeatProduced.value = self.HeatExtracted.value * self.heatpumpcop.value / ( - self.heatpumpcop.value - 1) * self.enduseefficiencyfactor.value # [MWth] - self.HeatPumpElectricityUsed.value = self.HeatExtracted.value / (self.heatpumpcop.value - 1) - - elif self.enduseoption.value == EndUseOptions.DISTRICT_HEATING: # district heating option - self.HeatExtracted.value = model.wellbores.nprod.value * model.wellbores.prodwellflowrate.value * model.reserv.cpwater.value * ( - model.wellbores.ProducedTemperature.value - model.wellbores.Tinj.value) / 1E6 # heat extracted from geofluid [MWth] - self.HeatProduced.value = self.HeatExtracted.value * self.enduseefficiencyfactor.value # useful direct-use heat provided to district heating network [MWth] - - [self.utilfactorarray.value, self.utilfactor.value, self.annualngdemand.value, self.maxpeakingboilerdemand.value, self.dhgeothermalheating.value, self.dhnaturalgasheating.value] = self.calc_util_factor(self.HeatProduced.value, self.dailyheatingdemand.value, model.economics.timestepsperyear.value) - self.annualheatingdemand.value = sum(self.dailyheatingdemand.value) / 1000 # GWh/year - - else: - if self.enduseoption.value in [EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICTY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT]: - self.TenteringPP.value = self.Tchpbottom.value*np.ones(len(model.reserv.timevector.value)) - else: - self.TenteringPP.value = model.wellbores.ProducedTemperature.value - # Availability water (copied from GEOPHIRES v1.0 Fortran Code) - A = 4.041650 - B = -1.204E-2 - C = 1.60500E-5 - T0 = self.Tenv.value + 273.15 - T1 = self.TenteringPP.value + 273.15 - T2 = self.Tenv.value + 273.15 - self.Availability.value = ((A-B*T0)*(T1-T2)+(B-C*T0)/2.0*(T1**2-T2**2)+C/3.0*(T1**3-T2**3)-A*T0*np.log(T1/T2))*2.2046/947.83 # MJ/kg - - if self.pptype.value == PowerPlantType.SUB_CRITICAL_ORC: - if self.Tenv.value < 15.: - C1 = 2.746E-3 - C0 = -8.3806E-2 - D1 = 2.713E-3 - D0 = -9.1841E-2 - Tfraction = (self.Tenv.value-5.)/10. - else: - C1 = 2.713E-3 - C0 = -9.1841E-2 - D1 = 2.676E-3 - D0 = -1.012E-1 - Tfraction = (self.Tenv.value-15.)/10. - etaull = C1*self.TenteringPP.value + C0 - etauul = D1*self.TenteringPP.value + D0 - etau = (1-Tfraction)*etaull + Tfraction*etauul - if self.Tenv.value < 15.: - C1 = 0.0894 - C0 = 55.6 - D1 = 0.0894 - D0 = 62.6 - Tfraction = (self.Tenv.value-5.)/10. - else: - C1 = 0.0894 - C0 = 62.6 - D1 = 0.0894 - D0 = 69.6 - Tfraction = (self.Tenv.value-15.)/10. - reinjtll = C1*self.TenteringPP.value + C0 - reinjtul = D1*self.TenteringPP.value + D0 - ReinjTemp = (1.-Tfraction)*reinjtll + Tfraction*reinjtul - elif self.pptype.value == PowerPlantType.SUPER_CRITICAL_ORC: - if self.Tenv.value < 15.: - C2 = -1.55E-5 - C1 = 7.604E-3 - C0 = -3.78E-1 - D2 = -1.499E-5 - D1 = 7.4268E-3 - D0 = -3.7915E-1 - Tfraction = (self.Tenv.value-5.)/10. - else: - C2 = -1.499E-5 - C1 = 7.4268E-3 - C0 = -3.7915E-1 - D2 = -1.55E-5 - D1 = 7.55136E-3 - D0 = -4.041E-1 - Tfraction = (self.Tenv.value-15.)/10. - etaull = C2*self.TenteringPP.value**2 + C1*self.TenteringPP.value + C0 - etauul = D2*self.TenteringPP.value**2 + D1*self.TenteringPP.value + D0 - etau = (1-Tfraction)*etaull + Tfraction*etauul - if self.Tenv.value < 15.: - C1 = 0.02 - C0 = 49.26 - D1 = 0.02 - D0 = 56.26 - Tfraction = (self.Tenv.value-5.)/10. - else: - C1 = 0.02 - C0 = 56.26 - D1 = 0.02 - D0 = 63.26 - Tfraction = (self.Tenv.value-15.)/10. - reinjtll = C1*self.TenteringPP.value + C0 - reinjtul = D1*self.TenteringPP.value + D0 - ReinjTemp = (1.-Tfraction)*reinjtll + Tfraction*reinjtul - elif self.pptype.value == PowerPlantType.SINGLE_FLASH: - if self.Tenv.value < 15.: - C2 = -4.27318E-7 - C1 = 8.65629E-4 - C0 = 1.78931E-1 - D2 = -5.85412E-7 - D1 = 9.68352E-4 - D0 = 1.58056E-1 - Tfraction = (self.Tenv.value-5.)/10. - else: - C2 = -5.85412E-7 - C1 = 9.68352E-4 - C0 = 1.58056E-1 - D2 = -7.78996E-7 - D1 = 1.09230E-3 - D0 = 1.33708E-1 - Tfraction = (self.Tenv.value-15.)/10. - etaull = C2*self.TenteringPP.value**2 + C1*self.TenteringPP.value + C0 - etauul = D2*self.TenteringPP.value**2 + D1*self.TenteringPP.value + D0 - etau = (1.-Tfraction)*etaull + Tfraction*etauul - if self.Tenv.value < 15.: - C2 = -1.11519E-3 - C1 = 7.79126E-1 - C0 = -10.2242 - D2 = -1.10232E-3 - D1 = 7.83893E-1 - D0 = -5.17039 - Tfraction = (self.Tenv.value-5.)/10. - else: - C2 = -1.10232E-3 - C1 = 7.83893E-1 - C0 = -5.17039 - D2 = -1.08914E-3 - D1 = 7.88562E-1 - D0 = -1.89707E-1 - Tfraction = (self.Tenv.value-15.)/10. - reinjtll = C2*self.TenteringPP.value**2 + C1*self.TenteringPP.value + C0 - reinjtul = D2*self.TenteringPP.value**2 + D1*self.TenteringPP.value + D0 - ReinjTemp = (1.-Tfraction)*reinjtll + Tfraction*reinjtul - elif self.pptype.value == PowerPlantType.DOUBLE_FLASH: - if self.Tenv.value < 15.: - C2 = -1.200E-6 - C1 = 1.22731E-3 - C0 = 2.26956E-1 - D2 = -1.42165E-6 - D1 = 1.37050E-3 - D0 = 1.99847E-1 - Tfraction = (self.Tenv.value-5.)/10. - else: - C2 = -1.42165E-6 - C1 = 1.37050E-3 - C0 = 1.99847E-1 - D2 = -1.66771E-6 - D1 = 1.53079E-3 - D0 = 1.69439E-1 - Tfraction = (self.Tenv.value-15.)/10. - etaull = C2*self.TenteringPP.value**2 + C1*self.TenteringPP.value + C0 - etauul = D2*self.TenteringPP.value**2 + D1*self.TenteringPP.value + D0 - etau = (1.-Tfraction)*etaull + Tfraction*etauul - if self.Tenv.value < 15.: - C2 = -7.70928E-4 - C1 = 5.02466E-1 - C0 = 5.22091 - D2 = -7.69455E-4 - D1 = 5.09406E-1 - D0 = 11.6859 - Tfraction = (self.Tenv.value-5.)/10. - else: - C2 = -7.69455E-4 - C1 = 5.09406E-1 - C0 = 11.6859 - D2 = -7.67751E-4 - D1 = 5.16356E-1 - D0 = 18.0798 - Tfraction = (self.Tenv.value-15.)/10. - reinjtll = C2*self.TenteringPP.value**2 + C1*self.TenteringPP.value + C0 - reinjtul = D2*self.TenteringPP.value**2 + D1*self.TenteringPP.value + D0 - ReinjTemp = (1.-Tfraction)*reinjtll + Tfraction*reinjtul - - # check if reinjectemp (model calculated) >= Tinj (user provided) - if self.enduseoption.value == EndUseOptions.ELECTRICITY: # pure electricity - if np.min(ReinjTemp) < model.wellbores.Tinj.value: - model.wellbores.Tinj.value = np.min(ReinjTemp) - print("Warning: injection temperature lowered") - model.logger.warning("injection temperature lowered") - elif self.enduseoption.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICTY, EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT]: # enduseoption = 3: cogen topping cycle - if np.min(ReinjTemp) < model.wellbores.Tinj.value: - self.Tinj = np.min(ReinjTemp) - print("Warning: injection temperature lowered") - model.logger.warning("injection temperature lowered") - elif self.enduseoption.value in [EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICTY]: # enduseoption = 4: cogen bottoming cycle - if np.min(ReinjTemp) < model.wellbores.Tinj.value: - model.wellbores.Tinj.value = np.min(ReinjTemp) - print("Warning: injection temperature lowered") - model.logger.warning("injection temperature lowered") - elif self.enduseoption.value in [EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICTY, EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT]: # cogen split of mass flow rate - if np.min(ReinjTemp) < model.wellbores.Tinj.value: - model.wellbores.Tinj.value = np.min(ReinjTemp) - print("Warning: injection temperature incorrect but cannot be lowered") - model.logger.warning("injection temperature incorrect but cannot be lowered") - - # calculate electricity/heat - if self.enduseoption.value == EndUseOptions.ELECTRICITY: # pure electricity - self.ElectricityProduced.value = self.Availability.value*etau*model.wellbores.nprod.value*model.wellbores.prodwellflowrate.value - self.HeatExtracted.value = model.wellbores.nprod.value*model.wellbores.prodwellflowrate.value*model.reserv.cpwater.value *\ - (model.wellbores.ProducedTemperature.value - model.wellbores.Tinj.value)/1E6 # Heat extracted from geofluid [MWth] - HeatExtractedTowardsElectricity = self.HeatExtracted.value - # enduseoption = 3: cogen topping cycle - elif self.enduseoption.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICTY, EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT]: - self.ElectricityProduced.value = self.Availability.value*etau*model.wellbores.nprod.value*model.wellbores.prodwellflowrate.value - self.HeatExtracted.value = model.wellbores.nprod.value*model.wellbores.prodwellflowrate.value*model.reserv.cpwater.value *\ - (model.wellbores.ProducedTemperature.value - model.wellbores.Tinj.value)/1E6 # Heat extracted from geofluid [MWth] - self.HeatProduced.value = self.enduseefficiencyfactor.value*model.wellbores.nprod.value *\ - model.wellbores.prodwellflowrate.value*model.reserv.cpwater.value *\ - (ReinjTemp - model.wellbores.Tinj.value)/1E6 # Useful heat for direct-use application [MWth] - HeatExtractedTowardsElectricity = model.wellbores.nprod.value*model.wellbores.prodwellflowrate.value *\ - model.reserv.cpwater.value*(model.wellbores.ProducedTemperature.value - ReinjTemp)/1E6 - # enduseoption = 4: cogen bottoming cycle - elif self.enduseoption.value in [EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICTY]: - self.ElectricityProduced.value = self.Availability.value*etau*model.wellbores.nprod.value*model.wellbores.prodwellflowrate.value - self.HeatExtracted.value = model.wellbores.nprod.value*model.wellbores.prodwellflowrate.value*model.reserv.cpwater.value *\ - (model.wellbores.ProducedTemperature.value - model.wellbores.Tinj.value)/1E6 # Heat extracted from geofluid [MWth] - self.HeatProduced.value = self.enduseefficiencyfactor.value*model.wellbores.nprod.value *\ - model.wellbores.prodwellflowrate.value*model.reserv.cpwater.value *\ - (model.wellbores.ProducedTemperature.value - self.Tchpbottom.value)/1E6 # Useful heat for direct-use application [MWth] - HeatExtractedTowardsElectricity = model.wellbores.nprod.value*model.wellbores.prodwellflowrate.value *\ - model.reserv.cpwater.value*(self.Tchpbottom.value - model.wellbores.Tinj.value)/1E6 - # enduseoption = 5: cogen split of mass flow rate - elif self.enduseoption.value in [EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICTY, EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT]: - self.ElectricityProduced.value = self.Availability.value*etau*model.wellbores.nprod.value *\ - model.wellbores.prodwellflowrate.value*(1.-self.chpfraction.value) # electricity part [MWe] - self.HeatExtracted.value = model.wellbores.nprod.value*model.wellbores.prodwellflowrate.value *\ - model.reserv.cpwater.value*(model.wellbores.ProducedTemperature.value - - model.wellbores.Tinj.value)/1E6 # Total amount of heat extracted from geofluid [MWth] - self.HeatProduced.value = self.enduseefficiencyfactor.value*self.chpfraction.value *\ - model.wellbores.nprod.value*model.wellbores.prodwellflowrate.value *\ - model.reserv.cpwater.value*(model.wellbores.ProducedTemperature.value - - model.wellbores.Tinj.value)/1E6 # useful heat part for direct-use application [MWth] - HeatExtractedTowardsElectricity = (1.-self.chpfraction.value)*model.wellbores.nprod.value *\ - model.wellbores.prodwellflowrate.value*model.reserv.cpwater.value *\ - (model.wellbores.ProducedTemperature.value - model.wellbores.Tinj.value)/1E6 - - # subtract pumping power for net electricity and calculate first law efficiency - if self.enduseoption.value != EndUseOptions.HEAT: - self.NetElectricityProduced.value = self.ElectricityProduced.value - model.wellbores.PumpingPower.value - self.FirstLawEfficiency.value = self.NetElectricityProduced.value/HeatExtractedTowardsElectricity - - # Calculate annual electricity/heat production - # all end-use options have "heat extracted from reservoir" and pumping kWs - self.HeatkWhExtracted.value = np.zeros(self.plantlifetime.value) - self.PumpingkWh.value = np.zeros(self.plantlifetime.value) - - for i in range(0, self.plantlifetime.value): - if self.enduseoption.value == EndUseOptions.DISTRICT_HEATING: # for district heating, we have a utilfactorarray - self.HeatkWhExtracted.value[i] = np.trapz(self.HeatExtracted.value[(0 + i * model.economics.timestepsperyear.value):((i + 1) * model.economics.timestepsperyear.value) + 1],dx=1. / model.economics.timestepsperyear.value * 365. * 24.) * 1000. *self.utilfactorarray.value[i] - self.PumpingkWh.value[i] = np.trapz(model.wellbores.PumpingPower.value[(0 + i * model.economics.timestepsperyear.value):((i + 1) * model.economics.timestepsperyear.value) + 1],dx=1. / model.economics.timestepsperyear.value * 365. * 24.) * 1000. * self.utilfactorarray.value[i] - else: - self.HeatkWhExtracted.value[i] = np.trapz(self.HeatExtracted.value[(0 + i * model.economics.timestepsperyear.value):((i + 1) * model.economics.timestepsperyear.value) + 1],dx=1. / model.economics.timestepsperyear.value * 365. * 24.) * 1000. * self.utilfactor.value - self.PumpingkWh.value[i] = np.trapz(model.wellbores.PumpingPower.value[(0 + i * model.economics.timestepsperyear.value):((i + 1) * model.economics.timestepsperyear.value) + 1],dx=1. / model.economics.timestepsperyear.value * 365. * 24.) * 1000. * self.utilfactor.value - - if self.enduseoption.value in [EndUseOptions.ELECTRICITY, EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICTY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICTY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICTY]: #all these end-use options have an electricity generation component - self.TotalkWhProduced.value = np.zeros(self.plantlifetime.value) - self.NetkWhProduced.value = np.zeros(self.plantlifetime.value) - for i in range(0, self.plantlifetime.value): - self.TotalkWhProduced.value[i] = np.trapz(self.ElectricityProduced.value[(0+i*model.economics.timestepsperyear.value):((i+1)*model.economics.timestepsperyear.value)+1], dx=1./model.economics.timestepsperyear.value*365.*24.)*1000.*self.utilfactor.value - self.NetkWhProduced.value[i] = np.trapz(self.NetElectricityProduced.value[(0+i*model.economics.timestepsperyear.value):((i+1)*model.economics.timestepsperyear.value)+1], dx=1./model.economics.timestepsperyear.value*365.*24.)*1000.*self.utilfactor.value - if self.enduseoption.value != EndUseOptions.ELECTRICITY: # all those end-use options have a direct-use component - self.HeatkWhProduced.value = np.zeros(self.plantlifetime.value) - if self.enduseoption.value == EndUseOptions.DISTRICT_HEATING: #for district heating, we have a utilfactorarray - for i in range(0,self.plantlifetime.value): - self.HeatkWhProduced.value[i] = np.trapz(self.HeatProduced.value[(0+i*model.economics.timestepsperyear.value):((i+1)*model.economics.timestepsperyear.value)+1],dx = 1./model.economics.timestepsperyear.value*365.*24.)*1000.*self.utilfactorarray.value[i] - else: - for i in range(0,self.plantlifetime.value): - self.HeatkWhProduced.value[i] = np.trapz(self.HeatProduced.value[(0+i*model.economics.timestepsperyear.value):((i+1)*model.economics.timestepsperyear.value)+1],dx = 1./model.economics.timestepsperyear.value*365.*24.)*1000.*self.utilfactor.value - - - if self.enduseoption.value == EndUseOptions.ABSORPTION_CHILLER: # absorption chiller: - self.CoolingkWhProduced.value = np.zeros(self.plantlifetime.value) - for i in range(0, self.plantlifetime.value): - self.CoolingkWhProduced.value[i] = np.trapz(self.CoolingProduced.value[ - (0 + i * model.economics.timestepsperyear.value):(( - i + 1) * model.economics.timestepsperyear.value) + 1], - dx=1. / model.economics.timestepsperyear.value * 365. * 24.) * 1000. * self.utilfactor.value - - - if self.enduseoption.value == EndUseOptions.HEAT_PUMP: #for heat pump, calculate electricity consumption: - self.HeatPumpElectricitykWhUsed.value = np.zeros(self.plantlifetime.value) - for i in range(0,self.plantlifetime.value): - self.HeatPumpElectricitykWhUsed.value[i] = np.trapz(self.HeatPumpElectricityUsed.value[(0+i*model.economics.timestepsperyear.value):((i+1)*model.economics.timestepsperyear.value)+1],dx = 1./model.economics.timestepsperyear.value*365.*24.)*1000.*self.utilfactor.value - - - #calculate reservoir heat content - self.RemainingReservoirHeatContent.value = model.reserv.InitialReservoirHeatContent.value-np.cumsum(self.HeatkWhExtracted.value)*3600*1E3/1E15 - - model.logger.info("complete " + str(__class__) + ": " + sys._getframe().f_code.co_name) - - # district heating routines below - def CalculateDHDemand(self, model: Model) -> None: - """ - Calculate the direct Heat demand of the district heating system based on the number of housing units and the census division - :param model: the model - :type model: :class:`~geophires_x.Model.Model` - :return: None - """ - # calculate heating demand for a district heating system - model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) - - if self.dhdemandoption.value == 1: # user provides district heating demand using csv file - self.dailyheatingdemand.value = self.read_daily_demand(self.dhdemandfilename.value, - self.dhdemanddatacolumnnumber.value, - self.dhdemandtimeresolution.value) # obtain daily heating demand - if self.dhdemandtimeresolution.value == 1: - self.hourlyheatingdemand.value = self.read_csv(self.dhdemandfilename.value, - self.dhdemanddatacolumnnumber.value) # if time interval is 1 hour, also store hourly heating demand - - elif self.dhdemandoption.value == 2: # calculate thermal demand from TMY and HDD - self.dailyheatingdemand.value = self.calculatedhdemand(self.dhnumberofhousingunits.value, - self.dhuscensusdivision.value, - self.dhconstantanchordemand.value, - self.dhtemperaturefilename.value, - self.dhtemperaturedatacolumnnumber.value) - - - def read_daily_demand(self, demand_file_name, demand_data_column, time_interval): - """ - Read the daily demand data column from the csv file and return the daily demand in MWh/day - :param demand_file_name: the name of the csv file - :type demand_file_name: str - :param demand_data_column: the column number of the demand data - :type demand_data_column: int - :param time_interval: the time interval of the demand data; - 1: hourly data, units in MW or MWh (both are treated equivalent) - 2: daily data, units in MWh - :type time_interval: int - :return: numpy array of daily demand in MWh/day - :rtype: numpy array - """ - - np.demand = [] - if time_interval == 1: # hourly data - hourly_demand = self.read_csv(demand_file_name, demand_data_column) - year_hour = 0 - for day in range(0, 365): # iterate through each day of the year - D_sum = 0 - for hour in range(0, 24): # iterate through hours of each day - D_sum += hourly_demand[year_hour] - year_hour += 1 - np.demand.append(D_sum) - elif time_interval == 2: # directly read in the daily values - np.demand = self.read_csv(demand_file_name, demand_data_column) - return np.demand - - - def read_csv(self, file_name, data_column): # data_column starts from 1 - # Extract data from CSV file - Data = pd.read_csv(file_name) # Read csv data using pandas to dataframe - data_column -= 1 # change index to start at 0 instead of 1 - data_array = Data.iloc[:, data_column].to_numpy() # Extract data and convert to numpy array [s] - return data_array - - - def calculatedhdemand(self, households, census_division, constant_demand, temp_file_name, temp_data_column): - """ - Parameters - ---------- - households : int - Number of households in the district heating system - census_division : int - 1-9, see manual or descriptions below for options - constant_demand : float - constant known demand in MW (do not include residential water heating) - temp_file_name : string - name of the hourly temperature profile CSV file to read - temp_data_column : int - column number of temperature data, starting from 1 - - Returns - ------- - numpy array of hourly and daily thermal demand - """ - # read in hourly temperature data - hourlytemperature = self.read_csv(temp_file_name, temp_data_column) - - # obtain HDD : 1 x 365 numpy array - daily_HDD = self.calc_HDD( - hourlytemperature) # Heating degree days for each day of the year as calculated in calc_HDD - - # space and water heating demand intensity values by census division - if census_division == 1: # new england - heat_intensity = 2.773 # KWh/household/HDD - water_intensity = 13.56 # KWh/household/day - elif census_division == 2: # middle atlantic - heat_intensity = 2.727 - water_intensity = 13.97 - elif census_division == 3: # east north central - heat_intensity = 2.650 - water_intensity = 14.24 - elif census_division == 4: # west north central - heat_intensity = 2.266 - water_intensity = 13.19 - elif census_division == 5: # south atlantic - heat_intensity = 2.583 - water_intensity = 10.35 - elif census_division == 6: # east south central - heat_intensity = 2.033 - water_intensity = 11.01 - elif census_division == 7: # west south central - heat_intensity = 2.872 - water_intensity = 10.35 - elif census_division == 8: # mountain - heat_intensity = 2.027 - water_intensity = 13.14 - elif census_division == 9: # pacific - heat_intensity = 1.845 - water_intensity = 12.41 - - np.demand = [] - for day in daily_HDD: - np.demand.append(households * (heat_intensity * day + water_intensity) / 1000 + constant_demand * 24) # MWh/day - - return np.demand - - - def calc_HDD(self, hourly_temp): - # this function calculates heating-degree-days (HDD) per day from a one-year hourly temperature file, deg. C only - T_mean = np.zeros(8760) # create an empty np array for daily mean temp - np.HDD = [] # create an empty np array for heating degree days - year_hour = 0 # counting variable for dataset (hours from 1 to 8760) - for day in range(0, 365): # iterate through each day of the year - T_sum = 0 # temporary summing variable for degrees in a day - for hour in range(0, 24): # loop over the hours of a single day - T_sum += hourly_temp[year_hour] # sum the temperatures within a single day - year_hour += 1 # advance the indexing variable - T_mean[day] = T_sum / 24 # calculate the mean temp for the day - if T_mean[day] < 18.3: # check whether heating was required for day - np.HDD.append(18.3 - T_mean[day]) # calculate HDD if heating was required - else: - np.HDD.append(0) # record a 0 if no heating was required - - # # optional plotting of HDD per day for provided temperature data - # year_day = np.arange(0, 365, 1) # make an array of days for plot x-axis - # plt.plot(year_day, np.HDD) - # plt.show() - - return np.HDD - - - def calc_util_factor(self, heatproduced, annualngdemand, timestepsperyear): - utilfactorarray = np.zeros(self.plantlifetime.value) # [-] - annualngdemand = np.zeros(self.plantlifetime.value) # MWh per year - instantaneouspeakingboilerdemand = np.zeros(self.plantlifetime.value * 365) - actualgeothermalused = np.zeros(self.plantlifetime.value * 365) - currentheatoutputstored = np.zeros(self.plantlifetime.value * 365) - - for i in range(0, self.plantlifetime.value): - for j in range(0, 365): - # compare thermal demand with supply - currentindex = i * 365 + j - currenttime = i + j / 365 - currentheatoutput = np.interp(currenttime, - np.arange(0, self.plantlifetime.value + 0.01, 1 / timestepsperyear), - heatproduced) - currentheatoutputstored[currentindex] = currentheatoutput - if self.dailyheatingdemand.value[j] / 24 > currentheatoutput: - actualgeothermalused[currentindex] = currentheatoutput - instantaneouspeakingboilerdemand[currentindex] = self.dailyheatingdemand.value[ - j] / 24 - currentheatoutput - else: - actualgeothermalused[currentindex] = self.dailyheatingdemand.value[j] / 24 - annualngdemand[i] = sum(instantaneouspeakingboilerdemand[i * 365:(i + 1) * 365]) * 24 # MWh/year - utilfactorarray[i] = sum(actualgeothermalused[i * 365:(i + 1) * 365]) / sum( - currentheatoutputstored[i * 365:(i + 1) * 365]) - - utilfactor = sum(actualgeothermalused) / sum(currentheatoutputstored) - if max(instantaneouspeakingboilerdemand) > 0: - maxpeakingboilerdemand = max( - instantaneouspeakingboilerdemand) / 20 * 24 # max instantaneous peaking boiler demand in MW, assuming it must meet peak demand day running for 20 hours in that day - else: - maxpeakingboilerdemand = 0 + model.logger.info(f"Init {self.__class__.__name__}: {__name__}") - return [utilfactorarray, utilfactor, annualngdemand, maxpeakingboilerdemand, actualgeothermalused, - instantaneouspeakingboilerdemand] + # All calculations are handled in subclasses of this class, so this function is empty. + model.logger.info(f"Complete {self.__class__.__name__}: {__name__}") diff --git a/src/geophires_x/SurfacePlantAbsorptionChiller.py b/src/geophires_x/SurfacePlantAbsorptionChiller.py new file mode 100644 index 00000000..7ac9aaf6 --- /dev/null +++ b/src/geophires_x/SurfacePlantAbsorptionChiller.py @@ -0,0 +1,146 @@ +import numpy as np +from .Parameter import floatParameter, OutputParameter +from .SurfacePlant import SurfacePlant +from .Units import * +import geophires_x.Model as Model + + +class surface_plant_absorption_chiller(SurfacePlant): + def __init__(self, model: Model): + """ + The __init__ function is called automatically when a class is instantiated. + It initializes the attributes of an object, and sets default values for certain arguments that can be overridden + by user input. + The __init__ function is used to set up all the parameters in the Surfaceplant. + :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__}") + super().__init__(model) # Initialize all the parameters in the superclass + + # 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. + + # 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.setinjectionpressurefixed = False + self.MyClass = self.__class__.__name__ + self.MyPath = __file__ + + # Input parameters absorption chiller + self.absorption_chiller_cop = self.ParameterDict[self.absorption_chiller_cop.Name] = floatParameter( + "Absorption Chiller COP", + value=0.7, + Min=0.1, + Max=1.5, + UnitType=Units.PERCENT, + PreferredUnits=PercentUnit.TENTH, CurrentUnits=PercentUnit.TENTH, + ErrMessage="assume default absorption chiller COP (0.7)", + ToolTipText="Specify the coefficient of performance (COP) of the absorption chiller" + ) + + # Output Parameters + self.cooling_produced = self.OutputParameterDict[self.cooling_produced.Name] = OutputParameter( + Name="Cooling Produced", + value=[0.0], + UnitType=Units.POWER, + PreferredUnits=PowerUnit.MW, + CurrentUnits=PowerUnit.MW + ) + self.cooling_kWh_Produced = self.OutputParameterDict[self.cooling_kWh_Produced.Name] = OutputParameter( + Name="Annual Cooling Produced", + value=[0.0], + UnitType=Units.ENERGYFREQUENCY, + PreferredUnits=EnergyFrequencyUnit.KWhPERYEAR, + CurrentUnits=EnergyFrequencyUnit.KWhPERYEAR + ) + + model.logger.info(f"Complete {self.__class__.__name__}: {__name__}") + + def __str__(self): + return "surface_plant_absorption_chiller" + + def read_parameters(self, model: Model) -> None: + """ + The read_parameters function reads in the parameters from a dictionary and stores them in the parameters. + It also handles special cases that need to be handled after a value has been read in and checked. + If you choose to subclass this master class, you can also choose to override this method (or not), and if you do + :param model: The container class of the application, giving access to everything else, including the logger + :return: None + """ + model.logger.info(f"Init {self.__class__.__name__}: {__name__}") + super().read_parameters(model) # Read in all the parameters from the superclass + + # Since there are no parameters that require unique adjustments in this class, we don't need to do anything. + + model.logger.info(f"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 produced electricity/direct-use heat + # absorption chiller: we don't consider end-use efficiency factor here. + # All extracted heat will go to absorption chiller and there is the end-use efficiency factor. [MWth] + self.HeatExtracted.value = model.wellbores.nprod.value * model.wellbores.prodwellflowrate.value * model.reserv.cpwater.value * ( + model.wellbores.ProducedTemperature.value - model.wellbores.Tinj.value) / 1E6 # heat extracted from geofluid [MWth] + self.HeatProduced.value = self.HeatExtracted.value + + self.cooling_produced.value = self.HeatProduced.value * self.absorption_chiller_cop.value * self.enduse_efficiency_factor.value # MW + + # Calculate annual electricity/heat production + # all end-use options have "heat extracted from reservoir" and pumping kWs + self.HeatkWhExtracted.value = np.zeros(self.plant_lifetime.value) + self.PumpingkWh.value = np.zeros(self.plant_lifetime.value) + + for i in range(0, self.plant_lifetime.value): + self.HeatkWhExtracted.value[i] = np.trapz(self.HeatExtracted.value[ + (0 + i * model.economics.timestepsperyear.value):(( + i + 1) * model.economics.timestepsperyear.value) + 1], + dx=1. / model.economics.timestepsperyear.value * 365. * 24.) * 1000. * self.utilization_factor.value + self.PumpingkWh.value[i] = np.trapz(model.wellbores.PumpingPower.value[ + (0 + i * model.economics.timestepsperyear.value):(( + i + 1) * model.economics.timestepsperyear.value) + 1], + dx=1. / model.economics.timestepsperyear.value * 365. * 24.) * 1000. * self.utilization_factor.value + + self.HeatkWhProduced.value = np.zeros(self.plant_lifetime.value) + for i in range(0, self.plant_lifetime.value): + self.HeatkWhProduced.value[i] = np.trapz(self.HeatProduced.value[ + (0 + i * model.economics.timestepsperyear.value):(( + i + 1) * model.economics.timestepsperyear.value) + 1], + dx=1. / model.economics.timestepsperyear.value * 365. * 24.) * 1000. * self.utilization_factor.value + + self.cooling_kWh_Produced.value = np.zeros(self.plant_lifetime.value) + for i in range(0, self.plant_lifetime.value): + self.cooling_kWh_Produced.value[i] = np.trapz(self.cooling_produced.value[ + (0 + i * model.economics.timestepsperyear.value):(( + i + 1) * model.economics.timestepsperyear.value) + 1], + dx=1. / model.economics.timestepsperyear.value * 365. * 24.) * 1000. * self.utilization_factor.value + + # calculate reservoir heat content + self.RemainingReservoirHeatContent.value = SurfacePlant.remaining_reservoir_heat_content( + self, model.reserv.InitialReservoirHeatContent.value, self.HeatkWhExtracted.value) + model.logger.info(f"complete {self.__class__.__name__}: {__name__}") diff --git a/src/geophires_x/SurfacePlantDirectUseHeat.py b/src/geophires_x/SurfacePlantDirectUseHeat.py new file mode 100644 index 00000000..91fd629a --- /dev/null +++ b/src/geophires_x/SurfacePlantDirectUseHeat.py @@ -0,0 +1,101 @@ +import os +import numpy as np +from .SurfacePlant import SurfacePlant +import geophires_x.Model as Model + + +class surface_plant_direct_use_heat(SurfacePlant): + def __init__(self, model: Model): + """ + The __init__ function is called automatically when a class is instantiated. + It initializes the attributes of an object, and sets default values for certain arguments that can be overridden + by user input. + The __init__ function is used to set up all the parameters in the Surfaceplant. + :param model: The container class of the application, giving access to everything else, including the logger + :type model: :class:`~geophires_x.Model.Model` + :return: None + """ + + model.logger.info("Init " + self.__class__.__name__ + ": " + __name__) + super().__init__(model) # Initialize all the parameters in the superclass + + # There are no parameters unique to this class, so we don't need to set any up here. + + # local variable initialization + sclass = self.__class__.__name__ + self.MyClass = sclass + self.MyPath = os.path.abspath(__file__) + + model.logger.info("Complete " + self.__class__.__name__ + ": " + __name__) + + def __str__(self): + return "surface_plant_direct_use_heat" + + def read_parameters(self, model: Model) -> None: + """ + The read_parameters function reads in the parameters from a dictionary and stores them in the parameters. + It also handles special cases that need to be handled after a value has been read in and checked. + If you choose to subclass this master class, you can also choose to override this method (or not), and if you do + :param model: The container class of the application, giving access to everything else, including the logger + :return: None + """ + model.logger.info("Init " + self.__class__.__name__ + ": " + __name__) + super().read_parameters(model) # Read in all the parameters from the superclass + + # Since there are no parameters unique to this class, we don't need to read any in here. + + model.logger.info("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("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 produced direct-use heat + # heat extracted from geofluid [MWth] + self.HeatExtracted.value = model.wellbores.nprod.value * model.wellbores.prodwellflowrate.value * \ + model.reserv.cpwater.value * (model.wellbores.ProducedTemperature.value - + model.wellbores.Tinj.value) / 1E6 + + # useful direct-use heat provided to application [MWth] + self.HeatProduced.value = self.HeatExtracted.value * self.enduse_efficiency_factor.value + + # Calculate annual electricity/heat production because all end-use options have "heat extracted from reservoir" and pumping kWs + self.HeatkWhExtracted.value = np.zeros(self.plant_lifetime.value) + self.PumpingkWh.value = np.zeros(self.plant_lifetime.value) + + for i in range(0, self.plant_lifetime.value): + self.HeatkWhExtracted.value[i] = np.trapz(self.HeatExtracted.value[ + (0 + i * model.economics.timestepsperyear.value):(( + i + 1) * model.economics.timestepsperyear.value) + 1], + dx=1. / model.economics.timestepsperyear.value * 365. * 24.) * 1000. * self.utilization_factor.value + self.PumpingkWh.value[i] = np.trapz(model.wellbores.PumpingPower.value[ + (0 + i * model.economics.timestepsperyear.value):(( + i + 1) * model.economics.timestepsperyear.value) + 1], + dx=1. / model.economics.timestepsperyear.value * 365. * 24.) * 1000. * self.utilization_factor.value + + self.HeatkWhProduced.value = np.zeros(self.plant_lifetime.value) + for i in range(0, self.plant_lifetime.value): + self.HeatkWhProduced.value[i] = np.trapz(self.HeatProduced.value[ + (0 + i * model.economics.timestepsperyear.value):(( + i + 1) * model.economics.timestepsperyear.value) + 1], + dx=1. / model.economics.timestepsperyear.value * 365. * 24.) * 1000. * self.utilization_factor.value + + # calculate reservoir heat content + self.RemainingReservoirHeatContent.value = SurfacePlant.remaining_reservoir_heat_content( + self, model.reserv.InitialReservoirHeatContent.value, self.HeatkWhExtracted.value) + + model.logger.info("complete " + self.__class__.__name__ + ": " + __name__) diff --git a/src/geophires_x/SurfacePlantDistrictHeating.py b/src/geophires_x/SurfacePlantDistrictHeating.py new file mode 100644 index 00000000..ed358370 --- /dev/null +++ b/src/geophires_x/SurfacePlantDistrictHeating.py @@ -0,0 +1,437 @@ +import os +import numpy as np +import pandas as pd +from geophires_x.OptionList import PlantType +from geophires_x.Parameter import floatParameter, intParameter, strParameter, OutputParameter +from geophires_x.SurfacePlant import SurfacePlant +from geophires_x.Units import * +import geophires_x.Model as Model + + +class surface_plant_district_heating(SurfacePlant): + def __init__(self, model: Model): + """ + The __init__ function is called automatically when a class is instantiated. + It initializes the attributes of an object, and sets default values for certain arguments that can be overridden + by user input. + The __init__ function is used to set up all the parameters in the Surfaceplant. + :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__}") + super().__init__(model) # this calls the __init__ function of the superclass, which is SurfacePlant + + # 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. + + # 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. + + # local variable initialization + sclass = str(__class__).replace("", "") + self.MyPath = os.path.abspath(__file__) + + self.dh_demand_option = self.ParameterDict[self.dh_demand_option.Name] = intParameter( + "District Heating Demand Option", + value=1, + AllowableRange=[1, 2], + UnitType=Units.NONE, + ErrMessage="assume default district heating demand option (1: known heat demand profile)", + ToolTipText="Select the method to provide the district heating demand to GEOPHIRES" + ) + self.dh_demand_filename = self.ParameterDict[self.dh_demand_filename.Name] = strParameter( + "District Heating Demand File Name", + value='HeatDemand.csv', + UnitType=Units.NONE, + ErrMessage="assume default district heating demand filename (HeatDemand.csv)", + ToolTipText="Provide district heating demand in csv file in MW or MWh per day (if district heating demand option is set to 1)" + ) + self.dh_demand_time_resolution = self.ParameterDict[self.dh_demand_time_resolution.Name] = intParameter( + "District Heating Demand Data Time Resolution", + value=1, + AllowableRange=[1, 2], + UnitType=Units.NONE, + ErrMessage="assume default district heating data time resolution (1: hourly data)", + ToolTipText="Provide time interval for thermal demand data: 1 = hourly (data provided as MW = MWh' 2 = daily (data provided as MWh/day) (if district heating demand option is set to 1)" + ) + self.dh_demand_data_column_number = self.ParameterDict[self.dh_demand_data_column_number.Name] = intParameter( + "District Heating Demand Data Column Number", + value=2, + AllowableRange=list(range(1, 101, 1)), + UnitType=Units.NONE, + ErrMessage="assume default district heating demand data column number (2)", + ToolTipText="Select the column number of the hourly or daily data in the district heating demand csv file (if district heating demand option is set to 1)" + ) + self.dh_temperature_filename = self.ParameterDict[self.dh_temperature_filename.Name] = strParameter( + "Temperature File Name", + value='Temperature.csv', + UnitType=Units.NONE, + ErrMessage="assume default temperature filename (Temperature.csv)", + ToolTipText="Provide filename of temperature file with hourly temperature to calculate district heating demand (if district heating demand option is set to 2)" + ) + self.dh_temperature_data_column_number = self.ParameterDict[ + self.dh_temperature_data_column_number.Name] = intParameter( + "Temperature Data Column Number", + value=2, + AllowableRange=list(range(1, 101, 1)), + UnitType=Units.NONE, + ErrMessage="assume default temperature data column number (2)", + ToolTipText="Select the column number of the hourly temperature data in the temperature csv file (if district heating demand option is set to 2)" + ) + self.dh_number_of_housing_units = self.ParameterDict[self.dh_number_of_housing_units.Name] = intParameter( + "Number of Housing Units", + value=100, + AllowableRange=list(range(0, 1000000, 1)), + UnitType=Units.NONE, + ErrMessage="assume default number of housing units (100)", + ToolTipText="Specify the number of housing units to calculate district heating demand (if district heating demand option is set to 2)" + ) + self.dh_constant_anchor_demand = self.ParameterDict[self.dh_constant_anchor_demand.Name] = floatParameter( + "Constant Anchor Demand", + value=0.0, + Min=0.0, + Max=100.0, + UnitType=Units.POWER, + PreferredUnits=PowerUnit.MW, + CurrentUnits=PowerUnit.MW, + ErrMessage="assume default constant anchor demand (10 MWth)", + ToolTipText="Specify the constant anchor demand to calculate the district heating demand (if district heating demand option is set to 2)" + ) + self.dh_us_census_division = self.ParameterDict[self.dh_us_census_division.Name] = intParameter( + "US Census Division", + value=1, + AllowableRange=[1, 2, 3, 4, 5, 6, 7, 8, 9], + UnitType=Units.NONE, + ErrMessage="assume default U.S. census division (1)", + ToolTipText="Select the U.S. census division to calculate district heating demand (if district heating demand option is set to 2)" + ) + + # Results - used by other objects or printed in output downstream + self.hourly_heating_demand = self.OutputParameterDict[self.hourly_heating_demand.Name] = OutputParameter( + Name="Hourly Heating Demand", + value=[0.0], + UnitType=Units.ENERGYFREQUENCY, + PreferredUnits=EnergyFrequencyUnit.MWhPERHOUR, + CurrentUnits=EnergyFrequencyUnit.MWhPERHOUR + ) + self.daily_heating_demand = self.OutputParameterDict[self.daily_heating_demand.Name] = OutputParameter( + Name="Daily Heating Demand", + value=[0.0], + UnitType=Units.ENERGYFREQUENCY, + PreferredUnits=EnergyFrequencyUnit.MWhPERDAY, + CurrentUnits=EnergyFrequencyUnit.MWhPERDAY + ) + self.annual_heating_demand = self.OutputParameterDict[self.annual_heating_demand.Name] = OutputParameter( + Name="Annual Heating Demand", + value=[0.0], + UnitType=Units.ENERGYFREQUENCY, + PreferredUnits=EnergyFrequencyUnit.GWhPERYEAR, + CurrentUnits=EnergyFrequencyUnit.GWhPERYEAR + ) + self.util_factor_array = self.OutputParameterDict[self.util_factor_array.Name] = OutputParameter( + Name="Utilisation Factor Array", + value=[0.0], + UnitType=Units.NONE + ) + self.annual_ng_demand = self.OutputParameterDict[self.annual_ng_demand.Name] = OutputParameter( + Name="Annual Peaking Boiler Natural Gas Demand", + value=[0.0], + UnitType=Units.ENERGYFREQUENCY, + PreferredUnits=EnergyFrequencyUnit.MWhPERYEAR, + CurrentUnits=EnergyFrequencyUnit.MWhPERYEAR + ) + self.max_peaking_boiler_demand = self.OutputParameterDict[ + self.max_peaking_boiler_demand.Name] = OutputParameter( + Name="Maximum Peaking Boiler Natural Gas Demand", + value=[0.0], + UnitType=Units.POWER, + PreferredUnits=PowerUnit.MW, + CurrentUnits=PowerUnit.MW + ) + self.dh_geothermal_heating = self.OutputParameterDict[self.dh_geothermal_heating.Name] = OutputParameter( + Name="Instantaneous Geothermal Heating Over Lifetime", + value=[0.0], + UnitType=Units.POWER, + PreferredUnits=PowerUnit.MW, + CurrentUnits=PowerUnit.MW + ) + self.dh_natural_gas_heating = self.OutputParameterDict[self.dh_natural_gas_heating.Name] = OutputParameter( + Name="Instantaneous Natural Gas Heating Over Lifetime", + value=[0.0], + UnitType=Units.POWER, + PreferredUnits=PowerUnit.MW, + CurrentUnits=PowerUnit.MW + ) + model.logger.info(f"Complete {self.__class__.__name__}: {__name__}") + + def __str__(self): + return "SurfacePlantDistrictHeating" + + def read_parameters(self, model: Model) -> None: + """ + The read_parameters function reads in the parameters from a dictionary and stores them in the parameters. + It also handles special cases that need to be handled after a value has been read in and checked. + If you choose to subclass this master class, you can also choose to override this method (or not), and if you do + :param model: The container class of the application, giving access to everything else, including the logger + :return: None + """ + model.logger.info(f"Init {self.__class__.__name__}: {__name__}") + super().read_parameters(model) # this calls the read_parameters function of the superclass, which is SurfacePlant + # Since there are no parameters that require unique adjustments in this class, we don't need to do anything. + model.logger.info(f"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! + + # useful direct-use heat provided to district heating network [MWth] + # heat extracted from geofluid [MWth] + self.HeatExtracted.value = model.wellbores.nprod.value * model.wellbores.prodwellflowrate.value * model.reserv.cpwater.value * ( + model.wellbores.ProducedTemperature.value - model.wellbores.Tinj.value) / 1E6 + self.HeatProduced.value = self.HeatExtracted.value * self.enduse_efficiency_factor.value + + [self.util_factor_array.value, self.utilization_factor.value, self.annual_ng_demand.value, + self.max_peaking_boiler_demand.value, self.dh_geothermal_heating.value, + self.dh_natural_gas_heating.value] = self.calc_util_factor(self.HeatProduced.value, model.economics.timestepsperyear.value) + self.annual_heating_demand.value = np.sum(self.daily_heating_demand.value) / 1000 # GWh/year + + # Calculate annual electricity/heat production + # all end-use options have "heat extracted from reservoir" and pumping kWs + self.HeatkWhExtracted.value = np.zeros(self.plant_lifetime.value) + self.PumpingkWh.value = np.zeros(self.plant_lifetime.value) + + for i in range(0, self.plant_lifetime.value): + if self.plant_type.value == PlantType.DISTRICT_HEATING: # for district heating, we have a util_factor_array + self.HeatkWhExtracted.value[i] = np.trapz(self.HeatExtracted.value[ + (0 + i * model.economics.timestepsperyear.value):(( + i + 1) * model.economics.timestepsperyear.value) + 1], + dx=1. / model.economics.timestepsperyear.value * 365. * 24.) * 1000. * \ + self.util_factor_array.value[i] + self.PumpingkWh.value[i] = np.trapz(model.wellbores.PumpingPower.value[ + (0 + i * model.economics.timestepsperyear.value):(( + i + 1) * model.economics.timestepsperyear.value) + 1], + dx=1. / model.economics.timestepsperyear.value * 365. * 24.) * 1000. * \ + self.util_factor_array.value[i] + else: + self.HeatkWhExtracted.value[i] = np.trapz(self.HeatExtracted.value[ + (0 + i * model.economics.timestepsperyear.value):(( + i + 1) * model.economics.timestepsperyear.value) + 1], + dx=1. / model.economics.timestepsperyear.value * 365. * 24.) * 1000. * self.utilization_factor.value + self.PumpingkWh.value[i] = np.trapz(model.wellbores.PumpingPower.value[ + (0 + i * model.economics.timestepsperyear.value):(( + i + 1) * model.economics.timestepsperyear.value) + 1], + dx=1. / model.economics.timestepsperyear.value * 365. * 24.) * 1000. * self.utilization_factor.value + + self.HeatkWhProduced.value = np.zeros(self.plant_lifetime.value) + for i in range(0, self.plant_lifetime.value): + self.HeatkWhProduced.value[i] = np.trapz(self.HeatProduced.value[ + (0 + i * model.economics.timestepsperyear.value):(( + i + 1) * model.economics.timestepsperyear.value) + 1], + dx=1. / model.economics.timestepsperyear.value * 365. * 24.) * 1000. * \ + self.util_factor_array.value[i] + + # calculate reservoir heat content + self.RemainingReservoirHeatContent.value = SurfacePlant.remaining_reservoir_heat_content( + self, model.reserv.InitialReservoirHeatContent.value, self.HeatkWhExtracted.value) + + model.logger.info(f"Complete {self.__class__.__name__}: {__name__}") + + # district heating routines below + def CalculateDHDemand(self, model: Model) -> None: + """ + Calculate the direct Heat demand of the district heating system based on the number of housing units and the census division + :param model: the model + :type model: :class:`~geophires_x.Model.Model` + :return: None + """ + # calculate heating demand for a district heating system + model.logger.info(f"Init {self.__class__.__name__}: {__name__}") + + if self.dh_demand_option.value == 1: # user provides district heating demand using csv file + # obtain daily heating demand from csv file + self.daily_heating_demand.value = self.read_daily_demand(self.dh_demand_filename.value, + self.dh_demand_data_column_number.value, + self.dh_demand_time_resolution.value) + if self.dh_demand_time_resolution.value == 1: + # if time interval is 1 hour, also store hourly heating demand + self.hourly_heating_demand.value = self.read_csv(self.dh_demand_filename.value, + self.dh_demand_data_column_number.value) + + elif self.dh_demand_option.value == 2: # calculate thermal demand from TMY and HDD + self.daily_heating_demand.value = self.calculate_dh_demand(self.dh_number_of_housing_units.value, + self.dh_us_census_division.value, + self.dh_constant_anchor_demand.value, + self.dh_temperature_filename.value, + self.dh_temperature_data_column_number.value) + + def read_daily_demand(self, demand_file_name, demand_data_column, time_interval) -> np.array: + """ + Read the daily demand data column from the csv file and return the daily demand in MWh/day + :param demand_file_name: the name of the csv file + :type demand_file_name: str + :param demand_data_column: the column number of the demand data + :type demand_data_column: int + :param time_interval: the time interval of the demand data; + 1: hourly data, units in MW or MWh (both are treated equivalent) + 2: daily data, units in MWh + :type time_interval: int + :return: numpy array of daily demand in MWh/day + :rtype: numpy array + """ + demand = np.empty(0) # create an empty np array for daily demand + if time_interval == 1: # hourly data + hourly_demand = self.read_csv(demand_file_name, demand_data_column) + year_hour = 0 + for day in range(0, 365): # iterate through each day of the year + D_sum = 0 + for hour in range(0, 24): # iterate through hours of each day + D_sum += hourly_demand[year_hour] + year_hour += 1 + demand = np.append(demand, D_sum) + elif time_interval == 2: # directly read in the daily values + np.demand = self.read_csv(demand_file_name, demand_data_column) + return demand + + def read_csv(self, file_name, data_column) -> np.array: # data_column starts from 1 + # Extract data from CSV file + Data = pd.read_csv(file_name) # Read csv data using pandas to dataframe + data_column -= 1 # change index to start at 0 instead of 1 + data_array = Data.iloc[:, data_column].to_numpy() # Extract data and convert to numpy array [s] + return data_array + + def calculate_dh_demand(self, households, census_division, constant_demand, temp_file_name, temp_data_column) -> np.array: + """ + :param households: Number of households in the district heating system + :type households: int + :param census_division: 1-9, see manual or descriptions below for options + :type: census_division: int + :param constant_demand : constant known demand in MW (do not include residential water heating) + :type constant_demand : float + :param temp_file_name : name of the hourly temperature profile CSV file to read + :type temp_file_name : string + :param temp_data_column : column number of temperature data, starting from 1 + :type temp_data_column : int + :return: numpy array of hourly and daily thermal demand + :rtype: numpy array + """ + # read in hourly temperature data + hourly_temperature = self.read_csv(temp_file_name, temp_data_column) + + # obtain HDD : 1 x 365 numpy array + # Heating degree days for each day of the year as calculated in calc_HDD + daily_HDD = self.calc_HDD(hourly_temperature) + + # space and water heating demand intensity values by census division + # default is New England + heat_intensity = 2.773 # KWh/household/HDD + water_intensity = 13.56 # KWh/household/day + if census_division == 2: # middle atlantic + heat_intensity = 2.727 + water_intensity = 13.97 + elif census_division == 3: # east north central + heat_intensity = 2.650 + water_intensity = 14.24 + elif census_division == 4: # west north central + heat_intensity = 2.266 + water_intensity = 13.19 + elif census_division == 5: # south atlantic + heat_intensity = 2.583 + water_intensity = 10.35 + elif census_division == 6: # east south central + heat_intensity = 2.033 + water_intensity = 11.01 + elif census_division == 7: # west south central + heat_intensity = 2.872 + water_intensity = 10.35 + elif census_division == 8: # mountain + heat_intensity = 2.027 + water_intensity = 13.14 + elif census_division == 9: # pacific + heat_intensity = 1.845 + water_intensity = 12.41 + + demand = np.empty(0) # create an empty np array for daily demand + for day in daily_HDD: + # MWh/day + demand = np.append(demand, households * (heat_intensity * day + water_intensity) / 1000 + constant_demand * 24) + + return demand + + def calc_HDD(self, hourly_temp) -> np.array: + """ + calculate heating-degree-days (HDD) per day from a one-year hourly temperature file, deg. C only + :param hourly_temp: 8760 hourly temperature data in deg. C + :type hourly_temp: numpy array + :return: numpy array of heating degree days + :rtype: numpy array + """ + T_mean = np.zeros(8760) # create an empty np array for daily mean temp + HDD = np.empty(0) # create an empty np array for heating degree days + year_hour = 0 # counting variable for dataset (hours from 1 to 8760) + for day in range(0, 365): # iterate through each day of the year + T_sum = 0 # temporary summing variable for degrees in a day + for hour in range(0, 24): # loop over the hours of a single day + T_sum += hourly_temp[year_hour] # sum the temperatures within a single day + year_hour += 1 # advance the indexing variable + T_mean[day] = T_sum / 24 # calculate the mean temp for the day + if T_mean[day] < 18.3: # check whether heating was required for day + HDD = np.append(HDD, 18.3 - T_mean[day]) # calculate HDD if heating was required + else: + HDD = np.append(HDD, 0) # record a 0 if no heating was required + + return HDD + + def calc_util_factor(self, heat_produced, time_steps_per_year): + util_factor_array = np.zeros(self.plant_lifetime.value) # [-] + annual_ng_demand = np.zeros(self.plant_lifetime.value) # MWh per year + instantaneous_peaking_boiler_demand = np.zeros(self.plant_lifetime.value * 365) + actual_geothermal_used = np.zeros(self.plant_lifetime.value * 365) + current_heat_output_stored = np.zeros(self.plant_lifetime.value * 365) + + for i in range(0, self.plant_lifetime.value): + for j in range(0, 365): + # compare thermal demand with supply + current_index = i * 365 + j + current_time = i + j / 365 + current_heat_output = np.interp(current_time, np.arange(0, self.plant_lifetime.value + 0.01, 1 / time_steps_per_year), heat_produced) + current_heat_output_stored[current_index] = current_heat_output + if self.daily_heating_demand.value[j] / 24 > current_heat_output: + actual_geothermal_used[current_index] = current_heat_output + instantaneous_peaking_boiler_demand[current_index] = self.daily_heating_demand.value[j] / 24 - current_heat_output + else: + actual_geothermal_used[current_index] = self.daily_heating_demand.value[j] / 24 + annual_ng_demand[i] = np.sum(instantaneous_peaking_boiler_demand[i * 365:(i + 1) * 365]) * 24 # MWh/year + util_factor_array[i] = np.sum(actual_geothermal_used[i * 365:(i + 1) * 365]) / np.sum(current_heat_output_stored[i * 365:(i + 1) * 365]) + + util_factor = np.sum(actual_geothermal_used) / np.sum(current_heat_output_stored) + if np.max(instantaneous_peaking_boiler_demand) > 0: + # max instantaneous peaking boiler demand in MW + # assuming it must meet peak demand day running for 20 hours in that day + max_peaking_boiler_demand = np.max(instantaneous_peaking_boiler_demand) / 20 * 24 + else: + max_peaking_boiler_demand = 0 + + return [util_factor_array, util_factor, annual_ng_demand, max_peaking_boiler_demand, actual_geothermal_used, instantaneous_peaking_boiler_demand] diff --git a/src/geophires_x/SurfacePlantDoubleFlash.py b/src/geophires_x/SurfacePlantDoubleFlash.py new file mode 100644 index 00000000..fdd823fc --- /dev/null +++ b/src/geophires_x/SurfacePlantDoubleFlash.py @@ -0,0 +1,136 @@ +from pathlib import Path +import numpy as np +from .OptionList import EndUseOptions +from .SurfacePlant import SurfacePlant +import geophires_x.Model as Model + + +class surface_plant_double_flash(SurfacePlant): + def __init__(self, model: Model): + """ + The __init__ function is called automatically when a class is instantiated. + It initializes the attributes of an object, and sets default values for certain arguments that can be overridden + by user input. + The __init__ function is used to set up all the parameters in the Surfaceplant. + :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__}") + super().__init__(model) # Initialize all the parameters in the superclass + + # 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. + + # 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. + + # local variable initialization + sclass = str(self.__class__).replace("", "") + self.MyPath = Path(__file__).resolve() + model.logger.info(f"Complete {self.__class__.__name__}: {__name__}") + + def __str__(self): + return "SurfacePlantDoubleFlash" + + def read_parameters(self, model:Model) -> None: + """ + The read_parameters function reads in the parameters from a dictionary and stores them in the parameters. + It also handles special cases that need to be handled after a value has been read in and checked. + If you choose to subclass this master class, you can also choose to override this method (or not), and if you do + :param model: The container class of the application, giving access to everything else, including the logger + :return: None + """ + model.logger.info(f"Init {self.__class__.__name__}: {__name__}") + super().read_parameters(model) # Initialize all the parameters in the superclass + model.logger.info(f"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 power plant entering temperature + self.TenteringPP.value = SurfacePlant.power_plant_entering_temperature(self, self.enduse_option.value, + model.reserv.timevector.value, self.T_chp_bottom.value, model.wellbores.ProducedTemperature.value) + + # Availability water + self.Availability.value = SurfacePlant.availability_water(self, self.ambient_temperature.value, self.TenteringPP.value, self.ambient_temperature.value) + + # Double flash-specific values + if self.ambient_temperature.value < 15.: + C21 = -1.200E-6 + C11 = 1.22731E-3 + C01 = 2.26956E-1 + D21 = -1.42165E-6 + D11 = 1.37050E-3 + D01 = 1.99847E-1 + C22 = -7.70928E-4 + C12 = 5.02466E-1 + C02 = 5.22091 + D22 = -7.69455E-4 + D12 = 5.09406E-1 + D02 = 11.6859 + else: + C21 = -1.42165E-6 + C11 = 1.37050E-3 + C01 = 1.99847E-1 + D21 = -1.66771E-6 + D11 = 1.53079E-3 + D01 = 1.69439E-1 + C22 = -7.69455E-4 + C12 = 5.09406E-1 + C02 = 11.6859 + D22 = -7.67751E-4 + D12 = 5.16356E-1 + D02 = 18.0798 + + model.wellbores.Tinj.value, ReinjTemp, etau = SurfacePlant.reinjection_temperature(self, model, + self.ambient_temperature.value, self.TenteringPP.value, model.wellbores.Tinj.value, + C01, C11, C21, D01, D11, D21, C02, C12, C22, D02, D12, D22) + + # calculate electricity & heat production + self.ElectricityProduced.value, self.HeatExtracted.value, self.HeatProduced.value, HeatExtractedTowardsElectricity = \ + SurfacePlant.electricity_heat_production(self, self.enduse_option.value, self.Availability.value, etau, + model.wellbores.nprod.value, model.wellbores.prodwellflowrate.value, + model.reserv.cpwater.value, model.wellbores.ProducedTemperature.value, + model.wellbores.Tinj.value, ReinjTemp, self.T_chp_bottom.value, + self.enduse_efficiency_factor.value, self.chp_fraction.value) + + # subtract pumping power for net electricity and calculate first law efficiency + self.NetElectricityProduced.value = self.ElectricityProduced.value - model.wellbores.PumpingPower.value + self.FirstLawEfficiency.value = self.NetElectricityProduced.value/HeatExtractedTowardsElectricity + + # Calculate annual electricity, pum;ping, and heat production + self.HeatkWhExtracted.value, self.PumpingkWh.value, self.TotalkWhProduced.value, self.NetkWhProduced.value, self.HeatkWhProduced.value = \ + SurfacePlant.annual_electricity_pumping_power(self, self.plant_lifetime.value, self.enduse_option.value, + self.HeatExtracted.value, model.economics.timestepsperyear.value, self.utilization_factor.value, + model.wellbores.PumpingPower.value, self.ElectricityProduced.value, + self.NetElectricityProduced.value, self.HeatProduced.value) + + # calculate reservoir heat content + self.RemainingReservoirHeatContent.value = SurfacePlant.remaining_reservoir_heat_content( + self, model.reserv.InitialReservoirHeatContent.value, self.HeatkWhExtracted.value) + + model.logger.info(f"complete {self.__class__.__name__}: {__name__}") diff --git a/src/geophires_x/SurfacePlantHeatPump.py b/src/geophires_x/SurfacePlantHeatPump.py new file mode 100644 index 00000000..216968fd --- /dev/null +++ b/src/geophires_x/SurfacePlantHeatPump.py @@ -0,0 +1,136 @@ +import inspect +from pathlib import Path +import numpy as np +from geophires_x.Parameter import floatParameter, OutputParameter +from geophires_x.SurfacePlant import SurfacePlant +from geophires_x.Units import * +import geophires_x.Model as Model + + +class surface_plant_heat_pump(SurfacePlant): + def __init__(self, model: Model): + """ + The __init__ function is called automatically when a class is instantiated. + It initializes the attributes of an object, and sets default values for certain arguments that can be overridden + by user input. + The __init__ function is used to set up all the parameters in the Surfaceplant. + :param model: The container class of the application, giving access to everything else, including the logger + :type model: :class:`~geophires_x.Model.Model` + :return: None + """ + + model.logger.info("Init " + str(__class__) + ": " + inspect.currentframe().f_code.co_name) + super().__init__(model) # Initialize all the parameters in the superclass + + # 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. + + # 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. + + # local variable initialization + sclass = str(__class__).replace("", "") + self.MyPath = Path(__file__).resolve() + + self.heat_pump_cop = self.ParameterDict[self.heat_pump_cop.Name] = floatParameter( + "Heat Pump COP", + value=5, + Min=1, + Max=10, + UnitType=Units.PERCENT, + PreferredUnits=PercentUnit.TENTH, + CurrentUnits=PercentUnit.TENTH, + ErrMessage="assume default heat pump COP (5)", + ToolTipText="Specify the coefficient of performance (COP) of the heat pump" + ) + + # Results - used by other objects or printed in output downstream + self.heat_pump_electricity_used = self.OutputParameterDict[self.heat_pump_electricity_used.Name] = OutputParameter( + Name="Heat Pump Electricity Consumed", + value=[0.0], + UnitType=Units.POWER, + PreferredUnits=PowerUnit.MW, + CurrentUnits=PowerUnit.MW + ) + self.heat_pump_electricity_kwh_used = self.OutputParameterDict[self.heat_pump_electricity_kwh_used.Name] = OutputParameter( + Name = "Annual Heat Pump Electricity Consumption", + value=[0.0], + UnitType=Units.ENERGYFREQUENCY, + PreferredUnits=EnergyFrequencyUnit.KWhPERYEAR, + CurrentUnits=EnergyFrequencyUnit.KWhPERYEAR + ) + + model.logger.info("Complete " + str(__class__) + ": " + inspect.currentframe().f_code.co_name) + + def __str__(self): + return "surface_plant_heat_pump" + + def read_parameters(self, model:Model) -> None: + """ + The read_parameters function reads in the parameters from a dictionary and stores them in the parameters. + It also handles special cases that need to be handled after a value has been read in and checked. + If you choose to subclass this master class, you can also choose to override this method (or not), and if you do + :param model: The container class of the application, giving access to everything else, including the logger + :return: None + """ + model.logger.info("Init " + str(__class__) + ": " + inspect.currentframe().f_code.co_name) + super().read_parameters(model) # Read in all the parameters from the superclass + + # Since there are no parameters unique to this class, we don't need to read any in here. + + model.logger.info("complete "+ str(__class__) + ": " + inspect.currentframe().f_code.co_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("Init " + str(__class__) + ": " + inspect.currentframe().f_code.co_name) + + # This is where all the calculations are made using all the values that have been set. + # If you subclass this class, you can choose to run these calculations before (or after) your calculations, + # but that assumes you have set all the values that are required for these calculations + # If you choose to subclass this master class, you can also choose to override this method (or not), + # and if you do, do it before or after you call you own version of this method. If you do, you can also choose + # to call this method from you class, which can effectively run the calculations of the superclass, making all + # the values available to your methods. but you had better have set all the parameters! + + self.HeatExtracted.value = model.wellbores.nprod.value * model.wellbores.prodwellflowrate.value * model.reserv.cpwater.value * ( + model.wellbores.ProducedTemperature.value - model.wellbores.Tinj.value) / 1E6 # heat extracted from geofluid [MWth] + + self.HeatProduced.value = (self.HeatExtracted.value * self.heat_pump_cop.value / (self.heat_pump_cop.value - 1) * + self.enduse_efficiency_factor.value) # [MWth] + self.heat_pump_electricity_used.value = self.HeatExtracted.value / (self.heat_pump_cop.value - 1) + + # Calculate annual electricity/heat production + # all end-use options have "heat extracted from reservoir" and pumping kWs + self.HeatkWhExtracted.value = np.zeros(self.plant_lifetime.value) + self.PumpingkWh.value = np.zeros(self.plant_lifetime.value) + + for i in range(0, self.plant_lifetime.value): + self.HeatkWhExtracted.value[i] = np.trapz(self.HeatExtracted.value[(0 + i * model.economics.timestepsperyear.value):((i + 1) * model.economics.timestepsperyear.value) + 1],dx=1. / model.economics.timestepsperyear.value * 365. * 24.) * 1000. * self.utilization_factor.value + self.PumpingkWh.value[i] = np.trapz(model.wellbores.PumpingPower.value[(0 + i * model.economics.timestepsperyear.value):((i + 1) * model.economics.timestepsperyear.value) + 1],dx=1. / model.economics.timestepsperyear.value * 365. * 24.) * 1000. * self.utilization_factor.value + + self.HeatkWhProduced.value = np.zeros(self.plant_lifetime.value) + for i in range(0, self.plant_lifetime.value): + self.HeatkWhProduced.value[i] = np.trapz(self.HeatProduced.value[(0+i*model.economics.timestepsperyear.value):((i+1)*model.economics.timestepsperyear.value)+1],dx = 1./model.economics.timestepsperyear.value*365.*24.)*1000.*self.utilization_factor.value + + self.heat_pump_electricity_kwh_used.value = np.zeros(self.plant_lifetime.value) + for i in range(0, self.plant_lifetime.value): + self.heat_pump_electricity_kwh_used.value[i] = np.trapz(self.heat_pump_electricity_used.value[(0 + i * model.economics.timestepsperyear.value):((i + 1) * model.economics.timestepsperyear.value) + 1], dx =1. / model.economics.timestepsperyear.value * 365. * 24.) * 1000. * self.utilization_factor.value + + # calculate reservoir heat content + self.RemainingReservoirHeatContent.value = SurfacePlant.remaining_reservoir_heat_content( + self, model.reserv.InitialReservoirHeatContent.value, self.HeatkWhExtracted.value) + + model.logger.info("complete " + str(__class__) + ": " + inspect.currentframe().f_code.co_name) diff --git a/src/geophires_x/SUTRASurfacePlant.py b/src/geophires_x/SurfacePlantSUTRA.py similarity index 59% rename from src/geophires_x/SUTRASurfacePlant.py rename to src/geophires_x/SurfacePlantSUTRA.py index 3f17460e..4db1a400 100644 --- a/src/geophires_x/SUTRASurfacePlant.py +++ b/src/geophires_x/SurfacePlantSUTRA.py @@ -1,15 +1,12 @@ -import sys -import os +from pathlib import Path import numpy as np -from .OptionList import EndUseOptions, PowerPlantType -from .Parameter import floatParameter, intParameter, strParameter, OutputParameter, ReadParameter +from .Parameter import OutputParameter +from .SurfacePlant import SurfacePlant from .Units import * import geophires_x.Model as Model -import pandas as pd -from matplotlib import pyplot as plt -class SUTRASurfacePlant: +class surface_plant_sutra(SurfacePlant): def __init__(self, model: Model): """ The __init__ function is called automatically when a class is instantiated. @@ -28,108 +25,17 @@ def __init__(self, model: Model): :return: None """ - model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) - self.Tinj = 0.0 + model.logger.info(f"Init {self.__class__.__name__}: {self.__init__.__name__}") # 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.enduseoption = self.ParameterDict[self.enduseoption.Name] = intParameter( - "End-Use Option", - value=EndUseOptions.ELECTRICITY, - AllowableRange=[1, 2, 31, 32, 41, 42, 51, 52, 6, 7, 8, 9], - UnitType=Units.NONE, - ErrMessage="assume default end-use option (1: electricity only)", - ToolTipText="Select the end-use application of the geofluid heat (see docs for details)" - ) - - self.pumpeff = self.ParameterDict[self.pumpeff.Name] = floatParameter( - "Circulation Pump Efficiency", - value=0.75, - DefaultValue=0.75, - Min=0.1, - Max=1.0, - UnitType=Units.PERCENT, - PreferredUnits=PercentUnit.PERCENT, - CurrentUnits=PercentUnit.TENTH, - Required=True, - ErrMessage="assume default circulation pump efficiency (0.75)", - ToolTipText="Specify the overall efficiency of the injection and production well pumps" - ) - - self.enduseefficiencyfactor = self.ParameterDict[self.enduseefficiencyfactor.Name] = floatParameter( - "End-Use Efficiency Factor", - value=0.9, - DefaultValue=0.9, - Min=0.1, - Max=1.0, - UnitType=Units.PERCENT, - PreferredUnits=PercentUnit.TENTH, - CurrentUnits=PercentUnit.TENTH, - ErrMessage="assume default end-use efficiency factor (0.9)", - ToolTipText="Constant thermal efficiency of the direct-use application" - ) - - self.Tenv = self.ParameterDict[self.Tenv.Name] = floatParameter( - "Ambient Temperature", - value=15.0, - DefaultValue=15.0, - Min=-50, - Max=50, - UnitType=Units.TEMPERATURE, - PreferredUnits=TemperatureUnit.CELSIUS, - CurrentUnits=TemperatureUnit.CELSIUS, - ErrMessage="assume default ambient temperature (15 deg.C)", - ToolTipText="Ambient (or dead-state) temperature used for calculating power plant utilization efficiency" - ) - self.plantlifetime = self.ParameterDict[self.plantlifetime.Name] = intParameter( - "Plant Lifetime", - value=30, - DefaultValue=30, - AllowableRange=list(range(1, 101, 1)), - UnitType=Units.TIME, - PreferredUnits=TimeUnit.YEAR, - CurrentUnits=TimeUnit.YEAR, - Required=True, - ErrMessage="assume default plant lifetime (30 years)", - ToolTipText="System lifetime" - ) - - self.elecprice = self.ParameterDict[self.elecprice.Name] = floatParameter( - "Electricity Rate", - value=0.07, - DefaultValue=0.07, - Min=0.0, - Max=1.0, - UnitType=Units.ENERGYCOST, - PreferredUnits=EnergyCostUnit.DOLLARSPERKWH, - CurrentUnits=EnergyCostUnit.DOLLARSPERKWH, - ErrMessage="assume default electricity rate ($0.07/kWh)", - ToolTipText="Price of electricity to calculate pumping costs in direct-use heat only mode or revenue" + - " from electricity sales in CHP mode." - ) + super().__init__(model) # Initialize all the parameters in the superclass # local variable initialization - self.setinjectionpressurefixed = False - sclass = str(__class__).replace("", "") - self.MyPath = os.path.abspath(__file__) - - #heat pump - self.heatpumpcop = self.ParameterDict[self.heatpumpcop.Name] = floatParameter( - "Heat Pump COP", - value = 5, - Min=1, - Max = 10, - UnitType = Units.PERCENT, - PreferredUnits = PercentUnit.TENTH, - CurrentUnits = PercentUnit.TENTH, - ErrMessage="assume default heat pump COP (5)", - ToolTipText="Specify the coefficient of performance (COP) of the heat pump" - ) + sclass = self.__class__.__name__ + self.MyClass = sclass + self.MyPath = Path(__file__).resolve() # Results - used by other objects or printed in output downstream self.SUTRATimeStep = self.OutputParameterDict[self.SUTRATimeStep.Name] = OutputParameter( @@ -235,10 +141,10 @@ def __init__(self, model: Model): CurrentUnits = EnergyFrequencyUnit.KWhPERYEAR ) - model.logger.info("Complete " + str(__class__) + ": " + sys._getframe().f_code.co_name) + model.logger.info(f"Complete {self.__class__.__name__}: {self.__init__.__name__}") def __str__(self): - return "SurfacePlant" + return "SurfacePlantSUTRA" def read_parameters(self, model:Model) -> None: """ @@ -258,52 +164,9 @@ def read_parameters(self, model:Model) -> None: :type model: :class:`~geophires_x.Model.Model` :return: None """ - model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) - - - if len(model.InputParameters) > 0: - # loop through all the parameters that the user wishes to set, looking for parameters that match this object - for item in self.ParameterDict.items(): - ParameterToModify = item[1] - key = ParameterToModify.Name.strip() - if key in model.InputParameters: - ParameterReadIn = model.InputParameters[key] - # 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 - # this should handle all the non-special cases - ReadParameter(ParameterReadIn, ParameterToModify, model) - - # handle special cases - if ParameterToModify.Name == "End-Use Option": - if ParameterReadIn.sValue == str(1): - ParameterToModify.value = EndUseOptions.ELECTRICITY - elif ParameterReadIn.sValue == str(2): - ParameterToModify.value = EndUseOptions.HEAT - elif ParameterReadIn.sValue == str(31): - ParameterToModify.value = EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT - elif ParameterReadIn.sValue == str(32): - ParameterToModify.value = EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICTY - elif ParameterReadIn.sValue == str(41): - ParameterToModify.value = EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT - elif ParameterReadIn.sValue == str(42): - ParameterToModify.value = EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICTY - elif ParameterReadIn.sValue == str(51): - ParameterToModify.value = EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT - elif ParameterReadIn.sValue == str(52): - ParameterToModify.value = EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICTY - elif ParameterReadIn.sValue == str(6): - ParameterToModify.value = EndUseOptions.ABSORPTION_CHILLER - elif ParameterReadIn.sValue == str(7): - ParameterToModify.value = EndUseOptions.HEAT_PUMP - elif ParameterReadIn.sValue == str(8): - ParameterToModify.value = EndUseOptions.DISTRICT_HEATING - elif ParameterReadIn.sValue == str(9): - ParameterToModify.value = EndUseOptions.RTES - - else: - model.logger.info("No parameters read because no content provided") - model.logger.info("complete "+ str(__class__) + ": " + sys._getframe().f_code.co_name) + model.logger.info(f"Init {self.__class__.__name__}: {self.__init__.__name__}") + super().read_parameters(model) # Read in all the parameters from the superclass + model.logger.info(f"complete {self.__class__.__name__}: {self.__init__.__name__}") def Calculate(self, model: Model) -> None: """ @@ -320,7 +183,7 @@ def Calculate(self, model: Model) -> None: :type model: :class:`~geophires_x.Model.Model` :return: Nothing, but it does make calculations and set values in the model """ - model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) + model.logger.info(f"Init {self.__class__.__name__}: {self.__init__.__name__}") # calculate and instantaneous heat injected, geothermal heat supplied, auxiliary heating required and total heat produced TimeVector = np.append(model.reserv.TimeProfile.value[0:-1:2],model.reserv.TimeProfile.value[-1]) @@ -351,8 +214,8 @@ def Calculate(self, model: Model) -> None: self.AnnualTotalHeatProduced.value[i] = sum(self.TotalHeatProduced.value[0+i*730:(i+1)*730])*self.SUTRATimeStep.value/1000 self.PumpingkWh.value[i] = sum(model.wellbores.PumpingPower.value[0+i*730:(i+1)*730])*self.SUTRATimeStep.value - #calculate maximum auxilary boiler demand + # calculate maximum auxiliary boiler demand self.maxpeakingboilerdemand.value = max(self.AnnualAuxiliaryHeatProduced.value) - model.logger.info("complete " + str(__class__) + ": " + sys._getframe().f_code.co_name) + model.logger.info(f"complete {self.__class__.__name__}: {self.__init__.__name__}") diff --git a/src/geophires_x/SurfacePlantSingleFlash.py b/src/geophires_x/SurfacePlantSingleFlash.py new file mode 100644 index 00000000..d4c0891d --- /dev/null +++ b/src/geophires_x/SurfacePlantSingleFlash.py @@ -0,0 +1,136 @@ +import sys +from pathlib import Path +import numpy as np +from geophires_x.OptionList import EndUseOptions +from geophires_x.SurfacePlant import SurfacePlant +import geophires_x.Model as Model + +class surface_plant_single_flash(SurfacePlant): + def __init__(self, model: Model): + """ + The __init__ function is called automatically when a class is instantiated. + It initializes the attributes of an object, and sets default values for certain arguments that can be overridden + by user input. + The __init__ function is used to set up all the parameters in the Surfaceplant. + :param model: The container class of the application, giving access to everything else, including the logger + :type model: :class:`~geophires_x.Model.Model` + :return: None + """ + + model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) + super().__init__(model) # Initialize all the parameters in the superclass + + # 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. + + # 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. + + # local variable initialization + sclass = self.__class__.__name__ + self.MyClass = sclass + self.MyPath = Path(__file__).resolve() + model.logger.info("Complete " + str(__class__) + ": " + sys._getframe().f_code.co_name) + + def __str__(self): + return "SurfacePlantSingleFlash" + + def read_parameters(self, model:Model) -> None: + """ + The read_parameters function reads in the parameters from a dictionary and stores them in the parameters. + It also handles special cases that need to be handled after a value has been read in and checked. + If you choose to subclass this master class, you can also choose to override this method (or not), and if you do + :param model: The container class of the application, giving access to everything else, including the logger + :return: None + """ + model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) + super().read_parameters(model) # Initialize all the parameters in the superclass + model.logger.info("complete "+ str(__class__) + ": " + sys._getframe().f_code.co_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("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) + + # This is where all the calculations are made using all the values that have been set. + # If you subclass this class, you can choose to run these calculations before (or after) your calculations, + # but that assumes you have set all the values that are required for these calculations + # If you choose to subclass this master class, you can also choose to override this method (or not), + # and if you do, do it before or after you call you own version of this method. If you do, you can also choose + # to call this method from you class, which can effectively run the calculations of the superclass, making all + # the values available to your methods. but you had better have set all the parameters! + + # calculate power plant entering temperature + self.TenteringPP.value = SurfacePlant.power_plant_entering_temperature(self, self.enduse_option.value, + model.reserv.timevector.value, self.T_chp_bottom.value, model.wellbores.ProducedTemperature.value) + + # Availability water + self.Availability.value = SurfacePlant.availability_water(self, self.ambient_temperature.value, self.TenteringPP.value, self.ambient_temperature.value) + + # Single flash-specific values. + if self.ambient_temperature.value < 15.: + C21 = -4.27318E-7 + C11 = 8.65629E-4 + C01 = 1.78931E-1 + D21 = -5.85412E-7 + D11 = 9.68352E-4 + D01 = 1.58056E-1 + C22 = -1.11519E-3 + C12 = 7.79126E-1 + C02 = -10.2242 + D22 = -1.10232E-3 + D12 = 7.83893E-1 + D02 = -5.17039 + else: + C21 = -5.85412E-7 + C11 = 9.68352E-4 + C01 = 1.58056E-1 + D21 = -7.78996E-7 + D11 = 1.09230E-3 + D01 = 1.33708E-1 + C22 = -1.10232E-3 + C12 = 7.83893E-1 + C02 = -5.17039 + D22 = -1.08914E-3 + D12 = 7.88562E-1 + D02 = -1.89707E-1 + + model.wellbores.Tinj.value, ReinjTemp, etau = SurfacePlant.reinjection_temperature(self, model, + self.ambient_temperature.value, self.TenteringPP.value, model.wellbores.Tinj.value, + C01, C11, C21, D01, D11, D21, C02, C12, C22, D02, D12, D22) + + # calculate electricity & heat production + self.ElectricityProduced.value, self.HeatExtracted.value, self.HeatProduced.value, HeatExtractedTowardsElectricity = \ + SurfacePlant.electricity_heat_production(self, self.enduse_option.value, self.Availability.value, etau, + model.wellbores.nprod.value, model.wellbores.prodwellflowrate.value, + model.reserv.cpwater.value, model.wellbores.ProducedTemperature.value, + model.wellbores.Tinj.value, ReinjTemp, self.T_chp_bottom.value, + self.enduse_efficiency_factor.value, self.chp_fraction.value) + + # subtract pumping power for net electricity and calculate first law efficiency + self.NetElectricityProduced.value = self.ElectricityProduced.value - model.wellbores.PumpingPower.value + self.FirstLawEfficiency.value = self.NetElectricityProduced.value/HeatExtractedTowardsElectricity + + # Calculate annual electricity, pum;ping, and heat production + self.HeatkWhExtracted.value, self.PumpingkWh.value, self.TotalkWhProduced.value, self.NetkWhProduced.value, self.HeatkWhProduced.value = \ + SurfacePlant.annual_electricity_pumping_power(self, self.plant_lifetime.value, self.enduse_option.value, + self.HeatExtracted.value, model.economics.timestepsperyear.value, self.utilization_factor.value, + model.wellbores.PumpingPower.value, self.ElectricityProduced.value, + self.NetElectricityProduced.value, self.HeatProduced.value) + + # calculate reservoir heat content + self.RemainingReservoirHeatContent.value = SurfacePlant.remaining_reservoir_heat_content( + self, model.reserv.InitialReservoirHeatContent.value, self.HeatkWhExtracted.value) + + model.logger.info("complete " + str(__class__) + ": " + sys._getframe().f_code.co_name) diff --git a/src/geophires_x/SurfacePlantSubcriticalORC.py b/src/geophires_x/SurfacePlantSubcriticalORC.py new file mode 100644 index 00000000..24f10853 --- /dev/null +++ b/src/geophires_x/SurfacePlantSubcriticalORC.py @@ -0,0 +1,136 @@ +from pathlib import Path +import numpy as np +from geophires_x.OptionList import EndUseOptions +from geophires_x.SurfacePlant import SurfacePlant +import geophires_x.Model as Model + + +class surface_plant_subcritical_orc(SurfacePlant): + def __init__(self, model: Model): + """ + The __init__ function is called automatically when a class is instantiated. + It initializes the attributes of an object, and sets default values for certain arguments that can be overridden + by user input. + The __init__ function is used to set up all the parameters in the Surfaceplant. + :param model: The container class of the application, giving access to everything else, including the logger + :type model: :class:`~geophires_x.Model.Model` + :return: None + """ + + model.logger.info("Init " + self.__class__.__name__ + ": " + __name__) + super().__init__(model) # Initialize all the parameters in the superclass + + # 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. + + # 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. + + # local variable initialization + sclass = self.__class__.__name__ + self.MyClass = sclass + self.MyPath = Path(__file__).resolve() + model.logger.info("Complete " + self.__class__.__name__ + ": " + __name__) + + def __str__(self): + return "SurfacePlantSubcriticalORC" + + def read_parameters(self, model:Model) -> None: + """ + The read_parameters function reads in the parameters from a dictionary and stores them in the parameters. + It also handles special cases that need to be handled after a value has been read in and checked. + If you choose to subclass this master class, you can also choose to override this method (or not), and if you do + :param model: The container class of the application, giving access to everything else, including the logger + :return: None + """ + model.logger.info("Init " + self.__class__.__name__ + ": " + __name__) + super().read_parameters(model) # Initialize all the parameters in the superclass + model.logger.info("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("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 power plant entering temperature + self.TenteringPP.value = SurfacePlant.power_plant_entering_temperature(self, self.enduse_option.value, + model.reserv.timevector.value, self.T_chp_bottom.value, model.wellbores.ProducedTemperature.value) + + # Availability water + self.Availability.value = SurfacePlant.availability_water(self, self.ambient_temperature.value, self.TenteringPP.value, self.ambient_temperature.value) + + # Subcritical ORC-specific values. + if self.ambient_temperature.value < 15.: + C21 = 0.0 + C11 = 2.746E-3 + C01 = -8.3806E-2 + D21 = 0.0 + D11 = 2.713E-3 + D01 = -9.1841E-2 + C22 = 0.0 + C12 = 0.0894 + C02 = 55.6 + D22 = 0.0 + D12 = 0.0894 + D02 = 62.6 + else: + C21 = 0.0 + C11 = 2.713E-3 + C01 = -9.1841E-2 + D21 = 0.0 + D11 = 2.676E-3 + D01 = -1.012E-1 + C22 = 0.0 + C12 = 0.0894 + C02 = 62.6 + D22 = 0.0 + D12 = 0.0894 + D02 = 69.6 + + model.wellbores.Tinj.value, ReinjTemp, etau = SurfacePlant.reinjection_temperature(self, model, + self.ambient_temperature.value, self.TenteringPP.value, model.wellbores.Tinj.value, + C01, C11, C21, D01, D11, D21, C02, C12, C22, D02, D12, D22) + + # calculate electricity & heat production + self.ElectricityProduced.value, self.HeatExtracted.value, self.HeatProduced.value, HeatExtractedTowardsElectricity = \ + SurfacePlant.electricity_heat_production(self, self.enduse_option.value, self.Availability.value, etau, + model.wellbores.nprod.value, model.wellbores.prodwellflowrate.value, + model.reserv.cpwater.value, model.wellbores.ProducedTemperature.value, + model.wellbores.Tinj.value, ReinjTemp, self.T_chp_bottom.value, + self.enduse_efficiency_factor.value, self.chp_fraction.value) + + # subtract pumping power for net electricity and calculate first law efficiency + self.NetElectricityProduced.value = self.ElectricityProduced.value - model.wellbores.PumpingPower.value + self.FirstLawEfficiency.value = self.NetElectricityProduced.value/HeatExtractedTowardsElectricity + + # Calculate annual electricity, pum;ping, and heat production + self.HeatkWhExtracted.value, self.PumpingkWh.value, self.TotalkWhProduced.value, self.NetkWhProduced.value, self.HeatkWhProduced.value = \ + SurfacePlant.annual_electricity_pumping_power(self, self.plant_lifetime.value, self.enduse_option.value, + self.HeatExtracted.value, model.economics.timestepsperyear.value, self.utilization_factor.value, + model.wellbores.PumpingPower.value, self.ElectricityProduced.value, + self.NetElectricityProduced.value, self.HeatProduced.value) + + # calculate reservoir heat content + self.RemainingReservoirHeatContent.value = SurfacePlant.remaining_reservoir_heat_content( + self, model.reserv.InitialReservoirHeatContent.value, self.HeatkWhExtracted.value) + + model.logger.info("complete " + self.__class__.__name__ + ": " + __name__) diff --git a/src/geophires_x/SurfacePlantSupercriticalORC.py b/src/geophires_x/SurfacePlantSupercriticalORC.py new file mode 100644 index 00000000..d8b9e736 --- /dev/null +++ b/src/geophires_x/SurfacePlantSupercriticalORC.py @@ -0,0 +1,133 @@ +from geophires_x.SurfacePlant import SurfacePlant +import geophires_x.Model as Model + + +class surface_plant_supercritical_orc(SurfacePlant): + def __init__(self, model: Model): + """ + The __init__ function is called automatically when a class is instantiated. + It initializes the attributes of an object, and sets default values for certain arguments that can be overridden + by user input. + The __init__ function is used to set up all the parameters in the Surfaceplant. + :param model: The container class of the application, giving access to everything else, including the logger + :type model: :class:`~geophires_x.Model.Model` + :return: None + """ + + model.logger.info("Init " + self.__class__.__name__ + ": " + __name__) + super().__init__(model) # Initialize all the parameters in the superclass + + # 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. + + # 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. + + # local variable initialization + sclass = self.__class__.__name__ + self.MyClass = sclass + self.MyPath = __file__ + model.logger.info("Complete " + self.__class__.__name__ + ": " + __name__) + + def __str__(self): + return "SurfacePlantSupercriticalORC" + + def read_parameters(self, model:Model) -> None: + """ + The read_parameters function reads in the parameters from a dictionary and stores them in the parameters. + It also handles special cases that need to be handled after a value has been read in and checked. + If you choose to subclass this master class, you can also choose to override this method (or not), and if you do + :param model: The container class of the application, giving access to everything else, including the logger + :return: None + """ + model.logger.info("Init " + self.__class__.__name__ + ": " + __name__) + super().read_parameters(model) # Initialize all the parameters in the superclass + model.logger.info("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("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 power plant entering temperature + self.TenteringPP.value = SurfacePlant.power_plant_entering_temperature(self, self.enduse_option.value, + model.reserv.timevector.value, self.T_chp_bottom.value, model.wellbores.ProducedTemperature.value) + + # Availability water + self.Availability.value = SurfacePlant.availability_water(self, self.ambient_temperature.value, self.TenteringPP.value, self.ambient_temperature.value) + + # Supercritical ORC-specific values + if self.ambient_temperature.value < 15.: + C21 = -1.55E-5 + C11 = 7.604E-3 + C01 = -3.78E-1 + D21 = -1.499E-5 + D11 = 7.4268E-3 + D01 = -3.7915E-1 + C22 = 0.0 + C12= 0.02 + C02 = 49.26 + D22 = 0.0 + D12 = 0.02 + D02 = 56.26 + else: + C21 = -1.499E-5 + C11 = 7.4268E-3 + C01 = -3.7915E-1 + D21 = -1.55E-5 + D11 = 7.55136E-3 + D01 = -4.041E-1 + C22 = 0.0 + C12 = 0.02 + C02 = 56.26 + D22 = 0.0 + D12 = 0.02 + D02 = 63.26 + + model.wellbores.Tinj.value, ReinjTemp, etau = SurfacePlant.reinjection_temperature(self, model, + self.ambient_temperature.value, self.TenteringPP.value, model.wellbores.Tinj.value, + C01, C11, C21, D01, D11, D21, C02, C12, C22, D02, D12, D22) + + # calculate electricity & heat production + self.ElectricityProduced.value, self.HeatExtracted.value, self.HeatProduced.value, HeatExtractedTowardsElectricity = \ + SurfacePlant.electricity_heat_production(self, self.enduse_option.value, self.Availability.value, etau, + model.wellbores.nprod.value, model.wellbores.prodwellflowrate.value, + model.reserv.cpwater.value, model.wellbores.ProducedTemperature.value, + model.wellbores.Tinj.value, ReinjTemp, self.T_chp_bottom.value, + self.enduse_efficiency_factor.value, self.chp_fraction.value) + + # subtract pumping power for net electricity and calculate first law efficiency + self.NetElectricityProduced.value = self.ElectricityProduced.value - model.wellbores.PumpingPower.value + self.FirstLawEfficiency.value = self.NetElectricityProduced.value/HeatExtractedTowardsElectricity + + # Calculate annual electricity, pum;ping, and heat production + self.HeatkWhExtracted.value, self.PumpingkWh.value, self.TotalkWhProduced.value, self.NetkWhProduced.value, self.HeatkWhProduced.value = \ + SurfacePlant.annual_electricity_pumping_power(self, self.plant_lifetime.value, self.enduse_option.value, + self.HeatExtracted.value, model.economics.timestepsperyear.value, self.utilization_factor.value, + model.wellbores.PumpingPower.value, self.ElectricityProduced.value, + self.NetElectricityProduced.value, self.HeatProduced.value) + + # calculate reservoir heat content + self.RemainingReservoirHeatContent.value = SurfacePlant.remaining_reservoir_heat_content( + self, model.reserv.InitialReservoirHeatContent.value, self.HeatkWhExtracted.value) + + model.logger.info("complete " + self.__class__.__name__ + ": " + __name__) diff --git a/src/geophires_x/TOUGH2Reservoir.py b/src/geophires_x/TOUGH2Reservoir.py index 16240ac9..44f77861 100644 --- a/src/geophires_x/TOUGH2Reservoir.py +++ b/src/geophires_x/TOUGH2Reservoir.py @@ -176,7 +176,7 @@ def Calculate(self, model:Model): f.write('START----1----*----2----*----3----*----4----*----5----*----6----*----7----*----8\n') f.write('PARAM----1-MOP* 123456789012345678901234----*----5----*----6----*----7----*----8\n') f.write(' 8 19999 5000000000001 03 000 0 \n') - f.write(' 0.0 %9.3E 5259490.0 0.0 9.81 4.0 1.0\n' % (model.surfaceplant.plantlifetime.value*365*24*3600)) + f.write(' 0.0 %9.3E 5259490.0 0.0 9.81 4.0 1.0\n' % (model.surfaceplant.plant_lifetime.value * 365 * 24 * 3600)) f.write(' 1.0E-5 1.0 1.0 1.0 \n') f.write(' 1000000.0 %10.1f\n' % initialtemp) f.write(' \n') diff --git a/src/geophires_x/UPPReservoir.py b/src/geophires_x/UPPReservoir.py index d34affd1..0ba0040d 100644 --- a/src/geophires_x/UPPReservoir.py +++ b/src/geophires_x/UPPReservoir.py @@ -89,15 +89,15 @@ def Calculate(self, model: Model): + model.reserv.filenamereservoiroutput.value+') and will abort simulation.') sys.exit() numlines = len(contentprodtemp) - if numlines != model.surfaceplant.plantlifetime.value*model.economics.timestepsperyear.value+1: + if numlines != model.surfaceplant.plant_lifetime.value*model.economics.timestepsperyear.value+1: model.logging.critical('Error: Reservoir output file (' + model.reserv.filenamereservoiroutput.value + ') does not have required ' + - str(model.surfaceplant.plantlifetime.value*model.economics.timestepsperyear.value+1) + + str(model.surfaceplant.plant_lifetime.value * model.economics.timestepsperyear.value + 1) + ' lines. GEOPHIRES will abort simulation.') print('Error: Reservoir output file (' + - model.reserv.filenamereservoiroutput.value+') does not have required ' + - str(model.surfaceplant.plantlifetime.value*model.economics.timestepsperyear.value+1) + + model.reserv.filenamereservoiroutput.value +') does not have required ' + + str(model.surfaceplant.plant_lifetime.value * model.economics.timestepsperyear.value + 1) + ' lines. GEOPHIRES will abort simulation.') sys.exit() for i in range(0, numlines-1): diff --git a/src/geophires_x/Units.py b/src/geophires_x/Units.py index c09a6357..32c37fe9 100644 --- a/src/geophires_x/Units.py +++ b/src/geophires_x/Units.py @@ -42,6 +42,8 @@ class Units(IntEnum): CO2PRODUCTION = auto() ENERGYPERCO2 = auto() POPDENSITY = auto() + HEATPERUNITAREA = auto() + POWERPERUNITAREA = auto() class TemperatureUnit(str, Enum): @@ -297,6 +299,17 @@ class MassUnit(str, Enum): LB = "pound" OZ = "ounce" + class PopDensityUnit(str,Enum): """Population Density Units""" perkm2 = "Population per square km" + + +class HeatPerUnitAreaUnit(str,Enum): + """Population Density Units""" + KJPERSQKM = "KJ/km**2" + + +class PowerPerUnitAreaUnit(str,Enum): + """Population Density Units""" + MWPERSQKM = "MW/km**2" diff --git a/src/geophires_x/WellBores.py b/src/geophires_x/WellBores.py index 4caf478e..1c8f63a2 100644 --- a/src/geophires_x/WellBores.py +++ b/src/geophires_x/WellBores.py @@ -441,7 +441,7 @@ def InjPressureDropAndPumpingPowerUsingIndexes(model: Model, usebuiltinhydrostat :type PumpingPowerProd: float :param II: injectivity index [kg/s/bar] :type II: float - :return: tuple of PumpingPower, PumpingPowerInj, DPInjWell, Pplantoutlet, Pprodwellhead [kPa] + :return: tuple of PumpingPower, PumpingPowerInj, DPInjWell, plant_outlet_pressure, Pprodwellhead [kPa] :rtype: tuple """ PumpingPowerInj = DPInjWell = Pprodwellhead = [0.0] # initialize value in case it doesn't get set. @@ -959,7 +959,7 @@ def Calculate(self, model: Model) -> None: model.reserv.rhorock.value, model.reserv.cprock.value, self.prodwelldiam.value, model.reserv.timevector.value, - model.surfaceplant.utilfactor.value, self.prodwellflowrate.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) @@ -1000,12 +1000,12 @@ def Calculate(self, model: Model) -> None: self.prodwelldiam.value, self.impedance.value, self.nprod.value, model.reserv.waterloss.value, - model.surfaceplant.pumpeff.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.pumpeff.value, + model.surfaceplant.pump_efficiency.value, self.DPOverall.value) else: # PI and II are used @@ -1018,8 +1018,8 @@ def Calculate(self, model: Model) -> None: model.reserv.averagegradient.value, self.ppwellhead.value, self.PI.value, self.prodwellflowrate.value, f3, vprod, self.prodwelldiam.value, self.nprod.value, - model.surfaceplant.pumpeff.value, self.rhowaterprod) - self.PumpingPower.value, self.PumpingPowerInj.value, self.DPInjWell.value, model.surfaceplant.Pplantoutlet.value, self.Pprodwellhead.value = \ + model.surfaceplant.pump_efficiency.value, self.rhowaterprod) + self.PumpingPower.value, self.PumpingPowerInj.value, self.DPInjWell.value, model.surfaceplant.plant_outlet_pressure.value, self.Pprodwellhead.value = \ InjPressureDropAndPumpingPowerUsingIndexes(model, self.usebuiltinhydrostaticpressurecorrelation, self.productionwellpumping.value, self.usebuiltinppwellheadcorrelation, @@ -1031,8 +1031,8 @@ def Calculate(self, model: Model) -> None: self.prodwellflowrate.value, f1, vinj, self.injwelldiam.value, self.nprod.value, self.ninj.value, model.reserv.waterloss.value, - model.surfaceplant.pumpeff.value, self.rhowaterinj, - model.surfaceplant.Pplantoutlet.value, + model.surfaceplant.pump_efficiency.value, self.rhowaterinj, + model.surfaceplant.plant_outlet_pressure.value, self.PumpingPowerProd.value) model.logger.info("complete " + str(__class__) + ": " + sys._getframe().f_code.co_name) diff --git a/src/geophires_x_client/geophires_input_parameters.py b/src/geophires_x_client/geophires_input_parameters.py index d0694f3b..99100798 100644 --- a/src/geophires_x_client/geophires_input_parameters.py +++ b/src/geophires_x_client/geophires_input_parameters.py @@ -40,7 +40,7 @@ def __init__(self, params: Optional[MappingProxyType] = None, from_file_path: Op if params is not None: self._params = dict(params) self._id = abs(hash(frozenset(self._params.items()))) - # TODO validate params - i.e. that all names are accepted by simulation, values don't exceed max allowed, + # TODO validate params - i.reservoir_enthalpy. that all names are accepted by simulation, values don't exceed max allowed, # etc. tmp_file_path = Path(tempfile.gettempdir(), f'geophires-input-params_{self._id}.txt') diff --git a/src/geophires_x_schema_generator/__init__.py b/src/geophires_x_schema_generator/__init__.py index d2a8b582..0c68bd8f 100644 --- a/src/geophires_x_schema_generator/__init__.py +++ b/src/geophires_x_schema_generator/__init__.py @@ -10,7 +10,7 @@ from geophires_x.Parameter import Parameter from geophires_x.SUTRAEconomics import SUTRAEconomics from geophires_x.SUTRAReservoir import SUTRAReservoir -from geophires_x.SUTRASurfacePlant import SUTRASurfacePlant +from geophires_x.SurfacePlantSUTRA import SUTRASurfacePlant from geophires_x.SUTRAWellBores import SUTRAWellBores diff --git a/src/hip_ra/HIP_RA.py b/src/hip_ra/HIP_RA.py index f7cc46ab..32435a0e 100755 --- a/src/hip_ra/HIP_RA.py +++ b/src/hip_ra/HIP_RA.py @@ -3,6 +3,8 @@ import logging import logging.config import os +import math +import numpy as np from geophires_x.GeoPHIRESUtils import read_input_file, EntropyH20_func, EnthalpyH20_func, DensityWater, \ HeatCapacityWater, RecoverableHeat, UtilEff_func @@ -22,6 +24,46 @@ """ +def rock_enthalpy_func(mass, specific_heat_capacity_kj, delta_temperature_k): + """ + Calculate the enthalpy change for a rock. + + Parameters: + - mass: Mass of the rock (in kg) + - specific_heat_capacity_kj: Specific heat capacity of the rock material (in kJ/(kg·K)) + - delta_temperature_celsius: Change in temperature (in degrees Celsius) + + Returns: + - Enthalpy change (in kJ/kg) + """ +# enthalpy_change = mass * specific_heat_capacity_kj * delta_temperature_celsius + enthalpy_change = (specific_heat_capacity_kj * delta_temperature_k)/ mass + return enthalpy_change + + +def rock_entropy_func(): + """ + Calculate the information entropy of a rock based on its composition. + + Parameters: + - composition: A dictionary representing the composition of the rock, + where keys are mineral names and values are their abundances. + + Returns: + - entropy: The information entropy of the rock. + """ + composition = {'quartz': 30, 'feldspar': 40, 'mica': 20, 'amphibole': 10} + total_abundance = sum(composition.values()) + + # Calculate the probability of each mineral in the rock + probabilities = [abundance / total_abundance for abundance in composition.values()] + + # Calculate the entropy using the Shannon entropy formula + entropy = -sum(p * math.log2(p) for p in probabilities if p > 0) + + return entropy + + class HIP_RA: """ HIP_RA is the container class of the HIP_RA application, giving access to everything else, including the logger @@ -62,7 +104,7 @@ def __init__(self, enable_geophires_logging_config=True): self.InputParameters = {} # dictionary to hold all the input parameter the user wants to change # inputs - self.ReservoirTemperature = self.ParameterDict[self.ReservoirTemperature.Name] = floatParameter( + self.reservoir_temperature = self.ParameterDict[self.reservoir_temperature.Name] = floatParameter( 'Reservoir Temperature', value=150.0, Min=50, @@ -74,7 +116,7 @@ def __init__(self, enable_geophires_logging_config=True): ErrMessage='assume default reservoir temperature (150 deg-C)', ToolTipText='Reservoir Temperature [150 dec-C]', ) - self.RejectionTemperature = self.ParameterDict[self.RejectionTemperature.Name] = floatParameter( + self.rejection_temperature = self.ParameterDict[self.rejection_temperature.Name] = floatParameter( 'Rejection Temperature', value=25.0, Min=0.1, @@ -86,8 +128,8 @@ def __init__(self, enable_geophires_logging_config=True): ErrMessage='assume default rejection temperature (25 deg-C)', ToolTipText='Rejection Temperature [25 dec-C]', ) - self.FormationPorosity = self.ParameterDict[self.FormationPorosity.Name] = floatParameter( - 'Formation Porosity', + self.reservoir_porosity = self.ParameterDict[self.reservoir_porosity.Name] = floatParameter( + 'Reservoir Porosity', value=18.0, Min=0.0, Max=100.0, @@ -95,10 +137,10 @@ def __init__(self, enable_geophires_logging_config=True): PreferredUnits=PercentUnit.PERCENT, CurrentUnits=PercentUnit.PERCENT, Required=True, - ErrMessage='assume default formation porosity (18%)', - ToolTipText='Formation Porosity [18%]', + ErrMessage='assume default reservoir porosity (18%)', + ToolTipText='Reservoir Porosity [18%]', ) - self.ReservoirArea = self.ParameterDict[self.ReservoirArea.Name] = floatParameter( + self.reservoir_area = self.ParameterDict[self.reservoir_area.Name] = floatParameter( 'Reservoir Area', value=81.0, Min=0.0, @@ -110,7 +152,7 @@ def __init__(self, enable_geophires_logging_config=True): ErrMessage='assume default reservoir area (81 km2)', ToolTipText='Reservoir Area [81 km2]', ) - self.ReservoirThickness = self.ParameterDict[self.ReservoirThickness.Name] = floatParameter( + self.reservoir_thickness = self.ParameterDict[self.reservoir_thickness.Name] = floatParameter( 'Reservoir Thickness', value=0.286, Min=0.0, @@ -122,7 +164,7 @@ def __init__(self, enable_geophires_logging_config=True): ErrMessage='assume default reservoir thickness (0.286 km2)', ToolTipText='Reservoir Thickness [0.286 km]', ) - self.ReservoirLifeCycle = self.ParameterDict[self.ReservoirLifeCycle.Name] = intParameter( + self.reservoir_life_cycle = self.ParameterDict[self.reservoir_life_cycle.Name] = intParameter( 'Reservoir Life Cycle', value=30, UnitType=Units.TIME, @@ -135,8 +177,8 @@ def __init__(self, enable_geophires_logging_config=True): ) # user-changeable semi-constants - self.ReservoirHeatCapacity = self.ParameterDict[self.ReservoirHeatCapacity.Name] = floatParameter( - 'Reservoir Heat Capacity', + self.rock_heat_capacity = self.ParameterDict[self.rock_heat_capacity.Name] = floatParameter( + 'Reservoir Rock Heat Capacity', value=2.84e12, Min=0.0, Max=1e14, @@ -147,8 +189,8 @@ def __init__(self, enable_geophires_logging_config=True): ErrMessage='assume default Reservoir Heat Capacity (2.84E+12 kJ/km3C)', ToolTipText='Reservoir Heat Capacity [2.84E+12 kJ/km3C]', ) - self.HeatCapacityOfWater = self.ParameterDict[self.HeatCapacityOfWater.Name] = floatParameter( - 'Heat Capacity Of Water', + self.fluid_heat_capacity = self.ParameterDict[self.fluid_heat_capacity.Name] = floatParameter( + 'Reservoir Fluid Heat Capacity', value=-1.0, Min=3.0, Max=10.0, @@ -159,8 +201,8 @@ def __init__(self, enable_geophires_logging_config=True): ErrMessage='calculate a value based on the water temperature', ToolTipText='Heat Capacity Of Water [4.18 kJ/kgC]', ) - self.DensityOfWater = self.ParameterDict[self.DensityOfWater.Name] = floatParameter( - 'Density Of Water', + self.fluid_density = self.ParameterDict[self.fluid_density.Name] = floatParameter( + 'Density Of Reservoir Fluid', value=-1.0, Min=1.000e11, Max=1.000e13, @@ -169,10 +211,10 @@ def __init__(self, enable_geophires_logging_config=True): CurrentUnits=DensityUnit.KGPERKILOMETERS3, Required=True, ErrMessage='calculate a value based on the water temperature', - ToolTipText='Heat Density Of Water [1.0E+12 kg/km3]', + ToolTipText='Density Of Water [1.0E+12 kg/km3]', ) - self.DensityOfRock = self.ParameterDict[self.DensityOfRock.Name] = floatParameter( - 'Density Of Rock', + self.rock_density = self.ParameterDict[self.rock_density.Name] = floatParameter( + 'Density Of Reservoir Rock', value=2.55e12, Min=1.000e11, Max=1.000e13, @@ -183,10 +225,10 @@ def __init__(self, enable_geophires_logging_config=True): ErrMessage='assume default Density Of Rock (2.55E+12 kg/km3)', ToolTipText='Heat Density Of Rock [2.55E+12 kg/km3]', ) - self.RecoverableHeat = self.ParameterDict[self.RecoverableHeat.Name] = floatParameter( - 'Recoverable Heat', + self.rock_recoverable_heat = self.ParameterDict[self.rock_recoverable_heat.Name] = floatParameter( + 'Rock Recoverable Heat', value=-1.0, - Min=0.001, + Min=0.0, Max=1.000, UnitType=Units.PERCENT, PreferredUnits=PercentUnit.TENTH, @@ -197,124 +239,244 @@ def __init__(self, enable_geophires_logging_config=True): ToolTipText='percent of heat that is recoverable from the rock in the reservoir 0.66 for high-T reservoirs,\ 0.43 for low-T reservoirs (Garg and Combs (2011)', ) - - # internal - self.WaterContent = self.ParameterDict[self.WaterContent.Name] = floatParameter( - 'Water Content', - value=18.0, - Min=0.0, - Max=100.0, + self.fluid_recoverable_heat = self.ParameterDict[self.fluid_recoverable_heat.Name] = floatParameter( + 'Fluid Recoverable Heat', + value=-1.0, + Min=0.00, + Max=1.000, UnitType=Units.PERCENT, - PreferredUnits=PercentUnit.PERCENT, - CurrentUnits=PercentUnit.PERCENT, - Required=True, - ErrMessage='assume default water content (18%)', - ToolTipText='Water Content', + PreferredUnits=PercentUnit.TENTH, + CurrentUnits=PercentUnit.TENTH, + Required=False, + ErrMessage='assume 0.66 for high-T reservoirs (>150C), 0.43 for low-T reservoirs \ + (>90, Garg and Combs (2011)', + ToolTipText='percent of heat that is recoverable from the fluid in the reservoir 0.66 for high-T reservoirs,\ + 0.43 for low-T reservoirs (Garg and Combs (2011)', ) - self.RockContent = self.ParameterDict[self.RockContent.Name] = floatParameter( - 'Rock Content', - value=82.0, - Min=0.0, - Max=100.0, + self.recoverable_fluid = self.ParameterDict[self.recoverable_fluid.Name] = floatParameter( + 'Recoverable Fluid Volume', + value=0.5, + Min=0.00, + Max=1.000, UnitType=Units.PERCENT, - PreferredUnits=PercentUnit.PERCENT, - CurrentUnits=PercentUnit.PERCENT, - Required=True, - ErrMessage='assume default rock content (82%)', - ToolTipText='Rock Content', - ) - self.RejectionTemperatureK = self.ParameterDict[self.RejectionTemperatureK.Name] = floatParameter( - 'Rejection Temperature in K', - value=298.15, - Min=0.1, - Max=1000.0, - UnitType=Units.TEMPERATURE, - PreferredUnits=TemperatureUnit.KELVIN, - CurrentUnits=TemperatureUnit.KELVIN, - Required=True, - ErrMessage='assume default rejection temperature in K (298.15 deg-K)', - ToolTipText='Rejection Temperature in K [298.15 deg-K]', + PreferredUnits=PercentUnit.TENTH, + CurrentUnits=PercentUnit.TENTH, + Required=False, + ErrMessage='assume 0.5 (50%) of fluid from the reservoir is recoverable', + ToolTipText='percent of fluid that is recoverable from the reservoir (0.5 = 50%)', ) - self.RejectionEntropy = self.ParameterDict[self.RejectionEntropy.Name] = floatParameter( - 'Rejection Entropy', - value=0.3670, - Min=0.0001, - Max=100.0, - UnitType=Units.ENTROPY, - PreferredUnits=EntropyUnit.KJPERKGK, - CurrentUnits=EntropyUnit.KJPERKGK, - Required=True, - ErrMessage='assume default Rejection Entropy (0.3670 kJ/kgK @25 deg-C)', - ToolTipText='Rejection Entropy [0.3670 kJ/kgK @25 deg-C]', - ) - self.RejectionEnthalpy = self.ParameterDict[self.RejectionEnthalpy.Name] = floatParameter( - 'Rejection Enthalpy', - value=104.8, - Min=0.0001, - Max=1000.0, - UnitType=Units.ENTHALPY, - PreferredUnits=EnthalpyUnit.KJPERKG, - CurrentUnits=EnthalpyUnit.KJPERKG, - Required=True, - ErrMessage='assume default Rejection Enthalpy (104.8 kJ/kg @25 deg-C)', - ToolTipText='Rejection Enthalpy [104.8 kJ/kg @25 deg-C]', + self.recoverable_rock_heat = self.ParameterDict[self.recoverable_rock_heat.Name] = floatParameter( + 'Recoverable Heat from Rock', + value=0.75, + Min=0.00, + Max=1.000, + UnitType=Units.PERCENT, + PreferredUnits=PercentUnit.TENTH, + CurrentUnits=PercentUnit.TENTH, + Required=False, + ErrMessage='assume 0.75 (75%) of fluid from the reservoir is recoverable', + ToolTipText='percent of fluid that is recoverable from the reservoir (0.75 = 75%)', ) - # Outputs - self.V = self.OutputParameterDict[self.V.Name] = OutputParameter( - Name='Reservoir Volume', + # Output parameters + self.reservoir_volume = self.OutputParameterDict[self.reservoir_volume.Name] = OutputParameter( + Name='Reservoir Volume (reservoir)', + UnitType=Units.VOLUME, + PreferredUnits=VolumeUnit.KILOMETERS3, + CurrentUnits=VolumeUnit.KILOMETERS3, + ) + self.volume_rock = self.OutputParameterDict[self.volume_rock.Name] = OutputParameter( + Name='Reservoir Volume (rock)', + UnitType=Units.VOLUME, + PreferredUnits=VolumeUnit.KILOMETERS3, + CurrentUnits=VolumeUnit.KILOMETERS3, + ) + self.volume_fluid = self.OutputParameterDict[self.volume_fluid.Name] = OutputParameter( + Name='Reservoir Volume (fluid)', UnitType=Units.VOLUME, PreferredUnits=VolumeUnit.KILOMETERS3, CurrentUnits=VolumeUnit.KILOMETERS3, ) - self.qR = self.OutputParameterDict[self.qR.Name] = OutputParameter( - Name='Stored Heat', + self.reservoir_stored_heat = self.OutputParameterDict[self.reservoir_stored_heat.Name] = OutputParameter( + Name='Stored Heat (reservoir)', + UnitType=Units.HEAT, + PreferredUnits=HeatUnit.KJ, + CurrentUnits=HeatUnit.KJ + ) + self.stored_heat_rock = self.OutputParameterDict[self.stored_heat_rock.Name] = OutputParameter( + Name='Stored Heat (rock)', + UnitType=Units.HEAT, + PreferredUnits=HeatUnit.KJ, + CurrentUnits=HeatUnit.KJ + ) + self.stored_heat_fluid = self.OutputParameterDict[self.stored_heat_fluid.Name] = OutputParameter( + Name='Stored Heat (fluid)', UnitType=Units.HEAT, PreferredUnits=HeatUnit.KJ, CurrentUnits=HeatUnit.KJ ) - self.mWH = self.OutputParameterDict[self.mWH.Name] = OutputParameter( - Name='Fluid Produced', + self.reservoir_mass = self.OutputParameterDict[self.reservoir_mass.Name] = OutputParameter( + Name='Mass of Reservoir (total)', UnitType=Units.MASS, PreferredUnits=MassUnit.KILOGRAM, CurrentUnits=MassUnit.KILOGRAM ) - self.e = self.OutputParameterDict[self.e.Name] = OutputParameter( - Name='Enthalpy', + self.mass_rock = self.OutputParameterDict[self.mass_rock.Name] = OutputParameter( + Name='Mass of Reservoir (rock)', + UnitType=Units.MASS, + PreferredUnits=MassUnit.KILOGRAM, + CurrentUnits=MassUnit.KILOGRAM + ) + self.mass_fluid = self.OutputParameterDict[self.mass_fluid.Name] = OutputParameter( + Name='Mass of Reservoir (fluid)', + UnitType=Units.MASS, + PreferredUnits=MassUnit.KILOGRAM, + CurrentUnits=MassUnit.KILOGRAM + ) + self.reservoir_enthalpy = self.OutputParameterDict[self.reservoir_enthalpy.Name] = OutputParameter( + Name='Enthalpy (reservoir)', + UnitType=Units.ENTHALPY, + PreferredUnits=EnthalpyUnit.KJPERKG, + CurrentUnits=EnthalpyUnit.KJPERKG, + ) + self.enthalpy_rock = self.OutputParameterDict[self.enthalpy_rock.Name] = OutputParameter( + Name='Enthalpy (rock)', UnitType=Units.ENTHALPY, PreferredUnits=EnthalpyUnit.KJPERKG, CurrentUnits=EnthalpyUnit.KJPERKG, ) - self.qWH = self.OutputParameterDict[self.qWH.Name] = OutputParameter( - Name='Wellhead Heat', + self.enthalpy_fluid = self.OutputParameterDict[self.enthalpy_fluid.Name] = OutputParameter( + Name='Enthalpy (fluid)', + UnitType=Units.ENTHALPY, + PreferredUnits=EnthalpyUnit.KJPERKG, + CurrentUnits=EnthalpyUnit.KJPERKG, + ) + self.wellhead_heat = self.OutputParameterDict[self.wellhead_heat.Name] = OutputParameter( + Name='Wellhead Heat (reservoir)', UnitType=Units.HEAT, PreferredUnits=HeatUnit.KJ, CurrentUnits=HeatUnit.KJ ) - self.Rg = self.OutputParameterDict[self.Rg.Name] = OutputParameter( - Name='Recovery Factor', + self.wellhead_heat_recovery_rock = self.OutputParameterDict[self.wellhead_heat_recovery_rock.Name] = OutputParameter( + Name='Wellhead Heat (rock)', + UnitType=Units.HEAT, + PreferredUnits=HeatUnit.KJ, + CurrentUnits=HeatUnit.KJ + ) + self.wellhead_heat_recovery_fluid = self.OutputParameterDict[self.wellhead_heat_recovery_fluid.Name] = OutputParameter( + Name='Wellhead Heat (fluid)', + UnitType=Units.HEAT, + PreferredUnits=HeatUnit.KJ, + CurrentUnits=HeatUnit.KJ + ) + self.reservoir_recovery_factor = self.OutputParameterDict[self.reservoir_recovery_factor.Name] = OutputParameter( + Name='Recovery Factor (reservoir)', UnitType=Units.PERCENT, PreferredUnits=PercentUnit.PERCENT, CurrentUnits=PercentUnit.PERCENT, ) - self.WA = self.OutputParameterDict[self.WA.Name] = OutputParameter( - Name='Available Heat', + self.recovery_factor_rock = self.OutputParameterDict[self.recovery_factor_rock.Name] = OutputParameter( + Name='Recovery Factor (rock)', + UnitType=Units.PERCENT, + PreferredUnits=PercentUnit.PERCENT, + CurrentUnits=PercentUnit.PERCENT, + ) + self.recovery_factor_fluid = self.OutputParameterDict[self.recovery_factor_fluid.Name] = OutputParameter( + Name='Recovery Factor (fluid)', + UnitType=Units.PERCENT, + PreferredUnits=PercentUnit.PERCENT, + CurrentUnits=PercentUnit.PERCENT, + ) + self.reservoir_available_heat = self.OutputParameterDict[self.reservoir_available_heat.Name] = OutputParameter( + Name='Available Heat (reservoir)', + UnitType=Units.HEAT, + PreferredUnits=HeatUnit.KJ, + CurrentUnits=HeatUnit.KJ + ) + self.available_heat_rock = self.OutputParameterDict[self.available_heat_rock.Name] = OutputParameter( + Name='Available Heat (rock)', + UnitType=Units.HEAT, + PreferredUnits=HeatUnit.KJ, + CurrentUnits=HeatUnit.KJ + ) + self.available_heat_fluid = self.OutputParameterDict[self.available_heat_fluid.Name] = OutputParameter( + Name='Available Heat (fluid)', + UnitType=Units.HEAT, + PreferredUnits=HeatUnit.KJ, + CurrentUnits=HeatUnit.KJ + ) + self.reservoir_producible_heat = self.OutputParameterDict[self.reservoir_producible_heat.Name] = OutputParameter( + Name='Producible Heat (reservoir)', UnitType=Units.HEAT, PreferredUnits=HeatUnit.KJ, CurrentUnits=HeatUnit.KJ ) - self.WE = self.OutputParameterDict[self.WE.Name] = OutputParameter( - Name='Producible Heat', + self.producible_heat_rock = self.OutputParameterDict[self.producible_heat_rock.Name] = OutputParameter( + Name='Producible Heat (rock)', UnitType=Units.HEAT, PreferredUnits=HeatUnit.KJ, CurrentUnits=HeatUnit.KJ ) - self.We = self.OutputParameterDict[self.We.Name] = OutputParameter( - Name='Producible Electricity', + self.producible_heat_fluid = self.OutputParameterDict[self.producible_heat_fluid.Name] = OutputParameter( + Name='Producible Heat (fluid)', + UnitType=Units.HEAT, + PreferredUnits=HeatUnit.KJ, + CurrentUnits=HeatUnit.KJ + ) + self.reservoir_producible_electricity = self.OutputParameterDict[self.reservoir_producible_electricity.Name] = OutputParameter( + Name='Producible Electricity (reservoir)', UnitType=Units.POWER, PreferredUnits=PowerUnit.MW, CurrentUnits=PowerUnit.MW ) + self.producible_electricity_rock = self.OutputParameterDict[self.producible_electricity_rock.Name] = OutputParameter( + Name='Producible Electricity (rock)', + UnitType=Units.POWER, + PreferredUnits=PowerUnit.MW, + CurrentUnits=PowerUnit.MW + ) + self.producible_electricity_fluid = self.OutputParameterDict[self.producible_electricity_fluid.Name] = OutputParameter( + Name='Producible Electricity (fluid)', + UnitType=Units.POWER, + PreferredUnits=PowerUnit.MW, + CurrentUnits=PowerUnit.MW + ) + self.producible_heat_per_unit_area = self.OutputParameterDict[self.producible_heat_per_unit_area.Name] = OutputParameter( + Name='Producible Heat/Unit Area (reservoir)', + UnitType=Units.HEATPERUNITAREA, + PreferredUnits=HeatPerUnitAreaUnit.KJPERSQKM, + CurrentUnits=HeatPerUnitAreaUnit.KJPERSQKM + ) + self.heat_per_unit_area_rock = self.OutputParameterDict[self.heat_per_unit_area_rock.Name] = OutputParameter( + Name='Producible Heat/Unit Area (rock)', + UnitType=Units.HEATPERUNITAREA, + PreferredUnits=HeatPerUnitAreaUnit.KJPERSQKM, + CurrentUnits=HeatPerUnitAreaUnit.KJPERSQKM + ) + self.heat_per_unit_area_fluid = self.OutputParameterDict[self.heat_per_unit_area_fluid.Name] = OutputParameter( + Name='Producible Heat/Unit Area (fluid)', + UnitType=Units.HEATPERUNITAREA, + PreferredUnits=HeatPerUnitAreaUnit.KJPERSQKM, + CurrentUnits=HeatPerUnitAreaUnit.KJPERSQKM + ) + self.producible_electricity_per_unit_area = self.OutputParameterDict[self.producible_electricity_per_unit_area.Name] = OutputParameter( + Name='Producible Electricity/Unit Area (reservoir)', + UnitType=Units.POWERPERUNITAREA, + PreferredUnits=PowerPerUnitAreaUnit.MWPERSQKM, + CurrentUnits=PowerPerUnitAreaUnit.MWPERSQKM + ) + self.electricity_per_unit_area_rock = self.OutputParameterDict[self.electricity_per_unit_area_rock.Name] = OutputParameter( + Name='Producible Electricity/Unit Area (rock)', + UnitType=Units.POWERPERUNITAREA, + PreferredUnits=PowerPerUnitAreaUnit.MWPERSQKM, + CurrentUnits=PowerPerUnitAreaUnit.MWPERSQKM + ) + self.electricity_per_unit_area_fluid = self.OutputParameterDict[self.electricity_per_unit_area_fluid.Name] = OutputParameter( + Name='Producible Electricity/Unit Area (fluid)', + UnitType=Units.POWERPERUNITAREA, + PreferredUnits=PowerPerUnitAreaUnit.MWPERSQKM, + CurrentUnits=PowerPerUnitAreaUnit.MWPERSQKM + ) self.logger.info(f'Complete {__class__.__name__!s}: {__name__}') @@ -360,33 +522,6 @@ def read_parameters(self) -> None: ParameterToModify.CurrentUnits = ParameterToModify.PreferredUnits ReadParameter(ParameterReadIn, ParameterToModify, self) # this should handle all the non-special cases - # handle special cases - if ParameterToModify.Name == 'Formation Porosity': - self.WaterContent.value = ParameterToModify.value - self.RockContent = 100.0 - ParameterToModify.value - - elif ParameterToModify.Name == 'Rejection Temperature': - self.RejectionTemperatureK.value = 273.15 + ParameterToModify.value - self.RejectionEntropy.value = EntropyH20_func(ParameterToModify.value) - self.RejectionEnthalpy.value = EnthalpyH20_func(ParameterToModify.value) - - elif ParameterToModify.Name == 'Density Of Water': - value = float(ParameterReadIn.sValue) - if value < 0: # if the user supplied -1 as the density, they want us to calculate it. - ParameterToModify.value = DensityWater(self.ReservoirTemperature.value) * 1_000_000_000.0 - self.DensityOfWater.value = ParameterToModify.value - - elif ParameterToModify.Name == 'Heat Capacity Of Water': - value = float(ParameterReadIn.sValue) - if value < 0: # if the user supplied -1 as the capacity, they want us to calculate it. - ParameterToModify.value = HeatCapacityWater(self.ReservoirTemperature.value) / 1000.0 - self.HeatCapacityOfWater.value = ParameterToModify.value - - elif ParameterToModify.Name == 'Recoverable Heat': - value = float(ParameterReadIn.sValue) - if value < 0: # if the user supplied -1 as the Recoverable Heat, they want us to calculate it. - ParameterToModify.value = RecoverableHeat(self.RecoverableHeat.value, self.ReservoirTemperature.value) - self.RecoverableHeat.value = ParameterToModify.value else: self.logger.info('No parameters read because no content provided') @@ -411,42 +546,107 @@ def Calculate(self): self.logger.info(f'Init {__class__!s}: {__class__.__name__!s}: {__name__}') try: - # This is where all the calculations are made using all the values that have been set. - # first, make sure that density and heat capacity of water are set - if self.DensityOfWater.value < self.DensityOfWater.Min: - self.DensityOfWater.value = DensityWater(self.ReservoirTemperature.value) * 1_000_000_000.0 - if self.HeatCapacityOfWater.value < self.HeatCapacityOfWater.Min: - self.HeatCapacityOfWater.value = HeatCapacityWater(self.ReservoirTemperature.value) / 1000.0 - - # now do the volume calculation - self.V.value = self.ReservoirArea.value * self.ReservoirThickness.value - - # calculate the stored heat in the reservoir - self.qR.value = self.V.value * ( self.ReservoirHeatCapacity.value * (self.ReservoirTemperature.value - self.RejectionTemperature.value)) - - # calculate the mass of the fluid in the reservoir - # TODO: this is wrong, it should be the mass of the producible fluid, not the total mass of all the fluid in the reservoir - self.mWH.value = (self.V.value * (self.FormationPorosity.value / 100.0)) * self.DensityOfWater.value - - # calculate the maximum energy out per unit of mass (equation 7 of Garg & Combs (2011)) - self.e.value = ((EnthalpyH20_func(self.ReservoirTemperature.value) - self.RejectionEnthalpy.value) - - (self.RejectionTemperatureK.value - * (EntropyH20_func(self.ReservoirTemperature.value) - self.RejectionEntropy.value))) - - # calculate the heat recovery at the wellhead - self.qWH.value = self.mWH.value * (EnthalpyH20_func(self.ReservoirTemperature.value) - self.RejectionTemperatureK.value) - - # calculate the recovery factor - self.Rg.value = self.qWH.value / self.qR.value + # calculate the volume of rock and fluid in the reservoir + # note that we can't recover all the fluid from the reservoir, so we multiply times the recoverable fluid factor + 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)) + self.volume_fluid.value = self.reservoir_volume.value * (self.reservoir_porosity.value / 100.0) * self.recoverable_fluid.value + + # calculate the mass of the rock and the fluid in the reservoir + if self.fluid_density.value < self.fluid_density.Min: + self.fluid_density.value = DensityWater(self.reservoir_temperature.value) * 1_000_000_000.0 # converted to kj/km3 + self.mass_rock.value = self.volume_rock.value * self.rock_density.value + self.mass_fluid.value = self.volume_fluid.value * self.fluid_density.value + self.reservoir_mass.value = self.mass_rock.value + self.mass_fluid.value + + # do all the simple calculations and look-ups only once + if self.fluid_heat_capacity.value < self.fluid_heat_capacity.Min: + self.fluid_heat_capacity.value = HeatCapacityWater(self.reservoir_temperature.value) / 1000.0 # converted to kJ/kg-K + rejection_temperature_k = 273.15 + self.rejection_temperature.value + reservoir_temperature_k = 273.15 + self.reservoir_temperature.value + delta_temperature = self.reservoir_temperature.value - self.rejection_temperature.value + delta_temperature_k = reservoir_temperature_k - rejection_temperature_k + #rejection_entropy = EntropyH20_func(self.rejection_temperature.value) + # rejection_enthalpy = EnthalpyH20_func(self.rejection_temperature.value) + fluid_net_enthalpy = EnthalpyH20_func(delta_temperature_k) + fluid_net_entropy = EntropyH20_func(delta_temperature_k) + rock_net_enthalpy = rock_enthalpy_func(self.mass_rock.value, self.rock_heat_capacity.value, delta_temperature_k) + rock_net_entropy = rock_entropy_func() + + # calculate the stored heat of the rock and the fluid in the reservoir (in kJ) + # note that the rock stored heat is a function of the volume, so we multiple times the volume of the rock (in km3) + # and the fluid stored heat is a function of the mass of the fluid, so we multiply times the mass of the fluid (in kg) + # Also note that we can't recover all the heat from the rock, so we multiply times the recoverable rock heat factor + self.stored_heat_rock.value = self.volume_rock.value * self.rock_heat_capacity.value * delta_temperature_k + self.stored_heat_rock.value = self.stored_heat_rock.value * self.recoverable_rock_heat.value + self.stored_heat_fluid.value = self.mass_fluid.value * self.fluid_heat_capacity.value * delta_temperature_k + self.reservoir_stored_heat.value = self.stored_heat_rock.value + self.stored_heat_fluid.value + + # calculate the maximum energy out per unit of mass (in kJ/kg) +# self.enthalpy_rock.value = fluid_net_enthalpy - (delta_temperature_k * rock_net_entropy) + self.enthalpy_rock.value = rock_net_enthalpy - (delta_temperature_k * rock_net_entropy) + self.enthalpy_fluid.value = fluid_net_enthalpy - (delta_temperature_k * fluid_net_entropy) + #self.enthalpy_rock.value = self.stored_heat_rock.value/self.mass_rock.value + #self.enthalpy_fluid.value = self.stored_heat_fluid.value/self.mass_fluid.value + self.reservoir_enthalpy.value = self.enthalpy_rock.value + self.enthalpy_fluid.value + + # calculate the heat recovery at the wellhead (in kJ) + # this assume negligible heat loss as the heat is transferred to the surface (i.e., no heat loss in the well) + # self.wellhead_heat_recovery_rock.value = self.mass_rock.value * rock_net_enthalpy + # self.wellhead_heat_recovery_fluid.value = self.mass_fluid.value * fluid_net_enthalpy + # rockx = self.mass_rock.value * self.enthalpy_rock.value + # fluidx = self.mass_fluid.value * self.enthalpy_fluid.value + self.wellhead_heat_recovery_rock.value = self.stored_heat_rock.value + self.wellhead_heat_recovery_fluid.value = self.stored_heat_fluid.value + self.wellhead_heat.value = self.wellhead_heat_recovery_rock.value + self.wellhead_heat_recovery_fluid.value + + # calculate the Recoverable heat: if the user supplied -1 as the Recoverable Heat, they want us to calculate it. + if self.rock_recoverable_heat.value < self.rock_recoverable_heat.Min: + self.rock_recoverable_heat.value = RecoverableHeat(self.reservoir_temperature.value) + if self.fluid_recoverable_heat.value < self.fluid_recoverable_heat.Min: + self.fluid_recoverable_heat.value = RecoverableHeat(self.reservoir_temperature.value) # calculate the available heat - self.WA.value = (self.mWH.value * self.e.value * self.Rg.value * RecoverableHeat(self.RecoverableHeat.value, self.ReservoirTemperature.value)) + self.available_heat_rock.value = self.mass_rock.value * self.enthalpy_rock.value * self.rock_recoverable_heat.value + self.available_heat_fluid.value = self.mass_fluid.value * self.enthalpy_fluid.value * self.fluid_recoverable_heat.value + self.reservoir_available_heat.value = self.available_heat_rock.value + self.available_heat_fluid.value - # calculate the producible heat given the utilization efficiency - self.WE.value = self.WA.value * UtilEff_func(self.ReservoirTemperature.value) + # calculate the producible heat given the utilization efficiency of producing electricity at that temperature + # This uses a function from Garg and Coombs that assumes ORC for low temperature and flash for high temperature + utilization_effectiveness = UtilEff_func(self.reservoir_temperature.value) + self.producible_heat_rock.value = self.available_heat_rock.value * utilization_effectiveness + self.producible_heat_fluid.value = self.available_heat_fluid.value * utilization_effectiveness + self.reservoir_producible_heat.value = self.producible_heat_rock.value + self.producible_heat_fluid.value - # calculate the producible electricity - self.We.value = (self.WE.value / 3_600_000) / 8_760 # convert Kilojoules of heat to MWe of electricity + # calculate the recovery factor + self.recovery_factor_rock.value = self.producible_heat_rock.value / self.stored_heat_rock.value + self.recovery_factor_fluid.value = self.producible_heat_fluid.value / self.stored_heat_fluid.value + self.reservoir_recovery_factor.value = self.reservoir_producible_heat.value / self.reservoir_stored_heat.value + + # calculate the producible heat per unit area + self.heat_per_unit_area_rock.value = self.producible_heat_rock.value / self.reservoir_area.value + self.heat_per_unit_area_fluid.value = self.producible_heat_fluid.value / self.reservoir_area.value + self.producible_heat_per_unit_area.value = self.reservoir_producible_heat.value / self.reservoir_area.value + + # calculate the producible electricity by converting Kilojoules of heat to MWe of electricity + # kJ_to_Mwe = 3.156e+7 #seconds in a year + # self.We.value = (self.WE.value / 3_600_000) / 8_760 # convert Kilojoules of heat to MWe of electricity + self.producible_electricity_rock.value = (self.producible_heat_rock.value / 3_600_000) / 8_760 + self.producible_electricity_fluid.value = (self.producible_heat_fluid.value / 3_600_000) / 8_760 +# self.producible_electricity_fluid.value = self.producible_heat_fluid.value / kJ_to_Mwe + self.reservoir_producible_electricity.value = self.producible_electricity_rock.value + self.producible_electricity_fluid.value + + # calculate the producible electricity by converting Kilojoules of heat to MWh of electricity +# kJ_to_Mwhe = 2.7778E-7 +# self.producible_electricity_rock.value = self.producible_heat_rock.value * kJ_to_Mwhe +# self.producible_electricity_fluid.value = self.producible_heat_fluid.value * kJ_to_Mwhe +# self.reservoir_producible_electricity.value = self.producible_electricity_rock.value + self.producible_electricity_fluid.value + #self.reservoir_producible_electricity.value = (self.reservoir_producible_heat.value / 3_600_000) / 8_760 # convert Kilojoules of heat to MWe of electricity + + # calculate the producible electricity per unit area + self.electricity_per_unit_area_rock.value = self.producible_electricity_rock.value / self.reservoir_area.value + self.electricity_per_unit_area_fluid.value = self.producible_electricity_fluid.value / self.reservoir_area.value + self.producible_electricity_per_unit_area.value = self.reservoir_producible_electricity.value / self.reservoir_area.value self.logger.info(f'Complete {__class__!s}: {__class__.__name__!s}: {__name__}') except Exception as e: @@ -462,7 +662,7 @@ def PrintOutputs(self): # Deal with converting Units back to PreferredUnits, if required. # before we write the outputs, we go through all the parameters for all the objects and set the values back # to the units that the user entered the data in - # We do this because the value may be displayed in the output, and we want the user to recognize their value, + # reservoir_producible_electricity do this because the value may be displayed in the output, and we want the user to recognize their value, # not some converted value for key in self.ParameterDict: param = self.ParameterDict[key] @@ -471,7 +671,7 @@ def PrintOutputs(self): # now we need to loop through all the output parameters to update their units to whatever # units the user has specified. - # i.e., they may have specified that all LENGTH results must be in feet, so we need to convert those from + # i.reservoir_enthalpy., they may have specified that all LENGTH results must be in feet, so we need to convert those from # whatever LENGTH unit they are to feet. # same for all the other classes of units (TEMPERATURE, DENSITY, etc). for key in self.OutputParameterDict: @@ -493,16 +693,40 @@ def render_scientific(p: floatParameter | OutputParameter) -> str: summary_of_results = {} for param, render in [ - (self.ReservoirTemperature, render_default), - (self.V, render_default), - (self.qR, render_scientific), - (self.mWH, render_scientific), - (self.e, render_default), - (self.qWH, render_scientific), - (self.Rg, lambda rg: f'{(100 * rg.value):10.2f} {rg.CurrentUnits.value}'), - (self.WA, render_scientific), - (self.WE, render_scientific), - (self.We, render_default),]: + (self.reservoir_temperature, render_default), + (self.reservoir_volume, render_default), + (self.volume_rock, render_default), + (self.volume_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_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_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_area_rock, render_default), + (self.electricity_per_unit_area_fluid, render_default),]: summary_of_results[param.Name] = render(param) case_data = {'SUMMARY OF RESULTS': summary_of_results} diff --git a/tests/HIP_RA_Unit_tests.py b/tests/HIP_RA_Unit_tests.py index 5776efcd..939429a0 100644 --- a/tests/HIP_RA_Unit_tests.py +++ b/tests/HIP_RA_Unit_tests.py @@ -655,14 +655,14 @@ def test_reading_input_parameters_from_file_and_updating_attributes(self): hip_ra.read_parameters() assert hip_ra.reservoir_temperature.value == 150.0 assert hip_ra.rejection_temperature.value == 25.0 - assert hip_ra.formation_porosity.value == 18.0 + assert hip_ra.reservoir_porosity.value == 18.0 assert hip_ra.reservoir_area.value == 81.0 assert hip_ra.reservoir_thickness.value == 0.286 assert hip_ra.reservoir_life_cycle.value == 30 - assert hip_ra.reservoir_heat_capacity.value == 2840000000000.0 - assert hip_ra.water_heat_capacity.value == -1.0 + assert hip_ra.rock_heat_capacity.value == 2840000000000.0 + assert hip_ra.fluid_heat_capacity.value == -1.0 assert hip_ra.HeatCapacityOfRock.value == 1.0 - assert hip_ra.water_density.value == -1.0 + assert hip_ra.fluid_density.value == -1.0 assert hip_ra.rock_density.value == 2550000000000.0 assert hip_ra.RecoverableHeat.value == -1.0 assert hip_ra.WaterContent.value == 18.0 @@ -676,15 +676,15 @@ def test_calculating_output_parameters_based_on_input_parameters(self): hip_ra = HIP_RA() hip_ra.read_parameters() hip_ra.Calculate() - assert hip_ra.V.value == pytest.approx(23.166, abs=1e-3) - assert hip_ra.qR.value == pytest.approx(1.034e+14, abs=1e+10) - assert hip_ra.mWH.value == pytest.approx(4.159e+13, abs=1e+10) - assert hip_ra.e.value == pytest.approx(0.0, abs=1e-3) - assert hip_ra.qWH.value == pytest.approx(0.0, abs=1e-3) - assert hip_ra.Rg.value == pytest.approx(0.0, abs=1e-3) - assert hip_ra.WA.value == pytest.approx(0.0, abs=1e-3) - assert hip_ra.WE.value == pytest.approx(0.0, abs=1e-3) - assert hip_ra.We.value == pytest.approx(0.0, abs=1e-3) + assert hip_ra.reservoir_volume.value == pytest.approx(23.166, abs=1e-3) + assert hip_ra.reservoir_stored_heat.value == pytest.approx(1.034e+14, abs=1e+10) + assert hip_ra.reservoir_mass.value == pytest.approx(4.159e+13, abs=1e+10) + assert hip_ra.reservoir_enthalpy.value == pytest.approx(0.0, abs=1e-3) + assert hip_ra.wellhead_heat.value == pytest.approx(0.0, abs=1e-3) + assert hip_ra.reservoir_recovery_factor.value == pytest.approx(0.0, abs=1e-3) + assert hip_ra.reservoir_available_heat.value == pytest.approx(0.0, abs=1e-3) + assert hip_ra.reservoir_producible_heat.value == pytest.approx(0.0, abs=1e-3) + assert hip_ra.reservoir_producible_electricity.value == pytest.approx(0.0, abs=1e-3) # The class prints the output parameters to a file. def test_printing_output_parameters_to_file(self): @@ -715,14 +715,14 @@ def test_handling_no_parameters_in_input_file(self): hip_ra.read_parameters() assert hip_ra.reservoir_temperature.value == 150.0 assert hip_ra.rejection_temperature.value == 25.0 - assert hip_ra.formation_porosity.value == 18.0 + assert hip_ra.reservoir_porosity.value == 18.0 assert hip_ra.reservoir_area.value == 81.0 assert hip_ra.reservoir_thickness.value == 0.286 assert hip_ra.reservoir_life_cycle.value == 30 - assert hip_ra.reservoir_heat_capacity.value == 2840000000000.0 - assert hip_ra.water_heat_capacity.value == -1.0 + assert hip_ra.rock_heat_capacity.value == 2840000000000.0 + assert hip_ra.fluid_heat_capacity.value == -1.0 assert hip_ra.HeatCapacityOfRock.value == 1.0 - assert hip_ra.water_density.value == -1.0 + assert hip_ra.fluid_density.value == -1.0 assert hip_ra.rock_density.value == 2550000000000.0 assert hip_ra.RecoverableHeat.value == -1.0 assert hip_ra.WaterContent.value == 18.0 @@ -767,15 +767,15 @@ def test_handling_output_parameter_units_not_matching_preferred_units(self): hip_ra = HIP_RA() hip_ra.read_parameters() hip_ra.Calculate() - hip_ra.V.CurrentUnits = VolumeUnit.METERS3 - hip_ra.qR.CurrentUnits = HeatUnit.J - hip_ra.mWH.CurrentUnits = MassUnit.GRAM - hip_ra.e.CurrentUnits = EnthalpyUnit.KJPERKG - hip_ra.qWH.CurrentUnits = HeatUnit.J - hip_ra.Rg.CurrentUnits = PercentUnit.PERCENT - hip_ra.WA.CurrentUnits = HeatUnit.J - hip_ra.WE.CurrentUnits = HeatUnit.J - hip_ra.We.CurrentUnits = PowerUnit.W + hip_ra.reservoir_volume.CurrentUnits = VolumeUnit.METERS3 + hip_ra.reservoir_stored_heat.CurrentUnits = HeatUnit.J + hip_ra.reservoir_mass.CurrentUnits = MassUnit.GRAM + hip_ra.reservoir_enthalpy.CurrentUnits = EnthalpyUnit.KJPERKG + hip_ra.wellhead_heat.CurrentUnits = HeatUnit.J + hip_ra.reservoir_recovery_factor.CurrentUnits = PercentUnit.PERCENT + hip_ra.reservoir_available_heat.CurrentUnits = HeatUnit.J + hip_ra.reservoir_producible_heat.CurrentUnits = HeatUnit.J + hip_ra.reservoir_producible_electricity.CurrentUnits = PowerUnit.W hip_ra.PrintOutputs() with open('HIP.out', 'r') as f: content = f.readlines() @@ -815,18 +815,18 @@ def test_convert_units_of_output_parameters(self): # The class handles the case when the density of water is less than the minimum allowed value. def test_handle_low_density_of_water(self): hip_ra = HIP_RA() - hip_ra.water_density.value = -1.0 + hip_ra.fluid_density.value = -1.0 hip_ra.reservoir_temperature.value = 100.0 hip_ra.Calculate() - assert hip_ra.water_density.value == pytest.approx(999.7, abs=1e-2) + assert hip_ra.fluid_density.value == pytest.approx(999.7, abs=1e-2) # The class handles the case when the heat capacity of water is less than the minimum allowed value. def test_handle_low_heat_capacity_of_water(self): hip_ra = HIP_RA() - hip_ra.water_heat_capacity.value = -1.0 + hip_ra.fluid_heat_capacity.value = -1.0 hip_ra.reservoir_temperature.value = 100.0 hip_ra.Calculate() - assert hip_ra.water_heat_capacity.value == pytest.approx(4.18, abs=1e-2) + assert hip_ra.fluid_heat_capacity.value == pytest.approx(4.18, abs=1e-2) class Test__Init__: @@ -846,13 +846,13 @@ def test_float_parameters_initialized(self): hip_ra = HIP_RA() assert isinstance(hip_ra.reservoir_temperature, floatParameter) assert isinstance(hip_ra.rejection_temperature, floatParameter) - assert isinstance(hip_ra.formation_porosity, floatParameter) + assert isinstance(hip_ra.reservoir_porosity, floatParameter) assert isinstance(hip_ra.reservoir_area, floatParameter) assert isinstance(hip_ra.reservoir_thickness, floatParameter) - assert isinstance(hip_ra.reservoir_heat_capacity, floatParameter) - assert isinstance(hip_ra.water_heat_capacity, floatParameter) + assert isinstance(hip_ra.rock_heat_capacity, floatParameter) + assert isinstance(hip_ra.fluid_heat_capacity, floatParameter) assert isinstance(hip_ra.HeatCapacityOfRock, floatParameter) - assert isinstance(hip_ra.water_density, floatParameter) + assert isinstance(hip_ra.fluid_density, floatParameter) assert isinstance(hip_ra.rock_density, floatParameter) assert isinstance(hip_ra.RecoverableHeat, floatParameter) assert isinstance(hip_ra.WaterContent, floatParameter) @@ -869,15 +869,15 @@ def test_int_parameters_initialized(self): # The method initializes several OutputParameter objects and assigns them to corresponding attributes in the OutputParameterDict dictionary. def test_output_parameters_initialized(self): hip_ra = HIP_RA() - assert isinstance(hip_ra.V, OutputParameter) - assert isinstance(hip_ra.qR, OutputParameter) - assert isinstance(hip_ra.mWH, OutputParameter) - assert isinstance(hip_ra.e, OutputParameter) - assert isinstance(hip_ra.qWH, OutputParameter) - assert isinstance(hip_ra.Rg, OutputParameter) - assert isinstance(hip_ra.WA, OutputParameter) - assert isinstance(hip_ra.WE, OutputParameter) - assert isinstance(hip_ra.We, OutputParameter) + assert isinstance(hip_ra.reservoir_volume, OutputParameter) + assert isinstance(hip_ra.reservoir_stored_heat, OutputParameter) + assert isinstance(hip_ra.reservoir_mass, OutputParameter) + assert isinstance(hip_ra.reservoir_enthalpy, OutputParameter) + assert isinstance(hip_ra.wellhead_heat, OutputParameter) + assert isinstance(hip_ra.reservoir_recovery_factor, OutputParameter) + assert isinstance(hip_ra.reservoir_available_heat, OutputParameter) + assert isinstance(hip_ra.reservoir_producible_heat, OutputParameter) + assert isinstance(hip_ra.reservoir_producible_electricity, OutputParameter) # The 'enable_geophires_logging_config' parameter is False, so the logger is not configured. def test_logger_not_configured_when_enable_geophires_logging_config_is_false(self): @@ -918,7 +918,7 @@ def test_read_all_parameters(self): # Assert that all the parameters have been read in and updated assert hip_ra.reservoir_temperature.value == 150.0 assert hip_ra.rejection_temperature.value == 25.0 - assert hip_ra.formation_porosity.value == 18.0 + assert hip_ra.reservoir_porosity.value == 18.0 assert hip_ra.reservoir_area.value == 81.0 assert hip_ra.reservoir_thickness.value == 0.286 # Add assertions for other parameters as needed @@ -940,7 +940,7 @@ def test_update_changed_parameters(self): # Assert that the changed parameters have been updated assert hip_ra.reservoir_temperature.value == 200.0 - assert hip_ra.formation_porosity.value == 25.0 + assert hip_ra.reservoir_porosity.value == 25.0 # Add assertions for other changed parameters as needed # handles any special cases @@ -967,8 +967,8 @@ def test_handle_special_cases(self): assert hip_ra.rejection_temperature_k.value == pytest.approx(323.15) assert hip_ra.rejection_entropy.value == pytest.approx(0.091) assert hip_ra.rejection_enthalpy.value == pytest.approx(105.0) - assert hip_ra.water_density.value == pytest.approx(999999999.999) - assert hip_ra.water_heat_capacity.value == pytest.approx(4.186) + assert hip_ra.fluid_density.value == pytest.approx(999999999.999) + assert hip_ra.fluid_heat_capacity.value == pytest.approx(4.186) assert hip_ra.RecoverableHeat.value == pytest.approx(0.0) # Add assertions for other special cases as needed @@ -989,7 +989,7 @@ def test_set_current_units_preferred_units_match(self): # Assert that the CurrentUnits have been set to PreferredUnits assert hip_ra.reservoir_temperature.CurrentUnits == TemperatureUnit.CELSIUS - assert hip_ra.formation_porosity.CurrentUnits == PercentUnit.PERCENT + assert hip_ra.reservoir_porosity.CurrentUnits == PercentUnit.PERCENT # Add assertions for other parameters with matching PreferredUnits as needed # sets the CurrentUnits of a parameter to the units provided by the user if they don't match @@ -1009,7 +1009,7 @@ def test_set_current_units_preferred_units_do_not_match(self): # Assert that the CurrentUnits have been set to the units provided by the user assert hip_ra.reservoir_temperature.CurrentUnits == TemperatureUnit.FAHRENHEIT - assert hip_ra.formation_porosity.CurrentUnits == PercentUnit.PERCENT + assert hip_ra.reservoir_porosity.CurrentUnits == PercentUnit.PERCENT # Add assertions for other parameters with non-matching PreferredUnits as needed @@ -1019,67 +1019,67 @@ class TestCalculate: def test_calculate_stored_heat(self): hip_ra = HIP_RA() hip_ra.Calculate() - assert hip_ra.qR.value == hip_ra.V.value * (hip_ra.reservoir_heat_capacity.value * (hip_ra.reservoir_temperature.value - hip_ra.rejection_temperature.value)) + assert hip_ra.reservoir_stored_heat.value == hip_ra.reservoir_volume.value * (hip_ra.rock_heat_capacity.value * (hip_ra.reservoir_temperature.value - hip_ra.rejection_temperature.value)) # Calculates the volume of the reservoir def test_calculate_reservoir_volume(self): hip_ra = HIP_RA() hip_ra.Calculate() - assert hip_ra.V.value == hip_ra.reservoir_area.value * hip_ra.reservoir_thickness.value + assert hip_ra.reservoir_volume.value == hip_ra.reservoir_area.value * hip_ra.reservoir_thickness.value # Calculates the maximum energy out per unit of mass def test_calculate_maximum_energy(self): hip_ra = HIP_RA() hip_ra.Calculate() - assert hip_ra.e.value == ((EnthalpyH20_func(hip_ra.reservoir_temperature.value) - hip_ra.rejection_enthalpy.value) - (hip_ra.rejection_temperature_k.value * (EntropyH20_func(hip_ra.reservoir_temperature.value) - hip_ra.rejection_entropy.value))) + assert hip_ra.reservoir_enthalpy.value == ((EnthalpyH20_func(hip_ra.reservoir_temperature.value) - hip_ra.rejection_enthalpy.value) - (hip_ra.rejection_temperature_k.value * (EntropyH20_func(hip_ra.reservoir_temperature.value) - hip_ra.rejection_entropy.value))) # Calculates the heat recovery at the wellhead def test_calculate_heat_recovery(self): hip_ra = HIP_RA() hip_ra.Calculate() - assert hip_ra.qWH.value == hip_ra.mWH.value * (EnthalpyH20_func(hip_ra.reservoir_temperature.value) - hip_ra.rejection_temperature_k.value) + assert hip_ra.wellhead_heat.value == hip_ra.reservoir_mass.value * (EnthalpyH20_func(hip_ra.reservoir_temperature.value) - hip_ra.rejection_temperature_k.value) # Calculates the available heat def test_calculate_available_heat(self): hip_ra = HIP_RA() hip_ra.Calculate() - assert hip_ra.WA.value == (hip_ra.mWH.value * hip_ra.e.value * hip_ra.Rg.value * RecoverableHeat(hip_ra.RecoverableHeat.value, hip_ra.reservoir_temperature.value)) + assert hip_ra.reservoir_available_heat.value == (hip_ra.reservoir_mass.value * hip_ra.reservoir_enthalpy.value * hip_ra.reservoir_recovery_factor.value * RecoverableHeat(hip_ra.RecoverableHeat.value, hip_ra.reservoir_temperature.value)) # Calculates the mass of the fluid in the reservoir with wrong formula def test_calculate_mass_of_fluid_wrong_formula(self): hip_ra = HIP_RA() hip_ra.Calculate() - assert hip_ra.mWH.value == (hip_ra.V.value * (hip_ra.formation_porosity.value / 100.0)) * hip_ra.water_density.value + assert hip_ra.reservoir_mass.value == (hip_ra.reservoir_volume.value * (hip_ra.reservoir_porosity.value / 100.0)) * hip_ra.fluid_density.value # Calculates the density of water with wrong formula def test_calculate_density_of_water_wrong_formula(self): hip_ra = HIP_RA() hip_ra.Calculate() - if hip_ra.water_density.value < hip_ra.water_density.Min: - hip_ra.water_density.value = DensityWater(hip_ra.reservoir_temperature.value) * 1_000_000_000.0 - assert hip_ra.water_density.value >= hip_ra.water_density.Min + if hip_ra.fluid_density.value < hip_ra.fluid_density.Min: + hip_ra.fluid_density.value = DensityWater(hip_ra.reservoir_temperature.value) * 1_000_000_000.0 + assert hip_ra.fluid_density.value >= hip_ra.fluid_density.Min # Calculates the heat capacity of water with wrong formula def test_calculate_heat_capacity_of_water_wrong_formula(self): hip_ra = HIP_RA() hip_ra.Calculate() - if hip_ra.water_heat_capacity.value < hip_ra.water_heat_capacity.Min: - hip_ra.water_heat_capacity.value = HeatCapacityWater(hip_ra.reservoir_temperature.value) / 1000.0 - assert hip_ra.water_heat_capacity.value >= hip_ra.water_heat_capacity.Min + if hip_ra.fluid_heat_capacity.value < hip_ra.fluid_heat_capacity.Min: + hip_ra.fluid_heat_capacity.value = HeatCapacityWater(hip_ra.reservoir_temperature.value) / 1000.0 + assert hip_ra.fluid_heat_capacity.value >= hip_ra.fluid_heat_capacity.Min # Calculates the stored heat in the reservoir with negative values def test_calculate_stored_heat_negative_values(self): hip_ra = HIP_RA() - hip_ra.reservoir_heat_capacity.value = -1e12 + hip_ra.rock_heat_capacity.value = -1e12 hip_ra.Calculate() - assert hip_ra.qR.value == hip_ra.V.value * (hip_ra.reservoir_heat_capacity.value * (hip_ra.reservoir_temperature.value - hip_ra.rejection_temperature.value)) + assert hip_ra.reservoir_stored_heat.value == hip_ra.reservoir_volume.value * (hip_ra.rock_heat_capacity.value * (hip_ra.reservoir_temperature.value - hip_ra.rejection_temperature.value)) # Calculates the maximum energy out per unit of mass with negative values def test_calculate_maximum_energy_negative_values(self): hip_ra = HIP_RA() hip_ra.rejection_enthalpy.value = 1e12 hip_ra.Calculate() - assert hip_ra.e.value == ((EnthalpyH20_func(hip_ra.reservoir_temperature.value) - hip_ra.rejection_enthalpy.value) - (hip_ra.rejection_temperature_k.value * (EntropyH20_func(hip_ra.reservoir_temperature.value) - hip_ra.rejection_entropy.value))) + assert hip_ra.reservoir_enthalpy.value == ((EnthalpyH20_func(hip_ra.reservoir_temperature.value) - hip_ra.rejection_enthalpy.value) - (hip_ra.rejection_temperature_k.value * (EntropyH20_func(hip_ra.reservoir_temperature.value) - hip_ra.rejection_entropy.value))) class TestPrintOutputs: diff --git a/tests/examples/HIPexample1.txt b/tests/examples/HIPexample1.txt index 3c8818e9..0c33afd1 100644 --- a/tests/examples/HIPexample1.txt +++ b/tests/examples/HIPexample1.txt @@ -1,8 +1,7 @@ Reservoir Temperature, 250.0 Rejection Temperature, 60.0 -Formation Porosity, 10.0 +Reservoir Porosity, 10.0 Reservoir Area, 55.0 Reservoir Thickness, 0.25 Reservoir Life Cycle, 25 -Heat Capacity Of Water, -1 -Density Of Water, -1 + diff --git a/tests/examples/MC_HIP_Settings_file.txt b/tests/examples/MC_HIP_Settings_file.txt index ef247bcb..3a6baae6 100644 --- a/tests/examples/MC_HIP_Settings_file.txt +++ b/tests/examples/MC_HIP_Settings_file.txt @@ -3,9 +3,10 @@ INPUT, Reservoir Area, uniform, 50.0, 120.0 INPUT, Reservoir Thickness, uniform, 0.122, 0.299 INPUT, Reservoir Temperature, uniform, 130, 170 INPUT, Rejection Temperature, uniform, 20, 33 -OUTPUT, Recovery Factor -OUTPUT, Available Heat -OUTPUT, Produceable Heat -OUTPUT, Produceable Electricity +OUTPUT, Available Heat (fluid) +OUTPUT, Producible Heat (fluid) +OUTPUT, Producible Heat/Unit Area (fluid) +OUTPUT, Producible Electricity (fluid) +OUTPUT, Producible Electricity/Unit Area (fluid) ITERATIONS, 250 MC_OUTPUT_FILE, MC_HIP_Result.txt diff --git a/tests/examples/example10_HP.txt b/tests/examples/example10_HP.txt index 2afb9a43..c72f4440 100644 --- a/tests/examples/example10_HP.txt +++ b/tests/examples/example10_HP.txt @@ -37,7 +37,8 @@ Water Loss Fraction,0.02, ---[-] ***Surface Technical Parameters*** ********************************** -End-Use Option,7, --- centralized heat pump +End-Use Option, 2, --- Direct use +Power Plant Type, 6, ---[-] Heat Pump Circulation Pump Efficiency,.80, ---[-] Utilization Factor,.9, ---[-] End-Use Efficiency Factor,.9, ---[-] diff --git a/tests/examples/example11_AC.txt b/tests/examples/example11_AC.txt index b896e6cf..d8e685a9 100644 --- a/tests/examples/example11_AC.txt +++ b/tests/examples/example11_AC.txt @@ -31,8 +31,8 @@ Water Loss Fraction,0.02, ---[-] ***Surface Technical Parameters*** ********************************** - -End-Use Option,6, ---Absorption chillers +End-Use Option,2, --- Direct use +Power Plant Type, 5, ---[-] Absorption Chiller Circulation Pump Efficiency,.80, ---[-] Utilization Factor,.9, ---[-] End-Use Efficiency Factor,.9, ---[-] diff --git a/tests/examples/example12_DH.txt b/tests/examples/example12_DH.txt index 5f83d9f2..c8787933 100644 --- a/tests/examples/example12_DH.txt +++ b/tests/examples/example12_DH.txt @@ -49,7 +49,8 @@ Reservoir Thermal Conductivity,3, --- [W/m/K] *** Surface technical parameters *** ************************************ -End-Use Option,8, --- [-] District Heating +End-Use Option,2, --- Direct use +Power Plant Type, 7, ---[-] District Heating Circulation Pump Efficiency,0.8, --- [-] Utilization Factor,0.8, --- [-] Surface Temperature,12, --- [deg.C]