Skip to content

Commit

Permalink
Merge pull request NREL#177 from malcolm-dsider/main
Browse files Browse the repository at this point in the history
Adding rich HTML output for All of GEOPHIRES.
  • Loading branch information
softwareengineerprogrammer authored Apr 12, 2024
2 parents 42e1953 + b3bb51a commit 0d0ebeb
Show file tree
Hide file tree
Showing 34 changed files with 2,974 additions and 758 deletions.
13 changes: 7 additions & 6 deletions src/geophires_monte_carlo/Examples/MC_HIP_Settings_file.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ 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, 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
OUTPUT, Producible Heat (reservoir)
OUTPUT, Producible Heat/Unit Area (reservoir)
OUTPUT, Producible Heat/Unit Volume (reservoir)
OUTPUT, Producible Electricity (reservoir)
OUTPUT, Producible Electricity/Unit Area (reservoir)
OUTPUT, Producible Electricity/Unit Volume (reservoir)
ITERATIONS, 25
MC_OUTPUT_FILE, MC_HIP_Result.txt
93 changes: 3 additions & 90 deletions src/geophires_monte_carlo/MC_GeoPHIRES3.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
from rich.table import Table

from geophires_monte_carlo.common import _get_logger
from geophires_x.Parameter import Parameter
from geophires_x.GeoPHIRESUtils import InsertImagesIntoHTML
from geophires_x.GeoPHIRESUtils import render_default
from geophires_x_client import GeophiresInputParameters
from geophires_x_client import GeophiresXClient
from geophires_x_client import GeophiresXResult
Expand Down Expand Up @@ -125,95 +126,7 @@ def Write_HTML_Output(
console.print(statistics_table)
console.save_html(html_path)

# Write a reference to the image(s) into the HTML file by inserting before the "</body>" tag
# build the string to be inserted first
insert_string = ''
for _ in range(len(full_names)):
name_to_use = short_names.pop()
insert_string = insert_string + f'<img src="{name_to_use}.png" alt="{name_to_use}">\n'

match_string = '</body>'
with open(html_path, 'r+', encoding='UTF-8') as html_file:
contents = html_file.readlines()
if match_string in contents[-1]: # Handle last line to prevent IndexError
pass
else:
for index, line in enumerate(contents):
if match_string in line and insert_string not in contents[index + 1]:
contents.insert(index, insert_string)
break
html_file.seek(0)
html_file.writelines(contents)


def UpgradeSymbologyOfUnits(unit: str) -> str:
"""
UpgradeSymbologyOfUnits is a function that takes a string that represents a unit and replaces the **2 and **3
with the appropriate unicode characters for superscript 2 and 3, and replaces "deg" with the unicode character
for degrees.
:param unit: a string that represents a unit
:return: a string that represents a unit with the appropriate unicode characters for superscript 2 and 3, and
replaces "deg" with the unicode character for degrees.
"""
return unit.replace('**2', '\u00b2').replace('**3', '\u00b3').replace('deg', '\u00b0')


def render_default(p: float, unit: str = '') -> str:
"""
RenderDefault - render a float as a string with 2 decimal places, or in scientific notation if it is greater than
10,000 with the unit appended to it if it is not an empty string (the default)
:param p: the float to render
:type p: float
:param unit: the unit to append to the string
:type unit: str
:return: the string representation of the float
:rtype: str
"""
unit = UpgradeSymbologyOfUnits(unit)
# if the number is greater than 10,000, render it in scientific notation
if p > 10_000:
return f'{p:10.2e} {unit}'.strip()
# otherwise, render it with 2 decimal places
else:
return f'{p:10.2f} {unit}'.strip()


def render_scientific(p: float, unit: str = '') -> str:
"""
RenderScientific - render a float as a string in scientific notation with 2 decimal places
and the unit appended to it if it is not an empty string (the default)
:param p: the float to render
:type p: float
:param unit: the unit to append to the string
:type unit: str
:return: the string representation of the float
:rtype: str
"""
unit = UpgradeSymbologyOfUnits(unit)
return f'{p:10.2e} {unit}'.strip()


def render_Parameter_default(p: Parameter) -> str:
"""
RenderDefault - render a float as a string with 2 decimal places, or in scientific notation if it is greater than
10,000 with the unit appended to it if it is not an empty string (the default) by calling the render_default base
function
:param p: the parameter to render
:type p: float
:return: the string representation of the float
"""
return render_default(p.value, p.CurrentUnits.value)


def render_parameter_scientific(p: Parameter) -> str:
"""
RenderScientific - render a float as a string in scientific notation with 2 decimal places
and the unit appended to it if it is not an empty string (the default) by calling the render_scientific base function
:param p: the parameter to render
:type p: float
:return: the string representation of the float
"""
return render_scientific(p.value, p.CurrentUnits.value)
InsertImagesIntoHTML(html_path, full_names, short_names)


def check_and_replace_mean(input_value, args) -> list:
Expand Down
1 change: 1 addition & 0 deletions src/geophires_x/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,4 @@ Examples\Test2.json
/References/Muffler and Cataldi Methods for regional assessment of geothermal resources.pdf
/Preliminary_Corpus Christi GRA_093019.xlsx
/temperature.txt
all_messages_conf.log
167 changes: 142 additions & 25 deletions src/geophires_x/Economics.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,38 +9,65 @@
from geophires_x.Units import *


def BuildPricingModel(plantlifetime: int, StartYear: int, StartPrice: float, EndPrice: float,
EscalationStart: int, EscalationRate: float):
def BuildPTCModel(plantlifetime: int, duration: int, ptc_price: float,
ptc_inflation_adjusted: bool, inflation_rate: float) -> list:
"""
BuildPricingModel builds the price model array for the project lifetime. It is used to calculate the revenue
stream for the project.
:param plantlifetime: The lifetime of the project in years
:type plantlifetime: int
:param duration: The duration of the PTC in years
:type duration: int
:param ptc_price: The PTC in $/kWh
:type ptc_price: float
:param ptc_inflation_adjusted: Is the PTC is inflation?
:type ptc_inflation_adjusted: bool
:param inflation_rate: The inflation rate in %
:type inflation_rate: float
:return: Price: The price model array for the PTC in $/kWh
:rtype: list
"""
# Build the PTC price model by setting the price to the PTCPrice for the duration of the PTC
Price = [0.0] * plantlifetime
for year in range(0, duration, 1):
Price[year] = ptc_price
if ptc_inflation_adjusted and year > 0:
Price[year] = Price[year-1] * (1 + inflation_rate)
return Price


def BuildPricingModel(plantlifetime: int, StartPrice: float, EndPrice: float,
EscalationStartYear: int, EscalationRate: float, PTCAddition: list) -> list:
"""
BuildPricingModel builds the price model array for the project lifetime. It is used to calculate the revenue
stream for the project.
:param plantlifetime: The lifetime of the project in years
:type plantlifetime: int
:param StartYear: The year the project starts in years (not including construction years)
:type StartYear: int
:param StartPrice: The price in the first year of the project in $/kWh
:type StartPrice: float
:param EndPrice: The price in the last year of the project in $/kWh
:type EndPrice: float
:param EscalationStart: The year the price escalation starts in years (not including construction years) in years
:type EscalationStart: int
:param EscalationStartYear: The year the price escalation starts in years (not including construction years) in years
:type EscalationStartYear: int
:param EscalationRate: The rate of price escalation in $/kWh/year
:type EscalationRate: float
:param PTCAddition: The PTC addition array for the project in $/kWh
:type PTCAddition: list
:return: Price: The price model array for the project in $/kWh
:rtype: list
"""
Price = [StartPrice] * plantlifetime
if StartPrice == EndPrice:
return Price
for i in range(StartYear, plantlifetime, 1):
if i >= EscalationStart:
Price[i] = Price[i] + ((i - EscalationStart) * EscalationRate)
Price = [0.0] * plantlifetime
for i in range(0, plantlifetime, 1):
Price[i] = StartPrice
if i >= EscalationStartYear:
Price[i] = Price[i] + ((i - EscalationStartYear) * EscalationRate)
if Price[i] > EndPrice:
Price[i] = EndPrice
Price[i] = Price[i] + PTCAddition[i]
return Price


def CalculateTotalRevenue(plantlifetime: int, ConstructionYears: int, CAPEX: float, OPEX: float, AnnualRev, CummRev):
def CalculateTotalRevenue(plantlifetime: int, ConstructionYears: int, CAPEX: float, OPEX: float, AnnualRev):
"""
CalculateRevenue calculates the revenue stream for the project. It is used to calculate the revenue
stream for the project.
Expand All @@ -52,10 +79,8 @@ def CalculateTotalRevenue(plantlifetime: int, ConstructionYears: int, CAPEX: flo
:type CAPEX: float
:param OPEX: The total annual operating cost of the project in MUSD
:type OPEX: float
:param Energy: The energy production array for the project in kWh
:type Energy: list
:param Price: The price model array for the project in $/kWh
:type Price: list
:param AnnualRev: The annual revenue array for the project in MUSD
:type AnnualRev: list
:return: CashFlow: The annual cash flow for the project in MUSD and CummCashFlow: The cumulative cash flow for the
project in MUSD
:rtype: list
Expand Down Expand Up @@ -307,6 +332,7 @@ def CalculateLCOELCOHLCOC(self, model: Model) -> tuple:
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.plant_lifetime.value) * discountvector)
NPVitc = (1 + self.inflrateconstruction.value) * self.CCap.value * self.RITC.value / (1 - self.CTR.value)

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)
Expand Down Expand Up @@ -1223,6 +1249,57 @@ def __init__(self, model: Model):
ErrMessage="assume calculation for CHP Electrical Plant Cost Allocation Ratio (cost electrical plant/total CAPEX)",
ToolTipText="CHP Electrical Plant Cost Allocation Ratio (cost electrical plant/total CAPEX)"
)
self.PTCElec = self.ParameterDict[self.PTCElec.Name] = floatParameter(
"Production Tax Credit Electricity",
DefaultValue=0.04,
Min=0.0,
Max=10.0,
UnitType=Units.ENERGYCOST,
PreferredUnits=EnergyCostUnit.DOLLARSPERKWH,
CurrentUnits=EnergyCostUnit.DOLLARSPERKWH,
ErrMessage="assume default for Production Tax Credit Electricity ($0.04/kWh)",
ToolTipText="Production tax credit for electricity in $/kWh"
)
self.PTCHeat = self.ParameterDict[self.PTCHeat.Name] = floatParameter(
"Production Tax Credit Heat",
DefaultValue=0.0,
Min=0.0,
Max=100.0,
UnitType=Units.ENERGYCOST,
PreferredUnits=EnergyCostUnit.DOLLARSPERMMBTU,
CurrentUnits=EnergyCostUnit.DOLLARSPERMMBTU,
ErrMessage="assume default for Production Tax Credit Heat ($0.0/MMBTU)",
ToolTipText="Production tax credit for heat in $/MMBTU"
)
self.PTCCooling = self.ParameterDict[self.PTCCooling.Name] = floatParameter(
"Production Tax Credit Cooling",
DefaultValue=0.0,
Min=0.0,
Max=100.0,
UnitType=Units.ENERGYCOST,
PreferredUnits=EnergyCostUnit.DOLLARSPERMMBTU,
CurrentUnits=EnergyCostUnit.DOLLARSPERMMBTU,
ErrMessage="assume default for Production Tax Credit Cooling ($0.0/MMBTU)",
ToolTipText="Production tax credit for cooling in $/MMBTU"
)
self.PTCDuration = self.ParameterDict[self.PTCDuration.Name] = intParameter(
"Production Tax Credit Duration",
DefaultValue=10,
AllowableRange=list(range(0, 100, 1)),
UnitType=Units.TIME,
PreferredUnits=TimeUnit.YEAR,
CurrentUnits=TimeUnit.YEAR,
ErrMessage="assume default for Production Tax Credit Duration (10 years)",
ToolTipText="Production tax credit for duration in years"
)
self.PTCInflationAdjusted = self.ParameterDict[self.PTCInflationAdjusted.Name] = boolParameter(
"Production Tax Credit Inflation Adjusted",
DefaultValue=False,
UnitType=Units.NONE,
Required=False,
ErrMessage="assume default for Production Tax Credit Inflation Adjusted (False)",
ToolTipText="Production tax credit inflation adjusted"
)

# local variable initialization
self.CAPEX_cost_electricity_plant = 0.0
Expand Down Expand Up @@ -1512,6 +1589,12 @@ def __init__(self, model: Model):
PreferredUnits=TimeUnit.YEAR,
CurrentUnits=TimeUnit.YEAR
)
self.RITCValue = self.OutputParameterDict[self.RITCValue.Name] = OutputParameter(
Name="Investment Tax Credit Value",
UnitType=Units.CURRENCY,
PreferredUnits=CurrencyUnit.MDOLLARS,
CurrentUnits=CurrencyUnit.MDOLLARS
)

model.logger.info(f'Complete {__class__!s}: {sys._getframe().f_code.co_name}')

Expand Down Expand Up @@ -2301,6 +2384,11 @@ def Calculate(self, model: Model) -> None:
else:
self.CCap.value = self.totalcapcost.value

# update the capitol costs, assuming the entire ITC is used to reduce the capitol costs
if self.RITC.Provided:
self.RITCValue.value = self.RITC.value * self.CCap.value
self.CCap.value = self.CCap.value - self.RITCValue.value

# Add in the FlatLicenseEtc, OtherIncentives, & TotalGrant
self.CCap.value = self.CCap.value + self.FlatLicenseEtc.value - self.OtherIncentives.value - self.TotalGrant.value

Expand Down Expand Up @@ -2410,19 +2498,41 @@ def Calculate(self, model: Model) -> None:
model.reserv.depth.value = model.reserv.depth.value / 1000.0
model.reserv.depth.CurrentUnits = LengthUnit.KILOMETERS

# build the PTC price models
self.PTCElecPrice = [0.0] * model.surfaceplant.plant_lifetime.value
self.PTCHeatPrice = [0.0] * model.surfaceplant.plant_lifetime.value
self.PTCCoolingPrice = [0.0] * model.surfaceplant.plant_lifetime.value
self.PTCCarbonPrice = [0.0] * model.surfaceplant.plant_lifetime.value
if self.PTCElec.Provided:
self.PTCElecPrice = BuildPTCModel(model.surfaceplant.plant_lifetime.value,
self.PTCDuration.value, self.PTCElec.value, self.PTCInflationAdjusted.value,
self.RINFL.value)
if self.PTCHeat.Provided:
self.PTCHeatPrice = BuildPTCModel(model.surfaceplant.plant_lifetime.value,
self.PTCDuration.value, self.PTCHeat.value, self.PTCInflationAdjusted.value,
self.RINFL.value)
if self.PTCCooling.Provided:
self.PTCCoolingPrice = BuildPTCModel(model.surfaceplant.plant_lifetime.value,
self.PTCDuration.value,self.PTCCooling.value, self.PTCInflationAdjusted.value,
self.RINFL.value)

# build the price models
self.ElecPrice.value = BuildPricingModel(model.surfaceplant.plant_lifetime.value, 0,
self.ElecPrice.value = BuildPricingModel(model.surfaceplant.plant_lifetime.value,
self.ElecStartPrice.value, self.ElecEndPrice.value,
self.ElecEscalationStart.value, self.ElecEscalationRate.value)
self.HeatPrice.value = BuildPricingModel(model.surfaceplant.plant_lifetime.value, 0,
self.ElecEscalationStart.value, self.ElecEscalationRate.value,
self.PTCElecPrice)
self.HeatPrice.value = BuildPricingModel(model.surfaceplant.plant_lifetime.value,
self.HeatStartPrice.value, self.HeatEndPrice.value,
self.HeatEscalationStart.value, self.HeatEscalationRate.value)
self.CoolingPrice.value = BuildPricingModel(model.surfaceplant.plant_lifetime.value, 0,
self.HeatEscalationStart.value, self.HeatEscalationRate.value,
self.PTCHeatPrice)
self.CoolingPrice.value = BuildPricingModel(model.surfaceplant.plant_lifetime.value,
self.CoolingStartPrice.value, self.CoolingEndPrice.value,
self.CoolingEscalationStart.value, self.CoolingEscalationRate.value)
self.CarbonPrice.value = BuildPricingModel(model.surfaceplant.plant_lifetime.value, self.CarbonEscalationStart.value,
self.CoolingEscalationStart.value, self.CoolingEscalationRate.value,
self.PTCCoolingPrice)
self.CarbonPrice.value = BuildPricingModel(model.surfaceplant.plant_lifetime.value,
self.CarbonStartPrice.value, self.CarbonEndPrice.value,
self.CarbonEscalationStart.value, self.CarbonEscalationRate.value)
self.CarbonEscalationStart.value, self.CarbonEscalationRate.value,
self.PTCCarbonPrice)

# do the additional economic calculations first, if needed, so the summaries below work.
if self.DoAddOnCalculations.value:
Expand Down Expand Up @@ -2492,6 +2602,13 @@ def Calculate(self, model: Model) -> None:
self.TotalRevenue.value[i] = self.TotalRevenue.value[i] + self.CarbonRevenue.value[i]
#self.TotalCummRevenue.value[i] = self.TotalCummRevenue.value[i] + self.CarbonCummCashFlow.value[i]

# for the sake of display, insert zeros at the beginning of the pricing arrays
for i in range(0, model.surfaceplant.construction_years.value, 1):
self.ElecPrice.value.insert(0, 0.0)
self.HeatPrice.value.insert(0, 0.0)
self.CoolingPrice.value.insert(0, 0.0)
self.CarbonPrice.value.insert(0, 0.0)

# 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
# This is the same for all projects
Expand Down
5 changes: 1 addition & 4 deletions src/geophires_x/GEOPHIRESv3.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def main(enable_geophires_logging_config=True):

# write the outputs as JSON
import jsons, json

jsons.suppress_warnings(True)
json_resrv = jsons.dumps(model.reserv.OutputParameterDict, indent=4, sort_keys=True, supress_warnings=True)
json_wells = jsons.dumps(model.wellbores.OutputParameterDict, indent=4, sort_keys=True, supress_warnings=True)
Expand Down Expand Up @@ -80,10 +81,6 @@ def main(enable_geophires_logging_config=True):
for line in content:
sys.stdout.write(line)

# make district heating plot
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}')


Expand Down
Loading

0 comments on commit 0d0ebeb

Please sign in to comment.