diff --git a/.bumpversion.cfg b/.bumpversion.cfg index a2b1fef8..f72d3bd0 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.9.62 +current_version = 3.9.64 commit = True tag = True diff --git a/.cookiecutterrc b/.cookiecutterrc index c5496a36..d73e0063 100644 --- a/.cookiecutterrc +++ b/.cookiecutterrc @@ -54,7 +54,7 @@ default_context: sphinx_doctest: "no" sphinx_theme: "sphinx-py3doc-enhanced-theme" test_matrix_separate_coverage: "no" - version: 3.9.62 + version: 3.9.64 version_manager: "bump2version" website: "https://github.com/NREL" year_from: "2023" diff --git a/README.rst b/README.rst index 0048e314..a198f768 100644 --- a/README.rst +++ b/README.rst @@ -58,9 +58,9 @@ Free software: `MIT license `__ :alt: Supported implementations :target: https://pypi.org/project/geophires-x -.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.9.62.svg +.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.9.64.svg :alt: Commits since latest release - :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.9.62...main + :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.9.64...main .. |docs| image:: https://readthedocs.org/projects/GEOPHIRES-X/badge/?style=flat :target: https://nrel.github.io/GEOPHIRES-X diff --git a/docs/Theoretical-Basis-for-GEOPHIRES.md b/docs/Theoretical-Basis-for-GEOPHIRES.md index 33e742f8..4027cff8 100644 --- a/docs/Theoretical-Basis-for-GEOPHIRES.md +++ b/docs/Theoretical-Basis-for-GEOPHIRES.md @@ -1,6 +1,6 @@ # Theoretical Basis for GEOPHIRES -This document describes the foundational theoretical basis for GEOPHIRES, adapted from the 2019 paper, "[GEOPHIRES v2.0: updated geothermal techno-economic simulation tool](https://doi.org/10.1186/s40517-019-0119-6)" by Koenraad F. Beckers & Kevin McCabe. The core theories described here remain valid and relevant to the current software. +This document describes the foundational theoretical basis for GEOPHIRES, adapted from the 2019 paper "[GEOPHIRES v2.0: updated geothermal techno-economic simulation tool](https://doi.org/10.1186/s40517-019-0119-6)" by Koenraad F. Beckers & Kevin McCabe. The core theories described here remain valid and relevant to the current software. However, the text has not been comprehensively updated to include the theory for features added in GEOPHIRES v3 (GEOPHIRES-X) and later. While pointers to the documentation for newer models have been added, the detailed technical descriptions herein still pertain to v2.0. diff --git a/docs/conf.py b/docs/conf.py index 97ecace7..5d0927f8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,7 +18,7 @@ year = '2025' author = 'NREL' copyright = f'{year}, {author}' -version = release = '3.9.62' +version = release = '3.9.64' pygments_style = 'trac' templates_path = ['./templates'] diff --git a/setup.py b/setup.py index 83bc6cdd..a141852c 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ def read(*names, **kwargs): setup( name='geophires-x', - version='3.9.62', + version='3.9.64', license='MIT', description='GEOPHIRES is a free and open-source geothermal techno-economic simulator.', long_description='{}\n{}'.format( diff --git a/src/geophires_x/Economics.py b/src/geophires_x/Economics.py index 5624f166..8a2cd185 100644 --- a/src/geophires_x/Economics.py +++ b/src/geophires_x/Economics.py @@ -625,7 +625,8 @@ def __init__(self, model: Model): CurrentUnits=CurrencyUnit.MDOLLARS, Provided=False, Valid=False, - ToolTipText="Total reservoir stimulation capital cost, including indirect costs and contingency." + ToolTipText='Total reservoir stimulation capital cost, including indirect costs and contingency. ' + f'For traditional hydrothermal reservoirs, this parameter should be set to $0.' ) max_stimulation_cost_per_well_MUSD = 100 @@ -938,7 +939,18 @@ def __init__(self, model: Model): UnitType=Units.NONE, Required=True, ErrMessage="assume default number of time steps per year (4)", - ToolTipText="Number of internal simulation time steps per year" + ToolTipText='Number of internal simulation time steps per year. GEOPHIRES assumes linear time ' + 'discretization with a user-provided number of time steps per year over the lifetime of the ' + 'plant. The default is four time steps per year, meaning a time step of 3 months. ' + 'At every time step, GEOPHIRES calculates the reservoir output temperature, production ' + 'wellhead temperature, direct-use heat and/or electricity power output (in MW), pressure ' + 'drops and pumping power. On an annual basis, GEOPHIRES calculates the O&M costs and ' + 'direct-use heat and/or electricity production. To investigate seasonal effects, e.g., to ' + 'assess the impact of more geothermal heat demand for district heating in winter than in ' + 'summer, the user can select a smaller time step, e.g., a month (or 12 time steps per year). ' + 'For even shorter timescale effects, e.g., to account for an hourly varying ambient ' + 'temperature or investigate the response in plant operation to a fluctuating revenue rate), ' + 'the user can select an even smaller time step, e.g., 1 h (or 8760 time steps per year).' ) self.FCR = self.ParameterDict[self.FCR.Name] = floatParameter( "Fixed Charge Rate", @@ -1824,14 +1836,19 @@ def __init__(self, model: Model): f'costs per well. ' f'Provide {self.ccstimadjfactor.Name} to multiply the correlation-calculated cost. ' f'Provide {self.ccstimfixed.Name} to override the correlation and set your own ' - f'total stimulation cost.' + f'total stimulation cost. ' + f'For traditional hydrothermal reservoirs, {self.ccstimfixed.Name} should be set to $0.' ) - contingency_and_indirect_costs_tooltip = ( - f'plus {self.contingency_percentage.quantity().to(convertible_unit("%")).magnitude:g}% contingency ' + # TODO switch order to align with theoretical basis, which lists indirect costs first + contingency_and_indirect_costs_tooltip_stem = ( + f'{self.contingency_percentage.quantity().to(convertible_unit("%")).magnitude:g}% contingency ' f'plus {self.indirect_capital_cost_percentage.quantity().to(convertible_unit("%")).magnitude}% ' f'indirect costs' ) + contingency_and_indirect_costs_tooltip = ( + f'plus {contingency_and_indirect_costs_tooltip_stem}' + ) self.Cexpl = self.OutputParameterDict[self.Cexpl.Name] = OutputParameter( Name="Exploration cost", @@ -1839,7 +1856,8 @@ def __init__(self, model: Model): UnitType=Units.CURRENCY, PreferredUnits=CurrencyUnit.MDOLLARS, CurrentUnits=CurrencyUnit.MDOLLARS, - ToolTipText=f'Default correlation: 60% of the cost of one production well ' + ToolTipText=f'The built-in exploration cost correlation considers drilling of a slim-hole well at 60% of ' + f'the cost of a regular well, $1M for geophysical and field work, ' f'{contingency_and_indirect_costs_tooltip}. ' f'Provide {self.ccexpladjfactor.Name} to multiply the default correlation. ' f'Provide {self.ccexplfixed.Name} to override the default correlation and set your own cost.' @@ -1865,14 +1883,21 @@ def __init__(self, model: Model): ToolTipText='Drilling and completion cost per well, including indirect costs ' f'(default: {self.wellfield_indirect_capital_cost_percentage.DefaultValue}%).' ) + + # noinspection SpellCheckingInspection self.Coamwell = self.OutputParameterDict[self.Coamwell.Name] = OutputParameter( Name="O&M Wellfield cost", display_name='Wellfield maintenance costs', UnitType=Units.CURRENCYFREQUENCY, PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, - CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR - # TODO TooltipText to document how this is calculated + CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, + # TODO parameterize relevant constants in tooltip text + ToolTipText='The built-in correlation for the wellfield O&M costs is similar as the surface plant O&M ' + 'costs: it assumes that it consists of 1% of the total wellfield plus field gathering system ' + 'costs (for annual non-labor costs) and 25% of the labor costs (the other 75% of the labor ' + 'costs are assigned to the surface plant O&M costs).' ) + self.redrilling_annual_cost = self.OutputParameterDict[self.redrilling_annual_cost.Name] = OutputParameter( Name="Redrilling costs", UnitType=Units.CURRENCYFREQUENCY, @@ -1884,25 +1909,85 @@ def __init__(self, model: Model): f'The total is then divided over {model.surfaceplant.plant_lifetime.Name} years to calculate ' f'Redrilling costs per year.' ) + # noinspection SpellCheckingInspection self.Cplant = self.OutputParameterDict[self.Cplant.Name] = OutputParameter( Name="Surface Plant cost", + display_name='Surface power plant costs', UnitType=Units.CURRENCY, PreferredUnits=CurrencyUnit.MDOLLARS, - CurrentUnits=CurrencyUnit.MDOLLARS + CurrentUnits=CurrencyUnit.MDOLLARS, + # TODO incorporate direct references to relevant parameters for adjusting correlation in tooltip text + # TODO interpolate relevant constants (that are currently hardcoded) in tooltip text + ToolTipText='The built-in power plant cost correlations are based on the original correlations developed ' + 'by Beckers (2016), indexed to 2017 using the IHS Markit North American Power Capital Costs ' + 'Index (NAPCCI) excluding nuclear plants (IHS 2018). The ORC power plant cost data have been ' + 'updated with data from the 2016 GETEM tool (DOE 2016) and the geothermal binary power plants ' + 'study by Verkis (2014). ' + # Note: actual author name above is "Verkís" but the unicode accented i may cause unexpected + # problems in consumers. + 'Figure 4 in the Theoretical Basis shows the power plant capital cost expressed in $ kWe-1 ' + 'as a function of plant size and initial production temperature for subcritical ORC and ' + 'double-flash power plants. ' + f'The default correlations in GEOPHIRES include {contingency_and_indirect_costs_tooltip_stem}. ' + 'For the same plant size and production temperature, double-flash power plants are considered ' + 'about 25% more expensive than single-flash power plants (Zeyghami 2010), and supercritical ' + 'ORC plants are roughly 10% more than subcritical ORC plants (Astolfi et al. 2014). A wide ' + 'range in power plant specific cost values is reported in academic and popular literature. ' + 'The GEOPHIRES built-in surface plant cost correlations represent typical values. However, ' + 'the user is recommended to provide their own power plant cost data if available for their ' + 'case study. The ORC plant specific cost decreases only moderately at higher temperatures. ' + 'The reasons are that when increasing the temperature, the ORC plant design also changes: ' + '(1) a different organic fluid is selected, (2) piping, pump, heat exchangers, and other ' + 'equipment are designed to handle the higher temperature (and potentially also pressure), ' + 'requiring thicker walls, potentially different materials, etc., and (3) additional components ' + 'may be implemented, such as a heat recuperator, making the design and operation more complex. ' + 'Unlike flash power plants, ORC plants are a small, niche market, typically case specific, ' + 'and rely on relatively young technology, which has not been subject yet to decades of ' + 'technological advancement. The cost for direct-use heat applications is highly dependent ' + 'on the type of application. A generic cost of $250 kWth-1 is assumed ' + f'{contingency_and_indirect_costs_tooltip}. ' + 'However, users are encouraged to provide their own cost figures for ' + 'their specific application. Beckers and Young (2017) collected several cost figures to ' + 'estimate the surface equipment cost for geothermal district-heating systems.' ) self.Coamplant = self.OutputParameterDict[self.Coamplant.Name] = OutputParameter( Name="O&M Surface Plant costs", display_name='Power plant maintenance costs', UnitType=Units.CURRENCYFREQUENCY, PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, - CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR + CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, + # TODO parameterize relevant constants in tooltip text + # TODO update index year and/or make indexing parameterizable in tooltip text + ToolTipText='GEOPHIRES estimates the annual surface plant O&M costs as the sum of 1.5% of the total plant ' + 'capital cost (for annual non-labor costs), and 75% of the annual labor costs. The other 25% ' + 'of the labor costs are assigned to the wellfield O&M cost. The labor costs are calculated ' + 'internally in GEOPHIRES using the 2014 labor costs provided by Beckers (2016), indexed to ' + '2017 using the Bureau of Labor Statistics (BLS) Employment Cost Index for utilities (2018). ' + 'The original 2014 labor cost correlation expresses the labor costs as a function of the plant ' + 'size (MW) using an approximate logarithmic curve fit to the built-in labor cost data in ' + 'GETEM.' ) + # noinspection SpellCheckingInspection self.Cgath = self.OutputParameterDict[self.Cgath.Name] = OutputParameter( Name="Field gathering system cost", display_name='Field gathering system costs', UnitType=Units.CURRENCY, PreferredUnits=CurrencyUnit.MDOLLARS, - CurrentUnits=CurrencyUnit.MDOLLARS + CurrentUnits=CurrencyUnit.MDOLLARS, + # TODO interpolate constant values in tooltip text instead of hardcoding in tooltip text + ToolTipText='The built-in cost correlation for estimating the field gathering system cost includes ' + 'the cost for surface piping from each well to the plant and pumps for production and ' + 'injection wells. The length of the surface piping is assumed 750 m per well at a cost of ' + '$500 per meter. The pumping cost for each pump in the production wells (line-shaft pumps) ' + 'and a single pump for the injection wells is calculated with the same correlation as GETEM. ' + f'Contingency (default: ' + f'{self.contingency_percentage.quantity().to(convertible_unit("%")).magnitude:g}%). ' + f'and indirect costs (default: ' + f'{self.indirect_capital_cost_percentage.quantity().to(convertible_unit("%")).magnitude}%) ' + f'are added. ' + 'The built-in cost correlation does not include the cost of pipelines to an off-site heat ' + 'user or a district-heating system. These costs are estimated at $750 per meter pipeline ' + 'length and can be manually added by the user to the pipeline distribution costs.' ) self.Cpiping = self.OutputParameterDict[self.Cpiping.Name] = OutputParameter( Name="Transmission pipeline costs", @@ -1917,7 +2002,9 @@ def __init__(self, model: Model): UnitType=Units.CURRENCYFREQUENCY, PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, - ToolTipText='Assumes $3.5/1,000 gallons of water' # TODO parameterize + ToolTipText=f'Default correlation: Assumes $3.50/1,000 gallons of water. ' + f'Provide {self.oamwateradjfactor.Name} to multiply the default correlation.' + # Note: $3.50 could possibly be parameterized, but adjustment factor param serves the same purpose for now. ) self.CCap = self.OutputParameterDict[self.CCap.Name] = OutputParameter( Name="Total Capital Cost", @@ -1934,7 +2021,9 @@ def __init__(self, model: Model): display_name='Total operating and maintenance costs', UnitType=Units.CURRENCYFREQUENCY, PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, - CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR + CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, + ToolTipText=f'GEOPHIRES estimates the annual O&M costs as the sum of the annual surface plant, wellfield, ' + f'make-up water, and pumping O&M costs.' ) self.averageannualpumpingcosts = OutputParameter( Name="Average Annual Pumping Costs", diff --git a/src/geophires_x/OptionList.py b/src/geophires_x/OptionList.py index c3c5ddd7..77d84f52 100644 --- a/src/geophires_x/OptionList.py +++ b/src/geophires_x/OptionList.py @@ -25,18 +25,22 @@ def __ne__(self, other): return str(self) != str(other) +_EXTRA_HEAT_SNIPPET = 'Heat sales considered as extra income' +_EXTRA_ELECTRICITY_SNIPPET = 'Electricity sales considered as extra income' + + class EndUseOptions(GeophiresInputEnum): - ELECTRICITY = 1, "Electricity" - HEAT = 2, "Direct-Use Heat" - COGENERATION_TOPPING_EXTRA_HEAT = 31, "Cogeneration Topping Cycle, Heat sales considered as extra income" - COGENERATION_TOPPING_EXTRA_ELECTRICITY = 32, "Cogeneration Topping Cycle, Electricity sales considered as extra income" - COGENERATION_BOTTOMING_EXTRA_HEAT = 41, "Cogeneration Bottoming Cycle, Heat sales considered as extra income" - COGENERATION_BOTTOMING_EXTRA_ELECTRICITY = 42, "Cogeneration Bottoming Cycle, Electricity sales considered as extra income" - COGENERATION_PARALLEL_EXTRA_HEAT = 51, "Cogeneration Parallel Cycle, Heat sales considered as extra income" - COGENERATION_PARALLEL_EXTRA_ELECTRICITY = 52, "Cogeneration Parallel Cycle, Electricity sales considered as extra income" + ELECTRICITY = 1, 'Electricity' + HEAT = 2, 'Direct-Use Heat' + COGENERATION_TOPPING_EXTRA_HEAT = 31, f'Cogeneration Topping Cycle, {_EXTRA_HEAT_SNIPPET}' + COGENERATION_TOPPING_EXTRA_ELECTRICITY = 32, f'Cogeneration Topping Cycle, {_EXTRA_ELECTRICITY_SNIPPET}' + COGENERATION_BOTTOMING_EXTRA_HEAT = 41, f'Cogeneration Bottoming Cycle, {_EXTRA_HEAT_SNIPPET}' + COGENERATION_BOTTOMING_EXTRA_ELECTRICITY = 42, f'Cogeneration Bottoming Cycle, {_EXTRA_ELECTRICITY_SNIPPET}' + COGENERATION_PARALLEL_EXTRA_HEAT = 51, f'Cogeneration Parallel Cycle, {_EXTRA_HEAT_SNIPPET}' + COGENERATION_PARALLEL_EXTRA_ELECTRICITY = 52, f'Cogeneration Parallel Cycle, {_EXTRA_ELECTRICITY_SNIPPET}' @staticmethod - def from_input_string(input_string: str): + def from_input_string(input_string: str) -> 'EndUseOptions': """ :rtype: EndUseOptions """ @@ -48,11 +52,17 @@ def from_input_string(input_string: str): raise ValueError(f'Unknown End-Use Option input value: {input_string}') @staticmethod - def from_int(int_val): + def from_int(int_val: int) -> 'EndUseOptions': + """ + :rtype: EndUseOptions + """ + for member in __class__: if member.int_value == int_val: return member + raise ValueError(f'Unknown End-Use Option integer input value: {int_val}') + class PlantType(GeophiresInputEnum): SUB_CRITICAL_ORC = 1, "Subcritical ORC" diff --git a/src/geophires_x/Outputs.py b/src/geophires_x/Outputs.py index 6f8546cd..6dbd57c6 100644 --- a/src/geophires_x/Outputs.py +++ b/src/geophires_x/Outputs.py @@ -195,7 +195,8 @@ def PrintOutputs(self, model: Model): f.write(NL) f.write(' ***SUMMARY OF RESULTS***\n') f.write(NL) - f.write(f' End-Use Option: {str(model.surfaceplant.enduse_option.value.value)}\n') + f.write(f' {model.surfaceplant.enduse_option_output.display_name}: ' + f'{model.surfaceplant.enduse_option_output.value}\n') if model.surfaceplant.plant_type.value in [PlantType.ABSORPTION_CHILLER, PlantType.HEAT_PUMP, PlantType.DISTRICT_HEATING]: f.write(' Surface Application: ' + str(model.surfaceplant.plant_type.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]: # there is an electricity component @@ -481,7 +482,7 @@ def PrintOutputs(self, model: Model): cpw_label = Outputs._field_label(econ.drilling_and_completion_costs_per_well.display_name, 47) f.write(f' {cpw_label}{econ.drilling_and_completion_costs_per_well.value:10.2f} {econ.Cwell.CurrentUnits.value}\n') f.write(f' {econ.Cstim.display_name}: {econ.Cstim.value:10.2f} {econ.Cstim.CurrentUnits.value}\n') - f.write(f' Surface power plant costs: {model.economics.Cplant.value:10.2f} ' + model.economics.Cplant.CurrentUnits.value + NL) + f.write(f' {econ.Cplant.display_name}: {econ.Cplant.value:10.2f} {econ.Cplant.CurrentUnits.value}\n') 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.plant_type.value == PlantType.HEAT_PUMP: diff --git a/src/geophires_x/SUTRAOutputs.py b/src/geophires_x/SUTRAOutputs.py index 813188cf..93172b3a 100644 --- a/src/geophires_x/SUTRAOutputs.py +++ b/src/geophires_x/SUTRAOutputs.py @@ -67,7 +67,8 @@ 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.enduse_option.value.value) + NL) + f.write(f' {model.surfaceplant.enduse_option_output.display_name}: ' + f'{model.surfaceplant.enduse_option_output.value}\n') 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) diff --git a/src/geophires_x/SurfacePlant.py b/src/geophires_x/SurfacePlant.py index 2fb9f9e0..9ac33f6d 100644 --- a/src/geophires_x/SurfacePlant.py +++ b/src/geophires_x/SurfacePlant.py @@ -471,6 +471,42 @@ def __init__(self, model: Model): self.MyPath = os.path.abspath(__file__) # Results - used by other objects or printed in output downstream + + self.enduse_option_output = self.OutputParameterDict[self.enduse_option_output.Name] = OutputParameter( + Name=self.enduse_option.Name, + UnitType=Units.NONE, + ToolTipText='Five different end-use options are available in GEOPHIRES: ' + '1: Electricity: All produced geothermal fluid is used to generate electricity with either an ' + 'ORC or flash power plant; ' + '2: Direct-use heat: All produced geothermal fluid is used to provide heating for a given ' + 'application, e.g., a district-heating system or industrial process; ' + '3: Cogeneration or combined heat and power (CHP): Both heat and electricity are produced. ' + 'Three different cogeneration configurations are available: ' + '(1): Cogeneration topping cycle: A power plant is followed by a direct-use heat application ' + 'in series. Heat at high temperatures from the geothermal fluid is first converted into ' + 'electricity. Any remaining heat in the geothermal fluid after leaving the power plant is ' + 'supplied to a low-temperature direct-use heat application; ' + '(2): Cogeneration bottoming cycle: A direct-use heat application is followed by a power plant ' + 'in series. In this less common configuration, the geothermal fluid first serves a ' + 'high-temperature direct-use heat application. Any remaining heat in the geothermal fluid ' + 'after leaving the direct-use heat process (at a user-specified temperature) is used to ' + 'generate electricity. The heat-to-power conversion typically occurs with an ORC plant ' + 'operating at low conversion efficiencies; ' + '(3): Cogeneration parallel cycle: A power plant operates in parallel with a direct-use heat ' + 'application. The produced geothermal fluid is split into two streams, providing heat at the ' + 'same temperature to a power plant and direct-use heat application. The user specifies the ' + 'fluid flow fraction going to each process.', + # TODO this tooltip text (ported from the theoretical basis) is probably better suited to being semantically + # associated with the End-Use Option input parameter rather than the corresponding OutputParameter here. + # However, the input parameter tooltip text is generated from an enumeration of EndUseOptions enum values + # which doesn't cleanly map to the information contained here because each of the CHP configurations has + # two EndUseOptions for whether heat or electricity sales are considered as extra income. This mapping + # incongruency could possibly eventually be addressed by an additional layer of metadata/indirection for + # adding pointers from input parameters to more complete documentation entries/sections. In the meantime, + # this output parameter tooltip is a logical-enough place to store the information, where it is reasonably + # user-accessible in the UI and parameters reference. + json_parameter_type='string' + ) self.usebuiltinoutletplantcorrelation = self.OutputParameterDict[self.usebuiltinoutletplantcorrelation.Name] = OutputParameter( Name="usebuiltinoutletplantcorrelation", UnitType=Units.NONE @@ -697,3 +733,5 @@ def _calculate_derived_outputs(self, model: Model) -> None: convertible_unit(self.heat_to_power_conversion_efficiency.CurrentUnits)).magnitude if avg_efficiency > 0: # 0 is presumed to mean N/A self.heat_to_power_conversion_efficiency.value = avg_efficiency + + self.enduse_option_output.value = self.enduse_option.value.value diff --git a/src/geophires_x/__init__.py b/src/geophires_x/__init__.py index 9524b5d4..0d1d652b 100644 --- a/src/geophires_x/__init__.py +++ b/src/geophires_x/__init__.py @@ -1 +1 @@ -__version__ = '3.9.62' +__version__ = '3.9.64' diff --git a/src/geophires_x_schema_generator/geophires-request.json b/src/geophires_x_schema_generator/geophires-request.json index 7c9ff113..0788b878 100644 --- a/src/geophires_x_schema_generator/geophires-request.json +++ b/src/geophires_x_schema_generator/geophires-request.json @@ -1396,7 +1396,7 @@ ] }, "Reservoir Stimulation Capital Cost": { - "description": "Total reservoir stimulation capital cost, including indirect costs and contingency.", + "description": "Total reservoir stimulation capital cost, including indirect costs and contingency. For traditional hydrothermal reservoirs, this parameter should be set to $0.", "type": "number", "units": "MUSD", "category": "Economics", @@ -1612,7 +1612,7 @@ "maximum": 100 }, "Time steps per year": { - "description": "Number of internal simulation time steps per year", + "description": "Number of internal simulation time steps per year. GEOPHIRES assumes linear time discretization with a user-provided number of time steps per year over the lifetime of the plant. The default is four time steps per year, meaning a time step of 3 months. At every time step, GEOPHIRES calculates the reservoir output temperature, production wellhead temperature, direct-use heat and/or electricity power output (in MW), pressure drops and pumping power. On an annual basis, GEOPHIRES calculates the O&M costs and direct-use heat and/or electricity production. To investigate seasonal effects, e.g., to assess the impact of more geothermal heat demand for district heating in winter than in summer, the user can select a smaller time step, e.g., a month (or 12 time steps per year). For even shorter timescale effects, e.g., to account for an hourly varying ambient temperature or investigate the response in plant operation to a fluctuating revenue rate), the user can select an even smaller time step, e.g., 1 h (or 8760 time steps per year).", "type": "integer", "units": null, "category": "Economics", diff --git a/src/geophires_x_schema_generator/geophires-result.json b/src/geophires_x_schema_generator/geophires-result.json index a4fa2c10..d14644b6 100644 --- a/src/geophires_x_schema_generator/geophires-result.json +++ b/src/geophires_x_schema_generator/geophires-result.json @@ -8,7 +8,11 @@ "SUMMARY OF RESULTS": { "type": "object", "properties": { - "End-Use Option": {}, + "End-Use Option": { + "type": "string", + "description": "Five different end-use options are available in GEOPHIRES: 1: Electricity: All produced geothermal fluid is used to generate electricity with either an ORC or flash power plant; 2: Direct-use heat: All produced geothermal fluid is used to provide heating for a given application, e.g., a district-heating system or industrial process; 3: Cogeneration or combined heat and power (CHP): Both heat and electricity are produced. Three different cogeneration configurations are available: (1): Cogeneration topping cycle: A power plant is followed by a direct-use heat application in series. Heat at high temperatures from the geothermal fluid is first converted into electricity. Any remaining heat in the geothermal fluid after leaving the power plant is supplied to a low-temperature direct-use heat application; (2): Cogeneration bottoming cycle: A direct-use heat application is followed by a power plant in series. In this less common configuration, the geothermal fluid first serves a high-temperature direct-use heat application. Any remaining heat in the geothermal fluid after leaving the direct-use heat process (at a user-specified temperature) is used to generate electricity. The heat-to-power conversion typically occurs with an ORC plant operating at low conversion efficiencies; (3): Cogeneration parallel cycle: A power plant operates in parallel with a direct-use heat application. The produced geothermal fluid is split into two streams, providing heat at the same temperature to a power plant and direct-use heat application. The user specifies the fluid flow fraction going to each process.", + "units": null + }, "End-Use": {}, "Surface Application": {}, "Reservoir Model": {}, @@ -400,11 +404,15 @@ "Drilling and completion costs per redrilled well": {}, "Stimulation costs": { "type": "number", - "description": "Default correlation: $1.25M per injection well plus 15% contingency plus 5% indirect costs. Provide Reservoir Stimulation Capital Cost per Injection Well and Reservoir Stimulation Capital Cost per Production Well to set the correlation costs per well. Provide Reservoir Stimulation Capital Cost Adjustment Factor to multiply the correlation-calculated cost. Provide Reservoir Stimulation Capital Cost to override the correlation and set your own total stimulation cost.", + "description": "Default correlation: $1.25M per injection well plus 15% contingency plus 5% indirect costs. Provide Reservoir Stimulation Capital Cost per Injection Well and Reservoir Stimulation Capital Cost per Production Well to set the correlation costs per well. Provide Reservoir Stimulation Capital Cost Adjustment Factor to multiply the correlation-calculated cost. Provide Reservoir Stimulation Capital Cost to override the correlation and set your own total stimulation cost. For traditional hydrothermal reservoirs, Reservoir Stimulation Capital Cost should be set to $0.", "units": "MUSD" }, "Stimulation costs (for redrilling)": {}, - "Surface power plant costs": {}, + "Surface power plant costs": { + "type": "number", + "description": "Surface Plant cost. The built-in power plant cost correlations are based on the original correlations developed by Beckers (2016), indexed to 2017 using the IHS Markit North American Power Capital Costs Index (NAPCCI) excluding nuclear plants (IHS 2018). The ORC power plant cost data have been updated with data from the 2016 GETEM tool (DOE 2016) and the geothermal binary power plants study by Verkis (2014). Figure 4 in the Theoretical Basis shows the power plant capital cost expressed in $ kWe-1 as a function of plant size and initial production temperature for subcritical ORC and double-flash power plants. The default correlations in GEOPHIRES include 15% contingency plus 12% indirect costs. For the same plant size and production temperature, double-flash power plants are considered about 25% more expensive than single-flash power plants (Zeyghami 2010), and supercritical ORC plants are roughly 10% more than subcritical ORC plants (Astolfi et al. 2014). A wide range in power plant specific cost values is reported in academic and popular literature. The GEOPHIRES built-in surface plant cost correlations represent typical values. However, the user is recommended to provide their own power plant cost data if available for their case study. The ORC plant specific cost decreases only moderately at higher temperatures. The reasons are that when increasing the temperature, the ORC plant design also changes: (1) a different organic fluid is selected, (2) piping, pump, heat exchangers, and other equipment are designed to handle the higher temperature (and potentially also pressure), requiring thicker walls, potentially different materials, etc., and (3) additional components may be implemented, such as a heat recuperator, making the design and operation more complex. Unlike flash power plants, ORC plants are a small, niche market, typically case specific, and rely on relatively young technology, which has not been subject yet to decades of technological advancement. The cost for direct-use heat applications is highly dependent on the type of application. A generic cost of $250 kWth-1 is assumed plus 15% contingency plus 12% indirect costs. However, users are encouraged to provide their own cost figures for their specific application. Beckers and Young (2017) collected several cost figures to estimate the surface equipment cost for geothermal district-heating systems.", + "units": "MUSD" + }, "of which Absorption Chiller Cost": {}, "of which Heat Pump Cost": {}, "of which Peaking Boiler Cost": {}, @@ -420,13 +428,13 @@ }, "Field gathering system costs": { "type": "number", - "description": "Field gathering system cost", + "description": "Field gathering system cost. The built-in cost correlation for estimating the field gathering system cost includes the cost for surface piping from each well to the plant and pumps for production and injection wells. The length of the surface piping is assumed 750 m per well at a cost of $500 per meter. The pumping cost for each pump in the production wells (line-shaft pumps) and a single pump for the injection wells is calculated with the same correlation as GETEM. Contingency (default: 15%). and indirect costs (default: 12%) are added. The built-in cost correlation does not include the cost of pipelines to an off-site heat user or a district-heating system. These costs are estimated at $750 per meter pipeline length and can be manually added by the user to the pipeline distribution costs.", "units": "MUSD" }, "Total surface equipment costs": {}, "Exploration costs": { "type": "number", - "description": "Exploration cost. Default correlation: 60% of the cost of one production well plus 15% contingency plus 12% indirect costs. Provide Exploration Capital Cost Adjustment Factor to multiply the default correlation. Provide Exploration Capital Cost to override the default correlation and set your own cost.", + "description": "Exploration cost. The built-in exploration cost correlation considers drilling of a slim-hole well at 60% of the cost of a regular well, $1M for geophysical and field work, plus 15% contingency plus 12% indirect costs. Provide Exploration Capital Cost Adjustment Factor to multiply the default correlation. Provide Exploration Capital Cost to override the default correlation and set your own cost.", "units": "MUSD" }, "Investment Tax Credit": { @@ -468,17 +476,17 @@ "properties": { "Wellfield maintenance costs": { "type": "number", - "description": "O&M Wellfield cost", + "description": "O&M Wellfield cost. The built-in correlation for the wellfield O&M costs is similar as the surface plant O&M costs: it assumes that it consists of 1% of the total wellfield plus field gathering system costs (for annual non-labor costs) and 25% of the labor costs (the other 75% of the labor costs are assigned to the surface plant O&M costs).", "units": "MUSD/yr" }, "Power plant maintenance costs": { "type": "number", - "description": "O&M Surface Plant costs", + "description": "O&M Surface Plant costs. GEOPHIRES estimates the annual surface plant O&M costs as the sum of 1.5% of the total plant capital cost (for annual non-labor costs), and 75% of the annual labor costs. The other 25% of the labor costs are assigned to the wellfield O&M cost. The labor costs are calculated internally in GEOPHIRES using the 2014 labor costs provided by Beckers (2016), indexed to 2017 using the Bureau of Labor Statistics (BLS) Employment Cost Index for utilities (2018). The original 2014 labor cost correlation expresses the labor costs as a function of the plant size (MW) using an approximate logarithmic curve fit to the built-in labor cost data in GETEM.", "units": "MUSD/yr" }, "Water costs": { "type": "number", - "description": "O&M Make-up Water costs. Assumes $3.5/1,000 gallons of water", + "description": "O&M Make-up Water costs. Default correlation: Assumes $3.50/1,000 gallons of water. Provide Water Cost Adjustment Factor to multiply the default correlation.", "units": "MUSD/yr" }, "Average Reservoir Pumping Cost": {}, @@ -515,7 +523,7 @@ "Total average annual O&M costs": {}, "Total operating and maintenance costs": { "type": "number", - "description": "Total O&M Cost", + "description": "Total O&M Cost. GEOPHIRES estimates the annual O&M costs as the sum of the annual surface plant, wellfield, make-up water, and pumping O&M costs.", "units": "MUSD/yr" }, "OPEX": {} diff --git a/src/geophires_x_schema_generator/main.py b/src/geophires_x_schema_generator/main.py index 7eacc944..fe7b1fee 100644 --- a/src/geophires_x_schema_generator/main.py +++ b/src/geophires_x_schema_generator/main.py @@ -22,14 +22,17 @@ def build(json_file_name_prefix: str, generator: GeophiresXSchemaGenerator, rst_ request_schema_json, result_schema_json = generator.generate_json_schema() request_build_path = Path(build_dir, f'{json_file_name_prefix}request.json') - with open(request_build_path, 'w') as f: - f.write(json.dumps(request_schema_json, indent=2)) + with open(request_build_path, 'w', encoding='utf-8') as f: + + print(json.dumps(request_schema_json, indent=2), file=f) + # using print([...], file=f) instead of f.write avoids need for pre-commit end of file fix + print(f'Wrote request JSON schema file to {request_build_path}.') if result_schema_json is not None: result_build_path = Path(build_dir, f'{json_file_name_prefix}result.json') - with open(result_build_path, 'w') as f: - f.write(json.dumps(result_schema_json, indent=2)) + with open(result_build_path, 'w', encoding='utf-8') as f: + print(json.dumps(result_schema_json, indent=2), file=f) print(f'Wrote result JSON schema file to {result_build_path}.') rst = generator.generate_parameters_reference_rst()