Skip to content

Commit 64ff19a

Browse files
WIP - preparation for SAM cash flow table
1 parent eb24b59 commit 64ff19a

File tree

3 files changed

+119
-90
lines changed

3 files changed

+119
-90
lines changed

src/geophires_x/Economics.py

Lines changed: 94 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -2769,94 +2769,7 @@ def Calculate(self, model: Model) -> None:
27692769
if self.DoSDACGTCalculations.value:
27702770
model.sdacgteconomics.Calculate(model)
27712771

2772-
# Calculate cashflow and cumulative cash flow
2773-
total_duration = model.surfaceplant.plant_lifetime.value + model.surfaceplant.construction_years.value
2774-
self.ElecRevenue.value = [0.0] * total_duration
2775-
self.ElecCummRevenue.value = [0.0] * total_duration
2776-
self.HeatRevenue.value = [0.0] * total_duration
2777-
self.HeatCummRevenue.value = [0.0] * total_duration
2778-
self.CoolingRevenue.value = [0.0] * total_duration
2779-
self.CoolingCummRevenue.value = [0.0] * total_duration
2780-
self.CarbonRevenue.value = [0.0] * total_duration
2781-
self.CarbonCummCashFlow.value = [0.0] * total_duration
2782-
self.TotalRevenue.value = [0.0] * total_duration
2783-
self.TotalCummRevenue.value = [0.0] * total_duration
2784-
self.CarbonThatWouldHaveBeenProducedTotal.value = 0.0
2785-
2786-
# Based on the style of the project, calculate the revenue & cumulative revenue
2787-
if model.surfaceplant.enduse_option.value == EndUseOptions.ELECTRICITY:
2788-
self.ElecRevenue.value, self.ElecCummRevenue.value = CalculateRevenue(
2789-
model.surfaceplant.plant_lifetime.value, model.surfaceplant.construction_years.value,
2790-
model.surfaceplant.NetkWhProduced.value, self.ElecPrice.value)
2791-
self.TotalRevenue.value = self.ElecRevenue.value.copy()
2792-
#self.TotalCummRevenue.value = self.ElecCummRevenue.value
2793-
elif model.surfaceplant.enduse_option.value == EndUseOptions.HEAT and model.surfaceplant.plant_type.value not in [PlantType.ABSORPTION_CHILLER]:
2794-
self.HeatRevenue.value, self.HeatCummRevenue.value = CalculateRevenue(
2795-
model.surfaceplant.plant_lifetime.value, model.surfaceplant.construction_years.value,
2796-
model.surfaceplant.HeatkWhProduced.value, self.HeatPrice.value)
2797-
self.TotalRevenue.value = self.HeatRevenue.value.copy()
2798-
#self.TotalCummRevenue.value = self.HeatCummRevenue.value
2799-
elif model.surfaceplant.enduse_option.value == EndUseOptions.HEAT and model.surfaceplant.plant_type.value in [PlantType.ABSORPTION_CHILLER]:
2800-
self.CoolingRevenue.value, self.CoolingCummRevenue.value = CalculateRevenue(
2801-
model.surfaceplant.plant_lifetime.value, model.surfaceplant.construction_years.value,
2802-
model.surfaceplant.cooling_kWh_Produced.value, self.CoolingPrice.value)
2803-
self.TotalRevenue.value = self.CoolingRevenue.value.copy()
2804-
#self.TotalCummRevenue.value = self.CoolingCummRevenue.value
2805-
elif model.surfaceplant.enduse_option.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT,
2806-
EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICITY,
2807-
EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICITY,
2808-
EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT,
2809-
EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT,
2810-
EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICITY]: # co-gen
2811-
# else:
2812-
self.ElecRevenue.value, self.ElecCummRevenue.value = CalculateRevenue(
2813-
model.surfaceplant.plant_lifetime.value, model.surfaceplant.construction_years.value,
2814-
model.surfaceplant.NetkWhProduced.value, self.ElecPrice.value)
2815-
self.HeatRevenue.value, self.HeatCummRevenue.value = CalculateRevenue(
2816-
model.surfaceplant.plant_lifetime.value, model.surfaceplant.construction_years.value,
2817-
model.surfaceplant.HeatkWhProduced.value, self.HeatPrice.value)
2818-
2819-
for i in range(0, model.surfaceplant.plant_lifetime.value + model.surfaceplant.construction_years.value, 1):
2820-
self.TotalRevenue.value[i] = self.ElecRevenue.value[i] + self.HeatRevenue.value[i]
2821-
#if i > 0:
2822-
# self.TotalCummRevenue.value[i] = self.TotalCummRevenue.value[i - 1] + self.TotalRevenue.value[i]
2823-
2824-
if self.DoCarbonCalculations.value:
2825-
self.CarbonRevenue.value, self.CarbonCummCashFlow.value, self.CarbonThatWouldHaveBeenProducedAnnually.value, \
2826-
self.CarbonThatWouldHaveBeenProducedTotal.value = CalculateCarbonRevenue(model,
2827-
model.surfaceplant.plant_lifetime.value, model.surfaceplant.construction_years.value,
2828-
self.CarbonPrice.value, self.GridCO2Intensity.value, self.NaturalGasCO2Intensity.value,
2829-
model.surfaceplant.NetkWhProduced.value, model.surfaceplant.HeatkWhProduced.value)
2830-
for i in range(model.surfaceplant.construction_years.value, model.surfaceplant.plant_lifetime.value + model.surfaceplant.construction_years.value, 1):
2831-
self.TotalRevenue.value[i] = self.TotalRevenue.value[i] + self.CarbonRevenue.value[i]
2832-
#self.TotalCummRevenue.value[i] = self.TotalCummRevenue.value[i] + self.CarbonCummCashFlow.value[i]
2833-
2834-
# for the sake of display, insert zeros at the beginning of the pricing arrays
2835-
for i in range(0, model.surfaceplant.construction_years.value, 1):
2836-
self.ElecPrice.value.insert(0, 0.0)
2837-
self.HeatPrice.value.insert(0, 0.0)
2838-
self.CoolingPrice.value.insert(0, 0.0)
2839-
self.CarbonPrice.value.insert(0, 0.0)
2840-
2841-
# Insert the cost of construction into the front of the array that will be used to calculate NPV
2842-
# the convention is that the upfront CAPEX is negative
2843-
# This is the same for all projects
2844-
ProjectCAPEXPerConstructionYear = self.CCap.value / model.surfaceplant.construction_years.value
2845-
for i in range(0, model.surfaceplant.construction_years.value, 1):
2846-
self.TotalRevenue.value[i] = -1.0 * ProjectCAPEXPerConstructionYear
2847-
self.TotalCummRevenue.value[i] = -1.0 * ProjectCAPEXPerConstructionYear
2848-
# self.TotalRevenue.value, self.TotalCummRevenue.value = CalculateTotalRevenue(
2849-
# model.surfaceplant.plant_lifetime.value, model.surfaceplant.construction_years.value, self.CCap.value,
2850-
# self.Coam.value, self.TotalRevenue.value, self.TotalCummRevenue.value)
2851-
2852-
# Do a one-time calculation that accounts for OPEX - no OPEX in the first year.
2853-
for i in range(model.surfaceplant.construction_years.value,
2854-
model.surfaceplant.plant_lifetime.value + model.surfaceplant.construction_years.value, 1):
2855-
self.TotalRevenue.value[i] = self.TotalRevenue.value[i] - self.Coam.value
2856-
2857-
# Now do a one-time calculation that calculates the cumulative cash flow after everything else has been accounted for
2858-
for i in range(1, model.surfaceplant.plant_lifetime.value + model.surfaceplant.construction_years.value, 1):
2859-
self.TotalCummRevenue.value[i] = self.TotalCummRevenue.value[i-1] + self.TotalRevenue.value[i]
2772+
self.calculate_cashflow(model)
28602773

