Skip to content

Commit 0d0ebeb

Browse files
Merge pull request NREL#177 from malcolm-dsider/main
Adding rich HTML output for All of GEOPHIRES.
2 parents 42e1953 + b3bb51a commit 0d0ebeb

34 files changed

+2974
-758
lines changed

src/geophires_monte_carlo/Examples/MC_HIP_Settings_file.txt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ INPUT, Reservoir Area, uniform, 50.0, 120.0
33
INPUT, Reservoir Thickness, uniform, 0.122, 0.299
44
INPUT, Reservoir Temperature, uniform, 130, 170
55
INPUT, Rejection Temperature, uniform, 20, 33
6-
OUTPUT, Available Heat (fluid)
7-
OUTPUT, Producible Heat (fluid)
8-
OUTPUT, Producible Heat/Unit Area (fluid)
9-
OUTPUT, Producible Electricity (fluid)
10-
OUTPUT, Producible Electricity/Unit Area (fluid)
11-
ITERATIONS, 250
6+
OUTPUT, Producible Heat (reservoir)
7+
OUTPUT, Producible Heat/Unit Area (reservoir)
8+
OUTPUT, Producible Heat/Unit Volume (reservoir)
9+
OUTPUT, Producible Electricity (reservoir)
10+
OUTPUT, Producible Electricity/Unit Area (reservoir)
11+
OUTPUT, Producible Electricity/Unit Volume (reservoir)
12+
ITERATIONS, 25
1213
MC_OUTPUT_FILE, MC_HIP_Result.txt

src/geophires_monte_carlo/MC_GeoPHIRES3.py

Lines changed: 3 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
from rich.table import Table
2727

