Skip to content

Commit e09b3a9

Browse files
Implementing PTC and ITC more fully
1 parent 5454b1d commit e09b3a9

File tree

4 files changed

+271
-33
lines changed

4 files changed

+271
-33
lines changed

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
@@ -1237,6 +1314,10 @@ def __init__(self, model: Model):
12371314
self.InputFile = ""
12381315
self.Cplantcorrelation = 0.0
12391316
self.C1well = 0.0
1317+
self.PTCElecPrice = [0.0] * model.surfaceplant.plant_lifetime.value
1318+
self.PTCHeatPrice = [0.0] * model.surfaceplant.plant_lifetime.value
1319+
self.PTCCoolingPrice = [0.0] * model.surfaceplant.plant_lifetime.value
1320+
self.PTCCarbonPrice = [0.0] * model.surfaceplant.plant_lifetime.value
12401321
sclass = str(__class__).replace("<class \'", "")
12411322
self.MyClass = sclass.replace("\'>", "")
12421323
self.MyPath = os.path.abspath(__file__)
@@ -1512,6 +1593,12 @@ def __init__(self, model: Model):
15121593
PreferredUnits=TimeUnit.YEAR,
15131594
CurrentUnits=TimeUnit.YEAR
15141595
)
1596+
self.RITCValue = self.OutputParameterDict[self.RITCValue.Name] = OutputParameter(
1597+
Name="Investment Tax Credit Value",
1598+
UnitType=Units.CURRENCY,
1599+
PreferredUnits=CurrencyUnit.MDOLLARS,
1600+
CurrentUnits=CurrencyUnit.MDOLLARS
1601+
)
15151602

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

@@ -2301,6 +2388,11 @@ def Calculate(self, model: Model) -> None:
23012388
else:
23022389
self.CCap.value = self.totalcapcost.value
23032390

2391+
# update the capitol costs, assuming the entire ITC is used to reduce the capitol costs
2392+
if self.RITC.Provided:
2393+
self.RITCValue.value = self.RITC.value * self.CCap.value
2394+
self.CCap.value = self.CCap.value - self.RITCValue.value
2395+
23042396
# Add in the FlatLicenseEtc, OtherIncentives, & TotalGrant
23052397
self.CCap.value = self.CCap.value + self.FlatLicenseEtc.value - self.OtherIncentives.value - self.TotalGrant.value
23062398

@@ -2410,19 +2502,37 @@ def Calculate(self, model: Model) -> None:
24102502
model.reserv.depth.value = model.reserv.depth.value / 1000.0
24112503
model.reserv.depth.CurrentUnits = LengthUnit.KILOMETERS
24122504

2505+
# build the PTC price models
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: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,31 @@ def main(enable_geophires_logging_config=True):
4141

4242
# write the outputs as JSON
4343
import jsons, json
44+
45+
# import pickle
46+
47+
# Use Pickle to dump the current model in its entirety
48+
# pickle_outputfile = 'HDR.pkl'
49+
# if len(sys.argv) > 2:
50+
# pickle_outputfile = str(sys.argv[2])
51+
# segs = pickle_outputfile.split('.')
52+
# pickle_outputfile = segs[0] + '.pkl'
53+
# with open(pickle_outputfile, 'wb') as f:
54+
# pickle.dump(model, f)
55+
56+
# # Use Pickle to load an example model output
57+
# with open('example1.pkl', 'rb') as f:
58+
# mod1 = pickle.load(f)
59+
60+
# the is redundant, but used here as an example
61+
# with open(pickle_outputfile, 'rb') as f:
62+
# mod1a = pickle.load(f)
63+
64+
# Use recursive_diff to compare the two models
65+
# from recursive_diff import recursive_diff
66+
# for diff in recursive_diff(mod1.reserv, mod1a.reserv, abs_tol=0.0001):
67+
# print(diff)
68+
4469
jsons.suppress_warnings(True)
4570
json_resrv = jsons.dumps(model.reserv.OutputParameterDict, indent=4, sort_keys=True, supress_warnings=True)
4671
json_wells = jsons.dumps(model.wellbores.OutputParameterDict, indent=4, sort_keys=True, supress_warnings=True)