28612774
# Calculate more financial values using numpy financials
28622775
self.ProjectNPV.value, self.ProjectIRR.value, self.ProjectVIR.value, self.ProjectMOIC.value = \
@@ -2899,6 +2812,99 @@ def Calculate(self, model: Model) -> None:
28992812
self._calculate_derived_outputs(model)
29002813
model.logger.info(f'complete {__class__!s}: {sys._getframe().f_code.co_name}')
29012814

2815+
2816+
def calculate_cashflow(self, model: Model) -> None:
2817+
"""
2818+
Calculate cashflow and cumulative cash flow
2819+
"""
2820+
total_duration = model.surfaceplant.plant_lifetime.value + model.surfaceplant.construction_years.value
2821+
self.ElecRevenue.value = [0.0] * total_duration
2822+
self.ElecCummRevenue.value = [0.0] * total_duration
2823+
self.HeatRevenue.value = [0.0] * total_duration
2824+
self.HeatCummRevenue.value = [0.0] * total_duration
2825+
self.CoolingRevenue.value = [0.0] * total_duration
2826+
self.CoolingCummRevenue.value = [0.0] * total_duration
2827+
self.CarbonRevenue.value = [0.0] * total_duration
2828+
self.CarbonCummCashFlow.value = [0.0] * total_duration
2829+
self.TotalRevenue.value = [0.0] * total_duration
2830+
self.TotalCummRevenue.value = [0.0] * total_duration
2831+
self.CarbonThatWouldHaveBeenProducedTotal.value = 0.0
2832+
2833+
# Based on the style of the project, calculate the revenue & cumulative revenue
2834+
if model.surfaceplant.enduse_option.value == EndUseOptions.ELECTRICITY:
2835+
self.ElecRevenue.value, self.ElecCummRevenue.value = CalculateRevenue(
2836+
model.surfaceplant.plant_lifetime.value, model.surfaceplant.construction_years.value,
2837+
model.surfaceplant.NetkWhProduced.value, self.ElecPrice.value)
2838+
self.TotalRevenue.value = self.ElecRevenue.value.copy()
2839+
#self.TotalCummRevenue.value = self.ElecCummRevenue.value
2840+
elif model.surfaceplant.enduse_option.value == EndUseOptions.HEAT and model.surfaceplant.plant_type.value not in [PlantType.ABSORPTION_CHILLER]:
2841+
self.HeatRevenue.value, self.HeatCummRevenue.value = CalculateRevenue(
2842+
model.surfaceplant.plant_lifetime.value, model.surfaceplant.construction_years.value,
2843+
model.surfaceplant.HeatkWhProduced.value, self.HeatPrice.value)
2844+
self.TotalRevenue.value = self.HeatRevenue.value.copy()
2845+
#self.TotalCummRevenue.value = self.HeatCummRevenue.value
2846+
elif model.surfaceplant.enduse_option.value == EndUseOptions.HEAT and model.surfaceplant.plant_type.value in [PlantType.ABSORPTION_CHILLER]:
2847+
self.CoolingRevenue.value, self.CoolingCummRevenue.value = CalculateRevenue(
2848+
model.surfaceplant.plant_lifetime.value, model.surfaceplant.construction_years.value,
2849+
model.surfaceplant.cooling_kWh_Produced.value, self.CoolingPrice.value)
2850+
self.TotalRevenue.value = self.CoolingRevenue.value.copy()
2851+
#self.TotalCummRevenue.value = self.CoolingCummRevenue.value
2852+
elif model.surfaceplant.enduse_option.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT,
2853+
EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICITY,
2854+
EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICITY,
2855+
EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT,
2856+
EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT,
2857+
EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICITY]: # co-gen
2858+
# else:
2859+
self.ElecRevenue.value, self.ElecCummRevenue.value = CalculateRevenue(
2860+
model.surfaceplant.plant_lifetime.value, model.surfaceplant.construction_years.value,
2861+
model.surfaceplant.NetkWhProduced.value, self.ElecPrice.value)
2862+
self.HeatRevenue.value, self.HeatCummRevenue.value = CalculateRevenue(
2863+
model.surfaceplant.plant_lifetime.value, model.surfaceplant.construction_years.value,
2864+
model.surfaceplant.HeatkWhProduced.value, self.HeatPrice.value)
2865+
2866+
for i in range(0, model.surfaceplant.plant_lifetime.value + model.surfaceplant.construction_years.value, 1):
2867+
self.TotalRevenue.value[i] = self.ElecRevenue.value[i] + self.HeatRevenue.value[i]
2868+
#if i > 0:
2869+
# self.TotalCummRevenue.value[i] = self.TotalCummRevenue.value[i - 1] + self.TotalRevenue.value[i]
2870+
2871+
if self.DoCarbonCalculations.value:
2872+
self.CarbonRevenue.value, self.CarbonCummCashFlow.value, self.CarbonThatWouldHaveBeenProducedAnnually.value, \
2873+
self.CarbonThatWouldHaveBeenProducedTotal.value = CalculateCarbonRevenue(model,
2874+
model.surfaceplant.plant_lifetime.value, model.surfaceplant.construction_years.value,
2875+
self.CarbonPrice.value, self.GridCO2Intensity.value, self.NaturalGasCO2Intensity.value,
2876+
model.surfaceplant.NetkWhProduced.value, model.surfaceplant.HeatkWhProduced.value)
2877+
for i in range(model.surfaceplant.construction_years.value, model.surfaceplant.plant_lifetime.value + model.surfaceplant.construction_years.value, 1):
2878+
self.TotalRevenue.value[i] = self.TotalRevenue.value[i] + self.CarbonRevenue.value[i]
2879+
#self.TotalCummRevenue.value[i] = self.TotalCummRevenue.value[i] + self.CarbonCummCashFlow.value[i]
2880+
2881+
# for the sake of display, insert zeros at the beginning of the pricing arrays
2882+
for i in range(0, model.surfaceplant.construction_years.value, 1):
2883+
self.ElecPrice.value.insert(0, 0.0)
2884+
self.HeatPrice.value.insert(0, 0.0)
2885+
self.CoolingPrice.value.insert(0, 0.0)
2886+
self.CarbonPrice.value.insert(0, 0.0)
2887+
2888+
# Insert the cost of construction into the front of the array that will be used to calculate NPV
2889+
# the convention is that the upfront CAPEX is negative
2890+
# This is the same for all projects
2891+
ProjectCAPEXPerConstructionYear = self.CCap.value / model.surfaceplant.construction_years.value
2892+
for i in range(0, model.surfaceplant.construction_years.value, 1):
2893+
self.TotalRevenue.value[i] = -1.0 * ProjectCAPEXPerConstructionYear
2894+
self.TotalCummRevenue.value[i] = -1.0 * ProjectCAPEXPerConstructionYear
2895+
# self.TotalRevenue.value, self.TotalCummRevenue.value = CalculateTotalRevenue(
2896+
# model.surfaceplant.plant_lifetime.value, model.surfaceplant.construction_years.value, self.CCap.value,
2897+
# self.Coam.value, self.TotalRevenue.value, self.TotalCummRevenue.value)
2898+
2899+
# Do a one-time calculation that accounts for OPEX - no OPEX in the first year.
2900+
for i in range(model.surfaceplant.construction_years.value,
2901+
model.surfaceplant.plant_lifetime.value + model.surfaceplant.construction_years.value, 1):
2902+
self.TotalRevenue.value[i] = self.TotalRevenue.value[i] - self.Coam.value
2903+
2904+
# Now do a one-time calculation that calculates the cumulative cash flow after everything else has been accounted for
2905+
for i in range(1, model.surfaceplant.plant_lifetime.value + model.surfaceplant.construction_years.value, 1):
2906+
self.TotalCummRevenue.value[i] = self.TotalCummRevenue.value[i-1] + self.TotalRevenue.value[i]
2907+
29022908
def _calculate_derived_outputs(self, model: Model) -> None:
29032909
"""
29042910
Subclasses should call _calculate_derived_outputs at the end of their Calculate methods to populate output

src/geophires_x/EconomicsSam.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import json
44
import os
5+
from functools import lru_cache
56
from pathlib import Path
67
from typing import Any
78

@@ -23,6 +24,7 @@
2324
import geophires_x.Model as Model
2425

2526

27+
@lru_cache(maxsize=12)
2628
def calculate_sam_economics(
2729
model: Model
2830
) -> dict[str, dict[str, Any]]:
@@ -59,11 +61,11 @@ def calculate_sam_economics(
5961
# ('Net Output', gt.Outputs.gross_output - gt.Outputs.pump_work, 'MW')
6062
]
6163

62-
max_field_name_len = max(len(x[0]) for x in display_data)
64+
# max_field_name_len = max(len(x[0]) for x in display_data)
6365

6466
ret = {}
6567
for e in display_data:
66-
field_display = e[0] + ':' + ' ' * (max_field_name_len - len(e[0]) - 1)
68+
# field_display = e[0] + ':' + ' ' * (max_field_name_len - len(e[0]) - 1)
6769
# print(f'{field_display}\t{sig_figs(e[1], 5)} {e[2]}')
6870
ret[e[0]] = {'value': _sig_figs(e[1], 5), 'unit': e[2]}
6971

@@ -101,9 +103,29 @@ def _get_single_owner_parameters(model: Model) -> dict[str, Any]:
101103

102104
ret['ppa_price_input'] = [econ.ElecStartPrice.value]
103105

106+
# TODO interest rate
107+
# TODO debt/equity ratio
108+
104109
return ret
105110

106111

112+
@lru_cache(maxsize=12)
113+
def _get_revenue_and_cashflow_profile(model: Model):
114+
"""
115+
ENERGY
116+
Electricity Provided -> cf_energy_sales
117+
118+
REVENUE
119+
Electricity Price -> cf_ppa_price
120+
Electricity Revenue -> cf_energy_value
121+
122+
OPERATING EXPENSES
123+
O&M fixed expense -> cf_om_fixed_expense
124+
125+
PROJECT RETURNS
126+
Net Revenue -> cf_total_revenue
127+
"""
128+
107129
def _get_average_net_generation_MW(model: Model) -> float:
108130
return np.average(model.surfaceplant.NetElectricityProduced.value)
109131

tests/geophires_x_tests/test_economics_sam.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ def _get_result(self, _params) -> GeophiresXResult:
1010
return GeophiresXClient().get_geophires_result(
1111
GeophiresInputParameters(
1212
from_file_path=self._get_test_file_path('generic-egs-case.txt'),
13+
# from_file_path=self._get_test_file_path('../examples/Fervo_Project_Cape-3.txt'), # FIXME TEMP
1314
params={'Economic Model': 5, **_params},
1415
)
1516
)

0 commit comments

Comments
 (0)