2828
from geophires_monte_carlo.common import _get_logger
29-
from geophires_x.Parameter import Parameter
29+
from geophires_x.GeoPHIRESUtils import InsertImagesIntoHTML
30+
from geophires_x.GeoPHIRESUtils import render_default
3031
from geophires_x_client import GeophiresInputParameters
3132
from geophires_x_client import GeophiresXClient
3233
from geophires_x_client import GeophiresXResult
@@ -125,95 +126,7 @@ def Write_HTML_Output(
125126
console.print(statistics_table)
126127
console.save_html(html_path)
127128

128-
# Write a reference to the image(s) into the HTML file by inserting before the "</body>" tag
129-
# build the string to be inserted first
130-
insert_string = ''
131-
for _ in range(len(full_names)):
132-
name_to_use = short_names.pop()
133-
insert_string = insert_string + f'<img src="{name_to_use}.png" alt="{name_to_use}">\n'
134-
135-
match_string = '</body>'
136-
with open(html_path, 'r+', encoding='UTF-8') as html_file:
137-
contents = html_file.readlines()
138-
if match_string in contents[-1]: # Handle last line to prevent IndexError
139-
pass
140-
else:
141-
for index, line in enumerate(contents):
142-
if match_string in line and insert_string not in contents[index + 1]:
143-
contents.insert(index, insert_string)
144-
break
145-
html_file.seek(0)
146-
html_file.writelines(contents)
147-
148-
149-
def UpgradeSymbologyOfUnits(unit: str) -> str:
150-
"""
151-
UpgradeSymbologyOfUnits is a function that takes a string that represents a unit and replaces the **2 and **3
152-
with the appropriate unicode characters for superscript 2 and 3, and replaces "deg" with the unicode character
153-
for degrees.
154-
:param unit: a string that represents a unit
155-
:return: a string that represents a unit with the appropriate unicode characters for superscript 2 and 3, and
156-
replaces "deg" with the unicode character for degrees.
157-
"""
158-
return unit.replace('**2', '\u00b2').replace('**3', '\u00b3').replace('deg', '\u00b0')
159-
160-
161-
def render_default(p: float, unit: str = '') -> str:
162-
"""
163-
RenderDefault - render a float as a string with 2 decimal places, or in scientific notation if it is greater than
164-
10,000 with the unit appended to it if it is not an empty string (the default)
165-
:param p: the float to render
166-
:type p: float
167-
:param unit: the unit to append to the string
168-
:type unit: str
169-
:return: the string representation of the float
170-
:rtype: str
171-
"""
172-
unit = UpgradeSymbologyOfUnits(unit)
173-
# if the number is greater than 10,000, render it in scientific notation
174-
if p > 10_000:
175-
return f'{p:10.2e} {unit}'.strip()
176-
# otherwise, render it with 2 decimal places
177-
else:
178-
return f'{p:10.2f} {unit}'.strip()
179-
180-
181-
def render_scientific(p: float, unit: str = '') -> str:
182-
"""
183-
RenderScientific - render a float as a string in scientific notation with 2 decimal places
184-
and the unit appended to it if it is not an empty string (the default)
185-
:param p: the float to render
186-
:type p: float
187-
:param unit: the unit to append to the string
188-
:type unit: str
189-
:return: the string representation of the float
190-
:rtype: str
191-
"""
192-
unit = UpgradeSymbologyOfUnits(unit)
193-
return f'{p:10.2e} {unit}'.strip()
194-
195-
196-
def render_Parameter_default(p: Parameter) -> str:
197-
"""
198-
RenderDefault - render a float as a string with 2 decimal places, or in scientific notation if it is greater than
199-
10,000 with the unit appended to it if it is not an empty string (the default) by calling the render_default base
200-
function
201-
:param p: the parameter to render
202-
:type p: float
203-
:return: the string representation of the float
204-
"""
205-
return render_default(p.value, p.CurrentUnits.value)
206-
207-
208-
def render_parameter_scientific(p: Parameter) -> str:
209-
"""
210-
RenderScientific - render a float as a string in scientific notation with 2 decimal places
211-
and the unit appended to it if it is not an empty string (the default) by calling the render_scientific base function
212-
:param p: the parameter to render
213-
:type p: float
214-
:return: the string representation of the float
215-
"""
216-
return render_scientific(p.value, p.CurrentUnits.value)
129+
InsertImagesIntoHTML(html_path, full_names, short_names)
217130

218131

219132
def check_and_replace_mean(input_value, args) -> list:

src/geophires_x/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,4 @@ Examples\Test2.json
133133
/References/Muffler and Cataldi Methods for regional assessment of geothermal resources.pdf
134134
/Preliminary_Corpus Christi GRA_093019.xlsx
135135
/temperature.txt
136+
all_messages_conf.log

src/geophires_x/Economics.py

Lines changed: 142 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,38 +9,65 @@
99
from geophires_x.Units import *
1010

1111

12-
def BuildPricingModel(plantlifetime: int, StartYear: int, StartPrice: float, EndPrice: float,
13-
EscalationStart: int, EscalationRate: float):
12+
def BuildPTCModel(plantlifetime: int, duration: int, ptc_price: float,
13+
ptc_inflation_adjusted: bool, inflation_rate: float) -> list:
14+
"""
15+
BuildPricingModel builds the price model array for the project lifetime. It is used to calculate the revenue
16+
stream for the project.
17+
:param plantlifetime: The lifetime of the project in years
18+
:type plantlifetime: int
19+
:param duration: The duration of the PTC in years
20+
:type duration: int
21+
:param ptc_price: The PTC in $/kWh
22+
:type ptc_price: float
23+
:param ptc_inflation_adjusted: Is the PTC is inflation?
24+
:type ptc_inflation_adjusted: bool
25+
:param inflation_rate: The inflation rate in %
26+
:type inflation_rate: float
27+
:return: Price: The price model array for the PTC in $/kWh
28+
:rtype: list
29+
"""
30+
# Build the PTC price model by setting the price to the PTCPrice for the duration of the PTC
31+
Price = [0.0] * plantlifetime
32+
for year in range(0, duration, 1):
33+
Price[year] = ptc_price
34+
if ptc_inflation_adjusted and year > 0:
35+
Price[year] = Price[year-1] * (1 + inflation_rate)
36+
return Price
37+
38+
39+
def BuildPricingModel(plantlifetime: int, StartPrice: float, EndPrice: float,
40+
EscalationStartYear: int, EscalationRate: float, PTCAddition: list) -> list:
1441
"""
1542
BuildPricingModel builds the price model array for the project lifetime. It is used to calculate the revenue
1643
stream for the project.
1744
:param plantlifetime: The lifetime of the project in years
1845
:type plantlifetime: int
19-
:param StartYear: The year the project starts in years (not including construction years)
20-
:type StartYear: int
2146
:param StartPrice: The price in the first year of the project in $/kWh
2247
:type StartPrice: float
2348
:param EndPrice: The price in the last year of the project in $/kWh
2449
:type EndPrice: float
25-
:param EscalationStart: The year the price escalation starts in years (not including construction years) in years
26-
:type EscalationStart: int
50+
:param EscalationStartYear: The year the price escalation starts in years (not including construction years) in years
51+
:type EscalationStartYear: int
2752
:param EscalationRate: The rate of price escalation in $/kWh/year
2853
:type EscalationRate: float
54+
:param PTCAddition: The PTC addition array for the project in $/kWh
55+
:type PTCAddition: list
2956
:return: Price: The price model array for the project in $/kWh
3057
:rtype: list
3158
"""
32-
Price = [StartPrice] * plantlifetime
33-
if StartPrice == EndPrice:
34-
return Price
35-
for i in range(StartYear, plantlifetime, 1):
36-
if i >= EscalationStart:
37-
Price[i] = Price[i] + ((i - EscalationStart) * EscalationRate)
59+
Price = [0.0] * plantlifetime
60+
for i in range(0, plantlifetime, 1):
61+
Price[i] = StartPrice
62+
if i >= EscalationStartYear:
63+
Price[i] = Price[i] + ((i - EscalationStartYear) * EscalationRate)
3864
if Price[i] > EndPrice:
3965
Price[i] = EndPrice
66+
Price[i] = Price[i] + PTCAddition[i]
4067
return Price
4168

4269

43-
def CalculateTotalRevenue(plantlifetime: int, ConstructionYears: int, CAPEX: float, OPEX: float, AnnualRev, CummRev):
70+
def CalculateTotalRevenue(plantlifetime: int, ConstructionYears: int, CAPEX: float, OPEX: float, AnnualRev):
4471
"""
4572
CalculateRevenue calculates the revenue stream for the project. It is used to calculate the revenue
4673
stream for the project.
@@ -52,10 +79,8 @@ def CalculateTotalRevenue(plantlifetime: int, ConstructionYears: int, CAPEX: flo
5279
:type CAPEX: float
5380
:param OPEX: The total annual operating cost of the project in MUSD
5481
:type OPEX: float
55-
:param Energy: The energy production array for the project in kWh
56-
:type Energy: list
57-
:param Price: The price model array for the project in $/kWh
58-
:type Price: list
82+
:param AnnualRev: The annual revenue array for the project in MUSD
83+
:type AnnualRev: list
5984
:return: CashFlow: The annual cash flow for the project in MUSD and CummCashFlow: The cumulative cash flow for the
6085
project in MUSD
6186
:rtype: list
@@ -307,6 +332,7 @@ def CalculateLCOELCOHLCOC(self, model: Model) -> tuple:
307332
NPVfc = np.sum((1 + self.inflrateconstruction.value) * self.CCap.value * self.PTR.value * inflationvector * discountvector)
308333
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)
309334
NPVitc = (1 + self.inflrateconstruction.value) * self.CCap.value * self.RITC.value / (1 - self.CTR.value)
335+
310336
if model.surfaceplant.enduse_option.value == EndUseOptions.ELECTRICITY:
311337
NPVoandm = np.sum(self.Coam.value * inflationvector * discountvector)
312338
NPVgrt = self.GTR.value / (1 - self.GTR.value) * (NPVcap + NPVoandm + NPVfc + NPVit - NPVitc)
@@ -1223,6 +1249,57 @@ def __init__(self, model: Model):
12231249
ErrMessage="assume calculation for CHP Electrical Plant Cost Allocation Ratio (cost electrical plant/total CAPEX)",
12241250
ToolTipText="CHP Electrical Plant Cost Allocation Ratio (cost electrical plant/total CAPEX)"
12251251
)
1252+
self.PTCElec = self.ParameterDict[self.PTCElec.Name] = floatParameter(
1253+
"Production Tax Credit Electricity",
1254+
DefaultValue=0.04,
1255+
Min=0.0,
1256+
Max=10.0,
1257+
UnitType=Units.ENERGYCOST,
1258+
PreferredUnits=EnergyCostUnit.DOLLARSPERKWH,
1259+
CurrentUnits=EnergyCostUnit.DOLLARSPERKWH,
1260+
ErrMessage="assume default for Production Tax Credit Electricity ($0.04/kWh)",
1261+
ToolTipText="Production tax credit for electricity in $/kWh"
1262+
)
1263+
self.PTCHeat = self.ParameterDict[self.PTCHeat.Name] = floatParameter(
1264+
"Production Tax Credit Heat",
1265+
DefaultValue=0.0,
1266+
Min=0.0,
1267+
Max=100.0,
1268+
UnitType=Units.ENERGYCOST,
1269+
PreferredUnits=EnergyCostUnit.DOLLARSPERMMBTU,
1270+
CurrentUnits=EnergyCostUnit.DOLLARSPERMMBTU,
1271+
ErrMessage="assume default for Production Tax Credit Heat ($0.0/MMBTU)",
1272+
ToolTipText="Production tax credit for heat in $/MMBTU"
1273+
)
1274+
self.PTCCooling = self.ParameterDict[self.PTCCooling.Name] = floatParameter(
1275+
"Production Tax Credit Cooling",
1276+
DefaultValue=0.0,
1277+
Min=0.0,
1278+
Max=100.0,
1279+
UnitType=Units.ENERGYCOST,
1280+
PreferredUnits=EnergyCostUnit.DOLLARSPERMMBTU,
1281+
CurrentUnits=EnergyCostUnit.DOLLARSPERMMBTU,
1282+
ErrMessage="assume default for Production Tax Credit Cooling ($0.0/MMBTU)",
1283+
ToolTipText="Production tax credit for cooling in $/MMBTU"
1284+
)
1285+
self.PTCDuration = self.ParameterDict[self.PTCDuration.Name] = intParameter(
1286+
"Production Tax Credit Duration",
1287+
DefaultValue=10,
1288+
AllowableRange=list(range(0, 100, 1)),
1289+
UnitType=Units.TIME,
1290+
PreferredUnits=TimeUnit.YEAR,
1291+
CurrentUnits=TimeUnit.YEAR,
1292+
ErrMessage="assume default for Production Tax Credit Duration (10 years)",
1293+
ToolTipText="Production tax credit for duration in years"
1294+
)
1295+
self.PTCInflationAdjusted = self.ParameterDict[self.PTCInflationAdjusted.Name] = boolParameter(
1296+
"Production Tax Credit Inflation Adjusted",
1297+
DefaultValue=False,
1298+
UnitType=Units.NONE,
1299+
Required=False,
1300+
ErrMessage="assume default for Production Tax Credit Inflation Adjusted (False)",
1301+
ToolTipText="Production tax credit inflation adjusted"
1302+
)
12261303

12271304
# local variable initialization
12281305
self.CAPEX_cost_electricity_plant = 0.0
@@ -1512,6 +1589,12 @@ def __init__(self, model: Model):
15121589
PreferredUnits=TimeUnit.YEAR,
15131590
CurrentUnits=TimeUnit.YEAR
15141591
)
1592+
self.RITCValue = self.OutputParameterDict[self.RITCValue.Name] = OutputParameter(
1593+
Name="Investment Tax Credit Value",
1594+
UnitType=Units.CURRENCY,
1595+
PreferredUnits=CurrencyUnit.MDOLLARS,
1596+
CurrentUnits=CurrencyUnit.MDOLLARS
1597+
)
15151598

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

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

2387+
# update the capitol costs, assuming the entire ITC is used to reduce the capitol costs
2388+
if self.RITC.Provided:
2389+
self.RITCValue.value = self.RITC.value * self.CCap.value
2390+
self.CCap.value = self.CCap.value - self.RITCValue.value
2391+
23042392
# Add in the FlatLicenseEtc, OtherIncentives, & TotalGrant
23052393
self.CCap.value = self.CCap.value + self.FlatLicenseEtc.value - self.OtherIncentives.value - self.TotalGrant.value
23062394

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

2501+
# build the PTC price models
2502+
self.PTCElecPrice = [0.0] * model.surfaceplant.plant_lifetime.value
2503+
self.PTCHeatPrice = [0.0] * model.surfaceplant.plant_lifetime.value
2504+
self.PTCCoolingPrice = [0.0] * model.surfaceplant.plant_lifetime.value
2505+
self.PTCCarbonPrice = [0.0] * model.surfaceplant.plant_lifetime.value
2506+
if self.PTCElec.Provided:
2507+
self.PTCElecPrice = BuildPTCModel(model.surfaceplant.plant_lifetime.value,
2508+
self.PTCDuration.value, self.PTCElec.value, self.PTCInflationAdjusted.value,
2509+
self.RINFL.value)
2510+
if self.PTCHeat.Provided:
2511+
self.PTCHeatPrice = BuildPTCModel(model.surfaceplant.plant_lifetime.value,
2512+
self.PTCDuration.value, self.PTCHeat.value, self.PTCInflationAdjusted.value,
2513+
self.RINFL.value)
2514+
if self.PTCCooling.Provided:
2515+
self.PTCCoolingPrice = BuildPTCModel(model.surfaceplant.plant_lifetime.value,
2516+
self.PTCDuration.value,self.PTCCooling.value, self.PTCInflationAdjusted.value,
2517+
self.RINFL.value)
2518+
24132519
# build the price models
2414-
self.ElecPrice.value = BuildPricingModel(model.surfaceplant.plant_lifetime.value, 0,
2520+
self.ElecPrice.value = BuildPricingModel(model.surfaceplant.plant_lifetime.value,
24152521
self.ElecStartPrice.value, self.ElecEndPrice.value,
2416-
self.ElecEscalationStart.value, self.ElecEscalationRate.value)
2417-
self.HeatPrice.value = BuildPricingModel(model.surfaceplant.plant_lifetime.value, 0,
2522+
self.ElecEscalationStart.value, self.ElecEscalationRate.value,
2523+
self.PTCElecPrice)
2524+
self.HeatPrice.value = BuildPricingModel(model.surfaceplant.plant_lifetime.value,
24182525
self.HeatStartPrice.value, self.HeatEndPrice.value,
2419-
self.HeatEscalationStart.value, self.HeatEscalationRate.value)
2420-
self.CoolingPrice.value = BuildPricingModel(model.surfaceplant.plant_lifetime.value, 0,
2526+
self.HeatEscalationStart.value, self.HeatEscalationRate.value,
2527+
self.PTCHeatPrice)
2528+
self.CoolingPrice.value = BuildPricingModel(model.surfaceplant.plant_lifetime.value,
24212529
self.CoolingStartPrice.value, self.CoolingEndPrice.value,
2422-
self.CoolingEscalationStart.value, self.CoolingEscalationRate.value)
2423-
self.CarbonPrice.value = BuildPricingModel(model.surfaceplant.plant_lifetime.value, self.CarbonEscalationStart.value,
2530+
self.CoolingEscalationStart.value, self.CoolingEscalationRate.value,
2531+
self.PTCCoolingPrice)
2532+
self.CarbonPrice.value = BuildPricingModel(model.surfaceplant.plant_lifetime.value,
24242533
self.CarbonStartPrice.value, self.CarbonEndPrice.value,
2425-
self.CarbonEscalationStart.value, self.CarbonEscalationRate.value)
2534+
self.CarbonEscalationStart.value, self.CarbonEscalationRate.value,
2535+
self.PTCCarbonPrice)
24262536

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

2605+
# for the sake of display, insert zeros at the beginning of the pricing arrays
2606+
for i in range(0, model.surfaceplant.construction_years.value, 1):
2607+
self.ElecPrice.value.insert(0, 0.0)
2608+
self.HeatPrice.value.insert(0, 0.0)
2609+
self.CoolingPrice.value.insert(0, 0.0)
2610+
self.CarbonPrice.value.insert(0, 0.0)
2611+
24952612
# Insert the cost of construction into the front of the array that will be used to calculate NPV
24962613
# the convention is that the upfront CAPEX is negative
24972614
# This is the same for all projects

src/geophires_x/GEOPHIRESv3.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ def main(enable_geophires_logging_config=True):
4141

4242
# write the outputs as JSON
4343
import jsons, json
44+
4445
jsons.suppress_warnings(True)
4546
json_resrv = jsons.dumps(model.reserv.OutputParameterDict, indent=4, sort_keys=True, supress_warnings=True)
4647
json_wells = jsons.dumps(model.wellbores.OutputParameterDict, indent=4, sort_keys=True, supress_warnings=True)
@@ -80,10 +81,6 @@ def main(enable_geophires_logging_config=True):
8081
for line in content:
8182
sys.stdout.write(line)
8283

83-
# make district heating plot
84-
if model.surfaceplant.plant_type.value == OptionList.PlantType.DISTRICT_HEATING:
85-
model.outputs.MakeDistrictHeatingPlot(model)
86-
8784
logger.info(f'Complete {str(__name__)}: {sys._getframe().f_code.co_name}')
8885

8986

0 commit comments

Comments
 (0)