src/geophires_x/Outputs.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1139,6 +1139,9 @@ def PrintOutputs(self, model: Model):
11391139
CAPEX.append(
11401140
OutputTableItem('Stimulation costs (for redrilling)', '{0:10.2f}'.format(model.economics.Cstim.value),
11411141
model.economics.Cstim.CurrentUnits.value))
1142+
if model.economics.RITC.Provided:
1143+
CAPEX.append(OutputTableItem('Investment tax Credit', '{0:10.2f}'.format(-1*model.economics.RITCValue.value),
1144+
model.economics.RITCValue.CurrentUnits.value))
11421145
CAPEX.append(OutputTableItem('Total capital costs', '{0:10.2f}'.format(model.economics.CCap.value),
11431146
model.economics.CCap.CurrentUnits.value))
11441147
if model.economics.econmodel.value == EconomicModel.FCR:
@@ -1468,22 +1471,18 @@ def PrintOutputs(self, model: Model):
14681471
# note that the price arrays need to be extended by the number of construction years. with price = 0
14691472
cashflow[f'Year|:3.0f'] = [i for i in range(1,
14701473
model.surfaceplant.plant_lifetime.value + model.surfaceplant.construction_years.value + 1)]
1471-
longer_version = np.insert(econ.ElecPrice.value, 0, construction_years_zeros)
1472-
cashflow[f'Electricity:Price ({econ.ElecPrice.CurrentUnits.value})|:7.4f'] = longer_version
1474+
cashflow[f'Electricity:Price ({econ.ElecPrice.CurrentUnits.value})|:7.4f'] = econ.ElecPrice.value
14731475
cashflow[f'Electricity:Ann. Rev. ({econ.ElecRevenue.CurrentUnits.value})|:5.2f'] = econ.ElecRevenue.value
14741476
cashflow[
14751477
f'Electricity:Cumm. Rev. ({econ.ElecCummRevenue.CurrentUnits.value})|:5.2f'] = econ.ElecCummRevenue.value
1476-
longer_version = np.insert(econ.HeatPrice.value, 0, construction_years_zeros)
1477-
cashflow[f'Heat:Price ({econ.HeatPrice.CurrentUnits.value})|:7.4f'] = longer_version
1478+
cashflow[f'Heat:Price ({econ.HeatPrice.CurrentUnits.value})|:7.4f'] = econ.HeatPrice.value
14781479
cashflow[f'Heat:Ann. Rev. ({econ.HeatRevenue.CurrentUnits.value})|:5.2f'] = econ.HeatRevenue.value
14791480
cashflow[f'Heat:Cumm. Rev. ({econ.HeatCummRevenue.CurrentUnits.value})|:5.2f'] = econ.HeatCummRevenue.value
1480-
longer_version = np.insert(econ.CoolingPrice.value, 0, construction_years_zeros)
1481-
cashflow[f'Cooling:Price ({econ.CoolingPrice.CurrentUnits.value})|:7.4f'] = longer_version
1481+
cashflow[f'Cooling:Price ({econ.CoolingPrice.CurrentUnits.value})|:7.4f'] = econ.CoolingPrice.value
14821482
cashflow[f'Cooling:Ann. Rev. ({econ.CoolingRevenue.CurrentUnits.value})|:5.2f'] = econ.CoolingRevenue.value
14831483
cashflow[
14841484
f'Cooling:Cumm. Rev. ({econ.CoolingCummRevenue.CurrentUnits.value})|:5.2f'] = econ.CoolingCummRevenue.value
1485-
longer_version = np.insert(econ.CarbonPrice.value, 0, construction_years_zeros)
1486-
cashflow[f'Carbon:Price ({econ.CarbonPrice.CurrentUnits.value})|:7.4f'] = longer_version
1485+
cashflow[f'Carbon:Price ({econ.CarbonPrice.CurrentUnits.value})|:7.4f'] = econ.CarbonPrice.value
14871486
cashflow[f'Carbon:Ann. Rev. ({econ.CarbonRevenue.CurrentUnits.value})|:5.2f'] = econ.CarbonRevenue.value
14881487
cashflow[
14891488
f'Carbon:Cumm. Rev. ({econ.CarbonCummCashFlow.CurrentUnits.value})|:5.2f'] = econ.CarbonCummCashFlow.value
@@ -1750,6 +1749,8 @@ def PrintOutputs(self, model: Model):
17501749
f.write(f' Drilling and completion costs (for redrilling):{model.economics.Cwell.value:10.2f} ' + model.economics.Cwell.CurrentUnits.value + NL)
17511750
f.write(f' Drilling and completion costs per redrilled well: {(model.economics.Cwell.value/(model.wellbores.nprod.value+model.wellbores.ninj.value)):10.2f} ' + model.economics.Cwell.CurrentUnits.value + NL)
17521751
f.write(f' Stimulation costs (for redrilling): {model.economics.Cstim.value:10.2f} ' + model.economics.Cstim.CurrentUnits.value + NL)
1752+
if model.economics.RITCValue.value:
1753+
f.write(f' Investment Tax Credit: {-1*model.economics.RITCValue.value:10.2f} ' + model.economics.RITCValue.CurrentUnits.value + NL)
17531754
f.write(f' Total capital costs: {model.economics.CCap.value:10.2f} ' + model.economics.CCap.CurrentUnits.value + NL)
17541755
if model.economics.econmodel.value == EconomicModel.FCR:
17551756
f.write(f' Annualized capital costs: {(model.economics.CCap.value*(1+model.economics.inflrateconstruction.value)*model.economics.FCR.value):10.2f} ' + model.economics.CCap.CurrentUnits.value + NL)

0 commit comments

Comments
 (0)